Import jacktrip_2.3.0+ds.orig.tar.xz
authorIOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
Fri, 17 May 2024 06:28:06 +0000 (08:28 +0200)
committerIOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
Fri, 17 May 2024 06:28:06 +0000 (08:28 +0200)
[dgit import orig jacktrip_2.3.0+ds.orig.tar.xz]

430 files changed:
.clang-format [new file with mode: 0644]
.clang-format-ignore [new file with mode: 0644]
.clang-tidy [new file with mode: 0644]
.clang-tidy-ignore [new file with mode: 0644]
.dockerignore [new file with mode: 0644]
.mailmap [new file with mode: 0644]
.pre-commit-config.yaml [new file with mode: 0644]
CMakeLists.txt [new file with mode: 0644]
Dockerfile [new file with mode: 0644]
Doxyfile [new file with mode: 0644]
LICENSE.md [new file with mode: 0644]
LICENSES/AVC.txt [new file with mode: 0644]
LICENSES/GPL-3.0.txt [new file with mode: 0644]
LICENSES/LGPL-3.0-only.txt [new file with mode: 0644]
LICENSES/MIT.txt [new file with mode: 0644]
QJackTrip.kdev4 [new file with mode: 0644]
README.md [new file with mode: 0644]
build [new file with mode: 0755]
build-aux/flatpak/org.jacktrip.JackTrip.json [new file with mode: 0644]
build-aux/flatpak/pypi-dependencies.json [new file with mode: 0644]
build-aux/flatpak/requirements.txt [new file with mode: 0644]
build-aux/flatpak/update_pip_deps.sh [new file with mode: 0755]
cross/x86_mingw64.txt [new file with mode: 0644]
cross/x86_mingw64_static.txt [new file with mode: 0644]
docs/About/CHANGELOG.md [new file with mode: 0644]
docs/About/Contributors.md [new file with mode: 0644]
docs/About/License.md [new file with mode: 0644]
docs/About/Resources.md [new file with mode: 0644]
docs/Build/CrossCompile.md [new file with mode: 0644]
docs/Build/Linux.md [new file with mode: 0644]
docs/Build/Mac.md [new file with mode: 0644]
docs/Build/Meson_build.md [new file with mode: 0644]
docs/Build/Windows.md [new file with mode: 0644]
docs/CustomJackServerName.md [new file with mode: 0644]
docs/DevTools/Formatting.md [new file with mode: 0644]
docs/DevTools/StaticAnalysis.md [new file with mode: 0644]
docs/Documentation/MkDocs.md [new file with mode: 0644]
docs/Install.md [new file with mode: 0644]
docs/VirtualStudio.md [new file with mode: 0644]
docs/assets/jacktrip.svg [new file with mode: 0644]
docs/changelog.yml [new file with mode: 0644]
docs/images/jacktrip_hubclient_basic.png [new file with mode: 0644]
docs/images/jacktrip_hubclient_plugins.png [new file with mode: 0644]
docs/images/jacktrip_hubserver_basic.png [new file with mode: 0644]
docs/images/jacktrip_hubserver_jitter.png [new file with mode: 0644]
docs/images/jacktrip_virtual_studio.png [new file with mode: 0644]
docs/index.md [new file with mode: 0644]
docs/requirements.txt [new file with mode: 0644]
documentation/documentation.cpp [new file with mode: 0644]
documentation/footer.html [new file with mode: 0644]
documentation/header.html [new file with mode: 0644]
documentation/img/jack_main_settings.jpg [new file with mode: 0644]
documentation/img/jack_routing.png [new file with mode: 0644]
documentation/img/qjackctl.png [new file with mode: 0644]
externals/Simple-FFT/LICENSE.md [new file with mode: 0644]
externals/Simple-FFT/README.md [new file with mode: 0644]
externals/Simple-FFT/include/simple_fft/check_fft.hpp [new file with mode: 0644]
externals/Simple-FFT/include/simple_fft/copy_array.hpp [new file with mode: 0644]
externals/Simple-FFT/include/simple_fft/error_handling.hpp [new file with mode: 0644]
externals/Simple-FFT/include/simple_fft/fft.h [new file with mode: 0644]
externals/Simple-FFT/include/simple_fft/fft.hpp [new file with mode: 0644]
externals/Simple-FFT/include/simple_fft/fft_impl.hpp [new file with mode: 0644]
externals/Simple-FFT/include/simple_fft/fft_settings.h [new file with mode: 0644]
faust-src/Makefile [new file with mode: 0644]
faust-src/Makefile.compile [new file with mode: 0644]
faust-src/Makefile.headers [new file with mode: 0644]
faust-src/Makefile.ladspacompile [new file with mode: 0644]
faust-src/Makefile.mspcompile [new file with mode: 0644]
faust-src/Makefile.pdcompile [new file with mode: 0644]
faust-src/Makefile.qcompile [new file with mode: 0644]
faust-src/Makefile.qtcompile [new file with mode: 0644]
faust-src/Makefile.sccompile [new file with mode: 0644]
faust-src/Makefile.svg [new file with mode: 0644]
faust-src/Makefile.vstcompile [new file with mode: 0644]
faust-src/README-Limiter.md [new file with mode: 0644]
faust-src/compressor-limiter-test.dsp [new file with mode: 0644]
faust-src/compressordsp.dsp [new file with mode: 0644]
faust-src/faust2header.cpp [new file with mode: 0644]
faust-src/freeverbdsp.dsp [new file with mode: 0644]
faust-src/freeverbmonodsp.dsp [new file with mode: 0644]
faust-src/limiterdsp.dsp [new file with mode: 0644]
faust-src/limitertest.dsp [new file with mode: 0644]
faust-src/meterdsp.dsp [new file with mode: 0644]
faust-src/minimal.cpp [new file with mode: 0644]
faust-src/monitordsp.dsp [new file with mode: 0644]
faust-src/net-ks.dsp [new file with mode: 0644]
faust-src/tests/compressor-limiter-test.dsp [new file with mode: 0644]
faust-src/tests/compressor2dsp.dsp [new file with mode: 0644]
faust-src/tonedsp.dsp [new file with mode: 0644]
faust-src/volumedsp.dsp [new file with mode: 0644]
faust-src/zitarevdsp.dsp [new file with mode: 0644]
faust-src/zitarevmonodsp.dsp [new file with mode: 0644]
jacktrip.pro [new file with mode: 0644]
jacktrip_and_rtaudio.pro [new file with mode: 0644]
linux/README.md [new file with mode: 0644]
linux/add_changelog_to_metainfo.py [new file with mode: 0755]
linux/container/README.md [new file with mode: 0644]
linux/container/jacktrip.service [new file with mode: 0644]
linux/icons/jacktrip-symbolic.svg [new file with mode: 0644]
linux/icons/jacktrip.svg [new file with mode: 0644]
linux/icons/jacktrip_48x48.png [new file with mode: 0644]
linux/icons/jacktrip_48x48_alt.png [new file with mode: 0644]
linux/icons/jacktrip_alt.svg [new file with mode: 0644]
linux/meson.build [new file with mode: 0644]
linux/org.jacktrip.JackTrip.desktop.in [new file with mode: 0644]
linux/org.jacktrip.JackTrip.metainfo.xml.in.in [new file with mode: 0644]
macos/Info_novs.plist [new file with mode: 0644]
macos/JackTrip.app_template/Contents/Info.plist [new file with mode: 0644]
macos/JackTrip.app_template/Contents/PkgInfo [new file with mode: 0644]
macos/JackTrip.app_template/Contents/Resources/jacktrip.icns [new file with mode: 0644]
macos/assemble_app.sh [new file with mode: 0755]
macos/entitlements.plist [new file with mode: 0644]
macos/jacktrip.iconset/icon_128x128.png [new file with mode: 0644]
macos/jacktrip.iconset/icon_128x128@2x.png [new file with mode: 0644]
macos/jacktrip.iconset/icon_16x16.png [new file with mode: 0644]
macos/jacktrip.iconset/icon_16x16@2x.png [new file with mode: 0644]
macos/jacktrip.iconset/icon_256x256.png [new file with mode: 0644]
macos/jacktrip.iconset/icon_256x256@2x.png [new file with mode: 0644]
macos/jacktrip.iconset/icon_32x32.png [new file with mode: 0644]
macos/jacktrip.iconset/icon_32x32@2x.png [new file with mode: 0644]
macos/jacktrip.iconset/icon_512x512.png [new file with mode: 0644]
macos/jacktrip.iconset/icon_512x512@2x.png [new file with mode: 0644]
macos/jacktrip_alt.icns [new file with mode: 0644]
macos/jacktrip_alt.iconset/icon_128x128.png [new file with mode: 0644]
macos/jacktrip_alt.iconset/icon_128x128@2x.png [new file with mode: 0644]
macos/jacktrip_alt.iconset/icon_16x16.png [new file with mode: 0644]
macos/jacktrip_alt.iconset/icon_16x16@2x.png [new file with mode: 0644]
macos/jacktrip_alt.iconset/icon_256x256.png [new file with mode: 0644]
macos/jacktrip_alt.iconset/icon_256x256@2x.png [new file with mode: 0644]
macos/jacktrip_alt.iconset/icon_32x32.png [new file with mode: 0644]
macos/jacktrip_alt.iconset/icon_32x32@2x.png [new file with mode: 0644]
macos/jacktrip_alt.iconset/icon_512x512.png [new file with mode: 0644]
macos/jacktrip_alt.iconset/icon_512x512@2x.png [new file with mode: 0644]
macos/meson_native_minversion_10.13.ini [new file with mode: 0644]
macos/meson_native_minversion_10.14.ini [new file with mode: 0644]
macos/meson_native_universal.ini [new file with mode: 0644]
macos/meson_native_universal_10.13.ini [new file with mode: 0644]
macos/meson_native_universal_10.14.ini [new file with mode: 0644]
macos/package/JackTrip.pkgproj_template [new file with mode: 0644]
macos/package/postinstall.sh [new file with mode: 0755]
macos/sign-stuff.sh [new file with mode: 0755]
meson.build [new file with mode: 0644]
meson_options.txt [new file with mode: 0644]
mkdocs.yml [new file with mode: 0644]
releases/edge/mac-manifests.json [new file with mode: 0644]
releases/edge/win-manifests.json [new file with mode: 0644]
releases/stable/linux-manifests.json [new file with mode: 0644]
releases/stable/mac-manifests.json [new file with mode: 0644]
releases/stable/win-manifests.json [new file with mode: 0644]
rtaudio.pro [new file with mode: 0644]
scripts/README.md [new file with mode: 0644]
scripts/hubMode/art.sh [new file with mode: 0755]
scripts/hubMode/startJacktripHubClient.sh [new file with mode: 0755]
scripts/hubMode/startJacktripHubServer.sh [new file with mode: 0755]
scripts/hubMode/test_hub_mode_server_and_client.sh [new file with mode: 0755]
scripts/slork/die.bin [new file with mode: 0644]
scripts/slork/feedjack-cleanslate.bin [new file with mode: 0644]
scripts/slork/feedjack-client.bin [new file with mode: 0644]
scripts/slork/feedjack-clientconnect.bin [new file with mode: 0644]
scripts/slork/feedjack-launch-n-jacktrip-servers.bin [new file with mode: 0644]
scripts/slork/feedjack-launch-one-jacktrip-client.bin [new file with mode: 0644]
scripts/slork/feedjack-launchjack-even-without-motu.bin [new file with mode: 0644]
scripts/slork/feedjack-launchjack.bin [new file with mode: 0644]
scripts/slork/feedjack-remotespeakerconnect.bin [new file with mode: 0644]
scripts/slork/feedjack-server.bin [new file with mode: 0644]
scripts/slork/feedjack-serverconnect.bin [new file with mode: 0644]
scripts/test/client_connection_test.sh [new file with mode: 0755]
scripts/test/version_test.sh [new file with mode: 0755]
scripts/update_manifests.sh [new file with mode: 0755]
scripts/utility/generate_auth.sh [new file with mode: 0755]
scripts/utility/record_jack_receiving_ports.sh [new file with mode: 0755]
src/Analyzer.cpp [new file with mode: 0644]
src/Analyzer.h [new file with mode: 0644]
src/AudioInterface.cpp [new file with mode: 0644]
src/AudioInterface.h [new file with mode: 0644]
src/AudioTester.cpp [new file with mode: 0644]
src/AudioTester.h [new file with mode: 0644]
src/Auth.cpp [new file with mode: 0644]
src/Auth.h [new file with mode: 0644]
src/Compressor.cpp [new file with mode: 0644]
src/Compressor.h [new file with mode: 0644]
src/CompressorPresets.h [new file with mode: 0644]
src/DataProtocol.cpp [new file with mode: 0644]
src/DataProtocol.h [new file with mode: 0644]
src/Effects.h [new file with mode: 0644]
src/JMess.cpp [new file with mode: 0644]
src/JMess.h [new file with mode: 0644]
src/JTApplication.h [new file with mode: 0644]
src/JackAudioInterface.cpp [new file with mode: 0644]
src/JackAudioInterface.h [new file with mode: 0644]
src/JackTrip.cpp [new file with mode: 0644]
src/JackTrip.h [new file with mode: 0644]
src/JackTripWorker.cpp [new file with mode: 0644]
src/JackTripWorker.h [new file with mode: 0644]
src/JitterBuffer.cpp [new file with mode: 0644]
src/JitterBuffer.h [new file with mode: 0644]
src/Limiter.cpp [new file with mode: 0644]
src/Limiter.h [new file with mode: 0644]
src/LoopBack.cpp [new file with mode: 0644]
src/LoopBack.h [new file with mode: 0644]
src/Meter.cpp [new file with mode: 0644]
src/Meter.h [new file with mode: 0644]
src/Monitor.cpp [new file with mode: 0644]
src/Monitor.h [new file with mode: 0644]
src/PacketHeader.cpp [new file with mode: 0644]
src/PacketHeader.h [new file with mode: 0644]
src/Patcher.cpp [new file with mode: 0644]
src/Patcher.h [new file with mode: 0644]
src/ProcessPlugin.cpp [new file with mode: 0644]
src/ProcessPlugin.h [new file with mode: 0644]
src/QtStaticPlugins.cpp [new file with mode: 0644]
src/Regulator.cpp [new file with mode: 0644]
src/Regulator.h [new file with mode: 0644]
src/Reverb.cpp [new file with mode: 0644]
src/Reverb.h [new file with mode: 0644]
src/RingBuffer.cpp [new file with mode: 0644]
src/RingBuffer.h [new file with mode: 0644]
src/RingBufferWavetable.h [new file with mode: 0644]
src/RtAudioInterface.cpp [new file with mode: 0644]
src/RtAudioInterface.h [new file with mode: 0644]
src/Settings.cpp [new file with mode: 0644]
src/Settings.h [new file with mode: 0644]
src/SslServer.cpp [new file with mode: 0644]
src/SslServer.h [new file with mode: 0644]
src/StereoToMono.cpp [new file with mode: 0644]
src/StereoToMono.h [new file with mode: 0644]
src/Tone.cpp [new file with mode: 0644]
src/Tone.h [new file with mode: 0644]
src/UdpDataProtocol.cpp [new file with mode: 0644]
src/UdpDataProtocol.h [new file with mode: 0644]
src/UdpHubListener.cpp [new file with mode: 0644]
src/UdpHubListener.h [new file with mode: 0644]
src/Volume.cpp [new file with mode: 0644]
src/Volume.h [new file with mode: 0644]
src/WaitFreeFrameBuffer.h [new file with mode: 0644]
src/WaitFreeRingBuffer.h [new file with mode: 0644]
src/build [new file with mode: 0755]
src/compressordsp.h [new file with mode: 0644]
src/dblsqd/feed.cpp [new file with mode: 0644]
src/dblsqd/feed.h [new file with mode: 0644]
src/dblsqd/release.cpp [new file with mode: 0644]
src/dblsqd/release.h [new file with mode: 0644]
src/dblsqd/semver.cpp [new file with mode: 0644]
src/dblsqd/semver.h [new file with mode: 0644]
src/dblsqd/update_dialog.cpp [new file with mode: 0644]
src/dblsqd/update_dialog.h [new file with mode: 0644]
src/dblsqd/update_dialog.ui [new file with mode: 0644]
src/freeverbdsp.h [new file with mode: 0644]
src/freeverbmonodsp.h [new file with mode: 0644]
src/gui/AppIcon.qml [new file with mode: 0644]
src/gui/AudioInterfaceMode.h [new file with mode: 0644]
src/gui/AudioSettings.qml [new file with mode: 0644]
src/gui/Browse.qml [new file with mode: 0644]
src/gui/ChangeDevices.qml [new file with mode: 0644]
src/gui/Connected.qml [new file with mode: 0644]
src/gui/CreateStudio.qml [new file with mode: 0644]
src/gui/DeviceControls.qml [new file with mode: 0644]
src/gui/DeviceControlsGroup.qml [new file with mode: 0644]
src/gui/DeviceRefreshButton.qml [new file with mode: 0644]
src/gui/DeviceWarning.qml [new file with mode: 0644]
src/gui/Failed.qml [new file with mode: 0644]
src/gui/FeedbackSurvey.qml [new file with mode: 0644]
src/gui/FirstLaunch.qml [new file with mode: 0644]
src/gui/Footer.qml [new file with mode: 0644]
src/gui/InfoTooltip.qml [new file with mode: 0644]
src/gui/JTOriginal.png [new file with mode: 0644]
src/gui/JTVS.png [new file with mode: 0644]
src/gui/LearnMoreButton.qml [new file with mode: 0644]
src/gui/Login.qml [new file with mode: 0644]
src/gui/Meter.qml [new file with mode: 0644]
src/gui/MeterBars.qml [new file with mode: 0644]
src/gui/NoNap.h [new file with mode: 0644]
src/gui/NoNap.mm [new file with mode: 0644]
src/gui/Permissions.qml [new file with mode: 0644]
src/gui/Poppins-Bold.ttf [new file with mode: 0644]
src/gui/Poppins-Regular.ttf [new file with mode: 0644]
src/gui/Prompt.svg [new file with mode: 0644]
src/gui/Recommendations.qml [new file with mode: 0644]
src/gui/SectionHeading.qml [new file with mode: 0644]
src/gui/Settings.qml [new file with mode: 0644]
src/gui/Setup.qml [new file with mode: 0644]
src/gui/Studio.qml [new file with mode: 0644]
src/gui/VolumeSlider.qml [new file with mode: 0644]
src/gui/Web.qml [new file with mode: 0644]
src/gui/WebEngine.qml [new file with mode: 0644]
src/gui/WebNull.qml [new file with mode: 0644]
src/gui/WebSocketTransport.cpp [new file with mode: 0644]
src/gui/WebSocketTransport.h [new file with mode: 0644]
src/gui/WebView.qml [new file with mode: 0644]
src/gui/about.cpp [new file with mode: 0644]
src/gui/about.h [new file with mode: 0644]
src/gui/about.png [new file with mode: 0644]
src/gui/about.ui [new file with mode: 0644]
src/gui/about@2x.png [new file with mode: 0644]
src/gui/alt/Jacktrip.ai [new file with mode: 0644]
src/gui/alt/about.png [new file with mode: 0644]
src/gui/alt/about@2x.png [new file with mode: 0644]
src/gui/alt/icon.png [new file with mode: 0644]
src/gui/check.svg [new file with mode: 0644]
src/gui/close.svg [new file with mode: 0644]
src/gui/cog.svg [new file with mode: 0644]
src/gui/ethernet.svg [new file with mode: 0644]
src/gui/expand_less.svg [new file with mode: 0644]
src/gui/expand_more.svg [new file with mode: 0644]
src/gui/externalMic.svg [new file with mode: 0644]
src/gui/flags/AE.svg [new file with mode: 0644]
src/gui/flags/AU.svg [new file with mode: 0644]
src/gui/flags/BE.svg [new file with mode: 0644]
src/gui/flags/BR.svg [new file with mode: 0644]
src/gui/flags/CA.svg [new file with mode: 0644]
src/gui/flags/CH.svg [new file with mode: 0644]
src/gui/flags/DE.svg [new file with mode: 0644]
src/gui/flags/FR.svg [new file with mode: 0644]
src/gui/flags/GB.svg [new file with mode: 0644]
src/gui/flags/HK.svg [new file with mode: 0644]
src/gui/flags/ID.svg [new file with mode: 0644]
src/gui/flags/IT.svg [new file with mode: 0644]
src/gui/flags/JP.svg [new file with mode: 0644]
src/gui/flags/RO.svg [new file with mode: 0644]
src/gui/flags/SE.svg [new file with mode: 0644]
src/gui/flags/SG.svg [new file with mode: 0644]
src/gui/flags/TW.svg [new file with mode: 0644]
src/gui/flags/US.svg [new file with mode: 0644]
src/gui/flags/ZA.svg [new file with mode: 0644]
src/gui/headphones.svg [new file with mode: 0644]
src/gui/help.svg [new file with mode: 0644]
src/gui/icon.png [new file with mode: 0644]
src/gui/jacktrip white.png [new file with mode: 0644]
src/gui/jacktrip.png [new file with mode: 0644]
src/gui/join.svg [new file with mode: 0644]
src/gui/language.svg [new file with mode: 0644]
src/gui/leave.svg [new file with mode: 0644]
src/gui/logo.svg [new file with mode: 0644]
src/gui/loud.svg [new file with mode: 0644]
src/gui/manage.svg [new file with mode: 0644]
src/gui/messageDialog.cpp [new file with mode: 0644]
src/gui/messageDialog.h [new file with mode: 0644]
src/gui/messageDialog.ui [new file with mode: 0644]
src/gui/mic.svg [new file with mode: 0644]
src/gui/micoff.svg [new file with mode: 0644]
src/gui/network.svg [new file with mode: 0644]
src/gui/networkCheck.svg [new file with mode: 0644]
src/gui/private.svg [new file with mode: 0644]
src/gui/public.svg [new file with mode: 0644]
src/gui/qjacktrip.cpp [new file with mode: 0644]
src/gui/qjacktrip.h [new file with mode: 0644]
src/gui/qjacktrip.qrc [new file with mode: 0644]
src/gui/qjacktrip.ui [new file with mode: 0644]
src/gui/qjacktrip_novs.qrc [new file with mode: 0644]
src/gui/quiet.svg [new file with mode: 0644]
src/gui/refresh.svg [new file with mode: 0644]
src/gui/sentiment_very_dissatisfied.svg [new file with mode: 0644]
src/gui/share.svg [new file with mode: 0644]
src/gui/speed.svg [new file with mode: 0644]
src/gui/star.svg [new file with mode: 0644]
src/gui/start.svg [new file with mode: 0644]
src/gui/textbuf.cpp [new file with mode: 0644]
src/gui/textbuf.h [new file with mode: 0644]
src/gui/video.svg [new file with mode: 0644]
src/gui/virtualstudio.cpp [new file with mode: 0644]
src/gui/virtualstudio.h [new file with mode: 0644]
src/gui/vs.qml [new file with mode: 0644]
src/gui/vsApi.cpp [new file with mode: 0644]
src/gui/vsApi.h [new file with mode: 0644]
src/gui/vsAudio.cpp [new file with mode: 0644]
src/gui/vsAudio.h [new file with mode: 0644]
src/gui/vsAuth.cpp [new file with mode: 0644]
src/gui/vsAuth.h [new file with mode: 0644]
src/gui/vsConstants.h [new file with mode: 0644]
src/gui/vsDeeplink.cpp [new file with mode: 0644]
src/gui/vsDeeplink.h [new file with mode: 0644]
src/gui/vsDevice.cpp [new file with mode: 0644]
src/gui/vsDevice.h [new file with mode: 0644]
src/gui/vsDeviceCodeFlow.cpp [new file with mode: 0644]
src/gui/vsDeviceCodeFlow.h [new file with mode: 0644]
src/gui/vsMacPermissions.h [new file with mode: 0644]
src/gui/vsMacPermissions.mm [new file with mode: 0644]
src/gui/vsPermissions.cpp [new file with mode: 0644]
src/gui/vsPermissions.h [new file with mode: 0644]
src/gui/vsPing.cpp [new file with mode: 0644]
src/gui/vsPing.h [new file with mode: 0644]
src/gui/vsPinger.cpp [new file with mode: 0644]
src/gui/vsPinger.h [new file with mode: 0644]
src/gui/vsQmlClipboard.h [new file with mode: 0644]
src/gui/vsQuickView.cpp [new file with mode: 0644]
src/gui/vsQuickView.h [new file with mode: 0644]
src/gui/vsServerInfo.cpp [new file with mode: 0644]
src/gui/vsServerInfo.h [new file with mode: 0644]
src/gui/vsWebSocket.cpp [new file with mode: 0644]
src/gui/vsWebSocket.h [new file with mode: 0644]
src/gui/vuMeter.cpp [new file with mode: 0644]
src/gui/vuMeter.h [new file with mode: 0644]
src/gui/warning.svg [new file with mode: 0644]
src/gui/wedge.svg [new file with mode: 0644]
src/gui/wedge_inactive.svg [new file with mode: 0644]
src/jacktrip_globals.cpp [new file with mode: 0644]
src/jacktrip_globals.h [new file with mode: 0644]
src/jacktrip_types.h [new file with mode: 0644]
src/limiterdsp.h [new file with mode: 0644]
src/main.cpp [new file with mode: 0644]
src/makeXcodeproj.sh [new file with mode: 0755]
src/meterdsp.h [new file with mode: 0644]
src/monitordsp.h [new file with mode: 0644]
src/stereotomonodsp.h [new file with mode: 0644]
src/tonedsp.h [new file with mode: 0644]
src/volumedsp.h [new file with mode: 0644]
src/zitarevdsp.h [new file with mode: 0644]
src/zitarevmonodsp.h [new file with mode: 0644]
subprojects/packagefiles/rtaudio-coreaudio-stream-channels.patch [new file with mode: 0644]
subprojects/packagefiles/rtaudio-remove-input-disconnect-listener.patch [new file with mode: 0644]
subprojects/rtaudio.wrap [new file with mode: 0644]
subprojects/wingetopt.wrap [new file with mode: 0644]
tests/jacktrip_tests.cpp [new file with mode: 0644]
win/CodeSignTool/CodeSignTool.sh [new file with mode: 0755]
win/CodeSignTool/conf/code_sign_tool.properties [new file with mode: 0644]
win/CodeSignTool/conf/log4j2.xml [new file with mode: 0644]
win/build_installer.bat [new file with mode: 0755]
win/dialog.bmp [new file with mode: 0644]
win/dialog_alt.bmp [new file with mode: 0644]
win/getCsv.sh [new file with mode: 0755]
win/jack.pc [new file with mode: 0644]
win/jacktrip.exe.manifest [new file with mode: 0644]
win/jacktrip.ico [new file with mode: 0644]
win/jacktrip.wxs [new file with mode: 0644]
win/jacktrip_alt.ico [new file with mode: 0644]
win/meson.build [new file with mode: 0644]
win/qjacktrip.rc [new file with mode: 0644]
win/qt5.wxs [new file with mode: 0644]
win/qt6-noguids.wxs [new file with mode: 0644]
win/qt6.wxs [new file with mode: 0644]

diff --git a/.clang-format b/.clang-format
new file mode 100644 (file)
index 0000000..273d78a
--- /dev/null
@@ -0,0 +1,184 @@
+---
+Language:        Cpp
+# BasedOnStyle:  Google
+AccessModifierOffset: -1
+AlignAfterOpenBracket: Align
+AlignConsecutiveMacros: true
+AlignConsecutiveAssignments: true
+AlignConsecutiveBitFields: false
+AlignConsecutiveDeclarations: false
+AlignEscapedNewlines: Left
+AlignOperands:   Align
+AlignTrailingComments: true
+AllowAllArgumentsOnNextLine: true
+AllowAllConstructorInitializersOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortEnumsOnASingleLine: true
+AllowShortBlocksOnASingleLine: Empty
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: Inline
+AllowShortLambdasOnASingleLine: Empty
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: true
+AlwaysBreakTemplateDeclarations: Yes
+BinPackArguments: true
+BinPackParameters: true
+BraceWrapping:
+  AfterCaseLabel:  false
+  AfterClass:      false
+  AfterControlStatement: Never
+  AfterEnum:       false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  AfterExternBlock: false
+  BeforeCatch:     false
+  BeforeElse:      false
+  BeforeLambdaBody: false
+  BeforeWhile:     false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: NonAssignment
+BreakBeforeBraces: Linux
+BreakBeforeInheritanceComma: false
+BreakInheritanceList: BeforeComma
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeComma
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: true
+ColumnLimit:     90
+CommentPragmas: '( \| |\*--|<li>|@ref | @p | @param  |@returns |@warning |@ingroup |@author |@date |@related |@relates |@relatesalso |@deprecated |@image |@return |@brief |@attention |@copydoc |@addtogroup |@todo |@tparam |@see |@note |@skip |@skipline |@until |@line |@dontinclude |@include)'
+CompactNamespaces: false
+ConstructorInitializerAllOnOneLineOrOnePerLine: true
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DeriveLineEnding: true
+DerivePointerAlignment: false
+DisableFormat:   false
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+ForEachMacros:
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+IncludeBlocks:   Regroup
+IncludeCategories:
+  - Regex:           '^<ext/.*\.h>'
+    Priority:        2
+    SortPriority:    0
+  - Regex:           '^<.*\.h>'
+    Priority:        1
+    SortPriority:    0
+  - Regex:           '^<.*'
+    Priority:        2
+    SortPriority:    0
+  - Regex:           '.*'
+    Priority:        3
+    SortPriority:    0
+IncludeIsMainRegex: '([-_](test|unittest))?$'
+IncludeIsMainSourceRegex: ''
+IndentCaseLabels: false
+IndentCaseBlocks: false
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentExternBlock: AfterExternBlock
+IndentWidth:     4
+IndentWrappedFunctionNames: false
+InsertTrailingCommas: None
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Never
+ObjCBlockIndentWidth: 2
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 1
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyReturnTypeOnItsOwnLine: 200
+PointerAlignment: Left
+RawStringFormats:
+  - Language:        Cpp
+    Delimiters:
+      - cc
+      - CC
+      - cpp
+      - Cpp
+      - CPP
+      - 'c++'
+      - 'C++'
+    CanonicalDelimiter: ''
+    BasedOnStyle:    google
+  - Language:        TextProto
+    Delimiters:
+      - pb
+      - PB
+      - proto
+      - PROTO
+    EnclosingFunctions:
+      - EqualsProto
+      - EquivToProto
+      - PARSE_PARTIAL_TEXT_PROTO
+      - PARSE_TEST_PROTO
+      - PARSE_TEXT_PROTO
+      - ParseTextOrDie
+      - ParseTextProtoOrDie
+      - ParseTestProto
+      - ParsePartialTestProto
+    CanonicalDelimiter: ''
+    BasedOnStyle:    google
+ReflowComments:  true
+SortIncludes:    true
+SortUsingDeclarations: true
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: false
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: ControlStatements
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 2
+SpacesInAngles:  false
+SpacesInConditionalStatement: false
+SpacesInContainerLiterals: true
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpaceBeforeSquareBrackets: false
+Standard:        Auto
+StatementMacros:
+  - Q_UNUSED
+  - QT_REQUIRE_VERSION
+TabWidth:        8
+UseCRLF:         false
+UseTab:          Never
+WhitespaceSensitiveMacros:
+  - STRINGIZE
+  - PP_STRINGIZE
+  - BOOST_PP_STRINGIZE
+---
+Language:        ObjC
+DisableFormat: true
+...
diff --git a/.clang-format-ignore b/.clang-format-ignore
new file mode 100644 (file)
index 0000000..9337953
--- /dev/null
@@ -0,0 +1,4 @@
+externals/*
+faust-src/faust2header.cpp
+src/*dsp.h
+
diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644 (file)
index 0000000..864b26f
--- /dev/null
@@ -0,0 +1 @@
+Checks: '-*,modernize-deprecated-headers,modernize-loop-convert,google-build-using-namespace'
diff --git a/.clang-tidy-ignore b/.clang-tidy-ignore
new file mode 100644 (file)
index 0000000..0cb64dc
--- /dev/null
@@ -0,0 +1,4 @@
+subprojects
+externals
+documentation
+src/*dsp.h
\ No newline at end of file
diff --git a/.dockerignore b/.dockerignore
new file mode 100644 (file)
index 0000000..b517c8e
--- /dev/null
@@ -0,0 +1,8 @@
+*
+!docs
+!meson*
+!container
+!src
+!subprojects
+!linux
+!win
\ No newline at end of file
diff --git a/.mailmap b/.mailmap
new file mode 100644 (file)
index 0000000..626b511
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,35 @@
+Nils Tonnätt <nils.tonnaett@posteo.de>
+Nils Tonnätt <nils.tonnaett@posteo.de> studio <akkijyrkkae@campus.tu-berlin.de>
+
+Marcin Pączkowski <dyfeer@gmail.com>
+
+Maximilian Wagenbach <maximilian.wagenbach@native-instruments.de>
+
+Julius Smith <jos@ccrma.stanford.edu>
+Julius Smith <jos@ccrma.stanford.edu> Julius Smith <julius.smith@gmail.com>
+
+Juan-Pablo Caceres <caceres@adobe.com> <jcacerec@gmail.com>
+Juan-Pablo Caceres <caceres@adobe.com> <jcacerec@users.noreply.github.com>
+Juan-Pablo Caceres <caceres@adobe.com> <jcacerec@3532cb72-8e7e-11dd-98a5-cbe9a756ba3b>
+Juan-Pablo Caceres <caceres@adobe.com> jcaceres <jcaceres@3532cb72-8e7e-11dd-98a5-cbe9a756ba3b>
+Juan-Pablo Caceres <caceres@adobe.com> jua20133 <jua20133@adobe.com>
+Juan-Pablo Caceres <caceres@adobe.com> jcaceres <jcaceres@ccrma.stanford.edu>
+
+Aaron Wyatt <github@psi-borg.org>
+Aaron Wyatt <github@psi-borg.org> psiborg112 <psiborg112@users.noreply.github.com>
+
+Chris Chafe <cc@ccrma.stanford.edu>
+Chris Chafe <cc@ccrma.stanford.edu> cc <xxx>
+Chris Chafe <cc@ccrma.stanford.edu> <cc@localhost.localdomain>
+Chris Chafe <cc@ccrma.stanford.edu> <4406287+cchafe@users.noreply.github.com>
+
+
+Bonnie Kwong <tersewings@protonmail.com>
+Bonnie Kwong <tersewings@protonmail.com> Bonnie Kwong <bonniekwong@Bonnies-MBP-2.attlocal.net>
+
+Omar Costa Hamido <ocostaha@uci.edu>
+
+Mike Dickey <mike@mikedickey.com>
+Mike Dickey <mike@mikedickey.com> Mike Dickey <mdickey@splunk.com>
+
+IOhannes m zmölnig <zmoelnig@umlautQ.umlaeute.mur.at> <zmoelnig@umlautS.umlaeute.mur.at>
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644 (file)
index 0000000..bcffe68
--- /dev/null
@@ -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
new file mode 100644 (file)
index 0000000..a4e80a8
--- /dev/null
@@ -0,0 +1,291 @@
+cmake_minimum_required(VERSION 3.12)
+set(CMAKE_OSX_DEPLOYMENT_TARGET 10.14)
+set(CMAKE_CXX_STANDARD 17)
+project(QJackTrip)
+
+set(nogui FALSE)
+set(rtaudio TRUE)
+set(weakjack TRUE)
+set(novs FALSE)
+set(vsftux FALSE)
+set(noupdater FALSE)
+set(psi FALSE)
+set(QtVersion "5")
+
+if (${QtVersion} MATCHES "5")
+  set(CMAKE_OSX_DEPLOYMENT_TARGET 10.13)
+  set(CMAKE_OSX_ARCHITECTURES arm64;x86_64)
+endif ()
+
+message(STATUS "Hello Aaron! For anyone else, heed the following warning:")
+message(WARNING "The CMake build of JackTrip is currently NOT officially supported. Meson or QMake are recommended for a full featured build."
+        "https://jacktrip.github.io/jacktrip/Build/Meson_build/")
+
+if (nogui)
+  add_compile_definitions(NO_GUI)
+  set(novs TRUE)
+  set(noupdater TRUE)
+endif ()
+
+if (psi)
+  set(novs TRUE)
+endif ()
+
+if (novs)
+  add_compile_definitions(NO_VS)
+  set(QRC_FILE "src/gui/qjacktrip_novs.qrc")
+else ()
+  set(QRC_FILE "src/gui/qjacktrip.qrc")
+endif ()
+
+if (vsftux)
+  add_compile_definitions(VS_FTUX)
+endif ()
+
+if (noupdater)
+  add_compile_definitions(NO_UPDATER)
+endif ()
+
+if (psi)
+  add_compile_definitions(PSI)
+  if (novs)
+    add_compile_definitions(BUILD_TYPE="psi-borg.org NO_VS binary")
+  else ()
+    add_compile_definitions(BUILD_TYPE="psi-borg.org binary")
+  endif ()
+
+  file(READ "${QRC_FILE}" QRC_CONTENTS)
+  string(REPLACE "<file>about@2x.png" "<file alias=\"about@2x.png\">alt/about@2x.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  string(REPLACE "<file>about.png" "<file alias=\"about.png\">alt/about.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  string(REPLACE "<file>icon.png" "<file alias=\"icon.png\">alt/icon.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  file(WRITE "${QRC_FILE}" "${QRC_CONTENTS}")
+  string(TIMESTAMP BUILD_DATE "%Y%m%d")
+  set(BUILD_NUMBER "00")
+  add_compile_definitions(BUILD_ID="${BUILD_DATE}${BUILD_NUMBER}")
+  add_compile_definitions(NDEBUG)
+else ()
+  file(READ "${QRC_FILE}" QRC_CONTENTS)
+  string(REPLACE "<file alias=\"about@2x.png\">alt/about@2x.png" "<file>about@2x.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  string(REPLACE "<file alias=\"about.png\">alt/about.png" "<file>about.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  string(REPLACE "<file alias=\"icon.png\">alt/icon.png" "<file>icon.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  file(WRITE "${QRC_FILE}" "${QRC_CONTENTS}")
+endif ()
+
+add_compile_definitions(QT_OPENSOURCE)
+
+if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+  set (ENV{PKG_CONFIG_PATH} "/usr/local/lib/pkgconfig")
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "Windows")
+  add_compile_definitions(_WIN32_WINNT=0x0600 WIN32_LEAN_AND_MEAN)
+  if (EXISTS "C:/Program Files/JACK2/include")
+    include_directories("C:/Program Files/JACK2/include")
+    set (jacklib "C:/Program Files/JACK2/lib/libjack64.lib")
+  else ()
+    include_directories("C:/Program Files (x86)/Jack/includes")
+    set (jacklib "C:/Program Files (x86)/Jack/lib/libjack64.lib")
+  endif ()
+  file(GLOB QtDirs "C:/Qt/${QtVersion}.*.*/mingw*_64")
+  list(GET QtDirs 0 QtDir)
+  message(STATUS "Using Qt found at ${QtDir}")
+  set (CMAKE_PREFIX_PATH "${QtDir}")
+  if (rtaudio)
+    include_directories("C:/Program Files (x86)/RtAudio/include")
+    set (rtaudiolib "C:/Program Files (x86)/RtAudio/lib/librtaudio.dll.a")
+  endif ()
+endif ()
+
+string(PREPEND QtVersion "Qt")
+
+if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+  find_package(PkgConfig REQUIRED)
+  pkg_check_modules(JACK REQUIRED IMPORTED_TARGET jack)
+  if (weakjack)
+    # On mac, weakjack doesn't find jack unless this is explicitly included.
+    include_directories(${JACK_INCLUDE_DIRS})
+  endif ()
+  if (rtaudio)
+    pkg_check_modules(RTAUDIO REQUIRED IMPORTED_TARGET rtaudio)
+  endif ()
+endif ()
+
+#set_property(SOURCE src/Regulator.h PROPERTY SKIP_AUTOGEN ON)
+# Find includes in corresponding build directories
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+# Instruct CMake to run moc automatically when needed.
+set(CMAKE_AUTOMOC ON)
+# Instruct CMake to create code from Qt designer ui files
+set(CMAKE_AUTOUIC ON)
+
+set(CMAKE_AUTORCC ON)
+
+# Find the QtWidgets library
+if (NOT nogui)
+  find_package(${QtVersion}Widgets CONFIG REQUIRED)
+  if (NOT novs)
+    find_package(${QtVersion}Quick CONFIG REQUIRED)
+    find_package(${QtVersion}NetworkAuth CONFIG REQUIRED)
+    find_package(${QtVersion}WebSockets CONFIG REQUIRED)
+  endif ()
+endif ()
+find_package(${QtVersion}Network CONFIG REQUIRED)
+
+set(qjacktrip_SRC
+  src/main.cpp
+  src/Settings.cpp
+  src/jacktrip_globals.cpp
+  src/JackTrip.cpp
+  src/UdpHubListener.cpp
+  src/JackTripWorker.cpp
+  src/DataProtocol.cpp
+  src/UdpDataProtocol.cpp
+  src/AudioInterface.cpp
+  src/JackAudioInterface.cpp
+  src/JMess.cpp
+  src/LoopBack.cpp
+  src/PacketHeader.cpp
+  src/RingBuffer.cpp
+  src/JitterBuffer.cpp
+  src/Regulator.cpp
+  src/Compressor.cpp
+  src/Limiter.cpp
+  src/Reverb.cpp
+  src/AudioTester.cpp
+  src/Patcher.cpp
+  src/SslServer.cpp
+  src/Auth.cpp
+  src/ProcessPlugin.cpp
+)
+
+if (rtaudio)
+  add_compile_definitions(RT_AUDIO)
+  set (qjacktrip_SRC ${qjacktrip_SRC}
+    src/RtAudioInterface.cpp
+    src/StereoToMono.cpp
+  )
+endif ()
+
+if (weakjack)
+  add_compile_definitions(USE_WEAK_JACK)
+  include_directories("externals/weakjack")
+  set (qjacktrip_SRC ${qjacktrip_SRC}
+    externals/weakjack/weak_libjack.c
+  )
+endif ()
+
+if (NOT nogui)
+  set (qjacktrip_SRC ${qjacktrip_SRC}
+    src/gui/qjacktrip.cpp
+    src/gui/about.cpp
+    src/gui/messageDialog.cpp
+    src/gui/textbuf.cpp
+    src/gui/vuMeter.cpp
+    src/Meter.cpp
+  )
+  
+  if (NOT novs)
+    set (qjacktrip_SRC ${qjacktrip_SRC}
+      src/gui/virtualstudio.cpp
+      src/gui/vsApi.cpp
+      src/gui/vsAuth.cpp
+      src/gui/vsDeviceCodeFlow.cpp
+      src/gui/vsDeeplink.cpp
+      src/gui/vsQuickView.cpp
+      src/gui/vsServerInfo.cpp
+      src/gui/vsPing.cpp
+      src/gui/vsPinger.cpp
+      src/gui/vsDevice.cpp
+      src/gui/vsAudio.cpp
+      src/gui/vsWebSocket.cpp
+      src/gui/vsPermissions.cpp
+      src/gui/qjacktrip.qrc
+      src/Analyzer.cpp
+      src/Monitor.cpp
+      src/Volume.cpp
+      src/Tone.cpp
+      # Need to include this for AUTOMOC to do its thing
+      src/JTApplication.h
+      src/gui/vsQmlClipboard.h
+    )
+    if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+      set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/vsMacPermissions.mm)
+    endif ()
+  else ()
+    set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/qjacktrip_novs.qrc)
+  endif ()
+  
+  if (NOT noupdater)
+    set (qjacktrip_SRC ${qjacktrip_SRC}
+      src/dblsqd/feed.cpp
+      src/dblsqd/release.cpp
+      src/dblsqd/semver.cpp
+      src/dblsqd/update_dialog.cpp
+    )
+  endif ()
+
+  if (${CMAKE_SYSTEM_NAME} MATCHES "Windows")
+    file(READ "win/qjacktrip.rc" RC_CONTENTS)
+    if (psi)
+      string(REPLACE "jacktrip.ico" "jacktrip_alt.ico" RC_CONTENTS "${RC_CONTENTS}")
+    else ()
+      string(REPLACE "jacktrip_alt.ico" "jacktrip.ico" RC_CONTENTS "${RC_CONTENTS}")
+    endif ()
+    file(WRITE "win/qjacktrip.rc" "${RC_CONTENTS}")
+    set (qjacktrip_SRC ${qjacktrip_SRC} win/qjacktrip.rc)
+  elseif (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+    set (qjacktrip_SRC ${qjacktrip_SRC} src/gui/NoNap.mm)
+    set (CMAKE_C_FLAGS "-x objective-c")
+    set (CMAKE_EXE_LINKER_FLAGS "-framework Foundation")
+    if (NOT novs)
+      set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -framework AVFoundation")
+    endif ()
+  endif ()
+endif ()
+
+add_compile_definitions(WAIRTOHUB)
+
+# Tell CMake to create the executable
+add_executable(jacktrip ${qjacktrip_SRC})
+
+# Set our libraries for our linker
+set (qjacktrip_LIBS ${QtVersion}::Network)
+if (NOT nogui)
+  set (qjacktrip_LIBS ${qjacktrip_LIBS} ${QtVersion}::Widgets)
+  if (NOT novs)
+    set (qjacktrip_LIBS ${qjacktrip_LIBS} ${QtVersion}::Quick ${QtVersion}::NetworkAuth ${QtVersion}::WebSockets)
+  endif ()
+endif ()
+
+if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+  if (weakjack)
+    set (qjacktrip_LIBS ${qjacktrip_LIBS} dl)
+  else ()
+    set (qjacktrip_LIBS ${qjacktrip_LIBS} PkgConfig::JACK)
+  endif ()
+  if (rtaudio)
+    set (qjacktrip_LIBS ${qjacktrip_LIBS} PkgConfig::RTAUDIO)
+  endif ()
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "Windows")
+  set (qjacktrip_LIBS ${qjacktrip_LIBS} ws2_32)
+  if (NOT weakjack)
+    set (qjacktrip_LIBS ${qjacktrip_LIBS} ${jacklib})
+  endif ()
+  if (rtaudio)
+    set (qjacktrip_LIBS ${qjacktrip_LIBS} ${rtaudiolib})
+  endif ()
+endif ()
+
+target_link_libraries(jacktrip ${qjacktrip_LIBS})
+
+if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+  add_custom_command(
+    TARGET jacktrip POST_BUILD
+    COMMAND help2man --no-info --section=1 --output=jacktrip.1 ./jacktrip
+    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+  )
+endif ()
+
+# Install the executable
+install(TARGETS jacktrip DESTINATION bin)
+if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" OR ${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+  install(FILES ${CMAKE_BINARY_DIR}/jacktrip.1 DESTINATION share/man/man1)
+endif ()
diff --git a/Dockerfile b/Dockerfile
new file mode 100644 (file)
index 0000000..2fa1bc0
--- /dev/null
@@ -0,0 +1,68 @@
+# JackTrip container image using Redhat Universal Base Image ubi-init
+#
+# Copyright (c) 2023-2024 JackTrip Labs, Inc.
+# MIT License.
+#
+# To build this from repository root: "podman build -t jacktrip/jacktrip ."
+
+# container image versions
+ARG FEDORA_VERSION=34
+ARG JACK_VERSION=latest
+
+
+###############
+# STAGE BUILDER
+###############
+# use a temporary container to build jacktrip
+FROM registry.fedoraproject.org/fedora:${FEDORA_VERSION} AS builder
+
+# install tools require to build jacktrip
+RUN dnf install -y --nodocs gcc gcc-c++ meson git python3-pyyaml python3-jinja2 glib2-devel jack-audio-connection-kit-devel
+
+ENV QT_VERSION=6.5.3
+RUN if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; else export ARCH=arm64; fi \
+       && curl -L -s -o /root/qt.tar.gz "https://files.jacktrip.org/contrib/qt/qt-${QT_VERSION}-static-linux-${ARCH}.tar.gz" \
+       && tar -C /opt -xzf /root/qt.tar.gz \
+       && rm -f /root/qt.tar.gz
+
+# copy files from repository to build container
+COPY . /root
+
+# configure and run the build
+RUN cd /root \
+       && export QT_PATH=/opt/qt-${QT_VERSION}-static \
+       && export PATH=${QT_PATH}/bin:${PATH} \
+       && export LDFLAGS="-L${QT_PATH}/lib -L${QT_PATH}/plugins/tls" \
+       && meson setup -Ddefault_library=static -Dnogui=true --buildtype release builddir \
+       && meson compile -C builddir
+
+# stage files in INSTALLDIR
+ENV INSTALLDIR=/artifacts
+RUN mkdir -p ${INSTALLDIR}/usr/local/bin/ ${INSTALLDIR}/etc/systemd/system/ \
+       && cp /root/builddir/jacktrip ${INSTALLDIR}/usr/local/bin/ \
+       && strip ${INSTALLDIR}/usr/local/bin/jacktrip
+COPY linux/container/jacktrip.service ${INSTALLDIR}/etc/systemd/system/
+
+
+########################
+# STAGE JACKTRIP (FINAL)
+########################
+# use the jack ubi-init container
+FROM jacktrip/jack:${JACK_VERSION}
+
+# add jacktrip user, enable service and allow access to jackd
+RUN useradd -r -m -N -G audio -s /usr/sbin/nologin jacktrip \
+       && chown -R jacktrip.audio /home/jacktrip \
+       && chmod g+rwx /home/jacktrip \
+       && ln -s /etc/systemd/system/jacktrip.service /etc/systemd/system/multi-user.target.wants \
+       && sed -i 's,PassEnvironment=.*,PassEnvironment=SAMPLE_RATE BUFFER_SIZE JACK_OPTS JACKTRIP_OPTS,' /etc/systemd/system/defaults.service \
+       && sed -i 's,WantedBy=.*,WantedBy=multi-user.target jack.service jacktrip.service,' /etc/systemd/system/defaults.service \
+       && echo 'if [[ -z "$JACKTRIP_OPTS" ]]; then JACKTRIP_OPTS="-S -t -z --hubpatch 4 --bufstrategy 4 -q auto"; fi' >> /usr/sbin/defaults.sh \
+       && echo 'echo "JACKTRIP_OPTS=\"$JACKTRIP_OPTS\"" > /etc/default/jacktrip' >> /usr/sbin/defaults.sh
+
+# copy the artifacts we built into the final container image
+COPY --from=builder /artifacts /
+
+# jacktrip hub server listens on 4464 and uses 61000+ for clients
+EXPOSE 4464/tcp
+EXPOSE 61000-61100/udp
\ No newline at end of file
diff --git a/Doxyfile b/Doxyfile
new file mode 100644 (file)
index 0000000..44a9421
--- /dev/null
+++ b/Doxyfile
@@ -0,0 +1,2603 @@
+# Doxyfile 1.9.1
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project.
+#
+# All text after a double hash (##) is considered a comment and is placed in
+# front of the TAG it is preceding.
+#
+# All text after a single hash (#) is considered a comment and will be ignored.
+# The format is:
+# TAG = value [value, ...]
+# For lists, items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (\" \").
+
+#---------------------------------------------------------------------------
+# Project related configuration options
+#---------------------------------------------------------------------------
+
+# This tag specifies the encoding used for all characters in the configuration
+# file that follow. The default is UTF-8 which is also the encoding used for all
+# text before the first occurrence of this tag. Doxygen uses libiconv (or the
+# iconv built into libc) for the transcoding. See
+# https://www.gnu.org/software/libiconv/ for the list of possible encodings.
+# The default value is: UTF-8.
+
+DOXYFILE_ENCODING      = UTF-8
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded by
+# double-quotes, unless you are using Doxywizard) that should identify the
+# project for which the documentation is generated. This name is used in the
+# title of most generated pages and in a few other places.
+# The default value is: My Project.
+
+PROJECT_NAME           = JackTrip
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number. This
+# could be handy for archiving the generated documentation or if some version
+# control system is used.
+
+PROJECT_NUMBER         =
+
+# Using the PROJECT_BRIEF tag one can provide an optional one line description
+# for a project that appears at the top of each page and should give viewer a
+# quick idea about the purpose of the project. Keep the description short.
+
+PROJECT_BRIEF          =
+
+# With the PROJECT_LOGO tag one can specify a logo or an icon that is included
+# in the documentation. The maximum height of the logo should not exceed 55
+# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy
+# the logo to the output directory.
+
+PROJECT_LOGO           = linux/icons/jacktrip.svg
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) path
+# into which the generated documentation will be written. If a relative path is
+# entered, it will be relative to the location where doxygen was started. If
+# left blank the current directory will be used.
+
+OUTPUT_DIRECTORY       = .
+
+# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub-
+# directories (in 2 levels) under the output directory of each output format and
+# will distribute the generated files over these directories. Enabling this
+# option can be useful when feeding doxygen a huge amount of source files, where
+# putting all generated files in the same directory would otherwise causes
+# performance problems for the file system.
+# The default value is: NO.
+
+CREATE_SUBDIRS         = NO
+
+# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII
+# characters to appear in the names of generated files. If set to NO, non-ASCII
+# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode
+# U+3044.
+# The default value is: NO.
+
+ALLOW_UNICODE_NAMES    = YES
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# Possible values are: Afrikaans, Arabic, Armenian, Brazilian, Catalan, Chinese,
+# Chinese-Traditional, Croatian, Czech, Danish, Dutch, English (United States),
+# Esperanto, Farsi (Persian), Finnish, French, German, Greek, Hungarian,
+# Indonesian, Italian, Japanese, Japanese-en (Japanese with English messages),
+# Korean, Korean-en (Korean with English messages), Latvian, Lithuanian,
+# Macedonian, Norwegian, Persian (Farsi), Polish, Portuguese, Romanian, Russian,
+# Serbian, Serbian-Cyrillic, Slovak, Slovene, Spanish, Swedish, Turkish,
+# Ukrainian and Vietnamese.
+# The default value is: English.
+
+OUTPUT_LANGUAGE        = English
+
+# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all generated output in the proper direction.
+# Possible values are: None, LTR, RTL and Context.
+# The default value is: None.
+
+OUTPUT_TEXT_DIRECTION  = None
+
+# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member
+# descriptions after the members that are listed in the file and class
+# documentation (similar to Javadoc). Set to NO to disable this.
+# The default value is: YES.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief
+# description of a member or function before the detailed description
+#
+# Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+# The default value is: YES.
+
+REPEAT_BRIEF           = YES
+
+# This tag implements a quasi-intelligent brief description abbreviator that is
+# used to form the text in various listings. Each string in this list, if found
+# as the leading text of the brief description, will be stripped from the text
+# and the result, after processing the whole list, is used as the annotated
+# text. Otherwise, the brief description is used as-is. If left blank, the
+# following values are used ($name is automatically replaced with the name of
+# the entity):The $name class, The $name widget, The $name file, is, provides,
+# specifies, contains, represents, a, an and the.
+
+ABBREVIATE_BRIEF       =
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# doxygen will generate a detailed section even if there is only a brief
+# description.
+# The default value is: NO.
+
+ALWAYS_DETAILED_SEC    = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all
+# inherited members of a class in the documentation of that class as if those
+# members were ordinary class members. Constructors, destructors and assignment
+# operators of the base classes will not be shown.
+# The default value is: NO.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path
+# before files name in the file list and in the header files. If set to NO the
+# shortest path that makes the file name unique will be used
+# The default value is: YES.
+
+FULL_PATH_NAMES        = YES
+
+# The STRIP_FROM_PATH tag can be used to strip a user-defined part of the path.
+# Stripping is only done if one of the specified strings matches the left-hand
+# part of the path. The tag can be used to show relative paths in the file list.
+# If left blank the directory from which doxygen is run is used as the path to
+# strip.
+#
+# Note that you can specify absolute paths here, but also relative paths, which
+# will be relative from the directory where doxygen is started.
+# This tag requires that the tag FULL_PATH_NAMES is set to YES.
+
+STRIP_FROM_PATH        =
+
+# The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the
+# path mentioned in the documentation of a class, which tells the reader which
+# header file to include in order to use a class. If left blank only the name of
+# the header file containing the class definition is used. Otherwise one should
+# specify the list of include paths that are normally passed to the compiler
+# using the -I flag.
+
+STRIP_FROM_INC_PATH    =
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but
+# less readable) file names. This can be useful is your file systems doesn't
+# support long names like on DOS, Mac, or CD-ROM.
+# The default value is: NO.
+
+SHORT_NAMES            = NO
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then doxygen will interpret the
+# first line (until the first dot) of a Javadoc-style comment as the brief
+# description. If set to NO, the Javadoc-style will behave just like regular Qt-
+# style comments (thus requiring an explicit @brief command for a brief
+# description.)
+# The default value is: NO.
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line
+# such as
+# /***************
+# as being the beginning of a Javadoc-style comment "banner". If set to NO, the
+# Javadoc-style will behave just like regular comments and it will not be
+# interpreted by doxygen.
+# The default value is: NO.
+
+JAVADOC_BANNER         = NO
+
+# If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first
+# line (until the first dot) of a Qt-style comment as the brief description. If
+# set to NO, the Qt-style will behave just like regular Qt-style comments (thus
+# requiring an explicit \brief command for a brief description.)
+# The default value is: NO.
+
+QT_AUTOBRIEF           = NO
+
+# The MULTILINE_CPP_IS_BRIEF tag can be set to YES to make doxygen treat a
+# multi-line C++ special comment block (i.e. a block of //! or /// comments) as
+# a brief description. This used to be the default behavior. The new default is
+# to treat a multi-line C++ comment block as a detailed description. Set this
+# tag to YES if you prefer the old behavior instead.
+#
+# Note that setting this tag to YES also means that rational rose comments are
+# not recognized any more.
+# The default value is: NO.
+
+MULTILINE_CPP_IS_BRIEF = NO
+
+# By default Python docstrings are displayed as preformatted text and doxygen's
+# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the
+# doxygen's special commands can be used and the contents of the docstring
+# documentation blocks is shown as doxygen documentation.
+# The default value is: YES.
+
+PYTHON_DOCSTRING       = YES
+
+# If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the
+# documentation from any documented member that it re-implements.
+# The default value is: YES.
+
+INHERIT_DOCS           = YES
+
+# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new
+# page for each member. If set to NO, the documentation of a member will be part
+# of the file/class/namespace that contains it.
+# The default value is: NO.
+
+SEPARATE_MEMBER_PAGES  = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab. Doxygen
+# uses this value to replace tabs by spaces in code fragments.
+# Minimum value: 1, maximum value: 16, default value: 4.
+
+TAB_SIZE               = 8
+
+# This tag can be used to specify a number of aliases that act as commands in
+# the documentation. An alias has the form:
+# name=value
+# For example adding
+# "sideeffect=@par Side Effects:\n"
+# will allow you to put the command \sideeffect (or @sideeffect) in the
+# documentation, which will result in a user-defined paragraph with heading
+# "Side Effects:". You can put \n's in the value part of an alias to insert
+# newlines (in the resulting output). You can put ^^ in the value part of an
+# alias to insert a newline as if a physical newline was in the original file.
+# When you need a literal { or } or , in the value part of an alias you have to
+# escape them by means of a backslash (\), this can lead to conflicts with the
+# commands \{ and \} for these it is advised to use the version @{ and @} or use
+# a double escape (\\{ and \\})
+
+ALIASES                =
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C. For
+# instance, some of the names that are used will be different. The list of all
+# members will be omitted, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_FOR_C  = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java or
+# Python sources only. Doxygen will then generate output that is more tailored
+# for that language. For instance, namespaces will be presented as packages,
+# qualified scopes will look different, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# Set the OPTIMIZE_FOR_FORTRAN tag to YES if your project consists of Fortran
+# sources. Doxygen will then generate output that is tailored for Fortran.
+# The default value is: NO.
+
+OPTIMIZE_FOR_FORTRAN   = NO
+
+# Set the OPTIMIZE_OUTPUT_VHDL tag to YES if your project consists of VHDL
+# sources. Doxygen will then generate output that is tailored for VHDL.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_VHDL   = NO
+
+# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice
+# sources only. Doxygen will then generate output that is more tailored for that
+# language. For instance, namespaces will be presented as modules, types will be
+# separated into more groups, etc.
+# The default value is: NO.
+
+OPTIMIZE_OUTPUT_SLICE  = NO
+
+# Doxygen selects the parser to use depending on the extension of the files it
+# parses. With this tag you can assign which parser to use for a given
+# extension. Doxygen has a built-in mapping, but you can override or extend it
+# using this tag. The format is ext=language, where ext is a file extension, and
+# language is one of the parsers supported by doxygen: IDL, Java, JavaScript,
+# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL,
+# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran:
+# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser
+# tries to guess whether the code is fixed or free formatted code, this is the
+# default for Fortran type files). For instance to make doxygen treat .inc files
+# as Fortran files (default is PHP), and .f files as C (default is Fortran),
+# use: inc=Fortran f=C.
+#
+# Note: For files without extension you can use no_extension as a placeholder.
+#
+# Note that for custom extensions you also need to set FILE_PATTERNS otherwise
+# the files are not read by doxygen. When specifying no_extension you should add
+# * to the FILE_PATTERNS.
+#
+# Note see also the list of default file extension mappings.
+
+EXTENSION_MAPPING      =
+
+# If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments
+# according to the Markdown format, which allows for more readable
+# documentation. See https://daringfireball.net/projects/markdown/ for details.
+# The output of markdown processing is further processed by doxygen, so you can
+# mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in
+# case of backward compatibilities issues.
+# The default value is: YES.
+
+MARKDOWN_SUPPORT       = YES
+
+# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up
+# to that level are automatically included in the table of contents, even if
+# they do not have an id attribute.
+# Note: This feature currently applies only to Markdown headings.
+# Minimum value: 0, maximum value: 99, default value: 5.
+# This tag requires that the tag MARKDOWN_SUPPORT is set to YES.
+
+TOC_INCLUDE_HEADINGS   = 5
+
+# When enabled doxygen tries to link words that correspond to documented
+# classes, or namespaces to their corresponding documentation. Such a link can
+# be prevented in individual cases by putting a % sign in front of the word or
+# globally by setting AUTOLINK_SUPPORT to NO.
+# The default value is: YES.
+
+AUTOLINK_SUPPORT       = YES
+
+# If you use STL classes (i.e. std::string, std::vector, etc.) but do not want
+# to include (a tag file for) the STL sources as input, then you should set this
+# tag to YES in order to let doxygen match functions declarations and
+# definitions whose arguments contain STL classes (e.g. func(std::string);
+# versus func(std::string) {}). This also make the inheritance and collaboration
+# diagrams that involve STL classes more complete and accurate.
+# The default value is: NO.
+
+BUILTIN_STL_SUPPORT    = NO
+
+# If you use Microsoft's C++/CLI language, you should set this option to YES to
+# enable parsing support.
+# The default value is: NO.
+
+CPP_CLI_SUPPORT        = NO
+
+# Set the SIP_SUPPORT tag to YES if your project consists of sip (see:
+# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen
+# will parse them like normal C++ but will assume all classes use public instead
+# of private inheritance when no explicit protection keyword is present.
+# The default value is: NO.
+
+SIP_SUPPORT            = NO
+
+# For Microsoft's IDL there are propget and propput attributes to indicate
+# getter and setter methods for a property. Setting this option to YES will make
+# doxygen to replace the get and set methods by a property in the documentation.
+# This will only work if the methods are indeed getting or setting a simple
+# type. If this is not the case, or you want to show the methods anyway, you
+# should set this option to NO.
+# The default value is: YES.
+
+IDL_PROPERTY_SUPPORT   = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+# The default value is: NO.
+
+DISTRIBUTE_GROUP_DOC   = NO
+
+# If one adds a struct or class to a group and this option is enabled, then also
+# any nested class or struct is added to the same group. By default this option
+# is disabled and one has to add nested compounds explicitly via \ingroup.
+# The default value is: NO.
+
+GROUP_NESTED_COMPOUNDS = NO
+
+# Set the SUBGROUPING tag to YES to allow class member groups of the same type
+# (for instance a group of public functions) to be put as a subgroup of that
+# type (e.g. under the Public Functions section). Set it to NO to prevent
+# subgrouping. Alternatively, this can be done per class using the
+# \nosubgrouping command.
+# The default value is: YES.
+
+SUBGROUPING            = YES
+
+# When the INLINE_GROUPED_CLASSES tag is set to YES, classes, structs and unions
+# are shown inside the group in which they are included (e.g. using \ingroup)
+# instead of on a separate page (for HTML and Man pages) or section (for LaTeX
+# and RTF).
+#
+# Note that this feature does not work in combination with
+# SEPARATE_MEMBER_PAGES.
+# The default value is: NO.
+
+INLINE_GROUPED_CLASSES = NO
+
+# When the INLINE_SIMPLE_STRUCTS tag is set to YES, structs, classes, and unions
+# with only public data fields or simple typedef fields will be shown inline in
+# the documentation of the scope in which they are defined (i.e. file,
+# namespace, or group documentation), provided this scope is documented. If set
+# to NO, structs, classes, and unions are shown on a separate page (for HTML and
+# Man pages) or section (for LaTeX and RTF).
+# The default value is: NO.
+
+INLINE_SIMPLE_STRUCTS  = NO
+
+# When TYPEDEF_HIDES_STRUCT tag is enabled, a typedef of a struct, union, or
+# enum is documented as struct, union, or enum with the name of the typedef. So
+# typedef struct TypeS {} TypeT, will appear in the documentation as a struct
+# with name TypeT. When disabled the typedef will appear as a member of a file,
+# namespace, or class. And the struct will be named TypeS. This can typically be
+# useful for C code in case the coding convention dictates that all compound
+# types are typedef'ed and only the typedef is referenced, never the tag name.
+# The default value is: NO.
+
+TYPEDEF_HIDES_STRUCT   = NO
+
+# The size of the symbol lookup cache can be set using LOOKUP_CACHE_SIZE. This
+# cache is used to resolve symbols given their name and scope. Since this can be
+# an expensive process and often the same symbol appears multiple times in the
+# code, doxygen keeps a cache of pre-resolved symbols. If the cache is too small
+# doxygen will become slower. If the cache is too large, memory is wasted. The
+# cache size is given by this formula: 2^(16+LOOKUP_CACHE_SIZE). The valid range
+# is 0..9, the default is 0, corresponding to a cache size of 2^16=65536
+# symbols. At the end of a run doxygen will report the cache usage and suggest
+# the optimal cache size from a speed point of view.
+# Minimum value: 0, maximum value: 9, default value: 0.
+
+LOOKUP_CACHE_SIZE      = 0
+
+# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use
+# during processing. When set to 0 doxygen will based this on the number of
+# cores available in the system. You can set it explicitly to a value larger
+# than 0 to get more control over the balance between CPU load and processing
+# speed. At this moment only the input processing can be done using multiple
+# threads. Since this is still an experimental feature the default is set to 1,
+# which efficively disables parallel processing. Please report any issues you
+# encounter. Generating dot graphs in parallel is controlled by the
+# DOT_NUM_THREADS setting.
+# Minimum value: 0, maximum value: 32, default value: 1.
+
+NUM_PROC_THREADS       = 1
+
+#---------------------------------------------------------------------------
+# Build related configuration options
+#---------------------------------------------------------------------------
+
+# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in
+# documentation are documented, even if no documentation was available. Private
+# class members and static file members will be hidden unless the
+# EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES.
+# Note: This will also disable the warnings about undocumented members that are
+# normally produced when WARNINGS is set to YES.
+# The default value is: NO.
+
+EXTRACT_ALL            = YES
+
+# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will
+# be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual
+# methods of a class will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PRIV_VIRTUAL   = NO
+
+# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal
+# scope will be included in the documentation.
+# The default value is: NO.
+
+EXTRACT_PACKAGE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be
+# included in the documentation.
+# The default value is: NO.
+
+EXTRACT_STATIC         = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined
+# locally in source files will be included in the documentation. If set to NO,
+# only classes defined in header files are included. Does not have any effect
+# for Java sources.
+# The default value is: YES.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# This flag is only useful for Objective-C code. If set to YES, local methods,
+# which are defined in the implementation section but not in the interface are
+# included in the documentation. If set to NO, only methods in the interface are
+# included.
+# The default value is: NO.
+
+EXTRACT_LOCAL_METHODS  = NO
+
+# If this flag is set to YES, the members of anonymous namespaces will be
+# extracted and appear in the documentation as a namespace called
+# 'anonymous_namespace{file}', where file will be replaced with the base name of
+# the file that contains the anonymous namespace. By default anonymous namespace
+# are hidden.
+# The default value is: NO.
+
+EXTRACT_ANON_NSPACES   = NO
+
+# If this flag is set to YES, the name of an unnamed parameter in a declaration
+# will be determined by the corresponding definition. By default unnamed
+# parameters remain unnamed in the output.
+# The default value is: YES.
+
+RESOLVE_UNNAMED_PARAMS = YES
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, doxygen will hide all
+# undocumented members inside documented classes or files. If set to NO these
+# members will be included in the various overviews, but no documentation
+# section is generated. This option has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_MEMBERS     = NO
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy. If set
+# to NO, these classes will be included in the various overviews. This option
+# has no effect if EXTRACT_ALL is enabled.
+# The default value is: NO.
+
+HIDE_UNDOC_CLASSES     = NO
+
+# If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend
+# declarations. If set to NO, these declarations will be included in the
+# documentation.
+# The default value is: NO.
+
+HIDE_FRIEND_COMPOUNDS  = NO
+
+# If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any
+# documentation blocks found inside the body of a function. If set to NO, these
+# blocks will be appended to the function's detailed documentation block.
+# The default value is: NO.
+
+HIDE_IN_BODY_DOCS      = NO
+
+# The INTERNAL_DOCS tag determines if documentation that is typed after a
+# \internal command is included. If the tag is set to NO then the documentation
+# will be excluded. Set it to YES to include the internal documentation.
+# The default value is: NO.
+
+INTERNAL_DOCS          = NO
+
+# With the correct setting of option CASE_SENSE_NAMES doxygen will better be
+# able to match the capabilities of the underlying filesystem. In case the
+# filesystem is case sensitive (i.e. it supports files in the same directory
+# whose names only differ in casing), the option must be set to YES to properly
+# deal with such files in case they appear in the input. For filesystems that
+# are not case sensitive the option should be be set to NO to properly deal with
+# output files written for symbols that only differ in casing, such as for two
+# classes, one named CLASS and the other named Class, and to also support
+# references to files without having to specify the exact matching casing. On
+# Windows (including Cygwin) and MacOS, users should typically set this option
+# to NO, whereas on Linux or other Unix flavors it should typically be set to
+# YES.
+# The default value is: system dependent.
+
+CASE_SENSE_NAMES       = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with
+# their full class and namespace scopes in the documentation. If set to YES, the
+# scope will be hidden.
+# The default value is: NO.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will
+# append additional text to a page's title, such as Class Reference. If set to
+# YES the compound reference will be hidden.
+# The default value is: NO.
+
+HIDE_COMPOUND_REFERENCE= NO
+
+# If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of
+# the files that are included by a file in the documentation of that file.
+# The default value is: YES.
+
+SHOW_INCLUDE_FILES     = YES
+
+# If the SHOW_GROUPED_MEMB_INC tag is set to YES then Doxygen will add for each
+# grouped member an include statement to the documentation, telling the reader
+# which file to include in order to use the member.
+# The default value is: NO.
+
+SHOW_GROUPED_MEMB_INC  = NO
+
+# If the FORCE_LOCAL_INCLUDES tag is set to YES then doxygen will list include
+# files with double quotes in the documentation rather than with sharp brackets.
+# The default value is: NO.
+
+FORCE_LOCAL_INCLUDES   = NO
+
+# If the INLINE_INFO tag is set to YES then a tag [inline] is inserted in the
+# documentation for inline members.
+# The default value is: YES.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the
+# (detailed) documentation of file and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order.
+# The default value is: YES.
+
+SORT_MEMBER_DOCS       = YES
+
+# If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief
+# descriptions of file, namespace and class members alphabetically by member
+# name. If set to NO, the members will appear in declaration order. Note that
+# this will also influence the order of the classes in the class list.
+# The default value is: NO.
+
+SORT_BRIEF_DOCS        = NO
+
+# If the SORT_MEMBERS_CTORS_1ST tag is set to YES then doxygen will sort the
+# (brief and detailed) documentation of class members so that constructors and
+# destructors are listed first. If set to NO the constructors will appear in the
+# respective orders defined by SORT_BRIEF_DOCS and SORT_MEMBER_DOCS.
+# Note: If SORT_BRIEF_DOCS is set to NO this option is ignored for sorting brief
+# member documentation.
+# Note: If SORT_MEMBER_DOCS is set to NO this option is ignored for sorting
+# detailed member documentation.
+# The default value is: NO.
+
+SORT_MEMBERS_CTORS_1ST = NO
+
+# If the SORT_GROUP_NAMES tag is set to YES then doxygen will sort the hierarchy
+# of group names into alphabetical order. If set to NO the group names will
+# appear in their defined order.
+# The default value is: NO.
+
+SORT_GROUP_NAMES       = NO
+
+# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
+# fully-qualified names, including namespaces. If set to NO, the class list will
+# be sorted only by class name, not including the namespace part.
+# Note: This option is not very useful if HIDE_SCOPE_NAMES is set to YES.
+# Note: This option applies only to the class list, not to the alphabetical
+# list.
+# The default value is: NO.
+
+SORT_BY_SCOPE_NAME     = NO
+
+# If the STRICT_PROTO_MATCHING option is enabled and doxygen fails to do proper
+# type resolution of all parameters of a function it will reject a match between
+# the prototype and the implementation of a member function even if there is
+# only one candidate or it is obvious which candidate to choose by doing a
+# simple string match. By disabling STRICT_PROTO_MATCHING doxygen will still
+# accept a match between prototype and implementation in such cases.
+# The default value is: NO.
+
+STRICT_PROTO_MATCHING  = NO
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo
+# list. This list is created by putting \todo commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TODOLIST      = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test
+# list. This list is created by putting \test commands in the documentation.
+# The default value is: YES.
+
+GENERATE_TESTLIST      = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug
+# list. This list is created by putting \bug commands in the documentation.
+# The default value is: YES.
+
+GENERATE_BUGLIST       = YES
+
+# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO)
+# the deprecated list. This list is created by putting \deprecated commands in
+# the documentation.
+# The default value is: YES.
+
+GENERATE_DEPRECATEDLIST= YES
+
+# The ENABLED_SECTIONS tag can be used to enable conditional documentation
+# sections, marked by \if <section_label> ... \endif and \cond <section_label>
+# ... \endcond blocks.
+
+ENABLED_SECTIONS       =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines that the
+# initial value of a variable or macro / define can have for it to appear in the
+# documentation. If the initializer consists of more lines than specified here
+# it will be hidden. Use a value of 0 to hide initializers completely. The
+# appearance of the value of individual variables and macros / defines can be
+# controlled using \showinitializer or \hideinitializer command in the
+# documentation regardless of this setting.
+# Minimum value: 0, maximum value: 10000, default value: 30.
+
+MAX_INITIALIZER_LINES  = 30
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated at
+# the bottom of the documentation of classes and structs. If set to YES, the
+# list will mention the files that were used to generate the documentation.
+# The default value is: YES.
+
+SHOW_USED_FILES        = YES
+
+# Set the SHOW_FILES tag to NO to disable the generation of the Files page. This
+# will remove the Files entry from the Quick Index and from the Folder Tree View
+# (if specified).
+# The default value is: YES.
+
+SHOW_FILES             = YES
+
+# Set the SHOW_NAMESPACES tag to NO to disable the generation of the Namespaces
+# page. This will remove the Namespaces entry from the Quick Index and from the
+# Folder Tree View (if specified).
+# The default value is: YES.
+
+SHOW_NAMESPACES        = YES
+
+# The FILE_VERSION_FILTER tag can be used to specify a program or script that
+# doxygen should invoke to get the current version for each file (typically from
+# the version control system). Doxygen will invoke the program by executing (via
+# popen()) the command command input-file, where command is the value of the
+# FILE_VERSION_FILTER tag, and input-file is the name of an input file provided
+# by doxygen. Whatever the program writes to standard output is used as the file
+# version. For an example see the documentation.
+
+FILE_VERSION_FILTER    =
+
+# The LAYOUT_FILE tag can be used to specify a layout file which will be parsed
+# by doxygen. The layout file controls the global structure of the generated
+# output files in an output format independent way. To create the layout file
+# that represents doxygen's defaults, run doxygen with the -l option. You can
+# optionally specify a file name after the option, if omitted DoxygenLayout.xml
+# will be used as the name of the layout file.
+#
+# Note that if you run doxygen from a directory containing a file called
+# DoxygenLayout.xml, doxygen will parse it automatically even if the LAYOUT_FILE
+# tag is left empty.
+
+LAYOUT_FILE            =
+
+# The CITE_BIB_FILES tag can be used to specify one or more bib files containing
+# the reference definitions. This must be a list of .bib files. The .bib
+# extension is automatically appended if omitted. This requires the bibtex tool
+# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info.
+# For LaTeX the style of the bibliography can be controlled using
+# LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the
+# search path. See also \cite for info how to create references.
+
+CITE_BIB_FILES         =
+
+#---------------------------------------------------------------------------
+# Configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated to
+# standard output by doxygen. If QUIET is set to YES this implies that the
+# messages are off.
+# The default value is: NO.
+
+QUIET                  = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES
+# this implies that the warnings are on.
+#
+# Tip: Turn warnings on while writing the documentation.
+# The default value is: YES.
+
+WARNINGS               = YES
+
+# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate
+# warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag
+# will automatically be disabled.
+# The default value is: YES.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# If the WARN_IF_DOC_ERROR tag is set to YES, doxygen will generate warnings for
+# potential errors in the documentation, such as not documenting some parameters
+# in a documented function, or documenting parameters that don't exist or using
+# markup commands wrongly.
+# The default value is: YES.
+
+WARN_IF_DOC_ERROR      = YES
+
+# This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that
+# are documented, but have no documentation for their parameters or return
+# value. If set to NO, doxygen will only warn about wrong or incomplete
+# parameter documentation, but not about the absence of documentation. If
+# EXTRACT_ALL is set to YES then this flag will automatically be disabled.
+# The default value is: NO.
+
+WARN_NO_PARAMDOC       = NO
+
+# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when
+# a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS
+# then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but
+# at the end of the doxygen process doxygen will return with a non-zero status.
+# Possible values are: NO, YES and FAIL_ON_WARNINGS.
+# The default value is: NO.
+
+WARN_AS_ERROR          = NO
+
+# The WARN_FORMAT tag determines the format of the warning messages that doxygen
+# can produce. The string should contain the $file, $line, and $text tags, which
+# will be replaced by the file and line number from which the warning originated
+# and the warning text. Optionally the format may contain $version, which will
+# be replaced by the version of the file (if it could be obtained via
+# FILE_VERSION_FILTER)
+# The default value is: $file:$line: $text.
+
+WARN_FORMAT            = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning and error
+# messages should be written. If left blank the output is written to standard
+# error (stderr).
+
+WARN_LOGFILE           =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag is used to specify the files and/or directories that contain
+# documented source files. You may enter file names like myfile.cpp or
+# directories like /usr/src/myproject. Separate the files or directories with
+# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING
+# Note: If this tag is empty the current directory is searched.
+
+INPUT                  = ./src \
+                         ./documentation
+
+# This tag can be used to specify the character encoding of the source files
+# that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses
+# libiconv (or the iconv built into libc) for the transcoding. See the libiconv
+# documentation (see:
+# https://www.gnu.org/software/libiconv/) for the list of possible encodings.
+# The default value is: UTF-8.
+
+INPUT_ENCODING         = UTF-8
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and
+# *.h) to filter out the source-files in the directories.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# read by doxygen.
+#
+# Note the list of default checked file patterns might differ from the list of
+# default file extension mappings.
+#
+# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp,
+# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h,
+# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc,
+# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment),
+# *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, *.vhdl,
+# *.ucf, *.qsf and *.ice.
+
+FILE_PATTERNS          =
+
+# The RECURSIVE tag can be used to specify whether or not subdirectories should
+# be searched for input files as well.
+# The default value is: NO.
+
+RECURSIVE              = YES
+
+# The EXCLUDE tag can be used to specify files and/or directories that should be
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+#
+# Note that relative paths are relative to the directory from which doxygen is
+# run.
+
+EXCLUDE                =
+
+# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
+# directories that are symbolic links (a Unix file system feature) are excluded
+# from the input.
+# The default value is: NO.
+
+EXCLUDE_SYMLINKS       = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories for example use the pattern */test/*
+
+EXCLUDE_PATTERNS       =
+
+# The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names
+# (namespaces, classes, functions, etc.) that should be excluded from the
+# output. The symbol name can be a fully qualified name, a word, or if the
+# wildcard * is used, a substring. Examples: ANamespace, AClass,
+# AClass::ANamespace, ANamespace::*Test
+#
+# Note that the wildcards are matched against the file with absolute path, so to
+# exclude all test directories use the pattern */test/*
+
+EXCLUDE_SYMBOLS        =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or directories
+# that contain example code fragments that are included (see the \include
+# command).
+
+EXAMPLE_PATH           =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and
+# *.h) to filter out the source-files in the directories. If left blank all
+# files are included.
+
+EXAMPLE_PATTERNS       =
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude commands
+# irrespective of the value of the RECURSIVE tag.
+# The default value is: NO.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or directories
+# that contain images that are to be included in the documentation (see the
+# \image command).
+
+IMAGE_PATH             = ./documentation/img
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command:
+#
+# <filter> <input-file>
+#
+# where <filter> is the value of the INPUT_FILTER tag, and <input-file> is the
+# name of an input file. Doxygen will then use the output that the filter
+# program writes to standard output. If FILTER_PATTERNS is specified, this tag
+# will be ignored.
+#
+# Note that the filter must not add or remove lines; it is applied before the
+# code is scanned, but not when the output code is generated. If lines are added
+# or removed, the anchors will not be placed correctly.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+INPUT_FILTER           =
+
+# The FILTER_PATTERNS tag can be used to specify filters on a per file pattern
+# basis. Doxygen will compare the file name with each pattern and apply the
+# filter if there is a match. The filters are a list of the form: pattern=filter
+# (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how
+# filters are used. If the FILTER_PATTERNS tag is empty or if none of the
+# patterns match the file name, INPUT_FILTER is applied.
+#
+# Note that for custom extensions or not directly supported extensions you also
+# need to set EXTENSION_MAPPING for the extension otherwise the files are not
+# properly processed by doxygen.
+
+FILTER_PATTERNS        =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will also be used to filter the input files that are used for
+# producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES).
+# The default value is: NO.
+
+FILTER_SOURCE_FILES    = NO
+
+# The FILTER_SOURCE_PATTERNS tag can be used to specify source filters per file
+# pattern. A pattern will override the setting for FILTER_PATTERN (if any) and
+# it is also possible to disable source filtering for a specific pattern using
+# *.ext= (so without naming a filter).
+# This tag requires that the tag FILTER_SOURCE_FILES is set to YES.
+
+FILTER_SOURCE_PATTERNS =
+
+# If the USE_MDFILE_AS_MAINPAGE tag refers to the name of a markdown file that
+# is part of the input, its contents will be placed on the main page
+# (index.html). This can be useful if you have a project on for instance GitHub
+# and want to reuse the introduction page also for the doxygen output.
+
+USE_MDFILE_AS_MAINPAGE =
+
+#---------------------------------------------------------------------------
+# Configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will be
+# generated. Documented entities will be cross-referenced with these sources.
+#
+# Note: To get rid of all source code in the generated output, make sure that
+# also VERBATIM_HEADERS is set to NO.
+# The default value is: NO.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body of functions,
+# classes and enums directly into the documentation.
+# The default value is: NO.
+
+INLINE_SOURCES         = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES will instruct doxygen to hide any
+# special comment blocks from generated source code fragments. Normal C, C++ and
+# Fortran comments will always remain visible.
+# The default value is: YES.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the REFERENCED_BY_RELATION tag is set to YES then for each documented
+# entity all documented functions referencing it will be listed.
+# The default value is: NO.
+
+REFERENCED_BY_RELATION = NO
+
+# If the REFERENCES_RELATION tag is set to YES then for each documented function
+# all documented entities called/used by that function will be listed.
+# The default value is: NO.
+
+REFERENCES_RELATION    = NO
+
+# If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set
+# to YES then the hyperlinks from functions in REFERENCES_RELATION and
+# REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will
+# link to the documentation.
+# The default value is: YES.
+
+REFERENCES_LINK_SOURCE = YES
+
+# If SOURCE_TOOLTIPS is enabled (the default) then hovering a hyperlink in the
+# source code will show a tooltip with additional information such as prototype,
+# brief description and links to the definition and documentation. Since this
+# will make the HTML file larger and loading of large files a bit slower, you
+# can opt to disable this feature.
+# The default value is: YES.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+SOURCE_TOOLTIPS        = YES
+
+# If the USE_HTAGS tag is set to YES then the references to source code will
+# point to the HTML generated by the htags(1) tool instead of doxygen built-in
+# source browser. The htags tool is part of GNU's global source tagging system
+# (see https://www.gnu.org/software/global/global.html). You will need version
+# 4.8.6 or higher.
+#
+# To use it do the following:
+# - Install the latest version of global
+# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file
+# - Make sure the INPUT points to the root of the source tree
+# - Run doxygen as normal
+#
+# Doxygen will invoke htags (and that will in turn invoke gtags), so these
+# tools must be available from the command line (i.e. in the search path).
+#
+# The result: instead of the source browser generated by doxygen, the links to
+# source code will now point to the output of htags.
+# The default value is: NO.
+# This tag requires that the tag SOURCE_BROWSER is set to YES.
+
+USE_HTAGS              = NO
+
+# If the VERBATIM_HEADERS tag is set the YES then doxygen will generate a
+# verbatim copy of the header file for each class for which an include is
+# specified. Set to NO to disable this.
+# See also: Section \class.
+# The default value is: YES.
+
+VERBATIM_HEADERS       = YES
+
+# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the
+# clang parser (see:
+# http://clang.llvm.org/) for more accurate parsing at the cost of reduced
+# performance. This can be particularly helpful with template rich C++ code for
+# which doxygen's built-in parser lacks the necessary type information.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse_libclang=ON option for CMake.
+# The default value is: NO.
+
+CLANG_ASSISTED_PARSING = NO
+
+# If clang assisted parsing is enabled and the CLANG_ADD_INC_PATHS tag is set to
+# YES then doxygen will add the directory of each input to the include path.
+# The default value is: YES.
+
+CLANG_ADD_INC_PATHS    = YES
+
+# If clang assisted parsing is enabled you can provide the compiler with command
+# line options that you would normally use when invoking the compiler. Note that
+# the include paths will already be set by doxygen for the files and directories
+# specified with INPUT and INCLUDE_PATH.
+# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES.
+
+CLANG_OPTIONS          =
+
+# If clang assisted parsing is enabled you can provide the clang parser with the
+# path to the directory containing a file called compile_commands.json. This
+# file is the compilation database (see:
+# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the
+# options used when the source files were built. This is equivalent to
+# specifying the -p option to a clang tool, such as clang-check. These options
+# will then be passed to the parser. Any options specified with CLANG_OPTIONS
+# will be added as well.
+# Note: The availability of this option depends on whether or not doxygen was
+# generated with the -Duse_libclang=ON option for CMake.
+
+CLANG_DATABASE_PATH    =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index of all
+# compounds will be generated. Enable this if the project contains a lot of
+# classes, structs, unions or interfaces.
+# The default value is: YES.
+
+ALPHABETICAL_INDEX     = NO
+
+# In case all classes in a project start with a common prefix, all classes will
+# be put under the same header in the alphabetical index. The IGNORE_PREFIX tag
+# can be used to specify a prefix (or a list of prefixes) that should be ignored
+# while generating the index headers.
+# This tag requires that the tag ALPHABETICAL_INDEX is set to YES.
+
+IGNORE_PREFIX          =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output
+# The default value is: YES.
+
+GENERATE_HTML          = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_OUTPUT            = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for each
+# generated HTML page (for example: .htm, .php, .asp).
+# The default value is: .html.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a user-defined HTML header file for
+# each generated HTML page. If the tag is left blank doxygen will generate a
+# standard header.
+#
+# To get valid HTML the header file that includes any scripts and style sheets
+# that doxygen needs, which is dependent on the configuration options used (e.g.
+# the setting GENERATE_TREEVIEW). It is highly recommended to start with a
+# default header using
+# doxygen -w html new_header.html new_footer.html new_stylesheet.css
+# YourConfigFile
+# and then modify the file new_header.html. See also section "Doxygen usage"
+# for information on how to generate the default header that doxygen normally
+# uses.
+# Note: The header is subject to change so you typically have to regenerate the
+# default header when upgrading to a newer version of doxygen. For a description
+# of the possible markers and block names see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_HEADER            = ./documentation/header.html
+
+# The HTML_FOOTER tag can be used to specify a user-defined HTML footer for each
+# generated HTML page. If the tag is left blank doxygen will generate a standard
+# footer. See HTML_HEADER for more information on how to generate a default
+# footer and what special commands can be used inside the footer. See also
+# section "Doxygen usage" for information on how to generate the default footer
+# that doxygen normally uses.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FOOTER            = ./documentation/footer.html
+
+# The HTML_STYLESHEET tag can be used to specify a user-defined cascading style
+# sheet that is used by each HTML page. It can be used to fine-tune the look of
+# the HTML output. If left blank doxygen will generate a default style sheet.
+# See also section "Doxygen usage" for information on how to generate the style
+# sheet that doxygen normally uses.
+# Note: It is recommended to use HTML_EXTRA_STYLESHEET instead of this tag, as
+# it is more robust and this tag (HTML_STYLESHEET) will in the future become
+# obsolete.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_STYLESHEET        =
+
+# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# cascading style sheets that are included after the standard style sheets
+# created by doxygen. Using this option one can overrule certain style aspects.
+# This is preferred over using HTML_STYLESHEET since it does not replace the
+# standard style sheet and is therefore more robust against future updates.
+# Doxygen will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list). For an example see the documentation.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_STYLESHEET  = externals/doxygen-awesome-css/doxygen-awesome.css externals/doxygen-awesome-css/doxygen-awesome-sidebar-only.css
+
+# The HTML_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the HTML output directory. Note
+# that these files will be copied to the base HTML output directory. Use the
+# $relpath^ marker in the HTML_HEADER and/or HTML_FOOTER files to load these
+# files. In the HTML_STYLESHEET file, use the file name only. Also note that the
+# files will be copied as-is; there are no commands or markers available.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_EXTRA_FILES       =
+
+# The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen
+# will adjust the colors in the style sheet and background images according to
+# this color. Hue is specified as an angle on a colorwheel, see
+# https://en.wikipedia.org/wiki/Hue for more information. For instance the value
+# 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300
+# purple, and 360 is red again.
+# Minimum value: 0, maximum value: 359, default value: 220.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_HUE    = 220
+
+# The HTML_COLORSTYLE_SAT tag controls the purity (or saturation) of the colors
+# in the HTML output. For a value of 0 the output will use grayscales only. A
+# value of 255 will produce the most vivid colors.
+# Minimum value: 0, maximum value: 255, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_SAT    = 100
+
+# The HTML_COLORSTYLE_GAMMA tag controls the gamma correction applied to the
+# luminance component of the colors in the HTML output. Values below 100
+# gradually make the output lighter, whereas values above 100 make the output
+# darker. The value divided by 100 is the actual gamma applied, so 80 represents
+# a gamma of 0.8, The value 220 represents a gamma of 2.2, and 100 does not
+# change the gamma.
+# Minimum value: 40, maximum value: 240, default value: 80.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_COLORSTYLE_GAMMA  = 80
+
+# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML
+# page will contain the date and time when the page was generated. Setting this
+# to YES can help to show when doxygen was last run and thus if the
+# documentation is up to date.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_TIMESTAMP         = NO
+
+# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML
+# documentation will contain a main index with vertical navigation menus that
+# are dynamically created via JavaScript. If disabled, the navigation index will
+# consists of multiple levels of tabs that are statically embedded in every HTML
+# page. Disable this option to support browsers that do not have JavaScript,
+# like the Qt help browser.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_MENUS     = YES
+
+# If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML
+# documentation will contain sections that can be hidden and shown after the
+# page has loaded.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_DYNAMIC_SECTIONS  = NO
+
+# With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries
+# shown in the various tree structured indices initially; the user can expand
+# and collapse entries dynamically later on. Doxygen will expand the tree to
+# such a level that at most the specified number of entries are visible (unless
+# a fully collapsed tree already exceeds this amount). So setting the number of
+# entries 1 will produce a full collapsed tree by default. 0 is a special value
+# representing an infinite number of entries and will result in a full expanded
+# tree by default.
+# Minimum value: 0, maximum value: 9999, default value: 100.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_INDEX_NUM_ENTRIES = 100
+
+# If the GENERATE_DOCSET tag is set to YES, additional index files will be
+# generated that can be used as input for Apple's Xcode 3 integrated development
+# environment (see:
+# https://developer.apple.com/xcode/), introduced with OSX 10.5 (Leopard). To
+# create a documentation set, doxygen will generate a Makefile in the HTML
+# output directory. Running make will produce the docset in that directory and
+# running make install will install the docset in
+# ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at
+# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy
+# genXcode/_index.html for more information.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_DOCSET        = NO
+
+# This tag determines the name of the docset feed. A documentation feed provides
+# an umbrella under which multiple documentation sets from a single provider
+# (such as a company or product suite) can be grouped.
+# The default value is: Doxygen generated docs.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_FEEDNAME        = "Doxygen generated docs"
+
+# This tag specifies a string that should uniquely identify the documentation
+# set bundle. This should be a reverse domain-name style string, e.g.
+# com.mycompany.MyDocSet. Doxygen will append .docset to the name.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_BUNDLE_ID       = org.doxygen.Project
+
+# The DOCSET_PUBLISHER_ID tag specifies a string that should uniquely identify
+# the documentation publisher. This should be a reverse domain-name style
+# string, e.g. com.mycompany.MyDocSet.documentation.
+# The default value is: org.doxygen.Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_ID    = org.doxygen.Publisher
+
+# The DOCSET_PUBLISHER_NAME tag identifies the documentation publisher.
+# The default value is: Publisher.
+# This tag requires that the tag GENERATE_DOCSET is set to YES.
+
+DOCSET_PUBLISHER_NAME  = Publisher
+
+# If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three
+# additional HTML index files: index.hhp, index.hhc, and index.hhk. The
+# index.hhp is a project file that can be read by Microsoft's HTML Help Workshop
+# (see:
+# https://www.microsoft.com/en-us/download/details.aspx?id=21138) on Windows.
+#
+# The HTML Help Workshop contains a compiler that can convert all HTML output
+# generated by doxygen into a single compiled HTML file (.chm). Compiled HTML
+# files are now used as the Windows 98 help format, and will replace the old
+# Windows help format (.hlp) on all Windows platforms in the future. Compressed
+# HTML files also contain an index, a table of contents, and you can search for
+# words in the documentation. The HTML workshop also contains a viewer for
+# compressed HTML files.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_HTMLHELP      = NO
+
+# The CHM_FILE tag can be used to specify the file name of the resulting .chm
+# file. You can add a path in front of the file if the result should not be
+# written to the html output directory.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_FILE               =
+
+# The HHC_LOCATION tag can be used to specify the location (absolute path
+# including file name) of the HTML help compiler (hhc.exe). If non-empty,
+# doxygen will try to run the HTML help compiler on the generated index.hhp.
+# The file has to be specified with full path.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+HHC_LOCATION           =
+
+# The GENERATE_CHI flag controls if a separate .chi index file is generated
+# (YES) or that it should be included in the main .chm file (NO).
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+GENERATE_CHI           = NO
+
+# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc)
+# and project file content.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+CHM_INDEX_ENCODING     =
+
+# The BINARY_TOC flag controls whether a binary table of contents is generated
+# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it
+# enables the Previous and Next buttons.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members to
+# the table of contents of the HTML help documentation and to the tree view.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTMLHELP is set to YES.
+
+TOC_EXPAND             = NO
+
+# If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and
+# QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that
+# can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help
+# (.qch) of the generated HTML documentation.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_QHP           = NO
+
+# If the QHG_LOCATION tag is specified, the QCH_FILE tag can be used to specify
+# the file name of the resulting .qch file. The path specified is relative to
+# the HTML output folder.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QCH_FILE               =
+
+# The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help
+# Project output. For more information please see Qt Help Project / Namespace
+# (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace).
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_NAMESPACE          = org.doxygen.Project
+
+# The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt
+# Help Project output. For more information please see Qt Help Project / Virtual
+# Folders (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual-folders).
+# The default value is: doc.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_VIRTUAL_FOLDER     = doc
+
+# If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom
+# filter to add. For more information please see Qt Help Project / Custom
+# Filters (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_NAME   =
+
+# The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the
+# custom filter to add. For more information please see Qt Help Project / Custom
+# Filters (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom-filters).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_CUST_FILTER_ATTRS  =
+
+# The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this
+# project's filter section matches. Qt Help Project / Filter Attributes (see:
+# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes).
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHP_SECT_FILTER_ATTRS  =
+
+# The QHG_LOCATION tag can be used to specify the location (absolute path
+# including file name) of Qt's qhelpgenerator. If non-empty doxygen will try to
+# run qhelpgenerator on the generated .qhp file.
+# This tag requires that the tag GENERATE_QHP is set to YES.
+
+QHG_LOCATION           =
+
+# If the GENERATE_ECLIPSEHELP tag is set to YES, additional index files will be
+# generated, together with the HTML files, they form an Eclipse help plugin. To
+# install this plugin and make it available under the help contents menu in
+# Eclipse, the contents of the directory containing the HTML and XML files needs
+# to be copied into the plugins directory of eclipse. The name of the directory
+# within the plugins directory should be the same as the ECLIPSE_DOC_ID value.
+# After copying Eclipse needs to be restarted before the help appears.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_ECLIPSEHELP   = NO
+
+# A unique identifier for the Eclipse help plugin. When installing the plugin
+# the directory name containing the HTML and XML files should also have this
+# name. Each documentation set should have its own identifier.
+# The default value is: org.doxygen.Project.
+# This tag requires that the tag GENERATE_ECLIPSEHELP is set to YES.
+
+ECLIPSE_DOC_ID         = org.doxygen.Project
+
+# If you want full control over the layout of the generated HTML pages it might
+# be necessary to disable the index and replace it with your own. The
+# DISABLE_INDEX tag can be used to turn on/off the condensed index (tabs) at top
+# of each HTML page. A value of NO enables the index and the value YES disables
+# it. Since the tabs in the index contain the same information as the navigation
+# tree, you can set this option to YES if you also set GENERATE_TREEVIEW to YES.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+DISABLE_INDEX          = NO
+
+# The GENERATE_TREEVIEW tag is used to specify whether a tree-like index
+# structure should be generated to display hierarchical information. If the tag
+# value is set to YES, a side panel will be generated containing a tree-like
+# index structure (just like the one that is generated for HTML Help). For this
+# to work a browser that supports JavaScript, DHTML, CSS and frames is required
+# (i.e. any modern browser). Windows users are probably better off using the
+# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can
+# further fine-tune the look of the index. As an example, the default style
+# sheet generated by doxygen has an example that shows how to put an image at
+# the root of the tree instead of the PROJECT_NAME. Since the tree basically has
+# the same information as the tab index, you could consider setting
+# DISABLE_INDEX to YES when enabling this option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+GENERATE_TREEVIEW      = YES
+
+# The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that
+# doxygen will group on one line in the generated HTML documentation.
+#
+# Note that a value of 0 will completely suppress the enum values from appearing
+# in the overview section.
+# Minimum value: 0, maximum value: 20, default value: 4.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used
+# to set the initial width (in pixels) of the frame in which the tree is shown.
+# Minimum value: 0, maximum value: 1500, default value: 250.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+TREEVIEW_WIDTH         = 250
+
+# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to
+# external symbols imported via tag files in a separate window.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+EXT_LINKS_IN_WINDOW    = NO
+
+# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg
+# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see
+# https://inkscape.org) to generate formulas as SVG images instead of PNGs for
+# the HTML output. These images will generally look nicer at scaled resolutions.
+# Possible values are: png (the default) and svg (looks nicer but requires the
+# pdf2svg or inkscape tool).
+# The default value is: png.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+HTML_FORMULA_FORMAT    = png
+
+# Use this tag to change the font size of LaTeX formulas included as images in
+# the HTML documentation. When you change the font size after a successful
+# doxygen run you need to manually remove any form_*.png images from the HTML
+# output directory to force them to be regenerated.
+# Minimum value: 8, maximum value: 50, default value: 10.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_FONTSIZE       = 10
+
+# Use the FORMULA_TRANSPARENT tag to determine whether or not the images
+# generated for formulas are transparent PNGs. Transparent PNGs are not
+# supported properly for IE 6.0, but are supported on all modern browsers.
+#
+# Note that when changing this option you need to delete any form_*.png files in
+# the HTML output directory before the changes have effect.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+FORMULA_TRANSPARENT    = YES
+
+# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands
+# to create new LaTeX commands to be used in formulas as building blocks. See
+# the section "Including formulas" for details.
+
+FORMULA_MACROFILE      =
+
+# Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see
+# https://www.mathjax.org) which uses client side JavaScript for the rendering
+# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX
+# installed or if you want to formulas look prettier in the HTML output. When
+# enabled you may also need to install MathJax separately and configure the path
+# to it using the MATHJAX_RELPATH option.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+USE_MATHJAX            = NO
+
+# When MathJax is enabled you can set the default output format to be used for
+# the MathJax output. See the MathJax site (see:
+# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details.
+# Possible values are: HTML-CSS (which is slower, but has the best
+# compatibility), NativeMML (i.e. MathML) and SVG.
+# The default value is: HTML-CSS.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_FORMAT         = HTML-CSS
+
+# When MathJax is enabled you need to specify the location relative to the HTML
+# output directory using the MATHJAX_RELPATH option. The destination directory
+# should contain the MathJax.js script. For instance, if the mathjax directory
+# is located at the same level as the HTML output directory, then
+# MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax
+# Content Delivery Network so you can quickly see the result without installing
+# MathJax. However, it is strongly recommended to install a local copy of
+# MathJax from https://www.mathjax.org before deployment.
+# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_RELPATH        = https://cdn.jsdelivr.net/npm/mathjax@2
+
+# The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax
+# extension names that should be enabled during MathJax rendering. For example
+# MATHJAX_EXTENSIONS = TeX/AMSmath TeX/AMSsymbols
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_EXTENSIONS     =
+
+# The MATHJAX_CODEFILE tag can be used to specify a file with javascript pieces
+# of code that will be used on startup of the MathJax code. See the MathJax site
+# (see:
+# http://docs.mathjax.org/en/v2.7-latest/output.html) for more details. For an
+# example see the documentation.
+# This tag requires that the tag USE_MATHJAX is set to YES.
+
+MATHJAX_CODEFILE       =
+
+# When the SEARCHENGINE tag is enabled doxygen will generate a search box for
+# the HTML output. The underlying search engine uses javascript and DHTML and
+# should work on any modern browser. Note that when using HTML help
+# (GENERATE_HTMLHELP), Qt help (GENERATE_QHP), or docsets (GENERATE_DOCSET)
+# there is already a search function so this one should typically be disabled.
+# For large projects the javascript based search engine can be slow, then
+# enabling SERVER_BASED_SEARCH may provide a better solution. It is possible to
+# search using the keyboard; to jump to the search box use <access key> + S
+# (what the <access key> is depends on the OS and browser, but it is typically
+# <CTRL>, <ALT>/<option>, or both). Inside the search box use the <cursor down
+# key> to jump into the search results window, the results can be navigated
+# using the <cursor keys>. Press <Enter> to select an item or <escape> to cancel
+# the search. The filter options can be selected when the cursor is inside the
+# search box by pressing <Shift>+<cursor down>. Also here use the <cursor keys>
+# to select a filter and <Enter> or <escape> to activate or cancel the filter
+# option.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_HTML is set to YES.
+
+SEARCHENGINE           = YES
+
+# When the SERVER_BASED_SEARCH tag is enabled the search engine will be
+# implemented using a web server instead of a web client using JavaScript. There
+# are two flavors of web server based searching depending on the EXTERNAL_SEARCH
+# setting. When disabled, doxygen will generate a PHP script for searching and
+# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing
+# and searching needs to be provided by external tools. See the section
+# "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SERVER_BASED_SEARCH    = NO
+
+# When EXTERNAL_SEARCH tag is enabled doxygen will no longer generate the PHP
+# script for searching. Instead the search results are written to an XML file
+# which needs to be processed by an external indexer. Doxygen will invoke an
+# external search engine pointed to by the SEARCHENGINE_URL option to obtain the
+# search results.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see:
+# https://xapian.org/).
+#
+# See the section "External Indexing and Searching" for details.
+# The default value is: NO.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH        = NO
+
+# The SEARCHENGINE_URL should point to a search engine hosted by a web server
+# which will return the search results when EXTERNAL_SEARCH is enabled.
+#
+# Doxygen ships with an example indexer (doxyindexer) and search engine
+# (doxysearch.cgi) which are based on the open source search engine library
+# Xapian (see:
+# https://xapian.org/). See the section "External Indexing and Searching" for
+# details.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHENGINE_URL       =
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the unindexed
+# search data is written to a file for indexing by an external tool. With the
+# SEARCHDATA_FILE tag the name of this file can be specified.
+# The default file is: searchdata.xml.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+SEARCHDATA_FILE        = searchdata.xml
+
+# When SERVER_BASED_SEARCH and EXTERNAL_SEARCH are both enabled the
+# EXTERNAL_SEARCH_ID tag can be used as an identifier for the project. This is
+# useful in combination with EXTRA_SEARCH_MAPPINGS to search through multiple
+# projects and redirect the results back to the right project.
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTERNAL_SEARCH_ID     =
+
+# The EXTRA_SEARCH_MAPPINGS tag can be used to enable searching through doxygen
+# projects other than the one defined by this configuration file, but that are
+# all added to the same external search index. Each project needs to have a
+# unique id set via EXTERNAL_SEARCH_ID. The search mapping then maps the id of
+# to a relative location where the documentation can be found. The format is:
+# EXTRA_SEARCH_MAPPINGS = tagname1=loc1 tagname2=loc2 ...
+# This tag requires that the tag SEARCHENGINE is set to YES.
+
+EXTRA_SEARCH_MAPPINGS  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output.
+# The default value is: YES.
+
+GENERATE_LATEX         = NO
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: latex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be
+# invoked.
+#
+# Note that when not enabling USE_PDFLATEX the default is latex when enabling
+# USE_PDFLATEX the default is pdflatex and when in the later case latex is
+# chosen this is overwritten by pdflatex. For specific output languages the
+# default can have been set differently, this depends on the implementation of
+# the output language.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate
+# index for LaTeX.
+# Note: This tag is used in the Makefile / make.bat.
+# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file
+# (.tex).
+# The default file is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+MAKEINDEX_CMD_NAME     = makeindex
+
+# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to
+# generate index for LaTeX. In case there is no backslash (\) as first character
+# it will be automatically added in the LaTeX code.
+# Note: This tag is used in the generated output file (.tex).
+# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat.
+# The default value is: makeindex.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_MAKEINDEX_CMD    = makeindex
+
+# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+COMPACT_LATEX          = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used by the
+# printer.
+# Possible values are: a4 (210 x 297 mm), letter (8.5 x 11 inches), legal (8.5 x
+# 14 inches) and executive (7.25 x 10.5 inches).
+# The default value is: a4.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PAPER_TYPE             = a4wide
+
+# The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names
+# that should be included in the LaTeX output. The package can be specified just
+# by its name or with the correct syntax as to be used with the LaTeX
+# \usepackage command. To get the times font for instance you can specify :
+# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times}
+# To use the option intlimits with the amsmath package you can specify:
+# EXTRA_PACKAGES=[intlimits]{amsmath}
+# If left blank no extra packages will be included.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+EXTRA_PACKAGES         =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for the
+# generated LaTeX document. The header should contain everything until the first
+# chapter. If it is left blank doxygen will generate a standard header. See
+# section "Doxygen usage" for information on how to let doxygen write the
+# default header to a separate file.
+#
+# Note: Only use a user-defined header if you know what you are doing! The
+# following commands have a special meaning inside the header: $title,
+# $datetime, $date, $doxygenversion, $projectname, $projectnumber,
+# $projectbrief, $projectlogo. Doxygen will replace $title with the empty
+# string, for the replacement values of the other commands the user is referred
+# to HTML_HEADER.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HEADER           =
+
+# The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the
+# generated LaTeX document. The footer should contain everything after the last
+# chapter. If it is left blank doxygen will generate a standard footer. See
+# LATEX_HEADER for more information on how to generate a default footer and what
+# special commands can be used inside the footer.
+#
+# Note: Only use a user-defined footer if you know what you are doing!
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_FOOTER           =
+
+# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined
+# LaTeX style sheets that are included after the standard style sheets created
+# by doxygen. Using this option one can overrule certain style aspects. Doxygen
+# will copy the style sheet files to the output directory.
+# Note: The order of the extra style sheet files is of importance (e.g. the last
+# style sheet in the list overrules the setting of the previous ones in the
+# list).
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_STYLESHEET =
+
+# The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or
+# other source files which should be copied to the LATEX_OUTPUT output
+# directory. Note that the files will be copied as-is; there are no commands or
+# markers available.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EXTRA_FILES      =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated is
+# prepared for conversion to PDF (using ps2pdf or pdflatex). The PDF file will
+# contain links (just like the HTML output) instead of page references. This
+# makes the output suitable for online browsing using a PDF viewer.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+PDF_HYPERLINKS         = YES
+
+# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as
+# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX
+# files. Set this option to YES, to get a higher quality PDF documentation.
+#
+# See also section LATEX_CMD_NAME for selecting the engine.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+USE_PDFLATEX           = YES
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode
+# command to the generated LaTeX files. This will instruct LaTeX to keep running
+# if errors occur, instead of asking the user for help. This option is also used
+# when generating formulas in HTML.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BATCHMODE        = NO
+
+# If the LATEX_HIDE_INDICES tag is set to YES then doxygen will not include the
+# index chapters (such as File Index, Compound Index, etc.) in the output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_HIDE_INDICES     = NO
+
+# If the LATEX_SOURCE_CODE tag is set to YES then doxygen will include source
+# code with syntax highlighting in the LaTeX output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_SOURCE_CODE      = NO
+
+# The LATEX_BIB_STYLE tag can be used to specify the style to use for the
+# bibliography, e.g. plainnat, or ieeetr. See
+# https://en.wikipedia.org/wiki/BibTeX and \cite for more info.
+# The default value is: plain.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_BIB_STYLE        = plain
+
+# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated
+# page will contain the date and time when the page was generated. Setting this
+# to NO can help when comparing the output of multiple runs.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_TIMESTAMP        = NO
+
+# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute)
+# path from which the emoji images will be read. If a relative path is entered,
+# it will be relative to the LATEX_OUTPUT directory. If left blank the
+# LATEX_OUTPUT directory will be used.
+# This tag requires that the tag GENERATE_LATEX is set to YES.
+
+LATEX_EMOJI_DIRECTORY  =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The
+# RTF output is optimized for Word 97 and may not look too pretty with other RTF
+# readers/editors.
+# The default value is: NO.
+
+GENERATE_RTF           = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: rtf.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_OUTPUT             = rtf
+
+# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF
+# documents. This may be useful for small projects and may help to save some
+# trees in general.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+COMPACT_RTF            = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated will
+# contain hyperlink fields. The RTF file will contain links (just like the HTML
+# output) instead of page references. This makes the output suitable for online
+# browsing using Word or some other Word compatible readers that support those
+# fields.
+#
+# Note: WordPad (write) and others do not support links.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's
+# configuration file, i.e. a series of assignments. You only have to provide
+# replacements, missing definitions are set to their default value.
+#
+# See also section "Doxygen usage" for information on how to generate the
+# default style sheet that doxygen normally uses.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_STYLESHEET_FILE    =
+
+# Set optional variables used in the generation of an RTF document. Syntax is
+# similar to doxygen's configuration file. A template extensions file can be
+# generated using doxygen -e rtf extensionFile.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_EXTENSIONS_FILE    =
+
+# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code
+# with syntax highlighting in the RTF output.
+#
+# Note that which sources are shown also depends on other settings such as
+# SOURCE_BROWSER.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_RTF is set to YES.
+
+RTF_SOURCE_CODE        = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for
+# classes and files.
+# The default value is: NO.
+
+GENERATE_MAN           = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it. A directory man3 will be created inside the directory specified by
+# MAN_OUTPUT.
+# The default directory is: man.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to the generated
+# man pages. In case the manual section does not start with a number, the number
+# 3 is prepended. The dot (.) at the beginning of the MAN_EXTENSION tag is
+# optional.
+# The default value is: .3.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_EXTENSION          = .3
+
+# The MAN_SUBDIR tag determines the name of the directory created within
+# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by
+# MAN_EXTENSION with the initial . removed.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_SUBDIR             =
+
+# If the MAN_LINKS tag is set to YES and doxygen generates man output, then it
+# will generate one additional man file for each entity documented in the real
+# man page(s). These additional files only source the real man page, but without
+# them the man command would be unable to find the correct page.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_MAN is set to YES.
+
+MAN_LINKS              = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that
+# captures the structure of the code including all documentation.
+# The default value is: NO.
+
+GENERATE_XML           = NO
+
+# The XML_OUTPUT tag is used to specify where the XML pages will be put. If a
+# relative path is entered the value of OUTPUT_DIRECTORY will be put in front of
+# it.
+# The default directory is: xml.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_OUTPUT             = xml
+
+# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program
+# listings (including syntax highlighting and cross-referencing information) to
+# the XML output. Note that enabling this will significantly increase the size
+# of the XML output.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_PROGRAMLISTING     = YES
+
+# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include
+# namespace members in file scope as well, matching the HTML output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_XML is set to YES.
+
+XML_NS_MEMB_FILE_SCOPE = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the DOCBOOK output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files
+# that can be used to generate PDF.
+# The default value is: NO.
+
+GENERATE_DOCBOOK       = NO
+
+# The DOCBOOK_OUTPUT tag is used to specify where the Docbook pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be put in
+# front of it.
+# The default directory is: docbook.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_OUTPUT         = docbook
+
+# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the
+# program listings (including syntax highlighting and cross-referencing
+# information) to the DOCBOOK output. Note that enabling this will significantly
+# increase the size of the DOCBOOK output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_DOCBOOK is set to YES.
+
+DOCBOOK_PROGRAMLISTING = NO
+
+#---------------------------------------------------------------------------
+# Configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an
+# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures
+# the structure of the code including all documentation. Note that this feature
+# is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the Perl module output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module
+# file that captures the structure of the code including all documentation.
+#
+# Note that this feature is still experimental and incomplete at the moment.
+# The default value is: NO.
+
+GENERATE_PERLMOD       = NO
+
+# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary
+# Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI
+# output from the Perl module output.
+# The default value is: NO.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_LATEX          = NO
+
+# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely
+# formatted so it can be parsed by a human reader. This is useful if you want to
+# understand what is going on. On the other hand, if this tag is set to NO, the
+# size of the Perl module output will be much smaller and Perl will parse it
+# just the same.
+# The default value is: YES.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_PRETTY         = YES
+
+# The names of the make variables in the generated doxyrules.make file are
+# prefixed with the string contained in PERLMOD_MAKEVAR_PREFIX. This is useful
+# so different doxyrules.make files included by the same Makefile don't
+# overwrite each other's variables.
+# This tag requires that the tag GENERATE_PERLMOD is set to YES.
+
+PERLMOD_MAKEVAR_PREFIX =
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all
+# C-preprocessor directives found in the sources and include files.
+# The default value is: YES.
+
+ENABLE_PREPROCESSING   = YES
+
+# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names
+# in the source code. If set to NO, only conditional compilation will be
+# performed. Macro expansion can be done in a controlled way by setting
+# EXPAND_ONLY_PREDEF to YES.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+MACRO_EXPANSION        = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES then
+# the macro expansion is limited to the macros specified with the PREDEFINED and
+# EXPAND_AS_DEFINED tags.
+# The default value is: NO.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES, the include files in the
+# INCLUDE_PATH will be searched if a #include is found.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SEARCH_INCLUDES        = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by the
+# preprocessor.
+# This tag requires that the tag SEARCH_INCLUDES is set to YES.
+
+INCLUDE_PATH           =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will be
+# used.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+INCLUDE_FILE_PATTERNS  =
+
+# The PREDEFINED tag can be used to specify one or more macro names that are
+# defined before the preprocessor is started (similar to the -D option of e.g.
+# gcc). The argument of the tag is a list of macros of the form: name or
+# name=definition (no spaces). If the definition and the "=" are omitted, "=1"
+# is assumed. To prevent a macro definition from being undefined via #undef or
+# recursively expanded use the := operator instead of the = operator.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+PREDEFINED             =
+
+# If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this
+# tag can be used to specify a list of macro names that should be expanded. The
+# macro definition that is found in the sources will be used. Use the PREDEFINED
+# tag if you want to use a different macro definition that overrules the
+# definition found in the source code.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+EXPAND_AS_DEFINED      =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will
+# remove all references to function-like macros that are alone on a line, have
+# an all uppercase name, and do not end with a semicolon. Such function macros
+# are typically used for boiler-plate code, and will confuse the parser if not
+# removed.
+# The default value is: YES.
+# This tag requires that the tag ENABLE_PREPROCESSING is set to YES.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tag files. For each tag
+# file the location of the external documentation should be added. The format of
+# a tag file without this location is as follows:
+# TAGFILES = file1 file2 ...
+# Adding location for the tag files is done as follows:
+# TAGFILES = file1=loc1 "file2 = loc2" ...
+# where loc1 and loc2 can be relative or absolute paths or URLs. See the
+# section "Linking to external documentation" for more information about the use
+# of tag files.
+# Note: Each tag file must have a unique name (where the name does NOT include
+# the path). If a tag file is not located in the directory in which doxygen is
+# run, you must also specify the path to the tagfile here.
+
+TAGFILES               =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create a
+# tag file that is based on the input files it reads. See section "Linking to
+# external documentation" for more information about the usage of tag files.
+
+GENERATE_TAGFILE       =
+
+# If the ALLEXTERNALS tag is set to YES, all external class will be listed in
+# the class index. If set to NO, only the inherited external classes will be
+# listed.
+# The default value is: NO.
+
+ALLEXTERNALS           = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will be
+# listed.
+# The default value is: YES.
+
+EXTERNAL_GROUPS        = YES
+
+# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in
+# the related pages index. If set to NO, only the current project's pages will
+# be listed.
+# The default value is: YES.
+
+EXTERNAL_PAGES         = YES
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram
+# (in HTML and LaTeX) for classes with base or super classes. Setting the tag to
+# NO turns the diagrams off. Note that this option also works with HAVE_DOT
+# disabled, but it is recommended to install and use dot, since it yields more
+# powerful graphs.
+# The default value is: YES.
+
+CLASS_DIAGRAMS         = YES
+
+# You can include diagrams made with dia in doxygen documentation. Doxygen will
+# then run dia to produce the diagram and insert it in the documentation. The
+# DIA_PATH tag allows you to specify the directory where the dia binary resides.
+# If left empty dia is assumed to be found in the default search path.
+
+DIA_PATH               =
+
+# If set to YES the inheritance and collaboration graphs will hide inheritance
+# and usage relations if the target is undocumented or is not a class.
+# The default value is: YES.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz (see:
+# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent
+# Bell Labs. The other options in this section have no effect if this option is
+# set to NO
+# The default value is: NO.
+
+HAVE_DOT               = YES
+
+# The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed
+# to run in parallel. When set to 0 doxygen will base this on the number of
+# processors available in the system. You can set it explicitly to a value
+# larger than 0 to get control over the balance between CPU load and processing
+# speed.
+# Minimum value: 0, maximum value: 32, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_NUM_THREADS        = 0
+
+# When you want a differently looking font in the dot files that doxygen
+# generates you can specify the font name using DOT_FONTNAME. You need to make
+# sure dot is able to find the font, which can be done by putting it in a
+# standard location or by setting the DOTFONTPATH environment variable or by
+# setting DOT_FONTPATH to the directory containing the font.
+# The default value is: Helvetica.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTNAME           = Sans
+
+# The DOT_FONTSIZE tag can be used to set the size (in points) of the font of
+# dot graphs.
+# Minimum value: 4, maximum value: 24, default value: 10.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTSIZE           = 10
+
+# By default doxygen will tell dot to use the default font as specified with
+# DOT_FONTNAME. If you specify a different font using DOT_FONTNAME you can set
+# the path where dot can find it using this tag.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_FONTPATH           =
+
+# If the CLASS_GRAPH tag is set to YES then doxygen will generate a graph for
+# each documented class showing the direct and indirect inheritance relations.
+# Setting this tag to YES will force the CLASS_DIAGRAMS tag to NO.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a
+# graph for each documented class showing the direct and indirect implementation
+# dependencies (inheritance, containment, and class references variables) of the
+# class with other documented classes.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+COLLABORATION_GRAPH    = YES
+
+# If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for
+# groups, showing the direct groups dependencies.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GROUP_GRAPHS           = YES
+
+# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and
+# collaboration diagrams in a style similar to the OMG's Unified Modeling
+# Language.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+UML_LOOK               = NO
+
+# If the UML_LOOK tag is enabled, the fields and methods are shown inside the
+# class node. If there are many fields or methods and many nodes the graph may
+# become too big to be useful. The UML_LIMIT_NUM_FIELDS threshold limits the
+# number of items for each type to make the size more manageable. Set this to 0
+# for no limit. Note that the threshold may be exceeded by 50% before the limit
+# is enforced. So when you set the threshold to 10, up to 15 fields may appear,
+# but if the number exceeds 15, the total amount of fields shown is limited to
+# 10.
+# Minimum value: 0, maximum value: 100, default value: 10.
+# This tag requires that the tag UML_LOOK is set to YES.
+
+UML_LIMIT_NUM_FIELDS   = 10
+
+# If the DOT_UML_DETAILS tag is set to NO, doxygen will show attributes and
+# methods without types and arguments in the UML graphs. If the DOT_UML_DETAILS
+# tag is set to YES, doxygen will add type and arguments for attributes and
+# methods in the UML graphs. If the DOT_UML_DETAILS tag is set to NONE, doxygen
+# will not generate fields with class member information in the UML graphs. The
+# class diagrams will look similar to the default class diagrams but using UML
+# notation for the relationships.
+# Possible values are: NO, YES and NONE.
+# The default value is: NO.
+# This tag requires that the tag UML_LOOK is set to YES.
+
+DOT_UML_DETAILS        = NO
+
+# The DOT_WRAP_THRESHOLD tag can be used to set the maximum number of characters
+# to display on a single line. If the actual line length exceeds this threshold
+# significantly it will wrapped across multiple lines. Some heuristics are apply
+# to avoid ugly line breaks.
+# Minimum value: 0, maximum value: 1000, default value: 17.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_WRAP_THRESHOLD     = 17
+
+# If the TEMPLATE_RELATIONS tag is set to YES then the inheritance and
+# collaboration graphs will show the relations between templates and their
+# instances.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+TEMPLATE_RELATIONS     = NO
+
+# If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to
+# YES then doxygen will generate a graph for each documented file showing the
+# direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDE_GRAPH          = YES
+
+# If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are
+# set to YES then doxygen will generate a graph for each documented file showing
+# the direct and indirect include dependencies of the file with other documented
+# files.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the CALL_GRAPH tag is set to YES then doxygen will generate a call
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable call graphs for selected
+# functions only using the \callgraph command. Disabling a call graph can be
+# accomplished by means of the command \hidecallgraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALL_GRAPH             = NO
+
+# If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller
+# dependency graph for every global function or class method.
+#
+# Note that enabling this option will significantly increase the time of a run.
+# So in most cases it will be better to enable caller graphs for selected
+# functions only using the \callergraph command. Disabling a caller graph can be
+# accomplished by means of the command \hidecallergraph.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+CALLER_GRAPH           = NO
+
+# If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical
+# hierarchy of all classes instead of a textual one.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the
+# dependencies a directory has on other directories in a graphical way. The
+# dependency relations are determined by the #include relations between the
+# files in the directories.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DIRECTORY_GRAPH        = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. For an explanation of the image formats see the section
+# output formats in the documentation of the dot tool (Graphviz (see:
+# http://www.graphviz.org/)).
+# Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order
+# to make the SVG files visible in IE 9+ (other browsers do not have this
+# requirement).
+# Possible values are: png, jpg, gif, svg, png:gd, png:gd:gd, png:cairo,
+# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and
+# png:gdiplus:gdiplus.
+# The default value is: png.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_IMAGE_FORMAT       = svg
+
+# If DOT_IMAGE_FORMAT is set to svg, then this option can be set to YES to
+# enable generation of interactive SVG images that allow zooming and panning.
+#
+# Note that this requires a modern browser other than Internet Explorer. Tested
+# and working are Firefox, Chrome, Safari, and Opera.
+# Note: For IE 9+ you need to set HTML_FILE_EXTENSION to xhtml in order to make
+# the SVG files visible. Older versions of IE do not have SVG support.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+INTERACTIVE_SVG        = YES
+
+# The DOT_PATH tag can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found in the path.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_PATH               =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the \dotfile
+# command).
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOTFILE_DIRS           =
+
+# The MSCFILE_DIRS tag can be used to specify one or more directories that
+# contain msc files that are included in the documentation (see the \mscfile
+# command).
+
+MSCFILE_DIRS           =
+
+# The DIAFILE_DIRS tag can be used to specify one or more directories that
+# contain dia files that are included in the documentation (see the \diafile
+# command).
+
+DIAFILE_DIRS           =
+
+# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the
+# path where java can find the plantuml.jar file. If left blank, it is assumed
+# PlantUML is not used or called during a preprocessing step. Doxygen will
+# generate a warning when it encounters a \startuml command in this case and
+# will not generate output for the diagram.
+
+PLANTUML_JAR_PATH      =
+
+# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a
+# configuration file for plantuml.
+
+PLANTUML_CFG_FILE      =
+
+# When using plantuml, the specified paths are searched for files specified by
+# the !include statement in a plantuml block.
+
+PLANTUML_INCLUDE_PATH  =
+
+# The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes
+# that will be shown in the graph. If the number of nodes in a graph becomes
+# larger than this value, doxygen will truncate the graph, which is visualized
+# by representing a node as a red box. Note that doxygen if the number of direct
+# children of the root node in a graph is already larger than
+# DOT_GRAPH_MAX_NODES then the graph will not be shown at all. Also note that
+# the size of a graph can be further restricted by MAX_DOT_GRAPH_DEPTH.
+# Minimum value: 0, maximum value: 10000, default value: 50.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_GRAPH_MAX_NODES    = 50
+
+# The MAX_DOT_GRAPH_DEPTH tag can be used to set the maximum depth of the graphs
+# generated by dot. A depth value of 3 means that only nodes reachable from the
+# root by following a path via at most 3 edges will be shown. Nodes that lay
+# further from the root node will be omitted. Note that setting this option to 1
+# or 2 may greatly reduce the computation time needed for large code bases. Also
+# note that the size of a graph can be further restricted by
+# DOT_GRAPH_MAX_NODES. Using a depth of 0 means no depth restriction.
+# Minimum value: 0, maximum value: 1000, default value: 0.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+MAX_DOT_GRAPH_DEPTH    = 0
+
+# Set the DOT_TRANSPARENT tag to YES to generate images with a transparent
+# background. This is disabled by default, because dot on Windows does not seem
+# to support this out of the box.
+#
+# Warning: Depending on the platform used, enabling this option may lead to
+# badly anti-aliased labels on the edges of a graph (i.e. they become hard to
+# read).
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_TRANSPARENT        = YES
+
+# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output
+# files in one run (i.e. multiple -o and -T options on the command line). This
+# makes dot run faster, but since only newer versions of dot (>1.8.10) support
+# this, this feature is disabled by default.
+# The default value is: NO.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+DOT_MULTI_TARGETS      = NO
+
+# If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page
+# explaining the meaning of the various boxes and arrows in the dot generated
+# graphs.
+# The default value is: YES.
+# This tag requires that the tag HAVE_DOT is set to YES.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate
+# files that are used to generate the various graphs.
+#
+# Note: This setting is not only used for dot files but also for msc and
+# plantuml temporary files.
+# The default value is: YES.
+
+DOT_CLEANUP            = YES
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644 (file)
index 0000000..0169495
--- /dev/null
@@ -0,0 +1,27 @@
+# JackTrip License
+
+Copyright © 2008-2020 Juan-Pablo Caceres, Chris Chafe.
+SoundWIRE group at CCRMA, Stanford University.
+
+Classic mode graphical user interface originally released as QJackTrip,
+Copyright © 2020 Aaron Wyatt
+
+Virtual Studio interface and integration
+Copyright © 2022-2023 JackTrip Labs, Inc.
+
+JackTrip project consists of files under MIT and GPL licenses, indicated in the
+header of individual files. Early versions of JackTrip were licensed under MIT.
+
+JackTrip uses Qt library throughout the project so the resulting binaries are
+also subject to Qt's license. The builds provided on GitHub's Releases page use
+open source distribution of Qt, licensed under LGPL.
+
+Windows builds of JackTrip may include support for ASIO.
+ASIO is a trademark and software of Steinberg Media Technologies GmbH.
+
+Using JackTrip to join Virtual Studios on Windows computers may use AVC (h264)
+video encoders and decoders subject to the AVC Patent Portfolio License.
+
+The text of individual licenses is provided in the `LICENSES/` folder. Qt's
+source code can be downloaded from
+[https://download.qt.io/official_releases/qt/](https://download.qt.io/official_releases/qt/).
diff --git a/LICENSES/AVC.txt b/LICENSES/AVC.txt
new file mode 100644 (file)
index 0000000..66cff0e
--- /dev/null
@@ -0,0 +1,10 @@
+THIS PRODUCT IS LICENSED UNDER THE AVC PATENT PORTFOLIO
+LICENSE FOR THE PERSONAL USE OF A CONSUMER OR OTHER USES
+IN WHICH IT DOES NOT RECEIVE REMUNERATION TO (i) ENCODE
+VIDEO IN COMPLIANCE WITH THE AVC STANDARD (“AVC VIDEO”)
+AND/OR (ii) DECODE AVC VIDEO THAT WAS ENCODED BY A
+CONSUMER ENGAGED IN A PERSONAL ACTIVITY AND/OR WAS
+OBTAINED FROM A VIDEO PROVIDER LICENSED TO PROVIDE AVC
+VIDEO. NO LICENSE IS GRANTED OR SHALL BE IMPLIED FOR
+ANY OTHER USE. ADDITIONAL INFORMATION MAY BE OBTAINED
+FROM MPEG LA, L.L.C. SEE HTTP://WWW.MPEGLA.COM
\ No newline at end of file
diff --git a/LICENSES/GPL-3.0.txt b/LICENSES/GPL-3.0.txt
new file mode 100644 (file)
index 0000000..f288702
--- /dev/null
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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 3 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, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/LICENSES/LGPL-3.0-only.txt b/LICENSES/LGPL-3.0-only.txt
new file mode 100644 (file)
index 0000000..0a04128
--- /dev/null
@@ -0,0 +1,165 @@
+                   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt
new file mode 100644 (file)
index 0000000..a843d97
--- /dev/null
@@ -0,0 +1,23 @@
+  Copyright (c) 2020 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.
diff --git a/QJackTrip.kdev4 b/QJackTrip.kdev4
new file mode 100644 (file)
index 0000000..ec8f93a
--- /dev/null
@@ -0,0 +1,3 @@
+[Project]
+Name=QJackTrip
+Manager=KDevCMakeManager
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..f508d91
--- /dev/null
+++ b/README.md
@@ -0,0 +1,9 @@
+# JackTrip is a multi-machine audio system used for network music performance over the Internet.
+
+It supports any number of channels (as many as the computer/network can handle) of bidirectional, high quality, uncompressed audio signal streaming.
+
+It runs on several platforms, such as Linux, macOS, Windows or FreeBSD. You can use it between any combination of machines e.g., one end using Linux can connect to another using macOS.
+
+Further information and instructions are available on https://jacktrip.github.io/jacktrip/. 
+
+Please report any security concerns to vulnerabilities@jacktrip.org
diff --git a/build b/build
new file mode 100755 (executable)
index 0000000..351873e
--- /dev/null
+++ b/build
@@ -0,0 +1,204 @@
+#!/bin/bash
+## Created by Juan-Pablo Caceres
+
+# Parse command line options
+clean=1
+install=0
+jobs=1
+CONFIG="DEFINES+=JACKTRIP_BUILD_INFO=\\\"$(git describe --tags)-$(git rev-parse --short HEAD)\\\""
+UNKNOWN_OPTIONS=()
+BUILD_RTAUDIO=0
+RTAUDIO=0
+NO_SYSTEM_RTAUDIO=0
+PRO_FILE="../jacktrip.pro"
+HELP_STR="usage:\n
+./build [noclean nojack rtaudio nogui novs vsftux static install [-config static]]\n\n
+options:\n
+noclean - do not run \"make clean\" first\n
+nojack - build without jack\n
+rtaudio - build with RtAudio\n
+no-system-rtaudio - use bundled RtAudio library even if it's available in the system\n
+nogui - build without the gui\n
+novs - build without Virtual Studio support\n
+vsftux - build with Virtual Studio first launch experience\n
+noupdater - build without auto-update support\n
+static - build with static libraries\n
+weakjack - build with weak linking of jack libraries\n
+install - install jacktrip in system location (uses sudo)\n
+"
+while [[ "$#" -gt 0 ]]; do
+  if [[ ${#UNKNOWN_OPTIONS[@]} -eq 0 ]]; then
+    case $1 in
+      noclean) clean=0 ;;
+      nojack)
+      echo "Building without jack"
+      CONFIG="-config nojack $CONFIG" 
+      ;;
+      rtaudio) 
+      RTAUDIO=1
+      ;;
+      no-system-rtaudio) 
+      NO_SYSTEM_RTAUDIO=1
+      ;;
+      nogui)
+      echo "Building without the gui"
+      CONFIG="-config nogui $CONFIG" 
+      ;;
+      novs)
+      echo "Building without Virtual Studio support"
+      CONFIG="-config novs $CONFIG" 
+      ;;
+      vsftux)
+      echo "Building with Virtual Studio first launch experience"
+      CONFIG="-config vsftux $CONFIG" 
+      ;;
+      noupdater)
+      echo "Building without auto-update support"
+      CONFIG="-config noupdater $CONFIG"
+      ;;
+      static)
+      echo "Building with static libraries"
+      CONFIG="-config static $CONFIG" 
+      ;;
+      weakjack)
+      echo "Building with weak linking of jack"
+      CONFIG="-config weakjack $CONFIG"
+      ;;
+      install)
+      echo "Will install JackTrip in system location"
+      install=1
+      ;;
+      -j*)
+      jobs=$(echo $1 |sed 's,-j0*\([0-9]*\),\1,')
+      if [[ $jobs -le 1 ]]; then
+        jobs=1
+      fi
+      echo "Will build using $jobs make jobs"
+      ;;
+      -h|--help) 
+      echo -e $HELP_STR; exit 
+      ;;
+      *) UNKNOWN_OPTIONS+=("$1") ;;
+    esac
+    shift
+  else
+    case $1 in
+      *) UNKNOWN_OPTIONS+=("$1") ;;
+    esac
+    shift
+  fi
+done
+
+echo "All build options:"
+echo "$CONFIG"
+
+# Check for Platform
+platform='unknown'
+unamestr=`uname`
+if [[ "$unamestr" == 'Linux' ]]; then
+  echo "Building on Linux"
+  platform='linux'
+elif [[ "$unamestr" == 'Darwin' ]]; then
+  echo "Building on macOS"
+  platform='macosx'
+elif [[ "$unamestr" == 'MINGW'* ]]; then
+  echo "Building on Windows (MinGW)"
+  platform='mingw'
+fi
+
+# Set qmake command name
+if [[ $platform == 'linux' ]]; then
+  if hash qmake-qt5 2>/dev/null; then
+    echo "Using qmake-qt5"
+    QCMD=qmake-qt5
+  elif hash qmake 2>/dev/null; then #in case qt was compiled by user
+    echo "Using qmake"
+    QCMD=qmake
+  fi
+  MCMD=make
+elif [[ $platform == 'macosx' ]]; then
+  QCMD=qmake
+  # if qmake is not in path, try homebrew qt5
+  echo "path to qmake"
+  echo "$(which qmake)"
+  if ! command -v $QCMD &> /dev/null; then
+    # echo "qmake not found in \$PATH, searching for homebrew qt5"
+    QT_PREFIX=`brew --prefix qt5`
+    if [[ -n $QT_PREFIX ]]; then
+      QCMD="$QT_PREFIX/bin/$QCMD"
+      echo "Using qmake at $QCMD"
+    else
+      echo "Homebrew installation of qt5 not found"
+      exit
+    fi
+  fi
+  MCMD=make
+elif [[ $platform == 'mingw' ]]; then
+  QCMD=qmake
+  MCMD=mingw32-make
+elif [[ $platform == 'unknown' ]]; then
+  echo "Unregonized platform, exiting"
+  exit
+fi
+
+# detect spec
+QSPEC=`$QCMD -query | grep QMAKE_SPEC`
+QSPEC=${QSPEC##QMAKE_SPEC:}
+
+# check spec for windows to update make command
+if [[ $QSPEC == "win32-msvc" ]]; then
+  MCMD=nmake
+  JOM=$(which jom.exe)
+  if [[ "x$JOM" != "x" ]]; then
+    MCMD=$JOM
+  fi
+fi
+
+# check for RtAudio
+if [[ $RTAUDIO == 1 ]]; then
+  pkg-config --exists rtaudio 2> /dev/null
+  if [[ $? -eq 0 ]]; then
+    if [[ $NO_SYSTEM_RTAUDIO == 1 ]]; then
+      echo "RtAudio library found, but using the bundled library anyway"
+      BUILD_RTAUDIO=1
+    else
+      echo "Using system-provided RtAudio library"
+    fi
+  else
+    echo "Using bundled RtAudio library"
+    BUILD_RTAUDIO=1
+  fi
+fi
+
+if [[ $BUILD_RTAUDIO == 1 ]]; then
+  PRO_FILE="../jacktrip_and_rtaudio.pro";
+  CONFIG="-config bundled_rtaudio $CONFIG"
+elif [[ $RTAUDIO == 1 ]]; then
+  echo "Building with RtAudio"
+  CONFIG="-config rtaudio $CONFIG"
+fi
+
+# Create our build directory
+mkdir -p builddir
+cd builddir
+
+# exit if build commands fail
+set -e
+
+# Build
+echo "qmake command:"
+echo $QCMD -spec $QSPEC $CONFIG "${UNKNOWN_OPTIONS[@]}" $PRO_FILE
+$QCMD -spec $QSPEC $CONFIG "${UNKNOWN_OPTIONS[@]}" $PRO_FILE
+if [[ $clean == 1 ]]; then
+  $MCMD clean
+fi
+if [[ "$MCMD" == "nmake" ]]; then
+  $MCMD release
+else
+  $MCMD -j$jobs release
+fi
+if [[ "$install" == 1 ]]; then
+  echo "*** Installing JackTrip ***"
+  echo "We need elevated privileges to install JackTrip in the system location"
+  sudo $MCMD release-install
+fi
diff --git a/build-aux/flatpak/org.jacktrip.JackTrip.json b/build-aux/flatpak/org.jacktrip.JackTrip.json
new file mode 100644 (file)
index 0000000..e4348c0
--- /dev/null
@@ -0,0 +1,41 @@
+{
+    "app-id": "org.jacktrip.JackTrip",
+    "runtime": "org.kde.Platform",
+    "runtime-version": "6.6",
+    "sdk": "org.kde.Sdk",
+    "base": "io.qt.qtwebengine.BaseApp",
+    "base-version": "6.6",
+    "command": "jacktrip",
+    "finish-args": [
+        "--share=ipc",
+        "--socket=wayland",
+        "--socket=fallback-x11",
+        "--device=dri",
+        "--share=network",
+        "--filesystem=xdg-run/pipewire-0",
+        "--env=PIPEWIRE_LATENCY=128/48000",
+        "--env=QML_IMPORT_PATH=/app/qml",
+        "--env=QT_QUICK_CONTROLS_STYLE=universal"
+    ],
+    "cleanup": [
+        "/lib/python3.10",
+        "/share/man"
+    ],
+    "modules": [
+        "pypi-dependencies.json",
+        {
+            "name": "jacktrip",
+            "buildsystem": "meson",
+            "config-opts": [
+                "-Dbuildtype=debugoptimized",
+                "-Dpkg_config_path=/app/lib/x86_64-linux-gnu/pkgconfig"
+            ],
+            "sources": [
+                {
+                    "type": "dir",
+                    "path": "../../"
+                }
+            ]
+        }
+    ]
+}
diff --git a/build-aux/flatpak/pypi-dependencies.json b/build-aux/flatpak/pypi-dependencies.json
new file mode 100644 (file)
index 0000000..e148c9d
--- /dev/null
@@ -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 (file)
index 0000000..33afd92
--- /dev/null
@@ -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 (executable)
index 0000000..b67a299
--- /dev/null
@@ -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/cross/x86_mingw64.txt b/cross/x86_mingw64.txt
new file mode 100644 (file)
index 0000000..7a07b2c
--- /dev/null
@@ -0,0 +1,20 @@
+[host_machine]
+system = 'windows'
+cpu_family = 'x86_64'
+cpu = 'x86_64'
+endian = 'little'
+
+[binaries]
+exe_wrapper =  'wine64'
+c = 'x86_64-w64-mingw32-gcc'
+cpp = 'x86_64-w64-mingw32-g++'
+ar = 'x86_64-w64-mingw32-ar'
+ld = 'x86_64-w64-mingw32-ld'
+objcopy = 'x86_64-w64-mingw32-objcopy'
+strip = 'x86_64-w64-mingw32-strip'
+pkgconfig = 'x86_64-w64-mingw32-pkg-config'
+windres = 'x86_64-w64-mingw32-windres'
+
+[project options]
+rtaudio = 'enabled'
+rtaudio:wasapi = 'true'
diff --git a/cross/x86_mingw64_static.txt b/cross/x86_mingw64_static.txt
new file mode 100644 (file)
index 0000000..43cb85a
--- /dev/null
@@ -0,0 +1,24 @@
+[host_machine]
+system = 'windows'
+cpu_family = 'x86_64'
+cpu = 'x86_64'
+endian = 'little'
+
+[binaries]
+exe_wrapper =  'wine64'
+c = 'x86_64-w64-mingw32-gcc'
+cpp = 'x86_64-w64-mingw32-g++'
+ar = 'x86_64-w64-mingw32-ar'
+ld = 'x86_64-w64-mingw32-ld'
+objcopy = 'x86_64-w64-mingw32-objcopy'
+strip = 'x86_64-w64-mingw32-strip'
+pkgconfig = 'x86_64-w64-mingw32-pkg-config'
+windres = 'x86_64-w64-mingw32-windres'
+
+[built-in options]
+cpp_link_args = ['-static-libgcc', '-static-libstdc++']
+
+[project options]
+default_library = 'static'
+rtaudio = 'enabled'
+rtaudio:wasapi = 'true'
diff --git a/docs/About/CHANGELOG.md b/docs/About/CHANGELOG.md
new file mode 100644 (file)
index 0000000..e0ceba1
--- /dev/null
@@ -0,0 +1,10 @@
+Changelog
+=========
+
+{% for release in releases %}
+## {{ release.Version }} {% if release.Type == 'development' %}(Development){% endif %}
+
+{% for change in release.Description %}
+- {{ change }}
+{% endfor %}
+{% endfor %}
diff --git a/docs/About/Contributors.md b/docs/About/Contributors.md
new file mode 100644 (file)
index 0000000..c82541d
--- /dev/null
@@ -0,0 +1,93 @@
+# Contributors
+
+In order of first contribution
+
+## 2008
+
+??? note "Chris Chafe /// ***CCRMA, Stanford University*** /// ***JackTrip Foundation***"
+
+    **2008 - **
+
+??? note  "Juan-Pablo Caceres /// ***CCRMA, Stanford University***"
+
+    **2008 - 2018**
+
+## 2019
+
+??? note "Nils Tonnätt /// ***Audiocommunication, TU Berlin***"
+
+    **2019 - **
+
+    Feature Work
+
+    * Different number of input and output channels
+    * Append Thread ID to JACK client names
+    * Meson build
+    * RtAudio backend
+    * MkDocs based documentation
+    * Github Actions
+
+## 2020
+
+??? note "Aaron Wyatt /// ***Monash University***"
+
+    **2020 - **
+
+    Feature Work
+
+    * Graphical User Interface (QJackTrip)
+    * IPv6 support
+    * Authentication
+    * Installer Creation Scripts
+
+??? note "Marcin Pączkowski /// ***Composer***"
+
+    **2020 - **
+
+    Feature Work
+
+    * Github Actions
+
+## 2021
+
+??? note  "Olivier Humbert" 
+
+    **2021**
+
+    * French Translation Work
+
+## unknown
+
+??? note "Julius Smith /// ***CCRMA, Stanford University***"
+
+??? note "Bonnie Kwong"
+
+??? note "Mike Dickey /// ***JackTrip Foundation***"
+
+??? note  "Anton Runov"
+
+??? note "Maximilian Wagenbach"
+
+??? note "Spencer Salazar"
+
+??? note "Omar Costa Hamido"
+
+??? note "Bruno Ruviaro"
+
+??? note "Daniel Riechers"
+
+??? note "Jasper Mackenzie"
+
+??? note "Michael Dessen"
+
+??? note "Gerald Mwangi"
+
+??? note "James Borden"
+
+??? note "Nicolas Kaiser"
+
+??? note "Noah Horn"
+
+??? note "Tom Ward"
+
+??? note "Ronen Barzel"
diff --git a/docs/About/License.md b/docs/About/License.md
new file mode 100644 (file)
index 0000000..159323b
--- /dev/null
@@ -0,0 +1,18 @@
+--8<-- "LICENSE.md"
+
+---
+### MIT License
+--8<-- "LICENSES/MIT.txt"
+
+
+---
+### GPL License
+--8<-- "LICENSES/GPL-3.0.txt"
+
+---
+### LGPL License
+--8<-- "LICENSES/LGPL-3.0-only.txt"
+
+---
+### AVC License
+--8<-- "LICENSES/AVC.txt"
\ No newline at end of file
diff --git a/docs/About/Resources.md b/docs/About/Resources.md
new file mode 100644 (file)
index 0000000..32e493d
--- /dev/null
@@ -0,0 +1,22 @@
+## Post Configuration & Using Jacktrip
+Detailed instructions at [CCRMA](https://ccrma.stanford.edu/docs/common/IETF.html)
+
+## Raspberry Pi
+
+[paper](https://lac.linuxaudio.org/2019/doc/chafe2.pdf) accompanying jacktrip demo at [Linux Audio Conference 2019](https://lac.linuxaudio.org/2019/)
+
+## Other Repos
+jacktrip (1.0) was released on google code. When that shut down, it migrated to github (1.05, 1.1).
+It then moved to the CCRMA's cm-gitlab for version 1.2.
+And as of spring 2020 it moved back to GitHub for the current development.
+
+
+## Links
+  * Preliminary [Documentation](http://ccrma.stanford.edu/groups/soundwire/software/jacktrip/) and [API](http://ccrma.stanford.edu/groups/soundwire/software/jacktrip/annotated.html).
+  * Subscribe to the [Mailing List](http://groups.google.com/group/jacktrip-users).
+  * [CCRMA](http://ccrma.stanford.edu/).
+  * [SoundWIRE group](http://ccrma.stanford.edu/groups/soundwire/).
+  * [Juan-Pablo Caceres](https://ccrma.stanford.edu/~jcaceres/).
+
+## Related Software
+[JMess](https://github.com/jcacerec/jmess-jack): A utility to save your audio connections (mess).
diff --git a/docs/Build/CrossCompile.md b/docs/Build/CrossCompile.md
new file mode 100644 (file)
index 0000000..b554a2c
--- /dev/null
@@ -0,0 +1,19 @@
+# Cross Compile JackTrip for Windows on Fedora
+
+```bash
+sudo dnf install mingw64-gcc mingw64-gcc-c++ mingw64-pkg-config mingw64-qt5-qtbase-static
+```
+
+```bash
+meson --cross-file win/cross_file.txt build_win
+cd build_win
+meson compile
+```
+
+```bash
+export WINEPATH=/usr/x86_64-w64-mingw32/sys-root/mingw/bin/
+wine64 ./jacktrip.exe
+```
+
+You might want to copy all necessary DLLs to the same directory as the jacktrip.exe
+binary.
diff --git a/docs/Build/Linux.md b/docs/Build/Linux.md
new file mode 100644 (file)
index 0000000..985228d
--- /dev/null
@@ -0,0 +1,179 @@
+# Build Instructions
+
+The following are instructions for compiling Jacktrip from source.  Compiling
+from source is the best way to keep up with the latest changes, both stable and
+experimental.  For quicker ways to install Jacktrip, go to README.md in the root
+directory of the project.
+
+## Dependencies
+- C++ compiler
+- Meson
+- Qt5, or Qt6 (required for Virtual Studio)
+
+Optional:
+
+- JACK (preferred) or RtAudio (for clients only)
+- help2man for generating the manpage
+
+### Fedora (Qt5)
+```sh
+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)" rtaudio-devel git help2man python3-jinja2
+```
+
+### Fedora (Qt6)
+```sh
+dnf install qt6-qtbase-devel qt5-qtnetworkauth-devel qt5-qtwebsockets-devel qt5-qtquickcontrols2-devel qt5-qtsvg-devel qt6-qtwebengine-devel qt6-qtwebchannel-devel qt6-qt5compat-devel qt6-qtshadertools-devel
+dnf groupinstall "C Development Tools and Libraries"
+dnf groupinstall "Development Tools"
+dnf install "pkgconfig(jack)" rtaudio-devel git help2man python3-jinja2
+```
+
+Clone the git repo with submodules and run `./build install` in the project
+directory or use QtCreator to compile.
+
+### Ubuntu and Debian/Raspbian (Qt5)
+```sh
+apt install --no-install-recommends build-essential autoconf automake libtool make libjack-jackd2-dev git help2man python3-jinja2
+apt install qtbase5-dev qtbase5-dev-tools qtchooser qt5-qmake qttools5-dev libqt5svg5-dev libqt5websockets5-dev qtdeclarative5-dev qtquickcontrols2-5-dev
+```
+
+### Ubuntu and Debian/Raspbian (Qt6)
+```sh
+apt install --no-install-recommends build-essential autoconf automake libtool make libjack-jackd2-dev git help2man libclang-dev libdbus-1-dev libdbus-1-dev python3-jinja2
+apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6svg6  libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window
+apt install qt6-base-dev qt6-base-dev-tools qmake6 qt6-tools-dev qt6-declarative-dev qt6-webengine-dev qt6-webview-dev qt6-webview-plugins libqt6svg6-dev libqt6websockets6-dev libqt6core5compat6-dev libqt6shadertools6-dev libgl1-mesa-dev
+# for GUI builds
+apt install libfreetype6-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev libdrm-dev libglu1-mesa-dev libwayland-dev libwayland-egl1-mesa libgles2-mesa-dev libwayland-server0 libwayland-egl-backend-dev libxcb1-dev libxext-dev libfontconfig1-dev libxrender-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev '^libxcb.*-dev' libxcb-render-util0-dev libxcomposite-dev libgtk-3-dev
+apt install librtaudio-dev # if building with RtAudio
+```
+
+Clone the git repo with submodules and run `./build install` in the project
+directory or use QtCreator to compile.
+
+### Building and Installation instructions
+
+For other Linux distributions, install the dependencies listed above.
+
+To clone the repo in the Terminal:
+```sh
+$ git clone --recurse-submodules https://github.com/jacktrip/jacktrip.git
+```
+Note that we need `--recurse-submodules` to also get the submodules!
+
+Next, navigate to the cloned repository:
+```sh
+$ cd jacktrip
+```
+
+JackTrip uses meson. To configure a build directory, use `meson setup` with
+the following parameters:
+
+* `rtaudio`: Enabled this to build with support for the RtAudio backend.
+  If an existing installation is found, it will be used. Otherwise, meson
+  uses a subproject to download and install the latest supported release.
+  JackTrip supports both RtAudio v5 and v6.
+
+* `jack`: Enable this to link directly with libraries for the Jack Audio
+  Toolkit. JackTrip will fail if these cannot be found at runtime.
+
+* `weakjack`: Enable this to build with support for the Jack Audio Toolkit,
+  with support for dynamically loading the libraries at runtime. JackTrip
+  will still work if they cannot be found. This requires `rtaudio` and
+  uses a git submodule.
+
+* `nogui`: Build without support for a graphical user interface (this
+  also disables support for Virtual Studio). Only basic command line
+  features will be included. This requires fewer dependencies, and
+  supports static builds.
+
+* `novs`: Build without support for Virtual Studio. Virtual Studio
+  requires Qt6 with the WebEngine and WebChannel libraries installed.
+
+* `vsftux`: Skip the "Yes" / "No" first time run screen asking users if
+  they would like to use the Virtual Studio interface. Classic mode can
+  still be selected at startup, and can be used at any time.
+
+* `noupdater`: Build without support for automatic updates. This is
+  implied and has no effect when building for Linux.
+
+* `nofeedback`: Build without support for feedback detection. This
+  feature is optional, and uses the SimpleFFT third party library.
+
+* `profile`: Choose build profile / Sets desktop id accordingly
+
+* `qtversion`: Choose to build with either Qt5 or Qt6
+
+* `buildinfo`: Additional info used to describe the build
+
+For example:
+```sh
+$ meson setup -Drtaudio=enabled builddir
+```
+
+To build JackTrip, run:
+```sh
+$ meson compile -C builddir
+```
+
+To install JackTrip, run:
+```sh
+$ meson install -C builddir
+# enter your password when prompted
+```
+
+### Verification
+
+If you have installed jacktrip, from anywhere in the Terminal, type:
+```sh
+$ jacktrip -v
+```
+
+If you have compiled from source without installing, in the `/builddir`
+directory type:
+```sh
+$ ./jacktrip -v
+```
+
+If you see something like this, you have successfully installed JackTrip:
+
+```
+JackTrip VERSION: 2.x.x
+Copyright (c) 2008-2020 Juan-Pablo Caceres, Chris Chafe.
+SoundWIRE group at CCRMA, Stanford University
+```
+
+### Running Two Versions of JackTrip in Parallel
+One level above the project directory of your current JackTrip installation, clone the JackTrip repository again in another directory (e.g. `jacktrip-1.x.x`):
+
+```sh
+$ git clone --recurse-submodules https://github.com/jacktrip/jacktrip.git jacktrip-1.x.x
+```
+
+You now have two separate folders side by side: the new version in the jacktrip-1.x.x folder and the original one in the jacktrip folder. To build the new version without installing it:
+```sh
+$ cd jacktrip-1.x.x
+$ ./build
+```
+
+To experiment with the new version of JackTrip, enter builddir/build directory and check the JackTrip version:  
+```sh
+$ cd builddir
+$ ./jacktrip -v
+```
+
+You can use JackTrip from this directory, e.g.:
+```sh
+$ ./jacktrip -C serveraddress
+```
+
+You can switch back and forth between different JackTrip versions by running them in their respective build directories.
+
+Hint: If you lose track of where you are, this command will show your present working directory:
+```sh
+$ pwd
+```
+
+The new version's directory structure might look like this: ``` jacktrip-1.x.x/builddir``` and the old version ``` jacktrip/builddir```.
diff --git a/docs/Build/Mac.md b/docs/Build/Mac.md
new file mode 100644 (file)
index 0000000..898361f
--- /dev/null
@@ -0,0 +1,114 @@
+# Build Instructions
+
+The following are instructions for compiling Jacktrip from source.  Compiling
+from source is the best way to keep up with the latest changes, both stable and
+experimental.  For quicker ways to install Jacktrip, go to README.md in the root
+directory of the project.
+
+## Dependencies
+- C++ compiler
+- Qt5
+- JACK
+
+RtAudio is no longer a dependency.
+You might want to skip the steps you don't need.
+Install Jack2 https://jackaudio.org/downloads/
+
+If this command returns the XCode version, you have it installed:
+```sh
+xcodebuild -version
+```
+If you don't have XCode, go to the AppStore to download and install it.
+
+If this command returns the version number of the package manager Homebrew, you have it installed:
+```sh
+brew -v
+```
+If you don't have Homebrew, install it:
+```sh
+/bin/bash -c "$(curl -fsSLhttps://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
+```
+
+To install git if you don't have it:
+```sh
+brew install git
+```
+
+Install and link qt5:
+```sh
+brew install qt5
+brew link qt5 --force
+```
+
+Clone the git repo and run `./build` in the src directory or use QtCreator to compile
+
+## Build
+You can compile using the build script or QtCreator.
+
+To clone the repo in the Terminal:
+$ git clone --recurse-submodules https://github.com/jacktrip/jacktrip.git
+
+To compile using the build script:
+```sh
+$ cd jacktrip
+$ ./build
+$ cd builddir
+$ ls
+```
+
+You should see a `jacktrip` executable in this folder.
+
+If the build script doesn't work, try building
+the Makefiles yourself. You'd need qmake. Then you can build by:
+
+```sh
+$ qmake jacktrip.pro
+$ make release
+```
+
+To build using QtCreator:
+
+  * Open jacktrip.pro using QtCreator
+  * Choose a correctly configured Kit
+
+QtCreator places the `jacktrip` executable by default in a folder
+with a name like `build-jacktrip-Desktop_x86_darwin_generic_mach_o_64bit-Release/`.
+
+## Installation
+You need to have a working Jack installation on your machine (see Dependencies above).
+
+To install using Terminal (skip the first three steps if you've already followed
+the Build instructions above):
+
+```sh
+$ git clone --recurse-submodules https://github.com/jacktrip/jacktrip.git
+$ cd jacktrip
+$ ./build
+$ cd builddir
+$ sudo cp qjacktrip /usr/local/bin/
+  (enter your password when prompted)
+$ sudo cp jacktrip /usr/local/bin/
+
+$ sudo chmod 755 /usr/local/bin/qjacktrip
+  (now you can run jacktrip from any directory using Terminal)
+```
+  
+### Verification
+
+If you have installed jacktrip, from anywhere in the Terminal, type:
+```sh
+$ jacktrip -v
+```
+
+If you have compiled from source without installing, in the /builddir directory type:
+```sh
+$ ./jacktrip -v
+```
+
+If you see something like this, you have successfully installed Jacktrip:
+
+>     JackTrip VERSION: 1.xx
+>     Copyright (c) 2008-2020 Juan-Pablo Caceres, Chris Chafe.
+>     SoundWIRE group at CCRMA, Stanford University
+
diff --git a/docs/Build/Meson_build.md b/docs/Build/Meson_build.md
new file mode 100644 (file)
index 0000000..6b95d69
--- /dev/null
@@ -0,0 +1,83 @@
+# Build and Installation Instructions with Meson
+
+Meson is a modern and fast build system with a lot of features. You can
+find its documentation at [mesonbuild.com](https://mesonbuild.com/).
+
+## Install Dependencies
+
+=== "Fedora"
+
+    ```bash
+    dnf install meson qt5-qtbase-devel qt5-qtquickcontrols2-devel qt5-qtsvg-devel qt5-qtnetworkauth-devel qt5-qtwebsockets-devel rtaudio-devel "pkgconfig(jack)" help2man python3-jinja2 python3-pyyaml
+    ```
+
+=== "Debian/Ubuntu"
+
+    ```bash
+    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"
+
+    ```bash
+    brew install meson qt5 rtaudio help2man
+    ```
+    
+    You also need to install Jack, unless you want to disable jack support
+    (`-Djack=disabled`). On macOS Jack is often installed using an installer
+    from the [Jack2 release
+    page](https://github.com/jackaudio/jack2-releases/releases). You can also
+    install it using homebrew (`brew install jack`), but you should not use both
+    installation methods simultaneously.
+
+    Meson might not find qt when installed with brew. But brew tells you to set
+    PKG_CONFIG_PATH to a directory where it finds qt's pkgconfig file.
+    This directory has to be set as additional pkgconfig path in meson:
+
+    ```bash
+    meson --buildtype release --pkg-config-path `brew --prefix qt5`/lib/pkgconfig build_release
+    ``` 
+
+## Configuration
+
+If you are in a build directory, `meson configure` shows you all available options.
+Current project options are `jack`, `nogui`, `rtaudio` and `wair`.
+Meson shows you also the options of subprojects like RtAudio.
+
+## Build
+
+Meson builds in a separate directory. It doesn't touch anything of your project.
+This way you can have separate debug and release build directories for example. 
+
+Prepare your build directory:
+```bash
+meson builddir                                  # defaults to debug build
+
+## Additional build directories
+meson --buildtype release build_release         # release build
+meson --buildtype debugoptimized build_debug    # optimized debug build
+```
+
+Meson can download and build RtAudio as a subproject, if RtAudio is not available
+on your system. By default it only checks the dependency on your system. If you
+want to use the subproject you have to explicitly enable rtaudio.
+
+```bash
+cd builddir
+meson configure -Drtaudio=enabled
+```
+
+If `help2man` is found, Meson will create a manpage from `jacktrip --help`.
+
+Now build with:
+```bash
+cd builddir
+ninja
+```
+
+Install with:
+```bash
+sudo ninja install
+```
+
+
diff --git a/docs/Build/Windows.md b/docs/Build/Windows.md
new file mode 100644 (file)
index 0000000..bd7dc90
--- /dev/null
@@ -0,0 +1,84 @@
+# WINDOWS (XP and later)
+- Note: Some users have reported success using the PortAudio driver, though it
+  is not currently supported.
+
+## Build
+Note: [WIN10BUILDINSTRUCTIONS.pdf](WIN10BUILDINSTRUCTIONS.pdf)
+(in the same directory as this file) has screenshots of the Windows 10 build process
+and step-by-step instructions.
+
+If you do not have Git installed, download it from https://git-scm.com/download/win.
+Open the command line by typing cmd.exe in the Windows search bar.
+
+Use the `cd` command to navigate to the directory where you would like to
+install jacktrip, e.g. `cd C:\Users\Your User Name\`.
+
+Use `git clone --recurse-submodules https://github.com/jacktrip/jacktrip.git` to
+download a fresh copy of the repo or `git pull` to update your repo.
+
+On Windows 10, the easiest way to build is in the command line:
+
+- To add the location of qmake to the path, in the Windows search bar, type
+"environment variable" and click on the Environment Variables button in the
+Advanced tab of System Properties.  Find the Path variable in System variables,
+click Edit, and enter the location of qmake, e.g. `C:\Qt\5.15.0\mingw81_64\bin`,
+where 5.15.0 is the version of Qt you installed.
+To verify you have g++ installed, type `where g++` in the command line.  If the
+command returns `not found` rather than a path, go to the Qt Maintenance Tool,
+which might be in a directory such as `C:\Qt\maintenancetool.exe`, and remove,
+then reinstall Qt.
+In the command line, use the `cd` command to navigate to the project directory,
+e.g. `cd jacktrip` and execute the following commands:
+mkdir builddir (this step creates the build directory, and is only necessary if
+you're building for the first time)
+
+```sh
+$ cd builddir
+$ qmake -spec win32-g++ ../src/jacktrip.pro # you may skip this step if you're building for the first time
+$ mingw32-make clean # you may skip this step if you're building for the first time
+$ qmake -spec win32-g++ ../src/jacktrip.pro
+$ mingw32-make release
+```
+
+On earlier Windows versions, the easiest way to build is to download the free
+*Qt Creator IDE* from https://www.qt.io/download since the jacktrip buildscript is
+written in qmake.
+Open the `src/jacktrip.pro` and configure the project.
+Make sure to select the MinGW compiler (for example the one shipped with QtCreator).
+Building with Clang or Microsoft Visual Studio Compilers is currently not supported!
+
+Download Jack2 from https://jackaudio.org/downloads/
+Make sure to install Jack into `C:\Program Files (x86)\Jack` (as this is the
+path where the jacktrip build script will look for it).
+
+Hit <kbd>build</kbd> in QtCreator.
+
+Copy the dll files `Qt5Core.dll` and `Qt5Network.dll` from your compiler's bin
+directory, e.g. `C:\Qt\5.15.0\mingw81_64\bin` to the folder in your project
+where your `jacktrip.exe` is located, e.g.
+`C:\Users\Your Name\jacktrip\build-jacktrip-Desktop-Qt_5_15_0_MinGW_64_bit-Release\release`.
+In the above example, 5.15.0 is the version of Qt, MinGW 64 bit is the compiler.
+The folder names may vary according to the Qt and compiler versions you are
+using.
+
+Note: compiling with modifications in the .pro file (like adding a new source or
+      header file) requires qmake which is only available in the Qt Creator
+      package.
+
+## Verification
+In the search field (Windows key + R), enter cmd.exe to open the command line.
+Use the `cd` command to navigate to the directory where the executable
+jacktrip.exe is located, e.g.
+`C:\Users\Your Name\jacktrip\build-jacktrip-Desktop-Qt_5_15_0_MinGW_64_bit-Release\release`.
+
+From there, the following command should return the version of Jacktrip you installed:
+~~~sh
+jacktrip.exe -v
+~~~
+
+If you see something like this, you have successfully installed Jacktrip:
+
+>     JackTrip VERSION: 1.xx
+>     Copyright (c) 2008-2020 Juan-Pablo Caceres, Chris Chafe.
+>     SoundWIRE group at CCRMA, Stanford University
+
diff --git a/docs/CustomJackServerName.md b/docs/CustomJackServerName.md
new file mode 100644 (file)
index 0000000..28d4101
--- /dev/null
@@ -0,0 +1,8 @@
+# Using custom JACK server name
+
+In case you want to use JackTrip with JACK server _that has non-default name_ (by default it is `default`), you need to set `JACK_DEFAULT_SERVER` environment variable for JackTrip.
+
+1. Run `jackd` with `--name` flag:`jackd --name myfancyserver ...`
+2. Run JackTrip with required environment variable set: `JACK_DEFAULT_SERVER=myfancyserver jacktrip ...`
+
+This is useful when you want to isolate multiple JackTrip instances on single machine.
diff --git a/docs/DevTools/Formatting.md b/docs/DevTools/Formatting.md
new file mode 100644 (file)
index 0000000..6abc7b3
--- /dev/null
@@ -0,0 +1,76 @@
+# Formatting with clang-format
+
+JackTrip uses clang-format to specify the Code Formatting Style.
+Most IDEs are able to recognize the `.clang-format` file in JackTrip's project
+directory and integrate the formatting capabilities.
+
+Formatting specific files is done like this:
+
+```bash
+clang-format -i class.cpp class.h
+```
+
+This overrides all given files. If you only want to check the formatting `-i` has
+to be removed.
+
+Areas in the source code that must not be formatted have to be embraced in following statements:
+
+```c++
+this = is.formatted;
+// clang-format off
+this+=   isnot     ;
+// clang-format on
+this = again;
+```
+
+## Forget about formatting with a git pre-commit hook
+
+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
+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/DevTools/StaticAnalysis.md b/docs/DevTools/StaticAnalysis.md
new file mode 100644 (file)
index 0000000..6145c28
--- /dev/null
@@ -0,0 +1,48 @@
+# Static Analysis
+
+Static analysis tools usually needs a `compile_commands.json` file.
+If you use Meson to build JackTrip, it already created this file in your build
+directory.
+
+## Scan-Build
+
+If you only want to run basic static analysis on JackTrip, you can install
+scan-build. Meson will automatically generate a scan-build target for you:
+
+```bash
+ninja -C builddir scan-build
+```
+
+This should never show any warnings or errors.
+
+## Clang-Tidy
+
+If you installed [clang-tidy](https://clang.llvm.org/extra/clang-tidy/) and
+there's a `.clang-tidy` file in the project directory,
+Meson generates a clang-tidy target, too. 
+The `.clang-tidy` file defines which [checks](https://clang.llvm.org/extra/clang-tidy/checks/list.html)
+should be run.
+Running clang-tidy on the entire code base:
+
+```bash
+ninja -C builddir clang-tidy
+```
+
+In most cases you want to run clang-tidy on single files.
+
+```bash
+cd src
+run-clang-tidy.py -j 4 -p ../builddir singlefile.cpp
+```
+
+In `-j #`, `#` specifies the number of tidy instances that run in parallel.
+
+### Fixes
+
+For some checks clang-tidy offers automatic fixes. Some of these are save to apply
+without worries. Others should be checked afterwards.
+
+```bash
+run-clang-tidy.py -fix -j 4 -p ../builddir singlefile.cpp
+```
+
diff --git a/docs/Documentation/MkDocs.md b/docs/Documentation/MkDocs.md
new file mode 100644 (file)
index 0000000..ab50a26
--- /dev/null
@@ -0,0 +1,25 @@
+# Write Documentation
+
+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 mkdocs-macros-plugin
+``` 
+
+When writing documentation it is very handy to run `mkdocs serve`. This will open
+a local webserver (usually at [http://127.0.0.1:8000/](http://127.0.0.1:8000/)).
+If you change Markdown files of the documentation and save them, the website automatically
+updates. 
diff --git a/docs/Install.md b/docs/Install.md
new file mode 100644 (file)
index 0000000..14ef46d
--- /dev/null
@@ -0,0 +1,62 @@
+# Installation
+## Linux
+On Linux the easiest way to install JackTrip is to use the distribution's package manager. However, this may not be the most up-to-date version. For the most recent version, go to the Github releases page below.
+
+=== "Fedora"
+
+    ```bash
+    sudo dnf install jacktrip
+    ```
+
+=== "Debian/Ubuntu"
+
+    ```bash
+    sudo apt install jacktrip
+    ```
+
+### 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.
+
+## Windows
+Windows installer and executable are available on the [GitHub releases page](https://github.com/jacktrip/jacktrip/releases). The installer will add a shortcut to your Start menu.
+
+# Experimental builds
+
+To keep up with the latest changes, including experimental functionality, you can access builds from the `dev` branch for Linux, macOS and Windows at [https://nightly.link/jacktrip/jacktrip/workflows/jacktrip/dev](https://nightly.link/jacktrip/jacktrip/workflows/jacktrip/dev). Please note that macOS binaries are not signed, so you need to right-click and select "Open" in order to run them.
+
+# Build from Source
+To build JackTrip yourself, follow instructions to compile for [Linux](Build/Linux.md), [MacOS](Build/Mac.md) or [Windows](Build/Windows.md).
diff --git a/docs/VirtualStudio.md b/docs/VirtualStudio.md
new file mode 100644 (file)
index 0000000..2bd2a0f
--- /dev/null
@@ -0,0 +1,26 @@
+# Virtual Studio
+
+Virtual Studio is the easiest way to get started with JackTrip. By leveraging cloud-based edge computing, realtime video and lossless audio transmits at close to the speed of light, enabling musicians to play and sing together in sync. JackTrip Virtual Studio stands out from other real-time music collaboration technologies with unsurpassed scalability supporting hundreds of musicians in a single session.
+
+![Virtual Studio](images/jacktrip_virtual_studio.png)
+
+## Getting Started
+
+Performing together online is as easy as:
+1. Sign up for a free account
+2. Start your Virtual Studio
+3. Invite your friends and play
+
+[Click here to start jamming!](https://app.jacktrip.org/)
+
+## Learn More
+
+You can find more information about this product on our [website](https://www.jacktrip.com/).
+
+## Community
+
+Join other JackTrippers on our [community forum](https://community.jacktrip.org/).
+
+## Help
+
+Having difficulties getting set up? Check out our [troubleshooting guides](https://help.jacktrip.org/hc/en-us) or [reach out to the team](https://help.jacktrip.org/hc/en-us/requests/new).
diff --git a/docs/assets/jacktrip.svg b/docs/assets/jacktrip.svg
new file mode 100644 (file)
index 0000000..2d1d67f
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="1366px" height="1366px" viewBox="0 0 1366 1366" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>jacktrip</title>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="jacktrip">
+            <path d="M754.755517,107 C789.35747,107 817.401215,135.042996 817.401215,169.633588 C817.401215,204.22418 789.35747,232.258717 754.755517,232.258717 C744.033904,232.258717 733.946957,229.56016 725.112416,224.822882 L599.778709,297.159431 C599.228666,297.523187 598.67016,297.878483 598.09473,298.2084 C565.109058,317.250568 542.88731,348.102093 533.688894,382.447362 C524.473554,416.818009 528.256159,454.631642 547.270732,487.555727 L548.463903,489.611367 C548.819315,490.220446 549.149341,490.837984 549.462443,491.463982 L808.998246,940.845596 C810.369123,942.765886 811.503059,944.804608 812.416977,946.927923 C823.857877,968.727864 825.90573,993.370171 819.91449,1015.71151 C813.830165,1038.44199 799.444418,1059.03223 778.195823,1072.16126 C765.240188,1081.24669 747.232618,1080.02007 735.656322,1068.44757 C722.692225,1055.48773 722.692225,1034.4576 735.656322,1021.49776 L735.664784,1021.49776 L735.715558,1021.54006 C737.712638,1019.55209 739.980509,1017.78407 742.536094,1016.30367 C749.280471,1012.41234 753.875448,1005.89858 755.830217,998.598096 C757.801911,991.246855 757.10801,983.294995 753.274631,976.59513 L753.23232,976.518995 L492.181782,524.498039 C491.817908,523.948176 491.462495,523.389854 491.132469,522.814613 L489.930836,520.758973 C461.709385,471.880411 456.031246,415.988985 469.604622,365.325484 C483.186459,314.645065 516.045198,269.074138 564.880579,240.887331 C565.489858,240.532035 566.107599,240.210577 566.733802,239.897579 L692.143668,167.518732 C693.260679,133.909433 720.864389,107 754.755517,107 M517.763026,1028.58676 C518.118438,1029.2043 518.448464,1029.81338 518.753104,1030.43937 L554.032033,1091.52489 L554.082806,1091.49105 C581.678054,1139.26143 626.375417,1171.44108 676.124716,1184.75622 C725.907863,1198.09673 780.700635,1192.58965 828.427467,1165.0373 C829.002897,1164.70738 829.569865,1164.40284 830.145295,1164.10676 C876.966671,1136.51211 908.547617,1092.27778 921.740193,1043.04392 C935.093551,993.217901 929.584656,938.400822 902.006333,890.647364 L677.284038,501.530698 C643.672162,500.414054 616.753891,472.827867 616.753891,438.939407 C616.753891,404.357275 644.806098,376.314278 679.408051,376.314278 C714.010004,376.314278 742.053749,404.357275 742.053749,438.939407 C742.053749,449.665959 739.354306,459.749594 734.615471,468.572775 L959.346229,857.70636 C996.114506,921.363539 1003.51047,994.173816 985.824466,1060.1658 C968.400787,1125.16802 926.589036,1183.73263 864.459533,1220.6411 C862.843252,1221.80004 861.1085,1222.82363 859.280664,1223.69496 C796.15262,1259.30068 724.257733,1266.29662 658.997214,1248.81945 C593.652073,1231.32536 534.831292,1189.27355 498.003779,1126.84299 C496.929079,1125.278 495.989774,1123.61149 495.185865,1121.86039 L461.472443,1063.47343 C461.117031,1062.92357 460.753156,1062.36525 460.42313,1061.79001 C456.530516,1055.0563 450.006157,1050.45438 442.694812,1048.50025 C435.366543,1046.54612 427.428996,1047.24825 420.743854,1051.10575 C404.91107,1060.26731 384.635629,1054.8702 375.4626,1039.04261 C366.298033,1023.21502 371.696919,1002.94624 387.529703,993.776223 C410.157637,980.706411 436.255075,978.143205 459.822314,984.437017 C483.423401,990.756208 504.722769,1006.00855 517.763026,1028.58676" id="Fill-1" fill="#F21B1B"></path>
+            <rect id="Rectangle" x="0" y="0" width="1365.33" height="1365.33"></rect>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/docs/changelog.yml b/docs/changelog.yml
new file mode 100644 (file)
index 0000000..4362431
--- /dev/null
@@ -0,0 +1,515 @@
+- Version: "2.3.0"
+  Date: 2024-05-15
+  Description:
+  - (added) Static Qt 5.15.13 nogui (CLI) builds for all platforms
+  - (added) VS Mode learn more buttons and warning links
+  - (updated) Significant PLC performance and quality improvements
+  - (updated) Reduced amount of latency added for PLC strategy
+  - (updated) Merged PLC buffer strategies (3 is now identical to 4)
+  - (updated) Automatically start PLC worker for slower predictions
+  - (updated) Builds now use Qt 6.2.8 for OSX and 5.15.13 for Linux
+  - (updated) Linux containers now use static builds with Qt 6.5.3
+  - (updated) VS Mode help links go to support.jacktrip.com
+  - (updated) VS Mode manage button goes to new studio dashboard
+  - (fixed) PLC degradation when peer != local buffer sizes
+  - (fixed) Port binding on machines that don't support IPv6
+  - (fixed) Command line interface debug logging improvements
+  - (fixed) VS Mode truncation of invite copied tooltip message
+- Version: "2.2.5"
+  Date: 2024-03-28
+  Description:
+  - (fixed) VS Mode only admins could join new sessions starting up
+  - (fixed) VS Mode only ask for feedback if you've joined a session
+  - (updated) VS Mode updates to support self hosted virtual studios
+- Version: "2.2.4"
+  Date: 2024-03-13
+  Description:
+  - (fixed) Allow software opengl for older windows video drivers
+  - (fixed) VS Mode avoid unnecessary JACK library checks at startup
+- Version: "2.2.3"
+  Date: 2024-03-04
+  Description:
+  - (added) New container images for JackTrip hub server
+  - (fixed) Support for audio interfaces on OSX with multiple channels
+  - (fixed) Hub server crashes when trying to rebind ports
+  - (fixed) VS Mode blacklisted Generic Low Latency ASIO Driver
+  - (fixed) VS Mode inconsistent initial connection state 
+- Version: "2.2.2"
+  Date: 2024-02-09
+  Description:
+  - (updated) VS Mode updated network connection thresholds
+  - (updated) VS Mode improved sample rate flexibility for Windows
+  - (fixed) VS Mode inconsistent deep link handling on Windows
+  - (fixed) Throttle console errors for UDP waiting too long
+- Version: "2.2.1"
+  Date: 2024-01-29
+  Description:
+  - (fixed) Sample rate issues with certain Windows ASIO drivers
+- Version: "2.2.0"
+  Date: 2024-01-22
+  Description:
+  - (updated) Improved support for different input and output devices
+  - (updated) Various latency improvements for packet loss concealment
+  - (updated) VS Mode make it easier to dismiss the user feedback dialog
+  - (updated) VS Mode error message for disconnected audio interfaces
+  - (fixed) VS Mode broken deeplinks when studio doesn't match filters
+  - (fixed) VS Mode refused to connect to studios not 48khz
+  - (fixed) VS Mode showed wrong devices selected when connected
+- Version: "2.1.0"
+  Date: 2023-11-06
+  Description:
+  - (added) VS Mode ability to create studios without a web browser
+  - (added) VS Mode improved network stability notifications
+  - (added) VS Mode dialog when QML plugins are missing
+  - (updated) VS Mode video improvements on Windows
+  - (updated) Packet loss concealment latency and quality improvements
+  - (fixed) Packet loss concealment glitches when buffer sizes don't match
+  - (fixed) VS Mode ensure that the app is disconnected at startup
+  - (fixed) Invalid escape sequence in Linux desktop file
+  - (fixed) VS Mode unable to change update channel
+- Version: "2.0.2"
+  Date: 2023-09-01
+  Description:
+  - (added) VS Mode latency categories for Linux audio devices
+  - (added) VS Mode audio warnings for high latency Linux devices
+  - (updated) Improved support for Pipewire latency on Linux
+  - (fixed) Crash on Windows when using the JACK audio backend
+  - (fixed) Include ALSA support for Linux builds using meson
+  - (fixed) VS Mode overlapping UI elements with max scaling
+  - (fixed) Don't require git to be present for meson builds
+  - (fixed) Linux man page description and meson build errors
+- Version: "2.0.1"
+  Date: 2023-08-29
+  Description:
+  - (fixed) VS Mode Mix to Mono setting was sending a buzzing noise
+  - (fixed) VS Mode Logout followed by Login was breaking network stats
+  - (fixed) Device names with special characters work again
+- Version: "2.0.0"
+  Date: 2023-08-28
+  Description:
+  - (added) VS Mode integrated video and other UI updates
+  - (added) VS support for refreshing devices while connected to studio
+  - (added) VS feedback survey request after leaving studios
+  - (added) VS Mode improved warnings for high latency audio devices
+  - (added) VS Mode improved getting started recommendations
+  - (updated) official builds now use Qt 6 and RtAudio 6
+  - (updated) official Windows builds now use schannel and MSVC
+  - (updated) Simple-FFT dependency copied into source tree
+  - (updated) Improved user experience when using the RtAudio backend
+  - (fixed) Crashes when audio interfaces don't support buffer size
+  - (fixed) Crashes when audio interfaces are unplugged while active
+  - (fixed) Blacklisting Steinberg Generic ASIO driver due to crashes
+  - (fixed) Bugs with Virtual Studio deep links and connections stats
+  - (fixed) VS Settings will now revert back when Cancel is selected
+  - (fixed) VS Mode device levels no longer reset on first registration
+- Version: "1.10.1"
+  Date: 2023-08-03
+  Description:
+  - (fixed) VS Mode crashes involving ASIO device selection
+- Version: "1.10.0"
+  Date: 2023-06-16
+  Description:
+  - (added) VS Mode automatically mute when feedback is detected
+  - (added) VS Mode - notify people of network outages
+  - (updated) PLC optimizations for slower hardware devices
+  - (updated) VS Mode volume meter performance improvements
+  - (updated) VS Mode login no longer listens on a local port
+  - (updated) VS Mode improved clarity of startup messages
+  - (fixed) Windows crash caused by Realtek ASIO driver
+  - (fixed) More friendly message for Windows Terminal bug
+  - (fixed) Faster startup by reducing and deferring audio interface scans
+  - (fixed) Switching from VS to Classic mode leaves audio open
+- Version: "1.9.0"
+  Date: 2023-05-05
+  Description:
+  - (added) buffer strategy 4 to run PLC in audio callback
+  - (added) VS Mode - change audio devices while connected
+  - (added) universal binary for macOS
+  - (added) tooltips, sliders, and positioning of connected interface
+  - (added) emails for vulnerability reporting
+  - (added) local monitoring
+  - (added) VS mode - Error message when single studio limit reached
+  - (updated) regulator thread uses real-time priority
+  - (updated) VS mode - use buffer strategy 4
+  - (updated) VS mode - Default to PLC
+  - (updated) VS Mode - PLC auto queue has 5ms headroom
+  - (updated) Enforcing using the same ASIO device on Windows
+  - (updated) VS Mode - JTL builds hide the yes/no screen on first launch
+  - (updated) GHA - wait for static Qt builds rather than failing
+  - (updated) VS Mode - "all devices" is now "high latency"
+  - (updated) VS Mode - Warning text for non-ASIO Windows devices
+  - (updated) Faust-generate code moved out of headers
+  - (fixed) PLC bugs
+  - (fixed) VS Mode - changing devices while connected refreshes device lists
+  - (fixed) play test tone on Linux
+  - (fixed) static openssl on Linux
+- 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:
+  - (added) VS Mode - Start and join inactive studios
+  - (added) JackTrip now prints build info on running from console
+  - (added) VS Mode - supports changing output volume from the server
+  - (added) VS Mode - Link to video on VS web when connected
+  - (updated) signing now happens in the main build workflow
+  - (updated) VS mode sorts active studios above inactive studios
+  - (updated) cmake build
+  - (updated) meson builds will fail if no backend is enabled
+  - (updated) replaced many ifdefs with if constexpr
+  - (updated) After signing out of VS mode, you will be asked to sign back in on the web
+  - (fixed) network stats failing after studio start
+  - (fixed) occasional immediate disconnects
+  - (fixed) segfault issue due to ifdefs
+  - (fixed) Hanging UI on Windows
+  - (fixed) turned a comment warning into an appropriate error
+  - (fixed) VS Mode - Join issue withs studios started in app
+  - (fixed) hanging app after refreshing studios
+  - (fixed) VS Mode - TCP 19 error after starting a studio
+- Version: "1.6.8"
+  Date: 2022-12-05
+  Description:
+  - (fixed) broken "Yes" button on launch
+  - (fixed) window resizing issues
+  - (fixed) subscribed studios showing up under Public in VS mode
+- Version: "1.6.7"
+  Date: 2022-11-03
+  Description:
+  - (added) volume meters in classic mode
+  - (added) release-acquire ordering for Regular
+  - (added) audio driver support article in VS mode
+  - (added) regulatorthread
+  - (added) studios page first time UI
+  - (added) non-asio audio devices can be used on windows
+  - (updated) dependency list in documentation
+  - (updated) windows opens jacktrip after install
+  - (updated) Move to overlapped I/O for Windows Networking
+  - (updated) New default device behavior in Virtual Studio mode
+  - (fixed) opening links from Virtual Studio
+  - (fixed) send capture volume as int
+  - (fixed) connection issues for servers without reverse dns
+  - (fixed) flatpak build errors
+  - (fixed) ventura updater crash
+  - (fixed) uninitialized delete issue
+  - (remvoed) extraneous call to readSlotNonBlocking
+- Version: "1.6.6"
+  Date: 2022-11-01
+  Description:
+  - (fixed) Notarization scripts for macOS
+  - Note - this version replaces 1.6.5, as that release was mistakenly deleted
+- Version: "1.6.5"
+  Date: 2022-10-28
+  Description:
+  - (added) Input/output volume control and input mute in VS
+  - (added) Volume plugin
+  - (added) Linux manifests used for download links
+  - (added) VS Test mode for jacktrip.org users
+  - (added) Qt6 support for NO_VS builds
+  - (added) Show Qt version in About dialog
+  - (updated) VS - makes inactive, admin'd studios visible by default
+  - (updated) using -q auto3 for buffer length with plc
+  - (updated) updated notarization process to use notarytool
+  - (updated) link to Qt source as it has changed
+  - (updated) Classic GUI reverts some pre-VS changes
+  - (updated) QMake build scripts more in line with documentation
+  - (fixed) new user screen in VS mode
+  - (fixed) logout freezing jacktrip
+  - (fixed) NO_VS builds work without setting NO_UPDATER
+  - (fixed) volume meter-related crash
+- Version: "1.6.4"
+  Date: 2022-09-16
+  Description:
+  - (added) Volume meters when connected to a Studio in VS mode
+  - (added) Validation of Linux desktop file in build steps
+  - (added) a copy invite link button to VS mode Studios
+  - (added) an advanced setting for buffer strategy in VS mode
+  - (updated) PLC workers now zero out the last good packet from stalled clients
+  - (updated) PLC distinguish stuck clients
+  - (updated) Fedora meson dependencies
+  - (fixed) UI is usable when Studio list in empty in VS mode
+  - (fixed) Crashing when unplugging a device while connected and using JACK
+  - (fixed) Launching from URL skipped setup
+  - (fixed) UI hanging when connecting to Studios in VS mode
+  - (fixed) "Refresh list" button disabling ui interaction in VS mode
+  - (fixed) Network stats not displaying on first connect after login (VS mode)
+  - (fixed) VS mode won't join studios when on warning or device setup screens
+- Version: "1.6.3"
+  Date: 2022-08-23
+  Description:
+  - (fixed) Segfault when creating a hub server via cmd line
+  - (fixed) Linux desktop file is no longer invalid
+  - (fixed) Command line arguments no longer print to console
+- Version: "1.6.2"
+  Date: 2022-08-05
+  Description:
+  - (updated) Static Qt version for Linux builds
+  - (updated) cleaner, easier to read VS settings
+  - (updated) icons for 'Manage' and 'Settings' in VS mode
+  - (added) human-readable locations in VS mode
+  - (added) warning that cmake is not officially supported
+  - (added) VS mode is treated as a device by VS web
+  - (added) Network statistics in Virtual Studio mode
+  - (added) URL scheme support to join a Studio from the VS web join button
+  - (added) banner images on Studios in VS mode
+  - (added) VS mode sets remote client name to app ID
+  - (fixed) WebSocket connection behavior in Virtual Studio (VS) mode
+  - (fixed) dblsqd errors in Linux builds
+  - (fixed) Windows datagramAvailable error
+  - (fixed) High Sierra compatibility in static builds
+  - (fixed) Doesn't crash if RtAudio sample rate isn't supported
+  - (fixed) Fractional UI scaling on Windows
+- Version: "1.6.1"
+  Date: 2022-06-20
+  Description:
+  - (added) ToS IP header to use DSCP Expedited Forwarding
+  - (fixed) Ubuntu deoendencies
+  - (fixed) timeout of client restored
+  - (fixed) bufstrategy 3 history minimum
+  - (fixed) perpetual logging in screen
+- Version: "1.6.0"
+  Date: 2022-05-30
+  Description:
+  - (added) Virtual Studio integration; previous GUI is now called "Classic Mode"
+  - (added) dblsqd for auto-updates
+  - (updated) buffer strategy 3 - multiple updates and fixes, still experimental
+  - (added) JackTrip Labs signing scripts
+  - (fixed) OpenSSL in the build script
+  - (updated) code cleanup and maintenance
+- Version: "1.5.3"
+  Date: 2022-03-28
+  Description:
+  - (added) linux instructions for parallel versions
+  - (added) docs on running JackTrip with a named JACK server
+  - (added) nogui linux release build
+  - (update) Auto mode for buffer strategy 3
+  - (update) remove extra macOS binary release artifact
+  - (fixed) Don't link nogui qmake build with gui libraries
+- Version: "1.5.2"
+  Date: 2022-03-02
+  Description:
+  - (update) RingBuffer replaced by Regulator for experimental buffer strategy 3
+  - (update) call out old jacktrip versions in package repos on Debian and others
+  - (update) first attempt to support BSDs (especially FreeBSD)
+  - (update) add signing to macOS packaging script
+  - (update) Rename __MANUAL_POLL__ TO MANUAL_POLL
+  - (update) RtAudio warning changes
+  - (update) Use bundled rtaudio for Windows release builds
+  - (update) Incorporate meson build in the main GHA file
+  - (fixed) build script doesn't exit if build fails
+  - (fixed) false error message for JACK 1.9.20 on M1 builds
+  - (fixed) Don't connect our UDP socket
+  - (fixed) Fix GUI command line warning
+  - (fixed) Limiter allocation in GUI
+- Version: "1.5.1"
+  Date: 2022-01-07
+  Description:
+  - (added) option to include the server in the patching for client fan out/in and full mix modes
+  - (added) clang-tidy and clang-format CI checks
+  - (update) [Meson] only add weakjack include directory when enabled
+  - (update) remove reserved macro identifiers // use predefined macros
+  - (update) remove 'using namespace std' and add clang-tidy check
+  - (fixed) nearly all clazy warnings level 2 fixed
+  - (fixed) missing Windows startmenu icon
+  - (fixed) missing initializer for PROCESSENTRY32 type (Windows)
+- Version: "1.5.0"
+  Date: 2022-01-03
+  Description:
+  - (added) option to upmix mono clients to stereo in hub server mode patching
+- Version: "1.4.3"
+  Date: 2021-12-18
+  Description:
+  - "Generating changelog from YAML file for MkDocs and Flatpaks by @ntonnaett in #442"
+  - "Update jacktrip_globals.h by @cchafe in #443"
+- Version: "1.4.2"
+  Date: 2021-12-06
+  Description:
+  - "[Meson] Set RtAudio subproject to version 5.2.0 by @ntonnaett in #436"
+  - "Verbose mode in GUI by @psiborg112 in #434"
+  - "Help screen for app bundle script by @psiborg112 in #439"
+  - "[GHA] Fix weekly cron job entry by @ntonnaett in #441"
+- Version: "1.4.1"
+  Date: 2021-11-11
+  Description:
+  - "Fix for OSX installer failures by @mikedickey in #431"
+- Version: "1.4.0"
+  Date: 2021-11-03
+  Description:
+  - (added) optional GUI from QJackTrip
+  - (added) authentication in hub server mode
+  - (added) different number of sending and receiving channels
+  - (added) append thread IDs to jack client names
+  - (added) new patcher mechanism that doesn't delete existing connections
+  - (added) MkDocs based documentation
+  - (added) weak jack linking
+  - (added) manpage
+  - (added) MSVC build
+  - (added) RtAudio Meson subproject
+  - (added) formatting with clang-format
+  - (added) static analysis with clang-tidy
+  - (added) cross compilation for Windows
+  - (added) flatpaks
+  - (added) appstream
+  - (added) automated builds and deployment for Linux, macOS and Windows
+  - (added) macOS and Windows Installers
+  - (fixed) regression in remote client name handling
+  - (fixed) long jack client names (> 27 characters) in 1.9.11
+  - (fixed) Hardcode Derived Class Names of ProcessPlugins to prevent undefined behavior
+  - (update) QJackTrip and JackTrip are now identical
+  - (update) Change helpscreen wording for --broadcast argument
+  - (update) jitter buffer alternatives
+  - (update) RtAudio revive
+  - (update) RtAudio device selection
+  - (update) build script moved to root directory
+- Version: "1.3.0"
+  Date: 2021-01-14
+  Description:
+  - (added) async networking in hub listener
+  - (added) limiter, compressor, reverb
+  - (added) examine audio delay
+  - (added) jitter buffer alternatives
+  - (added) broadcast output ports
+  - (added) PREFIX variable for installation path
+  - (added) disconnect on timeout
+  - (added) SIGTERM
+  - (added) simulate packet loss, jitter
+  - (added) hubpatch 5, no auto patching
+  - (added) jack client name length check
+  - (added) scripts/hubMode/test_hub_mode_server_and_client.sh
+  - (added) Meson build
+  - (fixed) misc. typos, indentation
+  - (fixed) short form IO stat options
+  - (fixed) nullptr jack server name when creating jack client
+  - (fixed) stop ring buffer blocking when jack has been stopped
+  - (fixed) JMess handling of non-western characters
+  - (fixed) closing curly brace on mJackTrip client creation
+  - (fixed) Warnings
+  - (fixed) remove rtaudio device and mpeeraddress msgs.
+  - (fixed) signal and slot connections
+  - (fixed) incorrect dependency from jacktrip_main
+  - (update) RT thread priority for network I/O
+  - (update) clipping to saturation
+  - (update) build instructions
+- Version: "1.2.2"
+  Date: 2020-12-27
+  Description:
+  - (added) bindPort range to reject oddball connections
+  - (fixed) jack client name strings
+- Version: "1.2.1"
+  Date: 2020-08-05
+  Description:
+  - (added) src/build script builds in ../builddir
+  - (fixed) refactor "Master" to be "Hub"
+  - (fixed) 1.2.1 correctly Versioned and tagged
+- Version: "1.2"
+  Date: 2020-06-07
+  Description:
+  - (added) jack patching modes (-p) for Hub Mode server (-S)
+  - (fixed) Compilation under ubuntu
+  - (removed) setRealtimeProcessPriority()
+  - (removed) Rtaudio mode (but still has dependencies)
+  - (fixed) IPv4-mapped IPv6 addressing bug
+  - ...
+  - (fixed) Fixed compilation for  MacOSX10.11.sdk.
+  - (update) Updated to RtAudio 4.1.1, and using shared lib in linux.
+- Version: "1.1"
+  Date: 2015-05-27
+  Description:
+  - (added) Support for RtAudio. Jacktrip can now be used without Jack
+  - (added) DNS Look-up support, now one machine can have a private IP (but still needs to have UDP ports open)
+  - (added) New port to Windows XP and Windows Vista (experimental and not tested for a long time, only when using jacktrip as a library)
+  - (added) Multiclient Server (experimental and not exposed in the executable)
+- Version: "1.0.5"
+  Date: 2010-11-25
+  Description:
+  - (added) Compatibility with JamLink boxes (restricted at the moment to 48KHz, 64 buffer size and 1 channel)
+  - (added) New port structure that allows the communication between a public server and a local client
+  - (added) Option for packets without header
+  - (added) Option to change default client name
+  - (fixed) General optimizations and code cleanup
+  - (added) Improved, now cross-platform build script
+- Version: "1.0.4"
+  Date: 2009-02-05
+  Description:
+  - (fixed) Buss error caused when no physical inputs or outputs ports are available
+- Version: "1.0.3"
+  Date: 2008-12-29
+  Description:
+  - (added) Redundancy Algorithm for UDP Packets to avoid glitches with packet losses
+  - (fixed) Now compiles on 64bits machines
+  - (fixed) Improved exceptions handling
+  - (added) Basic Karplus-Strong model added as Plug-in
+  - (added) Some functionality reimplemented using signals and slots for more flexibility
+  - (added) Multiple-Client-Server in alpha testing, expect it working in the next release
+- Version: "1.0.2"
+  Date: 2008-09-30
+  Type: development
+  Description:
+  - (added) Port offset mode, to use a different UDP port than the default one.
+  - (fixed) Improved thread behavior
+- Version: "1.0.1"
+  Date: 2008-09-23
+  Type: development
+  Description:
+  - (added) jamlink mode to connect with jamlink boxes
+  - (fixed) thread priority in both Linux and Mac OS X (still need some work on the Mac OS X Version)
+  - (fixed) Bug that was causing plug-ins not to behave correctly
+  - (added) Loopback mode
+  - "(added) Underrun Modes: Wavetable (default) and set to zeros"
+  - (added) Check for peer audio settings, program exists if they don't match
+  - (added) Automatically connect ports to available physical audio interface.
+- Version: "1.0"
+  Date: 2008-09-23
+  Type: "development"
+  Description:
+  - initial release
diff --git a/docs/images/jacktrip_hubclient_basic.png b/docs/images/jacktrip_hubclient_basic.png
new file mode 100644 (file)
index 0000000..a932eb4
Binary files /dev/null and b/docs/images/jacktrip_hubclient_basic.png differ
diff --git a/docs/images/jacktrip_hubclient_plugins.png b/docs/images/jacktrip_hubclient_plugins.png
new file mode 100644 (file)
index 0000000..5b39db7
Binary files /dev/null and b/docs/images/jacktrip_hubclient_plugins.png differ
diff --git a/docs/images/jacktrip_hubserver_basic.png b/docs/images/jacktrip_hubserver_basic.png
new file mode 100644 (file)
index 0000000..e77d95e
Binary files /dev/null and b/docs/images/jacktrip_hubserver_basic.png differ
diff --git a/docs/images/jacktrip_hubserver_jitter.png b/docs/images/jacktrip_hubserver_jitter.png
new file mode 100644 (file)
index 0000000..556cc75
Binary files /dev/null and b/docs/images/jacktrip_hubserver_jitter.png differ
diff --git a/docs/images/jacktrip_virtual_studio.png b/docs/images/jacktrip_virtual_studio.png
new file mode 100644 (file)
index 0000000..05573f2
Binary files /dev/null and b/docs/images/jacktrip_virtual_studio.png differ
diff --git a/docs/index.md b/docs/index.md
new file mode 100644 (file)
index 0000000..358dde0
--- /dev/null
@@ -0,0 +1,36 @@
+# Welcome to JackTrip
+
+JackTrip is a Linux, macOS, or Windows multi-machine audio system used for network music performance over the Internet.
+It supports any number of channels (as many as the computer/network can handle) of bidirectional, high quality, uncompressed audio signal streaming.
+
+You can use it between any combination of machines e.g., one end using Linux can connect to another using macOS.
+
+## Virtual Studio
+
+With JackTrip Virtual Studio, you may sing with your chorus, or jam with your band, online without leaving home. With built-in support for live broadcasting, you can share your sessions on [JackTrip Radio](https://jacktrip.radio/). Additionally, Virtual Studio allows you to enhance your performances and bring your music to life utilizing Soundscapes digital signal processing. Musicians have creative control with dozens of audio effects with various reverbs, compression, attack and more, in real-time.
+
+[See why thousands of musicians worldwide use JackTrip to rehearse together, perform together, collaborate together, and laugh together.](https://www.jacktrip.com/)
+
+## Screenshots
+
+
+=== "Virtual Studio"
+
+    ![Virtual Studio](images/jacktrip_virtual_studio.png){width=450}
+
+=== "Hubclient // Basic"
+
+    ![JackTrip Hubclient](images/jacktrip_hubclient_basic.png){width=450}
+
+=== "Hubclient // Plugins"
+
+    ![JackTrip Hubclient](images/jacktrip_hubclient_plugins.png){width=450}
+
+=== "HubServer // Basic"
+
+    ![JackTrip Hubserver](images/jacktrip_hubserver_basic.png){width=450}
+
+=== "HubServer // Jitter Buffer"
+
+    ![JackTrip Hubserver](images/jacktrip_hubserver_jitter.png){width=450}
+
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644 (file)
index 0000000..bba465d
--- /dev/null
@@ -0,0 +1,2 @@
+mkdocs-material
+mkdocs-macros-plugin
diff --git a/documentation/documentation.cpp b/documentation/documentation.cpp
new file mode 100644 (file)
index 0000000..e19bf51
--- /dev/null
@@ -0,0 +1,58 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008 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 documentation.cpp
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+// Main Page Documentation
+//-----------------------------------------------------------------
+/**
+\mainpage JackTrip API Documentation
+
+\section intro_sec About JackTrip
+
+JackTrip: A System for High-Quality Audio Network Performance
+over the Internet.
+
+JackTrip is a multi-machine network music
+performance system over the Internet. It supports any number of channels
+(as many as the computer/network can handle) of bidirectional, high quality,
+uncompressed audio signal steaming.
+
+General information for users and developers can be found at
+<a href="https://jacktrip.github.io/jacktrip/">the JackTrip documentation</a>.
+
+This is JackTrip's API documentation.
+
+*/
diff --git a/documentation/footer.html b/documentation/footer.html
new file mode 100644 (file)
index 0000000..efa3357
--- /dev/null
@@ -0,0 +1,17 @@
+<!-- HTML footer for doxygen 1.9.1-->
+<!-- start footer part -->
+<!--BEGIN GENERATE_TREEVIEW-->
+<div id="nav-path" class="navpath"><!-- id is needed for treeview function! -->
+  <ul>
+    $navpath
+    <li class="footer">$generatedby <a href="https://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion </li>
+  </ul>
+</div>
+<!--END GENERATE_TREEVIEW-->
+<!--BEGIN !GENERATE_TREEVIEW-->
+<hr class="footer"/><address class="footer"><small>
+$generatedby&#160;<a href="https://www.doxygen.org/index.html"><img class="footer" src="$relpath^doxygen.svg" width="104" height="31" alt="doxygen"/></a> $doxygenversion
+</small></address>
+<!--END !GENERATE_TREEVIEW-->
+</body>
+</html>
diff --git a/documentation/header.html b/documentation/header.html
new file mode 100644 (file)
index 0000000..9c4751e
--- /dev/null
@@ -0,0 +1,56 @@
+<!-- HTML header for doxygen 1.9.1-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "https://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/xhtml;charset=UTF-8"/>
+<meta http-equiv="X-UA-Compatible" content="IE=9"/>
+<meta name="generator" content="Doxygen $doxygenversion"/>
+<meta name="viewport" content="width=device-width, initial-scale=1"/>
+<!--BEGIN PROJECT_NAME--><title>$projectname: $title</title><!--END PROJECT_NAME-->
+<!--BEGIN !PROJECT_NAME--><title>$title</title><!--END !PROJECT_NAME-->
+<link href="$relpath^tabs.css" rel="stylesheet" type="text/css"/>
+<script type="text/javascript" src="$relpath^jquery.js"></script>
+<script type="text/javascript" src="$relpath^dynsections.js"></script>
+$treeview
+$search
+$mathjax
+<link href="$relpath^$stylesheet" rel="stylesheet" type="text/css" />
+$extrastylesheet
+</head>
+<body>
+<div id="top"><!-- do not remove this div, it is closed by doxygen! -->
+
+<!--BEGIN TITLEAREA-->
+<div id="titlearea">
+<table cellspacing="0" cellpadding="0">
+ <tbody>
+ <tr style="height: 56px;">
+  <!--BEGIN PROJECT_LOGO-->
+  <td id="projectlogo"><img alt="Logo" src="$relpath^$projectlogo"/></td>
+  <!--END PROJECT_LOGO-->
+  <!--BEGIN PROJECT_NAME-->
+  <td id="projectalign" style="padding-left: 0.5em;">
+   <div id="projectname">$projectname
+   <!--BEGIN PROJECT_NUMBER-->&#160;<span id="projectnumber">$projectnumber</span><!--END PROJECT_NUMBER-->
+   </div>
+   <!--BEGIN PROJECT_BRIEF--><div id="projectbrief">$projectbrief</div><!--END PROJECT_BRIEF-->
+  </td>
+  <!--END PROJECT_NAME-->
+  <!--BEGIN !PROJECT_NAME-->
+   <!--BEGIN PROJECT_BRIEF-->
+    <td style="padding-left: 0.5em;">
+    <div id="projectbrief">$projectbrief</div>
+    </td>
+   <!--END PROJECT_BRIEF-->
+  <!--END !PROJECT_NAME-->
+  <!--BEGIN DISABLE_INDEX-->
+   <!--BEGIN SEARCHENGINE-->
+   <td>$searchbox</td>
+   <!--END SEARCHENGINE-->
+  <!--END DISABLE_INDEX-->
+ </tr>
+ </tbody>
+</table>
+</div>
+<!--END TITLEAREA-->
+<!-- end header part -->
diff --git a/documentation/img/jack_main_settings.jpg b/documentation/img/jack_main_settings.jpg
new file mode 100644 (file)
index 0000000..822fde7
Binary files /dev/null and b/documentation/img/jack_main_settings.jpg differ
diff --git a/documentation/img/jack_routing.png b/documentation/img/jack_routing.png
new file mode 100644 (file)
index 0000000..77e42e2
Binary files /dev/null and b/documentation/img/jack_routing.png differ
diff --git a/documentation/img/qjackctl.png b/documentation/img/qjackctl.png
new file mode 100644 (file)
index 0000000..afde566
Binary files /dev/null and b/documentation/img/qjackctl.png differ
diff --git a/externals/Simple-FFT/LICENSE.md b/externals/Simple-FFT/LICENSE.md
new file mode 100644 (file)
index 0000000..cabf715
--- /dev/null
@@ -0,0 +1,21 @@
+Copyright (c) 2013-2020 Dmitry Ivanov
+
+The MIT License (MIT)
+
+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.
diff --git a/externals/Simple-FFT/README.md b/externals/Simple-FFT/README.md
new file mode 100644 (file)
index 0000000..299f320
--- /dev/null
@@ -0,0 +1,98 @@
+Simple-FFT
+==========
+
+**Header-only C++ library implementing fast Fourier transform of 1D, 2D and 3D data.**
+
+### What's this
+
+Simple FFT is a C++ library implementing fast Fourier transform. The implemented FFT is a radix-2 Cooley-Turkey algorithm. This algorithm can't handle transform of data which size is not a power of 2. It is not the most optimal known FFT algorithm.
+
+The library is header-only, you don't need to build anything, just include the files in your project. The user-level interface is simple and reminds the one typically found in mathematical software.
+
+The library is distributed under MIT license
+
+#### Why one more FFT library? Does it have any benefits compared to existing libraries?
+
+Why do humans create things even though they already have something similar? Because they are not satisfied with what they have. So was my impression with existing libraries implementing fast Fourier transforms. My desires were:
+ * Free & open-source library
+ * License allowing to use the library in both open and closed-source software.
+ * Convenient API somewhat similar to the one found in typical mathematical software.
+ * Limited size and dependencies, ideally no dependencies (handy for multiple supported platforms and compilers).
+ * As long as my arrays are not going to be huge, I don't need the fastest FFT in the galaxy, I'd be quite happy with some algorithm getting the job done.
+ * Having said that my arrays are not too large, I still want minimal or no overhead for copying the data to objects of types used in the library.
+
+The last statement deserves some explanation: many popular libraries performing FFT use their own data types, sometimes even their own containers. If you already have the data of your own type/container, you are going to either manually transform (e.g. copy) the data or try to cast pointers. Both ways are not elegant.
+
+I didn't find the solution corresponding to the whole wishlist of mine. So I decided to create my own simple library. I've already mentioned some of disadvantages of such approach:
+ * Not the fastest algorithm known nowadays.
+ * Can't handle data which size is not a power of 2.
+
+The advantages of Simple FFT behind some well-known and widely used libraries are:
+ * Tiny size - just some header files.
+ * No need to build it and link a library to your project.
+ * Can handle 1D, 2D and 3D arrays, extendable for larger dimensions.
+ * Designed to support any _reasonable_ multidimensional array/container type one can imagine.
+
+Again, the last statement needs some details to be revealed: C++ natively supports multidimensional arrays via pointers and also via "std vector of std vectors" approach. However, there are a lot of libraries with their own implementations of multidimensional arrays. I wanted to create a library which in theory can use _any_ type of multidimensional array without data copying or pointer casting. It is not possible to guarantee that my library will work with every multidimensional array type you can imagine but there is only a limited number of restrictions for used types.
+
+#### How the API is convenient? How to actually use the library?
+
+By convenience of API I mean the interface somewhat similar to that found in mathematical software like Mathcad, Matlab, Octave, Scilab etc. The simplest API you can think of is something like `A = FFT(B)`. It is not very easy to efficiently implement in C++ (well, without move semantic of C++11 at least) because with such interface you are going to return the result of B transform by value which means data copying i.e. overhead (unless return by value optimization is employed). So in C++ it is better to return the result by reference. The function can also return boolean which would indicate whether the transform was successful or not. So the simplest interface would look like
+
+```c++
+ b = FFT(A,B); // FFT from A to B
+```
+
+But FFT algorithm requires the knowledge of shape and dimensionality of used arrays. Most multidimensional array implementations can provide this information but they do it in different ways and I wanted something very generic. So I decided to create functions with the same name but different signature depending on the number of dimensions:
+
+```c++
+ b = FFT(A,B,n); // FFT from A to B where A and B are 1D arrays with n elements
+ b = FFT(A,B,n,m); // FFT from A to B where A and B are matrices (2D arrays) with n rows and m columns
+ b = FFT(A,B,n,m,l); // FFT from A to B where A and B are 3D arrays with n rows, m columns and l depth number;
+```
+
+One more thing: if the returned value is false then some error has happened. In order to protect user from debugging into 3rdparty library to figure out what happened I decided to return error description as C-style string (because some people don't use `std::string`). So the interface became looking like this:
+
+```c++
+ const char * error = NULL; // error description
+ b = FFT(A,B,n,error); // FFT from A to B where A and B are 1D arrays with n elements
+ b = FFT(A,B,n,m,error); // FFT from A to B where A and B are matrices (2D arrays) with n rows and m columns
+ b = FFT(A,B,n,m,l,error); // FFT from A to B where A and B are 3D arrays with n rows, m columns and l depth number;
+```
+
+But how about the inverse transform? The flag can be used to tell the forward transform from inverse but I thought that different function names would be easier: FFT for forward transform and IFFT for inverse transform:
+
+```c++
+ const char * error = NULL; // error description
+ b = FFT(A,B,n,error); // forward FFT from A to B where A and B are 1D arrays with n elements
+ b = FFT(A,B,n,m,error); // forward FFT from A to B where A and B are matrices (2D arrays) with n rows and m columns
+ b = FFT(A,B,n,m,l,error); // forward FFT from A to B where A and B are 3D arrays with n rows, m columns and l depth number;
+ b = IFFT(B,A,n,error); // inverse FFT from B to A where A and B are 1D arrays with n elements
+ b = IFFT(B,A,n,m,error); // inverse FFT from B to A where A and B are matrices (2D arrays) with n rows and m columns
+ b = IFFT(B,A,n,m,l,error); // inverse FFT from B to A where A and B are 3D arrays with n rows, m columns and l depth number;
+```
+
+Beyond that there are only two settings:
+* User needs to define two types called `real_type` and `complex_type`. These are needed by design to avoid extra problems with template instantiation
+* User needs to define macro `__USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR` if the multidimensional array type you want to use accesses elements via `operator[]` (for example, if it is native C++ multidimensional array or `boost::multi_array` or something similar). Otherwise the library will attempt to use `operator()` for element access.
+
+That's the whole explanation of API.
+
+#### How does it work with multiple libraries implementing matrices and multidimensional arrays in C++ without being aware of those?
+
+Well, the common generic technique of C++ was used - templates. I also added two `typedef`ed types (`real_type` and `complex_type`) to avoid overcomplication of code. But it's not all about templates - I also tried to implement the most generic element access I could imagine. After a bit of thinking I realized that in terms of interface different libraries implementing multidimensional arrays commonly differ only by their element access operator - some implementations use `operator[]` and others - `operator()`. So I splitted every code section using element access operator into two code blocks - the one with `operator[]` and the one with `operator()`. The switch between them is controlled by macro `__USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR` - if it's defined, `operator[]` is used, if not - `operator()`. User can define this macro in some translation units and not define/undef in other ones - just like I did with unit-tests.
+
+#### Are there any examples or, even better, tests?
+I wrote some tests illustrating how to use SimpleFFT library along with some well-known C++ libraries implementing multidimensional arrays. They can also serve as examples (probably, a bit overcomplicated). The tests are based on the following checks:
+* after each forward of inverse FFT [Parseval's theorem](https://en.wikipedia.org/wiki/Parseval%27s_theorem) must be satisfied for input and output data
+* after each sequence of FFT and IFFT the energy conservation law must be satisfied
+* after each sequence of FFT and IFFT the result should be the very same as initial input; so I decided to measure the largest discrepancy between the result and input and calculate the relative error there. If it is small enough (less than 0.01%), this test is considered passed.
+
+I also implemented a simple benchmark test comparing the execution time of multiple loops of Simple FFT and fftw3. The results for Linux with three compilers can be found in the "benchmark-tests" folder. As expected, fftw3 is much faster.
+
+#### There are multiple files here, which I should use?
+There are only two files of interest for library user:
+* `/include/simple_fft/fft_settings.h`
+* `/include/simple_fft/fft.h`
+
+The first one is supposed to contain `typedef`s for `real_type` and `complex_type` and, if needed, the define of `__USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR` macro. This stuff can also be done somewhere else. The second file is what's actually needed to calculate FFT and IFFT: it contains only API declarations and includes some of other files.
diff --git a/externals/Simple-FFT/include/simple_fft/check_fft.hpp b/externals/Simple-FFT/include/simple_fft/check_fft.hpp
new file mode 100644 (file)
index 0000000..b1b95c9
--- /dev/null
@@ -0,0 +1,571 @@
+/**
+ * Copyright (c) 2013-2020 Dmitry Ivanov
+ *
+ * This file is a part of Simple-FFT project and is distributed under the terms
+ * of MIT license: https://opensource.org/licenses/MIT
+ */
+
+#ifndef __SIMPLE_FFT__CHECK_FFT_HPP__
+#define __SIMPLE_FFT__CHECK_FFT_HPP__
+
+#include "fft_settings.h"
+#include "error_handling.hpp"
+#include "copy_array.hpp"
+#include <cstddef>
+#include <cmath>
+#include <numeric>
+
+using std::size_t;
+
+namespace simple_fft {
+namespace check_fft_private {
+
+enum CheckMode
+{
+    CHECK_FFT_PARSEVAL,
+    CHECK_FFT_ENERGY,
+    CHECK_FFT_EQUALITY
+};
+
+template <class TArray1D, class TComplexArray1D>
+void getMaxAbsoluteAndRelativeErrorNorms(const TArray1D & array1,
+                                         const TComplexArray1D & array2, const size_t size,
+                                         real_type & max_absolute_error_norm,
+                                         real_type & max_relative_error_norm)
+{
+    using std::abs;
+
+    real_type current_error;
+
+    // NOTE: no parallelization here, it is a completely sequential loop!
+    for(size_t i = 0; i < size; ++i) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+        current_error = abs(array1[i] - array2[i]);
+#else
+        current_error = abs(array1(i) - array2(i));
+#endif
+        if (current_error > max_absolute_error_norm) {
+            max_absolute_error_norm = current_error;
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+            if (abs(array1[i]) > abs(array2[i])) {
+                max_relative_error_norm = (abs(array1[i]) > 1e-20
+                                           ? max_absolute_error_norm / abs(array1[i])
+                                           : 0.0);
+            }
+            else {
+                max_relative_error_norm = (abs(array2[i]) > 1e-20
+                                           ? max_absolute_error_norm / abs(array2[i])
+                                           : 0.0);
+            }
+#else
+            if (abs(array1(i)) > abs(array2(i))) {
+                max_relative_error_norm = (abs(array1(i)) > 1e-20
+                                           ? max_absolute_error_norm / abs(array1(i))
+                                           : 0.0);
+            }
+            else {
+                max_relative_error_norm = (abs(array2(i)) > 1e-20
+                                           ? max_absolute_error_norm / abs(array2(i))
+                                           : 0.0);
+            }
+#endif
+        }
+    }
+}
+
+template <class TArray2D, class TComplexArray2D>
+void getMaxAbsoluteAndRelativeErrorNorms(const TArray2D & array1,
+                                         const TComplexArray2D & array2,
+                                         const size_t size1, const size_t size2,
+                                         real_type & max_absolute_error_norm,
+                                         real_type & max_relative_error_norm)
+{
+    using std::abs;
+
+    real_type current_error;
+
+    // NOTE: no parallelization here, it is a completely sequential loop!
+    for(int i = 0; i < static_cast<int>(size1); ++i) {
+        for(int j = 0; j < static_cast<int>(size2); ++j) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+            current_error = abs(array1[i][j] - array2[i][j]);
+#else
+            current_error = abs(array1(i,j) - array2(i,j));
+#endif
+            if (current_error > max_absolute_error_norm) {
+                max_absolute_error_norm = current_error;
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                if (abs(array1[i][j]) > abs(array2[i][j])) {
+                    max_relative_error_norm = (abs(array1[i][j]) > 1e-20
+                                               ? max_absolute_error_norm / abs(array1[i][j])
+                                               : 0.0);
+                }
+                else {
+                    max_relative_error_norm = (abs(array2[i][j]) > 1e-20
+                                               ? max_absolute_error_norm / abs(array2[i][j])
+                                               : 0.0);
+                }
+#else
+                if (abs(array1(i,j)) > abs(array2(i,j))) {
+                    max_relative_error_norm = (abs(array1(i,j)) > 1e-20
+                                               ? max_absolute_error_norm / abs(array1(i,j))
+                                               : 0.0);
+                }
+                else {
+                    max_relative_error_norm = (abs(array2(i,j)) > 1e-20
+                                               ? max_absolute_error_norm / abs(array2(i,j))
+                                               : 0.0);
+                }
+#endif
+            }
+        }
+    }
+}
+
+template <class TArray3D, class TComplexArray3D>
+void getMaxAbsoluteAndRelativeErrorNorms(const TArray3D & array1, const TComplexArray3D & array2,
+                                         const size_t size1, const size_t size2,
+                                         const size_t size3, real_type & max_absolute_error_norm,
+                                         real_type & max_relative_error_norm)
+{
+    using std::abs;
+
+    real_type current_error;
+
+    // NOTE: no parallelization here, it is a completely sequential loop!
+    for(int i = 0; i < static_cast<int>(size1); ++i) {
+        for(int j = 0; j < static_cast<int>(size2); ++j) {
+            for(int k = 0; k < static_cast<int>(size3); ++k) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                current_error = abs(array1[i][j][k] - array2[i][j][k]);
+#else
+                current_error = abs(array1(i,j,k) - array2(i,j,k));
+#endif
+                if (current_error > max_absolute_error_norm) {
+                    max_absolute_error_norm = current_error;
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                    if (abs(array1[i][j][k]) > abs(array2[i][j][k])) {
+                        max_relative_error_norm = (abs(array1[i][j][k]) > 1e-20
+                                                   ? max_absolute_error_norm / abs(array1[i][j][k])
+                                                   : 0.0);
+                    }
+                    else {
+                        max_relative_error_norm = (abs(array2[i][j][k]) > 1e-20
+                                                   ? max_absolute_error_norm / abs(array2[i][j][k])
+                                                   : 0.0);
+                    }
+#else
+                    if (abs(array1(i,j,k)) > abs(array2(i,j,k))) {
+                        max_relative_error_norm = (abs(array1(i,j,k)) > 1e-20
+                                                   ? max_absolute_error_norm / abs(array1(i,j,k))
+                                                   : 0.0);
+                    }
+                    else {
+                        max_relative_error_norm = (abs(array2(i,j,k)) > 1e-20
+                                                   ? max_absolute_error_norm / abs(array2(i,j,k))
+                                                   : 0.0);
+                    }
+#endif
+                }
+            }
+        }
+    }
+}
+
+template <class TArray1D>
+real_type squareAbsAccumulate(const TArray1D & array, const size_t size,
+                              const real_type init)
+{
+    int size_signed = static_cast<int>(size);
+    real_type sum = init;
+
+    using std::abs;
+
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for reduction(+:sum)
+#endif
+#endif
+    for(int i = 0; i < size_signed; ++i) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+        sum += abs(array[i] * array[i]);
+#else
+        sum += abs(array(i) * array(i));
+#endif
+    }
+
+    return sum;
+}
+
+template <class TArray2D>
+real_type squareAbsAccumulate(const TArray2D & array, const size_t size1,
+                              const size_t size2, const real_type init)
+{
+    int size1_signed = static_cast<int>(size1);
+    int size2_signed = static_cast<int>(size2);
+    real_type sum = init;
+
+    using std::abs;
+
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for reduction(+:sum)
+#endif
+#endif
+    for(int i = 0; i < size1_signed; ++i) {
+        for(int j = 0; j < size2_signed; ++j) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+            sum += abs(array[i][j] * array[i][j]);
+#else
+            sum += abs(array(i,j) * array(i,j));
+#endif
+        }
+    }
+
+    return sum;
+}
+
+template <class TArray3D>
+real_type squareAbsAccumulate(const TArray3D & array, const size_t size1,
+                              const size_t size2, const size_t size3,
+                              const real_type init)
+{
+    int size1_signed = static_cast<int>(size1);
+    int size2_signed = static_cast<int>(size2);
+    int size3_signed = static_cast<int>(size3);
+    real_type sum = init;
+
+    using std::abs;
+
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for reduction(+:sum)
+#endif
+#endif
+    for(int i = 0; i < size1_signed; ++i) {
+        for(int j = 0; j < size2_signed; ++j) {
+            for(int k = 0; k < size3_signed; ++k) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                sum += abs(array[i][j][k] * array[i][j][k]);
+#else
+                sum += abs(array(i,j,k) * array(i,j,k));
+#endif
+            }
+        }
+    }
+
+    return sum;
+}
+
+// Generic template for CCheckFFT struct followed by its explicit specializations
+// for certain numbers of dimensions. TArray can be either of real or complex type.
+// The technique is similar to the one applied for CFFT struct.
+template <class TArray, class TComplexArray, int NumDims>
+struct CCheckFFT
+{};
+
+template <class TArray1D, class TComplexArray1D>
+struct CCheckFFT<TArray1D,TComplexArray1D,1>
+{
+    static bool check_fft(const TArray1D & data_before,
+                          const TComplexArray1D & data_after,
+                          const size_t size, const real_type relative_tolerance,
+                          real_type & discrepancy, const CheckMode check_mode,
+                          const char *& error_description)
+    {
+        using namespace error_handling;
+
+        if(0 == size) {
+            GetErrorDescription(EC_NUM_OF_ELEMS_IS_ZERO, error_description);
+            return false;
+        }
+
+        if ( (CHECK_FFT_PARSEVAL != check_mode) &&
+             (CHECK_FFT_ENERGY   != check_mode) &&
+             (CHECK_FFT_EQUALITY != check_mode) )
+        {
+            GetErrorDescription(EC_WRONG_CHECK_FFT_MODE, error_description);
+            return false;
+        }
+
+        if (CHECK_FFT_EQUALITY != check_mode)
+        {
+            real_type sum_before = squareAbsAccumulate<TArray1D>(data_before, size, 0.0);
+            real_type sum_after  = squareAbsAccumulate<TComplexArray1D>(data_after, size, 0.0);
+
+            if (CHECK_FFT_PARSEVAL == check_mode) {
+                sum_after /= size;
+            }
+
+            using std::abs;
+
+            discrepancy = abs(sum_before - sum_after);
+
+            if (discrepancy / ((sum_before < 1e-20) ? (sum_before + 1e-20) : sum_before) > relative_tolerance) {
+                GetErrorDescription(EC_RELATIVE_ERROR_TOO_LARGE, error_description);
+                return false;
+            }
+            else {
+                return true;
+            }
+        }
+        else {
+            real_type relative_error;
+            getMaxAbsoluteAndRelativeErrorNorms(data_before, data_after, size,
+                                                discrepancy, relative_error);
+            if (relative_error < relative_tolerance) {
+                return true;
+            }
+            else {
+                GetErrorDescription(EC_RELATIVE_ERROR_TOO_LARGE, error_description);
+                return false;
+            }
+        }
+    }
+};
+
+template <class TArray2D, class TComplexArray2D>
+struct CCheckFFT<TArray2D,TComplexArray2D,2>
+{
+    static bool check_fft(const TArray2D & data_before,
+                          const TComplexArray2D & data_after,
+                          const size_t size1, const size_t size2,
+                          const real_type relative_tolerance, real_type & discrepancy,
+                          const CheckMode check_mode, const char *& error_description)
+    {
+        using namespace error_handling;
+
+        if( (0 == size1) || (0 == size2) ) {
+            GetErrorDescription(EC_NUM_OF_ELEMS_IS_ZERO, error_description);
+            return false;
+        }
+
+        if ( (CHECK_FFT_PARSEVAL != check_mode) &&
+             (CHECK_FFT_ENERGY   != check_mode) &&
+             (CHECK_FFT_EQUALITY != check_mode) )
+        {
+            GetErrorDescription(EC_WRONG_CHECK_FFT_MODE, error_description);
+            return false;
+        }
+
+        if (CHECK_FFT_EQUALITY != check_mode)
+        {
+            real_type sum_before = squareAbsAccumulate<TArray2D>(data_before, size1, size2, 0.0);
+            real_type sum_after  = squareAbsAccumulate<TComplexArray2D>(data_after, size1, size2, 0.0);
+
+            if (CHECK_FFT_PARSEVAL == check_mode) {
+                sum_after /= size1 * size2;
+            }
+
+            using std::abs;
+
+            discrepancy = abs(sum_before - sum_after);
+
+            if (discrepancy / ((sum_before < 1e-20) ? (sum_before + 1e-20) : sum_before) > relative_tolerance) {
+                GetErrorDescription(EC_RELATIVE_ERROR_TOO_LARGE, error_description);
+                return false;
+            }
+            else {
+                return true;
+            }
+        }
+        else {
+            real_type relative_error;
+            getMaxAbsoluteAndRelativeErrorNorms(data_before, data_after, size1,
+                                                size2, discrepancy, relative_error);
+            if (relative_error < relative_tolerance) {
+                return true;
+            }
+            else {
+                GetErrorDescription(EC_RELATIVE_ERROR_TOO_LARGE, error_description);
+                return false;
+            }
+        }
+    }
+};
+
+template <class TArray3D, class TComplexArray3D>
+struct CCheckFFT<TArray3D,TComplexArray3D,3>
+{
+    static bool check_fft(const TArray3D & data_before,
+                          const TComplexArray3D & data_after,
+                          const size_t size1, const size_t size2, const size_t size3,
+                          const real_type relative_tolerance, real_type & discrepancy,
+                          const CheckMode check_mode, const char *& error_description)
+    {
+        using namespace error_handling;
+
+        if( (0 == size1) || (0 == size2) || (0 == size3) ) {
+            GetErrorDescription(EC_NUM_OF_ELEMS_IS_ZERO, error_description);
+            return false;
+        }
+
+        if ( (CHECK_FFT_PARSEVAL != check_mode) &&
+             (CHECK_FFT_ENERGY   != check_mode) &&
+             (CHECK_FFT_EQUALITY != check_mode) )
+        {
+            GetErrorDescription(EC_WRONG_CHECK_FFT_MODE, error_description);
+            return false;
+        }
+
+        if (CHECK_FFT_EQUALITY != check_mode)
+        {
+            real_type sum_before = squareAbsAccumulate<TArray3D>(data_before, size1, size2, size3, 0.0);
+            real_type sum_after  = squareAbsAccumulate<TComplexArray3D>(data_after, size1, size2, size3, 0.0);
+
+            if (CHECK_FFT_PARSEVAL == check_mode) {
+                sum_after /= size1 * size2 * size3;
+            }
+
+            using std::abs;
+
+            discrepancy = abs(sum_before - sum_after);
+
+            if (discrepancy / ((sum_before < 1e-20) ? (sum_before + 1e-20) : sum_before) > relative_tolerance) {
+                GetErrorDescription(EC_RELATIVE_ERROR_TOO_LARGE, error_description);
+                return false;
+            }
+            else {
+                return true;
+            }
+        }
+        else {
+            real_type relative_error;
+            getMaxAbsoluteAndRelativeErrorNorms(data_before, data_after, size1,
+                                                size2, size3, discrepancy, relative_error);
+            if (relative_error < relative_tolerance) {
+                return true;
+            }
+            else {
+                GetErrorDescription(EC_RELATIVE_ERROR_TOO_LARGE, error_description);
+                return false;
+            }
+        }
+    }
+};
+
+} // namespace check_fft_private
+
+namespace check_fft {
+
+template <class TArray1D, class TComplexArray1D>
+bool checkParsevalTheorem(const TArray1D & data_before_FFT,
+                          const TComplexArray1D & data_after_FFT,
+                          const size_t size, const real_type relative_tolerance,
+                          real_type & discrepancy, const char *& error_description)
+{
+    return check_fft_private::CCheckFFT<TArray1D,TComplexArray1D,1>::check_fft(data_before_FFT,
+                                             data_after_FFT, size, relative_tolerance,
+                                             discrepancy, check_fft_private::CHECK_FFT_PARSEVAL,
+                                             error_description);
+}
+
+template <class TArray2D, class TComplexArray2D>
+bool checkParsevalTheorem(const TArray2D & data_before_FFT,
+                          const TComplexArray2D & data_after_FFT,
+                          const size_t size1, const size_t size2,
+                          const real_type relative_tolerance,
+                          real_type & discrepancy, const char *& error_description)
+{
+    return check_fft_private::CCheckFFT<TArray2D,TComplexArray2D,2>::check_fft(data_before_FFT,
+                                             data_after_FFT, size1, size2, relative_tolerance,
+                                             discrepancy, check_fft_private::CHECK_FFT_PARSEVAL,
+                                             error_description);
+}
+
+template <class TArray3D, class TComplexArray3D>
+bool checkParsevalTheorem(const TArray3D & data_before_FFT,
+                          const TComplexArray3D & data_after_FFT,
+                          const size_t size1, const size_t size2, const size_t size3,
+                          const real_type relative_tolerance, real_type & discrepancy,
+                          const char *& error_description)
+{
+    return check_fft_private::CCheckFFT<TArray3D,TComplexArray3D,3>::check_fft(data_before_FFT,
+                                                  data_after_FFT, size1, size2, size3,
+                                                  relative_tolerance, discrepancy,
+                                                  check_fft_private::CHECK_FFT_PARSEVAL,
+                                                  error_description);
+}
+
+template <class TArray1D, class TComplexArray1D>
+bool checkEnergyConservation(const TArray1D & data_before_FFT,
+                             const TComplexArray1D & data_after_FFT_and_IFFT,
+                             const size_t size, const real_type relative_tolerance,
+                             real_type & discrepancy, const char *& error_description)
+{
+    return check_fft_private::CCheckFFT<TArray1D,TComplexArray1D,1>::check_fft(data_before_FFT,
+                                    data_after_FFT_and_IFFT, size, relative_tolerance,
+                                    discrepancy, check_fft_private::CHECK_FFT_ENERGY,
+                                    error_description);
+}
+
+template <class TArray2D, class TComplexArray2D>
+bool checkEnergyConservation(const TArray2D & data_before_FFT,
+                             const TComplexArray2D & data_after_FFT_and_IFFT,
+                             const size_t size1, const size_t size2,
+                             const real_type relative_tolerance,
+                             real_type & discrepancy, const char *& error_description)
+{
+    return check_fft_private::CCheckFFT<TArray2D,TComplexArray2D,2>::check_fft(data_before_FFT,
+                                                data_after_FFT_and_IFFT, size1, size2,
+                                                relative_tolerance, discrepancy,
+                                                check_fft_private::CHECK_FFT_ENERGY,
+                                                error_description);
+}
+
+template <class TArray3D, class TComplexArray3D>
+bool checkEnergyConservation(const TArray3D & data_before_FFT,
+                             const TComplexArray3D & data_after_FFT_and_IFFT,
+                             const size_t size1, const size_t size2, const size_t size3,
+                             const real_type relative_tolerance, real_type & discrepancy,
+                             const char *& error_description)
+{
+    return check_fft_private::CCheckFFT<TArray3D,TComplexArray3D,3>::check_fft(data_before_FFT,
+                                                data_after_FFT_and_IFFT, size1, size2,
+                                                size3, relative_tolerance, discrepancy,
+                                                check_fft_private::CHECK_FFT_ENERGY,
+                                                error_description);
+}
+
+template <class TArray1D, class TComplexArray1D>
+bool checkEquality(const TArray1D & data_before_FFT,
+                   const TComplexArray1D & data_after_FFT_and_IFFT,
+                   const size_t size, const real_type relative_tolerance,
+                   real_type & discrepancy, const char *& error_description)
+{
+    return check_fft_private::CCheckFFT<TArray1D,TComplexArray1D,1>::check_fft(data_before_FFT,
+                                             data_after_FFT_and_IFFT, size, relative_tolerance,
+                                             discrepancy, check_fft_private::CHECK_FFT_EQUALITY,
+                                             error_description);
+}
+
+template <class TArray2D, class TComplexArray2D>
+bool checkEquality(const TArray2D & data_before_FFT,
+                   const TComplexArray2D & data_after_FFT_and_IFFT, const size_t size1,
+                   const size_t size2, const real_type relative_tolerance,
+                   real_type & discrepancy, const char *& error_description)
+{
+    return check_fft_private::CCheckFFT<TArray2D,TComplexArray2D,2>::check_fft(data_before_FFT,
+                                                         data_after_FFT_and_IFFT, size1, size2,
+                                                         relative_tolerance, discrepancy,
+                                                         check_fft_private::CHECK_FFT_EQUALITY,
+                                                         error_description);
+}
+
+template <class TArray3D, class TComplexArray3D>
+bool checkEquality(const TArray3D & data_before_FFT,
+                   const TComplexArray3D & data_after_FFT_and_IFFT, const size_t size1,
+                   const size_t size2, const size_t size3, const real_type relative_tolerance,
+                   real_type & discrepancy, const char *& error_description)
+{
+    return check_fft_private::CCheckFFT<TArray3D,TComplexArray3D,3>::check_fft(data_before_FFT,
+                                                         data_after_FFT_and_IFFT, size1, size2,
+                                                         size3, relative_tolerance, discrepancy,
+                                                         check_fft_private::CHECK_FFT_EQUALITY,
+                                                         error_description);
+}
+
+} // namespace check_fft
+} // namespace simple_fft
+
+#endif // __SIMPLE_FFT__CHECK_FFT_HPP__
diff --git a/externals/Simple-FFT/include/simple_fft/copy_array.hpp b/externals/Simple-FFT/include/simple_fft/copy_array.hpp
new file mode 100644 (file)
index 0000000..bb4c076
--- /dev/null
@@ -0,0 +1,173 @@
+/**\r
+ * Copyright (c) 2013-2020 Dmitry Ivanov\r
+ *\r
+ * This file is a part of Simple-FFT project and is distributed under the terms\r
+ * of MIT license: https://opensource.org/licenses/MIT\r
+ */\r
+\r
+#ifndef __SIMPLE_FFT__COPY_ARRAY_HPP\r
+#define __SIMPLE_FFT__COPY_ARRAY_HPP\r
+\r
+#include "fft_settings.h"\r
+#include "error_handling.hpp"\r
+#include <cstddef>\r
+\r
+using std::size_t;\r
+\r
+namespace simple_fft {\r
+namespace copy_array {\r
+\r
+template <class TComplexArray1D>\r
+void copyArray(const TComplexArray1D & data_from, TComplexArray1D & data_to,\r
+               const size_t size)\r
+{\r
+    int size_signed = static_cast<int>(size);\r
+\r
+#ifndef __clang__\r
+#ifdef __USE_OPENMP\r
+#pragma omp parallel for\r
+#endif\r
+#endif\r
+    for(int i = 0; i < size_signed; ++i) {\r
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR\r
+        data_to[i] = data_from[i];\r
+#else\r
+        data_to(i) = data_from(i);\r
+#endif\r
+    }\r
+}\r
+\r
+template <class TComplexArray1D, class TRealArray1D>\r
+void copyArray(const TRealArray1D & data_from, TComplexArray1D & data_to,\r
+               const size_t size)\r
+{\r
+    int size_signed = static_cast<int>(size);\r
+\r
+    // NOTE: user's complex type should have constructor like\r
+    // "complex(real, imag)", where each of real and imag has\r
+    // real type.\r
+\r
+#ifndef __clang__\r
+#ifdef __USE_OPENMP\r
+#pragma omp parallel for\r
+#endif\r
+#endif\r
+    for(int i = 0; i < size_signed; ++i) {\r
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR\r
+        data_to[i] = complex_type(data_from[i], 0.0);\r
+#else\r
+        data_to(i) = complex_type(data_from(i), 0.0);\r
+#endif\r
+    }\r
+}\r
+\r
+template <class TComplexArray2D>\r
+void copyArray(const TComplexArray2D & data_from, TComplexArray2D & data_to,\r
+               const size_t size1, const size_t size2)\r
+{\r
+    int size1_signed = static_cast<int>(size1);\r
+    int size2_signed = static_cast<int>(size2);\r
+\r
+#ifndef __clang__\r
+#ifdef __USE_OPENMP\r
+#pragma omp parallel for\r
+#endif\r
+#endif\r
+    for(int i = 0; i < size1_signed; ++i) {\r
+        for(int j = 0; j < size2_signed; ++j) {\r
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR\r
+            data_to[i][j] = data_from[i][j];\r
+#else\r
+            data_to(i,j) = data_from(i,j);\r
+#endif\r
+        }\r
+    }\r
+}\r
+\r
+template <class TComplexArray2D, class TRealArray2D>\r
+void copyArray(const TRealArray2D & data_from, TComplexArray2D & data_to,\r
+               const size_t size1, const size_t size2)\r
+{\r
+    int size1_signed = static_cast<int>(size1);\r
+    int size2_signed = static_cast<int>(size2);\r
+\r
+    // NOTE: user's complex type should have constructor like\r
+    // "complex(real, imag)", where each of real and imag has\r
+    // real type.\r
+\r
+#ifndef __clang__\r
+#ifdef __USE_OPENMP\r
+#pragma omp parallel for\r
+#endif\r
+#endif\r
+    for(int i = 0; i < size1_signed; ++i) {\r
+        for(int j = 0; j < size2_signed; ++j) {\r
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR\r
+            data_to[i][j] = complex_type(data_from[i][j], 0.0);\r
+#else\r
+            data_to(i,j) = complex_type(data_from(i,j), 0.0);\r
+#endif\r
+        }\r
+    }\r
+}\r
+\r
+template <class TComplexArray3D>\r
+void copyArray(const TComplexArray3D & data_from, TComplexArray3D & data_to,\r
+               const size_t size1, const size_t size2, const size_t size3)\r
+{\r
+    int size1_signed = static_cast<int>(size1);\r
+    int size2_signed = static_cast<int>(size2);\r
+    int size3_signed = static_cast<int>(size3);\r
+\r
+#ifndef __clang__\r
+#ifdef __USE_OPENMP\r
+#pragma omp parallel for\r
+#endif\r
+#endif\r
+    for(int i = 0; i < size1_signed; ++i) {\r
+        for(int j = 0; j < size2_signed; ++j) {\r
+            for(int k = 0; k < size3_signed; ++k) {\r
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR\r
+                data_to[i][j][k] = data_from[i][j][k];\r
+#else\r
+                data_to(i,j,k) = data_from(i,j,k);\r
+#endif\r
+            }\r
+        }\r
+    }\r
+}\r
+\r
+template <class TComplexArray3D, class TRealArray3D>\r
+void copyArray(const TRealArray3D & data_from, TComplexArray3D & data_to,\r
+               const size_t size1, const size_t size2, const size_t size3)\r
+{\r
+    int size1_signed = static_cast<int>(size1);\r
+    int size2_signed = static_cast<int>(size2);\r
+    int size3_signed = static_cast<int>(size3);\r
+\r
+    // NOTE: user's complex type should have constructor like\r
+    // "complex(real, imag)", where each of real and imag has\r
+    // real type.\r
+\r
+#ifndef __clang__\r
+#ifdef __USE_OPENMP\r
+#pragma omp parallel for\r
+#endif\r
+#endif\r
+    for(int i = 0; i < size1_signed; ++i) {\r
+        for(int j = 0; j < size2_signed; ++j) {\r
+            for(int k = 0; k < size3_signed; ++k) {\r
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR\r
+                data_to[i][j][k] = complex_type(data_from[i][j][k], 0.0);\r
+#else\r
+                data_to(i,j,k) = complex_type(data_from(i,j,k), 0.0);\r
+#endif\r
+            }\r
+        }\r
+    }\r
+}\r
+\r
+} // namespace copy_array\r
+} // namespace simple_fft\r
+\r
+#endif // __SIMPLE_FFT__COPY_ARRAY_HPP\r
diff --git a/externals/Simple-FFT/include/simple_fft/error_handling.hpp b/externals/Simple-FFT/include/simple_fft/error_handling.hpp
new file mode 100644 (file)
index 0000000..608ecc9
--- /dev/null
@@ -0,0 +1,64 @@
+/**\r
+ * Copyright (c) 2013-2020 Dmitry Ivanov\r
+ *\r
+ * This file is a part of Simple-FFT project and is distributed under the terms\r
+ * of MIT license: https://opensource.org/licenses/MIT\r
+ */\r
+\r
+#ifndef __SIMPLE_FFT__ERROR_HANDLING_HPP\r
+#define __SIMPLE_FFT__ERROR_HANDLING_HPP\r
+\r
+namespace simple_fft {\r
+namespace error_handling {\r
+\r
+enum EC_SimpleFFT\r
+{\r
+    EC_SUCCESS = 0,\r
+    EC_UNSUPPORTED_DIMENSIONALITY,\r
+    EC_WRONG_FFT_DIRECTION,\r
+    EC_ONE_OF_DIMS_ISNT_POWER_OF_TWO,\r
+    EC_NUM_OF_ELEMS_IS_ZERO,\r
+    EC_WRONG_CHECK_FFT_MODE,\r
+    EC_RELATIVE_ERROR_TOO_LARGE\r
+};\r
+\r
+inline void GetErrorDescription(const EC_SimpleFFT error_code,\r
+                                const char *& error_description)\r
+{\r
+    switch(error_code)\r
+    {\r
+    case EC_SUCCESS:\r
+        error_description = "Calculation was successful!";\r
+        break;\r
+    case EC_UNSUPPORTED_DIMENSIONALITY:\r
+        error_description = "Unsupported dimensionality: currently only 1D, 2D "\r
+                            "and 3D arrays are supported";\r
+        break;\r
+    case EC_WRONG_FFT_DIRECTION:\r
+        error_description = "Wrong direction for FFT was specified";\r
+        break;\r
+    case EC_ONE_OF_DIMS_ISNT_POWER_OF_TWO:\r
+        error_description = "Unsupported dimensionality: one of dimensions is not "\r
+                            "a power of 2";\r
+        break;\r
+    case EC_NUM_OF_ELEMS_IS_ZERO:\r
+        error_description = "Number of elements for FFT or IFFT is zero!";\r
+        break;\r
+    case EC_WRONG_CHECK_FFT_MODE:\r
+        error_description = "Wrong check FFT mode was specified (should be either "\r
+                            "Parseval theorem or energy conservation check";\r
+        break;\r
+    case EC_RELATIVE_ERROR_TOO_LARGE:\r
+        error_description = "Relative error returned by FFT test exceeds specified "\r
+                            "relative tolerance";\r
+        break;\r
+    default:\r
+        error_description = "Unknown error";\r
+        break;\r
+    }\r
+}\r
+\r
+} // namespace error_handling\r
+} // namespace simple_fft\r
+\r
+#endif // __SIMPLE_FFT__ERROR_HANDLING_HPP\r
diff --git a/externals/Simple-FFT/include/simple_fft/fft.h b/externals/Simple-FFT/include/simple_fft/fft.h
new file mode 100644 (file)
index 0000000..3cf77f8
--- /dev/null
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2013-2020 Dmitry Ivanov
+ *
+ * This file is a part of Simple-FFT project and is distributed under the terms
+ * of MIT license: https://opensource.org/licenses/MIT
+ */
+
+#ifndef __SIMPLE_FFT__FFT_H__
+#define __SIMPLE_FFT__FFT_H__
+
+#include <cstddef>
+
+using std::size_t;
+
+/// The public API
+namespace simple_fft {
+
+/// FFT and IFFT functions
+
+// in-place, complex, forward
+template <class TComplexArray1D>
+bool FFT(TComplexArray1D & data, const size_t size, const char *& error_description);
+
+template <class TComplexArray2D>
+bool FFT(TComplexArray2D & data, const size_t size1, const size_t size2,
+         const char *& error_description);
+
+template <class TComplexArray3D>
+bool FFT(TComplexArray3D & data, const size_t size1, const size_t size2, const size_t size3,
+         const char *& error_description);
+
+// in-place, complex, inverse
+template <class TComplexArray1D>
+bool IFFT(TComplexArray1D & data, const size_t size, const char *& error_description);
+
+template <class TComplexArray2D>
+bool IFFT(TComplexArray2D & data, const size_t size1, const size_t size2,
+          const char *& error_description);
+
+template <class TComplexArray3D>
+bool IFFT(TComplexArray3D & data, const size_t size1, const size_t size2, const size_t size3,
+          const char *& error_description);
+
+// not-in-place, complex, forward
+template <class TComplexArray1D>
+bool FFT(const TComplexArray1D & data_in, TComplexArray1D & data_out,
+         const size_t size, const char *& error_description);
+
+template <class TComplexArray2D>
+bool FFT(const TComplexArray2D & data_in, TComplexArray2D & data_out,
+         const size_t size1, const size_t size2, const char *& error_description);
+
+template <class TComplexArray3D>
+bool FFT(const TComplexArray3D & data_in, TComplexArray3D & data_out,
+         const size_t size1, const size_t size2, const size_t size3,
+         const char *& error_description);
+
+// not-in-place, complex, inverse
+template <class TComplexArray1D>
+bool IFFT(const TComplexArray1D & data_in, TComplexArray1D & data_out,
+          const size_t size, const char *& error_description);
+
+template <class TComplexArray2D>
+bool IFFT(const TComplexArray2D & data_in, TComplexArray2D & data_out,
+          const size_t size1, const size_t size2, const char *& error_description);
+
+template <class TComplexArray3D>
+bool IFFT(const TComplexArray3D & data_in, TComplexArray3D & data_out,
+          const size_t size1, const size_t size2, const size_t size3,
+          const char *& error_description);
+
+// not-in-place, real, forward
+template <class TRealArray1D, class TComplexArray1D>
+bool FFT(const TRealArray1D & data_in, TComplexArray1D & data_out,
+         const size_t size, const char *& error_description);
+
+template <class TRealArray2D, class TComplexArray2D>
+bool FFT(const TRealArray2D & data_in, TComplexArray2D & data_out,
+         const size_t size1, const size_t size2, const char *& error_description);
+
+template <class TRealArray3D, class TComplexArray3D>
+bool FFT(const TRealArray3D & data_in, TComplexArray3D & data_out,
+         const size_t size1, const size_t size2, const size_t size3,
+         const char *& error_description);
+
+// NOTE: There is no inverse transform from complex spectrum to real signal
+// because round-off errors during computation of inverse FFT lead to the appearance
+// of signal imaginary components even though they are small by absolute value.
+// These can be ignored but the author of this file thinks adding such an function
+// would be wrong methodogically: looking at complex result, you can estimate
+// the value of spurious imaginary part. Otherwise you may never know that IFFT
+// provides too large imaginary values due to too small grid size, for example.
+
+} // namespace simple_fft
+
+#endif // __SIMPLE_FFT__FFT_H__
+
+#include "fft.hpp"
diff --git a/externals/Simple-FFT/include/simple_fft/fft.hpp b/externals/Simple-FFT/include/simple_fft/fft.hpp
new file mode 100644 (file)
index 0000000..dca498d
--- /dev/null
@@ -0,0 +1,162 @@
+/**\r
+ * Copyright (c) 2013-2020 Dmitry Ivanov\r
+ *\r
+ * This file is a part of Simple-FFT project and is distributed under the terms\r
+ * of MIT license: https://opensource.org/licenses/MIT\r
+ */\r
+\r
+#ifndef __SIMPLE_FFT__FFT_HPP__\r
+#define __SIMPLE_FFT__FFT_HPP__\r
+\r
+#include "copy_array.hpp"\r
+#include "fft_impl.hpp"\r
+\r
+namespace simple_fft {\r
+\r
+// in-place, complex, forward\r
+template <class TComplexArray1D>\r
+bool FFT(TComplexArray1D & data, const size_t size, const char *& error_description)\r
+{\r
+    return impl::CFFT<TComplexArray1D,1>::FFT_inplace(data, size, impl::FFT_FORWARD,\r
+                                                      error_description);\r
+}\r
+\r
+template <class TComplexArray2D>\r
+bool FFT(TComplexArray2D & data, const size_t size1, const size_t size2,\r
+         const char *& error_description)\r
+{\r
+    return impl::CFFT<TComplexArray2D,2>::FFT_inplace(data, size1, size2, impl::FFT_FORWARD,\r
+                                                      error_description);\r
+}\r
+\r
+template <class TComplexArray3D>\r
+bool FFT(TComplexArray3D & data, const size_t size1, const size_t size2, const size_t size3,\r
+         const char *& error_description)\r
+{\r
+    return impl::CFFT<TComplexArray3D,3>::FFT_inplace(data, size1, size2, size3,\r
+                                                      impl::FFT_FORWARD,\r
+                                                      error_description);\r
+}\r
+\r
+// in-place, complex, inverse\r
+template <class TComplexArray1D>\r
+bool IFFT(TComplexArray1D & data, const size_t size, const char *& error_description)\r
+{\r
+    return impl::CFFT<TComplexArray1D,1>::FFT_inplace(data, size, impl::FFT_BACKWARD,\r
+                                                      error_description);\r
+}\r
+\r
+template <class TComplexArray2D>\r
+bool IFFT(TComplexArray2D & data, const size_t size1, const size_t size2,\r
+          const char *& error_description)\r
+{\r
+    return impl::CFFT<TComplexArray2D,2>::FFT_inplace(data, size1, size2, impl::FFT_BACKWARD,\r
+                                                      error_description);\r
+}\r
+\r
+template <class TComplexArray3D>\r
+bool IFFT(TComplexArray3D & data, const size_t size1, const size_t size2, const size_t size3,\r
+          const char *& error_description)\r
+{\r
+    return impl::CFFT<TComplexArray3D,3>::FFT_inplace(data, size1, size2, size3,\r
+                                                      impl::FFT_BACKWARD,\r
+                                                      error_description);\r
+}\r
+\r
+// not-in-place, complex, forward\r
+template <class TComplexArray1D>\r
+bool FFT(const TComplexArray1D & data_in, TComplexArray1D & data_out,\r
+         const size_t size, const char *& error_description)\r
+{\r
+    copy_array::copyArray(data_in, data_out, size);\r
+    return impl::CFFT<TComplexArray1D,1>::FFT_inplace(data_out, size, impl::FFT_FORWARD,\r
+                                                      error_description);\r
+}\r
+\r
+template <class TComplexArray2D>\r
+bool FFT(const TComplexArray2D & data_in, TComplexArray2D & data_out,\r
+         const size_t size1, const size_t size2, const char *& error_description)\r
+{\r
+    copy_array::copyArray(data_in, data_out, size1, size2);\r
+    return impl::CFFT<TComplexArray2D,2>::FFT_inplace(data_out, size1, size2,\r
+                                                      impl::FFT_FORWARD,\r
+                                                      error_description);\r
+}\r
+\r
+template <class TComplexArray3D>\r
+bool FFT(const TComplexArray3D & data_in, TComplexArray3D & data_out,\r
+         const size_t size1, const size_t size2, const size_t size3,\r
+         const char *& error_description)\r
+{\r
+    copy_array::copyArray(data_in, data_out, size1, size2, size3);\r
+    return impl::CFFT<TComplexArray3D,3>::FFT_inplace(data_out, size1, size2, size3,\r
+                                                      impl::FFT_FORWARD,\r
+                                                      error_description);\r
+}\r
+\r
+// not-in-place, complex, inverse\r
+template <class TComplexArray1D>\r
+bool IFFT(const TComplexArray1D & data_in, TComplexArray1D & data_out,\r
+          const size_t size, const char *& error_description)\r
+{\r
+    copy_array::copyArray(data_in, data_out, size);\r
+    return impl::CFFT<TComplexArray1D,1>::FFT_inplace(data_out, size, impl::FFT_BACKWARD,\r
+                                                      error_description);\r
+}\r
+\r
+template <class TComplexArray2D>\r
+bool IFFT(const TComplexArray2D & data_in, TComplexArray2D & data_out,\r
+          const size_t size1, const size_t size2, const char *& error_description)\r
+{\r
+    copy_array::copyArray(data_in, data_out, size1, size2);\r
+    return impl::CFFT<TComplexArray2D,2>::FFT_inplace(data_out, size1, size2,\r
+                                                      impl::FFT_BACKWARD,\r
+                                                      error_description);\r
+}\r
+\r
+template <class TComplexArray3D>\r
+bool IFFT(const TComplexArray3D & data_in, TComplexArray3D & data_out,\r
+          const size_t size1, const size_t size2, const size_t size3,\r
+          const char *& error_description)\r
+{\r
+    copy_array::copyArray(data_in, data_out, size1, size2, size3);\r
+    return impl::CFFT<TComplexArray3D,3>::FFT_inplace(data_out, size1, size2, size3,\r
+                                                      impl::FFT_BACKWARD,\r
+                                                      error_description);\r
+}\r
+\r
+// not-in-place, real, forward\r
+template <class TRealArray1D, class TComplexArray1D>\r
+bool FFT(const TRealArray1D & data_in, TComplexArray1D & data_out,\r
+         const size_t size, const char *& error_description)\r
+{\r
+    copy_array::copyArray(data_in, data_out, size);\r
+    return impl::CFFT<TComplexArray1D,1>::FFT_inplace(data_out, size,\r
+                                                      impl::FFT_FORWARD,\r
+                                                      error_description);\r
+}\r
+\r
+template <class TRealArray2D, class TComplexArray2D>\r
+bool FFT(const TRealArray2D & data_in, TComplexArray2D & data_out,\r
+         const size_t size1, const size_t size2, const char *& error_description)\r
+{\r
+    copy_array::copyArray(data_in, data_out, size1, size2);\r
+    return impl::CFFT<TComplexArray2D,2>::FFT_inplace(data_out, size1, size2,\r
+                                                      impl::FFT_FORWARD,\r
+                                                      error_description);\r
+}\r
+\r
+template <class TRealArray3D, class TComplexArray3D>\r
+bool FFT(const TRealArray3D & data_in, TComplexArray3D & data_out,\r
+         const size_t size1, const size_t size2, const size_t size3,\r
+         const char *& error_description)\r
+{\r
+    copy_array::copyArray(data_in, data_out, size1, size2, size3);\r
+    return impl::CFFT<TComplexArray3D,3>::FFT_inplace(data_out, size1, size2, size3,\r
+                                                      impl::FFT_FORWARD,\r
+                                                      error_description);\r
+}\r
+\r
+} // simple_fft\r
+\r
+#endif // __SIMPLE_FFT__FFT_HPP__\r
diff --git a/externals/Simple-FFT/include/simple_fft/fft_impl.hpp b/externals/Simple-FFT/include/simple_fft/fft_impl.hpp
new file mode 100644 (file)
index 0000000..b385f07
--- /dev/null
@@ -0,0 +1,515 @@
+/**
+ * Copyright (c) 2013-2020 Dmitry Ivanov
+ *
+ * This file is a part of Simple-FFT project and is distributed under the terms
+ * of MIT license: https://opensource.org/licenses/MIT
+ */
+
+#ifndef __SIMPLE_FFT__FFT_IMPL_HPP__
+#define __SIMPLE_FFT__FFT_IMPL_HPP__
+
+#include "fft_settings.h"
+#include "error_handling.hpp"
+#include <cstddef>
+#include <math.h>
+#include <vector>
+
+using std::size_t;
+
+#ifndef M_PI
+#define M_PI 3.1415926535897932
+#endif
+
+namespace simple_fft {
+namespace impl {
+
+enum FFT_direction
+{
+    FFT_FORWARD = 0,
+    FFT_BACKWARD
+};
+
+// checking whether the size of array dimension is power of 2
+// via "complement and compare" method
+inline bool isPowerOfTwo(const size_t num)
+{
+    return num && (!(num & (num - 1)));
+}
+
+inline bool checkNumElements(const size_t num_elements, const char *& error_description)
+{
+    using namespace error_handling;
+
+    if (!isPowerOfTwo(num_elements)) {
+        GetErrorDescription(EC_ONE_OF_DIMS_ISNT_POWER_OF_TWO, error_description);
+        return false;
+    }
+
+    return true;
+}
+
+template <class TComplexArray1D>
+inline void scaleValues(TComplexArray1D & data, const size_t num_elements)
+{
+    real_type mult = 1.0 / num_elements;
+    int num_elements_signed = static_cast<int>(num_elements);
+
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+    for(int i = 0; i < num_elements_signed; ++i) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+        data[i] *= mult;
+#else
+        data(i) *= mult;
+#endif
+    }
+}
+
+// NOTE: explicit template specialization for the case of std::vector<complex_type>
+// because it is used in 2D and 3D FFT for both array classes with square and round
+// brackets of element access operator; I need to guarantee that sub-FFT 1D will
+// use square brackets for element access operator anyway. It is pretty ugly
+// to duplicate the code but I haven't found more elegant solution.
+template <>
+inline void scaleValues<std::vector<complex_type> >(std::vector<complex_type> & data,
+                                                    const size_t num_elements)
+{
+    real_type mult = 1.0 / num_elements;
+    int num_elements_signed = static_cast<int>(num_elements);
+
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+    for(int i = 0; i < num_elements_signed; ++i) {
+        data[i] *= mult;
+    }
+}
+
+template <class TComplexArray1D>
+inline void bufferExchangeHelper(TComplexArray1D & data, const size_t index_from,
+                                 const size_t index_to, complex_type & buf)
+{
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+    buf = data[index_from];
+    data[index_from] = data[index_to];
+    data[index_to]= buf;
+#else
+    buf = data(index_from);
+    data(index_from) = data(index_to);
+    data(index_to)= buf;
+#endif
+}
+
+// NOTE: explicit template specialization for the case of std::vector<complex_type>
+// because it is used in 2D and 3D FFT for both array classes with square and round
+// brackets of element access operator; I need to guarantee that sub-FFT 1D will
+// use square brackets for element access operator anyway. It is pretty ugly
+// to duplicate the code but I haven't found more elegant solution.
+template <>
+inline void bufferExchangeHelper<std::vector<complex_type> >(std::vector<complex_type> & data,
+                                                             const size_t index_from,
+                                                             const size_t index_to,
+                                                             complex_type & buf)
+{
+    buf = data[index_from];
+    data[index_from] = data[index_to];
+    data[index_to]= buf;
+}
+
+template <class TComplexArray1D>
+void rearrangeData(TComplexArray1D & data, const size_t num_elements)
+{
+    complex_type buf;
+
+    size_t target_index = 0;
+    size_t bit_mask;
+
+    for (size_t i = 0; i < num_elements; ++i)
+    {
+        if (target_index > i)
+        {
+            bufferExchangeHelper(data, target_index, i, buf);
+        }
+
+        // Initialize the bit mask
+        bit_mask = num_elements;
+
+        // While bit is 1
+        while (target_index & (bit_mask >>= 1)) // bit_mask = bit_mask >> 1
+        {
+            // Drop bit:
+            // & is bitwise AND,
+            // ~ is bitwise NOT
+            target_index &= ~bit_mask; // target_index = target_index & (~bit_mask)
+        }
+
+        // | is bitwise OR
+        target_index |= bit_mask; // target_index = target_index | bit_mask
+    }
+}
+
+template <class TComplexArray1D>
+inline void fftTransformHelper(TComplexArray1D & data, const size_t match,
+                               const size_t k, complex_type & product,
+                               const complex_type factor)
+{
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+    product = data[match] * factor;
+    data[match] = data[k] - product;
+    data[k] += product;
+#else
+    product = data(match) * factor;
+    data(match) = data(k) - product;
+    data(k) += product;
+#endif
+}
+
+// NOTE: explicit template specialization for the case of std::vector<complex_type>
+// because it is used in 2D and 3D FFT for both array classes with square and round
+// brackets of element access operator; I need to guarantee that sub-FFT 1D will
+// use square brackets for element access operator anyway. It is pretty ugly
+// to duplicate the code but I haven't found more elegant solution.
+template <>
+inline void fftTransformHelper<std::vector<complex_type> >(std::vector<complex_type> & data,
+                                                           const size_t match,
+                                                           const size_t k,
+                                                           complex_type & product,
+                                                           const complex_type factor)
+{
+    product = data[match] * factor;
+    data[match] = data[k] - product;
+    data[k] += product;
+}
+
+template <class TComplexArray1D>
+bool makeTransform(TComplexArray1D & data, const size_t num_elements,
+                   const FFT_direction fft_direction, const char *& error_description)
+{
+    using namespace error_handling;
+    using std::sin;
+
+    double local_pi;
+    switch(fft_direction)
+    {
+    case(FFT_FORWARD):
+        local_pi = -M_PI;
+        break;
+    case(FFT_BACKWARD):
+        local_pi = M_PI;
+        break;
+    default:
+        GetErrorDescription(EC_WRONG_FFT_DIRECTION, error_description);
+        return false;
+    }
+
+    // declare variables to cycle the bits of initial signal
+    size_t next, match;
+    real_type sine;
+    real_type delta;
+    complex_type mult, factor, product;
+
+    // NOTE: user's complex type should have constructor like
+    // "complex(real, imag)", where each of real and imag has
+    // real type.
+
+    // cycle for all bit positions of initial signal
+    for (size_t i = 1; i < num_elements; i <<= 1)
+    {
+        next = i << 1;  // getting the next bit
+        delta = local_pi / i;    // angle increasing
+        sine = sin(0.5 * delta);    // supplementary sin
+        // multiplier for trigonometric recurrence
+        mult = complex_type(-2.0 * sine * sine, sin(delta));
+        factor = 1.0;   // start transform factor
+
+        for (size_t j = 0; j < i; ++j) // iterations through groups
+                                       // with different transform factors
+        {
+            for (size_t k = j; k < num_elements; k += next) // iterations through
+                                                            // pairs within group
+            {
+                match = k + i;
+                fftTransformHelper(data, match, k, product, factor);
+            }
+            factor = mult * factor + factor;
+        }
+    }
+
+    return true;
+}
+
+// Generic template for complex FFT followed by its explicit specializations
+template <class TComplexArray, int NumDims>
+struct CFFT
+{};
+
+// 1D FFT:
+template <class TComplexArray1D>
+struct CFFT<TComplexArray1D,1>
+{
+    // NOTE: passing by pointer is needed to avoid using element access operator
+    static bool FFT_inplace(TComplexArray1D & data, const size_t size,
+                            const FFT_direction fft_direction,
+                            const char *& error_description)
+    {
+        if(!checkNumElements(size, error_description)) {
+            return false;
+        }
+
+        rearrangeData(data, size);
+
+        if(!makeTransform(data, size, fft_direction, error_description)) {
+            return false;
+        }
+
+        if (FFT_BACKWARD == fft_direction) {
+            scaleValues(data, size);
+        }
+
+        return true;
+    }
+};
+
+// 2D FFT
+template <class TComplexArray2D>
+struct CFFT<TComplexArray2D,2>
+{
+    static bool FFT_inplace(TComplexArray2D & data, const size_t size1, const size_t size2,
+                            const FFT_direction fft_direction, const char *& error_description)
+    {
+        int n_rows = static_cast<int>(size1);
+        int n_cols = static_cast<int>(size2);
+
+        // fft for columns
+        std::vector<complex_type> subarray(n_rows); // each column has n_rows elements
+
+        for(int j = 0; j < n_cols; ++j)
+        {
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+            for(int i = 0; i < n_rows; ++i) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                subarray[i] = data[i][j];
+#else
+                subarray[i] = data(i,j);
+#endif
+            }
+
+            if(!CFFT<std::vector<complex_type>,1>::FFT_inplace(subarray, size1,
+                                                               fft_direction,
+                                                               error_description))
+            {
+                return false;
+            }
+
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+            for(int i = 0; i < n_rows; ++i) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                data[i][j] = subarray[i];
+#else
+                data(i,j) = subarray[i];
+#endif
+            }
+        }
+
+        // fft for rows
+        subarray.resize(n_cols); // each row has n_cols elements
+
+        for(int i = 0; i < n_rows; ++i)
+        {
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+            for(int j = 0; j < n_cols; ++j) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                subarray[j] = data[i][j];
+#else
+                subarray[j] = data(i,j);
+#endif
+            }
+
+            if(!CFFT<std::vector<complex_type>,1>::FFT_inplace(subarray, size2,
+                                                               fft_direction,
+                                                               error_description))
+            {
+                return false;
+            }
+
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+            for(int j = 0; j < n_cols; ++j) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                data[i][j] = subarray[j];
+#else
+                data(i,j) = subarray[j];
+#endif
+            }
+        }
+
+        return true;
+    }
+};
+
+// 3D FFT
+template <class TComplexArray3D>
+struct CFFT<TComplexArray3D,3>
+{
+    static bool FFT_inplace(TComplexArray3D & data, const size_t size1, const size_t size2,
+                            const size_t size3, const FFT_direction fft_direction,
+                            const char *& error_description)
+    {
+        int n_rows  = static_cast<int>(size1);
+        int n_cols  = static_cast<int>(size2);
+        int n_depth = static_cast<int>(size3);
+
+        std::vector<complex_type> subarray(n_rows); // for fft for columns: each column has n_rows elements
+
+        for(int k = 0; k < n_depth; ++k) // for all depth layers
+        {
+            // fft for columns
+            for(int j = 0; j < n_cols; ++j)
+            {
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+                for(int i = 0; i < n_rows; ++i) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                    subarray[i] = data[i][j][k];
+#else
+                    subarray[i] = data(i,j,k);
+#endif
+                }
+
+                if(!CFFT<std::vector<complex_type>,1>::FFT_inplace(subarray, size1,
+                                                                   fft_direction,
+                                                                   error_description))
+                {
+                    return false;
+                }
+
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+                for(int i = 0; i < n_rows; ++i) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                    data[i][j][k] = subarray[i];
+#else
+                    data(i,j,k) = subarray[i];
+#endif
+                }
+            }
+        }
+
+        subarray.resize(n_cols); // for fft for rows: each row has n_cols elements
+
+        for(int k = 0; k < n_depth; ++k) // for all depth layers
+        {
+            // fft for rows
+            for(int i = 0; i < n_rows; ++i)
+            {
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+                for(int j = 0; j < n_cols; ++j) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                    subarray[j] = data[i][j][k];
+#else
+                    subarray[j] = data(i,j,k);
+#endif
+                }
+
+                if(!CFFT<std::vector<complex_type>,1>::FFT_inplace(subarray, size2,
+                                                                   fft_direction,
+                                                                   error_description))
+                {
+                    return false;
+                }
+
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+                for(int j = 0; j < n_cols; ++j) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                    data[i][j][k] = subarray[j];
+#else
+                    data(i,j,k) = subarray[j];
+#endif
+                }
+            }
+        }
+
+        // fft for depth
+        subarray.resize(n_depth); // each depth strip contains n_depth elements
+
+        for(int i = 0; i < n_rows; ++i) // for all rows layers
+        {
+            for(int j = 0; j < n_cols; ++j) // for all cols layers
+            {
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+                for(int k = 0; k < n_depth; ++k) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                    subarray[k] = data[i][j][k];
+#else
+                    subarray[k] = data(i,j,k);
+#endif
+                }
+
+                if(!CFFT<std::vector<complex_type>,1>::FFT_inplace(subarray, size3,
+                                                                   fft_direction,
+                                                                   error_description))
+                {
+                    return false;
+                }
+
+#ifndef __clang__
+#ifdef __USE_OPENMP
+#pragma omp parallel for
+#endif
+#endif
+                for(int k = 0; k < n_depth; ++k) {
+#ifdef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR
+                    data[i][j][k] = subarray[k];
+#else
+                    data(i,j,k) = subarray[k];
+#endif
+                }
+            }
+        }
+
+        return true;
+    }
+};
+
+} // namespace impl
+} // namespace simple_fft
+
+#endif // __SIMPLE_FFT__FFT_IMPL_HPP__
diff --git a/externals/Simple-FFT/include/simple_fft/fft_settings.h b/externals/Simple-FFT/include/simple_fft/fft_settings.h
new file mode 100644 (file)
index 0000000..7d97b7d
--- /dev/null
@@ -0,0 +1,26 @@
+/**\r
+ * Copyright (c) 2013-2020 Dmitry Ivanov\r
+ *\r
+ * This file is a part of Simple-FFT project and is distributed under the terms\r
+ * of MIT license: https://opensource.org/licenses/MIT\r
+ */\r
+\r
+// In this file you can alter some settings of the library:\r
+// 1) Specify the desired real and complex types by typedef'ing real_type and complex_type.\r
+//    By default real_type is double and complex_type is std::complex<real_type>.\r
+// 2) If the array class uses square brackets for element access operator, define\r
+//    the macro __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR\r
+\r
+#ifndef __SIMPLE_FFT__FFT_SETTINGS_H__\r
+#define __SIMPLE_FFT__FFT_SETTINGS_H__\r
+\r
+#include <complex>\r
+\r
+typedef float real_type;\r
+typedef std::complex<real_type> complex_type;\r
+\r
+#ifndef __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR\r
+#define __USE_SQUARE_BRACKETS_FOR_ELEMENT_ACCESS_OPERATOR\r
+#endif\r
+\r
+#endif // __SIMPLE_FFT__FFT_SETTINGS_H__\r
diff --git a/faust-src/Makefile b/faust-src/Makefile
new file mode 100644 (file)
index 0000000..2eebaf7
--- /dev/null
@@ -0,0 +1,106 @@
+h headers :
+       install -d headersdir
+       $(MAKE) DEST='headersdir/' ARCH='faust2header.cpp' -f Makefile.headers
+
+ih hi installheaders : headers
+       install -m 0444 headersdir/*.h ../src/
+
+# The JackTrip project does not need the remaining make targets, except "clean" is nice to have:
+
+all : jackgtk
+
+test: jackgtk alsagtk jackqt alsaqt ladspa ossgtk bench plot sndfile jackconsole
+
+svg:
+       $(MAKE) -f Makefile.svg
+
+puredata :
+       install -d puredatadir
+       $(MAKE) DEST='puredatadir/' ARCH='puredata.cpp' LIB='' -f Makefile.pdcompile
+
+alsagtk :
+       install -d alsagtkdir
+       $(MAKE) DEST='alsagtkdir/' ARCH='alsa-gtk.cpp' LIB='-lpthread -lasound  `pkg-config --cflags --libs gtk+-2.0`' -f Makefile.compile
+
+jackgtk :
+       install -d jackgtkdir
+       $(MAKE) DEST='jackgtkdir/' ARCH='jack-gtk.cpp' LIB='`pkg-config --cflags --libs jack gtk+-2.0`' -f Makefile.compile
+
+jackqt :
+       install -d jackqtdir
+       $(MAKE) DEST='jackqtdir/' ARCH='jack-qt.cpp' LIB='-ljack' -f Makefile.qtcompile
+
+alsaqt :
+       install -d alsaqtdir
+       $(MAKE) DEST='alsaqtdir/' ARCH='alsa-qt.cpp' LIB='-lpthread -lasound' -f Makefile.qtcompile
+
+ladspa :
+       install -d ladspadir
+       $(MAKE) DEST='ladspadir/' ARCH='ladspa.cpp' LIB='-fPIC -shared' EXT='.so' -f Makefile.ladspacompile
+
+jackwx :
+       install -d jackwxdir
+       $(MAKE) DEST='jackwxdir/' ARCH='jack-wx.cpp' LIB='`pkg-config jack  --cflags --libs` `wx-config --cflags --libs`' -f Makefile.compile
+
+ossgtk :
+       install -d ossgtkdir
+       $(MAKE) DEST='ossgtkdir/' ARCH='oss-gtk.cpp' LIB='-lpthread  `pkg-config gtk+-2.0  --cflags --libs`' -f Makefile.compile
+
+osswx :
+       install -d osswxdir
+       $(MAKE) DEST='osswxdir/' ARCH='oss-wx.cpp' LIB='-lpthread  `wx-config --cflags --libs`' -f Makefile.compile
+
+pagtk :
+       install -d pagtkdir
+       $(MAKE) DEST='pagtkdir/' ARCH='pa-gtk.cpp' LIB='-lpthread  -lportaudio `pkg-config gtk+-2.0  --cflags --libs`' -f Makefile.compile
+
+pawx :
+       install -d pawxdir
+       $(MAKE) DEST='pawxdir/' ARCH='pa-wx.cpp' LIB='-lpthread  -lportaudio `wx-config --cflags --libs`' -f Makefile.compile
+
+module :
+       install -d moduledir
+       $(MAKE) DEST='moduledir/' ARCH='module.cpp' LIB='-fPIC -shared' EXT='.so' -f Makefile.compile
+
+bundle :
+       install -d bundledir
+       $(MAKE) DEST='bundledir/' ARCH='module.cpp' LIB='-fPIC -bundle' EXT='.so' -f Makefile.compile
+
+msp :
+       install -d mspdir
+       $(MAKE) DEST='mspdir/' ARCH='max-msp.cpp' LIB='' -f Makefile.mspcompile
+
+vst :
+       install -d vstdir
+       $(MAKE) DEST='vstdir/' ARCH='vst.cpp' LIB='' -f Makefile.vstcompile
+
+bench :
+       install -d benchdir
+       $(MAKE) DEST='benchdir/' ARCH='bench.cpp' LIB='' -f Makefile.compile
+
+sndfile :
+       install -d sndfiledir
+       $(MAKE) DEST='sndfiledir/' ARCH='sndfile.cpp' LIB='-lsndfile' -f Makefile.compile
+
+plot :
+       install -d plotdir
+       $(MAKE) DEST='plotdir/' ARCH='plot.cpp' LIB='' -f Makefile.compile
+
+matlabplot :
+       install -d matlabplotdir
+       $(MAKE) DEST='matlabplotdir/' ARCH='matlabplot.cpp' LIB='' -f Makefile.compile
+
+q :
+       install -d qdir
+       $(MAKE) DEST='qdir/' ARCH='q.cpp' LIB='' -f Makefile.qcompile
+
+supercollider :
+       install -d supercolliderdir
+       $(MAKE) DEST='supercolliderdir/' ARCH='../architecture/supercollider.cpp' CXXFLAGS='`pkg-config --cflags libscsynth`' LIB='-fPIC -shared' EXT='.so' -f Makefile.sccompile
+
+jackconsole :
+       install -d jackconsoledir
+       $(MAKE) DEST='jackconsoledir/' ARCH='jack-console.cpp' LIB='`pkg-config --cflags --libs jack `' -f Makefile.compile
+
+clean :
+       -rm -rf alsagtkdir jackgtkdir alsaqtdir jackqtdir vecalsagtkdir vecjackgtkdir ladspadir jackwxdir ossgtkdir osswxdir pagtkdir pawxdir moduledir bundledir mspdir vstdir benchdir sndfiledir plotdir benchdir supercolliderdir puredatadir qdir plotdir jackconsoledir matlabplotdir *-ps *-svg headersdir
diff --git a/faust-src/Makefile.compile b/faust-src/Makefile.compile
new file mode 100644 (file)
index 0000000..0681b10
--- /dev/null
@@ -0,0 +1,15 @@
+dspsrc  := $(wildcard *.dsp)
+cppsrc  := $(addprefix $(DEST), $(dspsrc:.dsp=.cpp))
+appl   := $(addprefix $(DEST), $(dspsrc:.dsp=$(EXT)))
+
+
+all :  $(appl)
+
+
+$(DEST)%$(EXT) : %.dsp 
+       faust $(VEC) -a $(ARCH) $< -o $@.cpp
+       $(CXX) -O3 $(CXXFLAGS) $(LIB) $@.cpp -o $@
+       
+
+clean :
+       rm -f $(DEST)
diff --git a/faust-src/Makefile.headers b/faust-src/Makefile.headers
new file mode 100644 (file)
index 0000000..e2304de
--- /dev/null
@@ -0,0 +1,27 @@
+dspsrc  := $(wildcard *.dsp)
+headers  := $(addprefix $(DEST), $(dspsrc:.dsp=.h))
+
+all :  $(headers)
+
+FAUST = faust --inline-architecture-files --in-place
+# --in-place (-inpl) means the input signal array can be used
+#           as the output signal array in the compute() function.
+# --inline-architecture-files (-i) means that all Faust headers
+#           and include files get included in the output C++ file
+#           so that it can be compiled without a Faust installation
+#           on the compiling machine.
+
+$(DEST)%.h : %.dsp
+       $(FAUST) $(VEC) -cn $(<:.dsp=) -a $(ARCH) $< -o $(DEST)$(<:.dsp=).h
+       #(cat $(@:.h=).cpp | sed -e s/mydsp/$(<:.dsp=)/g) > $@
+       #/bin/rm $(@:.h=).cpp
+
+# Probably needed for headers after the first: (cat $(@:.h=).cpp | sed -e /template/d | sed -e s/mydsp/$(<:.dsp=)/g) > $@
+
+clean :
+       rm -f $(DEST)
+
+# Working example:
+# lh limiterdsp.h: limiterdsp.dsp
+#      faust -inpl -cn limiterdsp -a faust2header.cpp limiterdsp.dsp -o limiterdsp.cpp
+#      (cat limiterdsp.cpp | sed -e /template/d | sed -e s/mydsp/limiterdsp/g) > limiterdsp.h
diff --git a/faust-src/Makefile.ladspacompile b/faust-src/Makefile.ladspacompile
new file mode 100644 (file)
index 0000000..f9d7ec5
--- /dev/null
@@ -0,0 +1,22 @@
+DEST   := ladspadir/
+dspsrc  := $(wildcard *.dsp)
+cppsrc  := $(addprefix $(DEST), $(dspsrc:.dsp=.cpp))
+modules        := $(addprefix $(DEST),  $(dspsrc:%.dsp=%.so))
+os     := $(shell uname)
+
+###allcpp: $(cppsrc)
+
+allmodules: $(modules)
+
+$(DEST)%.so: $(DEST)%.cpp
+ifeq ($(os), Darwin)
+       $(CXX) -fPIC -bundle -O3 $(CXXFLAGS) -Dmydsp=$(patsubst %.so,%,$(notdir $@)) $< -o $@
+else
+       $(CXX) -fPIC -shared -O3 $(CXXFLAGS) -Dmydsp=$(patsubst %.so,%,$(notdir $@)) $< -o $@
+endif
+
+$(DEST)%.cpp: %.dsp 
+       faust $(VEC) -a ladspa.cpp $< -o $@
+
+clean:
+       rm -rf $(DEST)
diff --git a/faust-src/Makefile.mspcompile b/faust-src/Makefile.mspcompile
new file mode 100644 (file)
index 0000000..6d1572d
--- /dev/null
@@ -0,0 +1,44 @@
+dspsrc  := $(wildcard *.dsp)
+cppsrc  := $(addprefix $(DEST), $(dspsrc:.dsp=.cpp))
+appl   := $(addprefix $(DEST), $(dspsrc:.dsp=~.mxo))
+processor := $(shell uname -p)
+
+INC    := -I/usr/local/include/c74support/max-includes -I/usr/local/include/c74support/msp-includes
+
+all :  $(appl)
+
+$(DEST)%~.mxo : %.dsp Info.plist.template
+       install -d $@/Contents/MacOS
+       faust $(VEC) -a $(ARCH) $< -o $@/$(<:.dsp=.cpp)
+ifeq ($(processor), i386)
+       g++ -arch i386 -fpascal-strings -fasm-blocks -g -O3 $(INC)  -c $@/$(<:.dsp=.cpp) -o $@/$(<:.dsp=.i386.o)
+       g++  -framework MaxAPI -framework Carbon -framework MaxAudioAPI -arch i386 -Wl,-Y,1455 -bundle $@/$(<:.dsp=.i386.o) -o $@/$(<:.dsp=.i386~) 
+       g++ -arch ppc -fpascal-strings -fasm-blocks -g -O3 $(INC)  -c $@/$(<:.dsp=.cpp) -o $@/$(<:.dsp=.ppc.o)
+       g++  -framework Carbon -framework MaxAPI -framework MaxAudioAPI -arch ppc -Wl,-Y,1455 -bundle $@/$(<:.dsp=.ppc.o) -o $@/$(<:.dsp=.ppc~)
+       sed s/FOO/$(<:.dsp=~)/ <Info.plist.template >$@/Contents/Info.plist
+       lipo -create $@/$(<:.dsp=.i386~) $@/$(<:.dsp=.ppc~) -output $@/Contents/MacOS/$(<:.dsp=~)
+       rm -f $@/$(<:.dsp=.ppc~) $@/$(<:.dsp=.ppc.o) $@/$(<:.dsp=.i386.o) $@/$(<:.dsp=.i386~)
+else
+       g++ -arch ppc -fpascal-strings -fasm-blocks -g -O3 $(INC)  -c $@/$(<:.dsp=.cpp) -o $@/$(<:.dsp=.ppc.o)
+       g++  -framework Carbon -framework MaxAPI -framework MaxAudioAPI -arch ppc -Wl,-Y,1455 -bundle $@/$(<:.dsp=.ppc.o) -o $@/$(<:.dsp=.ppc~)
+       sed s/FOO/$(<:.dsp=~)/ <Info.plist.template >$@/Contents/Info.plist
+       lipo -create $@/$(<:.dsp=.ppc~) -output $@/Contents/MacOS/$(<:.dsp=~)
+       rm -f $@/$(<:.dsp=.ppc~) $@/$(<:.dsp=.ppc.o)
+endif
+
+Info.plist.template :
+       echo '<?xml version="1.0" encoding="UTF-8"?>' > Info.plist.template
+       echo '<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'  >> Info.plist.template
+       echo '<plist version="1.0">'            >> Info.plist.template
+       echo '<dict>'                           >> Info.plist.template
+       echo '  <key>CFBundleExecutable</key>'  >> Info.plist.template
+       echo '  <string>FOO</string>'           >> Info.plist.template
+       echo '  <key>CFBundleName</key>'        >> Info.plist.template
+       echo '  <string>FOO</string>'           >> Info.plist.template
+       echo '  <key>CFBundlePackageType</key>'  >> Info.plist.template
+       echo '  <string>iLaX</string>'          >> Info.plist.template
+       echo '</dict>'                          >> Info.plist.template
+       echo '</plist>'                         >> Info.plist.template
+
+clean :
+       rm -f $(DEST)
diff --git a/faust-src/Makefile.pdcompile b/faust-src/Makefile.pdcompile
new file mode 100644 (file)
index 0000000..c591c66
--- /dev/null
@@ -0,0 +1,45 @@
+DEST   := pddir/
+dspsrc  := $(wildcard *.dsp)
+cppsrc  := $(addprefix $(DEST), $(dspsrc:.dsp=.cpp))
+modules        := $(addprefix $(DEST),  $(dspsrc:.dsp=~.pd_linux))
+patches        := $(addprefix $(DEST),  $(dspsrc:.dsp=.pd))
+FAUST2PD := faust2pd
+F2PDFLAGS := -r 10 -s
+
+LINUXCFLAGS = -DPD -O2 -funroll-loops -fomit-frame-pointer -fPIC \
+    -Wall -W -Wshadow -Wno-unused -Wno-parentheses -Wno-switch $(CFLAGS)
+
+LINUXINCLUDE =
+
+
+###--------------------------------------------
+### Will use faust2pd to create the GUI patches
+### only if it is installed
+
+helper:=$(shell whereis faust2pd)
+
+ifeq ($(helper),faust2pd:)
+       todo:=$(modules)
+else
+       todo:=$(modules) $(patches)
+endif
+
+###--------------------------------------------
+
+
+allmodules: $(todo)
+
+$(DEST)%~.pd_linux: $(DEST)%.cpp
+       $(CXX) $(LINUXCFLAGS) $(LINUXINCLUDE) -shared -Dmydsp=$(patsubst %~.pd_linux,%,$(notdir $@)) $< -o $@
+
+$(DEST)%.cpp: %.dsp
+       faust -a $(ARCH) $< -o $@
+       
+$(DEST)%.pd: %.dsp
+       faust -xml $< -o /dev/null
+       $(FAUST2PD) $(F2PDFLAGS) $<.xml
+       mv $(<:.dsp=.pd) $(DEST)
+       rm -f $<.xml
+
+clean:
+       rm -rf $(DEST)
diff --git a/faust-src/Makefile.qcompile b/faust-src/Makefile.qcompile
new file mode 100644 (file)
index 0000000..9a12af2
--- /dev/null
@@ -0,0 +1,17 @@
+DEST   := qdir/
+dspsrc  := $(wildcard *.dsp)
+cppsrc  := $(addprefix $(DEST), $(dspsrc:.dsp=.cpp))
+modules        := $(addprefix $(DEST),  $(dspsrc:%.dsp=%.so))
+
+###allcpp: $(cppsrc)
+
+allmodules: $(modules)
+
+$(DEST)%.so: $(DEST)%.cpp
+       $(CXX) -shared -O3 $(CXXFLAGS) -Dmydsp=$(patsubst %.so,%,$(notdir $@)) $< -o $@
+
+$(DEST)%.cpp: %.dsp 
+       faust $(VEC) -a q.cpp $< -o $@
+
+clean:
+       rm -rf $(DEST)
diff --git a/faust-src/Makefile.qtcompile b/faust-src/Makefile.qtcompile
new file mode 100644 (file)
index 0000000..70d1018
--- /dev/null
@@ -0,0 +1,49 @@
+###--------------------------------------------
+### DEST : directory where to put binaries
+### ARCH : faust architecture file
+
+system := $(shell uname -s)
+dspsrc  := $(wildcard *.dsp)
+cppsrc  := $(addprefix $(DEST), $(dspsrc:.dsp=.cpp))
+
+
+### check what type of applications to build (MacOSX Darwin or Linux)
+ifeq ($(system), Darwin)
+appls  := $(addprefix $(DEST),  $(dspsrc:.dsp=.app))
+else
+appls  := $(addprefix $(DEST),  $(dspsrc:.dsp=))
+endif
+
+
+TMP = /var/tmp/$(<:.dsp=)
+###--------------------------------------------
+
+
+all : $(appls)
+
+
+### Darwin 
+$(DEST)%.app : %.dsp
+       rm -rf $(TMP)
+       install -d $(TMP)
+       faust -a $(ARCH) $< -o $(TMP)/$<.cpp
+       cd $(TMP); qmake -project "INCLUDEPATH+=/usr/local/lib/faust/" "LIBS+=$(LIB)" "HEADERS+=/usr/local/lib/faust/faustqt.h" 
+       cd $(TMP); qmake
+       cd $(TMP); xcodebuild -project $(<:.dsp=).xcodeproj
+       mv $(TMP)/build/Default/$(<:.dsp=.app) $@
+       rm -rf $(TMP)
+
+
+### Linux
+$(DEST)% : %.dsp
+       rm -rf $(TMP)
+       install -d $(TMP)
+       faust -a $(ARCH) $< -o $(TMP)/$<.cpp
+       cd $(TMP); qmake -project "INCLUDEPATH+=/usr/local/lib/faust/" "LIBS+=$(LIB)" "HEADERS+=/usr/local/lib/faust/faustqt.h" 
+       cd $(TMP); qmake 
+       make -C $(TMP)
+       mv $(TMP)/$(<:.dsp=) $@
+       rm -rf $(TMP)
+
+clean:
+       rm -rf $(DEST)
diff --git a/faust-src/Makefile.sccompile b/faust-src/Makefile.sccompile
new file mode 100644 (file)
index 0000000..628b2c8
--- /dev/null
@@ -0,0 +1,41 @@
+# Makefile to produce supercollider plugins with Faust
+#      'foo.dsp' -> 'foo.so' and 'foo.sc'
+#
+
+dspsrc         := $(wildcard *.dsp)
+scfiles                := $(addprefix $(DEST), $(dspsrc:.dsp=.sc))
+sofiles                := $(addprefix $(DEST), $(dspsrc:.dsp=.so))
+CXXFLAGS       := `pkg-config --cflags libscsynth` $(CXXFLAGS)
+LIB            := -shared
+
+
+###--------------------------------------------
+### Will use faust2sc to create the class file
+### only if it is installed
+
+helper:=$(shell whereis faust2sc)
+
+ifeq ($(helper),faust2sc:)
+       todo:=$(sofiles)
+else
+       todo:=$(sofiles) $(scfiles)
+endif
+
+
+###--------------------------------------------
+
+
+all : $(todo)
+
+$(DEST)%.cpp: %.dsp
+       faust -a $(ARCH) $< -o $@
+
+$(DEST)%.so: $(DEST)%.cpp
+       $(CXX) $(CXXFLAGS) $(OPTFLAGS) $(LIB) $< -o $@
+
+$(DEST)%.sc : %.dsp.xml
+       faust2sc --prefix=Faust $< --output=$@
+
+%.dsp.xml: %.dsp
+       faust --xml -o /dev/null $<
+
diff --git a/faust-src/Makefile.svg b/faust-src/Makefile.svg
new file mode 100644 (file)
index 0000000..fd271fe
--- /dev/null
@@ -0,0 +1,12 @@
+src    := $(wildcard *.dsp)
+target         := $(src:.dsp=.dsp-svg)
+
+all :  $(target)
+
+
+%.dsp-svg : %.dsp
+       faust -svg $< > /dev/null
+       
+
+clean :
+       rm -rf $(target)
diff --git a/faust-src/Makefile.vstcompile b/faust-src/Makefile.vstcompile
new file mode 100644 (file)
index 0000000..658a69b
--- /dev/null
@@ -0,0 +1,31 @@
+dspsrc  := $(wildcard *.dsp)
+cppsrc  := $(addprefix $(DEST), $(dspsrc:.dsp=.cpp))
+appl   := $(addprefix $(DEST), $(dspsrc:.dsp=$(EXT)))
+
+# Setup this variable to access the VST SDK files
+vst_sdk := "/Volumes/Document1/Developpement/ProjectsCVS/JackCVS/JackOSX/jackosx/jackplugins/JACK-ASinsert/VST/VSTSDK"
+
+# Setup this variable with the location for the compiled VST plug-ins
+install_plug_ins := "/Library/Audio/Plug-Ins/VST"
+
+all :  $(appl)
+
+       
+$(DEST)% : %.dsp
+       install -d $@
+       cp -r $(vst_sdk) $@
+       cp -r /usr/local/lib/faust/VST/* $@
+       faust $(VEC) -a $(ARCH) $< -o $@/vst-output.cpp
+       mv  $@/vst-output.cpp  $@/$(<:.dsp=.cpp)
+       sed -e 's/vst-output.cpp/$(<:.dsp=.cpp)/'  $@/VST.xcode/project.pbxproj > $@/VST.xcode/new_project.pbxproj &&  mv $@/VST.xcode/new_project.pbxproj $@/VST.xcode/project.pbxproj
+       sed -e 's/XXXX/$(<:.dsp=)/'  $@/Info.plist  >  $@/new_Info.plist  &&  mv $@/new_Info.plist $@/Info.plist
+       xcodebuild -project $@/VST.xcode clean
+       xcodebuild -project $@/VST.xcode
+       mv $@/build/FaustVST.vst $@/build/$(<:.dsp=.vst)
+       rm -r $@/build/VST.build
+       install -d $(install_plug_ins)
+       cp -r $@/build/$(<:.dsp=.vst) $(install_plug_ins)
+       
+
+clean :
+       rm -f $(DEST)
diff --git a/faust-src/README-Limiter.md b/faust-src/README-Limiter.md
new file mode 100644 (file)
index 0000000..9caf104
--- /dev/null
@@ -0,0 +1,23 @@
+The audio limiter in use by jacktrip is limiterdsp.dsp
+
+To regenerate ../src/limiterdsp.h, you can say (if you have Faust installed)
+
+  make headers
+
+in this directory to create ./headersdir/limiterdsp.h
+and then copy that to ../src/ to reinstall it.
+
+To test the limiter separately, you can load it into
+fausteditor.grame.fr to compile and download an executable.  With
+Faust installed, you can do it at the command line:  To make a
+standalone JACK app for Mac, say
+
+  faust2jaqt limiterdsp.dsp
+
+For Linux, you want either that or
+
+  faust2jack limiterdsp.dsp
+
+(to use GTK in place of Qt for the GUI).
+
+Then you just run it and patch it in (using qjackctl) between your audio capture and jacktrip input.
diff --git a/faust-src/compressor-limiter-test.dsp b/faust-src/compressor-limiter-test.dsp
new file mode 100644 (file)
index 0000000..7384714
--- /dev/null
@@ -0,0 +1,2 @@
+process = _ : component("compressordsp.dsp") : component("limiterdsp.dsp") <: _,_;
+
diff --git a/faust-src/compressordsp.dsp b/faust-src/compressordsp.dsp
new file mode 100644 (file)
index 0000000..646ddc9
--- /dev/null
@@ -0,0 +1,73 @@
+declare name "compressor";
+declare version "0.0";
+declare author "Julius Smith";
+declare license "MIT Style STK-4.2";
+declare description "Compressor demo application, adapted from the Faust Library's dm.compressor_demo in demos.lib";
+declare documentation "https://faustlibraries.grame.fr/libs/compressors/#cocompressor_mono";
+
+import("stdfaust.lib");
+
+//----------------------------`(dm.)compressor_mono_demo`-------------------------
+// Mono Compressor
+//
+// #### Usage
+//
+// ```
+// _ : compressor_mono_demo : _;
+// ```
+//------------------------------------------------------------
+compressor_demo = ba.bypass1(cbp,compressor_mono_demo)
+with {
+        comp_group(x) = vgroup("COMPRESSOR [tooltip: References:
+                https://faustlibraries.grame.fr/libs/compressors/
+                http://en.wikipedia.org/wiki/Dynamic_range_compression]", x);
+
+        meter_group(x)  = comp_group(hgroup("[0]", x));
+        knob_group(x)  = comp_group(hgroup("[1]", x));
+
+        cbp = meter_group(checkbox("[0] Bypass  [tooltip: When this is checked, the compressor
+                has no effect]"));
+        gainview = co.compression_gain_mono(ratio,threshold,attack,release) : ba.linear2db :
+        meter_group(hbargraph("[1] Compressor Gain [unit:dB] [tooltip: Compressor gain in dB]",-50,+10));
+
+        displaygain = _ <: _,abs : _,gainview : attach;
+
+        compressor_stereo_demo =
+        displaygain(co.compressor_stereo(ratio,threshold,attack,release)) :
+        *(makeupgain), *(makeupgain);
+
+        compressor_mono_demo =
+        displaygain(co.compressor_mono(ratio,threshold,attack,release)) :
+        *(makeupgain);
+
+        ctl_group(x)  = knob_group(hgroup("[3] Compression Control", x));
+
+        ratio = ctl_group(hslider("[0] Ratio [style:knob]
+        [tooltip: A compression Ratio of N means that for each N dB increase in input
+        signal level above Threshold, the output level goes up 1 dB]",
+        2, 1, 20, 0.1));
+
+        threshold = ctl_group(hslider("[1] Threshold [unit:dB] [style:knob]
+        [tooltip: When the signal level exceeds the Threshold (in dB), its level
+        is compressed according to the Ratio]",
+        -24, -100, 10, 0.1));
+
+        env_group(x)  = knob_group(hgroup("[4] Compression Response", x));
+
+        attack = env_group(hslider("[1] Attack [unit:ms] [style:knob] [scale:log]
+        [tooltip: Time constant in ms (1/e smoothing time) for the compression gain
+        to approach (exponentially) a new lower target level (the compression
+        `kicking in')]", 15, 1, 1000, 0.1)) : *(0.001) : max(1/ma.SR);
+
+        release = env_group(hslider("[2] Release [unit:ms] [style: knob] [scale:log]
+        [tooltip: Time constant in ms (1/e smoothing time) for the compression gain
+        to approach (exponentially) a new higher target level (the compression
+        'releasing')]", 40, 1, 1000, 0.1)) : *(0.001) : max(1/ma.SR);
+
+        makeupgain = comp_group(hslider("[5] MakeUpGain [unit:dB]
+        [tooltip: The compressed-signal output level is increased by this amount
+        (in dB) to make up for the level lost due to compression]",
+        2, -96, 96, 0.1)) : ba.db2linear;
+};
+
+process = _ : compressor_demo : _;
diff --git a/faust-src/faust2header.cpp b/faust-src/faust2header.cpp
new file mode 100644 (file)
index 0000000..6b3a647
--- /dev/null
@@ -0,0 +1,21 @@
+// 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.
+
+#include <faust/dsp/dsp.h>
+#include <faust/gui/APIUI.h>
+
+// NOTE: "faust -scn name" changes the last line above to
+// #include <faust/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+<<includeIntrinsic>>
+
+<<includeclass>>
+
diff --git a/faust-src/freeverbdsp.dsp b/faust-src/freeverbdsp.dsp
new file mode 100644 (file)
index 0000000..354a885
--- /dev/null
@@ -0,0 +1,45 @@
+declare name "freeverb";
+declare version "0.0";
+declare author "Romain Michon";
+declare license "LGPL";
+declare description "Freeverb implementation in Faust, from the Faust Library's dm.freeverb_demo in demos.lib";
+
+import("stdfaust.lib");
+
+//----------------------------`(dm.)freeverb_demo`-------------------------
+// Freeverb demo application.
+//
+// #### Usage
+//
+// ```
+// _,_ : freeverb_demo : _,_;
+// ```
+//------------------------------------------------------------
+// Author: Romain Michon
+// License: LGPL
+freeverb_demo = _,_ <: (*(g)*fixedgain,*(g)*fixedgain :
+       re.stereo_freeverb(combfeed, allpassfeed, damping, spatSpread)),
+       *(1-g), *(1-g) :> _,_
+with{
+       scaleroom   = 0.28;
+       offsetroom  = 0.7;
+       allpassfeed = 0.5;
+       scaledamp   = 0.4;
+       fixedgain   = 0.1;
+       origSR = 44100;
+
+       parameters(x) = hgroup("Freeverb",x);
+       knobGroup(x) = parameters(vgroup("[0]",x));
+       damping = knobGroup(vslider("[0] Damp [style: knob] [tooltip: Somehow control the
+               density of the reverb.]",0.5, 0, 1, 0.025)*scaledamp*origSR/ma.SR);
+       combfeed = knobGroup(vslider("[1] RoomSize [style: knob] [tooltip: The room size
+               between 0 and 1 with 1 for the largest room.]", 0.1, 0, 1, 0.025)*scaleroom*
+               origSR/ma.SR + offsetroom);
+       spatSpread = knobGroup(vslider("[2] Stereo Spread [style: knob] [tooltip: Spatial
+               spread between 0 and 1 with 1 for maximum spread.]",0.5,0,1,0.01)*46*ma.SR/origSR
+               : int);
+       g = parameters(vslider("[1] Wet [tooltip: The amount of reverb applied to the signal
+               between 0 and 1 with 1 for the maximum amount of reverb.]", 0.1, 0, 1, 0.025));
+};
+
+process = freeverb_demo;
diff --git a/faust-src/freeverbmonodsp.dsp b/faust-src/freeverbmonodsp.dsp
new file mode 100644 (file)
index 0000000..8008c48
--- /dev/null
@@ -0,0 +1,2 @@
+process = _ <: _,_ : component("freeverbdsp.dsp") : _,_ :> _; // there is no mono-to-stereo support in jacktrip
+
diff --git a/faust-src/limiterdsp.dsp b/faust-src/limiterdsp.dsp
new file mode 100644 (file)
index 0000000..e177d64
--- /dev/null
@@ -0,0 +1,11 @@
+// Version added to JackTrip (standalone program test):
+
+import("stdfaust.lib");
+N = hslider("[0] NumClientsAssumed",2,1,64,1);
+softClipLevel = 0.5; // start compressing at this amplitude - KEEP IN SYNC with setWarningAmplitude() in ../src/Effects.h
+gain = 1.0 / sqrt(float(N)); // assume power-based client sum - KEEP IN SYNC with limiterAmp in ../src/Limiter.h
+// lookahead(s), threshold, attack(s), hold(s), release(s)
+limiter = co.limiter_lad_mono(0.0001, softClipLevel, 0.00001, 0.1, 0.25); // GPLv3 license
+// If you need a less restricted license, try co.limiter_1176_R4_mono (MIT style license)
+
+process = *(gain) : limiter;
diff --git a/faust-src/limitertest.dsp b/faust-src/limitertest.dsp
new file mode 100644 (file)
index 0000000..8d236b5
--- /dev/null
@@ -0,0 +1,7 @@
+// Test signal used by ../src/Limiter.cpp
+
+import("stdfaust.lib");
+freq = hslider("[0] Freq",110.0,20.0,10000.0,1);
+amp = hslider("[0] Amp",0.2,0.0,1.0,0.0001);
+//process = amp * os.oscrs(freq);
+process = amp * os.sawtooth(freq);
diff --git a/faust-src/meterdsp.dsp b/faust-src/meterdsp.dsp
new file mode 100644 (file)
index 0000000..95f37da
--- /dev/null
@@ -0,0 +1,19 @@
+
+declare name "meter";
+declare version "1.0";
+declare author "Dominick Hing";
+declare license "MIT Style STK-4.2";
+declare description "VU Meter Faust Plugin for JackTrip";
+
+// Originally modified from https://github.com/grame-cncm/faust/blob/master-dev/examples/analysis/meter.dsp
+
+import("stdfaust.lib");
+
+process = peakMeter
+with {
+
+    round(n, x) = x
+        <: (ma.copysign(_, 1) : _ * (10 ^ n) <: int(_) , ma.frac : _, (_ >= 0.5) :> + : _ / (10 ^ n) ), _
+        : ma.copysign(_, _);
+    peakMeter = _ : max(ba.db2linear(-80), _) : ba.linear2db(_) : round(2, _);
+};
diff --git a/faust-src/minimal.cpp b/faust-src/minimal.cpp
new file mode 100644 (file)
index 0000000..eb36d3c
--- /dev/null
@@ -0,0 +1,104 @@
+#ifdef __GNUC__
+
+#define max(x, y) (((x) > (y)) ? (x) : (y))
+#define min(x, y) (((x) < (y)) ? (x) : (y))
+
+// abs is now predefined
+// template<typename T> T abs (T a)                    { return (a<T(0)) ? -a : a; }
+
+inline int lsr(int x, int n)
+{
+    return int(((unsigned int)x) >> n);
+}
+
+/******************************************************************************
+*******************************************************************************
+
+                                                               VECTOR INTRINSICS
+
+*******************************************************************************
+*******************************************************************************/
+
+// inline void *aligned_calloc(size_t nmemb, size_t size) { return
+// (void*)((unsigned)(calloc((nmemb*size)+15,sizeof(char)))+15 & 0xfffffff0); }
+inline void* aligned_calloc(size_t nmemb, size_t size)
+{
+    return (void*)((size_t)(calloc((nmemb * size) + 15, sizeof(char))) + 15 & ~15);
+}
+
+// clang-format off
+<<includeIntrinsic>>
+    // clang-format on
+
+    /******************************************************************************
+*******************************************************************************
+
+                        ABSTRACT USER INTERFACE
+
+*******************************************************************************
+*******************************************************************************/
+
+    class UI
+{
+    bool fStopped;
+
+   public:
+    UI() : fStopped(false) {}
+    virtual ~UI() {}
+
+    virtual void addButton(char* label, float* zone)        = 0;
+    virtual void addToggleButton(char* label, float* zone)  = 0;
+    virtual void addCheckButton(char* label, float* zone)   = 0;
+    virtual void addVerticalSlider(char* label, float* zone, float init, float min,
+                                   float max, float step)   = 0;
+    virtual void addHorizontalSlider(char* label, float* zone, float init, float min,
+                                     float max, float step) = 0;
+    virtual void addNumEntry(char* label, float* zone, float init, float min, float max,
+                             float step)                    = 0;
+
+    virtual void openFrameBox(char* label)      = 0;
+    virtual void openTabBox(char* label)        = 0;
+    virtual void openHorizontalBox(char* label) = 0;
+    virtual void openVerticalBox(char* label)   = 0;
+    virtual void closeBox()                     = 0;
+
+    virtual void run() = 0;
+
+    void stop() { fStopped = true; }
+    bool stopped() { return fStopped; }
+};
+
+/******************************************************************************
+*******************************************************************************
+
+                            FAUST DSP
+
+*******************************************************************************
+*******************************************************************************/
+
+//----------------------------------------------------------------
+//  abstract definition of a signal processor
+//----------------------------------------------------------------
+
+class dsp
+{
+   protected:
+    int fSamplingFreq;
+
+   public:
+    dsp() {}
+    virtual ~dsp() {}
+
+    virtual int getNumInputs()                                     = 0;
+    virtual int getNumOutputs()                                    = 0;
+    virtual void buildUserInterface(UI* interface)                 = 0;
+    virtual void init(int samplingRate)                            = 0;
+    virtual void compute(int len, float** inputs, float** outputs) = 0;
+};
+
+//----------------------------------------------------------------------------
+//  FAUST generated signal processor
+//----------------------------------------------------------------------------
+// clang-format off
+<<includeclass>>
+    // clang-format on
diff --git a/faust-src/monitordsp.dsp b/faust-src/monitordsp.dsp
new file mode 100644 (file)
index 0000000..7984412
--- /dev/null
@@ -0,0 +1,15 @@
+declare name "monitor";
+declare version "1.0";
+declare author "Dominick Hing, adapted from 'Volume Control' by Matt Horton";
+declare license "MIT Style STK-4.2";
+declare description "Volume Control Faust Plugin for JackTrip, based on Faust examples";
+
+
+import("stdfaust.lib");
+mute = checkbox("[1] Mute");
+gain(v) = v : ba.db2linear : si.smoo : _;
+gainVMute(v) = _ * gain(v), 0 : select2(mute) : _;
+zeroCutoff(v) = _ , 0 : select2(v == -40) : _;
+volume = hslider("[0] Volume", 0, -40, 0, 0.1);
+
+process = _,_  <: vgroup("Monitor", _ : gainVMute(volume) : zeroCutoff(volume)), _ : +;
diff --git a/faust-src/net-ks.dsp b/faust-src/net-ks.dsp
new file mode 100644 (file)
index 0000000..858749a
--- /dev/null
@@ -0,0 +1,29 @@
+declare name        "net-ks";
+declare version     "1.0";
+declare author      "Juan-Pablo Caceres";
+declare license     "MIT";
+declare copyright   "(c) Juan-Pablo Caceres 2008";
+
+//-----------------------------------------------------
+//              Network-karplus-strong,
+//              Based on 'karplus' from Faust Examples
+//-----------------------------------------------------
+
+import("music.lib");
+
+// Excitation
+//-----------
+upfront(x)  = (x-x') > 0.0;
+decay(n,x)  = x - (x>0.0)/n;
+release(n)  = + ~ decay(n);
+trigger(n)  = upfront : release(n) : >(0.0);
+
+// Filters
+//--------
+// Average LowPass Filter
+av_lowpass(x)  = (x+x')/2;
+
+process = _ , 
+       ( noise * 0.9 :
+        vgroup("excitator", *(button("play") : trigger(300))) ) 
+        :> av_lowpass;
diff --git a/faust-src/tests/compressor-limiter-test.dsp b/faust-src/tests/compressor-limiter-test.dsp
new file mode 100644 (file)
index 0000000..64577e4
--- /dev/null
@@ -0,0 +1,21 @@
+// Doc: https://faustlibraries.grame.fr/libs/compressors/
+
+cs = hslider("Compressor [style:radio{'1':0; '2': 1 }]", 0,0,1,1);
+
+c1 = component("compressordsp.dsp");  // ./compressordsp.dsp
+c2 = component("compressor2dsp.dsp"); // ./compressor2dsp.dsp
+compressor(cs) = _ <: select2(cs,c1,c2);
+
+limiter_group(x) = vgroup("LIMITER [tooltip: https://faustlibraries.grame.fr/libs/compressors/#functions-reference]",x);
+
+process = _ : compressor(cs) : limiter_group(component("limiterdsp.dsp")) <: _,_;
+
+/*
+ * My present conclusion is to continue with c1, because it uses the
+ * more standard 'ratio' parameter, while c2 uses 'strength', which becomes
+ * hard-clipping at strength=1.  Also, I hear no compelling difference sonically
+ * (after laboriously finding a strength value that is roughly comparable to ratio).
+ *
+ * Also, c2 is GPL license, which cannot go into closed-source products,
+ * while c1 can be freely used as desired (STK-4.2 license).
+ */
diff --git a/faust-src/tests/compressor2dsp.dsp b/faust-src/tests/compressor2dsp.dsp
new file mode 100644 (file)
index 0000000..831bfd0
--- /dev/null
@@ -0,0 +1,82 @@
+declare name "compressor2"; // more modern feedback-compressor with release-to-threshold
+declare version "0.0";
+declare author "Julius Smith";
+declare license "MIT Style STK-4.2"; // but using GPLv3
+declare description "adapted from ./compressordsp.dsp adding use of co.FBFFcompressor_N_chan";
+declare documentation "https://faustlibraries.grame.fr/libs/compressors/#cofffbcompressor_n_chan";
+
+import("stdfaust.lib");
+
+// #### Usage
+//
+// ```
+// _ : compressor2_mono_demo : _;
+// ```
+//------------------------------------------------------------
+compressor2_demo = ba.bypass1(cbp,compressor2_mono_demo)
+with {
+       comp_group(x) = vgroup("COMPRESSOR2 [tooltip: Reference:
+               http://en.wikipedia.org/wiki/Dynamic_range_compression]", x);
+
+       meter_group(x)  = comp_group(hgroup("[0]", x));
+       knob_group(x)  = comp_group(hgroup("[1]", x));
+
+       cbp = meter_group(checkbox("[0] Bypass  [tooltip: When this is checked, the compressor2
+               has no effect]"));
+
+       // API: co.FBFFcompressor_N_chan(strength,thresh,att,rel,knee,prePost,link,FBFF,meter,N)
+       // strength = min(ratio-1.0,5)/5.0; // crude hack - will be wrong
+       knee = 5; // dB window about threshold for knee
+       prePost = 1; // level detector location: 0 for input, 1 for output (for feedback compressor)
+       link = 0; // linkage between channels (irrelevant for mono)
+       FBFF = 1; // cross-fade between feedforward (0) and feedback (1) compression
+       maxGR = -50; // dB - Max Gain Reduction (only affects display)
+       meter = _<:(_, (ba.linear2db:max(maxGR):meter_group((hbargraph("[1] Compressor Gain [unit:dB][tooltip: Compressor gain in dB]", maxGR, 10))))):attach;
+       //meter = _; // use gainview below instead to look more like compressordsp.dsp
+       NChans = 1;
+
+       // compressordsp.dsp: gainview = co.compression_gain_mono(strength,threshold,attack,release) 
+       // threshold gets doubled for the feedback case, but not for feedforward (see compressors.lib):
+       gainview = co.peak_compression_gain_N_chan(strength,2*threshold,attack,release,knee,prePost,link,NChans)
+       : ba.linear2db : max(maxGR) :
+       meter_group(hbargraph("[1] Compressor2 Gain [unit:dB] [tooltip: Current gain of
+       the compressor2 in dB]",maxGR,+10));
+
+       // use built-in gain display:
+       displaygain = _;
+       // not the same: displaygain = _ <: _,abs : _,gainview : attach;
+
+       compressor2_mono_demo =
+       displaygain(co.FBFFcompressor_N_chan(strength,threshold,attack,release,knee,prePost,link,FBFF,meter,NChans)) :
+       *(makeupgain);
+
+       ctl_group(x)  = knob_group(hgroup("[3] Compression Control", x));
+
+       strength = ctl_group(hslider("[0] Strength [style:knob]
+       [tooltip: A compression Strength of 0 means no compression, while 1 yields infinit compression (hard limiting)]",
+       0.1, 0, 1, 0.01)); // 0.1 seems to be pretty close to ratio == 2, based on watching the gain displays
+
+       threshold = ctl_group(hslider("[1] Threshold [unit:dB] [style:knob]
+       [tooltip: When the signal level exceeds the Threshold (in dB), its level
+       is compressed according to the Strength]",
+       -24, -100, 10, 0.1));
+
+       env_group(x)  = knob_group(hgroup("[4] Compression Response", x));
+
+       attack = env_group(hslider("[1] Attack [unit:ms] [style:knob] [scale:log]
+       [tooltip: Time constant in ms (1/e smoothing time) for the compression gain
+       to approach (exponentially) a new lower target level (the compression
+       `kicking in')]", 15, 1, 1000, 0.1)) : *(0.001) : max(1/ma.SR);
+
+       release = env_group(hslider("[2] Release [unit:ms] [style: knob] [scale:log]
+       [tooltip: Time constant in ms (1/e smoothing time) for the compression gain
+       to approach (exponentially) a new higher target level (the compression
+       'releasing')]", 40, 1, 1000, 0.1)) : *(0.001) : max(1/ma.SR);
+
+       makeupgain = comp_group(hslider("[5] MakeUpGain [unit:dB]
+       [tooltip: The compressed-signal output level is increased by this amount
+       (in dB) to make up for the level lost due to compression]",
+       2, -96, 96, 0.1)) : ba.db2linear;
+};
+
+process = _ : compressor2_demo : _;
diff --git a/faust-src/tonedsp.dsp b/faust-src/tonedsp.dsp
new file mode 100644 (file)
index 0000000..0f7a902
--- /dev/null
@@ -0,0 +1,17 @@
+// Source: https://faustdoc.grame.fr/examples/smartKeyboard/#turenas
+
+import("stdfaust.lib");
+
+y = hslider("y",0,0,1,0.01);
+freq = hslider("freq",400,50,2000,0.01);
+gate = button("gate");
+res = hslider("res[acc: 0 0 -10 0 10]",2.5,0.01,5,0.01);
+nModes = 6;
+maxModeSpread = 5;
+modeSpread = y*maxModeSpread;
+modeFreqRatios = par(i,nModes,1+(i+1)/nModes*modeSpread);
+minModeGain = 0.3;
+modeGains = par(i,nModes,1-(i+1)/(nModes*minModeGain));
+modeRes = res : si.smoo;
+
+process = sy.additiveDrum(freq,modeFreqRatios,modeGains,0.8,0.001,modeRes,gate)*0.05;
diff --git a/faust-src/volumedsp.dsp b/faust-src/volumedsp.dsp
new file mode 100644 (file)
index 0000000..1a1a617
--- /dev/null
@@ -0,0 +1,14 @@
+declare name "volume";
+declare version "1.0";
+declare author "Matt Horton, adapted from GRAME";
+declare license "MIT Style STK-4.2";
+declare description "Volume Control Faust Plugin for JackTrip, based on Faust examples";
+
+
+import("stdfaust.lib");
+mute = checkbox("[1] Mute");
+gain(v) = v : ba.db2linear : si.smoo : _;
+gainVMute(v) = _ * gain(v), 0 : select2(mute) : _;
+zeroCutoff(v) = _ , 0 : select2(v == -40) : _;
+volume = hslider("[0] Volume", 0, -40, 0, 0.1);
+process = _ <: vgroup("Volume Control", _ : gainVMute(volume) : zeroCutoff(volume));
diff --git a/faust-src/zitarevdsp.dsp b/faust-src/zitarevdsp.dsp
new file mode 100644 (file)
index 0000000..d709c6d
--- /dev/null
@@ -0,0 +1,106 @@
+import("stdfaust.lib");
+
+// Modified version from Faust Libraries demos.lib
+
+process = zita_rev1; // same as dm.zita_rev1 but for wetness control and some defaults
+
+//process = zita_rev1 : _,attach(cout); // Not using this solution yet, but it works
+//cout = ffunction (int cout(), <iostream>, ""); // dummy function to force #include <iostream> in output
+
+//----------------------------------`(dm.)zita_rev1`------------------------------
+// Example GUI for `zita_rev1_stereo` (mostly following the Linux `zita-rev1` GUI).
+//
+// Only the dry/wet and output level parameters are "dezippered" here. If
+// parameters are to be varied in real time, use `smooth(0.999)` or the like
+// in the same way.
+//
+// #### Usage
+//
+// ```
+// _,_ : zita_rev1 : _,_
+// ```
+//
+// #### Reference
+//
+// <http://www.kokkinizita.net/linuxaudio/zita-rev1-doc/quickguide.html>
+//------------------------------------------------------------
+zita_rev1 = _,_ <: re.zita_rev1_stereo(rdel,f1,f2,t60dc,t60m,fsmax),_,_ : out_eq,_,_ :
+       wet_dry_2(wet) : out_level
+with{
+       fsmax = 48000.0;  // highest sampling rate that will be used
+
+       fdn_group(x) = hgroup(
+       "[0] Zita_Rev1 [tooltip: ~ ZITA REV1 FEEDBACK DELAY NETWORK (FDN) & SCHROEDER
+       ALLPASS-COMB REVERBERATOR (8x8). See Faust's reverbs.lib for documentation and
+       references]", x);
+
+       in_group(x) = fdn_group(hgroup("[1] Input", x));
+
+       rdel = in_group(vslider("[1] In Delay [unit:ms] [style:knob] [tooltip: Delay in ms
+               before reverberation begins]",60,20,100,1));
+
+       freq_group(x) = fdn_group(hgroup("[2] Decay Times in Bands (see tooltips)", x));
+
+       f1 = freq_group(vslider("[1] LF X [unit:Hz] [style:knob] [scale:log] [tooltip:
+               Crossover frequency (Hz) separating low and middle frequencies]", 200, 50, 1000, 1));
+
+       t60dc = freq_group(vslider("[2] Low RT60 [unit:s] [style:knob] [scale:log]
+       [style:knob] [tooltip: T60 = time (in seconds) to decay 60dB in low-frequency band]",
+       3, 1, 8, 0.1));
+
+       t60m = freq_group(vslider("[3] Mid RT60 [unit:s] [style:knob] [scale:log] [tooltip:
+               T60 = time (in seconds) to decay 60dB in middle band]",2, 1, 8, 0.1));
+
+       f2 = freq_group(vslider("[4] HF Damping [unit:Hz] [style:knob] [scale:log]
+       [tooltip: Frequency (Hz) at which the high-frequency T60 is half the middle-band's T60]",
+       6000, 1500, 0.49*fsmax, 1));
+
+       out_eq = pareq_stereo(eq1f,eq1l,eq1q) : pareq_stereo(eq2f,eq2l,eq2q);
+       // Zolzer style peaking eq (not used in zita-rev1) (filters.lib):
+       // pareq_stereo(eqf,eql,Q) = peak_eq(eql,eqf,eqf/Q), peak_eq(eql,eqf,eqf/Q);
+       // Regalia-Mitra peaking eq with "Q" hard-wired near sqrt(g)/2 (filters.lib):
+       pareq_stereo(eqf,eql,Q) = fi.peak_eq_rm(eql,eqf,tpbt), fi.peak_eq_rm(eql,eqf,tpbt)
+       with {
+               tpbt = wcT/sqrt(max(0,g)); // tan(PI*B/SR), B bw in Hz (Q^2 ~ g/4)
+               wcT = 2*ma.PI*eqf/ma.SR;  // peak frequency in rad/sample
+               g = ba.db2linear(eql); // peak gain
+       };
+
+       eq1_group(x) = fdn_group(hgroup("[3] RM Peaking Equalizer 1", x));
+
+       eq1f = eq1_group(vslider("[1] Eq1 Freq [unit:Hz] [style:knob] [scale:log] [tooltip:
+               Center-frequency of second-order Regalia-Mitra peaking equalizer section 1]",
+       315, 40, 2500, 1));
+
+       eq1l = eq1_group(vslider("[2] Eq1 Level [unit:dB] [style:knob] [tooltip: Peak level
+               in dB of second-order Regalia-Mitra peaking equalizer section 1]", 0, -15, 15, 0.1));
+
+       eq1q = eq1_group(vslider("[3] Eq1 Q [style:knob] [tooltip: Q = centerFrequency/bandwidth
+               of second-order peaking equalizer section 1]", 3, 0.1, 10, 0.1));
+
+       eq2_group(x) = fdn_group(hgroup("[4] RM Peaking Equalizer 2", x));
+
+       eq2f = eq2_group(vslider("[1] Eq2 Freq [unit:Hz] [style:knob] [scale:log] [tooltip:
+               Center-frequency of second-order Regalia-Mitra peaking equalizer section 2]",
+       1500, 160, 10000, 1));
+
+       eq2l = eq2_group(vslider("[2] Eq2 Level [unit:dB] [style:knob] [tooltip: Peak level
+               in dB of second-order Regalia-Mitra peaking equalizer section 2]", 0, -15, 15, 0.1));
+
+       eq2q = eq2_group(vslider("[3] Eq2 Q [style:knob] [tooltip: Q = centerFrequency/bandwidth
+               of second-order peaking equalizer section 2]", 3, 0.1, 10, 0.1));
+
+       out_group(x)  = fdn_group(hgroup("[5] Output", x));
+
+       wet_dry(wet,y,x) = wet*y + (1-wet)*x;
+
+       wet_dry_2(wet,y1,y2,x1,x2) = wet_dry(wet,y1,x1), wet_dry(wet,y2,x2);
+
+       wet = out_group(vslider("[1] Wet [style:knob] [tooltip: Dry/Wet Mix: 0 = dry, 1 = wet]",
+       0, 0.0, 1.0, 0.01)) : si.smoo;
+
+       out_level = *(gain),*(gain);
+
+       gain = out_group(vslider("[2] Level [unit:dB] [style:knob] [tooltip: Output scale
+               factor]", -3, -70, 20, 0.1)) : ba.db2linear : si.smoo;
+};
diff --git a/faust-src/zitarevmonodsp.dsp b/faust-src/zitarevmonodsp.dsp
new file mode 100644 (file)
index 0000000..2a293d4
--- /dev/null
@@ -0,0 +1,2 @@
+process = _ <: _,_ : component("zitarevdsp.dsp") : _,_ :> _;
+
diff --git a/jacktrip.pro b/jacktrip.pro
new file mode 100644 (file)
index 0000000..c3f9fce
--- /dev/null
@@ -0,0 +1,432 @@
+#******************************
+# Created by Juan-Pablo Caceres
+#******************************
+
+CONFIG += c++17 console
+CONFIG -= app_bundle
+
+CONFIG += qt thread debug_and_release build_all qtquickcompiler
+CONFIG(debug, debug|release) {
+    TARGET = jacktrip_debug
+    application_id = 'org.jacktrip.JackTrip.Devel'
+    name_suffix = ' (Development Snapshot)'
+  } else {
+    TARGET = jacktrip
+    application_id = 'org.jacktrip.JackTrip'
+    name_suffix = ''
+}
+QMAKE_CFLAGS_RELEASE += -DNDEBUG
+QMAKE_CXXFLAGS_RELEASE += -DNDEBUG
+
+equals(QT_EDITION, "OpenSource") {
+  DEFINES += QT_OPENSOURCE
+}
+
+nogui {
+  DEFINES += NO_GUI
+  QT -= gui
+} else {
+  QT += gui
+  QT += widgets
+  novs {
+    DEFINES += NO_VS
+  } else {
+    QT += qml
+    QT += quick
+    QT += quickcontrols2
+    QT += svg
+    QT += websockets
+    QT += webview
+    QT += webenginequick
+    vsftux {
+      DEFINES += VS_FTUX
+    }
+  }
+  noupdater|linux-g++|linux-g++-64 {
+    DEFINES += NO_UPDATER
+  }
+}
+
+QT += network
+
+# switch added in rc.1.2 enables wair build, some of it merged as WAIRTOHUB
+# DEFINES += WAIR
+DEFINES += WAIRTOHUB
+
+# configuration with RtAudio
+rtaudio|bundled_rtaudio {
+  message(Building with RtAudio)
+  DEFINES += RT_AUDIO
+}
+# Configuration without Jack
+nojack {
+  DEFINES += NO_JACK
+}
+
+!win32 {
+  INCLUDEPATH+=/usr/local/include
+# wair needs stk, can be had from linux this way
+# INCLUDEPATH+=/usr/include/stk
+# LIBS += -L/usr/local/lib -ljack -lstk -lm
+  LIBS += -L/usr/local/lib -lm
+  QMAKE_CXXFLAGS += -fvisibility=hidden -fvisibility-inlines-hidden
+  weakjack {
+    message(Building with weak linking of JACK)
+    INCLUDEPATH += externals/weakjack
+    DEFINES += USE_WEAK_JACK
+  } else {
+    nojack {
+      message(Building NONJACK)
+    } else {
+      CONFIG += link_pkgconfig
+      PKGCONFIG += jack
+    }
+  }
+}
+
+bundled_rtaudio {
+  INCLUDEPATH += externals/rtaudio/
+  LIBS += -L$${OUT_PWD} -L$${OUT_PWD}/debug -L$${OUT_PWD}/release -lrtaudio
+  linux-g++ | linux-g++-64 {
+    LIBS += -lasound -lpthread -lpulse-simple -lpulse
+  }
+  macx {
+    LIBS += -lpthread -framework CoreAudio -framework CoreFoundation
+  }
+  win32 {
+    LIBS += -lole32 -lwinmm -lksuser -lmfplat -lmfuuid -lwmcodecdspuuid
+  }
+} else {
+  rtaudio {
+    # pkg-config is required for building with system-provided rtaudio
+    CONFIG += link_pkgconfig
+    PKGCONFIG += rtaudio
+    win32 {
+      # even though we get linker flags from pkg-config, define -lrtaudio again to enforce linking order
+      CONFIG += no_lflags_merge
+      LIBS += -lrtaudio -lole32 -lwinmm -lksuser -lmfplat -lmfuuid -lwmcodecdspuuid # -ldsound # -ldsound only needed if rtaudio is built with directsound support
+    }
+  }
+}
+
+macx {
+  message(Building on MAC OS X)
+  CONFIG -= app_bundle
+  LIBS += -framework CoreAudio -framework CoreFoundation
+  !nogui {
+    LIBS += -framework Foundation
+    CONFIG += objective_c
+    !novs {
+      LIBS += -framework AVFoundation -framework WebKit
+    }
+  }
+}
+
+linux-g++ | linux-g++-64 {
+
+  FEDORA = $$system(cat /proc/version | grep -o fc)
+
+  contains( FEDORA, fc): {
+    message(building on fedora)
+  }
+
+  UBUNTU = $$system(cat /proc/version | grep -o Ubuntu)
+
+  contains( UBUNTU, Ubuntu): {
+    message(building on  Ubuntu)
+
+    # workaround for Qt bug under ubuntu 18.04
+    # gcc version 7.3.0 (Ubuntu 7.3.0-16ubuntu3)
+    # QMake version 3.1
+    # Using Qt version 5.9.5 in /usr/lib/x86_64-linux-gnu
+    INCLUDEPATH += /usr/include/x86_64-linux-gnu/c++/7
+
+    # sets differences from original fedora version
+    DEFINES += __UBUNTU__
+  }
+
+  QMAKE_CXXFLAGS += -g -O2
+}
+
+linux-g++ {
+  message(Linux)
+}
+
+linux-g++-64 {
+  message(Linux 64bit)
+}
+
+win32 {
+  message(Building on win32)
+#cc  CONFIG += x86 console
+  CONFIG += c++17 console
+  exists("C:\Program Files\JACK2") {
+    message("using Jack in C:\Program Files\JACK2")
+    INCLUDEPATH += "C:\Program Files\JACK2\include"
+    weakjack {
+      message(Building with weak linking of JACK)
+      INCLUDEPATH += externals/weakjack
+      DEFINES += USE_WEAK_JACK
+    } else {
+      LIBS += "C:\Program Files\JACK2\lib\libjack64.lib"
+      LIBS += "C:\Program Files\JACK2\lib\libjackserver64.lib"
+    }
+  } else {
+    exists("C:\Program Files (x86)\Jack") {
+      message("using Jack in C:\Program Files (x86)\Jack")
+      INCLUDEPATH += "C:\Program Files (x86)\Jack\includes"
+      weakjack {
+        message(Building with weak linking of JACK)
+        INCLUDEPATH += externals/weakjack
+        DEFINES += USE_WEAK_JACK
+      } else {
+        LIBS += "C:\Program Files (x86)\Jack\lib\libjack64.lib"
+        LIBS += "C:\Program Files (x86)\Jack\lib\libjackserver64.lib"
+      }
+    } else {
+      message("Jack library not found")
+    }
+  }
+  LIBS += -lWs2_32
+  DEFINES += _WIN32_WINNT=0x0600 #needed for inet_pton
+  DEFINES += WIN32_LEAN_AND_MEAN
+  RC_FILE = win/qjacktrip.rc
+}
+
+DESTDIR = .
+QMAKE_CLEAN += -r ./jacktrip ./jacktrip_debug ./release/* ./debug/* ./$${application_id}.xml ./$${application_id}.desktop ./$${application_id}.png ./$${application_id}.svg ./jacktrip.1 ./librtaudio.a
+
+# isEmpty(PREFIX) will allow path to be changed during the command line
+# call to qmake, e.g. qmake PREFIX=/usr
+isEmpty(PREFIX) {
+  PREFIX = /usr/local
+}
+target.path = $$PREFIX/bin/
+INSTALLS += target
+
+# Input
+HEADERS += src/DataProtocol.h \
+           src/JackTrip.h \
+           src/Analyzer.h \
+           src/Effects.h \
+           src/Compressor.h \
+           src/CompressorPresets.h \
+           src/Limiter.h \
+           src/Regulator.h \
+           src/WaitFreeRingBuffer.h \
+           src/WaitFreeFrameBuffer.h \
+           src/Reverb.h \
+           src/Meter.h \
+           src/Monitor.h \
+           src/Volume.h \
+           src/Tone.h \
+           src/StereoToMono.h \
+           src/AudioTester.h \
+           src/jacktrip_globals.h \
+           src/jacktrip_types.h \
+           src/JackTripWorker.h \
+           src/JitterBuffer.h \
+           src/LoopBack.h \
+           src/PacketHeader.h \
+           src/ProcessPlugin.h \
+           src/RingBuffer.h \
+           src/RingBufferWavetable.h \
+           src/Settings.h \
+           src/UdpDataProtocol.h \
+           src/UdpHubListener.h \
+           src/AudioInterface.h \
+           src/compressordsp.h \
+           src/limiterdsp.h \
+           src/freeverbdsp.h \
+           src/meterdsp.h \
+           src/volumedsp.h \
+           src/tonedsp.h \
+           src/SslServer.h \
+           src/Auth.h
+#(Removed JackTripThread.h JackTripWorkerMessages.h NetKS.h TestRingBuffer.h ThreadPoolTest.h)
+
+!nojack {
+  HEADERS += src/JackAudioInterface.h \
+             src/JMess.h \
+             src/Patcher.h
+}
+
+!nogui {
+  HEADERS += src/gui/about.h \
+             src/gui/messageDialog.h \
+             src/gui/qjacktrip.h \
+             src/gui/textbuf.h \
+             src/gui/vuMeter.h
+  !novs {
+    HEADERS += src/gui/virtualstudio.h \
+               src/gui/vsApi.h \
+               src/gui/vsAuth.h \
+               src/gui/vsDeviceCodeFlow.h \
+               src/gui/vsDeeplink.h \
+               src/gui/vsDevice.h \
+               src/gui/vsAudio.h \
+               src/gui/vsServerInfo.h \
+               src/gui/vsQuickView.h \
+               src/gui/vsWebSocket.h \
+               src/gui/vsPermissions.h \
+               src/gui/vsPinger.h \
+               src/gui/vsPing.h \
+               src/gui/vsQmlClipboard.h \
+               src/JTApplication.h
+  }
+  !noupdater:!linux-g++:!linux-g++-64 {
+    HEADERS += src/dblsqd/feed.h \
+            src/dblsqd/release.h \
+            src/dblsqd/semver.h \
+            src/dblsqd/update_dialog.h
+  }
+}
+
+rtaudio|bundled_rtaudio {
+  HEADERS += src/RtAudioInterface.h
+}
+
+SOURCES += src/DataProtocol.cpp \
+           src/JackTrip.cpp \
+           src/Analyzer.cpp \
+           src/Compressor.cpp \
+           src/Limiter.cpp \
+           src/Regulator.cpp \
+           src/Reverb.cpp \
+           src/Meter.cpp \
+           src/Monitor.cpp \
+           src/StereoToMono.cpp \
+           src/Volume.cpp \
+           src/Tone.cpp \
+           src/AudioTester.cpp \
+           src/jacktrip_globals.cpp \
+           src/JackTripWorker.cpp \
+           src/JitterBuffer.cpp \
+           src/LoopBack.cpp \
+           src/PacketHeader.cpp \
+           src/RingBuffer.cpp \
+           src/Settings.cpp \
+           src/UdpDataProtocol.cpp \
+           src/UdpHubListener.cpp \
+           src/AudioInterface.cpp \
+           src/main.cpp \
+           src/SslServer.cpp \
+           src/Auth.cpp
+#(Removed jacktrip_main.cpp jacktrip_tests.cpp JackTripThread.cpp ProcessPlugin.cpp)
+
+!nojack {
+  SOURCES += src/JackAudioInterface.cpp \
+             src/JMess.cpp \
+             src/Patcher.cpp
+}
+
+!nogui {
+  SOURCES += src/gui/messageDialog.cpp \
+             src/gui/qjacktrip.cpp \
+             src/gui/about.cpp \
+             src/gui/textbuf.cpp \
+             src/gui/vuMeter.cpp
+  !novs {
+    SOURCES += src/gui/virtualstudio.cpp \
+               src/gui/vsApi.cpp \
+               src/gui/vsAuth.cpp \
+               src/gui/vsDeviceCodeFlow.cpp \
+               src/gui/vsDeeplink.cpp \
+               src/gui/vsDevice.cpp \
+               src/gui/vsAudio.cpp \
+               src/gui/vsServerInfo.cpp \
+               src/gui/vsQuickView.cpp \
+               src/gui/vsWebSocket.cpp \
+               src/gui/vsPermissions.cpp \
+               src/gui/vsPinger.cpp \
+               src/gui/vsPing.cpp
+  }
+  !noupdater:!linux-g++:!linux-g++-64 {
+    SOURCES += src/dblsqd/feed.cpp \
+              src/dblsqd/release.cpp \
+              src/dblsqd/semver.cpp \
+              src/dblsqd/update_dialog.cpp
+  }
+}
+
+!nogui {
+  macx {
+    HEADERS += src/gui/NoNap.h
+    OBJECTIVE_SOURCES += src/gui/NoNap.mm
+    !novs {
+      HEADERS += src/gui/vsMacPermissions.h
+      OBJECTIVE_SOURCES += src/gui/vsMacPermissions.mm
+    }
+  }
+  FORMS += src/gui/qjacktrip.ui \
+           src/gui/about.ui \
+           src/gui/messageDialog.ui
+  novs {
+    RESOURCES += src/gui/qjacktrip_novs.qrc
+  } else {
+    RESOURCES += src/gui/qjacktrip.qrc
+  }
+  !noupdater:!linux-g++:!linux-g++-64 {
+    FORMS += src/dblsqd/update_dialog.ui
+  }
+}
+
+rtaudio|bundled_rtaudio {
+  SOURCES += src/RtAudioInterface.cpp
+}
+
+weakjack {
+  SOURCES += externals/weakjack/weak_libjack.c
+}
+
+# install man page
+!win32 {
+    HELP2MAN_BIN = $$system(which help2man)
+
+    isEmpty(HELP2MAN_BIN) {
+        message("help2man not found")
+    } else {
+        message("Building man page with help2man")
+        man.extra = $${HELP2MAN_BIN} --no-info --section=1 --output $${OUT_PWD}/jacktrip.1 $${OUT_PWD}/jacktrip
+        man.CONFIG += no_check_exist
+        man.files = $${OUT_PWD}/jacktrip.1
+        man.path = $${PREFIX}/share/man/man1
+        INSTALLS += man
+    }
+}
+
+# install Linux desktop integration resources
+if(linux-g++ | linux-g++-64):!nogui {
+    appdata = $$cat($${PWD}/linux/org.jacktrip.JackTrip.metainfo.xml.in, blob)
+    appdata = $$replace(appdata, @appid@, $${application_id})
+    write_file($${OUT_PWD}/$${application_id}.metainfo.xml, appdata)
+
+    metainfo.files = $${OUT_PWD}/$${application_id}.metainfo.xml
+    metainfo.path = $${PREFIX}/share/metainfo
+
+    desktop_conf = $$cat($${PWD}/linux/org.jacktrip.JackTrip.desktop.in, blob)
+    desktop_conf = $$replace(desktop_conf, @icon@, $${application_id})
+    desktop_conf = $$replace(desktop_conf, @wmclass@, $$lower($${application_id}))
+    desktop_conf = $$replace(desktop_conf, @name_suffix@, $${name_suffix})
+    write_file($${OUT_PWD}/$${application_id}.desktop, desktop_conf)
+
+    desktop.files = $${OUT_PWD}/$${application_id}.desktop
+    desktop.path = $${PREFIX}/share/applications
+
+    icon48.extra = cp $${PWD}/linux/icons/jacktrip_48x48.png $${OUT_PWD}/$${application_id}.png
+    icon48.CONFIG += no_check_exist
+    icon48.files = $${OUT_PWD}/$${application_id}.png
+    icon48.path = $${PREFIX}/share/icons/hicolor/48x48/apps
+
+    icon_svg.extra = cp $${PWD}/linux/icons/jacktrip.svg $${OUT_PWD}/$${application_id}.svg
+    icon_svg.CONFIG += no_check_exist
+    icon_svg.files = $${OUT_PWD}/$${application_id}.svg
+    icon_svg.path = $${PREFIX}/share/icons/hicolor/scalable/apps
+
+    icon_symbolic.extra = cp $${PWD}/linux/icons/jacktrip-symbolic.svg $${OUT_PWD}/$${application_id}-symbolic.svg
+    icon_symbolic.CONFIG += no_check_exist
+    icon_symbolic.files = $${OUT_PWD}/$${application_id}-symbolic.svg
+    icon_symbolic.path = $${PREFIX}/share/icons/hicolor/symbolic/apps
+
+    INSTALLS += metainfo desktop icon48 icon_svg icon_symbolic
+}
diff --git a/jacktrip_and_rtaudio.pro b/jacktrip_and_rtaudio.pro
new file mode 100644 (file)
index 0000000..5d603ef
--- /dev/null
@@ -0,0 +1,14 @@
+# created by Marcin Pączkowski 
+# configuration for building JackTrip with the bundled RtAudio library
+# make sure to specify '-config bundled_rtaudio' when running qmake
+
+TEMPLATE = subdirs
+SUBDIRS = jacktrip \
+          rtaudio 
+
+jacktrip.file = jacktrip.pro
+rtaudio.file = rtaudio.pro
+
+jacktrip.depends = rtaudio
+
+CONFIG += debug_and_release
diff --git a/linux/README.md b/linux/README.md
new file mode 100644 (file)
index 0000000..fb8f267
--- /dev/null
@@ -0,0 +1,42 @@
+# JackTrip is a multi-machine audio system used for network music performance over the Internet.
+
+See LICENSE.md for license information.
+
+JackTrip requires that Qt6 is installed on your machine.
+
+For Fedora or RedHat:
+
+```
+dnf install -y qt6-qtbase qt6-qtbase-common qt6-qtbase-gui qt6-qtsvg qt6-qtwebsockets qt6-qtwebengine qt6-qtwebchannel qt6-qt5compat
+```
+
+For Debian or Ubuntu:
+
+```
+apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6svg6  libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window
+```
+
+To install JackTrip as a Linux desktop application:
+
+```
+sudo cp jacktrip /usr/local/bin
+mkdir -p $HOME/.local/share/applications $HOME/.local/share/icons/hicolor/scalable/apps $HOME/.local/share/icons/hicolor/48x48/apps
+cp org.jacktrip.JackTrip.svg $HOME/.local/share/icons/hicolor/scalable/apps/
+cp org.jacktrip.JackTrip.png $HOME/.local/share/icons/hicolor/48x48/apps/
+desktop-file-install --dir=$HOME/.local/share/applications org.jacktrip.JackTrip.desktop
+update-desktop-database $HOME/.local/share/applications
+```
+
+To install the manual page for JackTrip:
+
+```
+sudo mkdir -p /usr/local/share/man/man1
+sudo cp jacktrip.1.gz /usr/local/share/man/man1
+```
+
+When using jacktrip with the JACK Audio Connection Kit (or Pipewire), ensure that your user account has permission to schedule realtime processes.
+`ulimit -r` should return a value greater than 40.
+
+Further information and instructions are available on https://jacktrip.github.io/jacktrip/. 
+
+Please report any security concerns to vulnerabilities@jacktrip.org
diff --git a/linux/add_changelog_to_metainfo.py b/linux/add_changelog_to_metainfo.py
new file mode 100755 (executable)
index 0000000..8d460c0
--- /dev/null
@@ -0,0 +1,23 @@
+#!/usr/bin/env python3
+
+import sys
+import yaml
+from jinja2 import Template
+
+changelog_file = sys.path[0] + "/../docs/changelog.yml"
+with open(changelog_file) as f:
+    file_array = yaml.load(f, Loader=yaml.FullLoader)
+    releases = { "releases": file_array}
+
+if len(sys.argv) < 3:
+    exit('Not enough arguments')
+
+template_file = sys.argv[1]
+output_file = sys.argv[2]
+
+with open(template_file) as f2:
+    template = Template(f2.read())
+
+with open(output_file, "w") as f_out:
+    f_out.write(template.render(releases))
+
diff --git a/linux/container/README.md b/linux/container/README.md
new file mode 100644 (file)
index 0000000..5d825f5
--- /dev/null
@@ -0,0 +1,69 @@
+# Run a JackTrip Server in a Container
+
+Copyright (c) 2023-2024 JackTrip Labs, Inc.
+See [MIT License](../../LICENSES/MIT.txt)
+
+This repository provides the source code for building a container image to
+run your own JackTrip hub server. It uses the
+[Jack base container](https://github.com/jacktriplabs/jack-container)
+and runs the [Jack Audio Connection Kit](https://jackaudio.org/)
+(`jackd`) and JackTrip servers as systemd services. The `jackd` server
+is configued to use the `dummy` audio backend so that no audio interface
+is required.
+
+New container images are built automatically for each release of JackTrip
+and made freely available on
+[Docker Hub](https://hub.docker.com/repository/docker/jacktrip/jacktrip/general).
+
+To build a container image using `podman`:
+
+```bash
+podman build -t jacktrip/jacktrip .
+```
+
+To run a JackTrip container using `podman`:
+
+```bash
+podman run --name jacktrip --network=host --shm-size=128M -d jacktrip/jacktrip
+```
+
+`jackd` requires the ability to lock about 128MB of shared memory, and
+both `jackd` and `jacktrip` will try to run realtime priority threads.
+Be sure that your memory limits (`ulimit`) are set appropriately.
+
+```
+ulimit -l 128000000
+ulimit -r 10
+```
+
+If using `docker`, you will need to run this as a privileged container:
+
+```bash
+docker run --name jacktrip --network=host --shm-size=128M --privileged -d jacktrip/jacktrip
+```
+
+Docker Desktop users on Mac and Windows computers may prefer to specify
+a port range instead of using host networking:
+
+```bash
+docker run --name jacktrip -p 4464:4464/tcp -p 61000-61100:61000-61100/udp --shm-size=128M --privileged -d jacktrip/jacktrip
+```
+
+By default, the servers will run using a sample rate of 48Khz and buffer
+size of 128. You can override these using the following environment
+variables:
+
+* __SAMPLE_RATE__: use this to set the sample rate for the `jackd` server.
+Note that all clients connecting to the server must use the same setting.
+The default is 48000.
+
+* __BUFFER_SIZE__: use this to set the buffer size, also known as frames
+per period, for the `jackd` server. The default is 128.
+
+* __JACK_OPTS__: use this to override all of the options passed to the
+`jackd` server. __SAMPLE_RATE__ and __BUFFER_SIZE__ will be ignored
+if this is defined.
+
+* __JACKTRIP_OPTS__: use this to override all of the options passed to the
+`jacktrip` server. The default options are:
+`-S -t -z --hubpatch 4 --bufstrategy 4 -q auto`
diff --git a/linux/container/jacktrip.service b/linux/container/jacktrip.service
new file mode 100644 (file)
index 0000000..957d721
--- /dev/null
@@ -0,0 +1,33 @@
+[Unit]
+Description=JackTrip-Server
+After=network.target jack.service defaults.service
+Wants=jack.service defaults.service
+
+[Service]
+Type=simple
+User=jacktrip
+Group=audio
+NoNewPrivileges=true
+ProtectSystem=true
+ProtectHome=true
+Nice=-20
+#IOSchedulingClass=realtime
+#IOSchedulingPriority=0
+Environment="JACK_NO_AUDIO_RESERVATION=1"
+Environment="JACK_NO_START_SERVER=1"
+Environment="JACK_PROMISCUOUS_SERVER=audio"
+EnvironmentFile=/etc/default/jacktrip
+ExecStartPre=/usr/local/bin/jack_wait -w -t 5
+ExecStart=/usr/local/bin/jacktrip $JACKTRIP_OPTS
+Restart=always
+RestartSec=5
+StandardOutput=journal
+StandardError=inherit
+SyslogIdentifier=jacktrip
+LimitMEMLOCK=infinity
+LimitRTPRIO=99
+LimitNOFILE=200000
+LimitNPROC=200000
+
+[Install]
+WantedBy=multi-user.target
diff --git a/linux/icons/jacktrip-symbolic.svg b/linux/icons/jacktrip-symbolic.svg
new file mode 100644 (file)
index 0000000..cbb1f7b
--- /dev/null
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>Artboard Copy 2</title>
+    <g id="Artboard-Copy-2" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <path d="M6.64639356,12.2679165 C6.65038057,12.2752708 6.6540828,12.2825243 6.65750023,12.2899793 L7.05325841,13.017447 C7.36339081,13.5859417 7.86480474,13.9691694 8.42289117,14.1277394 C8.98135732,14.2866117 9.59602131,14.2210278 10.1314198,13.8929067 C10.1378749,13.8889777 10.1442351,13.8853509 10.1506903,13.8818249 C10.6759314,13.5532001 11.0302057,13.0264132 11.1781997,12.4400869 C11.3279973,11.8467086 11.2661987,11.1938917 10.9568257,10.6251955 L8.43589642,5.99120302 C8.05883921,5.9779049 7.75687069,5.64938084 7.75687069,5.245803 C7.75687069,4.83396419 8.07155967,4.5 8.45972355,4.5 C8.84788742,4.5 9.16248147,4.83396419 9.16248147,5.245803 C9.16248147,5.37354556 9.13219918,5.49363163 9.07903905,5.59870694 L11.6000633,10.2329009 C12.0125289,10.9909946 12.0954967,11.8580926 11.8970955,12.6439914 C11.7016371,13.4181033 11.2325939,14.1155495 10.5356266,14.554992 C10.5174952,14.5688945 10.4980348,14.5810845 10.4775302,14.591461 C9.7693613,15.0154898 8.9628462,15.0988045 8.23075527,14.8906688 C7.49771506,14.6823315 6.83786496,14.1814356 6.42473481,13.4380505 C6.41267885,13.4194129 6.40214176,13.3995665 6.39312352,13.3787126 L6.01492717,12.6833821 C6.01094016,12.6768337 6.00685822,12.6701847 6.003156,12.6633341 C5.95948875,12.5831424 5.88629864,12.528338 5.80428016,12.5050663 C5.72207181,12.4817946 5.6330286,12.4901563 5.55803484,12.5359945 C5.38042305,12.6450996 5.15297364,12.5809261 5.05007082,12.3924353 C4.94726293,12.2039445 5.00782751,11.9625634 5.18543929,11.8533576 C5.43927891,11.6977092 5.73203934,11.6670832 5.99641605,11.742137 C6.26117248,11.8173922 6.50010828,11.9990325 6.64639356,12.2679165 Z M9.30469654,1.73361307 C9.30469654,2.13876486 8.96448358,2.46712705 8.54470965,2.46712705 C8.4146403,2.46712705 8.2922704,2.43551947 8.18509408,2.3800328 L6.66460699,3.22729446 C6.65793413,3.23155504 6.65115862,3.23571654 6.64417779,3.23958079 C6.24401178,3.4626174 5.97442843,3.82397435 5.86283775,4.22625272 C5.75104176,4.62882834 5.79693047,5.07173088 6.02760595,5.45736325 L6.04208091,5.4814405 C6.04639261,5.4885745 6.05039632,5.49580759 6.05419471,5.50313975 L9.20275585,10.7666451 C9.21938666,10.789137 9.233143,10.8130161 9.24423021,10.837886 C9.3830256,11.0932238 9.40786915,11.3818536 9.33518636,11.6435327 C9.26137431,11.9097696 9.08685348,12.1509385 8.82907595,12.3047158 C8.67190454,12.4111313 8.45344552,12.3967642 8.31300758,12.2612182 C8.15573351,12.1094225 8.15573351,11.8631013 8.31300758,11.7113057 C8.33795379,11.6885165 8.36546649,11.6678081 8.3964696,11.6504685 C8.47828907,11.6048902 8.53403308,11.528596 8.55774738,11.4430871 C8.581667,11.3569836 8.57324894,11.2638453 8.52674427,11.1853713 L5.35929382,5.89006021 C5.35487947,5.88361979 5.35056778,5.87708029 5.34656407,5.87034262 L5.33198645,5.84626537 C4.98961764,5.27376181 4.92073324,4.61911817 5.08539878,4.02570803 C5.25016697,3.43209973 5.64879309,2.89833776 6.24123998,2.56819206 C6.24863145,2.56403056 6.25612558,2.5602654 6.26372237,2.55659931 L7.78513339,1.70884223 C7.79868442,1.31518411 8.1335591,1 8.54470965,1 C8.96448358,1 9.30469654,1.32846128 9.30469654,1.73361307 Z" id="Fill-3" stroke="#FFFFFF" stroke-width="0.5" fill="#FFFFFF" fill-rule="nonzero"></path>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/linux/icons/jacktrip.svg b/linux/icons/jacktrip.svg
new file mode 100644 (file)
index 0000000..2d1d67f
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="1366px" height="1366px" viewBox="0 0 1366 1366" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <title>jacktrip</title>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="jacktrip">
+            <path d="M754.755517,107 C789.35747,107 817.401215,135.042996 817.401215,169.633588 C817.401215,204.22418 789.35747,232.258717 754.755517,232.258717 C744.033904,232.258717 733.946957,229.56016 725.112416,224.822882 L599.778709,297.159431 C599.228666,297.523187 598.67016,297.878483 598.09473,298.2084 C565.109058,317.250568 542.88731,348.102093 533.688894,382.447362 C524.473554,416.818009 528.256159,454.631642 547.270732,487.555727 L548.463903,489.611367 C548.819315,490.220446 549.149341,490.837984 549.462443,491.463982 L808.998246,940.845596 C810.369123,942.765886 811.503059,944.804608 812.416977,946.927923 C823.857877,968.727864 825.90573,993.370171 819.91449,1015.71151 C813.830165,1038.44199 799.444418,1059.03223 778.195823,1072.16126 C765.240188,1081.24669 747.232618,1080.02007 735.656322,1068.44757 C722.692225,1055.48773 722.692225,1034.4576 735.656322,1021.49776 L735.664784,1021.49776 L735.715558,1021.54006 C737.712638,1019.55209 739.980509,1017.78407 742.536094,1016.30367 C749.280471,1012.41234 753.875448,1005.89858 755.830217,998.598096 C757.801911,991.246855 757.10801,983.294995 753.274631,976.59513 L753.23232,976.518995 L492.181782,524.498039 C491.817908,523.948176 491.462495,523.389854 491.132469,522.814613 L489.930836,520.758973 C461.709385,471.880411 456.031246,415.988985 469.604622,365.325484 C483.186459,314.645065 516.045198,269.074138 564.880579,240.887331 C565.489858,240.532035 566.107599,240.210577 566.733802,239.897579 L692.143668,167.518732 C693.260679,133.909433 720.864389,107 754.755517,107 M517.763026,1028.58676 C518.118438,1029.2043 518.448464,1029.81338 518.753104,1030.43937 L554.032033,1091.52489 L554.082806,1091.49105 C581.678054,1139.26143 626.375417,1171.44108 676.124716,1184.75622 C725.907863,1198.09673 780.700635,1192.58965 828.427467,1165.0373 C829.002897,1164.70738 829.569865,1164.40284 830.145295,1164.10676 C876.966671,1136.51211 908.547617,1092.27778 921.740193,1043.04392 C935.093551,993.217901 929.584656,938.400822 902.006333,890.647364 L677.284038,501.530698 C643.672162,500.414054 616.753891,472.827867 616.753891,438.939407 C616.753891,404.357275 644.806098,376.314278 679.408051,376.314278 C714.010004,376.314278 742.053749,404.357275 742.053749,438.939407 C742.053749,449.665959 739.354306,459.749594 734.615471,468.572775 L959.346229,857.70636 C996.114506,921.363539 1003.51047,994.173816 985.824466,1060.1658 C968.400787,1125.16802 926.589036,1183.73263 864.459533,1220.6411 C862.843252,1221.80004 861.1085,1222.82363 859.280664,1223.69496 C796.15262,1259.30068 724.257733,1266.29662 658.997214,1248.81945 C593.652073,1231.32536 534.831292,1189.27355 498.003779,1126.84299 C496.929079,1125.278 495.989774,1123.61149 495.185865,1121.86039 L461.472443,1063.47343 C461.117031,1062.92357 460.753156,1062.36525 460.42313,1061.79001 C456.530516,1055.0563 450.006157,1050.45438 442.694812,1048.50025 C435.366543,1046.54612 427.428996,1047.24825 420.743854,1051.10575 C404.91107,1060.26731 384.635629,1054.8702 375.4626,1039.04261 C366.298033,1023.21502 371.696919,1002.94624 387.529703,993.776223 C410.157637,980.706411 436.255075,978.143205 459.822314,984.437017 C483.423401,990.756208 504.722769,1006.00855 517.763026,1028.58676" id="Fill-1" fill="#F21B1B"></path>
+            <rect id="Rectangle" x="0" y="0" width="1365.33" height="1365.33"></rect>
+        </g>
+    </g>
+</svg>
\ No newline at end of file
diff --git a/linux/icons/jacktrip_48x48.png b/linux/icons/jacktrip_48x48.png
new file mode 100644 (file)
index 0000000..f8036d9
Binary files /dev/null and b/linux/icons/jacktrip_48x48.png differ
diff --git a/linux/icons/jacktrip_48x48_alt.png b/linux/icons/jacktrip_48x48_alt.png
new file mode 100644 (file)
index 0000000..115f265
Binary files /dev/null and b/linux/icons/jacktrip_48x48_alt.png differ
diff --git a/linux/icons/jacktrip_alt.svg b/linux/icons/jacktrip_alt.svg
new file mode 100644 (file)
index 0000000..ed23ed8
--- /dev/null
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   version="1.1"
+   id="svg2"
+   xml:space="preserve"
+   width="1365.3333"
+   height="1365.3333"
+   viewBox="0 0 1365.3333 1365.3333"><metadata
+     id="metadata8"><rdf:RDF><cc:Work
+         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+     id="defs6"><clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath24"><path
+         d="M 0,1024 H 1024 V 0 H 0 Z"
+         id="path22" /></clipPath></defs><g
+     id="g10"
+     transform="matrix(1.3333333,0,0,-1.3333333,0,1365.3333)"><path
+       d="m 681.391,888.908 h -31.223 v 19.043 h 31.223 z"
+       style="fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:none"
+       id="path12" /><g
+       id="g14"
+       transform="translate(665,280)"><path
+         d="M 0,0 V 252.232"
+         style="fill:none;stroke:#000000;stroke-width:42;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+         id="path16" /></g><g
+       id="g18"><g
+         id="g20"
+         clip-path="url(#clipPath24)"><g
+           id="g26"
+           transform="translate(312.3477,281.6523)"><path
+             d="M 0,0 C 0,-97.73 79.082,-176.812 176.812,-176.812"
+             style="fill:none;stroke:#000000;stroke-width:42;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+             id="path28" /></g><g
+           id="g30"
+           transform="translate(487.96,105.0908)"><path
+             d="M 0,0 C 97.73,0 176.812,79.081 176.812,176.812"
+             style="fill:none;stroke:#000000;stroke-width:42;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+             id="path32" /></g><g
+           id="g34"
+           transform="translate(626.5,747.5)"><path
+             d="m 0,0 v -104.385 -81.524 c 0,-14.58 12.403,-26.4 27.703,-26.4 h 23.594 c 15.3,0 27.703,11.82 27.703,26.4 V 0 Z"
+             style="fill:#5f709b;fill-opacity:1;fill-rule:nonzero;stroke:none"
+             id="path36" /></g><g
+           id="g38"
+           transform="translate(626.5,747.5)"><path
+             d="m 0,0 v -104.385 -81.524 c 0,-14.58 12.403,-26.4 27.703,-26.4 h 23.594 c 15.3,0 27.703,11.82 27.703,26.4 V 0 Z"
+             style="fill:none;stroke:#000000;stroke-width:16;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+             id="path40" /></g><path
+           d="m 686,752 h -40 v 76 h 40 z"
+           style="fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+           id="path42" /><path
+           d="m 686,834 h -40 v 24 h 40 z"
+           style="fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+           id="path44" /><path
+           d="m 686,864 h -40 v 24 h 40 z"
+           style="fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+           id="path46" /><g
+           id="g48"
+           transform="translate(652.6396,908.0542)"><path
+             d="M 0,0 -6.786,-16.832"
+             style="fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+             id="path50" /></g><g
+           id="g52"
+           transform="translate(686.5176,891.2578)"><path
+             d="M 0,0 -6.786,16.832"
+             style="fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+             id="path54" /></g><g
+           id="g56"
+           transform="translate(655.8232,902.8623)"><path
+             d="m 0,0 c -6.428,3.598 -10.772,10.473 -10.772,18.363 0,11.615 9.416,21.03 21.031,21.03 11.614,0 21.03,-9.415 21.03,-21.03 0,-8.028 -4.497,-15.004 -11.109,-18.549"
+             style="fill:#d3d3d3;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+             id="path58" /></g><g
+           id="g60"
+           transform="translate(260.0122,300.8564)"><path
+             d="M 0,0 H 104.874"
+             style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+             id="path62" /></g><g
+           id="g64"
+           transform="translate(303.1138,271.2197)"><path
+             d="M 0,0 C 0,15.936 -18.204,28.829 -40.7,28.829"
+             style="fill:none;stroke:#000000;stroke-width:22;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+             id="path66" /></g><g
+           id="g68"
+           transform="translate(322.312,272.6973)"><path
+             d="M 0,0 C 0,14.682 16.772,26.562 37.5,26.562"
+             style="fill:none;stroke:#000000;stroke-width:22;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+             id="path70" /></g><path
+           d="m 315.307,279.838 h -6.542 v 13.629 h 6.542 z"
+           style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
+           id="path72" /></g></g></g></svg>
diff --git a/linux/meson.build b/linux/meson.build
new file mode 100644 (file)
index 0000000..34d8690
--- /dev/null
@@ -0,0 +1,33 @@
+if host_machine.system() == 'linux'
+       install_data('icons/jacktrip.svg', rename: '@0@.svg'.format(application_id), install_dir: get_option('datadir') / 'icons' / 'hicolor' / 'scalable' / 'apps')
+       install_data('icons/jacktrip_48x48.png', rename: '@0@.png'.format(application_id), install_dir: get_option('datadir') / 'icons' / 'hicolor' / '48x48' / 'apps')
+       install_data('icons/jacktrip-symbolic.svg', rename: '@0@-symbolic.svg'.format(application_id), install_dir: get_option('datadir') / 'icons' / 'hicolor' / 'symbolic' / 'apps')
+       desktop_conf = configuration_data()
+       desktop_conf.set('icon', application_id)
+       desktop_conf.set('wmclass', application_id.to_lower())
+       desktop_conf.set('name_suffix', name_suffix)
+       configure_file(
+           input: files('org.jacktrip.JackTrip.desktop.in'),
+           output: '@0@.desktop'.format(application_id),
+           configuration: desktop_conf,
+           install_dir: get_option('datadir') / 'applications'
+       )
+
+       prog_python = import('python').find_installation('python3')
+
+       metainfo_in = configure_file(
+               input: ['add_changelog_to_metainfo.py', 'org.jacktrip.JackTrip.metainfo.xml.in.in'],
+               output: 'org.jacktrip.JackTrip.metainfo.xml.in',
+               command: [prog_python, '@INPUT@', '@OUTPUT@']
+       )
+
+       appdata_conf = configuration_data()
+       appdata_conf.set('appid', application_id)
+       configure_file(
+           input: metainfo_in,
+           output: '@0@.metainfo.xml'.format(application_id),
+           configuration: appdata_conf,
+           install_dir: get_option('datadir') / 'metainfo'
+       )
+
+endif
diff --git a/linux/org.jacktrip.JackTrip.desktop.in b/linux/org.jacktrip.JackTrip.desktop.in
new file mode 100644 (file)
index 0000000..c190cb0
--- /dev/null
@@ -0,0 +1,10 @@
+[Desktop Entry]
+Type=Application
+Name=JackTrip@name_suffix@
+Comment=Network Music Performance over the Internet
+Comment[fr]=Performance de musique en réseau sur internet
+Exec=jacktrip %u
+Icon=@icon@
+Terminal=false
+StartupWMClass=@wmclass@
+MimeType=application/jacktrip;x-scheme-handler/jacktrip;
diff --git a/linux/org.jacktrip.JackTrip.metainfo.xml.in.in b/linux/org.jacktrip.JackTrip.metainfo.xml.in.in
new file mode 100644 (file)
index 0000000..105afa2
--- /dev/null
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2021 Nils Tonnätt <nils.tonnaett@posteo.de> -->
+<component type="desktop-application">
+  <id>@appid@</id>
+  <metadata_license>MIT</metadata_license>
+  <project_license>GPL-3.0+</project_license>
+  <name>JackTrip</name>
+  <summary>Connect and play with other musicians</summary>
+
+  <categories>
+    <category>Audio</category>
+    <category>Network</category>
+  </categories>
+
+  <description>
+    <p>
+      JackTrip is a cross-platform multi-machine audio system
+      used for network music performance over the Internet.
+      It supports any number of channels (as many as the computer/network can handle)
+      of bidirectional, high quality, uncompressed audio signal streaming.
+    </p>
+    <p>
+      You can use it between any combination of machines e.g., one end using
+      Linux can connect to another using Mac OSX.
+    </p>
+  </description>
+
+  <launchable type="desktop-id">@appid@.desktop</launchable>
+
+  <url type="homepage">https://github.com/jacktrip/jacktrip</url>
+  <url type="help">https://jacktrip.github.io/jacktrip</url>
+  <url type="bugtracker">https://github.com/jacktrip/jacktrip/issues</url>
+  <project_group>JackTrip</project_group>
+  <developer_name>The JackTrip Community</developer_name>
+
+  <provides>
+    <binary>jacktrip</binary>
+  </provides>
+
+  <requires>
+    <control>keyboard</control>
+    <control>pointing</control>
+    <display_length compare="ge">907</display_length>
+  </requires>
+
+  <screenshots>
+    <screenshot type="default">
+      <caption>Hubclient Mode</caption>
+      <image>https://jacktrip.github.io/jacktrip/images/jacktrip_hubclient_basic.png</image>
+    </screenshot>
+    <screenshot>
+      <caption>Hubclient Mode // Plugins tab</caption>
+      <image>https://jacktrip.github.io/jacktrip/images/jacktrip_hubclient_plugins.png</image>
+    </screenshot>
+    <screenshot>
+      <caption>Hubserver Mode</caption>
+      <image>https://jacktrip.github.io/jacktrip/images/jacktrip_hubserver_basic.png</image>
+    </screenshot>
+    <screenshot>
+      <caption>Hubserver Mode // Jitter Buffer Tab</caption>
+      <image>https://jacktrip.github.io/jacktrip/images/jacktrip_hubserver_jitter.png</image>
+    </screenshot>
+  </screenshots>
+
+  <releases>
+  {%- for release in releases %}
+    <release version="{{ release.Version }}" {%- if release.Date %} date="{{ release.Date }}"{%- endif %}{% if release.Type == 'development' %} type="development"{% endif %}>
+      <description>
+        <ul>
+      {%- for change in release.Description %}
+          <li>{{ change }}</li>
+      {%- endfor %}
+        </ul>
+      </description>
+    </release>
+  {%- endfor %}
+  </releases>
+
+  <content_rating type="oars-1.1">
+    <content_attribute id="social-audio">intense</content_attribute>
+  </content_rating>
+
+</component>
diff --git a/macos/Info_novs.plist b/macos/Info_novs.plist
new file mode 100644 (file)
index 0000000..0ca37fd
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>BuildMachineOSBuild</key>
+       <string>19E287</string>
+       <key>CFBundleDevelopmentRegion</key>
+       <string>en</string>
+       <key>CFBundleExecutable</key>
+       <string>jacktrip</string>
+       <key>CFBundleIconFile</key>
+       <string>jacktrip</string>
+       <key>CFBundleIdentifier</key>
+       <string>%BUNDLEID%</string>
+       <key>CFBundleInfoDictionaryVersion</key>
+       <string>6.0</string>
+       <key>CFBundleName</key>
+       <string>%BUNDLENAME%</string>
+       <key>CFBundlePackageType</key>
+       <string>APPL</string>
+       <key>CFBundleShortVersionString</key>
+       <string>%VERSION%</string>
+       <key>CFBundleSignature</key>
+       <string>????</string>
+       <key>CFBundleSupportedPlatforms</key>
+       <array>
+               <string>MacOSX</string>
+       </array>
+       <key>CFBundleVersion</key>
+       <string>%VERSION%</string>
+       <key>DTCompiler</key>
+       <string>com.apple.compilers.llvm.clang.1_0</string>
+       <key>DTPlatformBuild</key>
+       <string>11E503a</string>
+       <key>DTPlatformVersion</key>
+       <string>GM</string>
+       <key>DTSDKBuild</key>
+       <string>19E258</string>
+       <key>DTSDKName</key>
+       <string>macosx10.15</string>
+       <key>DTXcode</key>
+       <string>1141</string>
+       <key>DTXcodeBuild</key>
+       <string>11E503a</string>
+       <key>LSMinimumSystemVersion</key>
+       <string>10.13</string>
+       <key>NSHighResolutionCapable</key>
+       <true/>
+       <key>NSHumanReadableCopyright</key>
+       <string>Copyright © 2020 Juan-Pablo Caceres, Chris Chafe, Aaron Wyatt, et al. All rights reserved.</string>
+       <key>NSMicrophoneUsageDescription</key>
+       <string>This app requires microphone access to allow the jack server to capture audio.</string>
+</dict>
+</plist>
diff --git a/macos/JackTrip.app_template/Contents/Info.plist b/macos/JackTrip.app_template/Contents/Info.plist
new file mode 100644 (file)
index 0000000..f28fbb1
--- /dev/null
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>BuildMachineOSBuild</key>
+       <string>19E287</string>
+       <key>CFBundleDevelopmentRegion</key>
+       <string>en</string>
+       <key>CFBundleExecutable</key>
+       <string>jacktrip</string>
+       <key>CFBundleIconFile</key>
+       <string>jacktrip</string>
+       <key>CFBundleIdentifier</key>
+       <string>%BUNDLEID%</string>
+       <key>CFBundleInfoDictionaryVersion</key>
+       <string>6.0</string>
+       <key>CFBundleName</key>
+       <string>%BUNDLENAME%</string>
+       <key>CFBundleURLTypes</key>
+               <array>
+                       <dict>
+                               <key>CFBundleURLSchemes</key>
+                                       <array>
+                                               <string>jacktrip</string>
+                                       </array>
+                               <key>CFBundleTypeRole</key>
+                               <string>Editor</string>
+                       </dict>
+               </array>
+       <key>CFBundlePackageType</key>
+       <string>APPL</string>
+       <key>CFBundleShortVersionString</key>
+       <string>%VERSION%</string>
+       <key>CFBundleSignature</key>
+       <string>????</string>
+       <key>CFBundleSupportedPlatforms</key>
+       <array>
+               <string>MacOSX</string>
+       </array>
+       <key>CFBundleVersion</key>
+       <string>%VERSION%</string>
+       <key>DTCompiler</key>
+       <string>com.apple.compilers.llvm.clang.1_0</string>
+       <key>DTPlatformBuild</key>
+       <string>11E503a</string>
+       <key>DTPlatformVersion</key>
+       <string>GM</string>
+       <key>DTSDKBuild</key>
+       <string>19E258</string>
+       <key>DTSDKName</key>
+       <string>macosx10.15</string>
+       <key>DTXcode</key>
+       <string>1141</string>
+       <key>DTXcodeBuild</key>
+       <string>11E503a</string>
+       <key>LSMinimumSystemVersion</key>
+       <string>10.13</string>
+       <key>NSHighResolutionCapable</key>
+       <true/>
+       <key>NSHumanReadableCopyright</key>
+       <string>Copyright © 2020 Juan-Pablo Caceres, Chris Chafe, Aaron Wyatt, et al. All rights reserved.</string>
+       <key>NSMicrophoneUsageDescription</key>
+       <string>JackTrip requires microphone access to capture audio.</string>
+       <key>NSCameraUsageDescription</key>
+       <string>JackTrip requires camera access to capture video.</string>
+</dict>
+</plist>
diff --git a/macos/JackTrip.app_template/Contents/PkgInfo b/macos/JackTrip.app_template/Contents/PkgInfo
new file mode 100644 (file)
index 0000000..bd04210
--- /dev/null
@@ -0,0 +1 @@
+APPL????
\ No newline at end of file
diff --git a/macos/JackTrip.app_template/Contents/Resources/jacktrip.icns b/macos/JackTrip.app_template/Contents/Resources/jacktrip.icns
new file mode 100644 (file)
index 0000000..532faa2
Binary files /dev/null and b/macos/JackTrip.app_template/Contents/Resources/jacktrip.icns differ
diff --git a/macos/assemble_app.sh b/macos/assemble_app.sh
new file mode 100755 (executable)
index 0000000..37c3d4c
--- /dev/null
@@ -0,0 +1,280 @@
+#!/bin/sh
+
+set -e
+
+APPNAME="JackTrip"
+BUNDLE_ID="org.jacktrip.jacktrip"
+BUILD_INSTALLER=false
+NOTARIZE=false
+
+#If you're lazy like I am, you can pre-populate these variables to save you stuffing about with command line options.
+#CERTIFICATE=""
+#PACKAGE_CERT=""
+USERNAME=""
+PASSWORD=""
+TEAM_ID=""
+KEY_STORE="AC_PASSWORD"
+TEMP_KEYCHAIN=""
+USE_DEFAULT_KEYCHAIN=false
+BINARY="../builddir/jacktrip"
+PSI=false
+
+OPTIND=1
+
+while getopts ":inhqklc:d:u:p:t:b:" opt; do
+    case $opt in
+      i)
+        BUILD_INSTALLER=true
+        ;;
+      n)
+        NOTARIZE=true
+        ;;
+      k)
+        TEMP_KEYCHAIN="$(pwd)/notarytool_temp.db"
+        ;;
+      l)
+        USE_DEFAULT_KEYCHAIN=true
+        ;;
+      c)
+        CERTIFICATE=$OPTARG
+        ;;
+      d)
+        PACKAGE_CERT=$OPTARG
+        ;;
+      u)
+        USERNAME=$OPTARG
+        ;;
+      p)
+        PASSWORD=$OPTARG
+        ;;
+      t)
+        TEAM_ID=$OPTARG
+        ;;
+      b)
+        BINARY=$OPTARG
+        ;;
+      q)
+        PSI=true
+        ;;
+      \?)
+        echo "Invalid option -$OPTARG ignored."
+        ;;
+      h)
+        echo "JackTrip App Bundle assembly script."
+        echo "Copyright (C) 2020-2021 Aaron Wyatt et al."
+        echo "Released under the GNU GPLv3 License."
+        echo
+        echo "Usage: ./assemble-app.sh [options] [appname] [bundlename]"
+        echo
+        echo "Options:"
+        echo " -b <filename>      The binary file to be placed in the app bundle. (Defaults to ../builddir/jacktrip)"
+        echo " -i                 Build an installer package as well. (Requires Packages to be installed.)"
+        echo " -n                 Send a notarization request to Apple. (Only takes effect if building an installer.)"
+        echo " -c <certname>      Name of the developer certificate to use for code signing. (No signing by default.)"
+        echo " -d <certname>      Name of the certificate to use for package signing. (No signing by default.)"
+        echo
+        echo "Important: If supplying one of the next three options, you must supply all of them."
+        echo " -u <username>      Apple ID username (email address) for installer notarization."
+        echo " -p <password>      App specific password for installer notarization."
+        echo " -t <teamid>        Team ID for notarization."
+        echo
+        echo " -k                 Use a temporary keychain to store notarization credentials. (Overrides -l.)"
+        echo " -l                 Use the default keychain instead of the login keychain to store credentials."
+        echo " -h                 Display this help screen and exit."
+        echo
+        echo "By default, appname is set to JackTrip and bundlename is org.jacktrip.jacktrip."
+        echo "(These should be left as is for official builds.)"
+        echo
+        echo "The username, password, and team ID are saved in the login keychain by notarytool."
+        echo "They only need to be supplied once, or in the event that you need to change them."
+        echo "(They need to be supplied every time if you opt to use a temporary keychain.)"
+        exit 0
+        ;;
+      :)
+        echo "Option $OPTARG requires an argument."
+        exit 1
+        ;;
+     esac
+done
+
+shift $((OPTIND - 1))
+[ "${1:-}" = "--" ] && shift
+
+[ "$#" -gt 0 ] && APPNAME="$1"
+[ "$#" -gt 1 ] && BUNDLE_ID="$2"
+
+DYNAMIC_QT=$(otool -L $BINARY | grep QtCore)
+DYNAMIC_VS=$(otool -L $BINARY | grep QtQml)
+
+if [[ -n "$DYNAMIC_QT" && -n "$QT_PATH" ]]; then
+  export DYLD_FRAMEWORK_PATH=$QT_PATH/lib
+fi
+
+VERSION="$($BINARY -v | awk '/VERSION/{print $NF}')"
+[ -z "$VERSION" ] && { echo "Unable to determine binary version. Quitting."; exit 1; }
+
+# Make sure that jacktrip has been built with GUI support.
+$BINARY --test-gui || { echo "You need to build jacktrip with GUI support to build an app bundle."; exit 1; }
+
+echo "Building bundle $APPNAME (id: $BUNDLE_ID)"
+echo "for binary version $VERSION"
+
+rm -rf "$APPNAME.app"
+[ ! -d "JackTrip.app_template/Contents/MacOS" ] && mkdir JackTrip.app_template/Contents/MacOS
+cp -a JackTrip.app_template "$APPNAME.app"
+cp -f $BINARY "$APPNAME.app/Contents/MacOS/"
+# copy licenses
+cp -f ../LICENSE.md "$APPNAME.app/Contents/Resources/"
+cp -Rf ../LICENSES "$APPNAME.app/Contents/Resources/"
+
+[ $PSI = true ] && cp jacktrip_alt.icns "$APPNAME.app/Contents/Resources/jacktrip.icns"
+
+if [ -n "$DYNAMIC_QT" ] && [ -z "$DYNAMIC_VS" ]; then
+    cp "Info_novs.plist" "$APPNAME.app/Contents/Info.plist" 
+fi
+
+sed -i '' "s/%VERSION%/$VERSION/" "$APPNAME.app/Contents/Info.plist"
+sed -i '' "s/%BUNDLENAME%/$APPNAME/" "$APPNAME.app/Contents/Info.plist"
+sed -i '' "s/%BUNDLEID%/$BUNDLE_ID/" "$APPNAME.app/Contents/Info.plist"
+
+if [ -n "$DYNAMIC_QT" ]; then
+    QT_VERSION="qt$(echo "$DYNAMIC_QT" | sed -E '1!d;s/.*compatibility version ([0-9]+)\.[0-9]+\.[0-9]+.*/\1/g')"
+    echo "Detected a dynamic Qt$QT_VERSION binary"
+    DEPLOY_CMD="$(which macdeployqt)"
+    if [ -z "$DEPLOY_CMD" ]; then
+        # Attempt to find macdeployqt. Try macports location first, then brew.
+        if [ -x "/opt/local/libexec/$QT_VERSION/bin/macdeployqt" ]; then
+            DEPLOY_CMD="/opt/local/libexec/$QT_VERSION/bin/macdeployqt"
+        elif [ -n $(which brew) ] && [ -n $(brew --prefix $QT_VERSION) ]; then
+            DEPLOY_CMD="$(brew --prefix $QT_VERSION)/bin/macdeployqt"
+        else
+            echo "Error: The Qt bin folder needs to be in your PATH for this script to work."
+            exit 1
+        fi
+    fi
+    DEPLOY_OPTS="-executable=$APPNAME.app/Contents/MacOS/jacktrip -libpath=$QT_PATH/lib"
+    if [ -n "$DYNAMIC_VS" ]; then
+        DEPLOY_OPTS="$DEPLOY_OPTS -qmldir=../src/gui"
+    fi
+    $DEPLOY_CMD "$APPNAME.app" $DEPLOY_OPTS
+
+    if [ -n "$CERTIFICATE" ]; then
+        # manually sign contents since the macdeployqt built-ins do not work (rpath errors)
+        echo "Signing app contents"
+        PATHS="$APPNAME.app/Contents/Frameworks $APPNAME.app/Contents/PlugIns $APPNAME.app/Contents/Resources"
+        find $PATHS -type f | while read fname; do
+            if [[ -f $fname ]]; then
+                codesign -f -s "$CERTIFICATE" --timestamp --entitlements entitlements.plist --options "runtime" "$fname"
+            fi
+        done
+    fi
+fi
+
+[ $BUILD_INSTALLER = true ] || exit 0
+
+# If you have Packages installed, you can build an installer for the newly created app bundle.
+[ -z $(which packagesbuild) ] && { echo "Error: You need to have Packages installed to build a package."; exit 1; }
+
+if [ $PSI = true ]; then
+    cp "package/postinstall.sh" "package/postinstall.sh.bak"
+    sed -i '' "s/^open/#open/" "package/postinstall.sh"
+fi
+
+# Needed for notarization.
+if [ -n "$CERTIFICATE" ]; then
+    echo "Signing $APPNAME.app"
+    codesign -f -s "$CERTIFICATE" --timestamp --entitlements entitlements.plist --options "runtime" "$APPNAME.app"
+fi
+
+# prepare license
+LICENSE_PATH="package/license.txt"
+cat ../LICENSE.md > "$LICENSE_PATH"
+printf "\n\n" >> "$LICENSE_PATH"
+cat ../LICENSES/MIT.txt >> "$LICENSE_PATH"
+printf "\n\n" >> "$LICENSE_PATH"
+cat ../LICENSES/GPL-3.0.txt >> "$LICENSE_PATH"
+printf "\n\n" >> "$LICENSE_PATH"
+cat ../LICENSES/LGPL-3.0-only.txt >> "$LICENSE_PATH"
+
+sed -i '' "s/# //" "$LICENSE_PATH" # remove markdown header
+perl -ane 'chop;print "\n\n" if(/^\s*$/); map{print "$_ ";}@F;' "$LICENSE_PATH" > tmp && mv tmp "$LICENSE_PATH" # unwrap lines
+
+# prepare readme
+README_PATH="package/readme.txt"
+cp ../README.md "$README_PATH"
+sed -i '' "s/# //" "$README_PATH" # remove markdown header
+perl -ane 'chop;print "\n\n" if(/^\s*$/); map{print "$_ ";}@F;' "$README_PATH" > tmp && mv tmp "$README_PATH" # unwrap lines
+
+cp package/JackTrip.pkgproj_template package/JackTrip.pkgproj
+sed -i '' "s/%VERSION%/$VERSION/" package/JackTrip.pkgproj
+sed -i '' "s/%BUNDLENAME%/$APPNAME/" package/JackTrip.pkgproj
+sed -i '' "s/%BUNDLEID%/$BUNDLE_ID/" package/JackTrip.pkgproj
+
+echo "Building JackTrip.pkg"
+packagesbuild package/JackTrip.pkgproj
+[ $PSI = true ] && mv "package/postinstall.sh.bak" "package/postinstall.sh"
+if pkgutil --check-signature package/build/JackTrip.pkg; then
+    echo "Package already signed."
+    SIGNED=true
+else
+    if [ -n "$PACKAGE_CERT" ]; then
+        echo "Signing package."
+        if productsign --sign "$PACKAGE_CERT" package/build/JackTrip.pkg package/build/JackTrip-signed.pkg; then
+            mv package/build/JackTrip-signed.pkg package/build/JackTrip.pkg
+            SIGNED=true
+        else
+            echo "Unable to sign package."
+            exit 1
+        fi
+    else
+        SIGNED=false
+    fi
+fi 
+
+[ $NOTARIZE = true ] || exit 0
+
+# Submit a notarization request to apple if we've chosen to and signed our package.
+if [ $SIGNED = false ]; then
+    echo "Not sending notarization request: package not signed."
+    exit 1
+fi
+
+if [ -n "$USERNAME" ] || [ -n "$PASSWORD" ] || [ -n "$TEAM_ID" ] || [ -n "$TEMP_KEYCHAIN" ]; then
+    if [ -z "$USERNAME" ] || [ -z "$PASSWORD" ] || [ -z "$TEAM_ID" ]; then
+        echo "Error: Missing credentials. Make sure you supply a username, password and team ID."
+        exit 1
+    fi
+fi 
+
+KEYCHAIN=""
+if [ -n "$TEMP_KEYCHAIN" ]; then
+    echo "Using a temporary keychain"
+    [ -e "$TEMP_KEYCHAIN" ] && rm "$TEMP_KEYCHAIN"
+    security create-keychain -p "supersecretpassword" "$TEMP_KEYCHAIN"
+    security set-keychain-settings -lut 3600 "$TEMP_KEYCHAIN"
+    security unlock-keychain -p "supersecretpassword" "$TEMP_KEYCHAIN"
+    KEYCHAIN=" --keychain \"$TEMP_KEYCHAIN\""
+elif [ $USE_DEFAULT_KEYCHAIN = true ]; then
+    echo "Using the default keychain"
+    DEFAULT_KEYCHAIN=$(security default-keychain | cut -d '"' -f2)
+    KEYCHAIN=" --keychain \"$DEFAULT_KEYCHAIN\""
+fi
+
+if [ -n "$USERNAME" ]; then
+    # We have new credentials. Store them in the keychain so we can use them.
+    ARGS="notarytool store-credentials \"$KEY_STORE\" --apple-id \"$USERNAME\" --password \"$PASSWORD\" --team-id \"$TEAM_ID\"$KEYCHAIN"
+    echo $ARGS | xargs xcrun
+fi
+
+echo "Sending notarization request"
+ARGS="notarytool submit \"package/build/$APPNAME.pkg\" --keychain-profile \"$KEY_STORE\" --wait$KEYCHAIN"
+echo $ARGS | xargs xcrun
+if [ $? -eq 0 ]; then
+    [ -n "$TEMP_KEYCHAIN" ] && security delete-keychain "$TEMP_KEYCHAIN"
+    xcrun stapler staple "package/build/$APPNAME.pkg"
+else
+    [ -n "$TEMP_KEYCHAIN" ] && security delete-keychain "$TEMP_KEYCHAIN"
+    echo "Error: Notarization failed"
+    exit 1
+fi
diff --git a/macos/entitlements.plist b/macos/entitlements.plist
new file mode 100644 (file)
index 0000000..11f9a7f
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>com.apple.security.cs.allow-dyld-environment-variables</key>
+       <true/>
+       <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
+       <true/>
+       <key>com.apple.security.cs.disable-library-validation</key>
+       <true/>
+       <key>com.apple.security.device.audio-input</key>
+       <true/>
+       <key>com.apple.security.device.microphone</key>
+       <true/>
+       <key>com.apple.security.device.camera</key>
+       <true/>
+       <key>com.apple.security.cs.allow-jit</key>
+       <true/>
+</dict>
+</plist>
diff --git a/macos/jacktrip.iconset/icon_128x128.png b/macos/jacktrip.iconset/icon_128x128.png
new file mode 100644 (file)
index 0000000..d80ff94
Binary files /dev/null and b/macos/jacktrip.iconset/icon_128x128.png differ
diff --git a/macos/jacktrip.iconset/icon_128x128@2x.png b/macos/jacktrip.iconset/icon_128x128@2x.png
new file mode 100644 (file)
index 0000000..63a9bd7
Binary files /dev/null and b/macos/jacktrip.iconset/icon_128x128@2x.png differ
diff --git a/macos/jacktrip.iconset/icon_16x16.png b/macos/jacktrip.iconset/icon_16x16.png
new file mode 100644 (file)
index 0000000..df9ffcd
Binary files /dev/null and b/macos/jacktrip.iconset/icon_16x16.png differ
diff --git a/macos/jacktrip.iconset/icon_16x16@2x.png b/macos/jacktrip.iconset/icon_16x16@2x.png
new file mode 100644 (file)
index 0000000..78ed69f
Binary files /dev/null and b/macos/jacktrip.iconset/icon_16x16@2x.png differ
diff --git a/macos/jacktrip.iconset/icon_256x256.png b/macos/jacktrip.iconset/icon_256x256.png
new file mode 100644 (file)
index 0000000..bc44f30
Binary files /dev/null and b/macos/jacktrip.iconset/icon_256x256.png differ
diff --git a/macos/jacktrip.iconset/icon_256x256@2x.png b/macos/jacktrip.iconset/icon_256x256@2x.png
new file mode 100644 (file)
index 0000000..d86ef17
Binary files /dev/null and b/macos/jacktrip.iconset/icon_256x256@2x.png differ
diff --git a/macos/jacktrip.iconset/icon_32x32.png b/macos/jacktrip.iconset/icon_32x32.png
new file mode 100644 (file)
index 0000000..9c8f5a7
Binary files /dev/null and b/macos/jacktrip.iconset/icon_32x32.png differ
diff --git a/macos/jacktrip.iconset/icon_32x32@2x.png b/macos/jacktrip.iconset/icon_32x32@2x.png
new file mode 100644 (file)
index 0000000..cfd969a
Binary files /dev/null and b/macos/jacktrip.iconset/icon_32x32@2x.png differ
diff --git a/macos/jacktrip.iconset/icon_512x512.png b/macos/jacktrip.iconset/icon_512x512.png
new file mode 100644 (file)
index 0000000..c0e615e
Binary files /dev/null and b/macos/jacktrip.iconset/icon_512x512.png differ
diff --git a/macos/jacktrip.iconset/icon_512x512@2x.png b/macos/jacktrip.iconset/icon_512x512@2x.png
new file mode 100644 (file)
index 0000000..dc87eb2
Binary files /dev/null and b/macos/jacktrip.iconset/icon_512x512@2x.png differ
diff --git a/macos/jacktrip_alt.icns b/macos/jacktrip_alt.icns
new file mode 100644 (file)
index 0000000..4f205c1
Binary files /dev/null and b/macos/jacktrip_alt.icns differ
diff --git a/macos/jacktrip_alt.iconset/icon_128x128.png b/macos/jacktrip_alt.iconset/icon_128x128.png
new file mode 100644 (file)
index 0000000..99d8c77
Binary files /dev/null and b/macos/jacktrip_alt.iconset/icon_128x128.png differ
diff --git a/macos/jacktrip_alt.iconset/icon_128x128@2x.png b/macos/jacktrip_alt.iconset/icon_128x128@2x.png
new file mode 100644 (file)
index 0000000..0b2f82c
Binary files /dev/null and b/macos/jacktrip_alt.iconset/icon_128x128@2x.png differ
diff --git a/macos/jacktrip_alt.iconset/icon_16x16.png b/macos/jacktrip_alt.iconset/icon_16x16.png
new file mode 100644 (file)
index 0000000..76b0f2f
Binary files /dev/null and b/macos/jacktrip_alt.iconset/icon_16x16.png differ
diff --git a/macos/jacktrip_alt.iconset/icon_16x16@2x.png b/macos/jacktrip_alt.iconset/icon_16x16@2x.png
new file mode 100644 (file)
index 0000000..d6a2d90
Binary files /dev/null and b/macos/jacktrip_alt.iconset/icon_16x16@2x.png differ
diff --git a/macos/jacktrip_alt.iconset/icon_256x256.png b/macos/jacktrip_alt.iconset/icon_256x256.png
new file mode 100644 (file)
index 0000000..0b2f82c
Binary files /dev/null and b/macos/jacktrip_alt.iconset/icon_256x256.png differ
diff --git a/macos/jacktrip_alt.iconset/icon_256x256@2x.png b/macos/jacktrip_alt.iconset/icon_256x256@2x.png
new file mode 100644 (file)
index 0000000..5befa4a
Binary files /dev/null and b/macos/jacktrip_alt.iconset/icon_256x256@2x.png differ
diff --git a/macos/jacktrip_alt.iconset/icon_32x32.png b/macos/jacktrip_alt.iconset/icon_32x32.png
new file mode 100644 (file)
index 0000000..d6a2d90
Binary files /dev/null and b/macos/jacktrip_alt.iconset/icon_32x32.png differ
diff --git a/macos/jacktrip_alt.iconset/icon_32x32@2x.png b/macos/jacktrip_alt.iconset/icon_32x32@2x.png
new file mode 100644 (file)
index 0000000..233148c
Binary files /dev/null and b/macos/jacktrip_alt.iconset/icon_32x32@2x.png differ
diff --git a/macos/jacktrip_alt.iconset/icon_512x512.png b/macos/jacktrip_alt.iconset/icon_512x512.png
new file mode 100644 (file)
index 0000000..5befa4a
Binary files /dev/null and b/macos/jacktrip_alt.iconset/icon_512x512.png differ
diff --git a/macos/jacktrip_alt.iconset/icon_512x512@2x.png b/macos/jacktrip_alt.iconset/icon_512x512@2x.png
new file mode 100644 (file)
index 0000000..cf7614e
Binary files /dev/null and b/macos/jacktrip_alt.iconset/icon_512x512@2x.png differ
diff --git a/macos/meson_native_minversion_10.13.ini b/macos/meson_native_minversion_10.13.ini
new file mode 100644 (file)
index 0000000..b1ff86a
--- /dev/null
@@ -0,0 +1,10 @@
+[constants]
+minversion_args = ['-mmacosx-version-min=10.13']
+
+[built-in options]
+c_args = minversion_args
+cpp_args = minversion_args
+objcpp_args = minversion_args
+c_link_args = minversion_args
+cpp_link_args = minversion_args
+objcpp_link_args = minversion_args
diff --git a/macos/meson_native_minversion_10.14.ini b/macos/meson_native_minversion_10.14.ini
new file mode 100644 (file)
index 0000000..aafcaff
--- /dev/null
@@ -0,0 +1,10 @@
+[constants]
+minversion_args = ['-mmacosx-version-min=10.14']
+
+[built-in options]
+c_args = minversion_args
+cpp_args = minversion_args
+objcpp_args = minversion_args
+c_link_args = minversion_args
+cpp_link_args = minversion_args
+objcpp_link_args = minversion_args
diff --git a/macos/meson_native_universal.ini b/macos/meson_native_universal.ini
new file mode 100644 (file)
index 0000000..2900e43
--- /dev/null
@@ -0,0 +1,10 @@
+[constants]
+universal_args = ['-arch', 'x86_64', '-arch', 'arm64']
+
+[properties]
+c_args = universal_args
+cpp_args = universal_args
+objcpp_args = universal_args
+c_link_args = universal_args
+cpp_link_args = universal_args
+objcpp_link_args = universal_args
diff --git a/macos/meson_native_universal_10.13.ini b/macos/meson_native_universal_10.13.ini
new file mode 100644 (file)
index 0000000..c41517d
--- /dev/null
@@ -0,0 +1,11 @@
+[constants]
+minversion_args = ['-mmacosx-version-min=10.13']
+universal_args = ['-arch', 'x86_64', '-arch', 'arm64']
+
+[built-in options]
+c_args = universal_args + minversion_args
+cpp_args = universal_args + minversion_args
+objcpp_args = universal_args + minversion_args
+c_link_args = universal_args + minversion_args
+cpp_link_args = universal_args + minversion_args
+objcpp_link_args = universal_args + minversion_args
diff --git a/macos/meson_native_universal_10.14.ini b/macos/meson_native_universal_10.14.ini
new file mode 100644 (file)
index 0000000..88926cd
--- /dev/null
@@ -0,0 +1,11 @@
+[constants]
+minversion_args = ['-mmacosx-version-min=10.14']
+universal_args = ['-arch', 'x86_64', '-arch', 'arm64']
+
+[built-in options]
+c_args = universal_args + minversion_args
+cpp_args = universal_args + minversion_args
+objcpp_args = universal_args + minversion_args
+c_link_args = universal_args + minversion_args
+cpp_link_args = universal_args + minversion_args
+objcpp_link_args = universal_args + minversion_args
diff --git a/macos/package/JackTrip.pkgproj_template b/macos/package/JackTrip.pkgproj_template
new file mode 100644 (file)
index 0000000..18a6e56
--- /dev/null
@@ -0,0 +1,1182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+       <key>PACKAGES</key>
+       <array>
+               <dict>
+                       <key>MUST-CLOSE-APPLICATION-ITEMS</key>
+                       <array/>
+                       <key>MUST-CLOSE-APPLICATIONS</key>
+                       <false/>
+                       <key>PACKAGE_FILES</key>
+                       <dict>
+                               <key>DEFAULT_INSTALL_LOCATION</key>
+                               <string>/</string>
+                               <key>HIERARCHY</key>
+                               <dict>
+                                       <key>CHILDREN</key>
+                                       <array>
+                                               <dict>
+                                                       <key>CHILDREN</key>
+                                                       <array>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>80</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Jack</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>2</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>509</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>2</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>BUNDLE_CAN_DOWNGRADE</key>
+                                                                       <false/>
+                                                                       <key>BUNDLE_POSTINSTALL_PATH</key>
+                                                                       <dict>
+                                                                               <key>PATH</key>
+                                                                               <string>postinstall.sh</string>
+                                                                               <key>PATH_TYPE</key>
+                                                                               <integer>1</integer>
+                                                                       </dict>
+                                                                       <key>BUNDLE_PREINSTALL_PATH</key>
+                                                                       <dict>
+                                                                               <key>PATH_TYPE</key>
+                                                                               <integer>0</integer>
+                                                                       </dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>80</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>../%BUNDLENAME%.app</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>3</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>80</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Utilities</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>-1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                       </array>
+                                                       <key>GID</key>
+                                                       <integer>80</integer>
+                                                       <key>PATH</key>
+                                                       <string>Applications</string>
+                                                       <key>PATH_TYPE</key>
+                                                       <integer>0</integer>
+                                                       <key>PERMISSIONS</key>
+                                                       <integer>509</integer>
+                                                       <key>TYPE</key>
+                                                       <integer>1</integer>
+                                                       <key>UID</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>CHILDREN</key>
+                                                       <array/>
+                                                       <key>GID</key>
+                                                       <integer>0</integer>
+                                                       <key>PATH</key>
+                                                       <string>bin</string>
+                                                       <key>PATH_TYPE</key>
+                                                       <integer>0</integer>
+                                                       <key>PERMISSIONS</key>
+                                                       <integer>493</integer>
+                                                       <key>TYPE</key>
+                                                       <integer>-1</integer>
+                                                       <key>UID</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>CHILDREN</key>
+                                                       <array>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>80</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Application Support</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Audio</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Automator</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>ColorPickers</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Documentation</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Extensions</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Filesystems</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>80</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Fonts</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>1021</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Frameworks</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Input Methods</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Internet Plug-Ins</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>LaunchAgents</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>LaunchDaemons</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>PreferencePanes</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Preferences</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>80</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Printers</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>PrivilegedHelperTools</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>1005</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>QuickLook</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>QuickTime</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Screen Savers</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Scripts</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Services</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Widgets</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                       </array>
+                                                       <key>GID</key>
+                                                       <integer>0</integer>
+                                                       <key>PATH</key>
+                                                       <string>Library</string>
+                                                       <key>PATH_TYPE</key>
+                                                       <integer>0</integer>
+                                                       <key>PERMISSIONS</key>
+                                                       <integer>493</integer>
+                                                       <key>TYPE</key>
+                                                       <integer>1</integer>
+                                                       <key>UID</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>CHILDREN</key>
+                                                       <array>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>etc</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>-1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>var</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>-1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                       </array>
+                                                       <key>GID</key>
+                                                       <integer>0</integer>
+                                                       <key>PATH</key>
+                                                       <string>private</string>
+                                                       <key>PATH_TYPE</key>
+                                                       <integer>0</integer>
+                                                       <key>PERMISSIONS</key>
+                                                       <integer>493</integer>
+                                                       <key>TYPE</key>
+                                                       <integer>-1</integer>
+                                                       <key>UID</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>CHILDREN</key>
+                                                       <array/>
+                                                       <key>GID</key>
+                                                       <integer>0</integer>
+                                                       <key>PATH</key>
+                                                       <string>sbin</string>
+                                                       <key>PATH_TYPE</key>
+                                                       <integer>0</integer>
+                                                       <key>PERMISSIONS</key>
+                                                       <integer>493</integer>
+                                                       <key>TYPE</key>
+                                                       <integer>-1</integer>
+                                                       <key>UID</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>CHILDREN</key>
+                                                       <array>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array>
+                                                                               <dict>
+                                                                                       <key>CHILDREN</key>
+                                                                                       <array/>
+                                                                                       <key>GID</key>
+                                                                                       <integer>0</integer>
+                                                                                       <key>PATH</key>
+                                                                                       <string>Extensions</string>
+                                                                                       <key>PATH_TYPE</key>
+                                                                                       <integer>0</integer>
+                                                                                       <key>PERMISSIONS</key>
+                                                                                       <integer>493</integer>
+                                                                                       <key>TYPE</key>
+                                                                                       <integer>1</integer>
+                                                                                       <key>UID</key>
+                                                                                       <integer>0</integer>
+                                                                               </dict>
+                                                                       </array>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Library</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                       </array>
+                                                       <key>GID</key>
+                                                       <integer>0</integer>
+                                                       <key>PATH</key>
+                                                       <string>System</string>
+                                                       <key>PATH_TYPE</key>
+                                                       <integer>0</integer>
+                                                       <key>PERMISSIONS</key>
+                                                       <integer>493</integer>
+                                                       <key>TYPE</key>
+                                                       <integer>1</integer>
+                                                       <key>UID</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>CHILDREN</key>
+                                                       <array>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Shared</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>1023</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                       </array>
+                                                       <key>GID</key>
+                                                       <integer>80</integer>
+                                                       <key>PATH</key>
+                                                       <string>Users</string>
+                                                       <key>PATH_TYPE</key>
+                                                       <integer>0</integer>
+                                                       <key>PERMISSIONS</key>
+                                                       <integer>493</integer>
+                                                       <key>TYPE</key>
+                                                       <integer>1</integer>
+                                                       <key>UID</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>CHILDREN</key>
+                                                       <array>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>bin</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>-1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>include</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>-1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>lib</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>-1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array>
+                                                                               <dict>
+                                                                                       <key>CHILDREN</key>
+                                                                                       <array/>
+                                                                                       <key>GID</key>
+                                                                                       <integer>0</integer>
+                                                                                       <key>PATH</key>
+                                                                                       <string>bin</string>
+                                                                                       <key>PATH_TYPE</key>
+                                                                                       <integer>0</integer>
+                                                                                       <key>PERMISSIONS</key>
+                                                                                       <integer>493</integer>
+                                                                                       <key>TYPE</key>
+                                                                                       <integer>-1</integer>
+                                                                                       <key>UID</key>
+                                                                                       <integer>0</integer>
+                                                                               </dict>
+                                                                       </array>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>local</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>-1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>sbin</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>-1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                               <dict>
+                                                                       <key>CHILDREN</key>
+                                                                       <array/>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>share</string>
+                                                                       <key>PATH_TYPE</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PERMISSIONS</key>
+                                                                       <integer>493</integer>
+                                                                       <key>TYPE</key>
+                                                                       <integer>-1</integer>
+                                                                       <key>UID</key>
+                                                                       <integer>0</integer>
+                                                               </dict>
+                                                       </array>
+                                                       <key>GID</key>
+                                                       <integer>0</integer>
+                                                       <key>PATH</key>
+                                                       <string>usr</string>
+                                                       <key>PATH_TYPE</key>
+                                                       <integer>0</integer>
+                                                       <key>PERMISSIONS</key>
+                                                       <integer>493</integer>
+                                                       <key>TYPE</key>
+                                                       <integer>-1</integer>
+                                                       <key>UID</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                       </array>
+                                       <key>GID</key>
+                                       <integer>0</integer>
+                                       <key>PATH</key>
+                                       <string>/</string>
+                                       <key>PATH_TYPE</key>
+                                       <integer>0</integer>
+                                       <key>PERMISSIONS</key>
+                                       <integer>493</integer>
+                                       <key>TYPE</key>
+                                       <integer>1</integer>
+                                       <key>UID</key>
+                                       <integer>0</integer>
+                               </dict>
+                               <key>PAYLOAD_TYPE</key>
+                               <integer>0</integer>
+                               <key>PRESERVE_EXTENDED_ATTRIBUTES</key>
+                               <false/>
+                               <key>SHOW_INVISIBLE</key>
+                               <true/>
+                               <key>SPLIT_FORKS</key>
+                               <true/>
+                               <key>TREAT_MISSING_FILES_AS_WARNING</key>
+                               <false/>
+                               <key>VERSION</key>
+                               <integer>5</integer>
+                       </dict>
+                       <key>PACKAGE_SCRIPTS</key>
+                       <dict>
+                               <key>POSTINSTALL_PATH</key>
+                               <dict>
+                                       <key>PATH_TYPE</key>
+                                       <integer>0</integer>
+                               </dict>
+                               <key>PREINSTALL_PATH</key>
+                               <dict>
+                                       <key>PATH_TYPE</key>
+                                       <integer>0</integer>
+                               </dict>
+                               <key>RESOURCES</key>
+                               <array/>
+                       </dict>
+                       <key>PACKAGE_SETTINGS</key>
+                       <dict>
+                               <key>AUTHENTICATION</key>
+                               <integer>1</integer>
+                               <key>CONCLUSION_ACTION</key>
+                               <integer>0</integer>
+                               <key>FOLLOW_SYMBOLIC_LINKS</key>
+                               <false/>
+                               <key>IDENTIFIER</key>
+                               <string>%BUNDLEID%</string>
+                               <key>LOCATION</key>
+                               <integer>0</integer>
+                               <key>NAME</key>
+                               <string>%BUNDLENAME%</string>
+                               <key>OVERWRITE_PERMISSIONS</key>
+                               <true/>
+                               <key>PAYLOAD_SIZE</key>
+                               <integer>-1</integer>
+                               <key>REFERENCE_PATH</key>
+                               <string></string>
+                               <key>RELOCATABLE</key>
+                               <false/>
+                               <key>USE_HFS+_COMPRESSION</key>
+                               <false/>
+                               <key>VERSION</key>
+                               <string>%VERSION%</string>
+                       </dict>
+                       <key>TYPE</key>
+                       <integer>0</integer>
+                       <key>UUID</key>
+                       <string>10E1CE8D-C84E-45FC-81DA-B174548AE779</string>
+               </dict>
+       </array>
+       <key>PROJECT</key>
+       <dict>
+               <key>PROJECT_COMMENTS</key>
+               <dict>
+                       <key>NOTES</key>
+                       <data>
+                       </data>
+               </dict>
+               <key>PROJECT_PRESENTATION</key>
+               <dict>
+                       <key>BACKGROUND</key>
+                       <dict>
+                               <key>APPAREANCES</key>
+                               <dict>
+                                       <key>DARK_AQUA</key>
+                                       <dict/>
+                                       <key>LIGHT_AQUA</key>
+                                       <dict/>
+                               </dict>
+                               <key>SHARED_SETTINGS_FOR_ALL_APPAREANCES</key>
+                               <true/>
+                       </dict>
+                       <key>INSTALLATION_STEPS</key>
+                       <array>
+                               <dict>
+                                       <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+                                       <string>ICPresentationViewIntroductionController</string>
+                                       <key>INSTALLER_PLUGIN</key>
+                                       <string>Introduction</string>
+                                       <key>LIST_TITLE_KEY</key>
+                                       <string>InstallerSectionTitle</string>
+                               </dict>
+                               <dict>
+                                       <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+                                       <string>ICPresentationViewReadMeController</string>
+                                       <key>INSTALLER_PLUGIN</key>
+                                       <string>ReadMe</string>
+                                       <key>LIST_TITLE_KEY</key>
+                                       <string>InstallerSectionTitle</string>
+                               </dict>
+                               <dict>
+                                       <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+                                       <string>ICPresentationViewLicenseController</string>
+                                       <key>INSTALLER_PLUGIN</key>
+                                       <string>License</string>
+                                       <key>LIST_TITLE_KEY</key>
+                                       <string>InstallerSectionTitle</string>
+                               </dict>
+                               <dict>
+                                       <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+                                       <string>ICPresentationViewDestinationSelectController</string>
+                                       <key>INSTALLER_PLUGIN</key>
+                                       <string>TargetSelect</string>
+                                       <key>LIST_TITLE_KEY</key>
+                                       <string>InstallerSectionTitle</string>
+                               </dict>
+                               <dict>
+                                       <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+                                       <string>ICPresentationViewInstallationTypeController</string>
+                                       <key>INSTALLER_PLUGIN</key>
+                                       <string>PackageSelection</string>
+                                       <key>LIST_TITLE_KEY</key>
+                                       <string>InstallerSectionTitle</string>
+                               </dict>
+                               <dict>
+                                       <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+                                       <string>ICPresentationViewInstallationController</string>
+                                       <key>INSTALLER_PLUGIN</key>
+                                       <string>Install</string>
+                                       <key>LIST_TITLE_KEY</key>
+                                       <string>InstallerSectionTitle</string>
+                               </dict>
+                               <dict>
+                                       <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+                                       <string>ICPresentationViewSummaryController</string>
+                                       <key>INSTALLER_PLUGIN</key>
+                                       <string>Summary</string>
+                                       <key>LIST_TITLE_KEY</key>
+                                       <string>InstallerSectionTitle</string>
+                               </dict>
+                       </array>
+                       <key>INTRODUCTION</key>
+                       <dict>
+                               <key>LOCALIZATIONS</key>
+                               <array/>
+                       </dict>
+                       <key>LICENSE</key>
+                       <dict>
+                               <key>LOCALIZATIONS</key>
+                               <array>
+                                       <dict>
+                                               <key>LANGUAGE</key>
+                                               <string>English</string>
+                                               <key>VALUE</key>
+                                               <dict>
+                                                       <key>PATH</key>
+                                                       <string>license.txt</string>
+                                                       <key>PATH_TYPE</key>
+                                                       <integer>3</integer>
+                                               </dict>
+                                       </dict>
+                               </array>
+                               <key>MODE</key>
+                               <integer>0</integer>
+                       </dict>
+                       <key>README</key>
+                       <dict>
+                               <key>LOCALIZATIONS</key>
+                               <array>
+                                       <dict>
+                                               <key>LANGUAGE</key>
+                                               <string>English</string>
+                                               <key>VALUE</key>
+                                               <dict>
+                                                       <key>PATH</key>
+                                                       <string>readme.txt</string>
+                                                       <key>PATH_TYPE</key>
+                                                       <integer>3</integer>
+                                               </dict>
+                                       </dict>
+                               </array>
+                       </dict>
+                       <key>TITLE</key>
+                       <dict>
+                               <key>LOCALIZATIONS</key>
+                               <array/>
+                       </dict>
+               </dict>
+               <key>PROJECT_REQUIREMENTS</key>
+               <dict>
+                       <key>LIST</key>
+                       <array/>
+                       <key>RESOURCES</key>
+                       <array/>
+                       <key>ROOT_VOLUME_ONLY</key>
+                       <false/>
+               </dict>
+               <key>PROJECT_SETTINGS</key>
+               <dict>
+                       <key>BUILD_FORMAT</key>
+                       <integer>0</integer>
+                       <key>BUILD_PATH</key>
+                       <dict>
+                               <key>PATH</key>
+                               <string>build</string>
+                               <key>PATH_TYPE</key>
+                               <integer>1</integer>
+                       </dict>
+                       <key>EXCLUDED_FILES</key>
+                       <array>
+                               <dict>
+                                       <key>PATTERNS_ARRAY</key>
+                                       <array>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>.DS_Store</string>
+                                                       <key>TYPE</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                       </array>
+                                       <key>PROTECTED</key>
+                                       <true/>
+                                       <key>PROXY_NAME</key>
+                                       <string>Remove .DS_Store files</string>
+                                       <key>PROXY_TOOLTIP</key>
+                                       <string>Remove ".DS_Store" files created by the Finder.</string>
+                                       <key>STATE</key>
+                                       <true/>
+                               </dict>
+                               <dict>
+                                       <key>PATTERNS_ARRAY</key>
+                                       <array>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>.pbdevelopment</string>
+                                                       <key>TYPE</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                       </array>
+                                       <key>PROTECTED</key>
+                                       <true/>
+                                       <key>PROXY_NAME</key>
+                                       <string>Remove .pbdevelopment files</string>
+                                       <key>PROXY_TOOLTIP</key>
+                                       <string>Remove ".pbdevelopment" files created by ProjectBuilder or Xcode.</string>
+                                       <key>STATE</key>
+                                       <true/>
+                               </dict>
+                               <dict>
+                                       <key>PATTERNS_ARRAY</key>
+                                       <array>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>CVS</string>
+                                                       <key>TYPE</key>
+                                                       <integer>1</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>.cvsignore</string>
+                                                       <key>TYPE</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>.cvspass</string>
+                                                       <key>TYPE</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>.svn</string>
+                                                       <key>TYPE</key>
+                                                       <integer>1</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>.git</string>
+                                                       <key>TYPE</key>
+                                                       <integer>1</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>.gitignore</string>
+                                                       <key>TYPE</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                       </array>
+                                       <key>PROTECTED</key>
+                                       <true/>
+                                       <key>PROXY_NAME</key>
+                                       <string>Remove SCM metadata</string>
+                                       <key>PROXY_TOOLTIP</key>
+                                       <string>Remove helper files and folders used by the CVS, SVN or Git Source Code Management systems.</string>
+                                       <key>STATE</key>
+                                       <true/>
+                               </dict>
+                               <dict>
+                                       <key>PATTERNS_ARRAY</key>
+                                       <array>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>classes.nib</string>
+                                                       <key>TYPE</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>designable.db</string>
+                                                       <key>TYPE</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>info.nib</string>
+                                                       <key>TYPE</key>
+                                                       <integer>0</integer>
+                                               </dict>
+                                       </array>
+                                       <key>PROTECTED</key>
+                                       <true/>
+                                       <key>PROXY_NAME</key>
+                                       <string>Optimize nib files</string>
+                                       <key>PROXY_TOOLTIP</key>
+                                       <string>Remove "classes.nib", "info.nib" and "designable.nib" files within .nib bundles.</string>
+                                       <key>STATE</key>
+                                       <true/>
+                               </dict>
+                               <dict>
+                                       <key>PATTERNS_ARRAY</key>
+                                       <array>
+                                               <dict>
+                                                       <key>REGULAR_EXPRESSION</key>
+                                                       <false/>
+                                                       <key>STRING</key>
+                                                       <string>Resources Disabled</string>
+                                                       <key>TYPE</key>
+                                                       <integer>1</integer>
+                                               </dict>
+                                       </array>
+                                       <key>PROTECTED</key>
+                                       <true/>
+                                       <key>PROXY_NAME</key>
+                                       <string>Remove Resources Disabled folders</string>
+                                       <key>PROXY_TOOLTIP</key>
+                                       <string>Remove "Resources Disabled" folders.</string>
+                                       <key>STATE</key>
+                                       <true/>
+                               </dict>
+                               <dict>
+                                       <key>SEPARATOR</key>
+                                       <true/>
+                               </dict>
+                       </array>
+                       <key>NAME</key>
+                       <string>%BUNDLENAME%</string>
+                       <key>PAYLOAD_ONLY</key>
+                       <false/>
+                       <key>TREAT_MISSING_PRESENTATION_DOCUMENTS_AS_WARNING</key>
+                       <false/>
+               </dict>
+       </dict>
+       <key>TYPE</key>
+       <integer>0</integer>
+       <key>VERSION</key>
+       <integer>2</integer>
+</dict>
+</plist>
diff --git a/macos/package/postinstall.sh b/macos/package/postinstall.sh
new file mode 100755 (executable)
index 0000000..a668d3c
--- /dev/null
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+# Link jacktrip to app binary
+mkdir -p /usr/local/bin
+rm -f /usr/local/bin/jacktrip
+ln -s "$2"/Contents/MacOS/jacktrip /usr/local/bin/jacktrip
+
+# Open JackTrip on intaller finish
+open -a /Applications/JackTrip.app
+exit 0
diff --git a/macos/sign-stuff.sh b/macos/sign-stuff.sh
new file mode 100755 (executable)
index 0000000..ed020aa
--- /dev/null
@@ -0,0 +1,62 @@
+#!/bin/sh
+
+# Set these for signing
+CERTIFICATE=""
+PACKAGE_CERT=""
+USERNAME=""
+PASSWORD=""
+TEAM_ID=""
+
+if [ -z $1 ]; then
+       echo "You need to provide a version number as an argument"
+       exit 1
+fi
+VERSION="v$1"
+
+if [ ! -z $2 ]; then
+       SCRIPT_BRANCH="$2"
+fi
+
+CHECK=$(echo "$1" | sed "s/-/./g")
+MAJOR=$(echo $CHECK | cut -d. -f1)
+MINOR=$(echo $CHECK | cut -d. -f2)
+REVISION=$(echo $CHECK | cut -d. -f3)
+
+if [ $MAJOR -le 1 ] && [ $MINOR -le 6 ] && [ $REVISION -lt 5 ] && [ -z "$SCRIPT_BRANCH" ]; then
+       echo "\033[1mVersion earlier than 1.6.5 detected\033[0m"
+       echo "\033[1mUsing assemble_app.sh script from dev branch\033[1m"
+       echo
+       SCRIPT_BRANCH="dev"
+fi
+
+if [ -d "$VERSION" ]; then
+       rm -rf $VERSION
+fi
+echo "\033[1mDownloading $VERSION from Github\033[0m"
+git clone --branch $VERSION https://github.com/jacktrip/jacktrip.git $VERSION || { echo "\n\033[1mCould not find tagged release for $VERSION\033[0m"; exit 1; }
+if [ ! -z "$SCRIPT_BRANCH" ]; then
+       if ! curl "https://raw.githubusercontent.com/jacktrip/jacktrip/$SCRIPT_BRANCH/macos/assemble_app.sh" -f -o "$VERSION/macos/assemble_app.sh"; then
+               echo "\033[1mUnable to download assemble_app.sh from $SCRIPT_BRANCH\033[0m"
+               echo "(Does this branch exist?)"
+               exit 1
+       fi
+fi
+echo "\n\033[1mDownloading compiled binary\033[0m"
+if ! curl "https://github.com/jacktrip/jacktrip/releases/download/$VERSION/JackTrip-$VERSION-macOS-x64-application.zip" -f -L -o binary.zip; then
+       echo "\033[1mUnable to download binary\033[0m"
+       exit 1
+fi
+echo "\n\033[1mExtracting binary\033[0m"
+mkdir -p "$VERSION/builddir"
+unzip -j binary.zip "JackTrip.app/Contents/MacOS/jacktrip" -d "$VERSION/builddir"
+rm binary.zip
+echo "\n\033[1mBuilding installer\033[0m"
+cd "$VERSION/macos"
+if ./assemble_app.sh -in -c "$CERTIFICATE" -d "$PACKAGE_CERT" -u "$USERNAME" -p "$PASSWORD" -t "$TEAM_ID"; then
+       echo "\n\033[1mCopying signed package to current directory and performing clean up\033[0m"
+       cp "package/build/JackTrip.pkg" ../../
+else
+       echo "\n\033[1mBuilding installer failed. Performing clean up.\033[0m"
+fi
+cd "../.."
+rm -rf $VERSION
diff --git a/meson.build b/meson.build
new file mode 100644 (file)
index 0000000..74ea661
--- /dev/null
@@ -0,0 +1,367 @@
+project('jacktrip', ['cpp','c'],
+               default_options: ['cpp_std=c++17','warning_level=2','optimization=2'])
+
+if get_option('profile') == 'development'
+       application_id = 'org.jacktrip.JackTrip.Devel'
+       name_suffix = ' (Development Snapshot)'
+else
+       application_id = 'org.jacktrip.JackTrip'
+       name_suffix = ''
+endif
+
+qt_version = get_option('qtversion')
+if qt_version == ''
+       qt_dep = dependency('qt6', modules: ['Core'], required: false)
+       if qt_dep.found()
+               qt_version = '6'
+       else
+               qt_version = '5'
+       endif
+endif
+if qt_version == '5' and get_option('nogui') == false and get_option('novs') == false
+       error('JackTrip Virtual Studio requires Qt 6.2 or later.')
+endif
+qt = import('qt' + qt_version)
+
+cmake = import('cmake')
+
+compiler = meson.get_compiler('cpp')
+
+defines = ['-DWAIRTOHUB']
+c_defines = []
+incdirs = []
+
+if get_option('debug') == false
+       defines += ['-DNDEBUG', '-DQT_NO_DEBUG']
+       c_defines += ['-DNDEBUG', '-DQT_NO_DEBUG']
+endif
+
+build_info = get_option('buildinfo')
+git = find_program('git', required: false)
+if build_info == '' and git.found()
+       git_tags_cmd = run_command(git, 'describe', '--tags', check: false)
+       git_hash_cmd = run_command(git, 'rev-parse', '--short', 'HEAD', check: false)
+       if git_tags_cmd.returncode() == 0 and git_hash_cmd.returncode() == 0
+               git_tags = git_tags_cmd.stdout().strip()
+               git_hash = git_hash_cmd.stdout().strip()
+               build_info = git_tags + '-' + git_hash
+       endif
+endif
+if build_info != ''
+       message('Build info: ' + build_info)
+       defines += ['-DJACKTRIP_BUILD_INFO=' + build_info]
+endif
+
+src = [        'src/DataProtocol.cpp',
+       'src/JackTrip.cpp',
+       'src/ProcessPlugin.cpp',
+       'src/AudioTester.cpp',
+       'src/jacktrip_globals.cpp',
+       'src/JackTripWorker.cpp',
+       'src/LoopBack.cpp',
+       'src/PacketHeader.cpp',
+       'src/RingBuffer.cpp',
+       'src/JitterBuffer.cpp',
+       'src/Regulator.cpp',
+       'src/Settings.cpp',
+       'src/UdpDataProtocol.cpp',
+       'src/UdpHubListener.cpp',
+       'src/AudioInterface.cpp',
+       'src/Compressor.cpp',
+       'src/Limiter.cpp',
+       'src/Meter.cpp',
+       'src/Monitor.cpp',
+       'src/Volume.cpp',
+       'src/Tone.cpp',
+       'src/StereoToMono.cpp',
+       'src/Reverb.cpp',
+       'src/main.cpp',
+       'src/SslServer.cpp',
+       'src/Auth.cpp']
+
+moc_h = ['src/DataProtocol.h',
+       'src/JackTrip.h',
+       'src/ProcessPlugin.h',
+       'src/Meter.h',
+       'src/Monitor.h',
+       'src/StereoToMono.h',
+       'src/Volume.h',
+       'src/Tone.h',
+       'src/JackTripWorker.h',
+       'src/PacketHeader.h',
+       'src/Regulator.h',
+       'src/Settings.h',
+       'src/UdpDataProtocol.h',
+       'src/UdpHubListener.h',
+       'src/Auth.h',
+       'src/SslServer.h']
+
+ui_h = []
+qres = []
+deps = [dependency('threads')]
+link_args = []
+
+subdir('win')
+subdir('linux')
+
+jack_dep = dependency('jack', required: get_option('jack'))
+if not jack_dep.found()
+       if get_option('weakjack') == true
+               error('unable to find jack and weakjack requested')
+       endif
+       defines += '-DNO_JACK'
+       c_defines += '-DNO_JACK'
+else
+       src +=  ['src/JackAudioInterface.cpp',
+               'src/JMess.cpp',
+               'src/Patcher.cpp']
+       moc_h += ['src/Patcher.h']
+       if get_option('weakjack') == true
+               incdirs += include_directories('externals/weakjack')
+               src += 'externals/weakjack/weak_libjack.c'
+               defines += '-DUSE_WEAK_JACK'
+               c_defines += '-DUSE_WEAK_JACK'
+               deps += jack_dep.partial_dependency(compile_args: true, includes: true)
+               deps += compiler.find_library('dl', required : false)
+       else
+               deps += jack_dep
+       endif
+endif
+
+if get_option('nogui') == true
+       defines += '-DNO_GUI'
+       if qt_version == '5'
+               deps += dependency('qt5', modules: ['Core', 'Network'], include_type: 'system')
+       else
+               deps += dependency('qt6', modules: ['Core', 'Network'], include_type: 'system')
+       endif
+else
+       src += [
+               'src/gui/qjacktrip.cpp',
+               'src/gui/about.cpp',
+               'src/gui/messageDialog.cpp',
+               'src/gui/textbuf.cpp',
+               'src/gui/vuMeter.cpp'
+       ]
+       moc_h += [
+               'src/gui/about.h',
+               'src/gui/qjacktrip.h',
+               'src/gui/messageDialog.h',
+               'src/gui/textbuf.h',
+               'src/gui/vuMeter.h'
+       ]
+       ui_h += [
+               'src/gui/qjacktrip.ui',
+               'src/gui/messageDialog.ui',
+               'src/gui/about.ui'
+       ]
+
+       if get_option('novs') == true
+               defines += '-DNO_VS'
+               if qt_version == '5'
+                       deps += dependency('qt5', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system')
+               else
+                       deps += dependency('qt6', modules: ['Core', 'Gui', 'Network', 'Widgets'], include_type: 'system')
+               endif
+               qres = ['src/gui/qjacktrip_novs.qrc']
+       else
+               src += [
+                       'src/gui/virtualstudio.cpp',
+                       'src/gui/vsAuth.cpp',
+                       'src/gui/vsApi.cpp',
+                       'src/gui/vsDeviceCodeFlow.cpp',
+                       'src/gui/vsDeeplink.cpp',
+                       'src/gui/vsDevice.cpp',
+                       'src/gui/vsAudio.cpp',
+                       'src/gui/vsServerInfo.cpp',
+                       'src/gui/vsQuickView.cpp',
+                       'src/gui/vsWebSocket.cpp',
+                       'src/gui/vsPermissions.cpp',
+                       'src/gui/vsPinger.cpp',
+                       'src/gui/vsPing.cpp',
+                       'src/gui/WebSocketTransport.cpp'
+               ]
+               moc_h += [
+                       'src/gui/virtualstudio.h',
+                       'src/gui/vsApi.h',
+                       'src/gui/vsAuth.h',
+                       'src/gui/vsDeviceCodeFlow.h',
+                       'src/gui/vsDeeplink.h',
+                       'src/gui/vsDevice.h',
+                       'src/gui/vsAudio.h',
+                       'src/gui/vsServerInfo.h',
+                       'src/gui/vsQuickView.h',
+                       'src/gui/vsWebSocket.h',
+                       'src/gui/vsPermissions.h',
+                       'src/gui/vsPinger.h',
+                       'src/gui/vsPing.h',
+                       'src/gui/vsQmlClipboard.h',
+                       'src/JTApplication.h',
+                       'src/gui/WebSocketTransport.h'
+               ]
+
+               if host_machine.system() == 'darwin'
+                       moc_h += ['src/gui/vsMacPermissions.h']
+               endif
+
+               if get_option('vsftux') == true
+                       defines += '-DVS_FTUX'
+               endif
+
+               deps += dependency('qt6', modules: ['Core', 'Gui', 'Network', 'Widgets', 'Core5Compat', 'Quick', 'QuickControls2', 'Qml', 'ShaderTools', 'Svg', 'WebSockets', 'WebEngineCore', 'WebEngineQuick', 'WebChannel'], include_type: 'system')
+               qres = ['src/gui/qjacktrip.qrc']
+       endif
+
+       if get_option('noupdater') == true or host_machine.system() == 'linux'
+               defines += '-DNO_UPDATER'
+       else
+               src += [
+                       'src/dblsqd/feed.cpp',
+                       'src/dblsqd/release.cpp',
+                       'src/dblsqd/semver.cpp',
+                       'src/dblsqd/update_dialog.cpp'
+               ]
+               moc_h += [
+                       'src/dblsqd/feed.h',
+                       'src/dblsqd/release.h',
+                       'src/dblsqd/semver.h',
+                       'src/dblsqd/update_dialog.h'
+               ]
+               ui_h += ['src/dblsqd/update_dialog.ui']
+       endif
+
+       if get_option('nofeedback') == true
+               defines += '-DNO_FEEDBACK'
+       else
+               src += [ 'src/Analyzer.cpp' ]
+               moc_h += [ 'src/Analyzer.h' ]
+       endif
+endif
+
+if get_option('default_library') == 'static'
+       # use qmake to get paths for qt libraries and plugins
+       # seems like qt module should have a method for this, but it doesn't
+       qmake = find_program('qmake', required: true)
+       qt_libdir = run_command(qmake, '-query', 'QT_INSTALL_LIBS', check : true).stdout().strip()
+       qt_plugindir = run_command(qmake, '-query', 'QT_INSTALL_PLUGINS', check : true).stdout().strip()
+       if qt_version == '6'
+               # qt6 requires "Bundled*" modules for linking
+               deps += dependency('qt6', modules: ['BundledLibpng', 'BundledPcre2', 'BundledHarfbuzz', 'BundledZLIB'], include_type: 'system')
+       else
+               deps += compiler.find_library('qtpcre2', required : true, dirs : [qt_libdir])
+       endif
+       if (host_machine.system() == 'linux')
+               # linux static
+               deps += compiler.find_library('ssl', required : true, dirs : [qt_libdir])
+               deps += compiler.find_library('crypto', required : true, dirs : [qt_libdir])
+               deps += compiler.find_library('dl', required : true)
+               deps += compiler.find_library('glib-2.0', required : true)
+               if qt_version == '6'
+                       # we need a Q_IMPORT_LIBRARY for the openssl backend on linux
+                       deps += compiler.find_library('qopensslbackend', required : true, dirs : [qt_plugindir+'/tls'])
+                       src += ['src/QtStaticPlugins.cpp']
+               endif
+       else
+               if (host_machine.system() == 'windows')
+                       # windows static
+                       deps += compiler.find_library('bcrypt', required : true)
+                       deps += compiler.find_library('winmm', required : true)
+                       deps += compiler.find_library('Crypt32', required : true)
+                       if qt_version == '6'
+                               deps += compiler.find_library('Authz', required : true)
+                       endif
+               else
+                       # mac static
+                       # this approach fails for universal builds, so we have to just append to link_args
+                       #deps += dependency('CoreServices', required : true)
+                       link_args += ['-framework', 'CoreServices']
+                       link_args += ['-framework', 'CFNetwork']
+                       link_args += ['-framework', 'AppKit']
+                       link_args += ['-framework', 'IOKit']
+                       link_args += ['-framework', 'Security']
+                       link_args += ['-framework', 'GSS']
+                       link_args += ['-framework', 'SystemConfiguration']
+                       deps += dependency('zlib', required : true)
+               endif
+       endif
+endif
+
+# TODO: QT_OPENSOURCE should only be defined for open source Qt distribution
+# in QMake this can be checked with QT_EDITION == 'OpenSource'
+defines += '-DQT_OPENSOURCE'
+
+rtaudio_dep = dependency('rtaudio', required: get_option('rtaudio'))
+if rtaudio_dep.found() == true
+       defines += '-DRT_AUDIO'
+       src += 'src/RtAudioInterface.cpp'
+       deps += rtaudio_dep
+endif
+
+if rtaudio_dep.found() == false and jack_dep.found() == false
+       error('''
+       JackTrip requires at least one available audio backend. Install the
+       appropriate library or enable the appropriate backends using meson
+       configure.''')
+endif
+
+if host_machine.system() == 'darwin'
+       src += ['src/gui/NoNap.mm']
+       # Adding CoreAudio here is a workaround and should be removed
+       # when https://github.com/thestk/rtaudio/issues/302 is fixed
+       # and arrived in all common package managers
+       # Check at 2022-07-30
+       apple_dep = dependency('appleframeworks', modules : ['foundation','coreaudio'])
+       deps += apple_dep
+       add_languages('objcpp')
+endif
+
+if host_machine.system() == 'darwin' and get_option('novs') == false
+       src += ['src/gui/vsMacPermissions.mm']
+       apple_av_dep = dependency('appleframeworks', modules : ['avfoundation', 'webkit'])
+       deps += apple_av_dep
+endif
+
+qres_files = []
+if qres.length() > 0
+       qres_files = qt.compile_resources(sources: qres)
+endif
+moc_files = qt.compile_moc(headers: moc_h, extra_args: defines)
+ui_files = []
+if ui_h.length() > 0
+       ui_files = qt.compile_ui(sources: ui_h)
+endif
+
+jacktrip = executable('jacktrip', src, qres_files, ui_files, moc_files, include_directories: incdirs, dependencies: deps, link_args: link_args, c_args: c_defines, cpp_args: defines, install: true )
+
+help2man = find_program('help2man', required: false)
+if (host_machine.system() == 'linux')
+       if help2man.found()
+               gzip = find_program('gzip', required: false)
+               help2man_opts = [
+                       '--name="high-quality system for audio network performances"',
+                       '--no-info',
+                       '--section=1']
+               manfile = custom_target('jacktrip.1',
+                       output: 'jacktrip.1',
+                       command: [help2man, help2man_opts, '--output=@OUTPUT@', jacktrip],
+                       install: not gzip.found(),
+                       install_dir: get_option('mandir') / 'man1')
+               if gzip.found()
+                       custom_target('jacktrip.1.gz',
+                               input: manfile,
+                               output: 'jacktrip.1.gz',
+                               command: [gzip, '-k', '-f', '@INPUT@'],
+                               install: true,
+                               install_dir: get_option('mandir') / 'man1')
+               endif
+       endif
+endif
+
+summary({'JACK': jack_dep.found(),
+       'Weak JACK Linking': get_option('weakjack'),
+       'RtAudio': rtaudio_dep.found()}, bool_yn: true, section: 'Audio Backends')
+
+summary({'Application ID': application_id,
+       'GUI': not get_option('nogui'),
+       'WAIR': get_option('wair'),
+       'Manpage': help2man.found()}, bool_yn: true, section: 'Configuration')
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644 (file)
index 0000000..e7daaf0
--- /dev/null
@@ -0,0 +1,12 @@
+option('wair', type : 'boolean', value : false, description: 'WAIR')
+option('rtaudio', type : 'feature', value : 'auto', description: 'Build with RtAudio Backend')
+option('jack', type : 'feature', value : 'auto', description: 'Build with JACK Backend')
+option('weakjack', type : 'boolean', value : false, description: 'Weak link JACK library')
+option('nogui', type : 'boolean', value : false, description: 'Build without graphical user interface')
+option('novs', type : 'boolean', value : false, description: 'Build without Virtual Studio support')
+option('vsftux', type : 'boolean', value : false, description: 'Build with Virtual Studio first launch experience')
+option('noupdater', type : 'boolean', value : false, description: 'Build without auto-update support')
+option('nofeedback', type : 'boolean', value : false, description: 'Build without feedback detection')
+option('profile', type: 'combo', choices: ['default', 'development'], value: 'default', description: 'Choose build profile / Sets desktop id accordingly')
+option('qtversion', type : 'combo', choices: ['', '5', '6'], description: 'Choose to build with either Qt5 or Qt6')
+option('buildinfo', type : 'string', value : '', yield : true, description: 'Additional info used to describe the build')
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644 (file)
index 0000000..501de72
--- /dev/null
@@ -0,0 +1,45 @@
+site_name: JackTrip
+repo_url: https://github.com/jacktrip/jacktrip
+edit_uri: edit/dev/docs/
+nav:
+  - Home: index.md
+  - User Guide:
+    - Installation: Install.md
+    - Virtual Studio: VirtualStudio.md
+    - Using custom JACK server name: CustomJackServerName.md
+  - Developer Guide:
+    - QMake Build:
+      - Linux: Build/Linux.md
+      - Mac OS: Build/Mac.md
+      - Windows: Build/Windows.md
+    - Meson Build: Build/Meson_build.md
+    - Development Tools:
+      - Formatting: DevTools/Formatting.md
+      - Static Analysis: DevTools/StaticAnalysis.md
+    - Write Documentation: Documentation/MkDocs.md
+  - About:
+    - Contributors: About/Contributors.md
+    - Resources: About/Resources.md
+    - 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:
+  - attr_list
+  - pymdownx.snippets
+  - pymdownx.tabbed:
+      alternate_style: true
+  - pymdownx.superfences
+  - pymdownx.details
+  - admonition
diff --git a/releases/edge/mac-manifests.json b/releases/edge/mac-manifests.json
new file mode 100644 (file)
index 0000000..efc8137
--- /dev/null
@@ -0,0 +1,565 @@
+{
+  "app_name": "JackTrip",
+  "releases": [
+    {
+      "version": "2.2.5",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.5",
+      "download": {
+        "date": "2024-03-29T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.5-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177374940",
+        "sha256": "4e1ae62b5717141c27b3a569311b3c17f7daa3065ee7bb69c18faf5c28ba5e8e"
+      }
+    },
+    {
+      "version": "2.2.4",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.4",
+      "download": {
+        "date": "2024-03-13T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.4-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177373843",
+        "sha256": "d01ebaa71cf101f57e20214d0cbdf402a989debda8ffd231ea84252da59656b1"
+      }
+    },
+    {
+      "version": "2.2.3",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.3",
+      "download": {
+        "date": "2024-03-04T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.3-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177372204",
+        "sha256": "68a4e2b0d4979d77607707a2c3c8bb25446465401a4c487a6d00fae70d8c2511"
+      }
+    },
+    {
+      "version": "2.2.2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.2",
+      "download": {
+        "date": "2024-02-10T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.2-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177374348",
+        "sha256": "80048be618c272c108fd9a45fb77167ca09686406a24065dba20edf05aabd088"
+      }
+    },
+    {
+      "version": "2.2.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.1",
+      "download": {
+        "date": "2024-01-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177373978",
+        "sha256": "d02e5de0cee389ee39c789ad6fe8859823944cf2c6af15f8d80249f3134f4653"
+      }
+    },
+    {
+      "version": "2.2.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0",
+      "download": {
+        "date": "2024-01-22T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177373618",
+        "sha256": "ecef1ac2ae1fd3f2da40017f5da1fca6d966946a016ba80116ca960d88f04a53"
+      }
+    },
+    {
+      "version": "2.2.0-beta1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0-beta1",
+      "download": {
+        "date": "2024-01-17T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-beta1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177373175",
+        "sha256": "6512e524d022eebe5b2e928d008c0fdb85dbaa453179952ee232f0d73d0d68eb"
+      }
+    },
+    {
+      "version": "2.1.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0",
+      "download": {
+        "date": "2023-11-06T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.1.0-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177373751",
+        "sha256": "e7ffb56b99f25de7c71774e6e6484c1e400ebe2fa05b9618695030e83a5de9a2"
+      }
+    },
+    {
+      "version": "2.1.0-beta1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0-beta1",
+      "download": {
+        "date": "2023-10-31T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.1.0-beta1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177365464",
+        "sha256": "46ab83844671028a27b59b09a4e7eec1398b772f85820c1213f330d35c2ceba9"
+      }
+    },
+    {
+      "version": "2.0.2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.2",
+      "download": {
+        "date": "2023-09-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.2-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177283016",
+        "sha256": "e4cd66790d0d008980402aef42d712fe42788a87e9d48a28c49d5e9c6ba55aea"
+      }
+    },
+    {
+      "version": "2.0.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.1",
+      "download": {
+        "date": "2023-08-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177272934",
+        "sha256": "5f12f512cd372beb01c6c888c8bbbaf4c1e5cf61881db9cd91c5d2c8582531b5"
+      }
+    },
+    {
+      "version": "2.0.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.0",
+      "download": {
+        "date": "2023-08-28T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.0-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177274079",
+        "sha256": "746f8dbefde53a175bc4144ab5b7d492d843c49c5ca6b7e9979fdbc9ce21e1be"
+      }
+    },
+    {
+      "version": "2.0.0-beta2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.0-beta2",
+      "download": {
+        "date": "2023-08-23T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.0-beta2-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177087895",
+        "sha256": "fa7663010309ae792dcb271218867602483f092f1946dd5d4257cf6be87b5a84"
+      }
+    },
+    {
+      "version": "2.0.0-beta1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.0-beta1",
+      "download": {
+        "date": "2023-08-07T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.0-beta1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177067983",
+        "sha256": "669852be48688e2b6e78a050f57e6e2776b699c4b50acdd4b2d762dbc4857431"
+      }
+    },
+    {
+      "version": "1.10.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.1",
+      "download": {
+        "date": "2023-08-04T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22717724",
+        "sha256": "5ee7d73035a2bfc5790b82fac5201d4147ccb3e8cf7573c9fd7078c1683255eb"
+      }
+    },
+    {
+      "version": "1.10.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0",
+      "download": {
+        "date": "2023-06-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22816785",
+        "sha256": "367cc26cb55946ffadc88f902de89b076b3b91bb36cea00131e9a2cad8a0901f"
+      }
+    },
+    {
+      "version": "1.10.0-beta4",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0-beta4",
+      "download": {
+        "date": "2023-06-13T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-beta4-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22824520",
+        "sha256": "1bf932eb1dac64639d397caf3cf6010417e029c32abcc6ee61f204fa3f242526"
+      }
+    },
+    {
+      "version": "1.10.0-beta3",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0-beta3",
+      "download": {
+        "date": "2023-06-10T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-beta3-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22825810",
+        "sha256": "ed8bf80f596843a153eee7930936d5943d15d78f391a6cbb8681783f2f562859"
+      }
+    },
+    {
+      "version": "1.10.0-beta2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0-beta2",
+      "download": {
+        "date": "2023-06-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-beta2-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22879032",
+        "sha256": "4dcd5f5939d30b3aa9d32dd649acc830752bc8e1c2d9d5ea884225a481dfcaf3"
+      }
+    },
+    {
+      "version": "1.10.0-beta1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0-beta1",
+      "download": {
+        "date": "2023-05-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-beta1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22804983",
+        "sha256": "ab46fccbdc2a9a80c782cc4b707ff3358d94677f680333416a74d37d6b54a781"
+      }
+    },
+    {
+      "version": "1.9.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.9.0",
+      "download": {
+        "date": "2023-05-12T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.9.0-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22806741",
+        "sha256": "71544c899c7ed4a6a93a4ee1c2452a895f61e25a000954e0e40584abef488488"
+      }
+    },
+    {
+      "version": "1.9.0-beta3",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.9.0-beta3",
+      "download": {
+        "date": "2023-05-05T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.9.0-beta3-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22811032",
+        "sha256": "0d6d0f34c4c99fb03810ea86360a8ac93874121db897b2ba23132acf9fdefc50"
+      }
+    },
+    {
+      "version": "1.9.0-beta2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.9.0-beta2",
+      "download": {
+        "date": "2023-04-25T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.9.0-beta2-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22776100",
+        "sha256": "99ea878f0fbd5913516cfd73e2403c61d181b03c7e4f251e8075b01fbd78ecd7"
+      }
+    },
+    {
+      "version": "1.9.0-beta1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.9.0-beta1",
+      "download": {
+        "date": "2023-04-18T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.9.0-beta1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "11768911",
+        "sha256": "a2f38e04cd03c3b8e549e7e82644df11dd1d59c03aa00806a02aa6ff7c76c1b0"
+      }
+    },
+    {
+      "version": "1.8.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.1",
+      "download": {
+        "date": "2023-04-03T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "11754694",
+        "sha256": "d073ff5f2b90b5dd86ccd07c5d30d61d7be776c5e3c0083e6f665f78fea48298"
+      }
+    },
+    {
+      "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-beta2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.0-beta2",
+      "download": {
+        "date": "2023-03-10T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.0-beta2-macOS-x64-signed-installer.pkg",
+        "downloadSize": "11769049",
+        "sha256": "9ffbf4c2c7b9419cd7e5000efe1441ca3eb0e44dfcdecd0347c561e7eecc4e36"
+      }
+    },
+    {
+      "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": "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"
+      }
+    },
+    {
+      "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": "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"
+      }
+    },
+    {
+      "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": "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"
+      }
+    },
+    {
+      "version": "1.6.9-beta1",
+      "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.9-beta1",
+      "download": {
+        "date": "2022-12-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.9-beta1-macOS-x64-installer.pkg",
+        "downloadSize": 11527211,
+        "sha256": "636055dee6fb84286cc73a95c887dd39c4a6d14780c451504db87860738b2278"
+      }
+    },
+    {
+      "version": "1.6.8",
+      "changelog": "Critical bug fix: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.8",
+      "download": {
+        "date": "2022-12-05T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.8-macOS-x64-installer.pkg",
+        "downloadSize": 11517093,
+        "sha256": "5c040b2caa1e7dea97d7f48fd888f532a1c30da7385535b2d4a2805551bc5f71"
+      }
+    },
+    {
+      "version": "1.6.7",
+      "changelog": "Updated Windows networking, enabling all audio devices on Windows, new default device behavior, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7",
+      "download": {
+        "date": "2022-12-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.7-macOS-x64-installer.pkg",
+        "downloadSize": 11517151,
+        "sha256": "38c25788895ec404bdb4b6148114cad8af31b65e5189ca08819d1e954e2ef4e7"
+      }
+    },
+    {
+      "version": "1.6.7-rc.2",
+      "changelog": "Virtual Studio first time UI, Non-ASIO devices on Windows, Windows can't connect fix: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7-rc.2",
+      "download": {
+        "date": "2022-11-29T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.7-rc.2/JackTrip-v1.6.7-rc.2-macOS-x64-installer.pkg",
+        "downloadSize": 11516784,
+        "sha256": "4fe2e4dbd67986926b6f738cd4d8a22b801fd3cd50aeebee13d2e1de0310b160"
+      }
+    },
+    {
+      "version": "1.6.7-rc.1",
+      "changelog": "New networking code on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7-rc.1",
+      "download": {
+        "date": "2022-11-07T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.7-rc.1-macOS-x64-installer.pkg",
+        "downloadSize": 11481444,
+        "sha256": "aa07023d130129684dc8d1e395d04eabea9709db32459e203c4fb7cf9759976e"
+      }
+    },
+    {
+      "version": "1.6.6",
+      "changelog": "Adding volume controls in Virtual Studio, preliminary QT6 support, classic mode GUI updates, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.6",
+      "download": {
+        "date": "2022-11-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.6-macOS-x64-installer.pkg",
+        "downloadSize": 11481324,
+        "sha256": "e99149ea9bbfb94a2000cfd3848013e44bb8d23e783198005681c2038bcbd313"
+      }
+    },
+    {
+      "version": "1.6.5-rc.1",
+      "changelog": "Adding volume controls, mute, early Qt6 support, and bug fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.5-rc1",
+      "download": {
+        "date": "2022-10-21T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.5-rc1/JackTrip-v1.6.5-rc1-macOS-x64-installer.pkg",
+        "downloadSize": 11481049,
+        "sha256": "cea18dd189d84eb9ca3be115819c597900e7bba7771185b61308518aa0e3d716"
+      }
+    },
+    {
+      "version": "1.6.4",
+      "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4",
+      "download": {
+        "date": "2022-09-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.4-macOS-x64-installer.pkg",
+        "downloadSize": 11554866,
+        "sha256": "e5898f3ff57fc2d734590decb4f82a28ad9208a33cad97152f119716cdcea1a9"
+      }
+    },
+    {
+      "version": "1.6.4-rc.2",
+      "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4-rc2",
+      "download": {
+        "date": "2022-09-08T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.4-rc2/JackTrip-v1.6.4-rc2-macOS-x64-installer.pkg",
+        "downloadSize": 11554117,
+        "sha256": "c849c22583883027f94141b3878cebc85295c0a617640449d4f66862982f75c0"
+      }
+    },
+    {
+      "version": "1.6.4-rc.1",
+      "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4-rc1",
+      "download": {
+        "date": "2022-09-01T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.4-rc1/JackTrip-v1.6.4-rc1-macOS-x64-installer.pkg",
+        "downloadSize": 11548759,
+        "sha256": "6e8af76766b164e40e7a44842a14e640cb3c0fac7b9b7067f9c75048d3354496"
+      }
+    },
+    {
+      "version": "1.6.3",
+      "changelog": "Fixes around Linux desktop file and hub server mode: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.3",
+      "download": {
+        "date": "2022-08-23T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.3-macOS-x64-installer.pkg",
+        "downloadSize": 11533023,
+        "sha256": "0b597c62544e9813949f859cc7b7c13bef893f6896f96b34a19889faf4855116"
+      }
+    },
+    {
+      "version": "1.6.2",
+      "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2",
+      "download": {
+        "date": "2022-08-17T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.2-macOS-x64-installer.pkg",
+        "downloadSize": 11536442,
+        "sha256": "450222f4db1922275e07286d0be6ea2df2873a8a39c3b23096bcfbce342b7b3c"
+      }
+    },
+    {
+      "version": "1.6.2-rc.3",
+      "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc3",
+      "download": {
+        "date": "2022-08-15T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc3/JackTrip-v1.6.2-rc3-macOS-x64-installer.pkg",
+        "downloadSize": 11536485,
+        "sha256": "accf625c8c797c13bde01fb50fe5bbb87fe4eefd0ae8ef06b74034e1cde6f22b"
+      }
+    },
+    {
+      "version": "1.6.2-rc.2",
+      "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc2",
+      "download": {
+        "date": "2022-08-09T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc2/JackTrip-v1.6.2-rc2-macOS-x64-installer.pkg",
+        "downloadSize": 11531462,
+        "sha256": "a8b5418992045a5d08bfce1e7a412a1ad8414f9d7ea770564f2bba0caa83297b"
+      }
+    },
+    {
+      "version": "1.6.2-rc.1",
+      "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc1",
+      "download": {
+        "date": "2022-08-06T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc1/JackTrip-v1.6.2-rc1-macOS-x64-installer.pkg",
+        "downloadSize": 11534071,
+        "sha256": "9a2200d157c4bb308b0b5ba5854ee5af17ae74991f7aa94fa5a0da19282cc571"
+      }
+    },
+    {
+      "version": "1.6.1",
+      "changelog": "Bugfixes around UDP timeout and 'Logging In' screen navigation. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.1",
+      "download": {
+        "date": "2022-06-21T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.1-macOS-x64-installer.pkg",
+        "downloadSize": 11476305,
+        "sha256": "eaf05c842d6b3ae799208a40b37da1cdb13e3700dcbbd97443c80cad81f4d2ac"
+      }
+    },
+    {
+      "version": "1.6.0",
+      "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0",
+      "download": {
+        "date": "2022-06-01T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-macOS-x64-installer.pkg",
+        "downloadSize": 11474299,
+        "sha256": "27259600ecd879106ebbf97754d72d6236075a049eafa0de6271d33f753f13e4"
+      }
+    },
+    {
+      "version": "1.6.0-rc.5",
+      "changelog": "Release candidate 5 for 1.6.0",
+      "download": {
+        "date": "2022-05-30T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.5/JackTrip-v1.6.0-rc.5-macOS-x64-installer.pkg",
+        "downloadSize": 11474262,
+        "sha256": "8289530a8e6ef1f772776c7078679e2dac146f366cfc4e8c09e0ad16865fe274"
+      }
+    },
+    {
+      "version": "1.6.0-rc.4",
+      "changelog": "Release candidate 4 for 1.6.0",
+      "download": {
+        "date": "2022-05-29T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.4/JackTrip-v1.6.0-rc.4-macOS-x64-installer.pkg",
+        "downloadSize": 11460550,
+        "sha256": "38d817f3e8cc61b707392ce74cee8ab46da9c8eb2086ea2b3f0c79496caf70a8"
+      }
+    },
+    {
+      "version": "1.6.0-rc.3",
+      "changelog": "Release candidate 3 for 1.6.0",
+      "download": {
+        "date": "2022-05-27T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.3-macOS-x64-installer.pkg",
+        "downloadSize": 11460230,
+        "sha256": "c9614964974d61c062d905f01c7d30ab04a697562ecfba6264392aebe7161051"
+      }
+    },
+    {
+      "version": "1.6.0-rc.2",
+      "changelog": "Release candidate 2 for 1.6.0",
+      "download": {
+        "date": "2022-05-26T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.2-macOS-x64-signed-installer.pkg",
+        "downloadSize": 11460155,
+        "sha256": "ad508680115f73036da3a5328ddf0841b86620406406e0ffaa4b982e24a27771"
+      }
+    },
+    {
+      "version": "1.6.0-rc.1",
+      "changelog": "Release candidate 1 for 1.6.0",
+      "download": {
+        "date": "2022-05-23T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.1/JackTrip-v1.6.0-rc.1-macOS-x64-installer.pkg",
+        "downloadSize": 11076221,
+        "sha256": "071cda0ce59361e474a04db00beec41e92d2d823dab71e3fab179faf89f6fd7e"
+      }
+    }
+  ]
+}
diff --git a/releases/edge/win-manifests.json b/releases/edge/win-manifests.json
new file mode 100644 (file)
index 0000000..9a71d21
--- /dev/null
@@ -0,0 +1,565 @@
+{
+  "app_name": "JackTrip",
+  "releases": [
+    {
+      "version": "2.2.5",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.5",
+      "download": {
+        "date": "2024-03-29T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.5-Windows-x64-signed-installer.msi",
+        "downloadSize": "116228096",
+        "sha256": "903343959992733001d4e7db30c83fc4ff8120903aeca0eb86b1601c9d21e15b"
+      }
+    },
+    {
+      "version": "2.2.4",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.4",
+      "download": {
+        "date": "2024-03-13T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.4-Windows-x64-signed-installer.msi",
+        "downloadSize": "116228096",
+        "sha256": "559cc71c0050d72706378997d9eb24c923953142abdba72a47d82b8887c29c9d"
+      }
+    },
+    {
+      "version": "2.2.3",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.3",
+      "download": {
+        "date": "2024-03-04T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.3-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "76f721e1ca52835ab074909e2044624363e5f63f0e1b8f6702bf2a857da59979"
+      }
+    },
+    {
+      "version": "2.2.2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.2",
+      "download": {
+        "date": "2024-02-10T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.2-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "f2ea2d2ea1b40607e1fc6fc22cac4f51630b66da4e8f1925f529396bb57e6010"
+      }
+    },
+    {
+      "version": "2.2.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.1",
+      "download": {
+        "date": "2024-01-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.1-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "193825d24745cd5a052ae57f1345b02924fc269aa69324428e7a177b9c58aa05"
+      }
+    },
+    {
+      "version": "2.2.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0",
+      "download": {
+        "date": "2024-01-22T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "92aeb6ba74fcb5cade48962aa4696a77fb7b2434622c22097cfa9da037b32fb3"
+      }
+    },
+    {
+      "version": "2.2.0-beta1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0-beta1",
+      "download": {
+        "date": "2024-01-17T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-beta1-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "24401a0adaf8753f68d4303bda0d08fed35032168254ab7445766216cfb73980"
+      }
+    },
+    {
+      "version": "2.1.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0",
+      "download": {
+        "date": "2023-11-06T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.1.0-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "cd5b4735421a484bf83635f07653755e56b095c785f021eedaa4ca2d4132dd7f"
+      }
+    },
+    {
+      "version": "2.1.0-beta1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0-beta1",
+      "download": {
+        "date": "2023-10-31T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.1.0-beta1-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "d52784050fdd9876d44bc3fddd1b70b93065ab4c6b67076dcbfe42a098d73447"
+      }
+    },
+    {
+      "version": "2.0.2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.2",
+      "download": {
+        "date": "2023-09-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.2-Windows-x64-signed-installer.msi",
+        "downloadSize": "95850496",
+        "sha256": "6d06a9843f223fe95a76de838a442ce64050a1aeea7e64d096a496921e1711f1"
+      }
+    },
+    {
+      "version": "2.0.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.1",
+      "download": {
+        "date": "2023-08-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.1-Windows-x64-signed-installer.msi",
+        "downloadSize": "95846400",
+        "sha256": "aeb8934b7ef5ab274a135f575b13f16e1e93b42cfc631cfefa611477ae092c23"
+      }
+    },
+    {
+      "version": "2.0.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.0",
+      "download": {
+        "date": "2023-08-28T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.0-Windows-x64-signed-installer.msi",
+        "downloadSize": "95846400",
+        "sha256": "c815d992ff7be9a67e5c5b2823cfeda5d652efacf6463d1f9c7fdfb1561296f8"
+      }
+    },
+    {
+      "version": "2.0.0-beta2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.0-beta2",
+      "download": {
+        "date": "2023-08-23T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.0-beta2-Windows-x64-signed-installer.msi",
+        "downloadSize": "95608832",
+        "sha256": "0ee198dab26a1ea26d2c2908dc110b27ee031e479f7be59527fbb82070d56bc3"
+      }
+    },
+    {
+      "version": "2.0.0-beta1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.0-beta1",
+      "download": {
+        "date": "2023-08-07T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.0-beta1-Windows-x64-signed-installer.msi",
+        "downloadSize": "95596544",
+        "sha256": "5f695fc4cde25cc7d5d0369e27d7e273120afa2251d90095e711825a2fc88093"
+      }
+    },
+    {
+      "version": "1.10.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.1",
+      "download": {
+        "date": "2023-08-04T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.1-Windows-x64-signed-installer.msi",
+        "downloadSize": "46419968",
+        "sha256": "f458526ebbde294771ddc47d5aa0cf13ae5e2ba55f659e15f9bfe57a26ed78bf"
+      }
+    },
+    {
+      "version": "1.10.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0",
+      "download": {
+        "date": "2023-06-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-Windows-x64-signed-installer.msi",
+        "downloadSize": "46432256",
+        "sha256": "6d08a83a8a2c153e0fdd43cdb913ebe244552f4f96d873ea4887e3d5be6d235e"
+      }
+    },
+    {
+      "version": "1.10.0-beta4",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0-beta4",
+      "download": {
+        "date": "2023-06-13T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-beta4-Windows-x64-signed-installer.msi",
+        "downloadSize": "46428160",
+        "sha256": "e9c15938fba55d3391a705394fbf4d0b1d2d83851fbd3ba58d9480739b4452c0"
+      }
+    },
+    {
+      "version": "1.10.0-beta3",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0-beta3",
+      "download": {
+        "date": "2023-06-10T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-beta3-Windows-x64-signed-installer.msi",
+        "downloadSize": "46436352",
+        "sha256": "2b377eaf1ded51b198274071ad564fbd78d066646f22e6230fca2743233e0d41"
+      }
+    },
+    {
+      "version": "1.10.0-beta2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0-beta2",
+      "download": {
+        "date": "2023-06-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-beta2-Windows-x64-signed-installer.msi",
+        "downloadSize": "46530560",
+        "sha256": "973c24a27e1594092c0b0c84cd029dce904404bd56d1f7c9bef1e1831425a2d5"
+      }
+    },
+    {
+      "version": "1.10.0-beta1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0-beta1",
+      "download": {
+        "date": "2023-05-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-beta1-Windows-x64-signed-installer.msi",
+        "downloadSize": "46403584",
+        "sha256": "6a415fbd4fbc7843c88cd2a635a50000aae13800aa00bd5ba6381e91f252e29a"
+      }
+    },
+    {
+      "version": "1.9.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.9.0",
+      "download": {
+        "date": "2023-05-12T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.9.0-Windows-x64-signed-installer.msi",
+        "downloadSize": "46383104",
+        "sha256": "ab73d244c04d7c5a6e553aa69dc989a2dcc519056bc2f7e99126af8a5bdd8f6f"
+      }
+    },
+    {
+      "version": "1.9.0-beta3",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.9.0-beta3",
+      "download": {
+        "date": "2023-05-05T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.9.0-beta3-Windows-x64-signed-installer.msi",
+        "downloadSize": "46383104",
+        "sha256": "7db404d0fe9d062409d9f1ebe3382ab99aa9638f82c9951cbc442dab91e42f21"
+      }
+    },
+    {
+      "version": "1.9.0-beta2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.9.0-beta2",
+      "download": {
+        "date": "2023-04-25T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.9.0-beta2-Windows-x64-signed-installer.msi",
+        "downloadSize": "46546944",
+        "sha256": "ee75aa4bb3e617f055b9a5b64c2bf98006d774845a4f1cdc247add7dfc1ba9fc"
+      }
+    },
+    {
+      "version": "1.9.0-beta1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.9.0-beta1",
+      "download": {
+        "date": "2023-04-18T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.9.0-beta1-Windows-x64-signed-installer.msi",
+        "downloadSize": "46530560",
+        "sha256": "80fac822af3e826743c501715681430fdecda9799dd6e8cf478e8b87f0d0d9c5"
+      }
+    },
+    {
+      "version": "1.8.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.1",
+      "download": {
+        "date": "2023-04-03T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.1-Windows-x64-signed-installer.msi",
+        "downloadSize": "46456832",
+        "sha256": "55da405ea55a3567a672eef0f5f3699c264f456e13dd04d2a55d90bd3d1a2f55"
+      }
+    },
+    {
+      "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-beta2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.0-beta2",
+      "download": {
+        "date": "2023-03-10T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.0-beta2-Windows-x64-signed-installer.msi",
+        "downloadSize": "45694976",
+        "sha256": "8d080a009b5583fc4360dfefb2efaa74966c2d76d9a982bba36c42c8a0e536fd"
+      }
+    },
+    {
+      "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": "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"
+      }
+    },
+    {
+      "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": "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"
+      }
+    },
+    {
+      "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": "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"
+      }
+    },
+    {
+      "version": "1.6.9-beta1",
+      "changelog": "Start/join inactive studios, fix crashes and hanging UI on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.9-beta1",
+      "download": {
+        "date": "2022-12-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.9-beta1-Windows-x64-installer.msi",
+        "downloadSize": 44548096,
+        "sha256": "c5cfbfc1c7650d685489974b69f005671ceb44a1301006d33ceeda45fc00f7c7"
+      }
+    },
+    {
+      "version": "1.6.8",
+      "changelog": "Critical bug fix: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.8",
+      "download": {
+        "date": "2022-12-05T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.8-Windows-x64-installer.msi",
+        "downloadSize": 44539904,
+        "sha256": "e4ac16e7a66b4d656eea4e88f703fe156d656aedc16ad7f63ec570d82c27ed54"
+      }
+    },
+    {
+      "version": "1.6.7",
+      "changelog": "Updated Windows networking, enabling all audio devices on Windows, new default device behavior, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7",
+      "download": {
+        "date": "2022-12-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.7-Windows-x64-installer.msi",
+        "downloadSize": 44535808,
+        "sha256": "75464575311da5521e011da8d2f2b5aff1379edb4dee9dcb90e1750dcc97f2f9"
+      }
+    },
+    {
+      "version": "1.6.7-rc.2",
+      "changelog": "Virtual Studio first time UI, Non-ASIO devices on Windows, Windows can't connect fix: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7-rc.2",
+      "download": {
+        "date": "2022-11-29T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.7-rc.2/JackTrip-v1.6.7-rc.2-Windows-x64-installer.msi",
+        "downloadSize": 44531712,
+        "sha256": "656716e1e665187474bda00c89348a405018a69830557b4722e382187ee367e1"
+      }
+    },
+    {
+      "version": "1.6.7-rc.1",
+      "changelog": "New networking code on Windows: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7-rc.1",
+      "download": {
+        "date": "2022-11-07T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.7-rc.1-Windows-x64-installer.msi",
+        "downloadSize": 44412928,
+        "sha256": "a0485d45c615c435fc4d6b0840d0bf8a74f990001eeacfe26a74d61afbd72dfd"
+      }
+    },
+    {
+      "version": "1.6.6",
+      "changelog": "Adding volume controls in Virtual Studio, preliminary QT6 support, classic mode GUI updates, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.6",
+      "download": {
+        "date": "2022-11-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.6-Windows-x64-installer.msi",
+        "downloadSize": 44408832,
+        "sha256": "828a1f43254db187a33601cc809551617db83e60a51dad3e992c30270967de0c"
+      }
+    },
+    {
+      "version": "1.6.5-rc.1",
+      "changelog": "Adding volume controls, mute, early Qt6 support, and bug fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.5-rc1",
+      "download": {
+        "date": "2022-10-21T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.5-rc1/JackTrip-v1.6.5-rc1-Windows-x64-installer.msi",
+        "downloadSize": 44408832,
+        "sha256": "99404fa7bf1a07df76a1b1048c7a0e64a0d5f552e2ca5dfb12e7cbaac0c6fb24"
+      }
+    },
+    {
+      "version": "1.6.4",
+      "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4",
+      "download": {
+        "date": "2022-09-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.4-Windows-x64-installer.msi",
+        "downloadSize": 43663360,
+        "sha256": "58dcbba584e1cc82373b8aa159800ad360eec7933ce9462e413ca347f09f3c26"
+      }
+    },
+    {
+      "version": "1.6.4-rc.2",
+      "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4-rc2",
+      "download": {
+        "date": "2022-09-08T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.4-rc2/JackTrip-v1.6.4-rc2-Windows-x64-installer.msi",
+        "downloadSize": 43663360,
+        "sha256": "fc782f0f9547ff096eb99891745e5d80056090b05a179f0209bd7785b1b808a6"
+      }
+    },
+    {
+      "version": "1.6.4-rc.1",
+      "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4-rc1",
+      "download": {
+        "date": "2022-09-01T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.4-rc1/JackTrip-v1.6.4-rc1-Windows-x64-installer.msi",
+        "downloadSize": 43651072,
+        "sha256": "fbe0f883429119b4c878e0dff4bb2c79f2ac0d995b9c947f7eb8cc70fa59bc67"
+      }
+    },
+    {
+      "version": "1.6.3",
+      "changelog": "Fixes around Linux desktop file and hub server mode: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.3",
+      "download": {
+        "date": "2022-08-23T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.3-Windows-x64-installer.msi",
+        "downloadSize": 43606016,
+        "sha256": "83a4def2a8c8fde24d147d39e70f60ee144e9425828f34eda2340c23ce72b1da"
+      }
+    },
+    {
+      "version": "1.6.2",
+      "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2",
+      "download": {
+        "date": "2022-08-17T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.2-Windows-x64-installer.msi",
+        "downloadSize": 43606016,
+        "sha256": "2bb06fe3624df447d56da0d72fef1f4328346fd92c6dffccf66ba37253ec5e62"
+      }
+    },
+    {
+      "version": "1.6.2-rc.3",
+      "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc3",
+      "download": {
+        "date": "2022-08-15T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc3/JackTrip-v1.6.2-rc3-Windows-x64-installer.msi",
+        "downloadSize": 43606016,
+        "sha256": "62771ca5efbf2e91fa4cd347214e6e517b76c032a8895ca80bcbc2fa765ab81a"
+      }
+    },
+    {
+      "version": "1.6.2-rc.2",
+      "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc2",
+      "download": {
+        "date": "2022-08-09T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc2/JackTrip-v1.6.2-rc2-Windows-x64-installer.msi",
+        "downloadSize": 43606016,
+        "sha256": "ff88acd1804362589478366a620d12be302071dba9781ea38ed6a8343c94c16d"
+      }
+    },
+    {
+      "version": "1.6.2-rc.1",
+      "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2-rc1",
+      "download": {
+        "date": "2022-08-06T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.2-rc1/JackTrip-v1.6.2-rc1-Windows-x64-installer.msi",
+        "downloadSize": 43601920,
+        "sha256": "f1412de0b13ff7599353a10aec8f2b69e9831a37103187f8fa68334c8f8f09de"
+      }
+    },
+    {
+      "version": "1.6.1",
+      "changelog": "Bugfixes around UDP timeout and 'Logging In' screen navigation. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.1",
+      "download": {
+        "date": "2022-06-21T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.1-Windows-x64-installer.msi",
+        "downloadSize": 43368448,
+        "sha256": "8eac390617488d849c0356e3305c96a59bbe46a8174d02b0321bb1dc86774b87"
+      }
+    },
+    {
+      "version": "1.6.0",
+      "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0",
+      "download": {
+        "date": "2022-06-01T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-Windows-x64-installer.msi",
+        "downloadSize": 43364352,
+        "sha256": "9562ab654202bfc432e05caa3bd2bf1d0b52c50581b0a567f0546983fe46c078"
+      }
+    },
+    {
+      "version": "1.6.0-rc.5",
+      "changelog": "Release candidate 5 for 1.6.0",
+      "download": {
+        "date": "2022-05-30T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.5/JackTrip-v1.6.0-rc.5-Windows-x64-installer.msi",
+        "downloadSize": 43364352,
+        "sha256": "d84e6e5d21cf31f5dd48e9dcc0c1a44fe7a37d977f94b6ff63d5e381745e5a44"
+      }
+    },
+    {
+      "version": "1.6.0-rc.4",
+      "changelog": "Release candidate 4 for 1.6.0",
+      "download": {
+        "date": "2022-05-29T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.4/JackTrip-v1.6.0-rc.4-Windows-x64-installer.msi",
+        "downloadSize": 43126784,
+        "sha256": "cdb0ef906cf0d6047289838bf013b31a626cdd74dd4f75d6c1c4c3adbc9cd41d"
+      }
+    },
+    {
+      "version": "1.6.0-rc.3",
+      "changelog": "Release candidate 3 for 1.6.0",
+      "download": {
+        "date": "2022-05-27T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.3-Windows-x64-installer.msi",
+        "downloadSize": 43118592,
+        "sha256": "dceaf670a67cf1541007db82c5ce937b25370a7140e48192b94470f575fc4988"
+      }
+    },
+    {
+      "version": "1.6.0-rc.2",
+      "changelog": "Release candidate 2 for 1.6.0",
+      "download": {
+        "date": "2022-05-26T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-rc.2-Windows-x64-signed-installer.msi",
+        "downloadSize": 43114496,
+        "sha256": "b1a7adc8dc0fb47f59515790e8531dd10838d799bacb4b5653192ed621bca208"
+      }
+    },
+    {
+      "version": "1.6.0-rc.1",
+      "changelog": "Release candidate 1 for 1.6.0",
+      "download": {
+        "date": "2022-05-23T00:00:00Z",
+        "url": "https://github.com/jacktrip/jacktrip/releases/download/v1.6.0-rc.1/JackTrip-v1.6.0-rc.1-Windows-x64-installer.msi",
+        "downloadSize": 43081728,
+        "sha256": "240f8b495ec5057228be922da80829a3718b474bafc2ba2d77750643abd1005c"
+      }
+    }
+  ]
+}
diff --git a/releases/stable/linux-manifests.json b/releases/stable/linux-manifests.json
new file mode 100644 (file)
index 0000000..514ed82
--- /dev/null
@@ -0,0 +1,215 @@
+{
+  "app_name": "JackTrip",
+  "releases": [
+    {
+      "version": "2.2.5",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.5",
+      "download": {
+        "date": "2024-03-29T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.5-Linux-x64-binary.zip",
+        "downloadSize": "1250262",
+        "sha256": "3995fb1c56983ee72f3a51c10ea1b94c635f3ee52308a376a0357a861b0a9e37"
+      }
+    },
+    {
+      "version": "2.2.4",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.4",
+      "download": {
+        "date": "2024-03-13T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.4-Linux-x64-binary.zip",
+        "downloadSize": "1249531",
+        "sha256": "5b83840c3df05d0399e5d2dea420e58cc4324dc13895b59897d62baa8a430991"
+      }
+    },
+    {
+      "version": "2.2.3",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.3",
+      "download": {
+        "date": "2024-03-04T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.3-Linux-x64-binary.zip",
+        "downloadSize": "1239992",
+        "sha256": "f06507caaed33705a8b4b6b20e2b49358fc77aba2138e79e4c543c1fce9b9bd5"
+      }
+    },
+    {
+      "version": "2.2.2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.2",
+      "download": {
+        "date": "2024-02-10T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.2-Linux-x64-binary.zip",
+        "downloadSize": "1239713",
+        "sha256": "d0c7e3291d3b1753968a52812a6cae9b926ed1d5c17c23e2a87c1a09f14f10ea"
+      }
+    },
+    {
+      "version": "2.2.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.1",
+      "download": {
+        "date": "2024-01-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.1-Linux-x64-binary.zip",
+        "downloadSize": "1239788",
+        "sha256": "bfd986377b54c1ab84f16e0c0fd5ca61ed50e6cec281f7505e95d2b663af32f7"
+      }
+    },
+    {
+      "version": "2.2.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0",
+      "download": {
+        "date": "2024-01-22T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-Linux-x64-binary.zip",
+        "downloadSize": "1239784",
+        "sha256": "c5ce96f64ea204f1a17a951e9d39fd247e925fd21f55ae48a8bc27d6767cf675"
+      }
+    },
+    {
+      "version": "2.1.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0",
+      "download": {
+        "date": "2023-11-06T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.1.0-Linux-x64-binary.zip",
+        "downloadSize": "1239221",
+        "sha256": "1f990a9d4e7874d5129f287eee3ace4881130c23531be9ca816a9cc01df17379"
+      }
+    },
+    {
+      "version": "2.0.2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.2",
+      "download": {
+        "date": "2023-09-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.2-Linux-x64-binary.zip",
+        "downloadSize": "1234376",
+        "sha256": "1382d61c49af64082d36ceac953b23dbe5913dcb1a09a0dd60fb93a6837e319a"
+      }
+    },
+    {
+      "version": "2.0.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.1",
+      "download": {
+        "date": "2023-08-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.1-Linux-x64-binary.zip",
+        "downloadSize": "1216267",
+        "sha256": "eb7f10108e4cd2ef09b3df8b83f67187dc9174ac632dcb0ddd61087e65c3f0cb"
+      }
+    },
+    {
+      "version": "2.0.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.0",
+      "download": {
+        "date": "2023-08-28T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.0-Linux-x64-binary.zip",
+        "downloadSize": "1216331",
+        "sha256": "fa493f9dc9b90df2b899f2caeee488039f4068844df75ae8a129145c37004bf9"
+      }
+    },
+    {
+      "version": "1.10.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.1",
+      "download": {
+        "date": "2023-08-04T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.1-Linux-x64-binary.zip",
+        "downloadSize": "13388786",
+        "sha256": "83911666f2752d273367d91b5ede0783f7993c53c418911d62bde726618200f9"
+      }
+    },
+    {
+      "version": "1.10.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0",
+      "download": {
+        "date": "2023-06-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-Linux-x64-binary.zip",
+        "downloadSize": "13669762",
+        "sha256": "c7b97dd56d7976e5ca753c4bcec580ec48fbd40381f22fef0ffa6d81fdf2dd8e"
+      }
+    },
+    {
+      "version": "1.9.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.9.0",
+      "download": {
+        "date": "2023-05-12T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.9.0-Linux-x64-binary.zip",
+        "downloadSize": "13665582",
+        "sha256": "1949e6ca152b753bc2113a8b663bd0b0aeecf871912245d056e2d28007ec45db"
+      }
+    },
+    {
+      "version": "1.8.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.1",
+      "download": {
+        "date": "2023-04-03T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.1-Linux-x64-binary.zip",
+        "downloadSize": "36128967",
+        "sha256": "c0c082942ae2010553ba01c51c23b4ba01d8e5d4f69d3d2d2e56e183156d6f07"
+      }
+    },
+    {
+      "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",
+      "download": {
+        "date": "2022-12-05T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.8-Linux-x64-binary.zip",
+        "downloadSize": 30496801,
+        "sha256": "c2fcbf86f8ad4db862431f9ccf6b52b275b3cf5fc3bc2f7eea7ac803ee7ca590"
+      }
+    },
+    {
+      "version": "1.6.7",
+      "changelog": "Updated Windows networking, enabling all audio devices on Windows, new default device behavior, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7",
+      "download": {
+        "date": "2022-12-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.7-Linux-x64-binary.zip",
+        "downloadSize": 30495373,
+        "sha256": "09421562aeeefe40bb15945bf1e8e03b3f905ea383ce0a9ddd1f93f099141cfd"
+      }
+    },
+    {
+      "version": "1.6.6",
+      "changelog": "Adding volume controls in Virtual Studio, preliminary QT6 support, classic mode GUI updates, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.6",
+      "download": {
+        "date": "2022-11-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.6-Linux-x64-binary.zip",
+        "downloadSize": 29837298,
+        "sha256": "25b15f3208521530b18e2096de722fefe5318149aa296610f8c5eec147586e0a"
+      }
+    },
+    {
+      "version": "1.6.4",
+      "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4",
+      "download": {
+        "date": "2022-09-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.4-Linux-x64-binary.zip",
+        "downloadSize": 22233485,
+        "sha256": "f1b9585e4eb72c07c735ee6f4dc94ad1d1a4c70474799eb1111bb5a39a99e295"
+      }
+    }
+  ]
+}
diff --git a/releases/stable/mac-manifests.json b/releases/stable/mac-manifests.json
new file mode 100644 (file)
index 0000000..7caef64
--- /dev/null
@@ -0,0 +1,255 @@
+{
+  "app_name": "JackTrip",
+  "releases": [
+    {
+      "version": "2.2.5",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.5",
+      "download": {
+        "date": "2024-03-29T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.5-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177374940",
+        "sha256": "4e1ae62b5717141c27b3a569311b3c17f7daa3065ee7bb69c18faf5c28ba5e8e"
+      }
+    },
+    {
+      "version": "2.2.4",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.4",
+      "download": {
+        "date": "2024-03-13T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.4-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177373843",
+        "sha256": "d01ebaa71cf101f57e20214d0cbdf402a989debda8ffd231ea84252da59656b1"
+      }
+    },
+    {
+      "version": "2.2.3",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.3",
+      "download": {
+        "date": "2024-03-04T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.3-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177372204",
+        "sha256": "68a4e2b0d4979d77607707a2c3c8bb25446465401a4c487a6d00fae70d8c2511"
+      }
+    },
+    {
+      "version": "2.2.2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.2",
+      "download": {
+        "date": "2024-02-10T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.2-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177374348",
+        "sha256": "80048be618c272c108fd9a45fb77167ca09686406a24065dba20edf05aabd088"
+      }
+    },
+    {
+      "version": "2.2.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.1",
+      "download": {
+        "date": "2024-01-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177373978",
+        "sha256": "d02e5de0cee389ee39c789ad6fe8859823944cf2c6af15f8d80249f3134f4653"
+      }
+    },
+    {
+      "version": "2.2.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0",
+      "download": {
+        "date": "2024-01-22T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177373618",
+        "sha256": "ecef1ac2ae1fd3f2da40017f5da1fca6d966946a016ba80116ca960d88f04a53"
+      }
+    },
+    {
+      "version": "2.1.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0",
+      "download": {
+        "date": "2023-11-06T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.1.0-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177373751",
+        "sha256": "e7ffb56b99f25de7c71774e6e6484c1e400ebe2fa05b9618695030e83a5de9a2"
+      }
+    },
+    {
+      "version": "2.0.2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.2",
+      "download": {
+        "date": "2023-09-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.2-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177283016",
+        "sha256": "e4cd66790d0d008980402aef42d712fe42788a87e9d48a28c49d5e9c6ba55aea"
+      }
+    },
+    {
+      "version": "2.0.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.1",
+      "download": {
+        "date": "2023-08-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177272934",
+        "sha256": "5f12f512cd372beb01c6c888c8bbbaf4c1e5cf61881db9cd91c5d2c8582531b5"
+      }
+    },
+    {
+      "version": "2.0.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.0",
+      "download": {
+        "date": "2023-08-28T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.0-macOS-x64-signed-installer.pkg",
+        "downloadSize": "177274079",
+        "sha256": "746f8dbefde53a175bc4144ab5b7d492d843c49c5ca6b7e9979fdbc9ce21e1be"
+      }
+    },
+    {
+      "version": "1.10.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.1",
+      "download": {
+        "date": "2023-08-04T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22717724",
+        "sha256": "5ee7d73035a2bfc5790b82fac5201d4147ccb3e8cf7573c9fd7078c1683255eb"
+      }
+    },
+    {
+      "version": "1.10.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0",
+      "download": {
+        "date": "2023-06-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22816785",
+        "sha256": "367cc26cb55946ffadc88f902de89b076b3b91bb36cea00131e9a2cad8a0901f"
+      }
+    },
+    {
+      "version": "1.9.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.9.0",
+      "download": {
+        "date": "2023-05-12T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.9.0-macOS-x64-signed-installer.pkg",
+        "downloadSize": "22806741",
+        "sha256": "71544c899c7ed4a6a93a4ee1c2452a895f61e25a000954e0e40584abef488488"
+      }
+    },
+    {
+      "version": "1.8.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.1",
+      "download": {
+        "date": "2023-04-03T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.1-macOS-x64-signed-installer.pkg",
+        "downloadSize": "11754694",
+        "sha256": "d073ff5f2b90b5dd86ccd07c5d30d61d7be776c5e3c0083e6f665f78fea48298"
+      }
+    },
+    {
+      "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",
+      "download": {
+        "date": "2022-12-05T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.8-macOS-x64-installer.pkg",
+        "downloadSize": 11517093,
+        "sha256": "5c040b2caa1e7dea97d7f48fd888f532a1c30da7385535b2d4a2805551bc5f71"
+      }
+    },
+    {
+      "version": "1.6.7",
+      "changelog": "Updated Windows networking, enabling all audio devices on Windows, new default device behavior, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7",
+      "download": {
+        "date": "2022-12-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.7-macOS-x64-installer.pkg",
+        "downloadSize": 11517151,
+        "sha256": "38c25788895ec404bdb4b6148114cad8af31b65e5189ca08819d1e954e2ef4e7"
+      }
+    },
+    {
+      "version": "1.6.6",
+      "changelog": "Adding volume controls in Virtual Studio, preliminary QT6 support, classic mode GUI updates, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.6",
+      "download": {
+        "date": "2022-11-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.6-macOS-x64-installer.pkg",
+        "downloadSize": 11481324,
+        "sha256": "e99149ea9bbfb94a2000cfd3848013e44bb8d23e783198005681c2038bcbd313"
+      }
+    },
+    {
+      "version": "1.6.4",
+      "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4",
+      "download": {
+        "date": "2022-09-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.4-macOS-x64-installer.pkg",
+        "downloadSize": 11554866,
+        "sha256": "e5898f3ff57fc2d734590decb4f82a28ad9208a33cad97152f119716cdcea1a9"
+      }
+    },
+    {
+      "version": "1.6.3",
+      "changelog": "Fixes around Linux desktop file and hub server mode: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.3",
+      "download": {
+        "date": "2022-08-23T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.3-macOS-x64-installer.pkg",
+        "downloadSize": 11533023,
+        "sha256": "0b597c62544e9813949f859cc7b7c13bef893f6896f96b34a19889faf4855116"
+      }
+    },
+    {
+      "version": "1.6.2",
+      "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2",
+      "download": {
+        "date": "2022-08-17T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.2-macOS-x64-installer.pkg",
+        "downloadSize": 11536442,
+        "sha256": "450222f4db1922275e07286d0be6ea2df2873a8a39c3b23096bcfbce342b7b3c"
+      }
+    },
+    {
+      "version": "1.6.1",
+      "changelog": "Bugfixes around UDP timeout and 'Logging In' screen navigation. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.1",
+      "download": {
+        "date": "2022-06-21T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.1-macOS-x64-installer.pkg",
+        "downloadSize": 11476305,
+        "sha256": "eaf05c842d6b3ae799208a40b37da1cdb13e3700dcbbd97443c80cad81f4d2ac"
+      }
+    },
+    {
+      "version": "1.6.0",
+      "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0",
+      "download": {
+        "date": "2022-06-01T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-macOS-x64-installer.pkg",
+        "downloadSize": 11474299,
+        "sha256": "27259600ecd879106ebbf97754d72d6236075a049eafa0de6271d33f753f13e4"
+      }
+    }
+  ]
+}
diff --git a/releases/stable/win-manifests.json b/releases/stable/win-manifests.json
new file mode 100644 (file)
index 0000000..e8451be
--- /dev/null
@@ -0,0 +1,255 @@
+{
+  "app_name": "JackTrip",
+  "releases": [
+    {
+      "version": "2.2.5",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.5",
+      "download": {
+        "date": "2024-03-29T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.5-Windows-x64-signed-installer.msi",
+        "downloadSize": "116228096",
+        "sha256": "903343959992733001d4e7db30c83fc4ff8120903aeca0eb86b1601c9d21e15b"
+      }
+    },
+    {
+      "version": "2.2.4",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.4",
+      "download": {
+        "date": "2024-03-13T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.4-Windows-x64-signed-installer.msi",
+        "downloadSize": "116228096",
+        "sha256": "559cc71c0050d72706378997d9eb24c923953142abdba72a47d82b8887c29c9d"
+      }
+    },
+    {
+      "version": "2.2.3",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.3",
+      "download": {
+        "date": "2024-03-04T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.3-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "76f721e1ca52835ab074909e2044624363e5f63f0e1b8f6702bf2a857da59979"
+      }
+    },
+    {
+      "version": "2.2.2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.2",
+      "download": {
+        "date": "2024-02-10T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.2-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "f2ea2d2ea1b40607e1fc6fc22cac4f51630b66da4e8f1925f529396bb57e6010"
+      }
+    },
+    {
+      "version": "2.2.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.1",
+      "download": {
+        "date": "2024-01-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.1-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "193825d24745cd5a052ae57f1345b02924fc269aa69324428e7a177b9c58aa05"
+      }
+    },
+    {
+      "version": "2.2.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0",
+      "download": {
+        "date": "2024-01-22T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "92aeb6ba74fcb5cade48962aa4696a77fb7b2434622c22097cfa9da037b32fb3"
+      }
+    },
+    {
+      "version": "2.1.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0",
+      "download": {
+        "date": "2023-11-06T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.1.0-Windows-x64-signed-installer.msi",
+        "downloadSize": "108511232",
+        "sha256": "cd5b4735421a484bf83635f07653755e56b095c785f021eedaa4ca2d4132dd7f"
+      }
+    },
+    {
+      "version": "2.0.2",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.2",
+      "download": {
+        "date": "2023-09-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.2-Windows-x64-signed-installer.msi",
+        "downloadSize": "95850496",
+        "sha256": "6d06a9843f223fe95a76de838a442ce64050a1aeea7e64d096a496921e1711f1"
+      }
+    },
+    {
+      "version": "2.0.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.1",
+      "download": {
+        "date": "2023-08-30T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.1-Windows-x64-signed-installer.msi",
+        "downloadSize": "95846400",
+        "sha256": "aeb8934b7ef5ab274a135f575b13f16e1e93b42cfc631cfefa611477ae092c23"
+      }
+    },
+    {
+      "version": "2.0.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.0.0",
+      "download": {
+        "date": "2023-08-28T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.0.0-Windows-x64-signed-installer.msi",
+        "downloadSize": "95846400",
+        "sha256": "c815d992ff7be9a67e5c5b2823cfeda5d652efacf6463d1f9c7fdfb1561296f8"
+      }
+    },
+    {
+      "version": "1.10.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.1",
+      "download": {
+        "date": "2023-08-04T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.1-Windows-x64-signed-installer.msi",
+        "downloadSize": "46419968",
+        "sha256": "f458526ebbde294771ddc47d5aa0cf13ae5e2ba55f659e15f9bfe57a26ed78bf"
+      }
+    },
+    {
+      "version": "1.10.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.10.0",
+      "download": {
+        "date": "2023-06-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.10.0-Windows-x64-signed-installer.msi",
+        "downloadSize": "46432256",
+        "sha256": "6d08a83a8a2c153e0fdd43cdb913ebe244552f4f96d873ea4887e3d5be6d235e"
+      }
+    },
+    {
+      "version": "1.9.0",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.9.0",
+      "download": {
+        "date": "2023-05-12T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.9.0-Windows-x64-signed-installer.msi",
+        "downloadSize": "46383104",
+        "sha256": "ab73d244c04d7c5a6e553aa69dc989a2dcc519056bc2f7e99126af8a5bdd8f6f"
+      }
+    },
+    {
+      "version": "1.8.1",
+      "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v1.8.1",
+      "download": {
+        "date": "2023-04-03T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.8.1-Windows-x64-signed-installer.msi",
+        "downloadSize": "46456832",
+        "sha256": "55da405ea55a3567a672eef0f5f3699c264f456e13dd04d2a55d90bd3d1a2f55"
+      }
+    },
+    {
+      "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",
+      "download": {
+        "date": "2022-12-05T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.8-Windows-x64-installer.msi",
+        "downloadSize": 44539904,
+        "sha256": "e4ac16e7a66b4d656eea4e88f703fe156d656aedc16ad7f63ec570d82c27ed54"
+      }
+    },
+    {
+      "version": "1.6.7",
+      "changelog": "Updated Windows networking, enabling all audio devices on Windows, new default device behavior, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.7",
+      "download": {
+        "date": "2022-12-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.7-Windows-x64-installer.msi",
+        "downloadSize": 44535808,
+        "sha256": "75464575311da5521e011da8d2f2b5aff1379edb4dee9dcb90e1750dcc97f2f9"
+      }
+    },
+    {
+      "version": "1.6.6",
+      "changelog": "Adding volume controls in Virtual Studio, preliminary QT6 support, classic mode GUI updates, and fixes: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.6",
+      "download": {
+        "date": "2022-11-02T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.6-Windows-x64-installer.msi",
+        "downloadSize": 44408832,
+        "sha256": "828a1f43254db187a33601cc809551617db83e60a51dad3e992c30270967de0c"
+      }
+    },
+    {
+      "version": "1.6.4",
+      "changelog": "Adding volume meters, bugfixes around Windows hanging, device disconnects, and PLC: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.4",
+      "download": {
+        "date": "2022-09-16T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.4-Windows-x64-installer.msi",
+        "downloadSize": 43663360,
+        "sha256": "58dcbba584e1cc82373b8aa159800ad360eec7933ce9462e413ca347f09f3c26"
+      }
+    },
+    {
+      "version": "1.6.3",
+      "changelog": "Fixes around Linux desktop file and hub server mode: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.3",
+      "download": {
+        "date": "2022-08-23T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.3-Windows-x64-installer.msi",
+        "downloadSize": 43606016,
+        "sha256": "83a4def2a8c8fde24d147d39e70f60ee144e9425828f34eda2340c23ce72b1da"
+      }
+    },
+    {
+      "version": "1.6.2",
+      "changelog": "Ability to open app via URL schemes, displaying latency stats, and Virtual Studio device support. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.2",
+      "download": {
+        "date": "2022-08-17T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.2-Windows-x64-installer.msi",
+        "downloadSize": 43606016,
+        "sha256": "2bb06fe3624df447d56da0d72fef1f4328346fd92c6dffccf66ba37253ec5e62"
+      }
+    },
+    {
+      "version": "1.6.1",
+      "changelog": "Bugfixes around UDP timeout and 'Logging In' screen navigation. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.1",
+      "download": {
+        "date": "2022-06-21T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.1-Windows-x64-installer.msi",
+        "downloadSize": 43368448,
+        "sha256": "8eac390617488d849c0356e3305c96a59bbe46a8174d02b0321bb1dc86774b87"
+      }
+    },
+    {
+      "version": "1.6.0",
+      "changelog": "Full integration with JackTrip Virtual Studio. Learn more here: https://github.com/jacktrip/jacktrip/releases/tag/v1.6.0",
+      "download": {
+        "date": "2022-06-01T00:00:00Z",
+        "url": "https://files.jacktrip.org/app-builds/JackTrip-v1.6.0-Windows-x64-installer.msi",
+        "downloadSize": 43364352,
+        "sha256": "9562ab654202bfc432e05caa3bd2bf1d0b52c50581b0a567f0546983fe46c078"
+      }
+    }
+  ]
+}
diff --git a/rtaudio.pro b/rtaudio.pro
new file mode 100644 (file)
index 0000000..6934b19
--- /dev/null
@@ -0,0 +1,35 @@
+# created by Marcin Pączkowski 
+# configuration for building RtAudio library using qmake
+
+TEMPLATE = lib
+INCLUDEPATH += . externals/rtaudio
+
+CONFIG += debug_and_release staticlib
+
+# Input
+HEADERS += externals/rtaudio/RtAudio.h
+SOURCES += externals/rtaudio/RtAudio.cpp
+
+linux-g++ | linux-g++-64 {
+  QMAKE_CXXFLAGS += -D__LINUX_PULSE__ -D__LINUX_ALSA__
+}
+macx {
+  QMAKE_CXXFLAGS += -D__MACOSX_CORE__
+}
+win32 {
+  QMAKE_CXXFLAGS += -D__WINDOWS_ASIO__ -D__WINDOWS_WASAPI__
+  INCLUDEPATH += externals/rtaudio/include
+  HEADERS += externals/rtaudio/include/asio.h \
+             externals/rtaudio/include/asiodrivers.h \
+             externals/rtaudio/include/asiolist.h \
+             externals/rtaudio/include/asiodrvr.h \
+             externals/rtaudio/include/asiosys.h \
+             externals/rtaudio/include/ginclude.h \
+             externals/rtaudio/include/iasiodrv.h \
+             externals/rtaudio/include/iasiothiscallresolver.h \
+             externals/rtaudio/include/functiondiscoverykeys_devpkey.h
+  SOURCES += externals/rtaudio/include/asio.cpp \
+             externals/rtaudio/include/asiodrivers.cpp \
+             externals/rtaudio/include/asiolist.cpp \
+             externals/rtaudio/include/iasiothiscallresolver.cpp
+}
diff --git a/scripts/README.md b/scripts/README.md
new file mode 100644 (file)
index 0000000..df42ace
--- /dev/null
@@ -0,0 +1,12 @@
+# Scripts
+Unless otherwise noted, these scripts are written for Linux.
+
+## slork ##
+There are examples in these scripts of how to connect jack ports across system audio, jacktrip, and other programs.  Matt Wright wrote for them for the Stanford Laptop Orchestra (SLOrk).
+
+## test ##
+These test scripts can be run on a development branch or in CI.
+
+## utility ##
+Scripts for specific tasks such as recording all channels jack receives from applications like jacktrip.
+
diff --git a/scripts/hubMode/art.sh b/scripts/hubMode/art.sh
new file mode 100755 (executable)
index 0000000..a30239f
--- /dev/null
@@ -0,0 +1,36 @@
+#!/bin/bash
+# bash script for jacktrip automation, Chris Chafe
+# art.sh <FPP>
+
+FPP=$1
+
+echo calculate audio round trip
+rm /tmp/art.dat
+$(PIPEWIRE_LATENCY="$FPP/48000" jack_iodelay > /tmp/art.dat 2>&1 & )
+
+$(jmess -D)
+
+# correct in main not in dev -- jack_connect JackTrip:receive_1 JackTrip:send_1
+jack_connect __1:receive_1 __1:send_1
+jack_connect jack_delay:out HUBCLIENT:send_1
+jack_connect HUBCLIENT:receive_1 jack_delay:in
+
+sleep 8
+killall jack_iodelay
+
+killall jacktrip
+
+DEFAULTOUTPUT=-1
+AWKOUTPUT=$(grep total  /tmp/art.dat | \
+    awk '{ sum += $3; n++ } END { if (n > 0) print sum / n; }')
+printf -v AWKWARDINT %.0f "$AWKOUTPUT"
+if (($AWKWARDINT > 0 ))
+  then
+    echo $AWKOUTPUT
+    exit 0
+  else
+    echo $DEFAULTOUTPUT
+    exit 1
+fi
+
+
diff --git a/scripts/hubMode/startJacktripHubClient.sh b/scripts/hubMode/startJacktripHubClient.sh
new file mode 100755 (executable)
index 0000000..db495c1
--- /dev/null
@@ -0,0 +1,43 @@
+#!/bin/bash
+# bash script for jacktrip automation, Chris Chafe
+# startJacktripHubClient.sh <JACKTRIP> <FPP>
+# 
+# /home/cc/Desktop/sh/startJacktripHubClient.sh /home/cc/jacktrip/builddir/jacktrip %p
+
+JACKTRIP=$1
+
+### qjackctl start / stop example
+## qjackctl Setup : Options : Execute scrpt after Startup
+# /home/cc/Desktop/sh/startJacktripHubClient.sh %p
+## qjackctl Setup : Options : Execute scrpt on Shutdown
+# /home/cc/Desktop/sh/stopJacktrip.sh
+## timing of scrpt call means jacktrip is still running and 
+## an alert will appear for each new server it tries to shutdown
+## for both <scripts> do, chmod +x <script>.sh
+
+### manual start example
+# ./jacktrip -C jackloop128.stanford.edu
+
+### automatic client start in qjackctl : Setup : Options Execute script after Startup
+### examples with line used in qjackctl and corresponding script
+## specify full server name
+# /home/cc/startJacktrip.sh jackloop128.stanford.edu
+# SERVER=localhost
+
+## server name from file
+# /home/cc/startJacktrip.sh jackloop1024.stanford.edu
+# SERVER=$(cat ../../server.txt)
+
+## composed server name, %p = fpp
+# /home/cc/startJacktrip.sh %p
+
+
+FPP=$2
+SERVER=localhost
+
+echo starting hub client of a server running on $SERVER
+
+$(PIPEWIRE_LATENCY="$FPP/48000" $JACKTRIP -C $SERVER -J HUBCLIENT --udprt  --bufstrategy 3 --pktpool 3 -q1 > /dev/null 2>&1 & )
+
+
+
diff --git a/scripts/hubMode/startJacktripHubServer.sh b/scripts/hubMode/startJacktripHubServer.sh
new file mode 100755 (executable)
index 0000000..b394225
--- /dev/null
@@ -0,0 +1,14 @@
+#!/bin/bash
+# bash script for jacktrip automation, Chris Chafe
+# startJacktripHubServer.sh <JACKTRIP> <FPP>
+
+JACKTRIP=$1
+FPP=$2
+
+echo starting $JACKTRIP server
+
+$(PIPEWIRE_LATENCY="$FPP/48000" $JACKTRIP -S -p1 --udprt  --bufstrategy 3 --pktpool 3 -q1 > /dev/null 2>&1 & )
+
+
+
+
diff --git a/scripts/hubMode/test_hub_mode_server_and_client.sh b/scripts/hubMode/test_hub_mode_server_and_client.sh
new file mode 100755 (executable)
index 0000000..3c3fc1d
--- /dev/null
@@ -0,0 +1,71 @@
+#!/bin/bash
+# test_hub_mode_server_and_client.sh <JACKTRIP> <FPP> <JACKD>
+
+# bash script for automatic testing of jacktrip in hub mode, Chris Chafe
+# connects a hub client to a hub server (started with with -p1) on the same host
+
+# to run jackd automatically, set JACKD to anything (e.g., 1)
+#    for example, for CI testing
+# prints avg audio RTT after about 20 sec, or -1 if fail 
+
+# to not run jackd automatically, leave off the 3rd argument
+#     for example, if starting jackd manually with qjackctl or using 
+# prints avg audio RTT after 8 sec, or -1 if fail 
+
+# requires jack_iodelay be installed and available from system
+
+# # requires 3 helper scripts in the same directory
+# -- startJacktripHubServer.sh
+# -- startJacktripHubClient.sh
+# -- art.sh
+
+# example session:
+
+# [cc@localhost sh]$ ./test_hub_mode_server_and_client.sh /home/cc/jacktrip/builddir/jacktrip 32
+# starting /home/cc/jacktrip/builddir/jacktrip hub mode test at 32 FPP
+# starting hub client for server localhost
+# calculate audio round trip
+# 4
+
+# a failed test prints to the console
+# -1
+
+JACKTRIP=$1
+FPP=$2
+
+if [ -z "$3" ]
+  then
+    JACKD=0
+  else
+    JACKD=$3
+fi
+
+echo starting $JACKTRIP hub mode test at $FPP FPP with JACKD = $JACKD
+
+if [ $JACKD != 0 ]
+  then
+    echo starting JACKD
+# killall jackd
+    if [ "$(ps -aux | grep -c jackd)" != 1 ]; then killall jackd; fi;
+# if jackd is or has been running with another driver
+# much experimentation shows it literally takes this long
+    sleep 17
+# to flush old connections before starting the dummy driver
+
+# start jack with dummy driver, or change to an audio interface by switching these lines
+    $( /usr/bin/jackd  -ddummy -r48000 -p$FPP > /dev/null 2>&1 & )
+# $( /usr/bin/jackd  -dalsa -dhw:A96 -r48000 -p$FPP -n2  > /dev/null 2>&1 & )
+# $( /usr/bin/jackd  -dalsa -dhw:PCH -r48000 -p$FPP -n2  > /dev/null 2>&1 & )
+
+    sleep 1
+fi
+
+$PWD/startJacktripHubServer.sh $JACKTRIP $FPP
+sleep 1
+$PWD/startJacktripHubClient.sh $JACKTRIP $FPP
+
+sleep 1
+
+# start measuring audio RTT
+$PWD/art.sh
+
diff --git a/scripts/slork/die.bin b/scripts/slork/die.bin
new file mode 100644 (file)
index 0000000..cef5a40
--- /dev/null
@@ -0,0 +1,14 @@
+killall chuck; killall miniAudicle; killall audicle
+kill -9 `ps -ef | grep /Applications/Max.app | awk '{print $2}'`
+kill -9 `ps -ef | grep /Applications/FaceTime.app | awk '{print $2}'`
+kill -9 `ps -ef | grep /Applications/Wekinator.app | awk '{print $2}'`
+kill -9 `ps -ef | grep FaceOSC_Wekinator_14Inputs.app | awk '{print $2}'`
+kill -9 `ps -ef | grep /users/lja/vr2019 | awk '{print $2}'`
+
+killall jacktrip; killall jackd
+
+killall FaceOSC; killall python
+killall Preview
+
+audiodevice input "MOTU UltraLite mk3 Hybrid"
+audiodevice output "MOTU UltraLite mk3 Hybrid"
diff --git a/scripts/slork/feedjack-cleanslate.bin b/scripts/slork/feedjack-cleanslate.bin
new file mode 100644 (file)
index 0000000..aef317d
--- /dev/null
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+echo killing jacktrip...
+killall jacktrip
+
+echo killing jackd...
+killall jackd
+
+echo the slate is clean
diff --git a/scripts/slork/feedjack-client.bin b/scripts/slork/feedjack-client.bin
new file mode 100644 (file)
index 0000000..eabdfc4
--- /dev/null
@@ -0,0 +1,81 @@
+#!/bin/bash
+
+if test $# -ne 2; then
+    echo usage: $0 [client-number] [program]
+    echo  client-number is 1-12
+    echo  program is like a max patch or something
+
+    exit -1
+fi
+
+echo I hope you died
+echo Someday this script to check to see if Max or MiniAudicle or jacktrip.. are sunning...
+#die
+
+
+echo Launching Jack \(in the background\) and requiring MOTU
+feedjack-launchjack &
+
+if [ $? -eq 0 ]
+then
+    echo Jack seems to have launched OK.
+else
+    echo Had to set system to MOTU - please try again
+    exit 1
+fi
+
+# a little time for Jack to get settled
+sleep 2
+
+
+echo Launching Jacktrip client
+feedjack-launch-one-jacktrip-client $1 &
+
+# We can't check to see if it was successful because we have to launch
+# it in the background
+
+# if [ $? -eq 0 ]
+# then
+#    echo Jacktrip client seems to have launched OK.
+# else
+#    echo Could not launch jacktrip client - try again.
+#    exit 1
+# fi
+
+
+# Disconnect All - workaround to problem of jacktrip auto-connecting
+# to system audio.  We'll do feedjack-clientconnect later anyway.
+~/slork/bin/jmess-1.0.3 -D
+
+
+echo your program is \"$2\"
+
+# Now see what to launch on the client side
+
+if [ "$2" = "remotespeaker" ]
+then
+    sleep 10
+    echo This machine will just be a remote speaker
+    feedjack-remotespeakerconnect
+else
+
+    # should check to see whether Max is really needed...
+    echo launching max
+    open ~/slork/users/matt/max-choose-JackRouter.maxpat
+    
+    sleep 2
+
+    echo launching the program you asked for:
+    echo $2
+    open-in-dir $2
+
+
+
+    echo Waiting for Max and Jacktrip to finish initializing
+    sleep 10
+
+    echo trying feedjack-clientconnect - You may need to run it again
+    echo     feedjack-clientconnect
+
+    feedjack-clientconnect
+fi
diff --git a/scripts/slork/feedjack-clientconnect.bin b/scripts/slork/feedjack-clientconnect.bin
new file mode 100644 (file)
index 0000000..ee530f8
--- /dev/null
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+# Disconnect All
+~/slork/bin/jmess-1.0.3 -D
+
+for PROG in Max miniAudicle
+ do
+    # Injection from MOTU input 1 (on the front!) into ${PROG}
+    jack_connect system:capture_1 ${PROG}:in1
+
+    # Hear from jacktrip to ${PROG}
+    jack_connect JackTrip:receive_1 ${PROG}:in2
+
+    # ${PROG} 1-6 to hemi
+    jack_connect ${PROG}:out1 system:playback_1
+    jack_connect ${PROG}:out2 system:playback_2
+    jack_connect ${PROG}:out3 system:playback_3
+    jack_connect ${PROG}:out4 system:playback_4
+    jack_connect ${PROG}:out5 system:playback_5
+    jack_connect ${PROG}:out6 system:playback_6
+
+    # Pre-fader say from ${PROG} to MOTU
+    jack_connect ${PROG}:out7 system:playback_7
+
+    # ${PROG} 1-6 to say (for injection)
+    echo \(special workaround for Kunwoo and Avery\)
+    jack_connect ${PROG}:out1 JackTrip:send_1
+    jack_connect ${PROG}:out2 JackTrip:send_1
+    jack_connect ${PROG}:out3 JackTrip:send_1
+    jack_connect ${PROG}:out4 JackTrip:send_1
+    jack_connect ${PROG}:out5 JackTrip:send_1
+    jack_connect ${PROG}:out6 JackTrip:send_1
+
+done
+
+
+# post-fader say to jacktrip
+jack_connect system:capture_3 JackTrip:send_1
+
+# post-fader say to hemi
+jack_connect system:capture_3 system:playback_1
+jack_connect system:capture_3 system:playback_2
+jack_connect system:capture_3 system:playback_3
+jack_connect system:capture_3 system:playback_4
+jack_connect system:capture_3 system:playback_5
+jack_connect system:capture_3 system:playback_6
diff --git a/scripts/slork/feedjack-launch-n-jacktrip-servers.bin b/scripts/slork/feedjack-launch-n-jacktrip-servers.bin
new file mode 100644 (file)
index 0000000..80f0dcc
--- /dev/null
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+echo Killing any running instances of jacktrip...
+killall jacktrip
+
+OUTPUTFILEPREFIX="/tmp/jacktrip-server-output-"
+rm $OUTPUTFILEPREFIX*
+
+echo Turning off wifi - thanks Tucker
+networksetup -setairportpower en0 off # disable wifi
+
+
+echo Launching 12 jacktrip servers...
+for i in {1..12}
+ do
+ echo Launching jacktrip server $i
+ let i10=i*10
+ JACKTRIP_CMD="jacktrip -s -z -n 1 -o $i10 --clientname node-$i "
+ OUTPUTFILE="$OUTPUTFILEPREFIX$i"
+ echo "   " $JACKTRIP_CMD
+ $JACKTRIP_CMD </dev/null &> $OUTPUTFILE &
+ echo "   " Redirecting output \(stdout and stderr\) to $OUTPUTFILE
+
+ # Disconnect All - workaround to problem of jacktrip auto-connecting to system audio.  We'll do feedjack-serverconnect later anyway.
+ ~/slork/bin/jmess-1.0.3 -D
+
+ done
+
+LIST_CMD="ls -l $OUTPUTFILEPREFIX*"
+echo $LIST_CMD
+$LIST_CMD
+echo "ls -l $OUTPUTFILEPREFIX*"
+
+echo now do feedjack-serverconnect
+#feedjack-serverconnect
diff --git a/scripts/slork/feedjack-launch-one-jacktrip-client.bin b/scripts/slork/feedjack-launch-one-jacktrip-client.bin
new file mode 100644 (file)
index 0000000..440254b
--- /dev/null
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+if test $# -ne 1; then
+    echo usage: $0 [client-number]
+    echo where client-number is 1-12
+    exit -1
+fi
+
+echo Killing any running instances of jacktrip...
+killall jacktrip
+
+#serverHostname="sine.local"
+serverHostname="quinoa.local"
+
+echo looking for server \($serverHostname\)...
+serverIP="$(ping -q -c1 $serverHostname | head -1 | awk -F'[()]' '{print $2}')"
+
+# if [ $? -eq 0 && ! -z "$serverIP" ]
+if [ ! -z "$serverIP" ]
+then
+    echo Yay found $serverHostname at IP $serverIP
+else
+    echo Oh no - cannot see $serverHostname on the network
+    echo Please check the ethernet cables and try again.
+    exit 1
+fi
+
+Echo
+echo serverIP is $serverIP
+
+
+let clientnum=$1
+echo you asked to be client $clientnum
+
+let offset=$clientnum*10
+echo so your port offset should be $offset
+
+command="/Users/slork/slork/bin/jacktrip-1.1 -c $serverIP -n 1 -o $offset -z"
+echo $command
+$command
+
diff --git a/scripts/slork/feedjack-launchjack-even-without-motu.bin b/scripts/slork/feedjack-launchjack-even-without-motu.bin
new file mode 100644 (file)
index 0000000..7b065b1
--- /dev/null
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+echo Launching Jack...
+jackd -dcoreaudio -r44100 -p256
+echo ...Jack has stopped
diff --git a/scripts/slork/feedjack-launchjack.bin b/scripts/slork/feedjack-launchjack.bin
new file mode 100644 (file)
index 0000000..e908990
--- /dev/null
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+echo Checking whether system sound input is MOTU...
+audiodevice input 2> /dev/null | grep MOTU
+if [ $? -eq 0 ]
+then
+    echo Yay found MOTU input!
+else
+    echo System sound input is not MOTU:
+    audiodevice input 2> /dev/null 
+    echo Attempting to set to MOTU:
+    system-sound-to-motu
+    echo Now try again...
+    exit 1
+fi
+
+echo Checking whether system sound output is MOTU...
+audiodevice output 2> /dev/null | grep MOTU
+if [ $? -eq 0 ]
+then
+    echo Yay found MOTU output!
+else
+    echo System sound output is not MOTU:
+    audiodevice output 2> /dev/null 
+    echo Attempting to set to MOTU:
+    system-sound-to-motu
+    echo Now try again...
+    exit 2
+fi
+
+
+feedjack-launchjack-even-without-motu
diff --git a/scripts/slork/feedjack-remotespeakerconnect.bin b/scripts/slork/feedjack-remotespeakerconnect.bin
new file mode 100644 (file)
index 0000000..fbe16d0
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+# Disconnect All
+~/slork/bin/jmess-1.0.3 -D
+
+    # Hear from jacktrip to hemi
+    jack_connect JackTrip:receive_1 system:playback_1
+    jack_connect JackTrip:receive_1 system:playback_2
+    jack_connect JackTrip:receive_1 system:playback_3
+    jack_connect JackTrip:receive_1 system:playback_4
+    jack_connect JackTrip:receive_1 system:playback_5
+    jack_connect JackTrip:receive_1 system:playback_6
diff --git a/scripts/slork/feedjack-server.bin b/scripts/slork/feedjack-server.bin
new file mode 100644 (file)
index 0000000..93e507b
--- /dev/null
@@ -0,0 +1,31 @@
+#!/bin/bash
+
+echo die
+die
+
+
+echo Launching Jack even without MOTU \(in the background\)
+feedjack-launchjack-even-without-motu &
+
+# a little time for Jack to get settled
+sleep 10
+
+#echo Launching Jack \(in the background\)
+# feedjack-launchjack &
+
+
+echo launching max
+open ~/slork/users/matt/max-choose-JackRouter.maxpat
+open ~/matt/feedback/maxmsp/externals-OSX/shell.mxo 
+# should actually wait until shell finishes loading before opening the patch that uses it...
+open ~/matt/feedback/maxmsp/tune-instruments-gain.maxpat
+
+
+echo Launching Jacktrip servers
+feedjack-launch-n-jacktrip-servers
+
+
+
+echo now do:
+echo     feedjack-serverconnect
+#feedjack-serverconnect
diff --git a/scripts/slork/feedjack-serverconnect.bin b/scripts/slork/feedjack-serverconnect.bin
new file mode 100644 (file)
index 0000000..f3f75b8
--- /dev/null
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+# Disconnect All
+~/slork/bin/jmess-1.0.3 -D
+
+# All clients' say into Max
+jack_connect node-1:receive_1 Max:in1
+jack_connect node-2:receive_1 Max:in2
+jack_connect node-3:receive_1 Max:in3
+jack_connect node-4:receive_1 Max:in4
+jack_connect node-5:receive_1 Max:in5
+jack_connect node-6:receive_1 Max:in6
+jack_connect node-7:receive_1 Max:in7
+jack_connect node-8:receive_1 Max:in8
+jack_connect node-9:receive_1 Max:in9
+jack_connect node-10:receive_1 Max:in10
+jack_connect node-11:receive_1 Max:in11
+jack_connect node-12:receive_1 Max:in12
+
+
+# All clients' hear from Max
+jack_connect Max:out1 node-1:send_1
+jack_connect Max:out2 node-2:send_1
+jack_connect Max:out3 node-3:send_1
+jack_connect Max:out4 node-4:send_1
+jack_connect Max:out5 node-5:send_1
+jack_connect Max:out6 node-6:send_1
+jack_connect Max:out7 node-7:send_1
+jack_connect Max:out8 node-8:send_1
+jack_connect Max:out9 node-9:send_1
+jack_connect Max:out10 node-10:send_1
+jack_connect Max:out11 node-11:send_1
+jack_connect Max:out12 node-12:send_1
+
+# echo KLUDGE: also connecting Max output 6 to this machine\'s hemi
+# jack_connect Max:out6 system:playback_1
+# jack_connect Max:out6 system:playback_2
+# jack_connect Max:out6 system:playback_3
+# jack_connect Max:out6 system:playback_4
+# jack_connect Max:out6 system:playback_5
+# jack_connect Max:out6 system:playback_6
diff --git a/scripts/test/client_connection_test.sh b/scripts/test/client_connection_test.sh
new file mode 100755 (executable)
index 0000000..cbdb178
--- /dev/null
@@ -0,0 +1,44 @@
+#!/bin/bash
+#tests the jacktrip client connection to a hub server
+
+#kills existing jackd process, assuming current user has perms
+if pgrep -x "jackd"; then
+  echo 'stopping jackd'
+  command killall jackd
+  sleep 5
+fi
+
+#kills existing jacktrip processes, assuming current user has perms
+if pgrep -x "jacktrip"; then
+  echo 'stopping jacktrip'
+  command killall jacktrip
+  sleep 15
+fi
+
+#start jackd
+gnome-terminal -- command jackd -dalsa -dhw:0 -r48000 -p1024 -n2 > /dev/null 2>&1
+#command jacktrip -C 171.64.197.165 >> output.txt 2>&1 &
+sleep 5
+command jacktrip -C 171.64.197.165 > output.txt 2>&1 &
+sleep 5
+outputmsg=`cat output.txt`
+echo "$outputmsg"
+
+regExp='.*received connection.*'
+
+if [[ "${outputmsg,,}" =~ $regExp ]]; then
+  echo "Connection test passed!"
+else
+  echo "Connection test failed!"
+fi
+
+sleep 5
+if pgrep -x "jackd"; then
+  echo "killing jackd to clean up"
+  command killall jackd
+fi
+
+if pgrep -x "jacktrip"; then
+  echo "killing jacktrip to clean up"
+  command killall jacktrip
+fi
diff --git a/scripts/test/version_test.sh b/scripts/test/version_test.sh
new file mode 100755 (executable)
index 0000000..30c927b
--- /dev/null
@@ -0,0 +1,17 @@
+#!/bin/bash
+# tests the jacktrip version command
+
+OUTPUTSTRING=$(command jacktrip -v)
+
+#this regular expression match the word "version,"
+#an optional colon and space(s), followed by at least one number
+regExp='.*version\:?\s*([0-9]+).*'
+
+echo "${OUTPUTSTRING,,}"
+#converts the jacktrip -v command output to lower case
+#for case insensitive matching and applies the regular expression to it
+if [[ "${OUTPUTSTRING,,}" =~ $regExp ]]; then
+  echo "version test passed"
+else
+  echo "version test failed"
+fi
diff --git a/scripts/update_manifests.sh b/scripts/update_manifests.sh
new file mode 100755 (executable)
index 0000000..f76bf1b
--- /dev/null
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+EDGE_PLATFORMS="mac win"
+STABLE_PLATFORMS="$EDGE_PLATFORMS linux"
+
+function update_manifest {
+       NAME="$1/$2-manifests.json"
+       echo "Updating $NAME"
+       curl -s -o "releases/$NAME" "https://files.jacktrip.org/app-releases/$NAME"
+}
+
+for x in $EDGE_PLATFORMS
+do
+       update_manifest edge $x
+done
+
+for y in $STABLE_PLATFORMS
+do
+       update_manifest stable $y
+done
diff --git a/scripts/utility/generate_auth.sh b/scripts/utility/generate_auth.sh
new file mode 100755 (executable)
index 0000000..8bf3965
--- /dev/null
@@ -0,0 +1,18 @@
+#!/bin/bash
+echo "This script will create a private key, certificate and credentials file for use with a hub server."
+echo "(You need to have openssl installed to successfully run it.)"
+echo
+echo "When generating the SSL certificate, you should enter the hostname and domain of the machine that"
+echo "you intend to run the server on as the \"Common Name\""
+echo ""
+read -n1 -rsp "Press any key to start..."
+openssl req -x509 -sha256 -nodes -days 3650 -newkey rsa:2048 -keyout jacktrip.key -out jacktrip.crt
+echo ""
+echo "Key created as jacktrip.key. Certificate created as jacktrip.crt."
+echo ""
+echo "Creating auth file. The username and password entered here can be used to access the server with"
+echo "no time restrictions."
+read -p "Username: " USERNAME
+PASSWD=`openssl passwd -6`
+echo "$USERNAME:$PASSWD:*" > auth
+echo "Credentials written to auth"
diff --git a/scripts/utility/record_jack_receiving_ports.sh b/scripts/utility/record_jack_receiving_ports.sh
new file mode 100755 (executable)
index 0000000..ac4a2cc
--- /dev/null
@@ -0,0 +1,36 @@
+#!/bin/bash
+#use this script to record jacktrip receive & broadcast channels on ecasound
+#Dependency: Ecasound https://ecasound.seul.org/ecasound/README
+#Make sure you have ecasound installed in the recording environment (server or client)
+#Start JACK and JackTrip on the server or the client you want to record from
+#IMPORTANT: Wait till all clients have connected
+#cd to the folder where this script is located
+#Start the script from the terminal: ./record_jack_receiving_ports.sh
+#Press ESC to stop recording.
+#Recording should appear as a .wav file in the same folder as the script
+
+if ! pgrep -x "jackd" ; then
+  printf "Please start jack with a 48K sample rate before running this script."
+  exit
+fi
+
+IPs=($(jack_lsp | grep ':receive_*\|:broadcast_*' | cut -d: -f1))
+
+for IP in "${IPs[@]}"
+do
+  printf '%s\n' "IP: ${IP}"
+  NOW=$( date '+%F_%H:%M:%S' )
+  ecasound -f:,4,48000 -i jack,${IP} -o rec_${IP}_${NOW}_output.wav &
+done 
+
+echo "Press ESC key to stop recording"
+# read a single character
+while read -r -n1 key
+do
+# if input == ESC key
+  if [[ $key == $'\e' ]];then
+  kill $(ps aux | grep '[e]casound' | awk '{print $2}') 
+  break;
+  fi
+done
+
diff --git a/src/Analyzer.cpp b/src/Analyzer.cpp
new file mode 100644 (file)
index 0000000..a87dff3
--- /dev/null
@@ -0,0 +1,349 @@
+//*****************************************************************
+/*
+  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 Analyzer.cpp
+ * \author Dominick Hing
+ * \date May 2023
+ * \license MIT
+ */
+
+#include "Analyzer.h"
+
+#include <QMutexLocker>
+#include <iostream>
+
+#include "jacktrip_types.h"
+
+//*******************************************************************************
+Analyzer::Analyzer(int numchans, bool verboseFlag)
+    : mNumChannels(numchans), mCircularBufferPtr(nullptr)
+{
+    setVerbose(verboseFlag);
+
+    // size full spectra
+    mCurrentNorms.resize(mFftSize);
+    mCurrentSpectra.resize(mFftSize);
+
+    // allocate buffers for holding on to past spectra
+    int nPositiveFreqs    = 0.5 * mFftSize + 1;
+    mSpectra              = new float*[mNumSpectra];
+    mSpectraDifferentials = new float*[mNumSpectra];
+    for (int i = 0; i < mNumSpectra; i++) {
+        mSpectra[i]              = new float[nPositiveFreqs];
+        mSpectraDifferentials[i] = new float[nPositiveFreqs];
+    }
+}
+
+//*******************************************************************************
+Analyzer::~Analyzer()
+{
+    mTimer.stop();
+    for (int i = 0; i < mNumSpectra; i++) {
+        delete mSpectra[i];
+        delete mSpectraDifferentials[i];
+    }
+
+    if (mCircularBufferPtr != nullptr) {
+        delete mCircularBufferPtr;
+    }
+
+    delete mSpectra;
+    delete mSpectraDifferentials;
+}
+
+//*******************************************************************************
+void Analyzer::init(int samplingRate, int bufferSize)
+{
+    ProcessPlugin::init(samplingRate, bufferSize);
+    fs = float(fSamplingFreq);
+
+    mPushBuffer.resize(mBufferSize);
+    mCircularBufferPtr = new WaitFreeFrameBuffer<4096>(mBufferSize * sizeof(float));
+
+    /* Start timer */
+    connect(&mTimer, &QTimer::timeout, this, &Analyzer::onTick);
+    mTimer.setTimerType(Qt::PreciseTimer);
+    mTimer.setInterval(mInterval);
+    mTimer.setSingleShot(false);
+    mTimer.start();
+
+    inited = true;
+}
+
+//*******************************************************************************
+void Analyzer::compute(int nframes, float** inputs, float** outputs)
+{
+    if (!inited) {
+        std::cerr << "*** Analyzer " << this << ": init never called! Doing it now.\n";
+        init(0, 0);
+    }
+
+    // just a sanity check; should never happen
+    if (nframes > mBufferSize)
+        nframes = mBufferSize;
+
+    // sum up all channels and add it to the buffer
+    for (int i = 0; i < nframes; i++) {
+        mPushBuffer[i] = 0;
+        for (int ch = 0; ch < mNumChannels; ch++) {
+            if (!mIsMonitoringAnalyzer) {
+                mPushBuffer[i] += inputs[ch][i];
+            } else {
+                mPushBuffer[i] += outputs[ch][i];
+            }
+        }
+    }
+    int8_t* ptr = reinterpret_cast<int8_t*>(mPushBuffer.data());
+    mCircularBufferPtr->push(ptr);
+
+    hasProcessedAudio = true;
+}
+
+//*******************************************************************************
+void Analyzer::onTick()
+{
+    // cannot process audio if the no samples have been added to the ring buffer yet
+    if (!hasProcessedAudio) {
+        return;
+    }
+
+    const uint32_t buffers = uint32_t(mCircularBufferPtr->size());
+    const uint32_t samples = buffers * mBufferSize;
+
+    // require at least mFftSize values to process, otherwise return
+    if (samples < mFftSize) {
+        // shouldn't happen due to 48khz sample rate and timing config, but just in case
+        return;
+    }
+
+    mPullBuffer.resize(samples);
+    int8_t* pullPtr = reinterpret_cast<int8_t*>(mPullBuffer.data());
+    for (uint32_t i = 0; i < buffers; i++) {
+        mCircularBufferPtr->pop(pullPtr);
+        pullPtr += mCircularBufferPtr->getBytesPerFrame();
+    }
+
+    const char* err_str = NULL;
+    simple_fft::FFT(&mPullBuffer[mPullBuffer.size() - mFftSize - 1], mCurrentSpectra,
+                    mFftSize, err_str);
+    for (uint32_t i = 0; i < mFftSize; i++) {
+        mCurrentNorms[i] = norm(mCurrentSpectra[i]);
+    }
+
+    // update instance spectra and differentials buffers
+    updateSpectra();
+    updateSpectraDifferentials();
+
+    // check for audio feedback loops
+    bool detectedFeedback = checkForAudioFeedback();
+
+    // use mDetectionHistory to aggregate number of consecutive feedback triggers to help
+    // with false positives
+    if (detectedFeedback) {
+        mDetectionHistory++;
+    } else {
+        if (mDetectionHistory > 0) {
+            mDetectionHistory--;
+        }
+    }
+    if (mDetectionHistory > 1) {
+        emit signalFeedbackDetected();
+    }
+}
+
+//*******************************************************************************
+void Analyzer::updateSpectra()
+{
+    int nPositiveFreqs    = .5 * mFftSize + 1;
+    float* currentSpectra = mSpectra[0];
+    for (int i = 0; i < nPositiveFreqs; i++) {
+        currentSpectra[i] = mCurrentNorms[i];
+    }
+
+    // shift all buffers by 1 forward
+    for (int i = 0; i < mNumSpectra - 1; i++) {
+        mSpectra[i] = mSpectra[i + 1];
+    }
+    mSpectra[mNumSpectra - 1] = currentSpectra;
+}
+
+//*******************************************************************************
+void Analyzer::updateSpectraDifferentials()
+{
+    int nPositiveFreqs = .5 * mFftSize + 1;
+
+    // compute spectra differentials
+    for (int i = 0; i < nPositiveFreqs; i++) {
+        // set the first spectra differential to 0
+        mSpectraDifferentials[0][i] = 0;
+    }
+
+    for (int i = 1; i < mNumSpectra; i++) {
+        for (int j = 0; j < nPositiveFreqs; j++) {
+            mSpectraDifferentials[i][j] = mSpectra[i][j] - mSpectra[i - 1][j];
+        }
+    }
+}
+
+//*******************************************************************************
+bool Analyzer::checkForAudioFeedback()
+{
+    return testSpectralPeakAboveThreshold() && testSpectralPeakAbnormallyHigh()
+           && testSpectralPeakGrowing();
+}
+
+//*******************************************************************************
+bool Analyzer::testSpectralPeakAboveThreshold()
+{
+    // this test checks if the peak of the latest spectra is above a certain threshold
+
+    float* latestSpectra = mSpectra[mNumSpectra - 1];
+    int nPositiveFreqs   = .5 * mFftSize + 1;
+
+    // the exact threshold can be adjusted using the mThresholdMultiplier
+    // for a non-clipping signal, we can expect any value to be between 0 and N^2
+    // with N being the number of FFT channels
+    float threshold = 128 * 128 * mPeakThresholdMultipler;
+
+    float peak = 0.0f;
+    for (int i = 0; i < nPositiveFreqs; i++) {
+        if (latestSpectra[i] > peak) {
+            peak = latestSpectra[i];
+        }
+    }
+    return peak > threshold;
+}
+
+//*******************************************************************************
+bool Analyzer::testSpectralPeakAbnormallyHigh()
+{
+    // this test checks if the peak of the latest spectra is substantially higher than
+    // the other frequencies in the sample. As a heuristic we are checking if the peak is
+    // more than a few orders of magnitude above the median frequency - in other words if
+    // the peak / median exceeds a certain threshold
+
+    float* latestSpectra = mSpectra[mNumSpectra - 1];
+    int nPositiveFreqs   = .5 * mFftSize + 1;
+
+    std::vector<float> latestSpectraSorted;
+    for (int i = 0; i < nPositiveFreqs; i++) {
+        latestSpectraSorted.push_back(latestSpectra[i]);
+    }
+    std::sort(latestSpectraSorted.begin(), latestSpectraSorted.end(), std::less<float>());
+
+    float threshold = mPeakDeviationThresholdMultiplier * 100 * 100;
+
+    float peak = 0.0f;
+    for (int i = 0; i < nPositiveFreqs; i++) {
+        if (latestSpectra[i] > peak) {
+            peak = latestSpectra[i];
+        }
+    }
+
+    float median = latestSpectraSorted[(int)(nPositiveFreqs / 2)];
+
+    return peak / median > threshold;
+}
+
+//*******************************************************************************
+bool Analyzer::testSpectralPeakGrowing()
+{
+    // this test checks if the peak of the spectra has a history of growth over the last
+    // few samples. This likely indicates a positive feedback loop
+
+    float* latestSpectra = mSpectra[mNumSpectra - 1];
+    int nPositiveFreqs   = .5 * mFftSize + 1;
+
+    float peak    = 0.0f;
+    int peakIndex = 0;
+    for (int i = 0; i < nPositiveFreqs; i++) {
+        if (latestSpectra[i] > peak) {
+            peak      = latestSpectra[i];
+            peakIndex = i;
+        }
+    }
+
+    std::vector<float> valueVsTime;
+    std::vector<float> valueVsTimeSorted;
+    std::vector<float> differentials;
+    for (int i = 0; i < mNumSpectra; i++) {
+        valueVsTime.push_back(mSpectra[i][peakIndex]);
+        valueVsTimeSorted.push_back(mSpectra[i][peakIndex]);
+        differentials.push_back(mSpectraDifferentials[i][peakIndex]);
+    }
+    std::sort(valueVsTimeSorted.begin(), valueVsTimeSorted.end(), std::less<float>());
+
+    // test that the current value is the largest value
+    if (valueVsTimeSorted[mNumSpectra - 1] != valueVsTime[mNumSpectra - 1]) {
+        return false;
+    }
+
+    uint32_t numPositiveDifferentials = 0;
+    uint32_t numLargeDifferentials    = 0;
+    for (int i = 0; i < mNumSpectra; i++) {
+        if (differentials[i] > 0) {
+            numPositiveDifferentials++;
+        }
+
+        if (differentials[i] > 10 * 10 * mDifferentialThresholdMultiplier) {
+            numLargeDifferentials++;
+        }
+    }
+
+    if (numPositiveDifferentials == (uint32_t)mNumSpectra * (mNumSpectra * 0.8)
+        && numLargeDifferentials >= 1) {
+        return true;
+    }
+
+    if (numPositiveDifferentials >= (uint32_t)(mNumSpectra * 0.6)
+        && numLargeDifferentials >= 2) {
+        return true;
+    }
+
+    return false;
+}
+
+//*******************************************************************************
+void Analyzer::updateNumChannels(int nChansIn, int nChansOut)
+{
+    if (outgoingPluginToNetwork) {
+        mNumChannels = nChansIn;
+    } else {
+        mNumChannels = nChansOut;
+    }
+}
+
+//*******************************************************************************
+void Analyzer::setIsMonitoringAnalyzer(bool isMonitoringAnalyzer)
+{
+    mIsMonitoringAnalyzer = isMonitoringAnalyzer;
+}
\ No newline at end of file
diff --git a/src/Analyzer.h b/src/Analyzer.h
new file mode 100644 (file)
index 0000000..8c4e6fe
--- /dev/null
@@ -0,0 +1,125 @@
+//*****************************************************************
+/*
+  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 Analyzer.h
+ * \author Dominick Hing
+ * \date May 2023
+ * \license MIT
+ */
+
+#ifndef __ANALYZER_H__
+#define __ANALYZER_H__
+
+#include <QMutex>
+#include <QObject>
+#include <QTimer>
+#include <vector>
+
+#include "ProcessPlugin.h"
+#include "WaitFreeFrameBuffer.h"
+#include "externals/Simple-FFT/include/simple_fft/fft.h"
+#include "externals/Simple-FFT/include/simple_fft/fft_settings.h"
+
+typedef std::vector<real_type> RealArray1D;
+typedef std::vector<complex_type> ComplexArray1D;
+
+/** \brief The Analyzer plugin adjusts the level of the signal via multiplication
+ */
+class Analyzer : public ProcessPlugin
+{
+    Q_OBJECT;
+
+   public:
+    /// \brief The class constructor sets the number of channels to measure
+    Analyzer(int numchans, bool verboseFlag = false);
+
+    /// \brief The class destructor
+    virtual ~Analyzer();
+
+    void init(int samplingRate, int bufferSize) override;
+    int getNumInputs() override { return (mNumChannels); }
+    int getNumOutputs() override { return (mNumChannels); }
+    void compute(int nframes, float** inputs, float** outputs) override;
+    const char* getName() const override { return "Analyzer"; };
+
+    void updateNumChannels(int nChansIn, int nChansOut) override;
+    void setIsMonitoringAnalyzer(bool isMonitoringAnalyzer);
+
+   private:
+    void addFramesToQueue(int nframes, float* samples);
+    void resizeRingBuffer();
+    void onTick();
+    void updateSpectra();
+    void updateSpectraDifferentials();
+    bool checkForAudioFeedback();
+
+    bool testSpectralPeakAboveThreshold();
+    bool testSpectralPeakAbnormallyHigh();
+    bool testSpectralPeakGrowing();
+
+    int mInterval                           = 100;
+    float mPeakThresholdMultipler           = float(0.5);
+    float mPeakDeviationThresholdMultiplier = float(0.4);
+    float mDifferentialThresholdMultiplier  = float(0.05);
+
+    float fs;
+    int mNumChannels;
+    bool mIsMonitoringAnalyzer = false;
+    bool hasProcessedAudio     = false;
+    QTimer mTimer;
+
+    uint32_t mFftSize = 128;  // FFT size parameter
+
+    // ring buffer that doesn't require locking
+    WaitFreeFrameBuffer<4096>* mCircularBufferPtr;
+
+    // buffer used to push sums into circular buffer
+    std::vector<float> mPushBuffer;
+
+    // buffer used to pull sums from circular buffer
+    std::vector<float> mPullBuffer;
+
+    // buffers used to store current points of FFT
+    std::vector<complex_type> mCurrentSpectra;
+    std::vector<float> mCurrentNorms;
+
+    // mSpectra and mSpectra store a history of the spectral analyses
+    int mNumSpectra               = 10;
+    float** mSpectra              = nullptr;
+    float** mSpectraDifferentials = nullptr;
+    uint32_t mDetectionHistory    = 0;
+
+   signals:
+    void signalFeedbackDetected();
+};
+
+#endif
\ No newline at end of file
diff --git a/src/AudioInterface.cpp b/src/AudioInterface.cpp
new file mode 100644 (file)
index 0000000..918f696
--- /dev/null
@@ -0,0 +1,891 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008 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 AudioInterface.cpp
+ * \author Juan-Pablo Caceres
+ * \date July 2009
+ */
+
+#include "AudioInterface.h"
+
+#include <cassert>
+#include <cmath>
+#include <iostream>
+
+#include "JackTrip.h"
+
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+AudioInterface::AudioInterface(QVarLengthArray<int> InputChans,
+                               QVarLengthArray<int> OutputChans,
+                               inputMixModeT InputMixMode,
+#ifdef WAIR  // wair
+                               int NumNetRevChans,
+#endif  // endwhere
+                               audioBitResolutionT AudioBitResolution,
+                               bool processWithNetwork, JackTrip* jacktrip)
+    : mInputChans(InputChans)
+    , mOutputChans(OutputChans)
+    ,
+#ifdef WAIR  // WAIR
+    mNumNetRevChans(NumNetRevChans)
+    ,
+#endif  // endwhere
+    mAudioBitResolution(AudioBitResolution * 8)
+    , mBitResolutionMode(AudioBitResolution)
+    , mSampleRate(gDefaultSampleRate)
+    , mBufferSizeInSamples(gDefaultBufferSizeInSamples)
+    , mMonitorQueuePtr(NULL)
+    , mAudioInputPacket(NULL)
+    , mAudioOutputPacket(NULL)
+    , mLoopBack(false)
+    , mProcessWithNetwork(processWithNetwork)
+    , mMonitorStarted(false)
+    , mJackTrip(jacktrip)
+    , mInputMixMode(InputMixMode)
+    , mProcessingAudio(false)
+{
+}
+
+//*******************************************************************************
+AudioInterface::~AudioInterface()
+{
+    delete[] mAudioInputPacket;
+    delete[] mAudioOutputPacket;
+    for (int i = 0; i < mInProcessBuffer.size(); i++) {
+        delete[] mInProcessBuffer[i];
+    }
+    for (int i = 0; i < mOutProcessBuffer.size(); i++) {
+        delete[] mOutProcessBuffer[i];
+    }
+    delete mMonitorQueuePtr;
+#ifdef WAIR  // NOT WAIR:
+    for (int i = 0; i < mAPInBuffer.size(); i++) {
+        delete[] mAPInBuffer[i];
+    }
+#endif  // endwhere
+    for (auto* i : std::as_const(mProcessPluginsFromNetwork)) {
+        i->disconnect();
+        delete i;
+    }
+    for (auto* i : std::as_const(mProcessPluginsToNetwork)) {
+        i->disconnect();
+        delete i;
+    }
+    for (auto* i : std::as_const(mProcessPluginsToMonitor)) {
+        i->disconnect();
+        delete i;
+    }
+}
+
+//*******************************************************************************
+void AudioInterface::setup(bool /*verbose*/)
+{
+    // Allocate buffer memory to read and write
+    mSizeInBytesPerChannel = getSizeInBytesPerChannel();
+    int nframes            = getBufferSizeInSamples();
+    int size_audio_input   = int(mSizeInBytesPerChannel * mInputChans.size());
+    int size_audio_output  = int(mSizeInBytesPerChannel * mOutputChans.size());
+#ifdef WAIR               // WAIR
+    if (mNumNetRevChans)  // else don't change sizes
+    {
+        size_audio_input  = mSizeInBytesPerChannel * mNumNetRevChans;
+        size_audio_output = mSizeInBytesPerChannel * mNumNetRevChans;
+    }
+#endif  // endwhere
+    const size_t audioInputPacketSize = std::max<size_t>(
+        size_audio_input, mInputChans.size() * sizeof(sample_t) * nframes);
+    const size_t audioOutputPacketSize = std::max<size_t>(
+        size_audio_output, mOutputChans.size() * sizeof(sample_t) * nframes);
+    mAudioInputPacket  = new int8_t[audioInputPacketSize];
+    mAudioOutputPacket = new int8_t[audioOutputPacketSize];
+
+    // Initialize and assign memory for ProcessPlugins Buffers
+#ifdef WAIR  // WAIR
+    if (mNumNetRevChans) {
+        mInProcessBuffer.resize(mNumNetRevChans);
+        mOutProcessBuffer.resize(mNumNetRevChans);
+        mAPInBuffer.resize(mInputChans.size());
+        mNetInBuffer.resize(mNumNetRevChans);
+        for (int i = 0; i < mAPInBuffer.size(); i++) {
+            mAPInBuffer[i] = new sample_t[nframes];
+            // set memory to 0
+            std::memset(mAPInBuffer[i], 0, sizeof(sample_t) * nframes);
+        }
+        for (int i = 0; i < mNumNetRevChans; i++) {
+            mNetInBuffer[i] = new sample_t[nframes];
+            // set memory to 0
+            std::memset(mNetInBuffer[i], 0, sizeof(sample_t) * nframes);
+        }
+    } else  // don't change sizes
+#endif      // endwhere
+    {
+        mInProcessBuffer.resize(mInputChans.size());
+        mOutProcessBuffer.resize(mOutputChans.size());
+        mMonitorQueuePtr = new WaitFreeFrameBuffer<64>(audioInputPacketSize);
+    }
+
+    for (int i = 0; i < mInputChans.size(); 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 < mOutputChans.size(); i++) {
+        mOutProcessBuffer[i] = new sample_t[nframes];
+        // set memory to 0
+        std::memset(mOutProcessBuffer[i], 0, sizeof(sample_t) * nframes);
+    }
+}
+
+//*******************************************************************************
+size_t AudioInterface::getSizeInBytesPerChannel() const
+{
+    return (getBufferSizeInSamples() * getAudioBitResolution() / 8);
+}
+
+//*******************************************************************************
+void AudioInterface::callback(QVarLengthArray<sample_t*>& in_buffer,
+                              QVarLengthArray<sample_t*>& out_buffer,
+                              unsigned int n_frames)
+{
+    this->audioInputCallback(in_buffer, n_frames);
+    this->audioOutputCallback(out_buffer, n_frames);
+}
+
+//*******************************************************************************
+void AudioInterface::audioInputCallback(QVarLengthArray<sample_t*>& in_buffer,
+                                        unsigned int n_frames)
+{
+    // in_buffer is "in" from local audio hardware
+    if (getBufferSizeInSamples() < n_frames) {  // allocated in constructor above
+        std::cerr << "*** AudioInterface::audioInputCallback n_frames = " << n_frames
+                  << " larger than expected = " << getBufferSizeInSamples() << "\n";
+        exit(1);
+    }
+
+#ifndef WAIR
+    if (mMonitorQueuePtr != nullptr && mProcessPluginsToMonitor.size() > 0) {
+        // copy audio input to monitor queue
+        for (int i = 0; i < mInputChans.size(); i++) {
+            int8_t* sample_ptr = mAudioInputPacket + (i * sizeof(sample_t) * n_frames);
+            std::memcpy(sample_ptr, in_buffer[i], sizeof(sample_t) * n_frames);
+        }
+        mMonitorQueuePtr->push(mAudioInputPacket);
+    }
+#endif  // not WAIR
+
+    // process incoming signal from audio interface using process plugins
+    for (auto* p : std::as_const(mProcessPluginsToNetwork)) {
+        if (p->getInited()) {
+            p->compute(n_frames, in_buffer.data(), in_buffer.data());
+        }
+    }
+
+    // add audio testing impulse, if enabled
+    if (mAudioTesterP && mAudioTesterP->getEnabled()) {
+        mAudioTesterP->writeImpulse(
+            in_buffer,
+            n_frames);  // writes last channel of in_buffer with test impulse
+    }
+
+    // send the final signal to the network
+    if (mProcessWithNetwork) {
+        computeProcessToNetwork(in_buffer, n_frames);
+    }
+}
+
+//*******************************************************************************
+void AudioInterface::audioOutputCallback(QVarLengthArray<sample_t*>& out_buffer,
+                                         unsigned int n_frames)
+{
+    // in_buffer is "in" from local audio hardware
+    if (getBufferSizeInSamples() < n_frames) {  // allocated in constructor above
+        std::cerr << "*** AudioInterface::audioOutputCallback n_frames = " << n_frames
+                  << " larger than expected = " << getBufferSizeInSamples() << "\n";
+        exit(1);
+    }
+
+    // 1) First, process incoming packets
+
+#ifdef WAIR  // WAIR
+    //    qDebug() << "--" << mProcessPluginsFromNetwork.size();
+    bool client = (mProcessPluginsFromNetwork.size() == 2);
+#define COMBDSP 1  // client
+#define APDSP   0  // client
+#define DCBDSP  0  // server
+    for (int i = 0; i < mNetInBuffer.size(); i++) {
+        std::memset(mNetInBuffer[i], 0, sizeof(sample_t) * n_frames);
+    }
+#endif  // endwhere
+
+    // ==== RECEIVE AUDIO CHANNELS FROM NETWORK ====
+    // out_buffer is from the network and goes "out" to local audio hardware
+    if (mProcessWithNetwork) {
+        computeProcessFromNetwork(out_buffer, n_frames);
+    }
+    // =============================================
+
+    // mAudioTesterP will be nullptr for hub server's JackTripWorker instances
+    if (mAudioTesterP && mAudioTesterP->getEnabled()) {
+        mAudioTesterP->lookForReturnPulse(out_buffer, n_frames);
+    }
+
+    // apply process plugins to the signal
+    // -----------------------------------------------
+    // The processing will be done in order of allocation
+    /// \todo Implement for more than one process plugin, now it just works propertely
+    /// with one. do it chaining outputs to inputs in the buffers. May need a tempo buffer
+
+#ifndef WAIR  // NOT WAIR:
+    for (auto* p : std::as_const(mProcessPluginsFromNetwork)) {
+        if (p->getInited()) {
+            p->compute(n_frames, out_buffer.data(), out_buffer.data());
+        }
+    }
+
+    if (mMonitorQueuePtr != nullptr && mProcessPluginsToMonitor.size() > 0) {
+        // mix in the monitor signal
+        // note that using memory_order_acquire ensures all data written to the buffers
+        // will be also available be available to this thread before read
+        std::memset(mAudioOutputPacket, 0,
+                    sizeof(sample_t) * n_frames * getNumInputChannels());
+        if (mMonitorStarted) {
+            mMonitorQueuePtr->pop(mAudioOutputPacket);
+        } else {
+            // drain the monitor queue to minimize latency
+            while (mMonitorQueuePtr->pop(mAudioOutputPacket)) {}
+            mMonitorStarted = true;
+        }
+        for (int i = 0; i < getNumOutputChannels(); i++) {
+            // if using mix-to-mono, in_buffer[0] should already contain the mixed
+            // audio, so copy it to the monitor buffer. See RtAudioInterface.cpp
+
+            // likewise if using mono, we simply copy the input to every monitor
+            // channel
+            int8_t* sample_ptr = mAudioOutputPacket;
+            if (i > 0 && getNumInputChannels() > i
+                && mInputMixMode == AudioInterface::STEREO) {
+                // otherwise, copy each channel individually
+                sample_ptr += (i * sizeof(sample_t) * n_frames);
+            }
+            std::memcpy(mOutProcessBuffer[i], sample_ptr, sizeof(sample_t) * n_frames);
+        }
+        for (int i = 0; i < mProcessPluginsToMonitor.size(); i++) {
+            ProcessPlugin* p = mProcessPluginsToMonitor[i];
+            if (p->getInited()) {
+                // note: for monitor plugins, the output is out_buffer (to the speakers)
+                p->compute(n_frames, mOutProcessBuffer.data(), out_buffer.data());
+            }
+        }
+    }
+
+#else  // WAIR:
+    // nib16 result now in mNetInBuffer
+    int nChansIn  = mInputChans.size();
+    int nChansOut = mOutputChans.size();
+    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 : nChansIn); i++) {
+        std::memset(mInProcessBuffer[i], 0, sizeof(sample_t) * n_frames);
+        if (mNumNetRevChans) {
+            if (client)
+                std::memcpy(mInProcessBuffer[i], mNetInBuffer[i],
+                            sizeof(sample_t) * n_frames);
+            else
+                std::memcpy(mOutProcessBuffer[i], mNetInBuffer[i],
+                            sizeof(sample_t) * n_frames);
+        }
+    }
+    // nib16 to cib16
+
+    if (mNumNetRevChans && client) {
+        mProcessPluginsFromNetwork[COMBDSP]->compute(n_frames, mInProcessBuffer.data(),
+                                                     mOutProcessBuffer.data());
+    }
+    // compute cob16
+
+    // aib2 + cob16 to nob16
+
+    if (mNumNetRevChans)  // else not wair, so skip all this
+    {
+#define AP
+#ifndef AP
+        // straight to audio out
+        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 % nChansOut];
+            sample_t* tmp_sample = mNetInBuffer[i];  // mNetInBuffer
+            for (int j = 0; j < (int)n_frames; j++) {
+                mix_sample[j] += tmp_sample[j];
+            }
+        }  // nib6 to aob2
+#else  // AP
+
+        // 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 < 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 % 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 < nChansOut; i++) {
+            std::memset(out_buffer[i], 0, sizeof(sample_t) * n_frames);
+        }
+        mProcessPluginsFromNetwork[APDSP]->compute(n_frames, mAPInBuffer.data(),
+                                                   out_buffer.data());
+        // compute ap2 into aob2
+
+        // #define ADD_DIRECT
+#ifdef ADD_DIRECT
+        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++) {
+                mix_sample[j] += tmp_sample[j];
+            }
+        }
+        // add aib2 to aob2
+#endif  // ADD_DIRECT
+#endif  // AP
+    }
+#endif  // endwhere
+}
+
+//*******************************************************************************
+void AudioInterface::broadcastCallback(QVarLengthArray<sample_t*>& mon_buffer,
+                                       unsigned int n_frames)
+{
+    /// \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 < mOutputChans.size(); 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 * mOutputChans.size())
+                                    + (i * mBitResolutionMode)],
+                &tmp_sample[j], mBitResolutionMode);
+        }
+    }
+}
+
+//*******************************************************************************
+// Before sending and reading to Jack, we have to round to the sample resolution
+// that the program is using. Jack uses 32 bits (gJackBitResolution in globals.h)
+// by default
+void AudioInterface::computeProcessFromNetwork(QVarLengthArray<sample_t*>& out_buffer,
+                                               unsigned int n_frames)
+{
+    /// \todo cast *mInBuffer[i] to the bit resolution
+    // Output Process (from NETWORK to JACK)
+    // ----------------------------------------------------------------
+    // Read Audio buffer from RingBuffer (read from incoming packets)
+    mJackTrip->receiveNetworkPacket(mAudioOutputPacket);
+
+#ifdef WAIR  // WAIR
+    if (mNumNetRevChans)
+        // Extract separate channels
+        for (int i = 0; i < mNumNetRevChans; i++) {
+            sample_t* tmp_sample = mNetInBuffer[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)],
+                    &mOutputPacket[(j * mBitResolutionMode * mOutputChans.size())
+                                   + (i * mBitResolutionMode)],
+                    &tmp_sample[j], mBitResolutionMode);
+            }
+        }
+    else  // not wair
+#endif    // endwhere
+
+        // Extract separate channels to send to Jack
+        for (int i = 0; i < mOutputChans.size(); i++) {
+            //--------
+            // This should be faster for 32 bits
+            // std::memcpy(mOutBuffer[i], &mOutputPacket[i*mSizeInBytesPerChannel],
+            //         mSizeInBytesPerChannel);
+            //--------
+            sample_t* tmp_sample = out_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 * mOutputChans.size())
+                                        + (i * mBitResolutionMode)],
+                    &tmp_sample[j], mBitResolutionMode);
+            }
+        }
+}
+
+//*******************************************************************************
+void AudioInterface::computeProcessToNetwork(QVarLengthArray<sample_t*>& in_buffer,
+                                             unsigned int n_frames)
+{
+    const int nChansIn = (MIXTOMONO == mInputMixMode) ? 1 : mInputChans.size();
+    // Input Process (from JACK to NETWORK)
+    // ----------------------------------------------------------------
+    // Concatenate  all the channels from jack to form packet
+
+#ifdef WAIR  // WAIR
+    if (mNumNetRevChans)
+        for (int i = 0; i < mNumNetRevChans; i++) {
+            sample_t* tmp_sample =
+                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;
+            for (unsigned int j = 0; j < n_frames; j++) {
+                // Change the bit resolution on each sample
+                // Add the input jack buffer to the buffer resulting from the output
+                // process
+#define INGAIN \
+    (0.9999)  // 0.9999 because 1.0 can saturate the fixed pt rounding on output
+#define COMBGAIN (1.0)
+                tmp_result = INGAIN * tmp_sample[j] + COMBGAIN * tmp_process_sample[j];
+                fromSampleToBitConversion(
+                    &tmp_result,
+                    // use interleaved channel layout
+                    //&mInputPacket[(i*mSizeInBytesPerChannel) + (j*mBitResolutionMode)],
+                    &mInputPacket[(j * mBitResolutionMode * nChansOut)
+                                  + (i * mBitResolutionMode)],
+                    mBitResolutionMode);
+            }
+        }
+    else  // not wair
+#endif    // endwhere
+
+        for (int i = 0; i < nChansIn; i++) {
+            //--------
+            // This should be faster for 32 bits
+            // std::memcpy(&mInputPacket[i*mSizeInBytesPerChannel], mInBuffer[i],
+            //         mSizeInBytesPerChannel);
+            //--------
+            sample_t* tmp_sample = in_buffer[i];  // sample buffer for channel i
+            sample_t* tmp_process_sample =
+                mInProcessBuffer[i];  // sample buffer from the output process
+            sample_t tmp_result;
+            for (unsigned int j = 0; j < n_frames; j++) {
+                // Change the bit resolution on each sample
+                // Add the input jack buffer to the buffer resulting from the output
+                // process
+                tmp_result = tmp_sample[j] + tmp_process_sample[j];
+                fromSampleToBitConversion(
+                    &tmp_result,
+                    // use interleaved channel layout
+                    //&mInputPacket[(i*mSizeInBytesPerChannel) + (j*mBitResolutionMode)],
+                    &mAudioInputPacket[(j * mBitResolutionMode * nChansIn)
+                                       + (i * mBitResolutionMode)],
+                    mBitResolutionMode);
+            }
+        }
+    // Send Audio buffer to Network
+    mJackTrip->sendNetworkPacket(mAudioInputPacket);
+}  // /computeProcessToNetwork
+
+//*******************************************************************************
+// This function quantize from 32 bit to a lower bit resolution
+// 24 bit is not working yet
+void AudioInterface::fromSampleToBitConversion(
+    const sample_t* const input, int8_t* output,
+    const AudioInterface::audioBitResolutionT targetBitResolution)
+{
+    int8_t tmp_8;
+    uint8_t tmp_u8;  // unsigned to quantize the remainder in 24bits
+    int16_t tmp_16;
+    double tmp_sample;
+    sample_t tmp_sample16;
+    sample_t tmp_sample8;
+    switch (targetBitResolution) {
+    case BIT8:
+        // 8bit integer between -128 to 127
+        tmp_sample = std::max<double>(
+            -127.0, std::min<double>(127.0, std::round((*input) * 127.0)));  // 2^7 = 128
+        tmp_8 = static_cast<int8_t>(tmp_sample);
+        std::memcpy(output, &tmp_8, 1);  // 8bits = 1 bytes
+        break;
+    case BIT16:
+        // 16bit integer between -32768 to 32767
+        // original scaling: tmp_sample = floor( (*input) * 32768.0 ); // 2^15 = 32768.0
+        tmp_sample = std::max<double>(
+            -32767.0,
+            std::min<double>(32767.0, std::round((*input) * 32767.0)));  // 2^15 = 32768
+        tmp_16 = static_cast<int16_t>(tmp_sample);
+        std::memcpy(
+            output, &tmp_16,
+            2);  // 2 bytes output in Little Endian order (LSB -> smallest address)
+        break;
+    case BIT24:
+        // To convert to 24 bits, we first quantize the number to 16bit
+        tmp_sample   = (*input) * 32768.0;  // 2^15 = 32768.0
+        tmp_sample16 = floor(tmp_sample);
+        tmp_16       = static_cast<int16_t>(tmp_sample16);
+
+        // Then we compute the remainder error, and quantize that part into an 8bit number
+        // Note that this remainder is always positive, so we use an unsigned integer
+        tmp_sample8 = floor(
+            (tmp_sample - tmp_sample16)  // this is a positive number, between 0.0-1.0
+            * 256.0);
+        tmp_u8 = static_cast<uint8_t>(tmp_sample8);
+
+        // Finally, we copy the 16bit number in the first 2 bytes,
+        // and the 8bit number in the third bite
+        std::memcpy(output, &tmp_16, 2);      // 16bits = 2 bytes
+        std::memcpy(output + 2, &tmp_u8, 1);  // 8bits = 1 bytes
+        break;
+    case BIT32:
+        tmp_sample = *input;
+        // not necessary yet:
+        // tmp_sample = std::max<double>(-1.0, std::min<double>(1.0, tmp_sample));
+        std::memcpy(output, &tmp_sample, 4);  // 32bit = 4 bytes
+        break;
+    }
+}
+
+//*******************************************************************************
+void AudioInterface::fromBitToSampleConversion(
+    const int8_t* const input, sample_t* output,
+    const AudioInterface::audioBitResolutionT sourceBitResolution)
+{
+    int8_t tmp_8;
+    uint8_t tmp_u8;
+    int16_t tmp_16;
+    sample_t tmp_sample;
+    sample_t tmp_sample16;
+    sample_t tmp_sample8;
+    switch (sourceBitResolution) {
+    case BIT8:
+        tmp_8      = *input;
+        tmp_sample = static_cast<sample_t>(tmp_8) / 128.0;
+        std::memcpy(output, &tmp_sample, 4);  // 4 bytes
+        break;
+    case BIT16:
+        tmp_16     = *(reinterpret_cast<const int16_t*>(input));  // *((int16_t*) input);
+        tmp_sample = static_cast<sample_t>(tmp_16) / 32768.0;
+        std::memcpy(output, &tmp_sample, 4);  // 4 bytes
+        break;
+    case BIT24:
+        // We first extract the 16bit and 8bit number from the 3 bytes
+        tmp_16 = *(reinterpret_cast<const int16_t*>(input));
+        tmp_u8 = *(reinterpret_cast<const uint8_t*>(input + 2));
+
+        // Then we recover the number
+        tmp_sample16 = static_cast<sample_t>(tmp_16);
+        tmp_sample8  = static_cast<sample_t>(tmp_u8) / 256.0;
+        tmp_sample   = (tmp_sample16 + tmp_sample8) / 32768.0;
+        std::memcpy(output, &tmp_sample, 4);  // 4 bytes
+        break;
+    case BIT32:
+        std::memcpy(output, input, 4);  // 4 bytes
+        break;
+    }
+}
+
+//*******************************************************************************
+void AudioInterface::setPipewireLatency(unsigned int bufferSize, unsigned int sampleRate)
+{
+    if (bufferSize == 0 || sampleRate == 0)
+        return;
+#if defined(__unix__)
+    char latency_env[40];
+    sprintf(latency_env, "%d/%d", bufferSize, sampleRate);
+    setenv("PIPEWIRE_LATENCY", latency_env, 1);
+#endif
+}
+
+//*******************************************************************************
+void AudioInterface::appendProcessPluginToNetwork(ProcessPlugin* plugin)
+{
+    if (!plugin) {
+        return;
+    }
+
+    const int nChansIn = (MIXTOMONO == mInputMixMode) ? 1 : mInputChans.size();
+    int nTestChans     = (mAudioTesterP && mAudioTesterP->getEnabled()) ? 1 : 0;
+    int nPluginChans   = nChansIn - nTestChans;
+    assert(nTestChans == 0 || (mAudioTesterP->getSendChannel() == nChansIn - 1));
+    if (plugin->getNumInputs() < nPluginChans) {
+        std::cerr
+            << "*** AudioInterface.cpp: appendProcessPluginToNetwork: ProcessPlugin "
+            << typeid(plugin).name() << " REJECTED due to having "
+            << plugin->getNumInputs() << " inputs, while the audio to JACK needs "
+            << nPluginChans << " inputs\n";
+        return;
+    }
+    mProcessPluginsToNetwork.append(plugin);
+}
+
+void AudioInterface::appendProcessPluginFromNetwork(ProcessPlugin* plugin)
+{
+    if (!plugin) {
+        return;
+    }
+
+    int nTestChans   = (mAudioTesterP && mAudioTesterP->getEnabled()) ? 1 : 0;
+    int nPluginChans = mOutputChans.size() - nTestChans;
+    assert(nTestChans == 0
+           || (mAudioTesterP->getSendChannel() == mOutputChans.size() - 1));
+    if (plugin->getNumOutputs() > nPluginChans) {
+        std::cerr
+            << "*** AudioInterface.cpp: appendProcessPluginFromNetwork: ProcessPlugin "
+            << typeid(plugin).name() << " REJECTED due to having "
+            << plugin->getNumOutputs() << " inputs, while the JACK audio output requires "
+            << nPluginChans << " outputs\n";
+        return;
+    }
+    mProcessPluginsFromNetwork.append(plugin);
+}
+
+void AudioInterface::appendProcessPluginToMonitor(ProcessPlugin* plugin)
+{
+    if (!plugin) {
+        return;
+    }
+
+    const int nChansMon = getNumMonChannels();
+
+    if (plugin->getNumInputs() > nChansMon) {
+        std::cerr
+            << "*** AudioInterface.cpp: appendProcessPluginToMonitor: ProcessPlugin "
+            << typeid(plugin).name() << " REJECTED due to having "
+            << plugin->getNumInputs()
+            << " inputs, while the monitor audio input requires " << nChansMon
+            << " outputs\n";
+        return;
+    }
+
+    if (plugin->getNumOutputs() > nChansMon) {
+        std::cerr
+            << "*** AudioInterface.cpp: appendProcessPluginToMonitor: ProcessPlugin "
+            << typeid(plugin).name() << " REJECTED due to having "
+            << plugin->getNumOutputs()
+            << " inputs, while the monitor audio output requires " << nChansMon
+            << " outputs\n";
+        return;
+    }
+
+    mProcessPluginsToMonitor.append(plugin);
+}
+
+void AudioInterface::initPlugins(bool verbose)
+{
+    const int nChansIn  = (MIXTOMONO == mInputMixMode) ? 1 : mInputChans.size();
+    const int nChansOut = mOutputChans.size();
+    const int nChansMon = getNumMonChannels();
+    int nPlugins = mProcessPluginsFromNetwork.size() + mProcessPluginsToNetwork.size()
+                   + mProcessPluginsToMonitor.size();
+    if (nPlugins > 0) {
+        if (verbose) {
+            std::cout << "Initializing Faust plugins (have " << nPlugins
+                      << ") at sampling rate " << mSampleRate << "\n";
+        }
+
+        for (ProcessPlugin* plugin : std::as_const(mProcessPluginsFromNetwork)) {
+            plugin->setOutgoingToNetwork(false);
+            plugin->updateNumChannels(nChansIn, nChansOut);
+            plugin->init(mSampleRate, mBufferSizeInSamples);
+        }
+        for (ProcessPlugin* plugin : std::as_const(mProcessPluginsToNetwork)) {
+            plugin->setOutgoingToNetwork(true);
+            plugin->updateNumChannels(nChansIn, nChansOut);
+            plugin->init(mSampleRate, mBufferSizeInSamples);
+        }
+        for (ProcessPlugin* plugin : std::as_const(mProcessPluginsToMonitor)) {
+            plugin->setOutgoingToNetwork(false);
+            plugin->updateNumChannels(nChansMon, nChansMon);
+            plugin->init(mSampleRate, mBufferSizeInSamples);
+        }
+    }
+}
+
+//*******************************************************************************
+AudioInterface::samplingRateT AudioInterface::getSampleRateType() const
+{
+    int32_t rate = getSampleRate();
+
+    if (100 > qAbs(rate - 22050)) {
+        return AudioInterface::SR22;
+    } else if (100 > qAbs(rate - 32000)) {
+        return AudioInterface::SR32;
+    } else if (100 > qAbs(rate - 44100)) {
+        return AudioInterface::SR44;
+    } else if (100 > qAbs(rate - 48000)) {
+        return AudioInterface::SR48;
+    } else if (100 > qAbs(rate - 88200)) {
+        return AudioInterface::SR88;
+    } else if (100 > qAbs(rate - 96000)) {
+        return AudioInterface::SR96;
+    } else if (100 > qAbs(rate - 19200)) {
+        return AudioInterface::SR192;
+    }
+
+    return AudioInterface::UNDEF;
+}
+
+//*******************************************************************************
+int AudioInterface::getSampleRateFromType(samplingRateT rate_type)
+{
+    int sample_rate = 0;
+    switch (rate_type) {
+    case SR22:
+        sample_rate = 22050;
+        return sample_rate;
+        break;
+    case SR32:
+        sample_rate = 32000;
+        return sample_rate;
+        break;
+    case SR44:
+        sample_rate = 44100;
+        return sample_rate;
+        break;
+    case SR48:
+        sample_rate = 48000;
+        return sample_rate;
+        break;
+    case SR88:
+        sample_rate = 88200;
+        return sample_rate;
+        break;
+    case SR96:
+        sample_rate = 96000;
+        return sample_rate;
+        break;
+    case SR192:
+        sample_rate = 192000;
+        return sample_rate;
+        break;
+    default:
+        return sample_rate;
+        break;
+    }
+
+    return sample_rate;
+}
+
+//*******************************************************************************
+void AudioInterface::setDevicesWarningMsg(warningMessageT msg)
+{
+    switch (msg) {
+    case DEVICE_WARN_BUFFER_LATENCY:
+        mWarningMsg =
+            "The buffer size setting for your audio device will cause high latency "
+            "or audio delay. Use an audio device that supports small buffer sizes "
+            "to reduce audio delays. Click for more info.";
+        mWarningHelpUrl  = "https://support.jacktrip.com/recommended-audio-interfaces";
+        mHighLatencyFlag = true;
+        break;
+    case DEVICE_WARN_ASIO_LATENCY:
+        mWarningMsg =
+            "You audio device drivers may cause high latency or audio delay. Install "
+            "and use ASIO drivers provided by your device's manufacturer to reduce "
+            "audio delays. Click for more info.";
+        mWarningHelpUrl =
+            "https://support.jacktrip.com/troubleshooting-windows-drivers-and-asio";
+        mHighLatencyFlag = true;
+        break;
+    case DEVICE_WARN_ALSA_LATENCY:
+        mWarningMsg =
+            "You audio device drivers may cause high latency or audio delay. Use "
+            "JACK backend or Linux ALSA drivers to reduce audio delays.";
+        mWarningHelpUrl  = "";
+        mHighLatencyFlag = true;
+        break;
+    default:
+        mWarningMsg      = "";
+        mWarningHelpUrl  = "";
+        mHighLatencyFlag = false;
+        break;
+    }
+}
+
+//*******************************************************************************
+void AudioInterface::setDevicesErrorMsg(errorMessageT msg)
+{
+    mErrorMsg = msg;
+    switch (msg) {
+    case DEVICE_ERR_INCOMPATIBLE:
+        mErrorMsg =
+            "The two devices you have selected are not compatible. Please select a "
+            "different pair of devices.";
+#ifdef _WIN32
+        mErrorHelpUrl =
+            "https://support.jacktrip.com/troubleshooting-windows-drivers-and-asio";
+#else
+        mErrorHelpUrl = "";
+#endif
+        break;
+    case DEVICE_ERR_NO_INPUTS:
+        mErrorMsg     = "JackTrip couldn't find any input devices!";
+        mErrorHelpUrl = "";
+        break;
+    case DEVICE_ERR_NO_OUTPUTS:
+        mErrorMsg     = "JackTrip couldn't find any output devices!";
+        mErrorHelpUrl = "";
+        break;
+    case DEVICE_ERR_NO_DEVICES:
+        mErrorMsg     = "JackTrip couldn't find any audio devices!";
+        mErrorHelpUrl = "";
+        break;
+#ifdef _WIN32
+    case DEVICE_ERR_SAME_ASIO:
+        mErrorMsg =
+            "When using ASIO, please select the same device for your input and output.";
+        mErrorHelpUrl =
+            "https://support.jacktrip.com/troubleshooting-windows-drivers-and-asio";
+        break;
+#endif
+    default:
+        mErrorMsg     = "";
+        mErrorHelpUrl = "";
+        break;
+    }
+}
diff --git a/src/AudioInterface.h b/src/AudioInterface.h
new file mode 100644 (file)
index 0000000..6ed62be
--- /dev/null
@@ -0,0 +1,378 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008 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 AudioInterface.h
+ * \author Juan-Pablo Caceres
+ * \date July 2009
+ */
+
+#ifndef __AUDIOINTERFACE_H__
+#define __AUDIOINTERFACE_H__
+
+#include <QVarLengthArray>
+#include <QVector>
+#include <functional>
+
+#include "AudioTester.h"
+#include "ProcessPlugin.h"
+#include "WaitFreeFrameBuffer.h"
+#include "jacktrip_types.h"
+
+// Forward declarations
+class JackTrip;
+
+// using namespace JackTripNamespace;
+
+// callback function for audio interface errors
+typedef std::function<void(const std::string& errorText)> AudioErrorCallback;
+
+/** \brief Base Class that provides an interface with audio
+ */
+class AudioInterface
+{
+   public:
+    /// \brief Enum for Audio Resolution in bits
+    enum audioBitResolutionT {
+        BIT8  = 1,  ///< 8 bits
+        BIT16 = 2,  ///< 16 bits (default)
+        BIT24 = 3,  ///< 24 bits
+        BIT32 = 4   ///< 32 bits
+    };
+
+    /// \brief Sampling Rates supported by JACK
+    enum samplingRateT {
+        SR22,   ///<  22050 Hz
+        SR32,   ///<  32000 Hz
+        SR44,   ///<  44100 Hz
+        SR48,   ///<  48000 Hz
+        SR88,   ///<  88200 Hz
+        SR96,   ///<  96000 Hz
+        SR192,  ///< 192000 Hz
+        UNDEF   ///< Undefined
+    };
+
+    enum warningMessageT {
+        DEVICE_WARN_NONE,
+        DEVICE_WARN_BUFFER_LATENCY,
+        DEVICE_WARN_ASIO_LATENCY,
+        DEVICE_WARN_ALSA_LATENCY
+    };
+
+    enum errorMessageT {
+        DEVICE_ERR_NONE,
+        DEVICE_ERR_INCOMPATIBLE,
+        DEVICE_ERR_NO_INPUTS,
+        DEVICE_ERR_NO_OUTPUTS,
+        DEVICE_ERR_NO_DEVICES,
+#ifdef _WIN32
+        DEVICE_ERR_SAME_ASIO
+#endif
+    };
+
+    enum inputMixModeT : int {
+        MIX_UNSET = 0,
+        MONO      = 1,
+        STEREO    = 2,
+        MIXTOMONO = 3,
+    };
+
+    /** \brief The class constructor
+     * \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(
+        QVarLengthArray<int> InputChans, QVarLengthArray<int> OutputChans,
+        inputMixModeT InputMixMode,
+#ifdef WAIR  // wair
+        int NumNetRevChans,
+#endif  // endwhere
+        AudioInterface::audioBitResolutionT AudioBitResolution = AudioInterface::BIT16,
+        bool processWithNetwork = false, JackTrip* jacktrip = nullptr);
+
+    /// \brief The class destructor
+    virtual ~AudioInterface();
+
+    /** \brief Setup the client. This function should be called just before
+     *
+     * starting the audio processes, it will setup the audio client with
+     * the class parameters, like Sampling Rate,
+     * Packet Size, Bit Resolution, etc... Sub-classes should also call the parent
+     * method to ensure correct inizialization.
+     */
+    virtual void setup(bool verbose = true);
+
+    /// \brief Tell the audio server that we are ready to roll. The
+    /// process-callback will start running. This runs on its own thread.
+    /// \return 0 on success, otherwise a non-zero error code
+    virtual int startProcess() = 0;
+
+    /// \brief Stops the process-callback thread
+    /// \return 0 on success, otherwise a non-zero error code
+    virtual int stopProcess() = 0;
+
+    /** \brief Broadcast callback. Subclass should call this callback after
+     * obtaining the mon_buffer pointer.
+     *
+     * \param in_buffer Array of input audio samplers for each channel. The user
+     * is responsible to check that each channel has n_frames samplers
+     * \param in_buffer Array of output audio samplers for each channel. The user
+     * is responsible to check that each channel has n_frames samplers
+     */
+    virtual void broadcastCallback(QVarLengthArray<sample_t*>& mon_buffer,
+                                   unsigned int n_frames);
+
+    /** \brief Audio interface callback. Subclass should call this callback after
+     * obtaining the in_buffer and out_buffer pointers (for duplux mode).
+     *
+     * \param in_buffer Array of input audio samplers for each channel. The user
+     * is responsible to check that each channel has n_frames samplers
+     * \param out_buffer Array of output audio samplers for each channel. The user
+     * is responsible to check that each channel has n_frames samplers
+     */
+    virtual void callback(QVarLengthArray<sample_t*>& in_buffer,
+                          QVarLengthArray<sample_t*>& out_buffer, unsigned int n_frames);
+
+    /** \brief Audio input process callback. Subclass should call this callback
+     * after obtaining the in_buffer pointer (for input only).
+     *
+     * \param in_buffer Array of input audio samplers for each channel. The user
+     * is responsible to check that each channel has n_frames samplers
+     */
+    virtual void audioInputCallback(QVarLengthArray<sample_t*>& in_buffer,
+                                    unsigned int n_frames);
+
+    /** \brief Audio output process callback. Subclass should call this callback
+     * after obtaining the out_buffer pointer (for output only).
+     *
+     * \param out_buffer Array of output audio samplers for each channel. The user
+     * is responsible to check that each channel has n_frames samplers
+     */
+    virtual void audioOutputCallback(QVarLengthArray<sample_t*>& out_buffer,
+                                     unsigned int n_frames);
+
+    /** \brief appendProcessPluginToNetwork(): Append a ProcessPlugin for outgoing audio.
+     * The processing order equals order they were appended.
+     * This processing is in the JackTrip client before sending to the network.
+     * \param plugin a ProcessPlugin smart pointer. Create the object instance
+     * using something like:\n
+     * <tt>std::tr1::shared_ptr<ProcessPluginName> loopback(new ProcessPluginName);</tt>
+     */
+    virtual void appendProcessPluginToNetwork(ProcessPlugin* plugin);
+
+    /** \brief appendProcessPluginFromNetwork():
+     * Same as appendProcessPluginToNetwork() except that these plugins operate
+     * on the audio received from the network (typically from a JackTrip server).
+     * The complete processing chain then looks like this:
+     * audio -> JACK -> JackTrip client -> processPlugin to network
+     *               -> remote JackTrip server
+     *               -> JackTrip client -> processPlugin from network -> JACK -> audio
+     */
+    virtual void appendProcessPluginFromNetwork(ProcessPlugin* plugin);
+
+    /** \brief appendProcessPluginToMonitor():
+     * Appends plugins used for local monitoring
+     */
+    virtual void appendProcessPluginToMonitor(ProcessPlugin* plugin);
+
+    /** \brief initPlugins():
+     * Initialize all ProcessPlugin modules.
+     * The audio sampling rate (mSampleRate) must be set at this time.
+     */
+    void initPlugins(bool verbose = true);
+
+    virtual void connectDefaultPorts() = 0;
+
+    /** \brief Convert a 32bit number (sample_t) into one of the bit resolution
+     * supported (audioBitResolutionT).
+     *
+     * The result is stored in an int_8 array of the
+     * appropriate size to hold the value. The caller is responsible to allocate
+     * enough space to store the result.
+     */
+    static void fromSampleToBitConversion(
+        const sample_t* const input, int8_t* output,
+        const AudioInterface::audioBitResolutionT targetBitResolution);
+
+    /** \brief Convert a audioBitResolutionT bit resolution number into a
+     * 32bit number (sample_t)
+     *
+     * The result is stored in an sample_t array of the
+     * appropriate size to hold the value. The caller is responsible to allocate
+     * enough space to store the result.
+     */
+    static void fromBitToSampleConversion(
+        const int8_t* const input, sample_t* output,
+        const AudioInterface::audioBitResolutionT sourceBitResolution);
+
+    /** \brief Sets PIPEWIRE_LATENCY environment variable on unix */
+    static void setPipewireLatency(unsigned int bufferSize, unsigned int sampleRate);
+
+    //--------------SETTERS---------------------------------------------
+    virtual void setInputChannels(QVarLengthArray<int> inputChans)
+    {
+        mInputChans = inputChans;
+    }
+    virtual void setOutputChannels(QVarLengthArray<int> outputChans)
+    {
+        mOutputChans = outputChans;
+    }
+    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; }
+    virtual void setInputDevice(std::string device_name)
+    {
+        mInputDeviceName = device_name;
+    }
+    virtual void setOutputDevice(std::string device_name)
+    {
+        mOutputDeviceName = device_name;
+    }
+    virtual void setBufferSizeInSamples(uint32_t buf_size)
+    {
+        mBufferSizeInSamples = buf_size;
+    }
+    /// \brief Set Client Name to something different that the default (JackTrip)
+    virtual void setClientName(const QString& ClientName) = 0;
+    virtual void setLoopBack(bool b) { mLoopBack = b; }
+    virtual void enableBroadcastOutput() {}
+    virtual void setAudioTesterP(AudioTester* atp) { mAudioTesterP = atp; }
+    void setErrorCallback(AudioErrorCallback c) { mErrorCallback = c; }
+    //------------------------------------------------------------------
+
+    //--------------GETTERS---------------------------------------------
+    /// \brief Get Number of Input Channels
+    virtual int getNumInputChannels() const { return mInputChans.size(); }
+    /// \brief Get Number of Output Channels
+    virtual int getNumOutputChannels() const { return mOutputChans.size(); }
+    /// \brief Get Number of Monitor Channels
+    virtual int getNumMonChannels() const { return mOutputChans.size(); }
+    virtual QVarLengthArray<int> getInputChannels() const { return mInputChans; }
+    virtual QVarLengthArray<int> 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; }
+    virtual std::string getOutputDevice() const { return mOutputDeviceName; }
+    virtual size_t getSizeInBytesPerChannel() const;
+    /// \brief Get the Jack Server Sampling Rate, in samples/second
+    virtual uint32_t getSampleRate() const { return mSampleRate; }
+    /// \brief Get the Jack Server Sampling Rate Enum Type samplingRateT
+    /// \return  AudioInterface::samplingRateT enum type
+    virtual samplingRateT getSampleRateType() const;
+    /** \brief Get the Audio Bit Resolution, in bits
+     *
+     * This is one of the audioBitResolutionT set in construction
+     */
+    virtual int getAudioBitResolution() const { return mAudioBitResolution; }
+    /** \brief Helper function to get the sample rate (in Hz) for a
+     * JackAudioInterface::samplingRateT
+     * \param rate_type  JackAudioInterface::samplingRateT enum type
+     * \return Sample Rate in Hz
+     */
+    static int getSampleRateFromType(samplingRateT rate_type);
+    const std::string& getDevicesWarningMsg() const { return mWarningMsg; }
+    const std::string& getDevicesErrorMsg() const { return mErrorMsg; }
+    const std::string& getDevicesWarningHelpUrl() const { return mWarningHelpUrl; }
+    const std::string& getDevicesErrorHelpUrl() const { return mErrorHelpUrl; }
+    bool highLatencyBufferSize() const { return getBufferSizeInSamples() > 256; }
+    bool getHighLatencyFlag() const { return mHighLatencyFlag; }
+    //------------------------------------------------------------------
+
+   private:
+    /// \brief Compute the process to receive packets
+    void computeProcessFromNetwork(QVarLengthArray<sample_t*>& out_buffer,
+                                   unsigned int n_frames);
+    /// \brief Compute the process to send packets
+    void computeProcessToNetwork(QVarLengthArray<sample_t*>& in_buffer,
+                                 unsigned int n_frames);
+
+    QVarLengthArray<int> mInputChans;
+    QVarLengthArray<int> mOutputChans;
+#ifdef WAIR               // wair
+    int mNumNetRevChans;  ///<  Number of Network Audio Channels (net comb filters)
+    QVarLengthArray<sample_t*>
+        mNetInBuffer;  ///< Vector of Input buffers/channel read from net
+    QVarLengthArray<sample_t*>
+        mAPInBuffer;          ///< Vector of Input buffers/channel for AllPass input
+#endif                        // endwhere
+    int mAudioBitResolution;  ///< Bit resolution in audio samples
+    AudioInterface::audioBitResolutionT
+        mBitResolutionMode;  ///< Bit resolution (audioBitResolutionT) mode
+    uint32_t mSampleRate;    ///< Sampling Rate
+    uint32_t mDeviceID;      ///< RTAudio DeviceID
+    std::string mInputDeviceName, mOutputDeviceName;  ///< RTAudio device names
+    uint32_t mBufferSizeInSamples;                    ///< Buffer size in samples
+    size_t mSizeInBytesPerChannel;                    ///< Size in bytes per audio channel
+    QVector<ProcessPlugin*>
+        mProcessPluginsFromNetwork;  ///< Vector of ProcessPlugin<EM>s</EM>
+    QVector<ProcessPlugin*>
+        mProcessPluginsToNetwork;  ///< Vector of ProcessPlugin<EM>s</EM>
+    QVector<ProcessPlugin*>
+        mProcessPluginsToMonitor;  ///< Vector of ProcessPlugin<EM>s</EM>
+    QVarLengthArray<sample_t*>
+        mInProcessBuffer;  ///< Vector of Input buffers/channel for ProcessPlugin
+    QVarLengthArray<sample_t*>
+        mOutProcessBuffer;  ///< Vector of Output buffers/channel for ProcessPlugin
+    WaitFreeFrameBuffer<64>*
+        mMonitorQueuePtr;        //< Queue of audio frames from monitor signal
+    int8_t* mAudioInputPacket;   ///< Packet containing all the channels to read from the
+                                 ///< RingBuffer
+    int8_t* mAudioOutputPacket;  ///< Packet containing all the channels to send to the
+                                 ///< RingBuffer
+    bool mLoopBack;
+    bool mProcessWithNetwork;  ///< whether or not to send/receive data via the network
+    bool mMonitorStarted;      ///< True if we have started to consume monitor audio
+    AudioTester* mAudioTesterP{nullptr};
+
+   protected:
+    JackTrip* mJackTrip;          ///< JackTrip Mediator Class pointer
+    inputMixModeT mInputMixMode;  ///< Input mixing mode
+
+    void setDevicesWarningMsg(warningMessageT msg);
+    void setDevicesErrorMsg(errorMessageT msg);
+
+    bool mProcessingAudio;  ///< Set when processing an audio callback buffer pair
+    const uint32_t MAX_AUDIO_BUFFER_SIZE = 8192;
+
+    std::string mWarningMsg;
+    std::string mErrorMsg;
+    std::string mWarningHelpUrl;
+    std::string mErrorHelpUrl;
+    bool mHighLatencyFlag;
+    AudioErrorCallback mErrorCallback;
+};
+
+#endif  // __AUDIOINTERFACE_H__
diff --git a/src/AudioTester.cpp b/src/AudioTester.cpp
new file mode 100644 (file)
index 0000000..64e141e
--- /dev/null
@@ -0,0 +1,254 @@
+//*****************************************************************
+/*
+  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 AudioTester.cpp
+ * \author Julius Smith
+ * \license MIT
+ * \date Aug-Oct 2020
+ */
+
+#include "AudioTester.h"
+
+#include <cassert>
+
+// Called 1st in Audiointerface.cpp
+void AudioTester::lookForReturnPulse(QVarLengthArray<sample_t*>& out_buffer,
+                                     unsigned int n_frames)
+{
+    if (!enabled) {
+        std::cerr << "*** AudioTester.h: lookForReturnPulse: NOT ENABLED\n";
+        return;
+    }
+    if (impulsePending) {  // look for return impulse in channel sendChannel:
+        assert(sendChannel < out_buffer.size());
+        for (uint n = 0; n < n_frames; n++) {
+            float amp = out_buffer[sendChannel][n];
+            if (amp > 0.5 * ampCellHeight) {  // got something
+                int cellNum = getImpulseCellNum(out_buffer[sendChannel][n]);
+                if (cellNum != pendingCell) {  // not our impulse!
+                    std::cerr << "*** AudioTester.h: computeProcessFromNetwork: Received "
+                                 "pulse amplitude "
+                              << amp << " (cell " << cellNum
+                              << ") while looking for cell " << pendingCell << "\n";
+
+                    if (cellNum > pendingCell) {  // we missed it
+                        std::cerr << " - ABORTING CURRENT PULSE\n";
+                        impulsePending = false;
+                    } else {  // somehow we got the previous pulse again - repeated packet
+                              // or underrun-caused repetition (old buffer)
+                        std::cerr << " - IGNORING FOUND PULSE WAITING FURTHER\n";
+                    }
+                } else {  // found our impulse:
+                    int64_t elapsedSamples = -1;
+                    if (n >= n_frames - 1) {
+                        // Impulse timestamp didn't make it so we skip this one.
+                    } else {
+                        float sampleCountWhenImpulseSent =
+                            -32768.0f * out_buffer[sendChannel][n + 1];
+                        elapsedSamples = sampleCountSinceImpulse + n
+                                         - int64_t(sampleCountWhenImpulseSent);
+                        sampleCountSinceImpulse =
+                            1;  // reset sample counter between impulses
+                        roundTripCount += 1.0;
+                    }
+                    // int64_t curTimeUS = timeMicroSec(); // time since launch in us
+                    // int64_t impulseDelayUS = curTimeUS - ImpulseTimeUS;
+                    // float impulseDelaySec = float(impulseDelayUS) * 1.0e-6;
+                    // float impulseDelayBuffers = impulseDelaySec /
+                    // (float(n_frames)/float(sampleRate)); int64_t impulseDelayMS =
+                    // (int64_t)round(double(impulseDelayUS)/1000.0);
+                    if (elapsedSamples
+                        > 0) {  // found impulse and reset, time to print buffer results:
+                        double elapsedSamplesMS =
+                            1000.0 * double(elapsedSamples) / double(sampleRate);  // ms
+                        extendLatencyHistogram(elapsedSamplesMS);
+                        if (roundTripCount > 1.0) {
+                            double prevSum =
+                                roundTripMean
+                                * (roundTripCount - 1.0);  // undo previous normalization
+                            roundTripMean =
+                                (prevSum + elapsedSamplesMS)
+                                / roundTripCount;  // add latest and renormalize
+                            double prevSumSq =
+                                roundTripMeanSquare
+                                * (roundTripCount - 1.0);  // undo previous normalization
+                            roundTripMeanSquare =
+                                (prevSumSq + elapsedSamplesMS * elapsedSamplesMS)
+                                / roundTripCount;
+                        } else {  // just getting started:
+                            roundTripMean       = elapsedSamplesMS;
+                            roundTripMeanSquare = elapsedSamplesMS * elapsedSamplesMS;
+                        }
+                        if (roundTripCount == 1.0) {
+                            printf(
+                                "JackTrip Test Mode (option -x "
+                                "printIntervalInSeconds=%0.3f)\n",
+                                printIntervalSec);
+                            printf(
+                                "\tA test impulse-train is output on channel %d (from 0) "
+                                "with repeatedly ramping amplitude\n",
+                                sendChannel);
+                            if (printIntervalSec == 0.0) {
+                                printf(
+                                    "\tPrinting each audio buffer round-trip latency in "
+                                    "ms followed by cumulative (mean and [standard "
+                                    "deviation])");
+                            } else {
+                                printf(
+                                    "\tPrinting cumulative mean and [standard deviation] "
+                                    "of audio round-trip latency in ms");
+                                printf(" every %0.3f seconds", printIntervalSec);
+                            }
+                            printf(" after skipping first %d buffers:\n",
+                                   bufferSkipStart);
+                            // not printing this presently: printf("( * means buffer
+                            // skipped due missing timestamp or lost impulse)\n");
+                            lastPrintTimeUS = timeMicroSec();
+                        }
+                        // printf("%d (%d) ", elapsedSamplesMS, impulseDelayMS); //
+                        // measured time is "buffer time" not sample time
+                        int64_t curTimeUS = timeMicroSec();  // time since launch in us
+                        double timeSinceLastPrintUS = double(curTimeUS - lastPrintTimeUS);
+                        double stdDev               = sqrt(std::max<double>(
+                            0.0,
+                            (roundTripMeanSquare - (roundTripMean * roundTripMean))));
+                        if (timeSinceLastPrintUS >= printIntervalSec * 1.0e6) {
+                            if (printIntervalSec == 0.0) {
+                                printf("%0.1f (", elapsedSamplesMS);
+                            }
+                            printf("%0.1f [%0.1f]", roundTripMean, stdDev);
+                            if (printIntervalSec == 0.0) {
+                                printf(") ");
+                            } else {
+                                printf(" ");
+                            }
+                            lastPrintTimeUS = curTimeUS;
+                            if (printIntervalSec >= 1.0) {  // print histogram
+                                std::cout << "\n" << getLatencyHistogramString() << "\n";
+                            }
+                        }
+                        std::cout << std::flush;
+                    } else {
+                        // not printing this presently: printf("* "); // we got the
+                        // impulse but lost its timestamp in samples
+                    }
+                    impulsePending = false;
+                }  // found our impulse
+                // remain pending until timeout, hoping to find our return pulse
+            }  // got something
+        }      // loop over samples
+        sampleCountSinceImpulse +=
+            n_frames;  // gets reset to 1 when impulse is found, counts freely until then
+    }                  // ImpulsePending
+}
+
+// Called 2nd in Audiointerface.cpp
+void AudioTester::writeImpulse(QVarLengthArray<sample_t*>& mInBufCopy,
+                               unsigned int n_frames)
+{
+    if (!enabled) {
+        std::cerr << "*** AudioTester.h: writeImpulse: NOT ENABLED\n";
+        return;
+    }
+    if (bufferSkip <= 0) {  // send test signals (-x option)
+        bool sendImpulse;
+        if (impulsePending) {
+            sendImpulse            = false;  // unless:
+            const uint64_t timeOut = 500e3;  // time out after waiting 500 ms
+            if (timeMicroSec() > (impulseTimeUS + timeOut)) {
+                sendImpulse = true;
+                std::cout << "\n*** Audio Latency Test (-x): TIMED OUT waiting for "
+                             "return impulse *** sending a new one\n";
+            }
+        } else {  // time for the next repeating impulse:
+            sendImpulse = true;
+        }
+        if (sendImpulse) {
+            assert(sendChannel < mInBufCopy.size());
+            mInBufCopy[sendChannel][0] = getImpulseAmp();
+            for (uint n = 1; n < n_frames; n++) {
+                mInBufCopy[sendChannel][n] = 0;
+            }
+            impulsePending     = true;
+            impulseTimeUS      = timeMicroSec();
+            impulseTimeSamples = sampleCountSinceImpulse;  // timer in samples for current
+                                                           // impulse loopback test
+            // Also send impulse time:
+            if (n_frames > 1) {  // always true?
+                mInBufCopy[sendChannel][1] =
+                    -float(impulseTimeSamples)
+                    / 32768.0f;  // survives if there is no digital processing at the
+                                 // server
+            } else {
+                std::cerr << "\n*** AudioTester.h: Timestamp cannot fit into a length "
+                          << n_frames << " buffer ***\n";
+            }
+        } else {
+            mInBufCopy[sendChannel][0] =
+                0.0f;  // send zeros until a new impulse is needed
+            if (n_frames > 1) {
+                mInBufCopy[sendChannel][1] = 0.0f;
+            }
+        }
+    } else {
+        bufferSkip--;
+    }
+}
+
+void AudioTester::printHelp(char* command, [[maybe_unused]] char helpCase)
+{
+    std::cout << "HELP for \"" << command
+              << " printIntervalSec\" // (end-of-line comments start with `//'):\n";
+    std::cout << "\n";
+    std::cout << "Print roundtrip audio delay statistics for the highest-numbered audio "
+                 "channel every printIntervalSec seconds,\n";
+    std::cout
+        << "including an ASCII latency histogram if printIntervalSec is 1.0 or more.\n";
+    std::cout << "\n";
+    std::cout << "A test impulse is sent to the server in the last audio channel,\n";
+    std::cout
+        << "  the number of samples until it returns is measured, and this repeats.\n";
+    std::cout << "The jacktrip server must provide audio loopback (e.g., -p4).\n";
+    std::cout << "The cumulative mean and standard-deviation (\"statistics\") are "
+                 "computed for the measured loopback times,\n";
+    std::cout << "  and printed every printIntervalSec seconds.\n";
+    std::cout << "If printIntervalSec is zero, the roundtrip-time and statistics in "
+                 "milliseconds are printed for each individual impulse.\n";
+    std::cout << "If printIntervalSec is positive, statistics are printed after each "
+                 "print interval, with no individual measurements.\n";
+    std::cout << "If printIntervalSec is 1.0 or larger, a cumulative histogram of all "
+                 "impulse roundtrip-times is printed as well.\n";
+    std::cout << "The first 100 audio buffers are skipped in order to measure only "
+                 "steady-state network-audio-delay performance.\n";
+    std::cout << "Lower audio channels are not affected, enabling latency measurement "
+                 "and display during normal operation.\n";
+}
diff --git a/src/AudioTester.h b/src/AudioTester.h
new file mode 100644 (file)
index 0000000..0d31be1
--- /dev/null
@@ -0,0 +1,232 @@
+//*****************************************************************
+/*
+  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 AudioTester.h
+ * \author Julius Smith
+ * \license MIT
+ * \date Aug-Oct 2020
+ */
+
+#pragma once
+
+#include <iostream>
+
+#include "jacktrip_types.h"  // sample_t
+// #include <ctime>
+#include <QVarLengthArray>
+#include <chrono>
+#include <cmath>
+#include <cstdint>
+#include <map>
+#include <string>
+
+class AudioTester
+{
+    bool enabled{false};
+    float printIntervalSec{1.0f};
+    int sendChannel{0};
+
+    bool impulsePending{false};
+    int64_t lastPrintTimeUS{0};
+    int64_t impulseTimeUS{0};
+    int64_t impulseTimeSamples{0};
+    uint64_t sampleCountSinceImpulse{1};  // 0 not used
+    double roundTripMean{0.0};
+    double roundTripMeanSquare{0.0};
+    double roundTripCount{0.0};
+    const int bufferSkipStart{100};
+    int bufferSkip{bufferSkipStart};
+    const float impulseAmplitude{0.1f};
+    const int numAmpCells{10};
+    const float ampCellHeight{impulseAmplitude / numAmpCells};
+
+    const double latencyHistogramCellWidth{5.0};  // latency range in ms covered one cell
+    const double latencyHistogramCellMin{0.0};
+    const double latencyHistogramCellMax{
+        19.0};  // in cells, so 5x this is max latency in ms
+    const int latencyHistogramPrintCountMax{
+        72};  // normalize when asterisks exceed this number
+
+    int pendingCell{0};  // 0 is not used
+    float sampleRate{48000.0f};
+
+   public:
+    AudioTester() {}
+    ~AudioTester() = default;
+
+    void lookForReturnPulse(QVarLengthArray<sample_t*>& out_buffer,
+                            unsigned int n_frames);
+
+    void writeImpulse(QVarLengthArray<sample_t*>& mInBufCopy, unsigned int n_frames);
+
+    bool getEnabled() { return enabled; }
+    void setEnabled(bool e) { enabled = e; }
+    void setPrintIntervalSec(float s) { printIntervalSec = s; }
+    void setSendChannel(int c) { sendChannel = c; }
+    int getSendChannel() { return sendChannel; }
+    int getPendingCell() { return pendingCell; }
+    void setPendingCell(int pc) { pendingCell = pc; }
+    void setSampleRate(float fs) { sampleRate = fs; }
+    int getBufferSkip() { return bufferSkip; }  // used for debugging breakpoints
+    void printHelp(char* command, char helpCase);
+
+   private:
+    float getImpulseAmp()
+    {
+        pendingCell += 1;  // only called when no impulse is pending
+        if (pendingCell >= numAmpCells) {
+            pendingCell = 1;  // wrap-around, not using zero
+        }
+        float imp = float(pendingCell) * (impulseAmplitude / float(numAmpCells));
+        return imp;
+    }
+
+    int getImpulseCellNum(float amp)
+    {
+        float ch   = ampCellHeight;
+        float cell = amp / ch;
+        int iCell  = int(std::floor(0.5f + cell));
+        if (iCell > numAmpCells - 1) {
+            std::cerr << "*** AudioTester.h: getImpulseCellNum(" << amp
+                      << "): Return pulse amplitude is beyond maximum expected\n";
+            iCell = numAmpCells - 1;
+        } else if (iCell < 0) {
+            std::cerr << "*** AudioTester.h: getImpulseCellNum(" << amp
+                      << "): Return pulse amplitude is below minimum expected\n";
+            iCell = 0;
+        }
+        return iCell;
+    }
+
+    uint64_t timeMicroSec()
+    {
+#if 1
+        using namespace std::chrono;
+        // return
+        // duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
+        return duration_cast<microseconds>(
+                   high_resolution_clock::now().time_since_epoch())
+            .count();
+#else
+        clock_t tics_since_launch = std::clock();
+        double timeUS             = double(tics_since_launch) / double(CLOCKS_PER_SEC);
+        return (uint64_t)timeUS;
+#endif
+    }
+
+    std::map<int, int> latencyHistogram;
+
+    std::map<int, int> getLatencyHistogram() { return latencyHistogram; }
+
+    void extendLatencyHistogram(double latencyMS)
+    {
+        int latencyCell = static_cast<int>(floor(std::max<double>(
+            latencyHistogramCellMin,
+            std::min<double>(latencyHistogramCellMax,
+                             std::floor(latencyMS / latencyHistogramCellWidth)))));
+        latencyHistogram[latencyCell] += 1;
+    }
+
+    int latencyHistogramCountMax()
+    {
+        int lhMax     = 0;
+        int histStart = latencyHistogramFirstNonzeroCellIndex();
+        int histLast  = latencyHistogramLastNonzeroCellIndex();
+        for (int i = histStart; i <= histLast; ++i) {
+            int lhi = latencyHistogram[i];
+            if (lhi > lhMax) {
+                lhMax = lhi;
+            }
+        }
+        return lhMax;
+    }
+
+    int latencyHistogramFirstNonzeroCellIndex()
+    {
+        for (int i = latencyHistogramCellMin; i <= latencyHistogramCellMax; i++) {
+            if (latencyHistogram[i] > 0) {
+                return i;
+            }
+        }
+        std::cerr << "*** AudioTester: LATENCY HISTOGRAM IS EMPTY!\n";
+        return -1;
+    }
+
+    int latencyHistogramLastNonzeroCellIndex()
+    {
+        for (int i = latencyHistogramCellMax; i >= latencyHistogramCellMin; i--) {
+            if (latencyHistogram[i] > 0) {
+                return i;
+            }
+        }
+        std::cerr << "*** AudioTester: LATENCY HISTOGRAM IS EMPTY!\n";
+        return -1;
+    }
+
+    std::string getLatencyHistogramString()
+    {
+        int histStart      = latencyHistogramFirstNonzeroCellIndex();
+        int histLast       = latencyHistogramLastNonzeroCellIndex();
+        std::string marker = "*";
+        double histScale   = 1.0;
+        int lhcm           = latencyHistogramCountMax();
+        int lhpcm          = latencyHistogramPrintCountMax;
+        bool normalizing   = lhpcm < lhcm;
+        if (normalizing) {
+            marker    = "#";
+            histScale = double(lhpcm) / double(lhcm);
+        }
+        std::string rows = "";
+        for (int i = histStart; i <= histLast; ++i) {
+            int hi  = latencyHistogram[i];
+            int hin = int(std::round(histScale * double(hi)));
+            std::string istrm1 =
+                std::to_string(int(latencyHistogramCellWidth * double(i)));
+            std::string istr =
+                std::to_string(int(latencyHistogramCellWidth * double(i + 1)));
+            // std::string histr = boost::format("%02d",hi);
+            std::string histr = std::to_string(hi);
+            while (histr.length() < 3) {
+                histr = " " + histr;
+            }
+            std::string row = "[" + istrm1 + "-" + istr + "ms]=" + histr + ":";
+            for (int j = 0; j < hin; j++) {
+                row += marker;
+            }
+            rows += row + "\n";
+        }
+        if (histLast == latencyHistogramCellMax) {
+            rows += " and above\n";
+        }
+        return rows;
+    }
+};
diff --git a/src/Auth.cpp b/src/Auth.cpp
new file mode 100644 (file)
index 0000000..3b59809
--- /dev/null
@@ -0,0 +1,484 @@
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008 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 Auth.cpp
+ * \author Aaron Wyatt
+ * \date September 2020
+ */
+
+#include "Auth.h"
+
+#include <QCryptographicHash>
+#include <QDate>
+#include <QFile>
+#include <QRandomGenerator>
+#include <QTextStream>
+#include <QThread>
+#include <iostream>
+
+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)
+{
+    if (!m_authFileName.isEmpty()) {
+        // Load our credentials file.
+        loadAuthFile(m_authFileName);
+
+        // 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,
+                                           const QString& password)
+{
+    if (username.isEmpty() || password.isEmpty()) {
+        return WRONGCREDS;
+    }
+
+    if (m_passwordTable.contains(username)) {
+        // Check our generated hash against our stored hash.
+        QString salt = m_passwordTable[username].section(QStringLiteral("$"), 2, 2);
+        QString hash(generateSha512Hash(password, salt));
+
+        if (hash == m_passwordTable[username]) {
+            if (checkTime(username)) {
+                return OK;
+            } else {
+                return WRONGTIME;
+            }
+        }
+    }
+
+    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<QString, QString> 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
+    // one. Re-add our file to the watcher. (This has no effect if it's still there.)
+    QThread::msleep(200);
+    std::cout << "Auth file changed. Reloading..." << std::endl;
+    m_authFileWatcher.addPath(m_authFileName);
+    loadAuthFile(m_authFileName);
+}
+
+bool Auth::loadAuthFile(const QString& filename)
+{
+    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;
+        while (!input.atEnd()) {
+            lineNumber++;
+            QStringList lineParts = input.readLine().split(QStringLiteral(":"));
+            if (lineParts.count() < 3) {
+                // We don't have a correctly formatted line. Ignore it.
+                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;
+            }
+
+            // Check that our password hash is usable.
+            bool invalid = false;
+            if (lineParts.at(1).startsWith(QLatin1String("$6$"))) {
+                QStringList hashParts = lineParts.at(1).split(QStringLiteral("$"));
+                if (hashParts.count() < 4) {
+                    invalid = true;
+                } else if (hashParts.at(2).isEmpty() || hashParts.at(3).isEmpty()) {
+                    invalid = true;
+                }
+            } else {
+                invalid = true;
+            }
+            if (invalid) {
+                errorFree = false;
+                if (m_monitorChanges) {
+                    std::cout << "WARNING: Invalid password hash in auth file. (Line "
+                              << lineNumber << ")" << std::endl;
+                }
+                continue;
+            }
+
+            m_passwordTable[lineParts.at(0)] = lineParts.at(1);
+            m_timesTable[lineParts.at(0)]    = lineParts.at(2);
+        }
+        file.close();
+    }
+    return errorFree;
+}
+
+bool Auth::checkTime(const QString& username)
+{
+    QStringList times = m_timesTable[username].split(QStringLiteral(","));
+    // First check for the all or none cases.
+    if (times.count() == 1 && times.at(0).isEmpty()) {
+        return false;
+    } else if (times.contains(QStringLiteral("*"))) {
+        return true;
+    }
+
+    // Now check for weekly schedule information.
+    QString dayOfWeek = m_days.at(QDate::currentDate().dayOfWeek() - 1);
+    for (int i = 0; i < times.count(); i++) {
+        if (times.at(i).startsWith(dayOfWeek)) {
+            QString accessTime = QString(times.at(i)).remove(0, 2);
+            // Check for the all day option first.
+            if (accessTime == QLatin1String("*")) {
+                return true;
+            }
+
+            // See if we can interpret it as a time range.
+            bool valid        = false;
+            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()) {
+                    valid = true;
+                    if (QTime::currentTime() >= start && QTime::currentTime() <= end) {
+                        return true;
+                    }
+                }
+            }
+            if (!valid) {
+                std::cout << "WARNING: The access time \"" << times.at(i).toStdString()
+                          << "\" in the auth file for user \"" << username.toStdString()
+                          << "\" is not valid." << std::endl;
+            }
+        }
+    }
+
+    // We didn't find a match.
+    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:
+    // ./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
+    if (value < 0 || value >= 64) {
+        return 0;
+    }
+
+    if (value < 12) {
+        return (value + 46);
+    } else if (value < 38) {
+        return (value + 53);
+    } else {
+        return (value + 59);
+    }
+}
+
+QByteArray Auth::charGroup(unsigned char byte2, unsigned char byte1, unsigned char byte0,
+                           unsigned int n)
+{
+    // Returns n base64 encoded characters from 24bits of input.
+    // Based on the SHA-crypt algorithm.
+
+    unsigned int w = (byte2 << 16) | (byte1 << 8) | byte0;
+    QByteArray output;
+    while (n-- > 0) {
+        output.append(char64(w & 0x3f));
+        w >>= 6;
+    }
+    return output;
+}
+
+QByteArray Auth::generateSha512Hash(const QString& passwordString,
+                                    const QString& saltString)
+{
+    // Qt implementation of the unix crypt using SHA-512
+    // (Should give the same output as openssl passwd -6)
+
+    // For details on the algorithm see https://www.akkadia.org/drepper/SHA-crypt.txt
+    // (The steps referred to here follow the implementation instructions.)
+    QByteArray passwd = passwordString.toUtf8();
+    passwd.truncate(256);
+    QByteArray salt = saltString.toUtf8();
+    salt.truncate(16);
+
+    int rounds = 5000;
+    QCryptographicHash a(QCryptographicHash::Sha512);
+    QCryptographicHash b(QCryptographicHash::Sha512);
+
+    a.addData(passwd);
+    a.addData(salt);
+
+    b.addData(passwd);
+    b.addData(salt);
+    b.addData(passwd);
+    QByteArray bResult = b.result();
+
+    // Step 9 and 10
+    int n;
+    for (n = passwd.length(); n > 64; n -= 64) {
+        a.addData(bResult);
+    }
+    a.addData(bResult.left(n));
+
+    // Step 11
+    n = passwd.length();
+    while (n) {
+        if (n & 1) {
+            a.addData(bResult);
+        } else {
+            a.addData(passwd);
+        }
+        n >>= 1;
+    }
+    QByteArray aResult = a.result();
+
+    // Step 13
+    // Reuse a as dp.
+    a.reset();
+    for (n = 0; n < passwd.length(); n++) {
+        a.addData(passwd);
+    }
+    QByteArray dp = a.result();
+
+    // Step 16
+    QByteArray p;
+    for (n = passwd.length(); n > 64; n -= 64) {
+        p.append(dp);
+    }
+    p.append(dp.constData(), n);
+
+    // Step 17
+    // Reuse b as ds
+    b.reset();
+    for (n = 16 + (unsigned char)aResult.at(0); n > 0; n--) {
+        b.addData(salt);
+    }
+    QByteArray ds = b.result();
+
+    // Step 20
+    QByteArray s;
+    for (n = salt.length(); n > 64; n -= 64) {
+        s.append(ds);
+    }
+    s.append(ds.constData(), n);
+
+    // Step 21
+    for (n = 0; n < rounds; n++) {
+        // Reuse a as c.
+        a.reset();
+        if (n & 1) {
+            a.addData(p);
+        } else {
+            a.addData(aResult);
+        }
+
+        if (n % 3) {
+            a.addData(s);
+        }
+        if (n % 7) {
+            a.addData(p);
+        }
+
+        if (n & 1) {
+            a.addData(aResult);
+        } else {
+            a.addData(p);
+        }
+
+        aResult = a.result();
+    }
+
+    // Step 22
+    QByteArray output("$6$");
+    output.append(salt);
+    output.append("$");
+    output.append(charGroup(aResult.at(0), aResult.at(21), aResult.at(42), 4));
+    output.append(charGroup(aResult.at(22), aResult.at(43), aResult.at(1), 4));
+    output.append(charGroup(aResult.at(44), aResult.at(2), aResult.at(23), 4));
+    output.append(charGroup(aResult.at(3), aResult.at(24), aResult.at(45), 4));
+    output.append(charGroup(aResult.at(25), aResult.at(46), aResult.at(4), 4));
+    output.append(charGroup(aResult.at(47), aResult.at(5), aResult.at(26), 4));
+    output.append(charGroup(aResult.at(6), aResult.at(27), aResult.at(48), 4));
+    output.append(charGroup(aResult.at(28), aResult.at(49), aResult.at(7), 4));
+    output.append(charGroup(aResult.at(50), aResult.at(8), aResult.at(29), 4));
+    output.append(charGroup(aResult.at(9), aResult.at(30), aResult.at(51), 4));
+    output.append(charGroup(aResult.at(31), aResult.at(52), aResult.at(10), 4));
+    output.append(charGroup(aResult.at(53), aResult.at(11), aResult.at(32), 4));
+    output.append(charGroup(aResult.at(12), aResult.at(33), aResult.at(54), 4));
+    output.append(charGroup(aResult.at(34), aResult.at(55), aResult.at(13), 4));
+    output.append(charGroup(aResult.at(56), aResult.at(14), aResult.at(35), 4));
+    output.append(charGroup(aResult.at(15), aResult.at(36), aResult.at(57), 4));
+    output.append(charGroup(aResult.at(37), aResult.at(58), aResult.at(16), 4));
+    output.append(charGroup(aResult.at(59), aResult.at(17), aResult.at(38), 4));
+    output.append(charGroup(aResult.at(18), aResult.at(39), aResult.at(60), 4));
+    output.append(charGroup(aResult.at(40), aResult.at(61), aResult.at(19), 4));
+    output.append(charGroup(aResult.at(62), aResult.at(20), aResult.at(41), 4));
+    output.append(charGroup(0, 0, aResult.at(63), 2));
+
+    return output;
+}
+
+Auth::Auth::~Auth() = default;
diff --git a/src/Auth.h b/src/Auth.h
new file mode 100644 (file)
index 0000000..10cff27
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008 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 Auth.h
+ * \author Aaron Wyatt
+ * \date September 2020
+ */
+
+#ifndef __AUTH_H__
+#define __AUTH_H__
+
+#include <QFileSystemWatcher>
+#include <QHash>
+#include <QObject>
+
+class Auth : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    enum AuthResponseT {
+        OK          = 1 << 16,
+        REQUIRED    = 2 << 16,
+        NOTREQUIRED = 3 << 16,
+        WRONGCREDS  = 4 << 16,
+        WRONGTIME   = 5 << 16
+    };
+
+    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:
+    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,
+                         unsigned int n);
+    QByteArray generateSha512Hash(const QString& passwordString,
+                                  const QString& saltString);
+
+    QStringList m_days;
+    QHash<QString, QString> m_passwordTable;
+    QHash<QString, QString> m_timesTable;
+
+    QString m_authFileName;
+    bool m_monitorChanges;
+    QFileSystemWatcher m_authFileWatcher;
+};
+
+#endif  // __AUTH_H__
diff --git a/src/Compressor.cpp b/src/Compressor.cpp
new file mode 100644 (file)
index 0000000..04311fe
--- /dev/null
@@ -0,0 +1,122 @@
+//*****************************************************************
+/*
+  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 Compressor.cpp
+ * \author Julius Smith, based on LoopBack.h
+ * \date July 2008
+ */
+
+#include "Compressor.h"
+
+#include "compressordsp.h"
+
+//*******************************************************************************
+Compressor::Compressor(int numchans,  // xtor
+                       bool verboseIn, float ratioIn, float thresholdDBIn,
+                       float attackMSIn, float releaseMSIn, float makeUpGainDBIn)
+    : mNumChannels(numchans)
+    , ratio(ratioIn)
+    , thresholdDB(thresholdDBIn)
+    , attackMS(attackMSIn)
+    , releaseMS(releaseMSIn)
+    , makeUpGainDB(makeUpGainDBIn)
+{
+    setVerbose(verboseIn);
+    // presets.push_back(std::make_unique<CompressorPreset>(ratio,thresholdDB,attackMS,releaseMS,makeUpGainDB));
+    for (int i = 0; i < mNumChannels; i++) {
+        compressordsp* dsp_ptr = new compressordsp;
+        APIUI* ui_ptr          = new APIUI;
+        compressorP.push_back(dsp_ptr);
+        compressorUIP.push_back(ui_ptr);  // #included in compressordsp.h
+        dsp_ptr->buildUserInterface(ui_ptr);
+    }
+}
+
+//*******************************************************************************
+Compressor::~Compressor()
+{
+    for (int i = 0; i < mNumChannels; i++) {
+        delete static_cast<compressordsp*>(compressorP[i]);
+        delete static_cast<APIUI*>(compressorUIP[i]);
+    }
+    compressorP.clear();
+    compressorUIP.clear();
+}
+
+//*******************************************************************************
+void Compressor::setParamAllChannels(const char pName[], float p)
+{
+    for (int i = 0; i < mNumChannels; i++) {
+        APIUI* ui_ptr = static_cast<APIUI*>(compressorUIP[i]);
+        int ndx       = ui_ptr->getParamIndex(pName);
+        if (ndx >= 0) {
+            ui_ptr->setParamValue(ndx, p);
+            if (verbose) {
+                std::cout << "Compressor.h: parameter " << pName << " set to " << p
+                          << " on audio channel " << i << "\n";
+            }
+        } else {
+            std::cerr << "*** Compressor.h: Could not find parameter named " << pName
+                      << "\n";
+        }
+    }
+}
+
+//*******************************************************************************
+void Compressor::init(int samplingRate, int bufferSize)
+{
+    ProcessPlugin::init(samplingRate, bufferSize);
+    fs = float(fSamplingFreq);
+    for (int i = 0; i < mNumChannels; i++) {
+        static_cast<compressordsp*>(compressorP[i])
+            ->init(fs);  // compression filter parameters depend on sampling rate
+    }
+    setParamAllChannels("Ratio", ratio);
+    setParamAllChannels("Threshold", thresholdDB);
+    setParamAllChannels("Attack", attackMS);
+    setParamAllChannels("Release", releaseMS);
+    setParamAllChannels("MakeUpGain", makeUpGainDB);
+    inited = true;
+}
+
+//*******************************************************************************
+void Compressor::compute(int nframes, float** inputs, float** outputs)
+{
+    if (!inited) {
+        std::cerr << "*** Compressor " << this << ": init never called! Doing it now.\n";
+        init(0, 0);
+    }
+    for (int i = 0; i < mNumChannels; i++) {
+        static_cast<compressordsp*>(compressorP[i])
+            ->compute(nframes, &inputs[i], &outputs[i]);
+    }
+}
diff --git a/src/Compressor.h b/src/Compressor.h
new file mode 100644 (file)
index 0000000..94de5e4
--- /dev/null
@@ -0,0 +1,94 @@
+//*****************************************************************
+/*
+  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 Compressor.h
+ * \author Julius Smith, starting from Limiter.h
+ * \date August 2020
+ */
+
+/** \brief Applies compressor_mono from the faustlibraries distribution, compressors.lib
+ *
+ */
+#ifndef __COMPRESSOR_H__
+#define __COMPRESSOR_H__
+
+#include <iostream>
+#include <vector>
+
+#include "CompressorPresets.h"
+#include "ProcessPlugin.h"
+
+/** \brief A Compressor reduces the output dynamic range when the
+ *         signal level exceeds the threshold.
+ */
+class Compressor : public ProcessPlugin
+{
+   public:
+    /// \brief The class constructor sets the number of audio channels and default
+    /// parameters.
+    Compressor(int numchans,  // xtor
+               bool verboseIn = false, float ratioIn = 2.0f, float thresholdDBIn = -24.0f,
+               float attackMSIn = 15.0f, float releaseMSIn = 40.0f,
+               float makeUpGainDBIn = 2.0f);
+
+    Compressor(int numchans,  // xtor
+               bool verboseIn = false, CompressorPreset preset = CompressorPresets::voice)
+        : Compressor(numchans, verboseIn, preset.ratio, preset.thresholdDB,
+                     preset.attackMS, preset.releaseMS, preset.makeUpGainDB)
+    {
+    }
+
+    /// \brief The class destructor
+    virtual ~Compressor();
+
+    //  void setParamAllChannels(std::string& pName, float p) {
+    void setParamAllChannels(const char pName[], float p);
+
+    void init(int samplingRate, int bufferSize) override;
+    int getNumInputs() override { return (mNumChannels); }
+    int getNumOutputs() override { return (mNumChannels); }
+    void compute(int nframes, float** inputs, float** outputs) override;
+    const char* getName() const override { return "Compressor"; }
+
+   private:
+    float fs;
+    int mNumChannels;
+    std::vector<void*> compressorP;
+    std::vector<void*> compressorUIP;
+    float ratio;
+    float thresholdDB;
+    float attackMS;
+    float releaseMS;
+    float makeUpGainDB;
+};
+
+#endif
diff --git a/src/CompressorPresets.h b/src/CompressorPresets.h
new file mode 100644 (file)
index 0000000..ab7745e
--- /dev/null
@@ -0,0 +1,77 @@
+#pragma once
+
+#include <array>
+
+struct CompressorPreset {
+    float ratio;
+    float thresholdDB;
+    float attackMS;
+    float releaseMS;
+    float makeUpGainDB;
+};
+
+namespace CompressorPresets
+{
+//                     name   ratio  thresh  attack  rel  mugain
+const CompressorPreset voice{2.0f, -24.0f, 15.0f, 40.0f, 2.0f};
+const CompressorPreset horns{3.0f, -10.0f, 100.0f, 250.0f, 2.0f};
+const CompressorPreset snare{5.0f, -4.0f, 5.0f, 150.0f, 3.0f};
+const unsigned int numPresets{3};
+const std::array<CompressorPreset, numPresets> standardPresets{voice, horns, snare};
+enum CompressorPresetNames { CPN_VOICE, CPN_BRASS, CPN_SNARE, CPN_NUMPRESETS };
+}  // namespace CompressorPresets
+
+#if 0  // not yet using this
+// Dynamic extension of CompressorPresets:
+struct CompressorPresetList {
+  std::vector<CompressorPreset*> presets;
+  CompressorPresetList() { // define some standard presets
+    presets.push_back( new CompressorPreset(CompressorPresets::voice) );
+    presets.push_back( new CompressorPreset(CompressorPresets::horns) );
+    presets.push_back( new CompressorPreset(CompressorPresets::snare) );
+  }
+  ~CompressorPresetList() = default;
+};
+#endif
+
+// clang-format off
+
+/* Settings from http://www.anythingpeaceful.org/sonar/settings/comp.html
+
+   Name     Thresh(dB) Att(ms) Rel(ms) Ratio:1 Gain(dB)    Comments
+   Vocal 1     -20     31      342     2.5     2       Compressor for Solo Vocal
+   Vocal 2     -8      26      331     2.5     1.5     Variation of Solo 1
+   Full Comp 1 -8      60      2500    2.5     0       For Overall Volume Level
+   Full Comp 2 -18     94      447     3.5     2.5     Variation of Total Comp 1: Harder ratio
+   Full Comp 3 -16     11      180     6       6       Nearly a limiter effect
+   Kick Comp   -24     9       58      3       5.5     Compressor for Acoustic Bass Drum
+   Snare Comp  -17     8       12      2.5     3.5     Compressor for Acoustic Snare Drum
+   Guitar      -10     5       238     2.5     1.5     Compressor for Acoustic Guitar
+   Brass Sec   -18     18      226     1.7     4       Brass Sounds for Strong Attacks
+   Bass 1      -12     15      470     2       4.5     Finger Picked Bass Guitar
+   Bass 2      -12     6       133     1.7     4       Slap Electric Bass
+   E Guitar    -8      7       261     3.5     2.5     Electric Guitar Compressor
+   Piano 1     -9      17      238     2.5     1       Brightens Piano
+   Piano 2     -18     7       174     3.5     6       Variation of Piano 1
+   Kick        -14     2       35      2       3.5     For sampled Bass Drum
+   Snare       -18     8       354     4       8       For sampled Snare Drum
+   Strings 1   -11     33      749     2       1.5     For String instruments
+   Strings 2   -12     93      2500    1.5     1.5     For Violas and Cellos
+   Strings 3   -17     76      186     1.5     2.5     Cellos or DoubleBass
+   Syn Bass    -10     9       250     3.5     3       Adjust level of Synth Bass
+   Syn Pad     -13     58      238     2       2       Prevents diffusion of sound in synth pad
+   Limiting    -1      0.1     325     20      0       Slow release limiter
+   Chorusing   -9      39      225     1.7     2.5     For vocal Chorusing
+
+   From https://www.dummies.com/art-center/music/recording-music/dynamic-music-compression-settings-for-horns-piano-and-percussion/
+
+   Horns       –8      100     300     2.5-3   2       Brasses not normally compressed [jos gain estimate based on above table]
+   Piano       -10     100-105 115     1.5-2   2       Normally not compressed ["]
+   Kick        -6      40-50   200-300 4-6     3       Looks more like a limiter to me [jos] ["]
+   Snare       -4      5-10    125-175 4-6     3       Crucial for a tight, punchy sound
+   Bongos      -6      10-25   100-300 3-6     3       "Hand Drums" - protect against excess "slap"
+   Perc.       -10     10-20   50      3-6     3       Transient overdrive protection in mix
+
+*/
+
+// clang-format on
diff --git a/src/DataProtocol.cpp b/src/DataProtocol.cpp
new file mode 100644 (file)
index 0000000..bea9d59
--- /dev/null
@@ -0,0 +1,80 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008 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 DataProtocol.cpp
+ * \author Juan-Pablo Caceres
+ * \date June 2008
+ */
+
+#include "DataProtocol.h"
+
+#include <QHostAddress>
+#include <QHostInfo>
+#include <cstdlib>
+#include <iostream>
+
+#include "JackTrip.h"
+#include "jacktrip_globals.h"
+
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+DataProtocol::DataProtocol(JackTrip* jacktrip, const runModeT runmode, int /*bind_port*/,
+                           int /*peer_port*/)
+    : mStopped(true)
+    , mHasPacketsToReceive(false)
+    , mRunMode(runmode)
+    , mJackTrip(jacktrip)
+    , mUseRtPriority(false)
+{
+}
+
+//*******************************************************************************
+DataProtocol::~DataProtocol() {}
+
+//*******************************************************************************
+void DataProtocol::threadHasStarted()
+{
+    QMutexLocker lock(&mMutex);
+    mStopped = false;
+    mThreadHasStarted.notify_all();
+}
+
+//*******************************************************************************
+void DataProtocol::waitForStart()
+{
+    QMutexLocker lock(&mMutex);
+    while (mStopped) {
+        mThreadHasStarted.wait(&mMutex);
+    }
+}
diff --git a/src/DataProtocol.h b/src/DataProtocol.h
new file mode 100644 (file)
index 0000000..5c5c1a8
--- /dev/null
@@ -0,0 +1,235 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008 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 DataProtocol.h
+ * \author Juan-Pablo Caceres
+ * \date June 2008
+ */
+
+#ifndef __DATAPROTOCOL_H__
+#define __DATAPROTOCOL_H__
+
+#ifdef _WIN32
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#endif
+
+#ifndef _WIN32
+#include <arpa/inet.h>  //inet(3) functions
+#include <netdb.h>
+#include <netinet/in.h>  //sockaddr_in{} and other Internet defns
+// #include <tr1/memory> //for shared_ptr
+#endif
+
+#include <QHostAddress>
+#include <QMutex>
+#include <QMutexLocker>
+#include <QThread>
+#include <QWaitCondition>
+#include <iostream>
+
+class JackTrip;  // forward declaration
+
+/** \brief Base class that defines the transmission protocol.
+ *
+ * This base class defines most of the common method to setup and connect
+ * sockets using the individual protocols (UDP, TCP, SCTP, etc).
+ *
+ * The class has to be constructed using one of two modes (runModeT):\n
+ * - SENDER
+ * - RECEIVER
+ *
+ * This has to be specified as a constructor argument. When using, create two instances
+ * of the class, one to receive and one to send packets. Each instance will run on a
+ * separate thread.
+ *
+ * Redundancy and forward error correction should be implemented on each
+ * Transport protocol, cause they depend on the protocol itself
+ *
+ * \todo This Class should contain definition of jacktrip header and basic funcionality to
+ * obtain local machine IPs and maybe functions to manipulate IPs. Redundancy and forward
+ * error correction should be implemented on each Transport protocol, cause they depend on
+ * the protocol itself
+ *
+ * \todo The transport protocol itself has to be implemented subclassing this class, i.e.,
+ * using a TCP or UDP protocol.
+ *
+ * Even if the underlined transmission protocol is stream oriented (as in TCP),
+ * we send packets that are the size of the audio processing buffer.
+ * Use AudioInterface::getBufferSize to obtain this value.
+ *
+ * Each transmission (i.e., inputs and outputs) run on its own thread.
+ */
+class DataProtocol : public QThread
+{
+    Q_OBJECT;
+
+   public:
+    //----------ENUMS------------------------------------------
+    /// \brief Enum to define packet header types
+    enum packetHeaderTypeT {
+        DEFAULT,  ///< Default application header
+        JAMLINK,  ///< Header to use with Jamlinks
+        EMPTY     ///< Empty Header
+    };
+
+    /// \brief Enum to define class modes, SENDER or RECEIVER
+    enum runModeT {
+        SENDER,   ///< Set class as a Sender (send packets)
+        RECEIVER  ///< Set class as a Receiver (receives packets)
+    };
+    //---------------------------------------------------------
+
+    /** \brief The class constructor
+     * \param jacktrip Pointer to the JackTrip class that connects all classes (mediator)
+     * \param runmode Sets the run mode, use either DataProtocol::SENDER or
+     * DataProtocol::RECEIVER
+     * \param headertype packetHeaderTypeT header type to use for packets
+     * \param bind_port Port number to bind for this socket (this is the receive or send
+     * port depending on the runmode) \param peer_port Peer port number (this is the
+     * receive or send port depending on the runmode)
+     */
+    DataProtocol(JackTrip* jacktrip, const runModeT runmode, int bind_port,
+                 int peer_port);
+
+    /// \brief The class destructor
+    virtual ~DataProtocol();
+
+    /** \brief Implements the thread loop
+     *
+     * Depending on the runmode, with will run a DataProtocol::SENDER thread or
+     * DataProtocol::RECEIVER thread
+     */
+    virtual void run() = 0;
+
+    /// \brief Stops the execution of the Thread
+    virtual void stop()
+    {
+        QMutexLocker lock(&mMutex);
+        mStopped = true;
+    }
+
+    /** \brief Sets the size of the audio part of the packets
+     * \param size_bytes Size in bytes
+     */
+    void setAudioPacketSize(const size_t size_bytes) { mAudioPacketSize = size_bytes; }
+
+    /** \brief Get the size of the audio part of the packets
+     * \return size_bytes Size in bytes
+     */
+    size_t getAudioPacketSizeInBites() { return (mAudioPacketSize); }
+
+    /** \brief Set the peer address
+     * \param peerHostOrIP IPv4 number or host name
+     * \todo implement here instead of in the subclass UDP
+     */
+    virtual void setPeerAddress(const char* peerHostOrIP) = 0;
+
+    /** \brief Set the peer incoming (receiving) port number
+     * \param port Port number
+     * \todo implement here instead of in the subclass UDP
+     */
+    virtual void setPeerPort(int port) = 0;
+
+    // virtual void getPeerAddressFromFirstPacket(QHostAddress& peerHostAddress,
+    //                              uint16_t& port) = 0;
+
+#if defined(_WIN32)
+    virtual void setSocket(SOCKET& socket) = 0;
+#else
+    virtual void setSocket(int& socket) = 0;
+#endif
+
+    struct PktStat {
+        uint32_t tot;
+        uint32_t lost;
+        uint32_t outOfOrder;
+        uint32_t revived;
+        uint32_t statCount;
+    };
+    virtual bool getStats(PktStat*) { return false; }
+
+    virtual void setIssueSimulation(double /*loss*/, double /*jitter*/,
+                                    double /*max_delay*/)
+    {
+    }
+    void setUseRtPriority(bool use) { mUseRtPriority = use; }
+
+    /// @brief wait for the thread to start up
+    void waitForStart();
+
+   signals:
+
+    void signalError(const char* error_message);
+    void signalReceivedConnectionFromPeer();
+    void signalCeaseTransmission(const QString& reason = QLatin1String(""));
+
+   protected:
+    /** \brief Get the Run Mode of the object
+     * \return SENDER or RECEIVER
+     */
+    runModeT getRunMode() const { return mRunMode; }
+
+    /// @brief called by the thread during startup
+    void threadHasStarted();
+
+    /// Boolean stop the execution of the thread
+    volatile bool mStopped;
+    /// Boolean to indicate if the RECEIVER is waiting to obtain peer address
+    volatile bool mHasPeerAddress;
+    /// Boolean that indicates if a packet was received
+    volatile bool mHasPacketsToReceive;
+    QMutex mMutex;
+    QWaitCondition mThreadHasStarted;
+
+   private:
+    int mLocalPort;           ///< Local Port number to Bind
+    int mPeerPort;            ///< Peer Port number to Bind
+    const runModeT mRunMode;  ///< Run mode, either SENDER or RECEIVER
+
+    struct sockaddr_in mLocalIPv4Addr;  ///< Local IPv4 Address struct
+    struct sockaddr_in mPeerIPv4Addr;   ///< Peer IPv4 Address struct
+
+    /// Number of clients running to check for ports already used
+    /// \note Unimplemented, try to find another way to check for used ports
+    static int sClientsRunning;
+
+    size_t mAudioPacketSize;  ///< Packet audio part size
+
+    /// \todo check a better way to access the header from the subclasses
+   protected:
+    // PacketHeader* mHeader; ///< Packet Header
+    JackTrip* mJackTrip;  ///< JackTrip mediator class
+    bool mUseRtPriority;
+};
+
+#endif
diff --git a/src/Effects.h b/src/Effects.h
new file mode 100644 (file)
index 0000000..0332c4e
--- /dev/null
@@ -0,0 +1,713 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2020 Julius Smith.
+  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 Effects.h
+ * \author Julius Smith
+ * \date Aug 2020
+ */
+
+#pragma once
+
+#include <cassert>
+#include <vector>
+
+#include "Compressor.h"
+#include "CompressorPresets.h"
+#include "Limiter.h"
+#include "ProcessPlugin.h"
+#include "Reverb.h"
+
+class Effects
+{
+    int mNumIncomingChans;
+    int mNumOutgoingChans;
+    int gVerboseFlag = 0;
+
+   public:
+    enum LIMITER_MODE {
+        LIMITER_NONE,
+        LIMITER_INCOMING,  // from network
+        LIMITER_OUTGOING,  // to network
+        LIMITER_BOTH
+    };
+
+   private:
+    LIMITER_MODE mLimit;              ///< audio limiter controls
+    unsigned int mNumClientsAssumed;  ///< assumed number of clients (audio sources)
+    double limiterWarningAmplitude;
+
+    enum InOrOut { IO_NEITHER, IO_IN, IO_OUT } io;
+    bool inCompressor             = false;
+    bool outCompressor            = false;
+    bool inZitarev                = false;
+    bool outZitarev               = false;
+    bool inFreeverb               = false;
+    bool outFreeverb              = false;
+    bool incomingEffectsAllocated = false;
+    bool outgoingEffectsAllocated = false;
+    Compressor* inCompressorP     = nullptr;
+    Compressor* outCompressorP    = nullptr;
+    CompressorPreset inCompressorPreset =
+        CompressorPresets::voice;  // ./CompressorPresets.h
+    CompressorPreset outCompressorPreset = CompressorPresets::voice;
+    Reverb* inZitarevP                   = nullptr;
+    Reverb* outZitarevP                  = nullptr;
+    Reverb* inFreeverbP                  = nullptr;
+    Reverb* outFreeverbP                 = nullptr;
+    int parenLevel                       = 0;
+    char lastEffect                      = '\0';
+    float zitarevInLevel                 = 1.0f;  // "Level" = wetness from 0 to 1
+    float freeverbInLevel                = 1.0f;
+    float zitarevOutLevel                = 1.0f;
+    float freeverbOutLevel               = 1.0f;
+    float mReverbLevel;  // for backward compatibility: 0-1 Freeverb, 1-2 Zitarev
+    Limiter* inLimiterP  = nullptr;
+    Limiter* outLimiterP = nullptr;
+
+   public:
+    Effects(bool outGoingLimiterOn = true)
+        : mNumIncomingChans(2)
+        , mNumOutgoingChans(2)
+        , mLimit(outGoingLimiterOn ? LIMITER_OUTGOING : LIMITER_NONE)
+        , mNumClientsAssumed(2)
+        , limiterWarningAmplitude(0.0)
+    {
+    }
+
+    ~Effects()
+    {
+        /*
+      Plugin ownership presently passes to JackTrip,
+      and deletion occurs in AudioInterface.cpp. See
+        delete mProcessPluginsFromNetwork[i];
+        delete mProcessPluginsToNetwork[i];
+      there.  If/when we ever do it here:
+        if (inCompressor) { delete inCompressorP; }
+        if (outCompressor) { delete outCompressorP; }
+        if (inZitarev) { delete inZitarevP; }
+        if (outZitarev) { delete outZitarevP; }
+        if (inFreeverb) { delete inFreeverbP; }
+        if (outFreeverb) { delete outFreeverbP; }
+      but if everyone can compile C++11,
+      let's switch to using std::unique_ptr.
+    */
+    }
+
+    unsigned int getNumClientsAssumed() { return mNumClientsAssumed; }
+
+    LIMITER_MODE getLimit() { return mLimit; }
+    void setNoLimiters() { mLimit = LIMITER_NONE; }
+
+    ProcessPlugin* getInCompressor() { return inCompressorP; }
+    ProcessPlugin* getOutCompressor() { return outCompressorP; }
+    ProcessPlugin* getInZitarev() { return inZitarevP; }
+    ProcessPlugin* getOutZitarev() { return outZitarevP; }
+    ProcessPlugin* getInFreeverb() { return inFreeverbP; }
+    ProcessPlugin* getOutFreeverb() { return outFreeverbP; }
+    ProcessPlugin* getInLimiter() { return inLimiterP; }
+    ProcessPlugin* getOutLimiter() { return outLimiterP; }
+
+    bool getHaveEffect()
+    {
+        return inCompressor || outCompressor || inZitarev || outZitarev || inFreeverb
+               || outFreeverb;
+    }
+
+    bool getHaveLimiter() { return mLimit != LIMITER_NONE; }
+
+    void setVerboseFlag(int v) { gVerboseFlag = v; }
+
+    int getNumIncomingChans() { return mNumIncomingChans; }
+
+    int getOutgoingNumChans() { return mNumOutgoingChans; }
+
+    // call these next two after it is decided what effects we will be using for the
+    // duration:
+
+    std::vector<ProcessPlugin*> allocateIncomingEffects(int nIncomingChans)
+    {
+        mNumIncomingChans = nIncomingChans;
+        if (incomingEffectsAllocated) {
+            std::cerr
+                << "*** Effects.h: attempt to allocate incoming effects more than once\n";
+            std::exit(1);
+        }
+        std::vector<ProcessPlugin*> incomingEffects;
+        if (inCompressor) {
+            assert(inCompressorP == nullptr);
+            inCompressorP =
+                new Compressor(mNumIncomingChans, gVerboseFlag, inCompressorPreset);
+            if (gVerboseFlag) {
+                std::cout << "Set up INCOMING COMPRESSOR\n";
+            }
+            incomingEffects.push_back(inCompressorP);
+        }
+        if (inZitarev) {
+            assert(inZitarevP == nullptr);
+            inZitarevP =
+                new Reverb(mNumIncomingChans, mNumIncomingChans, 1.0 + zitarevInLevel);
+            if (gVerboseFlag) {
+                std::cout << "Set up INCOMING REVERB (Zitarev)\n";
+            }
+            incomingEffects.push_back(inZitarevP);
+        }
+        if (inFreeverb) {
+            assert(inFreeverbP == nullptr);
+            inFreeverbP =
+                new Reverb(mNumIncomingChans, mNumIncomingChans, freeverbInLevel);
+            if (gVerboseFlag) {
+                std::cout << "Set up INCOMING REVERB (Freeverb)\n";
+            }
+            incomingEffects.push_back(inFreeverbP);
+        }
+        // LIMITER MUST GO LAST:
+        if (mLimit == LIMITER_INCOMING || mLimit == LIMITER_BOTH) {
+            if (gVerboseFlag) {
+                std::cout << "Set up INCOMING LIMITER for " << mNumIncomingChans
+                          << " input channels\n";
+            }
+            assert(inLimiterP == nullptr);
+            inLimiterP = new Limiter(
+                mNumIncomingChans, 1,
+                gVerboseFlag);  // mNumClientsAssumed not needed this direction
+            // Never needed in normal practice for incoming limiter:
+            // inLimiterP->setWarningAmplitude(limiterWarningAmplitude);
+            incomingEffects.push_back(inLimiterP);
+        }
+        incomingEffectsAllocated = true;
+        return incomingEffects;
+    }
+
+    std::vector<ProcessPlugin*> allocateOutgoingEffects(int nOutgoingChans)
+    {
+        mNumOutgoingChans = nOutgoingChans;
+        if (outgoingEffectsAllocated) {
+            std::cerr
+                << "*** Effects.h: attempt to allocate outgoing effects more than once\n";
+            std::exit(1);
+        }
+        std::vector<ProcessPlugin*> outgoingEffects;
+        if (outCompressor) {
+            assert(outCompressorP == nullptr);
+            outCompressorP =
+                new Compressor(mNumOutgoingChans, gVerboseFlag, outCompressorPreset);
+            if (gVerboseFlag) {
+                std::cout << "Set up OUTGOING COMPRESSOR\n";
+            }
+            outgoingEffects.push_back(outCompressorP);
+        }
+        if (outZitarev) {
+            assert(outZitarevP == nullptr);
+            outZitarevP =
+                new Reverb(mNumOutgoingChans, mNumOutgoingChans, 1.0 + zitarevOutLevel);
+            if (gVerboseFlag) {
+                std::cout << "Set up OUTGOING REVERB (Zitarev)\n";
+            }
+            outgoingEffects.push_back(outZitarevP);
+        }
+        if (outFreeverb) {
+            assert(outFreeverbP == nullptr);
+            outFreeverbP =
+                new Reverb(mNumOutgoingChans, mNumOutgoingChans, freeverbOutLevel);
+            if (gVerboseFlag) {
+                std::cout << "Set up OUTGOING REVERB (Freeverb)\n";
+            }
+            outgoingEffects.push_back(outFreeverbP);
+        }
+        // LIMITER MUST GO LAST:
+        if (mLimit != LIMITER_NONE) {
+            if (mLimit == LIMITER_OUTGOING || mLimit == LIMITER_BOTH) {
+                if (gVerboseFlag) {
+                    std::cout << "Set up OUTGOING LIMITER for " << mNumOutgoingChans
+                              << " output channels and " << mNumClientsAssumed
+                              << " assumed client(s) ...\n";
+                }
+                assert(outLimiterP == nullptr);
+                outLimiterP = new Limiter(mNumOutgoingChans, mNumClientsAssumed);
+                outLimiterP->setWarningAmplitude(limiterWarningAmplitude);
+                // do not have mSampleRate yet, so cannot call limiter->init(mSampleRate)
+                // here
+                outgoingEffects.push_back(outLimiterP);
+            }
+        }
+        outgoingEffectsAllocated = true;
+        return outgoingEffects;
+    }
+
+    void printHelp(char* command, char helpCase)
+    {
+        std::cout << "HELP for `" << command
+                  << "' (end-of-line comments start with `//')\n";
+        std::cout << "\n";
+        std::cout << "Examples:\n";
+        std::cout << "\n";
+        if (helpCase == 0 || helpCase == 'f') {  //
+            std::cout
+                << command
+                << " 0.3 // add a default outgoing compressor (for voice) and incoming "
+                   "reverb (freeverb) with wetness 0.3 (wetness from 0 to 1)\n";
+            std::cout << command
+                      << " 1.3 // add a default outgoing compressor (for voice) and "
+                         "incoming reverb (zitarev) with wetness 0.3 = 1.3-1 (i.e., 1+ "
+                         "to 2 is for zitarev)\n";
+            std::cout << "\n";
+            std::cout << command
+                      << " \"o:c i:f(0.3)\" // outgoing-compressor and incoming-freeverb "
+                         "example above using more general string argument\n";
+            std::cout << command
+                      << " \"o:c i:z(0.3)\" // outgoing-compressor and incoming-zitarev "
+                         "example above using more general string argument\n";
+            std::cout << command
+                      << " \"o:c(1)\" // outgoing compressor, using preset 1 (designed "
+                         "for voice - see below for details)\n";
+            std::cout
+                << command
+                << " \"o:c(2)\" // outgoing compressor, using preset 2 (for horns)\n";
+            std::cout
+                << command
+                << " \"o:c(3)\" // outgoing compressor, using preset 3 (for snare)\n";
+            std::cout << command
+                      << " \"o:c(c:compressionRatio t:thresholdDB a:attackTimeMS "
+                         "r:releaseTimeMS g:makeUpGainDB)\" // general compression "
+                         "parameter specification (all floats)\n";
+            std::cout << command
+                      << " \"o:c(c:2 t:-24 a:15 r:40 g:2)\"   // outgoing compressor, "
+                         "preset 1 details\n";
+            std::cout << command
+                      << " \"o:c(c:3 t:-10 a:100 r:250 g:2)\" // outgoing compressor, "
+                         "preset 2 details\n";
+            std::cout << command
+                      << " \"o:c(c:5 t:-4 a:5 r:150 g:3)\"    // outgoing compressor, "
+                         "preset 3 details\n";
+            std::cout << "  For these and more suggested compression settings, see "
+                         "http://www.anythingpeaceful.org/sonar/settings/comp.html\n";
+            std::cout << "\n";
+        }
+        if (helpCase == 0 || helpCase == 'O') {  // limiter (-O option most likely)
+            std::cout << command
+                      << " i   // add limiter to INCOMING audio from network (only "
+                         "helpful for floats, i.e., -b32 used by server)\n";
+            std::cout << command
+                      << " o   // add limiter to OUTGOING audio to network (prevents "
+                         "your sound from harshly clipping going out)\n";
+            std::cout << command
+                      << " ow  // also warn and advise on levels when outgoing limiter "
+                         "compresses audio near clipping\n";
+            std::cout << command
+                      << " io  // add limiter to both INCOMING and OUTGOING audio\n";
+            std::cout << command
+                      << " iow // limiters both ways and compression warnings on "
+                         "outgoing direction only\n";
+            std::cout << "\n";
+        }
+        if (helpCase == 0 || helpCase == 'a') {  // assumedNumClients (-a option)
+            std::cout << command
+                      << " 1 // assume 1 client - fine for loopback test, or if only one "
+                         "client plays at a time, or server uses -b32 and -Oi is used\n";
+            std::cout << command
+                      << " 2 // assume 2 clients possibly playing at the same time\n";
+            std::cout
+                << command
+                << " N // any integer N>0 can be used - the outgoing limiter will divide "
+                   "final amplitude by 1/sqrt(N) to reduce overages in server\n";
+            std::cout << "\n";
+        }
+    }
+
+    // ----------- Compressor stuff --------------
+
+    int setCompressorPresetIndexFrom1(unsigned long presetIndexFrom1, InOrOut io)
+    {
+        int returnCode = 0;
+        if (presetIndexFrom1 <= 0 || presetIndexFrom1 > CompressorPresets::numPresets) {
+            std::cerr << "*** Effects.h: setCompressorPresetFrom1: Index "
+                      << presetIndexFrom1 << " out of range\n";
+            returnCode = 1;
+        } else {
+            CompressorPreset stdPreset =
+                CompressorPresets::standardPresets[presetIndexFrom1 - 1];
+            if (io == IO_IN) {
+                inCompressorPreset = stdPreset;
+            } else if (io == IO_OUT) {
+                outCompressorPreset = stdPreset;
+            } else if (io != IO_NEITHER) {
+                std::cerr
+                    << "*** Effects.h: setCompressorPresetFrom1: Invalid InOrOut value "
+                    << io << "\n";
+                returnCode = 1;
+            }
+        }
+        return returnCode;
+    }
+
+    int parseCompresserArgs(char* args, InOrOut inOrOut)
+    {
+        // args can be integerPresetNumberFrom1 or (all optional, any order):
+        // c:compressionRatio, a:attackTimeMS, r:releaseTimeMS, g:makeUpGain
+        int returnCode = 0;
+        if (!isalpha(args[0])) {
+            int presetIndexFrom1 = atoi(args);
+            setCompressorPresetIndexFrom1(presetIndexFrom1, inOrOut);
+        } else {
+            // args can be presetIndexFrom1, handled above, or (all optional, any order):
+            // c(c:compressionRatio, t:thresholdDB, a:attackTimeMS, r:releaseTimeMS,
+            // g:makeUpGainDB) See ./CompressorPresets.h for example settings.
+            if (gVerboseFlag) {
+                std::cout << "parseCompressorArgs = " << args << std::endl;
+            }
+            ulong argLen   = ulong(strlen(args));
+            char lastParam = '\0';
+
+            CompressorPreset newPreset(
+                CompressorPresets::voice);  // Anything unset gets voice value (most
+                                            // gentle)
+
+            int nSkip = 0;
+            for (ulong i = 0; i < argLen; i++) {
+                if (nSkip > 0) {
+                    nSkip--;
+                    continue;
+                }
+                char ch = args[i];
+                switch (ch) {
+                case ' ':
+                    break;
+                case '\t':
+                    break;
+                case 'c':
+                case 't':
+                case 'a':
+                case 'r':
+                case 'g':
+                    lastParam = ch;
+                    break;
+                case ':':
+                    break;
+                default:  // must be a floating-point number at this point:
+                    if (ch != '-' && isalpha(ch)) {
+                        std::cerr << "*** Effects.h: parseCompressorArgs: " << ch
+                                  << " not recognized in args = " << args << "\n";
+                        returnCode = 2;
+                    } else {  // must have a digit or '-' or '.'
+                        assert(ch == '-' || ch == '.' || isdigit(ch));
+                        float paramValue = -1.0e10;
+                        for (ulong j = i; j < argLen;
+                             j++) {  // scan ahead for end of number
+                            if (args[j] == ',' || args[j] == ' '
+                                || j == argLen - 1) {  // comma or space required between
+                                                       // parameters
+                                char argsj = args[j];
+                                if (j < argLen - 1) {  // there's more
+                                    args[j] = '\0';
+                                }
+                                paramValue = atof(&args[i]);
+                                args[j]    = argsj;
+                                nSkip      = j - i;
+                                break;
+                            }
+                        }
+                        if (paramValue == -1.0e10) {
+                            std::cerr << "*** Effects.h: parseCompressorArgs: Could not "
+                                         "find parameter for "
+                                      << lastParam << " in args = " << args << "\n";
+                            returnCode = 2;
+                        } else {
+                            switch (lastParam) {
+                            case 'c':
+                                newPreset.ratio = paramValue;
+                                break;
+                            case 't':
+                                newPreset.thresholdDB = paramValue;
+                                break;
+                            case 'a':
+                                newPreset.attackMS = paramValue;
+                                break;
+                            case 'r':
+                                newPreset.releaseMS = paramValue;
+                                break;
+                            case 'g':
+                                newPreset.makeUpGainDB = paramValue;
+                                break;
+                            default:  // cannot happen:
+                                std::cerr
+                                    << "*** Effects.h: parseCompressorArgs: lastParam "
+                                    << lastParam << " invalid\n";
+                                returnCode = 3;  // "reality failure"
+                            }                    // switch(lastParam)
+                        }                        // have valid parameter from atof
+                    }  // have valid non-alpha char for parameter
+                }      // switch(ch)
+            }          // for (ulong i=0; i<argLen; i++) {
+            if (inOrOut == IO_IN) {
+                inCompressorPreset = newPreset;
+            } else if (inOrOut == IO_OUT) {
+                outCompressorPreset = newPreset;
+            } else if (inOrOut != IO_NEITHER) {
+                std::cerr << "*** Effects.h: parseCompressorArgs: invalid InOrOut value "
+                          << inOrOut << "\n";
+                returnCode = 2;
+            }
+        }  // long-form compressor args
+        return returnCode;
+    }  // int parseCompresserArgs(char* args, InOrOut inOrOut)
+
+    // ============== General argument processing for all effects =================
+
+    int parseEffectsOptArg(char* cmd, char* optarg)
+    {
+        int returnCode =
+            0;  // 0 means go, 1 means exit without error, higher => error exit
+
+        char c = optarg[0];
+        if (c == '-' || c == 0) {
+            // happens when no -f argument specified
+            returnCode = 2;
+        } else if (!isalpha(c)) {  // backward compatibility why not?, e.g., "-f 0.5"
+            // -f reverbLevelFloat
+            mReverbLevel  = atof(optarg);
+            outCompressor = true;
+            inZitarev     = mReverbLevel > 1.0;
+            inFreeverb    = mReverbLevel <= 1.0;
+            if (inZitarev) {
+                zitarevInLevel = mReverbLevel - 1.0;  // wetness from 0 to 1
+            }
+            if (inFreeverb) {
+                freeverbInLevel = mReverbLevel;  // wetness from 0 to 1
+            }
+        } else {  // long-form argument:
+            // -f "i:[c][f|z][(reverbLevel)]], o:[c][f|z][(rl)]"
+            // c can be c(integerPresetNumberFrom1) or (all optional, any order):
+            // c(c:compressionRatio, a:attackTimeMS, r:releaseTimeMS, g:makeUpGain)
+            if (gVerboseFlag) {
+                std::cout << cmd << " argument = " << optarg << std::endl;
+            }
+            ulong argLen = ulong(strlen(optarg));
+
+            for (ulong i = 0; i < argLen; i++) {
+                if (optarg[i] != ')' && parenLevel > 0) {
+                    continue;
+                }
+                switch (optarg[i]) {
+                case ' ':
+                    break;
+                case ',':
+                    break;
+                case ';':
+                    break;
+                case '\t':
+                    break;
+                case 'h':
+                    printHelp(cmd, 'f');
+                    returnCode = 1;
+                    break;
+                case 'i':
+                    io = IO_IN;
+                    break;
+                case 'o':
+                    io = IO_OUT;
+                    break;
+                case ':':
+                    break;
+                case 'c':
+                    if (io == IO_IN) {
+                        inCompressor = true;
+                    } else if (io == IO_OUT) {
+                        outCompressor = true;
+                    } else {
+                        std::cerr << "-f arg `" << optarg << "' malformed\n";
+                        exit(1);
+                    }
+                    lastEffect = 'c';
+                    break;
+                case 'f':
+                    if (io == IO_IN) {
+                        inFreeverb = true;
+                    } else if (io == IO_OUT) {
+                        outFreeverb = true;
+                    } else {
+                        std::cerr << "-f arg `" << optarg << "' malformed\n";
+                        exit(1);
+                    }
+                    lastEffect = 'f';
+                    break;
+                case 'z':
+                    if (io == IO_IN) {
+                        inZitarev = true;
+                    } else if (io == IO_OUT) {
+                        outZitarev = true;
+                    } else {
+                        std::cerr << "-f arg `" << optarg << "' malformed\n";
+                        exit(1);
+                    }
+                    lastEffect = 'z';
+                    break;
+                case '(':
+                    parenLevel++;
+                    for (ulong j = i + 1; j < argLen; j++) {
+                        if (optarg[j] == ')') {
+                            optarg[j] = '\0';
+                            switch (lastEffect) {
+                            case 'c': {
+                                returnCode += parseCompresserArgs(&optarg[i + 1], io);
+                                break;
+                            }
+                            case 'z': {
+                                float farg = atof(&optarg[i + 1]);
+                                if (io == IO_IN) {
+                                    zitarevInLevel = farg;
+                                } else if (io == IO_OUT) {
+                                    zitarevOutLevel = farg;
+                                }  // else ignore the argument
+                                break;
+                            }
+                            case 'f': {
+                                float farg = atof(&optarg[i + 1]);
+                                if (io == IO_IN) {
+                                    freeverbInLevel = farg;
+                                } else if (io == IO_OUT) {
+                                    freeverbOutLevel = farg;
+                                }  // else ignore the argument
+                                break;
+                            }
+                            default: {  // ignore
+                                break;
+                            }
+                            }
+                            optarg[j] = ')';
+                            break;
+                        }
+                    }
+                    break;
+                case ')':
+                    parenLevel--;
+                    break;
+                default:
+                    break;  // ignore
+                }           // switch(optarg[i])
+            }
+        }
+        return returnCode;
+    }
+
+    int parseLimiterOptArg(char* cmd, char* optarg)
+    {
+        int returnCode = 0;
+        lastEffect     = 'O';  // OverflowLimiter
+        char ch        = tolower(optarg[0]);
+        if (ch == '-' || ch == 0) {
+            std::cerr << cmd << " argument i, o, or io is REQUIRED\n";
+            returnCode = 2;
+        } else if (ch == 'h') {
+            printHelp(cmd, 'O');
+            returnCode = 1;
+        } else {
+            bool haveIncoming = false;
+            bool haveOutgoing = false;
+            bool haveWarnings = false;
+            for (uint i = 0; i < strlen(optarg); i++) {
+                ch = tolower(optarg[i]);
+                switch (ch) {
+                case ' ':
+                    break;
+                case '\t':
+                    break;
+                case 'i':
+                    haveIncoming = true;
+                    break;
+                case 'o':
+                    haveOutgoing = true;
+                    break;
+                case 'w':
+                    haveWarnings = true;
+                    break;
+                case 'n':
+                    haveIncoming = false;
+                    haveOutgoing = false;
+                    break;
+                default:
+                    std::cerr << "*** Effects.h: parseLimiterOptArg: Unrecognized option "
+                              << ch << "\n";
+                    returnCode = 2;
+                }  // switch(ch)
+            }      // process optarg char ch
+            mLimit =
+                (haveIncoming && haveOutgoing
+                     ? LIMITER_BOTH
+                     : (haveIncoming ? LIMITER_INCOMING
+                                     : (haveOutgoing ? LIMITER_OUTGOING : LIMITER_NONE)));
+            if (haveWarnings) {
+                limiterWarningAmplitude =
+                    0.5;  // KEEP IN SYNC WITH LIMITER THRESHOLD/CEILING 'softClipLevel'
+                          // in ../faust-src/limiterdsp.dsp
+                // the warning amplitude and limiter compression threshold can of course
+                // be brought as a parameters, e.g. w(0.5)
+            }
+            if (gVerboseFlag) {
+                if (haveIncoming) {
+                    std::cout << "Set up INCOMING Overflow Limiter\n";
+                }
+                if (haveOutgoing) {
+                    std::cout << "Set up OUTGOING Overflow Limiter\n";
+                }
+                if (haveWarnings) {
+                    std::cout << "Enable DISTORTION WARNINGS in Overflow Limiters\n";
+                }
+                if (!haveIncoming && !haveOutgoing) {
+                    std::cout << "Set up NO Overflow Limiters\n";
+                }
+            }  // gVerboseFlag
+        }      // optarg cases
+        return returnCode;
+    }  // parseLimiterOptArg()
+
+    int parseAssumedNumClientsOptArg(char* cmd, char* optarg)
+    {
+        int returnCode = 0;
+        lastEffect     = 'a';  // assumedNumClients
+        char ch        = optarg[0];
+        if (ch == 'h') {
+            printHelp(cmd, 'a');
+            returnCode = 1;
+        } else if (ch == '-' || isalpha(ch) || ch == 0) {
+            std::cerr << cmd << " argument help or integer > 0 is REQUIRED\n";
+            returnCode = 2;
+        } else {
+            mNumClientsAssumed = atoi(optarg);
+            if (mNumClientsAssumed < 1) {
+                std::cerr << "-p ERROR: Must have at least one assumed sound source: "
+                          << atoi(optarg) << " is not supported." << std::endl;
+                returnCode = 2;
+            }
+        }
+        return returnCode;
+    }
+};
diff --git a/src/JMess.cpp b/src/JMess.cpp
new file mode 100644 (file)
index 0000000..2b967f2
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+  JMess: A simple utility so save your jack-audio mess.
+  Incorporated into JackTrip:
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+  JMess itself is:
+  Copyright (C) 2007-2010 Juan-Pablo Caceres.
+
+  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.
+*/
+
+/*
+ * JMess.cpp
+ */
+
+#include "JMess.h"
+
+#include <QDebug>
+
+#include "JackTrip.h"
+#include "jacktrip_globals.h"
+
+// sJackMutex definition
+QMutex JMess::sJMessMutex;
+
+//-------------------------------------------------------------------------------
+/*! \brief Constructs a JMess object that has a jack client.
+ *
+ */
+//-------------------------------------------------------------------------------
+JMess::JMess()
+{
+    // Open a client connection to the JACK server.  Starting a
+    // new server only to list its ports seems pointless, so we
+    // specify JackNoStartServer.
+    mClient = jack_client_open("lsp", JackNoStartServer, &mStatus);
+    if (mClient == NULL) {
+        if (mStatus & JackServerFailed) {
+            std::cerr << "JACK server not running"
+                      << "\n";
+        } else {
+            std::cerr << "jack_client_open() failed, "
+                      << "status = 0x%2.0x\n"
+                      << mStatus << "\n";
+        }
+        exit(1);
+    }
+}
+
+//-------------------------------------------------------------------------------
+/*! \brief Distructor closes the jmess jack audio client.
+ *
+ */
+//-------------------------------------------------------------------------------
+JMess::~JMess()
+{
+    if (jack_client_close(mClient))
+        std::cerr << "ERROR: Could not close the hidden jmess jack client."
+                  << "\n";
+}
+
+//*******************************************************************************
+// connectTUB is called when in hubpatch mode 4 = RESERVEDMATRIX
+// TU Berlin Raspberry Pi ensemble, Winter 2019
+// this gets run on the ensemble's hub server with
+// ./jacktrip -S -p3
+// it connects a set of client jacktrips with known hardwired IP addresses
+// to a known hardwired audio process with known hardwired audio port names
+// when clients connect / disconnect dynamically this just runs through the
+// audio connection sequence bruteforce at every new connection change
+// those that are preexisting won't change
+// a new one will connect accordingly and
+// those that fail because they don't exist will fail, no worries
+
+// setting the connections tested with jacktrip_globals.h
+// const QString gDOMAIN_TRIPLE = QString("130.149.23"); // for TUB multiclient hub
+// const int gMIN_TUB = 215; // lowest client address
+// const int gMAX_TUB = 215; // highest client address
+
+///////////////////////////////
+// test NUC as server
+// #define HARDWIRED_AUDIO_PROCESS_ON_SERVER "par20straightWire"
+// #define ENUMERATE ""
+// #define HARDWIRED_AUDIO_PROCESS_ON_SERVER_IN ":in_"
+// #define HARDWIRED_AUDIO_PROCESS_ON_SERVER_OUT ":out_"
+
+///////////////////////////////
+// test Riviera as server
+// for deployment change jacktrip_globals.h to
+// const QString gDOMAIN_TRIPLE = QString("192.168.0"); // for TUB multiclient hub
+// const int gMIN_TUB = 11; // lowest client address
+// const int gMAX_TUB = 20; // highest client address
+// and give the proper audio process and connection names
+
+#define HARDWIRED_AUDIO_PROCESS_ON_SERVER     "SuperCollider"
+#define HARDWIRED_AUDIO_PROCESS_ON_SERVER_IN  ":in_"
+#define HARDWIRED_AUDIO_PROCESS_ON_SERVER_OUT ":out_"
+// On server side it is SC jack-clients with indivisual names:
+// POE_0...POE_16
+// and each has (at this moment) one port in/out:
+// receive_1
+// send_1
+// I think it should be extended to 4 in/out ports per client.
+
+// this is brute force, does not look at individual clients, just patches the whole
+// ensemble each time
+void JMess::connectTUB(int /*nChans*/)
+// called from UdpHubListener::connectPatch
+{
+    for (int i = 0; i <= gMAX_TUB - gMIN_TUB; i++)  // last IP decimal octet
+        for (int l = 1; l <= 1; l++)  // mono for now // chans are 1-based, 1...2
+        {
+            // jacktrip to SC
+            QString client =
+                gDOMAIN_TRIPLE + QStringLiteral(".") + QString::number(gMIN_TUB + i);
+            QString serverAudio = QStringLiteral(HARDWIRED_AUDIO_PROCESS_ON_SERVER);
+            int tmp =
+                i + l;  // only works for mono... completely wrong for 2 or more chans
+            qDebug() << "connect " << client << ":receive_ " << l << "with "
+                     << serverAudio << HARDWIRED_AUDIO_PROCESS_ON_SERVER_IN << tmp;
+
+            QString left  = QString(client + ":receive_" + QString::number(l));
+            QString right = QString(serverAudio + HARDWIRED_AUDIO_PROCESS_ON_SERVER_IN
+                                    + QString::number(tmp));
+
+            if (0
+                != jack_connect(mClient, left.toStdString().c_str(),
+                                right.toStdString().c_str())) {
+                qDebug() << "WARNING: port: " << left << "and port: " << right
+                         << " could not be connected.";
+            }
+
+            // SC to jacktrip
+            tmp += 4;  // increase tmp for port offset
+            qDebug() << "connect " << serverAudio << HARDWIRED_AUDIO_PROCESS_ON_SERVER_OUT
+                     << tmp << "with " << client << ":send_" << l;
+
+            left  = QString(serverAudio + HARDWIRED_AUDIO_PROCESS_ON_SERVER_OUT
+                            + QString::number(tmp));
+            right = QString(client + ":send_" + QString::number(l));
+
+            if (0
+                != jack_connect(mClient, left.toStdString().c_str(),
+                                right.toStdString().c_str())) {
+                qDebug() << "WARNING: port: " << left << "and port: " << right
+                         << " could not be connected.";
+            }
+        }
+}
diff --git a/src/JMess.h b/src/JMess.h
new file mode 100644 (file)
index 0000000..08b602b
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+  JMess: A simple utility so save your jack-audio mess.
+  Incorporated into JackTrip:
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+  JMess itself is:
+  Copyright (C) 2007-2010 Juan-Pablo Caceres.
+
+  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.
+*/
+
+/*
+ * JMess.h
+ */
+
+#ifndef __JMESS_H
+#define __JMESS_H
+
+#include <QString>
+#ifdef USE_WEAK_JACK
+#include "weak_libjack.h"
+#else
+#include <jack/jack.h>
+#endif
+
+#include <QMutexLocker>
+
+const int Indent = 2;
+
+//-------------------------------------------------------------------------------
+/*! \brief Class to save and load all jack client connections.
+ *
+ * Saves an XML file with all the current jack connections. This same file can
+ * be loaded to connect everything again. The XML file can also be edited.
+ *
+ * Has also an option to disconnect all the clients.
+ */
+//-------------------------------------------------------------------------------
+class JMess
+{
+   public:
+    JMess();
+    virtual ~JMess();
+
+    void connectTUB(int nChans);
+
+   private:
+    jack_client_t* mClient;  // Class client
+    jack_status_t mStatus;   // Class client status
+
+    static QMutex sJMessMutex;  ///< Mutex to make thread safe jack functions that are not
+};
+#endif
diff --git a/src/JTApplication.h b/src/JTApplication.h
new file mode 100644 (file)
index 0000000..7d27b52
--- /dev/null
@@ -0,0 +1,66 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file JTApplication.h
+ * \author Matt Hortoon
+ * \date July 2022
+ */
+
+#ifndef JTAPPLICATION_H
+#define JTAPPLICATION_H
+
+#include <QApplication>
+#include <QDebug>
+#include <QDesktopServices>
+#include <QEvent>
+#include <QFileOpenEvent>
+#include <QObject>
+
+class JTApplication : public QApplication
+{
+    Q_OBJECT
+
+   public:
+    JTApplication(int& argc, char** argv) : QApplication(argc, argv) {}
+
+    bool event(QEvent* event) override
+    {
+        if (event->type() == QEvent::FileOpen) {
+            QFileOpenEvent* openEvent = static_cast<QFileOpenEvent*>(event);
+
+            QDesktopServices::openUrl(openEvent->url());
+        }
+        return QApplication::event(event);
+    }
+};
+
+#endif  // JTAPPLICATION_H
diff --git a/src/JackAudioInterface.cpp b/src/JackAudioInterface.cpp
new file mode 100644 (file)
index 0000000..b78b385
--- /dev/null
@@ -0,0 +1,383 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file JackAudioInterface.cpp
+ * \author Juan-Pablo Caceres
+ * \date June 2008
+ */
+
+#include "JackAudioInterface.h"
+
+#include <cmath>
+#include <cstdlib>
+#include <cstring>
+#include <stdexcept>
+
+#include "JackTrip.h"
+#include "jacktrip_globals.h"
+
+///************PROTORYPE FOR CELT**************************
+// #include <celt/celt.h>
+// #include <celt/celt_header.h>
+// #include <celt/celt_types.h>
+///********************************************************
+
+#include <QMutexLocker>
+#include <QTextStream>
+
+using std::cout;
+using std::endl;
+
+// sJackMutex definition
+QMutex JackAudioInterface::sJackMutex;
+
+//*******************************************************************************
+JackAudioInterface::JackAudioInterface(
+    QVarLengthArray<int> InputChans, QVarLengthArray<int> OutputChans,
+#ifdef WAIR  // wair
+    int NumNetRevChans,
+#endif  // endwhere
+    AudioInterface::audioBitResolutionT AudioBitResolution, bool processWithNetwork,
+    JackTrip* jacktrip, const QString& ClientName)
+    : AudioInterface(InputChans, OutputChans, MIX_UNSET,
+#ifdef WAIR  // wair
+                     NumNetRevChans,
+#endif  // endwhere
+                     AudioBitResolution, processWithNetwork, jacktrip)
+    , mClient(NULL)
+    , mClientName(ClientName)
+    , mBroadcast(false)
+{
+}
+
+//*******************************************************************************
+JackAudioInterface::~JackAudioInterface() {}
+
+//*******************************************************************************
+void JackAudioInterface::setup(bool verbose)
+{
+    setupClient();
+    AudioInterface::setup(verbose);
+    setProcessCallback();
+    if (highLatencyBufferSize()) {
+        AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_BUFFER_LATENCY);
+    } else {
+        AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_NONE);
+    }
+    AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NONE);
+}
+
+//*******************************************************************************
+void JackAudioInterface::setupClient()
+{
+    QByteArray clientName = mClientName.toUtf8();
+    int maxSize           = jack_client_name_size();
+    if (clientName.length() > maxSize) {
+        int length = maxSize;
+        // Make sure we don't cut mid multi-byte character.
+        while ((length > 0) && ((clientName.at(length) & 0xc0) == 0x80)) {
+            length--;
+        }
+        clientName.truncate(length);
+    }
+
+    // was  jack_options_t options = JackNoStartServer;
+    // and then jack_options_t options = JackLoadName;
+    jack_options_t options = JackNullOption;  // from jackSimpleClient example
+    if (mJackTrip == nullptr) {
+        options = JackNoStartServer;
+    }
+    jack_status_t status;
+
+    // Try to connect to the server
+    /// \todo Write better warning messages. This following line displays very
+    /// verbose message, check how to desable them.
+    {
+        QMutexLocker locker(&sJackMutex);
+        // TODO: this needs a timeout because it will hang indefinitely
+        // if the Jack server is not running
+// #ifndef WAIR // WAIR
+//         mClient = jack_client_open (client_name, options, &status, server_name);
+// #else
+//         mClient = jack_client_open (client_name, JackUseExactName, &status,
+//         server_name);
+// #endif // endwhere
+#ifndef WAIR  // WAIR
+        mClient = jack_client_open(clientName.constData(), options, &status);
+#else
+        mClient = jack_client_open(clientName.constData(), JackUseExactName, &status);
+#endif  // endwhere
+    }
+
+    if (mClient == NULL) {
+        // fprintf (stderr, "jack_client_open() failed, "
+        //          "status = 0x%2.0x\n", status);
+        if (status & JackServerFailed) {
+            fprintf(stderr, "Unable to connect to JACK server\n");
+            // std::cerr << "ERROR: Maybe the JACK server is not running?" << std::endl;
+            // std::cerr << gPrintSeparator << std::endl;
+        }
+        // std::exit(1);
+        throw std::runtime_error("Maybe the JACK server is not running?");
+    }
+
+    mAssignedClientName = jack_get_client_name(mClient);
+    if (status & JackServerStarted) {
+        fprintf(stderr, "JACK server started\n");
+    }
+    if (status & JackNameNotUnique) {
+        fprintf(stderr, "unique name `%s' assigned\n",
+                mAssignedClientName.toUtf8().constData());
+    }
+
+    // Set function to call if Jack shuts down
+    jack_on_info_shutdown(mClient, this->jackShutdown, this);
+
+    // Create input and output channels
+    createChannels();
+
+    // Buffer size member
+    mNumFrames = getBufferSizeInSamples();
+
+    // Initialize Buffer array to read and write audio
+    mInBuffer.resize(getNumInputChannels());
+    mOutBuffer.resize(getNumOutputChannels());
+    mBroadcastBuffer.resize(getNumOutputChannels());
+}
+
+//*******************************************************************************
+void JackAudioInterface::createChannels()
+{
+    // Create Input Ports
+    mInPorts.resize(getNumInputChannels());
+    for (int i = 0; i < getNumInputChannels(); i++) {
+        QString inName;
+        QTextStream(&inName) << "send_" << i + 1;
+        mInPorts[i] =
+            jack_port_register(mClient, inName.toLatin1(), JACK_DEFAULT_AUDIO_TYPE,
+                               JackPortIsInput | JackPortIsTerminal, 0);
+    }
+
+    // Create Output Ports
+    mOutPorts.resize(getNumOutputChannels());
+    for (int i = 0; i < getNumOutputChannels(); i++) {
+        QString outName;
+        QTextStream(&outName) << "receive_" << i + 1;
+        mOutPorts[i] =
+            jack_port_register(mClient, outName.toLatin1(), JACK_DEFAULT_AUDIO_TYPE,
+                               JackPortIsOutput | JackPortIsTerminal, 0);
+    }
+    // Create Broadcast Ports
+    if (mBroadcast) {
+        mBroadcastPorts.resize(getNumOutputChannels());
+        for (int i = 0; i < getNumOutputChannels(); i++) {
+            QString outName;
+            QTextStream(&outName) << "broadcast_" << i + 1;
+            mBroadcastPorts[i] =
+                jack_port_register(mClient, outName.toLatin1(), JACK_DEFAULT_AUDIO_TYPE,
+                                   JackPortIsOutput | JackPortIsTerminal, 0);
+        }
+    }
+}
+
+//*******************************************************************************
+uint32_t JackAudioInterface::getSampleRate() const
+{
+    return jack_get_sample_rate(mClient);
+}
+
+//*******************************************************************************
+uint32_t JackAudioInterface::getBufferSizeInSamples() const
+{
+    return jack_get_buffer_size(mClient);
+}
+
+//*******************************************************************************
+size_t JackAudioInterface::getSizeInBytesPerChannel() const
+{
+    return (getBufferSizeInSamples() * getAudioBitResolution() / 8);
+}
+
+//*******************************************************************************
+void JackAudioInterface::setProcessCallback()
+{
+    std::cout << "Setting JACK Process Callback..." << std::endl;
+    if (int code = jack_set_process_callback(
+            mClient, JackAudioInterface::wrapperProcessCallback, this)) {
+        // std::cerr << "Could not set the process callback" << std::endl;
+        // return(code);
+        (void)code;  // to avoid compiler warnings
+        throw std::runtime_error("Could not set the Jack process callback");
+        // std::exit(1);
+    }
+    std::cout << "SUCCESS" << std::endl;
+    std::cout << gPrintSeparator << std::endl;
+    // return(0);
+}
+
+//*******************************************************************************
+int JackAudioInterface::startProcess()
+{
+    // Tell the JACK server that we are ready to roll.  Our
+    // process() callback will start running now.
+    if (int code = (jack_activate(mClient))) {
+        std::cerr << "Cannot activate JACK client" << std::endl;
+        return (code);
+    }
+    return (0);
+}
+
+//*******************************************************************************
+int JackAudioInterface::stopProcess()
+{
+    QMutexLocker locker(&sJackMutex);
+    int code = (jack_deactivate(mClient));
+    if (code != 0) {
+        std::cerr << "Cannot deactivate JACK client" << std::endl;
+        return (code);
+    }
+    code = (jack_client_close(mClient));
+    if (code != 0) {
+        std::cerr << "Cannot disconnect JACK client" << std::endl;
+        return (code);
+    }
+    return (0);
+}
+
+//*******************************************************************************
+void JackAudioInterface::jackShutdown(jack_status_t /*code*/, const char* reason,
+                                      void* arg)
+{
+    std::string errorMsg = "The Jack server was shut down";
+    if (reason != nullptr) {
+        errorMsg += ": ";
+        errorMsg += reason;
+    }
+    if (arg != nullptr) {
+        JackAudioInterface* ifPtr = static_cast<JackAudioInterface*>(arg);
+        ifPtr->mErrorMsg          = errorMsg;
+        if (ifPtr->mErrorCallback) {
+            ifPtr->mErrorCallback(errorMsg);
+        }
+    }
+    std::cerr << errorMsg << std::endl;
+    JackTrip::sAudioStopped = true;
+}
+
+//*******************************************************************************
+int JackAudioInterface::processCallback(jack_nframes_t nframes)
+{
+    if (mProcessingAudio) {
+        std::cerr << "*** JackAudioInterface.cpp: DROPPED A BUFFER because "
+                     "AudioInterface::callback() not finished\n";
+        return 1;
+    }
+
+    // Get input and output buffers from JACK
+    //-------------------------------------------------------------------
+    for (int i = 0; i < getNumInputChannels(); i++) {
+        // Input Ports are READ ONLY and change as needed (no locks) - make a copy for
+        // debugging
+        mInBuffer[i] = (sample_t*)jack_port_get_buffer(mInPorts[i], nframes);
+    }
+    for (int i = 0; i < getNumOutputChannels(); i++) {
+        // Output Ports are WRITABLE
+        mOutBuffer[i] = (sample_t*)jack_port_get_buffer(mOutPorts[i], nframes);
+    }
+    //-------------------------------------------------------------------
+    // TEST: Loopback
+    // To test, uncomment and send audio to client input. The same audio
+    // should come out as output in the first channel
+    // memcpy (mOutBuffer[0], mInBuffer[0], sizeof(sample_t) * nframes);
+    // memcpy (mOutBuffer[1], mInBuffer[1], sizeof(sample_t) * nframes);
+    //-------------------------------------------------------------------
+
+    AudioInterface::callback(mInBuffer, mOutBuffer, nframes);
+
+    if (mBroadcast) {
+        for (int i = 0; i < getNumOutputChannels(); i++) {
+            // Broadcast Ports are WRITABLE
+            mBroadcastBuffer[i] =
+                (sample_t*)jack_port_get_buffer(mBroadcastPorts[i], nframes);
+        }
+        AudioInterface::broadcastCallback(mBroadcastBuffer, nframes);
+    }
+    return 0;
+}
+
+//*******************************************************************************
+int JackAudioInterface::wrapperProcessCallback(jack_nframes_t nframes, void* arg)
+{
+    return static_cast<JackAudioInterface*>(arg)->processCallback(nframes);
+}
+
+//*******************************************************************************
+void JackAudioInterface::connectDefaultPorts()
+{
+    const char** ports;
+
+    // Get physical output (capture) ports
+    if ((ports =
+             jack_get_ports(mClient, NULL, NULL, JackPortIsPhysical | JackPortIsOutput))
+        == NULL) {
+        cout << "WARNING: Cannot find any physical capture ports" << endl;
+    } else {
+        // Connect capure ports to jacktrip send
+        for (int i = 0; i < getNumInputChannels(); i++) {
+            // Check that we don't run out of capture ports
+            if (ports[i] != NULL) {
+                jack_connect(mClient, ports[i], jack_port_name(mInPorts[i]));
+            } else {
+                break;
+            }
+        }
+        jack_free(ports);
+    }
+
+    // Get physical input (playback) ports
+    if ((ports =
+             jack_get_ports(mClient, NULL, NULL, JackPortIsPhysical | JackPortIsInput))
+        == NULL) {
+        cout << "WARNING: Cannot find any physical playback ports" << endl;
+    } else {
+        // Connect playback ports to jacktrip receive
+        for (int i = 0; i < getNumOutputChannels(); i++) {
+            // Check that we don't run out of capture ports
+            if (ports[i] != NULL) {
+                jack_connect(mClient, jack_port_name(mOutPorts[i]), ports[i]);
+            } else {
+                break;
+            }
+        }
+        jack_free(ports);
+    }
+}
diff --git a/src/JackAudioInterface.h b/src/JackAudioInterface.h
new file mode 100644 (file)
index 0000000..b6fc6c0
--- /dev/null
@@ -0,0 +1,199 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file JackAudioInterface.h
+ * \author Juan-Pablo Caceres
+ * \date June 2008
+ */
+
+#ifndef __JACKAUDIOINTERFACE_H__
+#define __JACKAUDIOINTERFACE_H__
+
+#include <iostream>
+// #include <tr1/memory> //for shared_ptr
+#ifdef USE_WEAK_JACK
+#include "weak_libjack.h"
+#else
+#include <jack/jack.h>
+#endif
+
+#include <QMutex>
+#include <QVarLengthArray>
+#include <QVector>
+#include <functional>  //for mem_fun_ref
+
+#include "AudioInterface.h"
+#include "ProcessPlugin.h"
+#include "jacktrip_types.h"
+
+// class JackTrip; //forward declaration
+
+/** \brief Class that provides an interface with the Jack Audio Server
+ *
+ * \todo implement srate_callback
+ * \todo automatically starts jack with buffer and sample rate settings specified by the
+ * user
+ */
+class JackAudioInterface : public AudioInterface
+{
+   public:
+    /** \brief The class constructor
+     * \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(
+        QVarLengthArray<int> InputChans, QVarLengthArray<int> 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) 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() override;
+    /** \brief Stops the process-callback thread
+     * \return 0 on success, otherwise a non-zero error code
+     */
+    virtual int stopProcess() override;
+    /// \brief Connect the default ports, capture to sends, and receives to playback
+    void connectDefaultPorts() override;
+
+    //--------------SETTERS---------------------------------------------
+    /// \brief Set Client Name to something different that the default (JackTrip)
+    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*/) override
+    {
+        std::cout << "WARNING: Setting the Sample Rate in Jack mode has no effect."
+                  << std::endl;
+    }
+    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 override;
+    /// \brief Get the Jack Server Buffer Size, in samples
+    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 override;
+    //------------------------------------------------------------------
+
+   private:
+    /** \brief Private method to setup a client of the Jack server.
+     * \exception std::runtime_error Can't start Jack
+     *
+     * This method is called by the class constructors. It does the following:\n
+     *  - Connects to the JACK server
+     *  - Sets the shutdown process callback
+     *  - Creates the appropriate number of input and output channels
+     */
+    void setupClient();
+    /// \brief Creates input and output channels in the Jack client
+    void createChannels();
+    /** \brief JACK calls this shutdown_callback if the server ever shuts down or
+     * decides to disconnect the client and has a message to deliver.
+     */
+    static void jackShutdown(jack_status_t code, const char* reason, void* arg);
+    /** \brief Set the process callback of the member function processCallback.
+     * This process will be called by the JACK server whenever there is work to be done.
+     */
+    void setProcessCallback();
+    /** \brief JACK process callback
+     *
+     * This is the function to be called to process audio. This function is
+     * of the type JackProcessCallback, which is defined as:\n
+     * <tt>typedef int(* JackProcessCallback)(jack_nframes_t nframes, void *arg)</tt>
+     * \n
+     * See
+     * http://jackaudio.org/files/docs/html/types_8h.html#4923142208a8e7dacf00ca7a10681d2b
+     * for more details
+     */
+    int processCallback(jack_nframes_t nframes);
+    /** \brief Wrapper to cast the member processCallback to a static function pointer
+     * that can be used with <tt>jack_set_process_callback</tt>
+     *
+     * <tt>jack_set_process_callback</tt> needs a static member function pointer. A normal
+     * member function won't work because a <b><i>this</i></b> pointer is passed under the
+     * scenes. That's why we need to cast the member function processCallback to the
+     * static function wrapperProcessCallback. The callback is then set as:\n
+     * <tt>jack_set_process_callback(mClient, JackAudioInterface::wrapperProcessCallback,
+     *                              this)</tt>
+     */
+    // reference : http://article.gmane.org/gmane.comp.audio.jackit/12873
+    static int wrapperProcessCallback(jack_nframes_t nframes, void* arg);
+
+    int mNumFrames;  ///< Buffer block size, in samples
+
+    jack_client_t* mClient;  ///< Jack Client
+    QString mClientName;     ///< Jack Client Name
+    QString mAssignedClientName;
+    QVarLengthArray<jack_port_t*> mInPorts;         ///< Vector of Input Ports (Channels)
+    QVarLengthArray<jack_port_t*> mOutPorts;        ///< Vector of Output Ports (Channels)
+    QVarLengthArray<jack_port_t*> mBroadcastPorts;  ///< Vector of Output Ports (Channels)
+    QVarLengthArray<sample_t*>
+        mInBuffer;  ///< Vector of Input buffers/channel read from JACK
+    QVarLengthArray<sample_t*>
+        mOutBuffer;  ///< Vector of Output buffer/channel to write to JACK
+    QVarLengthArray<sample_t*>
+        mBroadcastBuffer;  ///< Vector of Output buffer/channel to write to JACK
+    bool mBroadcast;
+    static QMutex sJackMutex;  ///< Mutex to make thread safe jack functions that are not
+};
+
+#endif
diff --git a/src/JackTrip.cpp b/src/JackTrip.cpp
new file mode 100644 (file)
index 0000000..ad5bd5b
--- /dev/null
@@ -0,0 +1,1582 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2024 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 JackTrip.cpp
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+#include "JackTrip.h"
+
+#ifndef NO_JACK
+#include "JackAudioInterface.h"
+#endif
+#include "Auth.h"
+#include "JitterBuffer.h"
+#include "Regulator.h"
+#include "RingBufferWavetable.h"
+#include "UdpDataProtocol.h"
+#include "jacktrip_globals.h"
+#ifdef RT_AUDIO
+#include "RtAudioInterface.h"
+#endif
+
+#include <QDateTime>
+#include <QHostAddress>
+#include <QHostInfo>
+#include <QRandomGenerator>
+#include <QThread>
+#include <QTimer>
+#include <QtEndian>
+#include <QtGlobal>
+#include <cstdlib>
+#include <iomanip>
+#include <iostream>
+#include <stdexcept>
+using std::setw;
+
+using std::cout;
+using std::endl;
+
+// the following function has to remain outside the Jacktrip class definition
+// its purpose is to close the app when control c is hit by the user in rtaudio/asio4all
+// mode
+/*if defined _WIN32
+void sigint_handler(int sig)
+{
+    exit(0);
+}
+#endif*/
+
+bool JackTrip::sSigInt       = false;
+bool JackTrip::sAudioStopped = false;
+
+//*******************************************************************************
+JackTrip::JackTrip(jacktripModeT JacktripMode, dataProtocolT DataProtocolType,
+                   int BaseChanIn, int NumChansIn, int BaseChanOut, int NumChansOut,
+                   AudioInterface::inputMixModeT InputMixMode,
+#ifdef WAIR  // WAIR
+                   int NumNetRevChans,
+#endif  // endwhere
+                   int BufferQueueLength, unsigned int redundancy,
+                   AudioInterface::audioBitResolutionT AudioBitResolution,
+                   DataProtocol::packetHeaderTypeT PacketHeaderType,
+                   underrunModeT UnderRunMode, int receiver_bind_port,
+                   int sender_bind_port, int receiver_peer_port, int sender_peer_port,
+                   int tcp_peer_port, QObject* parent)
+    : QObject(parent)
+    , mJackTripMode(JacktripMode)
+    , mDataProtocol(DataProtocolType)
+    , mPacketHeaderType(PacketHeaderType)
+    , mAudiointerfaceMode(JackTrip::JACK)
+    , mBaseAudioChanIn(BaseChanIn)
+    , mNumAudioChansIn(NumChansIn)
+    , mBaseAudioChanOut(BaseChanOut)
+    , mNumAudioChansOut(NumChansOut)
+    , mInputMixMode(InputMixMode)
+#ifdef WAIR  // WAIR
+    , mNumNetRevChans(NumNetRevChans)
+#endif  // endwhere
+    , mBufferQueueLength(BufferQueueLength)
+    , mBufferStrategy(1)
+    , mBroadcastQueueLength(0)
+    , mSampleRate(gDefaultSampleRate)
+    , mDeviceID(gDefaultDeviceID)
+    , mAudioBufferSize(gDefaultBufferSizeInSamples)
+    , mAudioBitResolution(AudioBitResolution)
+    , mLoopBack(false)
+    , mDataProtocolSender(NULL)
+    , mDataProtocolReceiver(NULL)
+    , mAudioInterface(NULL)
+    , mPacketHeader(NULL)
+    , mUnderRunMode(UnderRunMode)
+    , mStopOnTimeout(false)
+    , mSendRingBuffer(NULL)
+    , mReceiveRingBuffer(NULL)
+    , mReceiverBindPort(receiver_bind_port)
+    , mSenderPeerPort(sender_peer_port)
+    , mSenderBindPort(sender_bind_port)
+    , mReceiverPeerPort(receiver_peer_port)
+    , mTcpServerPort(tcp_peer_port)
+    , mUseAuth(false)
+    , mRedundancy(redundancy)
+    , mTimeoutTimer(this)
+    , mRetryTimer(this)
+    , mRetries(0)
+    , mSleepTime(100)
+    , mElapsedTime(0)
+    , mEndTime(0)
+    , mTcpClient(this)
+    , mUdpSockTemp(this)
+    , mAwaitingUdp(false)
+    , mAwaitingTcp(false)
+    , mReceivedConnection(false)
+    , mTcpConnectionError(false)
+    , mStopped(false)
+    , mHasShutdown(false)
+    , mConnectDefaultAudioPorts(true)
+    , mIOStatTimeout(0)
+    , mIOStatLogStream(std::cout.rdbuf())
+    , mSimulatedLossRate(0.0)
+    , mSimulatedJitterRate(0.0)
+    , mSimulatedDelayRel(0.0)
+    , mUseRtUdpPriority(false)
+{
+    createHeader(mPacketHeaderType);
+    sAudioStopped = false;
+}
+
+//*******************************************************************************
+JackTrip::~JackTrip()
+{
+    // wait();
+    stop();
+    delete mDataProtocolSender;
+    delete mDataProtocolReceiver;
+    delete mAudioInterface;
+    delete mPacketHeader;
+    delete mSendRingBuffer;
+    delete mReceiveRingBuffer;
+}
+
+//*******************************************************************************
+void JackTrip::setupAudio(
+#ifdef WAIRTOHUB  // WAIR
+    [[maybe_unused]] int ID
+#endif  // endwhere
+)
+{
+    // Check if mAudioInterface has already been created or not
+    if (mAudioInterface != nullptr)
+        return;
+
+    // Create AudioInterface Client Object
+    if (mAudiointerfaceMode == JackTrip::JACK) {
+#ifndef NO_JACK
+        if (gVerboseFlag)
+            std::cout << "  JackTrip:setupAudio before new JackAudioInterface"
+                      << std::endl;
+        QVarLengthArray<int> inputChannels;
+        QVarLengthArray<int> 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,
+#endif  // endwhere
+                                                 mAudioBitResolution, true, this);
+
+#ifdef WAIRTOHUB  // WAIR
+
+        // Set our Jack client name if we're a hub server or a custom name hasn't been set
+        if (mJackClientName.isEmpty()) {
+            if (!mPeerAddress.isEmpty()) {
+                mJackClientName =
+                    QString(mPeerAddress).replace(QLatin1String(":"), QLatin1String("_"));
+            } else {
+                mJackClientName = gJackDefaultClientName;
+            }
+        }
+        // std::cout  << "WAIR ID " << ID << " jacktrip client name set to=" <<
+        //              mJackClientName.toStdString() << std::endl;
+
+#endif  // endwhere
+        mAudioInterface->setClientName(mJackClientName);
+        if (0 < mBroadcastQueueLength) {
+            mAudioInterface->enableBroadcastOutput();
+        }
+
+        if (gVerboseFlag)
+            std::cout << "  JackTrip:setupAudio before mAudioInterface->setup"
+                      << std::endl;
+        mAudioInterface->setup(true);
+        if (gVerboseFlag)
+            std::cout << "  JackTrip:setupAudio before mAudioInterface->getSampleRate"
+                      << std::endl;
+        mSampleRate = mAudioInterface->getSampleRate();
+        if (gVerboseFlag)
+            std::cout << "  JackTrip:setupAudio before mAudioInterface->getDeviceID"
+                      << std::endl;
+        mDeviceID = mAudioInterface->getDeviceID();
+        if (gVerboseFlag)
+            std::cout
+                << "  JackTrip:setupAudio before mAudioInterface->getBufferSizeInSamples"
+                << std::endl;
+        mAudioBufferSize = mAudioInterface->getBufferSizeInSamples();
+#endif          //__NON_JACK__
+#ifdef NO_JACK  /// \todo FIX THIS REPETITION OF CODE
+#ifdef RT_AUDIO
+        cout << "Warning: using non jack version, RtAudio will be used instead" << endl;
+        QVarLengthArray<int> inputChannels;
+        QVarLengthArray<int> 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);
+        mAudioInterface->setOutputDevice(mOutputDeviceName);
+        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
+        QVarLengthArray<int> inputChannels;
+        QVarLengthArray<int> 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);
+        mAudioInterface->setOutputDevice(mOutputDeviceName);
+        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
+    }
+
+    mAudioInterface->setLoopBack(mLoopBack);
+    if (!mAudioTesterP.isNull()) {  // if we're a hub server, this will be a nullptr -
+                                    // MAJOR REFACTOR NEEDED, in my opinion
+        mAudioTesterP->setSampleRate(mSampleRate);
+    }
+    mAudioInterface->setAudioTesterP(mAudioTesterP.data());
+
+    std::cout << "The Sampling Rate is: " << mSampleRate << std::endl;
+    std::cout << gPrintSeparator << std::endl;
+    int AudioBufferSizeInBytes = mAudioBufferSize * sizeof(sample_t);
+    std::cout << "The Audio Buffer Size is: " << mAudioBufferSize << " samples"
+              << std::endl;
+    std::cout << "                      or: " << AudioBufferSizeInBytes << " bytes"
+              << std::endl;
+    if (0 < mBroadcastQueueLength) {
+        std::cout << gPrintSeparator << std::endl;
+        cout << "Broadcast Output is enabled, delay = "
+             << mBroadcastQueueLength * mAudioBufferSize * 1000 / mSampleRate << " ms"
+             << " (" << mBroadcastQueueLength * mAudioBufferSize << " samples)" << endl;
+    }
+    std::cout << gPrintSeparator << std::endl;
+    cout << "The Number of Channels is: " << mAudioInterface->getNumInputChannels()
+         << endl;
+    std::cout << gPrintSeparator << std::endl;
+    QThread::usleep(100);
+}
+
+//*******************************************************************************
+void JackTrip::closeAudio()
+{
+    // mAudioInterface->close();
+    if (mAudioInterface != NULL) {
+        mAudioInterface->stopProcess();
+        delete mAudioInterface;
+        mAudioInterface = NULL;
+    }
+}
+
+//*******************************************************************************
+void JackTrip::setupDataProtocol()
+{
+    double simulated_max_delay =
+        mSimulatedDelayRel * getBufferSizeInSamples() / getSampleRate();
+    // Create DataProtocol Objects
+    switch (mDataProtocol) {
+    case UDP:
+        std::cout << "Using UDP Protocol" << std::endl;
+        QThread::usleep(100);
+        mDataProtocolSender =
+            new UdpDataProtocol(this, DataProtocol::SENDER,
+                                // mSenderPeerPort, mSenderBindPort,
+                                mSenderBindPort, mSenderPeerPort, mRedundancy);
+        mDataProtocolReceiver =
+            new UdpDataProtocol(this, DataProtocol::RECEIVER, mReceiverBindPort,
+                                mReceiverPeerPort, mRedundancy);
+        if (0.0 < mSimulatedLossRate || 0.0 < mSimulatedJitterRate
+            || 0.0 < simulated_max_delay) {
+            mDataProtocolReceiver->setIssueSimulation(
+                mSimulatedLossRate, mSimulatedJitterRate, simulated_max_delay);
+        }
+        mDataProtocolSender->setUseRtPriority(mUseRtUdpPriority);
+        mDataProtocolReceiver->setUseRtPriority(mUseRtUdpPriority);
+        if (mUseRtUdpPriority) {
+            cout << "Using RT thread priority for UDP data" << endl;
+        }
+        std::cout << gPrintSeparator << std::endl;
+        break;
+    case TCP:
+        throw std::invalid_argument("TCP Protocol is not implemented");
+        break;
+    case SCTP:
+        throw std::invalid_argument("SCTP Protocol is not implemented");
+        break;
+    default:
+        throw std::invalid_argument("Protocol not defined or unimplemented");
+        break;
+    }
+
+    // JackTrip's inputs send to the network
+    mDataProtocolSender->setAudioPacketSize(getTotalAudioInputPacketSizeInBytes());
+
+    // JackTrip's outputs receive from the network
+    mDataProtocolReceiver->setAudioPacketSize(getTotalAudioOutputPacketSizeInBytes());
+}
+
+//*******************************************************************************
+void JackTrip::setupRingBuffers()
+{
+    // Create RingBuffers with the apprioprate size
+    /// \todo Make all this operations cleaner
+    // int total_audio_packet_size = getTotalAudioPacketSizeInBytes();
+    int audio_input_slot_size  = getInputRingBuffersSlotSize();
+    int audio_output_slot_size = getOutputRingBuffersSlotSize();
+    if (0 <= mBufferStrategy) {
+        mUnderRunMode = ZEROS;
+    } else if (0 > mBufferQueueLength) {
+        throw std::invalid_argument("Auto queue is not supported by RingBuffer");
+    }
+
+    switch (mUnderRunMode) {
+    case WAVETABLE:
+        mSendRingBuffer =
+            new RingBufferWavetable(audio_input_slot_size, gDefaultOutputQueueLength);
+        mReceiveRingBuffer =
+            new RingBufferWavetable(audio_output_slot_size, mBufferQueueLength);
+        mPacketHeader->setBufferRequiresSameSettings(true);
+        break;
+    case ZEROS:
+        mSendRingBuffer =
+            new RingBuffer(audio_input_slot_size, gDefaultOutputQueueLength);
+        if (0 > mBufferStrategy) {
+            mReceiveRingBuffer =
+                new RingBuffer(audio_output_slot_size, mBufferQueueLength);
+            mPacketHeader->setBufferRequiresSameSettings(true);
+        } else if ((mBufferStrategy == 3) || (mBufferStrategy == 4)) {
+            cout << "Using experimental buffer strategy " << mBufferStrategy
+                 << "-- Regulator with PLC" << endl;
+            Regulator* regulator_ptr =
+                new Regulator(mNumAudioChansOut, mAudioBitResolution, mAudioBufferSize,
+                              mBufferQueueLength, mBroadcastQueueLength, mSampleRate);
+            mReceiveRingBuffer = regulator_ptr;
+            // bufStrategy 3 or 4, mBufferQueueLength is in integer msec not packets
+
+            mPacketHeader->setBufferRequiresSameSettings(false);  // = asym is default
+
+            if (0 < mBroadcastQueueLength) {
+                mAudioInterface->enableBroadcastOutput();
+            }
+
+        } else {
+            cout << "Using JitterBuffer strategy " << mBufferStrategy << endl;
+            if (0 > mBufferQueueLength) {
+                cout << "Using AutoQueue 1/" << -mBufferQueueLength << endl;
+            }
+            mReceiveRingBuffer = new JitterBuffer(
+                mAudioBufferSize, mBufferQueueLength, mSampleRate, mBufferStrategy,
+                mBroadcastQueueLength, mNumAudioChansOut, mAudioBitResolution);
+            static_cast<JitterBuffer*>(mReceiveRingBuffer)->setJackTrip(this);
+        }
+        break;
+    default:
+        throw std::invalid_argument("Underrun Mode undefined");
+        break;
+    }
+}
+
+//*******************************************************************************
+void JackTrip::setPeerAddress(const QString& PeerHostOrIP)
+{
+    mPeerAddress = PeerHostOrIP;
+}
+
+//*******************************************************************************
+void JackTrip::appendProcessPluginToNetwork(ProcessPlugin* plugin)
+{
+    if (plugin) {
+        mProcessPluginsToNetwork.append(plugin);  // ownership transferred
+        // mAudioInterface->appendProcessPluginToNetwork(plugin);
+    }
+}
+
+//*******************************************************************************
+void JackTrip::appendProcessPluginFromNetwork(ProcessPlugin* plugin)
+{
+    if (plugin) {
+        mProcessPluginsFromNetwork.append(plugin);  // ownership transferred
+        // mAudioInterface->appendProcessPluginFromNetwork(plugin);
+    }
+}
+
+//*******************************************************************************
+void JackTrip::appendProcessPluginToMonitor(ProcessPlugin* plugin)
+{
+    if (plugin) {
+        mProcessPluginsToMonitor.append(plugin);  // ownership transferred
+        // mAudioInterface->appendProcessPluginFromNetwork(plugin);
+    }
+}
+
+//*******************************************************************************
+void JackTrip::startProcess(
+#ifdef WAIRTOHUB  // WAIR
+    int ID
+#endif  // endwhere
+)
+{  // signal that catches ctrl c in rtaudio-asio mode
+    /*#if defined (_WIN32)
+    if (signal(SIGINT, sigint_handler) == SIG_ERR) {
+        perror("signal");
+        exit(1);
+    }
+#endif*/
+    // Check if ports are already binded by another process on this machine
+    // ------------------------------------------------------------------
+    if (gVerboseFlag)
+        std::cout << "step 1" << std::endl;
+
+    if (gVerboseFlag)
+        std::cout
+            << "  JackTrip:startProcess before checkIfPortIsBinded(mReceiverBindPort)"
+            << std::endl;
+#if defined _WIN32
+        // cc fixed windows crash with this print statement!
+        // qDebug() << "before mJackTrip->startProcess" << mReceiverBindPort<<
+        // mSenderBindPort;
+#endif
+    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;
+    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)
+        std::cout << "  JackTrip:startProcess before setupAudio" << std::endl;
+    setupAudio(
+#ifdef WAIRTOHUB  // wair
+        ID
+#endif  // endwhere
+    );
+
+    QString audioInterfaceError =
+        QString::fromStdString(mAudioInterface->getDevicesErrorMsg());
+    if (audioInterfaceError != "") {
+        stop(audioInterfaceError);
+        return;
+    }
+
+    // AudioInterface::setup() can return a different buffer size
+    // if the audio interface doesn't support the one that was requested
+    if (mAudioInterface->getBufferSizeInSamples() != getBufferSizeInSamples()) {
+        setAudioBufferSizeInSamples(mAudioInterface->getBufferSizeInSamples());
+    }
+
+    // cc redundant with instance creator  createHeader(mPacketHeaderType); next line
+    // fixme
+    createHeader(mPacketHeaderType);
+    setupDataProtocol();
+    setupRingBuffers();
+    // Connect Signals and Slots
+    // -------------------------
+    QObject::connect(mPacketHeader, &PacketHeader::signalError, this,
+                     &JackTrip::slotStopProcessesDueToError, Qt::QueuedConnection);
+    QObject::connect(mDataProtocolReceiver,
+                     &DataProtocol::signalReceivedConnectionFromPeer, this,
+                     &JackTrip::slotReceivedConnectionFromPeer, Qt::QueuedConnection);
+    // QObject::connect(this, SIGNAL(signalUdpTimeOut()),
+    //                 this, SLOT(slotStopProcesses()), Qt::QueuedConnection);
+    QObject::connect(static_cast<UdpDataProtocol*>(mDataProtocolReceiver),
+                     &UdpDataProtocol::signalUdpWaitingTooLong, this,
+                     &JackTrip::slotUdpWaitingTooLong, Qt::QueuedConnection);
+    QObject::connect(mDataProtocolSender, &DataProtocol::signalCeaseTransmission, this,
+                     &JackTrip::slotStopProcessesDueToError, Qt::QueuedConnection);
+    QObject::connect(mDataProtocolReceiver, &DataProtocol::signalCeaseTransmission, this,
+                     &JackTrip::slotStopProcessesDueToError, Qt::QueuedConnection);
+
+    // QObject::connect(mDataProtocolSender, SIGNAL(signalError(const char*)),
+    //                 this, SLOT(slotStopProcesses()), Qt::QueuedConnection);
+    QObject::connect(mDataProtocolReceiver, &DataProtocol::signalError, this,
+                     &JackTrip::slotStopProcessesDueToError, Qt::QueuedConnection);
+
+    // Start the threads for the specific mode
+    // ---------------------------------------
+    switch (mJackTripMode) {
+    case CLIENT:
+        if (gVerboseFlag)
+            std::cout << "step 2c client only" << std::endl;
+        if (gVerboseFlag)
+            std::cout << "  JackTrip:startProcess case CLIENT before clientStart"
+                      << std::endl;
+        clientStart();
+        break;
+    case SERVER:
+        if (gVerboseFlag)
+            std::cout << "step 2s server only" << std::endl;
+        if (gVerboseFlag)
+            std::cout << "  JackTrip:startProcess case SERVER before serverStart"
+                      << std::endl;
+        serverStart();
+        break;
+    case CLIENTTOPINGSERVER:
+        if (gVerboseFlag)
+            std::cout << "step 2C client only" << std::endl;
+        if (gVerboseFlag)
+            std::cout << "  JackTrip:startProcess case CLIENTTOPINGSERVER before "
+                         "clientPingToServerStart"
+                      << std::endl;
+        if (clientPingToServerStart()
+            == -1) {  // if error on server start (-1) we return immediately
+            stop(QStringLiteral(
+                "Peer Address has to be set if you run in CLIENTTOPINGSERVER mode"));
+            return;
+        }
+        break;
+    case SERVERPINGSERVER:
+        if (gVerboseFlag)
+            std::cout << "step 2S server only (same as 2s)" << std::endl;
+        if (gVerboseFlag)
+            std::cout
+                << "  JackTrip:startProcess case SERVERPINGSERVER before serverStart"
+                << std::endl;
+        if (serverStart(true)
+            == -1) {  // if error on server start (-1) we return immediately
+            stop();
+            return;
+        }
+        break;
+    default:
+        throw std::invalid_argument("Jacktrip Mode undefined");
+        break;
+    }
+}
+
+void JackTrip::completeConnection()
+{
+    // Have the threads share a single socket that operates at full duplex.
+#if defined(_WIN32)
+    SOCKET sock_fd = INVALID_SOCKET;
+#else
+    int sock_fd = -1;
+#endif
+    mDataProtocolReceiver->setSocket(sock_fd);
+    mDataProtocolSender->setSocket(sock_fd);
+
+    // Start Threads
+    if (gVerboseFlag)
+        std::cout << "  JackTrip:startProcess before mDataProtocolReceiver->start"
+                  << std::endl;
+    mDataProtocolReceiver->start();
+    mDataProtocolReceiver->waitForStart();
+    if (gVerboseFlag)
+        std::cout << "  JackTrip:startProcess before mDataProtocolSender->start"
+                  << std::endl;
+    mDataProtocolSender->start();
+    mDataProtocolSender->waitForStart();
+    /*
+     * changed order so that audio starts after receiver and sender
+     * because UdpDataProtocol:run0 before setRealtimeProcessPriority()
+     * causes an audio hiccup from jack JackPosixSemaphore::TimedWait err = Interrupted
+     */
+    if (gVerboseFlag)
+        std::cout << "step 5" << std::endl;
+    if (gVerboseFlag)
+        std::cout << "  JackTrip:startProcess before mAudioInterface->startProcess"
+                  << std::endl;
+    for (auto& i : mProcessPluginsFromNetwork) {
+        mAudioInterface->appendProcessPluginFromNetwork(i);
+    }
+
+    for (auto& i : mProcessPluginsToNetwork) {
+        mAudioInterface->appendProcessPluginToNetwork(i);
+    }
+
+    for (auto& i : mProcessPluginsToMonitor) {
+        mAudioInterface->appendProcessPluginToMonitor(i);
+    }
+
+    mAudioInterface->initPlugins(true);  // mSampleRate known now, which plugins require
+    mAudioInterface->startProcess();  // Tell JACK server we are ready for audio flow now
+
+    if (mConnectDefaultAudioPorts) {
+        mAudioInterface->connectDefaultPorts();
+    }
+    emit signalAudioStarted();
+
+    // Start our IO stat timer
+    if (mIOStatTimeout > 0) {
+        cout << "STATS" << mIOStatTimeout << endl;
+        if (!mIOStatStream.isNull()) {
+            mIOStatLogStream.rdbuf((mIOStatStream.data()->rdbuf()));
+        }
+        QTimer* timer = new QTimer(this);
+        connect(timer, &QTimer::timeout, this, &JackTrip::onStatTimer);
+        timer->start(mIOStatTimeout * 1000);
+    }
+}
+
+//*******************************************************************************
+void JackTrip::onStatTimer()
+{
+    DataProtocol::PktStat pkt_stat;
+    if (!mDataProtocolReceiver->getStats(&pkt_stat)) {
+        return;
+    }
+    bool reset = (0 == pkt_stat.statCount);
+    RingBuffer::IOStat recv_io_stat;
+    if (!mReceiveRingBuffer->getStats(&recv_io_stat, reset)) {
+        return;
+    }
+    RingBuffer::IOStat send_io_stat;
+    if (!mSendRingBuffer->getStats(&send_io_stat, reset)) {
+        return;
+    }
+    QString now = QDateTime::currentDateTime().toString(Qt::ISODate);
+
+    static QMutex mutex;
+    QMutexLocker locker(&mutex);
+    if (!mAudioTesterP.isNull() && mAudioTesterP->getEnabled()) {
+        mIOStatLogStream << "\n";
+    }
+    if (getBufferStrategy() != 3 && getBufferStrategy() != 4)
+        mIOStatLogStream << now.toLocal8Bit().constData() << " "
+                         << getPeerAddress().toLocal8Bit().constData()
+                         << " send: " << send_io_stat.underruns << "/"
+                         << send_io_stat.overflows << " recv: " << recv_io_stat.underruns
+                         << "/" << recv_io_stat.overflows << " prot: " << pkt_stat.lost
+                         << "/" << pkt_stat.outOfOrder << "/" << pkt_stat.revived
+                         << " tot: " << pkt_stat.tot << " sync: " << recv_io_stat.level
+                         << "/" << recv_io_stat.buf_inc_underrun << "/"
+                         << recv_io_stat.buf_inc_compensate << "/"
+                         << recv_io_stat.buf_dec_overflows << "/"
+                         << recv_io_stat.buf_dec_pktloss << " skew: " << recv_io_stat.skew
+                         << "/" << recv_io_stat.skew_raw
+                         << " bcast: " << recv_io_stat.broadcast_skew << "/"
+                         << recv_io_stat.broadcast_delta
+                         << " autoq: " << 0.1 * recv_io_stat.autoq_corr << "/"
+                         << 0.1 * recv_io_stat.autoq_rate << endl;
+    else {  // bufstrategy 3 or 4
+        mIOStatLogStream
+            << now.toLocal8Bit().constData() << " "
+            << getPeerAddress().toLocal8Bit().constData()
+            << " send: " << send_io_stat.underruns << "/" << send_io_stat.overflows
+            << " Glitches: " << recv_io_stat.underruns  // pullStat->lastPlcUnderruns;
+#define INVFLOATFACTOR 0.001
+            << "\nPUSH -- SD avg/last: " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.overflows  // pushStat->longTermStdDev;
+            << " / " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.buf_dec_overflows  // pushStat->lastStdDev;
+            << " \t mean/min/max: " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.skew  // pushStat->lastMean;
+            << " / " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.skew_raw  // pushStat->lastMin;
+            << " / " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.level  // pushStat->lastMax;
+
+            << "\nPULL -- SD avg/last: " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.buf_dec_pktloss  // pullStat->longTermStdDev;
+            << " / " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.broadcast_delta  // pullStat->lastStdDev;
+            << " \t mean/min/max: " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.buf_inc_underrun  // pullStat->lastMean;
+            << " / " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.buf_inc_compensate  // pullStat->lastMin;
+            << " / " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.broadcast_skew  // pullStat->lastMax;
+
+            //                     << "/" << recv_io_stat.overflows << " prot: " <<
+            //                     pkt_stat.lost << "/"
+            //                     << pkt_stat.outOfOrder << "/" << pkt_stat.revived
+            << " \n tot: " << pkt_stat.tot << " \t tol: " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.autoq_corr << " \t dsp (max): " << setw(5)
+            << INVFLOATFACTOR * recv_io_stat.autoq_rate
+            //                     << " sync: " << recv_io_stat.level << "/"
+            //                     << recv_io_stat.buf_inc_underrun << "/"
+            //                     << recv_io_stat.buf_inc_compensate << "/"
+            //                     << recv_io_stat.buf_dec_overflows << "/"
+            //                     << recv_io_stat.buf_dec_pktloss << " skew: " <<
+            //                     recv_io_stat.skew
+            //                     << "/" << recv_io_stat.skew_raw
+            //                     << " bcast: " << recv_io_stat.broadcast_skew << "/"
+            //                     << recv_io_stat.broadcast_delta
+            //                     << " autoq: " << 0.1 * recv_io_stat.autoq_corr << "/"
+            //                     << 0.1 * recv_io_stat.autoq_rate
+            << "\n"
+            << endl;
+    }
+}
+
+void JackTrip::receivedConnectionTCP()
+{
+    {
+        QMutexLocker lock(&mTimerMutex);
+        if (!mAwaitingTcp) {
+            return;
+        }
+        mAwaitingTcp = false;
+        mRetryTimer.stop();
+    }
+    if (gVerboseFlag)
+        cout << "TCP Socket Connected to Server!" << endl;
+    emit signalTcpClientConnected();
+
+    // If we're planning to authenticate, signal the server.
+    if (mUseAuth) {
+        char port_buf[sizeof(qint32)];
+        qToLittleEndian<qint32>(Auth::OK, port_buf);
+        mTcpClient.write(port_buf, sizeof(port_buf));
+        if (gVerboseFlag)
+            cout << "Auth request sent to Server" << endl;
+        return;
+    }
+    // Send Client Port Number to Server
+    // ---------------------------------
+    char port_buf[sizeof(qint32) + gMaxRemoteNameLength];
+    qToLittleEndian<qint32>(mReceiverBindPort, port_buf);
+    // std::memcpy(port_buf, &mReceiverBindPort, sizeof(mReceiverBindPort));
+
+    std::memset(port_buf + sizeof(qint32), 0, gMaxRemoteNameLength);
+    if (!mRemoteClientName.isEmpty()) {
+        // If our remote client name is set, send it too.
+        QByteArray name = mRemoteClientName.toUtf8();
+        // Find a clean place to truncate if we're over length.
+        // (Make sure we're not in the middle of a multi-byte characetr.)
+        int length = name.length();
+        // Need to take the final null terminator into account here.
+        if (length > gMaxRemoteNameLength - 1) {
+            length = gMaxRemoteNameLength - 1;
+            while ((length > 0) && ((name.at(length) & 0xc0) == 0x80)) {
+                // We're in the middle of a multi-byte character. Work back.
+                length--;
+            }
+        }
+        name.truncate(length);
+        std::memcpy(port_buf + sizeof(qint32), name.data(), length + 1);
+    }
+
+    mTcpClient.write(port_buf, sizeof(port_buf));
+    /*while ( mTcpClient.bytesToWrite() > 0 ) {
+        mTcpClient.waitForBytesWritten(-1);
+    }*/
+    if (gVerboseFlag)
+        cout << "Port " << mReceiverBindPort << " sent to Server" << endl;
+    // Continued in receivedDataTCP slot
+}
+
+void JackTrip::receivedDataTCP()
+{
+    if (mUseAuth && !mTcpClient.isEncrypted()) {
+        // If we're using authentication and haven't established a secure connection yet
+        // check that our server supports it.
+        qint32 authResponse;
+        int size       = sizeof(authResponse);
+        char* auth_buf = new char[size];
+        mTcpClient.read(auth_buf, size);
+        authResponse = qFromLittleEndian<qint32>(auth_buf);
+        delete[] auth_buf;
+        if (authResponse == Auth::OK) {
+            mTcpClient.startClientEncryption();
+        } else {
+            if (authResponse == Auth::NOTREQUIRED) {
+                std::cout << "ERROR: The Server does not require authentication."
+                          << std::endl;
+                stop(QStringLiteral("The server does not require authentication"));
+            } else {
+                std::cout << "ERROR: The Server does not support authentication."
+                          << std::endl;
+                stop(QStringLiteral("The server does not support authentication"));
+                // Send a header sized packet to the server so we don't lock up the
+                // main/UdpHubListener thread on the server. (Prevents a denial of
+                // service.) TODO: This should ultimately be fixed server side, but work
+                // around it here so we don't interfere with older deployments.
+                if (mUdpSockTemp.bind(QHostAddress::Any, mReceiverBindPort,
+                                      QUdpSocket::DefaultForPlatform)) {
+                    QThread::msleep(100);
+                    DefaultHeader temp(nullptr);
+                    size           = temp.getHeaderSizeInBytes();
+                    int8_t* header = new int8_t[size];
+                    // The header doesn't need to make sense, it just has to be non-zero
+                    // so we don't cause any divide by zero errors on the other end.
+                    memset(header, 1, size);
+                    mUdpSockTemp.writeDatagram((const char*)header, size,
+                                               mTcpClient.peerAddress(), authResponse);
+                    mUdpSockTemp.close();
+                }
+            }
+            mTcpClient.close();
+        }
+        return;
+    }
+
+    if (mTcpClient.bytesAvailable() < (int)sizeof(qint32)) {
+        return;
+    }
+
+    // Read the size of the package
+    // ----------------------------
+    if (gVerboseFlag)
+        cout << "Reading UDP port from Server..." << endl;
+    if (gVerboseFlag)
+        cout << "Ready To Read From Socket!" << endl;
+
+    // Read UDP Port Number from Server
+    // --------------------------------
+    uint32_t udp_port;
+    int size       = sizeof(udp_port);
+    char* port_buf = new char[size];
+    mTcpClient.read(port_buf, size);
+    udp_port = qFromLittleEndian<qint32>(port_buf);
+    delete[] port_buf;
+    // std::memcpy(&udp_port, port_buf, size);
+    // cout << "Received UDP Port Number: " << udp_port << endl;
+
+    // Close the TCP Socket
+    // --------------------
+    mTcpClient.close();  // Close the socket
+    // cout << "TCP Socket Closed!" << endl;
+
+    // If we sent authentication data, check if our authentication attempt was successful
+    if (mUseAuth && udp_port > 65535) {
+        QString error_message;
+        if (udp_port == Auth::WRONGCREDS) {
+            error_message = QStringLiteral("Incorrect username or password.");
+        } else if (udp_port == Auth::WRONGTIME) {
+            error_message = QStringLiteral(
+                "You are not authorized to access the server at this time.");
+        } else {
+            error_message = QStringLiteral("Unknown authentication error.");
+        }
+        std::cout << "ERROR: " << error_message.toStdString() << std::endl;
+        stop(error_message);
+        return;
+    } else if (udp_port > 65535) {
+        QString error_message;
+        if (udp_port == Auth::REQUIRED) {
+            error_message = QStringLiteral(
+                "The server you are attempting to connect to requires authentication.");
+        } else {
+            error_message = QStringLiteral("Unknown authentication error.");
+        }
+        std::cout << "ERROR: " << error_message.toStdString() << std::endl;
+        stop(error_message);
+        return;
+    }
+
+    if (gVerboseFlag)
+        cout << "Connection Successful!" << endl;
+
+    // Set with the received UDP port
+    // ------------------------------
+    setPeerPorts(udp_port);
+    mDataProtocolReceiver->setPeerAddress(mPeerAddress.toLatin1().data());
+    mDataProtocolSender->setPeerAddress(mPeerAddress.toLatin1().data());
+    mDataProtocolSender->setPeerPort(udp_port);
+    mDataProtocolReceiver->setPeerPort(udp_port);
+    cout << "Server Address set to: " << mPeerAddress.toStdString()
+         << " Port: " << udp_port << std::endl;
+    cout << gPrintSeparator << endl;
+    completeConnection();
+}
+
+void JackTrip::receivedErrorTCP(QAbstractSocket::SocketError socketError)
+{
+    if (socketError != QAbstractSocket::ConnectionRefusedError) {
+        mTcpClient.close();
+        mRetryTimer.stop();
+        stop(QStringLiteral("TCP Socket Error: ") + QString::number(socketError));
+    }
+}
+
+void JackTrip::connectionSecured()
+{
+    // Now that the connection is encrypted, send out port, and credentials.
+    //(Remember to include an additional 2 bytes for the username and password
+    // terminators.)
+    QByteArray username = mUsername.toUtf8();
+    QByteArray password = mPassword.toUtf8();
+    int size            = (sizeof(qint32) * 3) + gMaxRemoteNameLength + username.length()
+               + password.length() + 2;
+    char* buf    = new char[size];
+    int location = sizeof(qint32);
+    std::memset(buf, 0, size);
+    qToLittleEndian<qint32>(mReceiverBindPort, buf);
+
+    if (!mRemoteClientName.isEmpty()) {
+        // If our remote client name is set, send it too.
+        QByteArray name = mRemoteClientName.toUtf8();
+        // Find a clean place to truncate if we're over length.
+        // (Make sure we're not in the middle of a multi-byte character.)
+        int length = name.length();
+        // Need to take the final null terminator into account here.
+        if (length > gMaxRemoteNameLength - 1) {
+            length = gMaxRemoteNameLength - 1;
+            while ((length > 0) && ((name.at(length) & 0xc0) == 0x80)) {
+                // We're in the middle of a multi-byte character. Work back.
+                length--;
+            }
+        }
+        name.truncate(length);
+        std::memcpy(buf + location, name.data(), length + 1);
+    }
+    location += gMaxRemoteNameLength;
+
+    qToLittleEndian<qint32>(username.length(), buf + location);
+    location += sizeof(qint32);
+    qToLittleEndian<qint32>(password.length(), buf + location);
+    location += sizeof(qint32);
+
+    std::memcpy(buf + location, username.data(), username.length() + 1);
+    location += username.length() + 1;
+    std::memcpy(buf + location, password.data(), password.length() + 1);
+
+    mTcpClient.write(buf, size);
+    if (gVerboseFlag)
+        cout << "Port " << mReceiverBindPort << " sent to Server with credentials"
+             << endl;
+}
+
+void JackTrip::receivedDataUDP()
+{
+    // Stop our timer.
+    {
+        QMutexLocker lock(&mTimerMutex);
+        if (!mAwaitingUdp) {
+            return;
+        }
+        mAwaitingUdp = false;
+        mTimeoutTimer.stop();
+    }
+
+    QHostAddress peerHostAddress;
+    uint16_t peer_port;
+
+    // IPv6 addition from fyfe
+    // Get the datagram size to avoid problems with IPv6
+    qint64 datagramSize = mUdpSockTemp.pendingDatagramSize();
+    char* buf           = new char[datagramSize];
+    // set client address
+    mUdpSockTemp.readDatagram(buf, datagramSize, &peerHostAddress, &peer_port);
+    mUdpSockTemp.close();  // close the socket
+    delete[] buf;
+    // Check for mapped IPv4->IPv6 addresses that look like ::ffff:x.x.x.x
+    if (peerHostAddress.protocol() == QAbstractSocket::IPv6Protocol) {
+        bool mappedIPv4;
+        uint32_t address = peerHostAddress.toIPv4Address(&mappedIPv4);
+        // If the IPv4 address is mapped to IPv6, convert it to IPv4
+        if (mappedIPv4) {
+            QHostAddress ipv4Address = QHostAddress(address);
+            mPeerAddress             = ipv4Address.toString();
+        } else {
+            mPeerAddress = peerHostAddress.toString();
+        }
+    } else {
+        mPeerAddress = peerHostAddress.toString();
+    }
+
+    // Set the peer address to send packets (in the protocol sender)
+    if (gVerboseFlag)
+        std::cout << "JackTrip:serverStart before mDataProtocolSender->setPeerAddress()"
+                  << std::endl;
+    mDataProtocolSender->setPeerAddress(mPeerAddress.toLatin1().constData());
+    if (gVerboseFlag)
+        std::cout << "JackTrip:serverStart before mDataProtocolReceiver->setPeerAddress()"
+                  << std::endl;
+    mDataProtocolReceiver->setPeerAddress(mPeerAddress.toLatin1().constData());
+    //     We reply to the same port the peer sent the packets from
+    //     This way we can go through NAT
+    //     Because of the NAT traversal scheme, the portn need to be
+    //     "symmetric", e.g.:
+    //     from Client to Server : src = 4474, dest = 4464
+    //     from Server to Client : src = 4464, dest = 4474
+    // no -- all are the same -- 4464
+    if (gVerboseFlag)
+        std::cout << "JackTrip:serverStart before setting all peer_port instances to "
+                  << peer_port << std::endl;
+    mDataProtocolSender->setPeerPort(peer_port);
+    mDataProtocolReceiver->setPeerPort(peer_port);
+    setPeerPorts(peer_port);
+    completeConnection();
+}
+
+void JackTrip::udpTimerTick()
+{
+    QMutexLocker lock(&mTimerMutex);
+    if (!mAwaitingUdp) {
+        return;
+    }
+
+    if (mStopped || sSigInt || sAudioStopped) {
+        // Stop everything.
+        mAwaitingUdp = false;
+        mUdpSockTemp.close();
+        mTimeoutTimer.stop();
+        stop();
+    }
+
+    if (gVerboseFlag)
+        std::cout << mSleepTime << "ms  " << std::flush;
+    mElapsedTime += mSleepTime;
+    if (mEndTime > 0 && mElapsedTime >= mEndTime) {
+        mAwaitingUdp = false;
+        mUdpSockTemp.close();
+        mTimeoutTimer.stop();
+        cout << "JackTrip Server Timed Out!" << endl;
+        stop(QStringLiteral("JackTrip Server Timed Out"));
+    }
+}
+
+void JackTrip::tcpTimerTick()
+{
+    QMutexLocker lock(&mTimerMutex);
+    if (!mAwaitingTcp) {
+        return;
+    }
+
+    if (mStopped || sSigInt || sAudioStopped) {
+        // Stop everything.
+        mAwaitingTcp = false;
+        mTcpClient.close();
+        mRetryTimer.stop();
+        stop();
+        return;
+    }
+
+    mElapsedTime += mRetryTimer.interval();
+    if (mEndTime > 0 && mElapsedTime >= mEndTime) {
+        mAwaitingTcp = false;
+        mTcpClient.close();
+        mRetryTimer.stop();
+        cout << "JackTrip Server Timed Out!" << endl;
+        stop(QStringLiteral("Initial TCP Connection Timed Out"));
+        return;
+    }
+
+    // Use randomized exponential backoff to reconnect the TCP client
+    mRetries++;
+    // exponential backoff sleep with 6s maximum + jitter
+    int newInterval = 2000 * pow(2, mRetries);
+    newInterval     = std::min(newInterval, 6000);
+    newInterval += QRandomGenerator::global()->bounded(0, 500);
+    QString now = QDateTime::currentDateTime().toString(Qt::ISODate);
+    qDebug() << "Sleep time " << newInterval << " ms at " << now;
+    mRetryTimer.setInterval(newInterval);
+
+    qDebug() << "Connection timed out. Retrying again using exponential backoff";
+
+    mTcpClient.abort();
+
+    // Create Socket Objects
+    QHostAddress serverHostAddress;
+    if (!serverHostAddress.setAddress(mPeerAddress)) {
+        QHostInfo info = QHostInfo::fromName(mPeerAddress);
+        if (!info.addresses().isEmpty()) {
+            // use the first IP address
+            serverHostAddress = info.addresses().constFirst();
+        }
+    }
+
+    if (mTcpClient.state() == QAbstractSocket::UnconnectedState) {
+        mTcpClient.connectToHost(serverHostAddress, mTcpServerPort);
+    }
+
+    mRetryTimer.start();
+}
+
+//*******************************************************************************
+void JackTrip::stop(const QString& errorMessage)
+{
+    // Take a snapshot of sAudioStopped
+    bool audioStopped = sAudioStopped;
+    mStopped          = true;
+    // Make sure we're only run once
+    if (mHasShutdown) {
+        return;
+    }
+    mHasShutdown = true;
+    std::cout << "Stopping JackTrip..." << std::endl;
+
+    if (mDataProtocolSender != nullptr) {
+        // Stop The Sender
+        mDataProtocolSender->stop();
+        mDataProtocolSender->wait();
+    }
+
+    if (mDataProtocolReceiver != nullptr) {
+        // Stop The Receiver
+        mDataProtocolReceiver->stop();
+        mDataProtocolReceiver->wait();
+    }
+
+    // check for errors from audio interface
+    QString audioErrorMsg;
+    if (mAudioInterface != nullptr && !mAudioInterface->getDevicesErrorMsg().empty()) {
+        audioErrorMsg = QString::fromStdString(mAudioInterface->getDevicesErrorMsg());
+    } else if (audioStopped) {
+        audioErrorMsg = QStringLiteral("Your audio interface has stopped!");
+    }
+
+    // Stop the audio processes
+    // mAudioInterface->stopProcess();
+    closeAudio();
+
+    cout << "JackTrip Processes STOPPED!" << endl;
+    cout << gPrintSeparator << endl;
+
+    // Emit the jack stopped signal
+    if (!audioErrorMsg.isEmpty()) {
+        emit signalError(audioErrorMsg);
+    } else if (errorMessage.isEmpty()) {
+        emit signalProcessesStopped();
+    } else {
+        emit signalError(errorMessage);
+    }
+}
+
+//*******************************************************************************
+void JackTrip::waitThreads()
+{
+    mDataProtocolSender->wait();
+    mDataProtocolReceiver->wait();
+}
+
+//*******************************************************************************
+void JackTrip::clientStart()
+{
+    // For the Client mode, the peer (or server) address has to be specified by the user
+    if (mPeerAddress.isEmpty()) {
+        throw std::invalid_argument(
+            "Peer Address has to be set if you run in CLIENT mode");
+    } else {
+        mDataProtocolSender->setPeerAddress(mPeerAddress.toLatin1().data());
+        mDataProtocolReceiver->setPeerAddress(mPeerAddress.toLatin1().data());
+        cout << "Peer Address set to: " << mPeerAddress.toStdString() << std::endl;
+        cout << gPrintSeparator << endl;
+        completeConnection();
+    }
+}
+
+//*******************************************************************************
+int JackTrip::serverStart(bool timeout, int udpTimeout)  // udpTimeout unused
+{
+    // Set the peer address
+    if (!mPeerAddress.isEmpty()) {
+        if (gVerboseFlag)
+            std::cout << "WARNING: SERVER mode: Peer Address was set but will be deleted."
+                      << endl;
+        // throw std::invalid_argument("Peer Address has to be set if you run in CLIENT
+        // mode");
+        mPeerAddress.clear();
+        // return;
+    }
+
+    // Start timer before binding our port and waiting for datagrams
+    {
+        QMutexLocker lock(&mTimerMutex);
+        mAwaitingUdp = true;
+        mElapsedTime = 0;
+        if (timeout) {
+            mEndTime = udpTimeout;
+        }
+        mTimeoutTimer.setInterval(mSleepTime);
+        mTimeoutTimer.disconnect();
+        connect(&mTimeoutTimer, &QTimer::timeout, this, &JackTrip::udpTimerTick);
+        mTimeoutTimer.start();
+    }
+
+    // Get the client address when it connects
+    if (gVerboseFlag)
+        std::cout << "JackTrip:serverStart before mUdpSockTemp.bind(Any)" << std::endl;
+    // Bind the socket
+    if (!mUdpSockTemp.bind(QHostAddress::Any, mReceiverBindPort,
+                           QUdpSocket::DefaultForPlatform)) {
+        {
+            QMutexLocker lock(&mTimerMutex);
+            mAwaitingUdp = false;
+            mTimeoutTimer.stop();
+        }
+        std::cerr << "in JackTrip: Could not bind UDP socket. It may be already binded."
+                  << endl;
+        throw std::runtime_error("Could not bind UDP socket. It may be already binded.");
+    }
+    connect(&mUdpSockTemp, &QUdpSocket::readyRead, this, &JackTrip::receivedDataUDP);
+
+    if (gVerboseFlag)
+        std::cout << "JackTrip:serverStart before !UdpSockTemp.hasPendingDatagrams()"
+                  << std::endl;
+    cout << "Waiting for Connection From a Client..." << endl;
+    return 0;
+    // Continued in the receivedDataUDP slot.
+
+    //    char buf[1];
+    //    // set client address
+    //    UdpSockTemp.readDatagram(buf, 1, &peerHostAddress, &peer_port);
+    //    UdpSockTemp.close(); // close the socket
+}
+
+//*******************************************************************************
+int JackTrip::clientPingToServerStart()
+{
+    // mConnectionMode = JackTrip::KSTRONG;
+    // mConnectionMode = JackTrip::JAMTEST;
+
+    // Set Peer (server in this case) address
+    // --------------------------------------
+    // For the Client mode, the peer (or server) address has to be specified by the user
+    if (mPeerAddress.isEmpty()) {
+        throw std::invalid_argument(
+            "Peer Address has to be set if you run in CLIENTTOPINGSERVER mode");
+        return -1;
+    }
+
+    // If we're using authentication, check that SSL support is available.
+    if (mUseAuth) {
+        if (!QSslSocket::supportsSsl()) {
+            QString error_message =
+                "SSL not supported. Make sure you have the appropriate SSL "
+                "libraries\ninstalled to enable authentication.";
+            std::cerr << "ERROR: " << error_message.toStdString() << std::endl;
+            stop(error_message);
+            return -1;
+        } else if (mUsername.isEmpty() || mPassword.isEmpty()) {
+            QString error_message =
+                "You must supply a username and password to authenticate with a hub "
+                "server.";
+            std::cerr << "ERROR: " << error_message.toStdString() << std::endl;
+            stop(error_message);
+            return -1;
+        } else {
+            // At the moment, don't verify the certificate so we can use self signed ones.
+            mTcpClient.setPeerVerifyMode(QSslSocket::VerifyNone);
+            QObject::connect(&mTcpClient, &QSslSocket::encrypted, this,
+                             &JackTrip::connectionSecured, Qt::QueuedConnection);
+        }
+    }
+
+    // Create Socket Objects
+    // --------------------
+    QHostAddress serverHostAddress;
+    if (!serverHostAddress.setAddress(mPeerAddress)) {
+        QHostInfo info = QHostInfo::fromName(mPeerAddress);
+        if (!info.addresses().isEmpty()) {
+            // use the first IP address
+            serverHostAddress = info.addresses().constFirst();
+        }
+    }
+
+    // Connect Socket to Server and wait for response
+    // ----------------------------------------------
+    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
+#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
+    connect(&mTcpClient,
+            QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error), this,
+            &JackTrip::receivedErrorTCP);
+#else
+    connect(&mTcpClient, &QTcpSocket::errorOccurred, this, &JackTrip::receivedErrorTCP);
+#endif
+    {
+        QMutexLocker lock(&mTimerMutex);
+        mAwaitingTcp = true;
+        mElapsedTime = 0;
+        mEndTime     = 30000;  // Timeout after 30 seconds.
+        mRetryTimer.setInterval(
+            QRandomGenerator::global()->bounded(0, int(2000 * pow(2, mRetries))));
+        mRetryTimer.setSingleShot(true);
+        mRetryTimer.disconnect();
+        connect(&mRetryTimer, &QTimer::timeout, this, &JackTrip::tcpTimerTick);
+        mRetryTimer.start();
+    }
+
+    if (mTcpClient.state() == QAbstractSocket::UnconnectedState) {
+        mTcpClient.connectToHost(serverHostAddress, mTcpServerPort);
+    }
+
+    if (gVerboseFlag)
+        cout << "Connecting to TCP Server at "
+             << serverHostAddress.toString().toLatin1().constData() << " port "
+             << mTcpServerPort << "..." << endl;
+    return 0;
+    // Continued in the receivedConnectionTCP slot.
+
+    /*
+  else {
+    // Set the peer address
+    mDataProtocolSender->setPeerAddress( mPeerAddress.toLatin1().data() );
+  }
+
+  // Start the Sender Threads
+  // ------------------------
+  mAudioInterface->startProcess();
+  mDataProtocolSender->start();
+  // block until mDataProtocolSender thread starts
+  while ( !mDataProtocolSender->isRunning() ) { QThread::msleep(100); }
+
+  // Create a Socket to listen to Server's answer
+  // --------------------------------------------
+  QHostAddress serverHostAddress;
+  QUdpSocket UdpSockTemp;// Create socket to wait for server answer
+  uint16_t server_port;
+
+  // Bind the socket
+  //bindReceiveSocket(UdpSockTemp, mReceiverBindPort,
+  //                  mPeerAddress, peer_port);
+  if ( !UdpSockTemp.bind(QHostAddress::Any,
+                         mReceiverBindPort,
+                         QUdpSocket::ShareAddress) ) {
+    //throw std::runtime_error("Could not bind PingToServer UDP socket. It may be already
+  binded.");
+  }
+
+  // Listen to server response
+  cout << "Waiting for server response..." << endl;
+  while ( !UdpSockTemp.hasPendingDatagrams() ) { QThread::msleep(100); }
+  cout << "Received response from server!" << endl;
+  char buf[1];
+  // set client address
+  UdpSockTemp.readDatagram(buf, 1, &serverHostAddress, &server_port);
+  UdpSockTemp.close(); // close the socket
+
+  // Stop the sender thread to change server port
+  mDataProtocolSender->stop();
+  mDataProtocolSender->wait(); // Wait for the thread to terminate
+
+  cout << "Server port now set to: " << server_port << endl;
+  cout << gPrintSeparator << endl;
+  mDataProtocolSender->setPeerPort(server_port);
+
+  // Start Threads
+  //mAudioInterface->connectDefaultPorts();
+  mDataProtocolSender->start();
+  mDataProtocolReceiver->start();
+  */
+}
+
+//*******************************************************************************
+/*
+void JackTrip::bindReceiveSocket(QUdpSocket& UdpSocket, int bind_port,
+                                 QHostAddress PeerHostAddress, int peer_port)
+throw(std::runtime_error)
+{
+  // Creat socket descriptor
+  int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
+
+  // Set local IPv4 Address
+  struct sockaddr_in local_addr;
+  ::bzero(&local_addr, sizeof(local_addr));
+  local_addr.sin_family = AF_INET; //AF_INET: IPv4 Protocol
+  local_addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY: let the kernel decide the
+active address local_addr.sin_port = htons(bind_port); //set bind port
+
+  // Set socket to be reusable, this is platform dependent
+  int one = 1;
+#if defined ( __linux__ )
+  ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+#endif
+#if defined ( __APPLE__ )
+  // This option is not available on Linux, and without it MAC OS X
+  // has problems rebinding a socket
+  ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
+#endif
+
+  // Bind the Socket
+  if ( (::bind(sock_fd, (struct sockaddr *) &local_addr, sizeof(local_addr))) < 0 )
+  { throw std::runtime_error("ERROR: UDP Socket Bind Error"); }
+
+  // To be able to use the two UDP sockets bound to the same port number,
+  // we connect the receiver and issue a SHUT_WR.
+  // Set peer IPv4 Address
+  struct sockaddr_in peer_addr;
+  bzero(&peer_addr, sizeof(peer_addr));
+  peer_addr.sin_family = AF_INET; //AF_INET: IPv4 Protocol
+  peer_addr.sin_addr.s_addr = htonl(INADDR_ANY); //INADDR_ANY: let the kernel decide the
+active address peer_addr.sin_port = htons(peer_port); //set local port
+  // Connect the socket and issue a Write shutdown (to make it a
+  // reader socket only)
+  if ( (::inet_pton(AF_INET, PeerHostAddress.toString().toLatin1().constData(),
+                    &peer_addr.sin_addr)) < 1 )
+  { throw std::runtime_error("ERROR: Invalid address presentation format"); }
+  if ( (::connect(sock_fd, (struct sockaddr *) &peer_addr, sizeof(peer_addr))) < 0)
+  { throw std::runtime_error("ERROR: Could not connect UDP socket"); }
+  if ( (::shutdown(sock_fd,SHUT_WR)) < 0)
+  { throw std::runtime_error("ERROR: Could suntdown SHUT_WR UDP socket"); }
+
+  UdpSocket.setSocketDescriptor(sock_fd, QUdpSocket::ConnectedState,
+                                QUdpSocket::ReadOnly);
+  cout << "UDP Socket Receiving in Port: " << bind_port << endl;
+  cout << gPrintSeparator << endl;
+}
+*/
+
+//*******************************************************************************
+void JackTrip::createHeader(const DataProtocol::packetHeaderTypeT headertype)
+{
+    delete mPacketHeader;  // Just in case it has already been allocated
+    switch (headertype) {
+    case DataProtocol::DEFAULT:
+        mPacketHeader = new DefaultHeader(this);
+        break;
+    case DataProtocol::JAMLINK:
+        mPacketHeader = new JamLinkHeader(this);
+        break;
+    case DataProtocol::EMPTY:
+        mPacketHeader = new EmptyHeader(this);
+        break;
+    default:
+        throw std::invalid_argument("Undefined Header Type");
+        break;
+    }
+}
+
+//*******************************************************************************
+void JackTrip::putHeaderInIncomingPacket(int8_t* full_packet, int8_t* audio_packet)
+{
+    mPacketHeader->fillHeaderCommonFromAudio();
+    mPacketHeader->putHeaderInPacket(full_packet);
+
+    int8_t* audio_part;
+    audio_part = full_packet + mPacketHeader->getHeaderSizeInBytes();
+    // std::memcpy(audio_part, audio_packet, mAudioInterface->getBufferSizeInBytes());
+    // std::memcpy(audio_part, audio_packet, mAudioInterface->getSizeInBytesPerChannel() *
+    // mNumChans);
+    std::memcpy(audio_part, audio_packet, getTotalAudioOutputPacketSizeInBytes());
+}
+
+void JackTrip::putHeaderInOutgoingPacket(int8_t* full_packet, int8_t* audio_packet)
+{
+    mPacketHeader->fillHeaderCommonFromAudio();
+    mPacketHeader->putHeaderInPacket(full_packet);
+
+    int8_t* audio_part;
+    audio_part = full_packet + mPacketHeader->getHeaderSizeInBytes();
+    // std::memcpy(audio_part, audio_packet, mAudioInterface->getBufferSizeInBytes());
+    // std::memcpy(audio_part, audio_packet, mAudioInterface->getSizeInBytesPerChannel() *
+    // mNumChans);
+    std::memcpy(audio_part, audio_packet, getTotalAudioInputPacketSizeInBytes());
+}
+
+//*******************************************************************************
+int JackTrip::getSendPacketSizeInBytes() const
+{
+    return (getTotalAudioInputPacketSizeInBytes()
+            + mPacketHeader->getHeaderSizeInBytes());
+}
+
+int JackTrip::getReceivePacketSizeInBytes() const
+{
+    return (getTotalAudioOutputPacketSizeInBytes()
+            + mPacketHeader->getHeaderSizeInBytes());
+}
+
+//*******************************************************************************
+bool JackTrip::checkPeerSettings(int8_t* full_packet)
+{
+    return mPacketHeader->checkPeerSettings(full_packet);
+}
+
+//*******************************************************************************
+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) )
+
+    // check all combinations to ensure the port is free
+    std::map<std::string, QHostAddress::SpecialAddress> interfaces = {
+        {"IPv4", QHostAddress::AnyIPv4},
+        {"IPv6", QHostAddress::AnyIPv6},
+        {"IPv4+IPv6", QHostAddress::Any}};
+
+    std::map<std::string, QHostAddress::SpecialAddress>::iterator it;
+    for (it = interfaces.begin(); it != interfaces.end(); it++) {
+        bool binded = UdpSockTemp.bind(it->second, port, QUdpSocket::DontShareAddress);
+        QUdpSocket::SocketError err = UdpSockTemp.error();
+        UdpSockTemp.close();
+        if (!binded && err != QUdpSocket::UnsupportedSocketOperationError) {
+            return true;
+        }
+    }
+    return false;
+}
diff --git a/src/JackTrip.h b/src/JackTrip.h
new file mode 100644 (file)
index 0000000..166106b
--- /dev/null
@@ -0,0 +1,743 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2024 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 JackTrip.h
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+#ifndef __JACKTRIP_H__
+#define __JACKTRIP_H__
+
+// #include <tr1/memory> //for shared_ptr
+#include <QObject>
+#include <QSharedPointer>
+#include <QSslSocket>
+#include <QString>
+#include <QTimer>
+#include <QUdpSocket>
+#include <stdexcept>
+
+#include "AudioInterface.h"
+#include "DataProtocol.h"
+
+#ifndef NO_JACK
+#include "JackAudioInterface.h"
+#endif  // NO_JACK
+
+#include "AudioTester.h"
+#include "PacketHeader.h"
+#include "RingBuffer.h"
+
+// #include <signal.h>
+/** \brief Main class to creates a SERVER (to listen) or a CLIENT (to connect
+ * to a listening server) to send audio streams in the network.
+ *
+ * All audio and network settings can be set in this class.
+ * This class also acts as a Mediator between all the other class.
+ * Classes that uses JackTrip methods need to register with it.
+ */
+
+class JackTrip : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    //----------ENUMS------------------------------------------
+    /// \brief Enum for the data Protocol. At this time only UDP is implemented
+    enum dataProtocolT {
+        UDP,  ///< Use UDP (User Datagram Protocol)
+        TCP,  ///< <B>NOT IMPLEMENTED</B>: Use TCP (Transmission Control Protocol)
+        SCTP  ///< <B>NOT IMPLEMENTED</B>: Use SCTP (Stream Control Transmission Protocol)
+    };
+
+    /// \brief Enum for the JackTrip mode
+    enum jacktripModeT {
+        SERVER,              ///< Run in P2P Server Mode
+        CLIENT,              ///< Run in P2P Client Mode
+        CLIENTTOPINGSERVER,  ///< Client of the Ping Server Mode
+        SERVERPINGSERVER     ///< Server of the MultiThreaded JackTrip
+    };
+
+    /// \brief Enum for the JackTrip Underrun Mode, when packets
+    enum underrunModeT {
+        WAVETABLE,  ///< Loops on the last received packet
+        ZEROS       ///< Set new buffers to zero if there are no new ones
+    };
+
+    /// \brief Enum for Audio Interface Mode
+    enum audiointerfaceModeT {
+        JACK,    ///< Jack Mode
+        RTAUDIO  ///< RtAudio Mode
+    };
+
+    /// \brief Enum for Connection Mode (in packet header)
+    enum connectionModeT {
+        NORMAL,   ///< Normal Mode
+        KSTRONG,  ///< Karplus Strong
+        JAMTEST   ///< Karplus Strong
+    };
+
+    /// \brief Enum for Hub Server Audio Connection Mode (connections to hub server are
+    /// automatically patched in Jack)
+    enum hubConnectionModeT {
+        SERVERTOCLIENT,  ///< Normal Mode, Sever to All Clients (but not client to any
+                         ///< client)
+        CLIENTECHO,      ///< Client Echo (client self-to-self)
+        CLIENTFOFI,      ///< Client Fan Out to Clients and Fan In from Clients (but not
+                         ///< self-to-self)
+        RESERVEDMATRIX,  ///< Reserved for custom patch matrix (for TUB ensemble)
+        FULLMIX,         ///< Client Fan Out to Clients and Fan In from Clients (including
+                         ///< self-to-self)
+        NOAUTO,          ///< No automatic patching
+        SERVFOFI,        ///< Like CLIENTFOFI, but include the server in the mix
+        SERVFULLMIX      ///< Like FULLMIX, but include the server in the mix
+    };
+    //---------------------------------------------------------
+
+    /** \brief The class Constructor with Default Parameters
+     * \param JacktripMode JackTrip::CLIENT or JackTrip::SERVER
+     * \param DataProtocolType JackTrip::dataProtocolT
+     * \param NumChans Number of Audio Channels (same for inputs and outputs)
+     * \param BufferQueueLength Audio Buffer for receiving packets
+     * \param AudioBitResolution Audio Sample Resolutions in bits
+     * \param redundancy redundancy factor for network data
+     */
+    JackTrip(
+        jacktripModeT JacktripMode = CLIENT, dataProtocolT DataProtocolType = UDP,
+        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
+        int BufferQueueLength                                  = gDefaultQueueLength,
+        unsigned int redundancy                                = gDefaultRedundancy,
+        AudioInterface::audioBitResolutionT AudioBitResolution = AudioInterface::BIT16,
+        DataProtocol::packetHeaderTypeT PacketHeaderType       = DataProtocol::DEFAULT,
+        underrunModeT UnderRunMode = WAVETABLE, int receiver_bind_port = gDefaultPort,
+        int sender_bind_port = gDefaultPort, int receiver_peer_port = gDefaultPort,
+        int sender_peer_port = gDefaultPort, int tcp_peer_port = gDefaultPort,
+        QObject* parent = nullptr);
+
+    /// \brief The class destructor
+    virtual ~JackTrip();
+
+    static void sigIntHandler(int /*unused*/)
+    {
+        std::cout << std::endl << "Shutting Down..." << std::endl;
+        sSigInt = true;
+    }
+    static bool sSigInt;
+    static bool sAudioStopped;
+
+    /// \brief Starting point for the thread
+    /*virtual void run() {
+        if (gVerboseFlag) std::cout << "Settings:startJackTrip before mJackTrip->run" <<
+    std::endl;
+    }*/
+
+    /// \brief Set the Peer Address for jacktripModeT::CLIENT mode only
+    virtual void setPeerAddress(const QString& PeerHostOrIP);
+
+    /** \brief Append a process plugin. Processes will be appended in order
+     * \param plugin Pointer to ProcessPlugin Class
+     */
+    // void appendProcessPlugin(const std::tr1::shared_ptr<ProcessPlugin> plugin);
+    virtual void appendProcessPluginToNetwork(ProcessPlugin* plugin);
+    virtual void appendProcessPluginFromNetwork(ProcessPlugin* plugin);
+    virtual void appendProcessPluginToMonitor(ProcessPlugin* plugin);
+
+    /// \brief Start the processing threads
+    virtual void startProcess(
+#ifdef WAIRTOHUB  // wair
+        int ID
+#endif  // endwhere
+    );
+    virtual void completeConnection();
+
+    /// \brief Stop the processing threads
+    virtual void stop(const QString& errorMessage = QLatin1String(""));
+
+    /// \brief Wait for all the threads to finish. This functions is used when JackTrip is
+    /// run as a thread
+    virtual void waitThreads();
+
+    /// \brief Check if UDP port is already binded
+    /// \param port Port number
+    virtual bool checkIfPortIsBinded(int port);
+
+    //------------------------------------------------------------------------------------
+    /// \name Getters and Setters Methods to change parameters after construction
+    //@{
+    //
+    /// \brief Sets (override) JackTrip Mode after construction
+    virtual void setJackTripMode(jacktripModeT JacktripMode)
+    {
+        mJackTripMode = JacktripMode;
+    }
+    /// \brief Sets (override) DataProtocol Type after construction
+    virtual void setDataProtocoType(dataProtocolT DataProtocolType)
+    {
+        mDataProtocol = DataProtocolType;
+    }
+    /// \brief Sets the Packet header type
+    virtual void setPacketHeaderType(DataProtocol::packetHeaderTypeT PacketHeaderType)
+    {
+        mPacketHeaderType = PacketHeaderType;
+        delete mPacketHeader;
+        mPacketHeader = NULL;
+        createHeader(mPacketHeaderType);
+    }
+    /// \brief Sets (override) Buffer Queue Length Mode after construction
+    virtual void setBufferQueueLength(int BufferQueueLength)
+    {
+        mBufferQueueLength = BufferQueueLength;
+    }
+    virtual void setBufferStrategy(int BufferStrategy)
+    {
+        mBufferStrategy = BufferStrategy;
+    }
+    /// \brief Sets (override) Audio Bit Resolution after construction
+    virtual void setAudioBitResolution(
+        AudioInterface::audioBitResolutionT AudioBitResolution)
+    {
+        mAudioBitResolution = AudioBitResolution;
+    }
+    /// \brief Sets (override) Underrun Mode
+    virtual void setUnderRunMode(underrunModeT UnderRunMode)
+    {
+        mUnderRunMode = UnderRunMode;
+    }
+    /// \brief Sets whether to quit on timeout.
+    virtual void setStopOnTimeout(bool stopOnTimeout) { mStopOnTimeout = stopOnTimeout; }
+    /// \brief Sets port numbers for the local and peer machine.
+    /// Receive port is <tt>port</tt>
+    virtual void setAllPorts(int port)
+    {
+        mReceiverBindPort = port;
+        mSenderPeerPort   = port;
+        mSenderBindPort   = port;
+        mReceiverPeerPort = port;
+    }
+    /// \brief Sets port numbers to bind in RECEIVER and SENDER sockets.
+    void setBindPorts(int port)
+    {
+        mReceiverBindPort = port;
+        mSenderBindPort   = port;
+    }
+    /// \brief Sets port numbers for the peer (remote) machine.
+    void setPeerPorts(int port)
+    {
+        mSenderPeerPort   = port;
+        mReceiverPeerPort = port;
+    }
+    void setPeerHandshakePort(int port) { mTcpServerPort = port; }
+    void setUseAuth(bool auth) { mUseAuth = auth; }
+    void setUsername(const QString& username) { mUsername = username; }
+    void setPassword(const QString& password) { mPassword = password; }
+    /// \brief Set Client Name to something different that the default (JackTrip)
+    virtual void setClientName(const QString& clientName)
+    {
+        mJackClientName = clientName;
+    }
+    virtual void setRemoteClientName(const QString& remoteClientName)
+    {
+        mRemoteClientName = remoteClientName;
+    }
+    /// \brief Set the number of audio input channels
+    virtual void setNumInputChannels(int num_chans) { mNumAudioChansIn = num_chans; }
+    /// \brief Set the number of audio output channels
+    virtual void setNumOutputChannels(int num_chans) { mNumAudioChansOut = num_chans; }
+
+    virtual void setIOStatTimeout(int timeout) { mIOStatTimeout = timeout; }
+    virtual void setIOStatStream(QSharedPointer<std::ostream> statStream)
+    {
+        mIOStatStream = statStream;
+    }
+
+    /// Set to connect or not default audio ports (only implemented in Jack)
+    virtual void setConnectDefaultAudioPorts(bool connect)
+    {
+        mConnectDefaultAudioPorts = connect;
+    }
+
+    virtual int getReceiverBindPort() const { return mReceiverBindPort; }
+    virtual int getSenderPeerPort() const { return mSenderPeerPort; }
+    virtual int getSenderBindPort() const { return mSenderBindPort; }
+    virtual int getReceiverPeerPort() const { return mReceiverPeerPort; }
+
+    virtual DataProtocol* getDataProtocolSender() const { return mDataProtocolSender; }
+    virtual DataProtocol* getDataProtocolReceiver() const
+    {
+        return mDataProtocolReceiver;
+    }
+    virtual void setDataProtocolSender(DataProtocol* const DataProtocolSender)
+    {
+        mDataProtocolSender = DataProtocolSender;
+    }
+    virtual void setDataProtocolReceiver(DataProtocol* const DataProtocolReceiver)
+    {
+        mDataProtocolReceiver = DataProtocolReceiver;
+    }
+    virtual int getBufferStrategy() const noexcept { return mBufferStrategy; }
+
+    virtual RingBuffer* getSendRingBuffer() const { return mSendRingBuffer; }
+    virtual RingBuffer* getReceiveRingBuffer() const { return mReceiveRingBuffer; }
+    virtual void setSendRingBuffer(RingBuffer* const SendRingBuffer)
+    {
+        mSendRingBuffer = SendRingBuffer;
+    }
+    virtual void setReceiveRingBuffer(RingBuffer* const ReceiveRingBuffer)
+    {
+        mReceiveRingBuffer = ReceiveRingBuffer;
+    }
+
+    virtual void setPacketHeader(PacketHeader* const PacketHeader)
+    {
+        mPacketHeader = PacketHeader;
+    }
+
+    virtual int getInputRingBuffersSlotSize()
+    {
+        return getTotalAudioInputPacketSizeInBytes();
+    }
+    virtual int getOutputRingBuffersSlotSize()
+    {
+        return getTotalAudioOutputPacketSizeInBytes();
+    }
+
+    virtual void setAudiointerfaceMode(JackTrip::audiointerfaceModeT audiointerface_mode)
+    {
+        mAudiointerfaceMode = audiointerface_mode;
+    }
+    virtual void setAudioInterface(AudioInterface* const AudioInterface)
+    {
+        mAudioInterface = AudioInterface;
+    }
+    virtual void setLoopBack(bool b) { mLoopBack = b; }
+    virtual void setAudioTesterP(QSharedPointer<AudioTester> atp) { mAudioTesterP = atp; }
+
+    void setSampleRate(uint32_t sample_rate) { mSampleRate = sample_rate; }
+    void setDeviceID(uint32_t device_id) { mDeviceID = device_id; }
+    void setInputDevice(std::string device_name) { mInputDeviceName = device_name; }
+    void setOutputDevice(std::string device_name) { mOutputDeviceName = device_name; }
+    void setAudioBufferSizeInSamples(uint32_t buf_size) { mAudioBufferSize = buf_size; }
+
+    JackTrip::hubConnectionModeT getHubConnectionModeT() const
+    {
+        return mHubConnectionModeT;
+    }
+    void setHubConnectionModeT(JackTrip::hubConnectionModeT connection_mode)
+    {
+        mHubConnectionModeT = connection_mode;
+    }
+
+    JackTrip::jacktripModeT getJackTripMode() const { return mJackTripMode; }
+
+    QString getPeerAddress() const { return mPeerAddress; }
+
+    bool receivedConnectionFromPeer() { return mReceivedConnection; }
+
+    bool tcpConnectionError() { return mTcpConnectionError; }
+
+    //@}
+    //------------------------------------------------------------------------------------
+
+    //------------------------------------------------------------------------------------
+    /// \name Mediator Functions
+    //@{
+    /// \todo Document all these functions
+    virtual void createHeader(const DataProtocol::packetHeaderTypeT headertype);
+    void putHeaderInIncomingPacket(int8_t* full_packet, int8_t* audio_packet);
+    void putHeaderInOutgoingPacket(int8_t* full_packet, int8_t* audio_packet);
+    int getSendPacketSizeInBytes() const;
+    int getReceivePacketSizeInBytes() const;
+    virtual void sendNetworkPacket(const int8_t* ptrToSlot)
+    {
+        mSendRingBuffer->insertSlotNonBlocking(ptrToSlot, 0, 0, 0);
+    }
+    virtual void receiveBroadcastPacket(int8_t* ptrToReadSlot)
+    {
+        mReceiveRingBuffer->readBroadcastSlot(ptrToReadSlot);
+    }
+    virtual void receiveNetworkPacket(int8_t* ptrToReadSlot)
+    {
+        mReceiveRingBuffer->readSlotNonBlocking(ptrToReadSlot);
+    }
+    virtual void readAudioBuffer(int8_t* ptrToReadSlot)
+    {
+        mSendRingBuffer->readSlotBlocking(ptrToReadSlot);
+    }
+    virtual bool writeAudioBuffer(const int8_t* ptrToSlot, int len, int lostLen, int seq)
+    {
+        return mReceiveRingBuffer->insertSlotNonBlocking(ptrToSlot, len, lostLen, seq);
+    }
+    uint32_t getBufferSizeInSamples() const
+    {
+        return mAudioBufferSize; /*return mAudioInterface->getBufferSizeInSamples();*/
+    }
+    uint32_t getDeviceID() const
+    {
+        return mDeviceID; /*return mAudioInterface->mDeviceID();*/
+    }
+
+    AudioInterface::samplingRateT getSampleRateType() const
+    {
+        return mAudioInterface->getSampleRateType();
+    }
+    int getSampleRate() const
+    {
+        return mSampleRate; /*return mAudioInterface->getSampleRate();*/
+    }
+
+    uint8_t getAudioBitResolution() const
+    {
+        return mAudioBitResolution
+               * 8; /*return mAudioInterface->getAudioBitResolution();*/
+    }
+    unsigned int getNumInputChannels() const
+    {
+        return mNumAudioChansIn; /*return mAudioInterface->getNumInputChannels();*/
+    }
+    unsigned int getNumOutputChannels() const
+    {
+        return mNumAudioChansOut; /*return mAudioInterface->getNumOutputChannels();*/
+    }
+    QString getAssignedClientName()
+    {
+#ifndef NO_JACK
+        if (mAudioInterface && mAudiointerfaceMode == JackTrip::JACK) {
+            return static_cast<JackAudioInterface*>(mAudioInterface)
+                ->getAssignedClientName();
+        } else {
+            return QLatin1String("");
+        }
+#else
+        return QLatin1String("");
+#endif
+    }
+    virtual bool checkPeerSettings(int8_t* full_packet);
+    void increaseSequenceNumber() { mPacketHeader->increaseSequenceNumber(); }
+    int getSequenceNumber() const { return mPacketHeader->getSequenceNumber(); }
+
+    uint64_t getPeerTimeStamp(int8_t* full_packet) const
+    {
+        return mPacketHeader->getPeerTimeStamp(full_packet);
+    }
+
+    uint16_t getPeerSequenceNumber(int8_t* full_packet) const
+    {
+        return mPacketHeader->getPeerSequenceNumber(full_packet);
+    }
+
+    uint16_t getPeerBufferSize(int8_t* full_packet) const
+    {
+        return mPacketHeader->getPeerBufferSize(full_packet);
+    }
+
+    uint8_t getPeerSamplingRate(int8_t* full_packet) const
+    {
+        return mPacketHeader->getPeerSamplingRate(full_packet);
+    }
+
+    uint8_t getPeerBitResolution(int8_t* full_packet) const
+    {
+        return mPacketHeader->getPeerBitResolution(full_packet);
+    }
+
+    uint8_t getPeerNumIncomingChannels(int8_t* full_packet) const
+    {
+        return mPacketHeader->getPeerNumIncomingChannels(full_packet);
+    }
+
+    uint8_t getPeerNumOutgoingChannels(int8_t* full_packet) const
+    {
+        if (0 == mPacketHeader->getPeerNumOutgoingChannels(full_packet)) {
+            return mPacketHeader->getPeerNumIncomingChannels(full_packet);
+        } else {
+            return mPacketHeader->getPeerNumOutgoingChannels(full_packet);
+        }
+    }
+
+    size_t getSizeInBytesPerChannel() const
+    {
+        return mAudioInterface->getSizeInBytesPerChannel();
+    }
+    int getHeaderSizeInBytes() const { return mPacketHeader->getHeaderSizeInBytes(); }
+    int getTotalAudioInputPacketSizeInBytes() const
+    {
+#ifdef WAIR  // WAIR
+        if (mNumNetRevChans)
+            return mAudioInterface->getSizeInBytesPerChannel() * mNumNetRevChans;
+        else  // not wair
+#endif        // endwhere
+            return int(mAudioInterface->getSizeInBytesPerChannel()) * mNumAudioChansIn;
+    }
+
+    int getTotalAudioOutputPacketSizeInBytes() const
+    {
+#ifdef WAIR  // WAIR
+        if (mNumNetRevChans)
+            return mAudioInterface->getSizeInBytesPerChannel() * mNumNetRevChans;
+        else  // not wair
+#endif        // endwhere
+            return int(mAudioInterface->getSizeInBytesPerChannel()) * mNumAudioChansOut;
+    }
+    QString getDevicesWarningMsg() const
+    {
+        if (mAudioInterface == nullptr)
+            return QLatin1String("");
+        return QString::fromStdString(mAudioInterface->getDevicesWarningMsg());
+    }
+    QString getDevicesErrorMsg() const
+    {
+        if (mAudioInterface == nullptr)
+            return QLatin1String("");
+        return QString::fromStdString(mAudioInterface->getDevicesErrorMsg());
+    }
+    QString getDevicesWarningHelpUrl() const
+    {
+        if (mAudioInterface == nullptr)
+            return QLatin1String("");
+        return QString::fromStdString(mAudioInterface->getDevicesWarningHelpUrl());
+    }
+    QString getDevicesErrorHelpUrl() const
+    {
+        if (mAudioInterface == nullptr)
+            return QLatin1String("");
+        return QString::fromStdString(mAudioInterface->getDevicesErrorHelpUrl());
+    }
+    bool getHighLatencyFlag() const
+    {
+        return (mAudioInterface == nullptr) ? false
+                                            : mAudioInterface->getHighLatencyFlag();
+    }
+    //@}
+    //------------------------------------------------------------------------------------
+
+    void printTextTest() { std::cout << "=== JackTrip PRINT ===" << std::endl; }
+    void printTextTest2() { std::cout << "=== JackTrip PRINT2 ===" << std::endl; }
+
+    void setNetIssuesSimulation(double loss, double jitter, double delay_rel)
+    {
+        mSimulatedLossRate   = loss;
+        mSimulatedJitterRate = jitter;
+        mSimulatedDelayRel   = delay_rel;
+    }
+    void setBroadcast(int broadcast_queue) { mBroadcastQueueLength = broadcast_queue; }
+    void queueLengthChanged(int queueLength)
+    {
+        emit signalQueueLengthChanged(queueLength);
+    }
+    void setUseRtUdpPriority(bool use) { mUseRtUdpPriority = use; }
+
+   public slots:
+    /// \brief Slot to stop all the processes and threads
+    virtual void slotStopProcesses() { this->stop(); }
+    virtual void slotStopProcessesDueToError(const QString& errorMessage)
+    {
+        this->stop(errorMessage);
+    }
+
+    /** \brief This slot emits in turn the signal signalNoUdpPacketsForSeconds
+     * when UDP has waited for more than 30 seconds.
+     *
+     * It is used to remove the thread from the server.
+     */
+    void slotUdpWaitingTooLongClientGoneProbably(int wait_msec)
+    {
+        int wait_time = 10000;  // msec
+        if (!(wait_msec % wait_time)) {
+            std::cerr << "UDP WAITED MORE THAN 10 seconds." << std::endl;
+            if (mStopOnTimeout) {
+                stop(QStringLiteral("No network data received for 10 seconds"));
+            }
+            emit signalNoUdpPacketsForSeconds();
+        }
+    }
+    void slotUdpWaitingTooLong() { emit signalUdpWaitingTooLong(); }
+    void slotPrintTest() { std::cout << "=== TESTING ===" << std::endl; }
+    void slotReceivedConnectionFromPeer()
+    {
+        mReceivedConnection = true;
+        emit signalReceivedConnectionFromPeer();
+    }
+    void onStatTimer();
+
+   private slots:
+    void receivedConnectionTCP();
+    void receivedDataTCP();
+    void receivedErrorTCP(QAbstractSocket::SocketError socketError);
+    void connectionSecured();
+    void receivedDataUDP();
+    void udpTimerTick();
+    void tcpTimerTick();
+
+   signals:
+    // void signalUdpTimeOut();
+    /// \brief Signal emitted when all the processes and threads are stopped
+    void signalProcessesStopped();
+    /// \brief Signal emitted when no UDP Packets have been received for a while
+    void signalNoUdpPacketsForSeconds();
+    void signalTcpClientConnected();
+    void signalError(const QString& errorMessage);
+    void signalReceivedConnectionFromPeer();
+    void signalUdpWaitingTooLong();
+    void signalQueueLengthChanged(int queueLength);
+    void signalAudioStarted();
+
+   public:
+    /// \brief Set the AudioInteface object
+    virtual void setupAudio(
+#ifdef WAIRTOHUB  // WAIR
+        int ID
+#endif  // endwhere
+    );
+    /// \brief Close the JackAudioInteface and disconnects it from JACK
+    void closeAudio();
+    /// \brief Set the DataProtocol objects
+    virtual void setupDataProtocol();
+    /// \brief Set the RingBuffer objects
+    void setupRingBuffers();
+    /// \brief Starts for the CLIENT mode
+    void clientStart();
+    /// \brief Starts for the SERVER mode
+    /// \param timeout Set the server to timeout after 2 seconds if no client connections
+    /// are received. Useful for the multithreaded server \return 0 on success, -1 on
+    /// error
+    int serverStart(bool timeout = false, int udpTimeout = gTimeOutMultiThreadedServer);
+    /// \brief Stats for the Client to Ping Server
+    /// \return -1 on error, 0 on success
+    virtual int clientPingToServerStart();
+    /// \brief Sets the client ID
+    ///
+    void setID(int ID) { mID = ID; }
+    /// \brief Gets the client ID
+    ///
+    int getID() { return mID; }
+
+   private:
+    int mID = 0;
+    jacktripModeT mJackTripMode;                        ///< JackTrip::jacktripModeT
+    dataProtocolT mDataProtocol;                        ///< Data Protocol Tipe
+    DataProtocol::packetHeaderTypeT mPacketHeaderType;  ///< Packet Header Type
+    JackTrip::audiointerfaceModeT mAudiointerfaceMode;
+
+    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)
+#endif                       // endwhere
+    int mBufferQueueLength;  ///< Audio Buffer from network queue length
+    int mBufferStrategy;
+    int mBroadcastQueueLength;
+    uint32_t mSampleRate;                             ///< Sample Rate
+    uint32_t mDeviceID;                               ///< RTAudio DeviceID
+    std::string mInputDeviceName, mOutputDeviceName;  ///< RTAudio device names
+    uint32_t mAudioBufferSize;  ///< Audio buffer size to process on each callback
+    AudioInterface::audioBitResolutionT mAudioBitResolution;  ///< Audio Bit Resolutions
+    bool mLoopBack;
+    QString mPeerAddress;  ///< Peer Address to use in jacktripModeT::CLIENT Mode
+
+    /// Pointer to Abstract Type DataProtocol that sends packets
+    DataProtocol* mDataProtocolSender;
+    /// Pointer to Abstract Type DataProtocol that receives packets
+    DataProtocol* mDataProtocolReceiver;
+    AudioInterface* mAudioInterface;  ///< Interface to Jack Client
+    PacketHeader* mPacketHeader;      ///< Pointer to Packet Header
+    underrunModeT mUnderRunMode;      ///< underrunModeT Mode
+    bool mStopOnTimeout;              ///< Stop on 10 second timeout
+
+    /// Pointer for the Send RingBuffer
+    RingBuffer* mSendRingBuffer;
+    /// Pointer for the Receive RingBuffer
+    RingBuffer* mReceiveRingBuffer;
+
+    int mReceiverBindPort;  ///< Incoming (receiving) port for local machine
+    int mSenderPeerPort;    ///< Incoming (receiving) port for peer machine
+    int mSenderBindPort;    ///< Outgoing (sending) port for local machine
+    int mReceiverPeerPort;  ///< Outgoing (sending) port for peer machine
+    int mTcpServerPort;
+
+    bool mUseAuth;
+    QString mUsername;
+    QString mPassword;
+
+    unsigned int mRedundancy;   ///< Redundancy factor in network data
+    QString mJackClientName;    ///< JackAudio Client Name
+    QString mRemoteClientName;  ///< Remote JackAudio Client Name for hub client mode
+
+    // JackTrip::connectionModeT mConnectionMode;  ///< Connection Mode
+    JackTrip::hubConnectionModeT
+        mHubConnectionModeT;  ///< Hub Server Jack Audio Patch Connection Mode
+
+    QVector<ProcessPlugin*>
+        mProcessPluginsFromNetwork;  ///< Vector of ProcessPlugin<EM>s</EM>
+    QVector<ProcessPlugin*>
+        mProcessPluginsToNetwork;  ///< Vector of ProcessPlugin<EM>s</EM>
+    QVector<ProcessPlugin*>
+        mProcessPluginsToMonitor;  ///< Vector of ProcessPlugin<EM>s</EM>
+    QTimer mTimeoutTimer;
+    QTimer mRetryTimer;
+    int mRetries;
+    int mSleepTime;
+    int mElapsedTime;
+    int mEndTime;
+    QSslSocket mTcpClient;
+    QUdpSocket mUdpSockTemp;
+    QMutex mTimerMutex;
+    bool mAwaitingUdp;
+    bool mAwaitingTcp;
+
+    volatile bool mReceivedConnection;  ///< Bool of received connection from peer
+    volatile bool mTcpConnectionError;
+    volatile bool mStopped;
+    volatile bool mHasShutdown;
+
+    bool mConnectDefaultAudioPorts;  ///< Connect or not default audio ports
+    QSharedPointer<std::ostream> mIOStatStream;
+    int mIOStatTimeout;
+    std::ostream mIOStatLogStream;
+    double mSimulatedLossRate;
+    double mSimulatedJitterRate;
+    double mSimulatedDelayRel;
+    bool mUseRtUdpPriority;
+
+    QSharedPointer<AudioTester> mAudioTesterP;
+};
+
+#endif
diff --git a/src/JackTripWorker.cpp b/src/JackTripWorker.cpp
new file mode 100644 (file)
index 0000000..2c59173
--- /dev/null
@@ -0,0 +1,378 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file JackTripWorker.cpp
+ * \author Juan-Pablo Caceres
+ * \date September 2008
+ */
+
+#include "JackTripWorker.h"
+
+#include <QMutexLocker>
+#include <QTimer>
+#include <QWaitCondition>
+#include <iostream>
+#include <limits>
+
+#include "JackTrip.h"
+#include "UdpHubListener.h"
+// #include "NetKS.h"
+#include "LoopBack.h"
+#include "Settings.h"
+#ifdef WAIR  // wair
+#include "dcblock2gain.dsp.h"
+#endif  // endwhere
+
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+JackTripWorker::JackTripWorker(UdpHubListener* udphublistener, int BufferQueueLength,
+                               JackTrip::underrunModeT UnderRunMode,
+                               AudioInterface::audioBitResolutionT AudioBitResolution,
+                               const QString& clientName)
+    : mAppendThreadID(false)
+    , mSleepTime(100)
+    , mUdpHubListener(udphublistener)
+    , mBufferQueueLength(BufferQueueLength)
+    , mUnderRunMode(UnderRunMode)
+    , mAudioBitResolution(AudioBitResolution)
+    , mClientName(clientName)
+{
+    // mNetks = new NetKS;
+    // mNetks->play();
+    connect(&mUdpSockTemp, &QUdpSocket::readyRead, this,
+            &JackTripWorker::receivedDataUDP);
+}
+
+//*******************************************************************************
+void JackTripWorker::setJackTrip(int id, const QString& client_address,
+                                 uint16_t server_port, uint16_t client_port,
+                                 bool connectDefaultAudioPorts)
+{
+    QMutexLocker locker(&mMutex);
+    mUdpSockTemp.close();
+    if (mRunning) {
+        mJackTrip->slotStopProcesses();
+#ifndef NO_JACK
+        if (mPatched) {
+            mUdpHubListener->unregisterClientWithPatcher(mAssignedClientName);
+            mPatched = false;
+        }
+#endif
+        mRunning = false;
+    }
+    // Set as spawning from this point on.
+    mSpawning = true;
+
+    mID = id;
+    // Set the jacktrip address and ports
+    mClientAddress             = client_address;
+    mServerPort                = server_port;
+    mClientPort                = client_port;
+    m_connectDefaultAudioPorts = connectDefaultAudioPorts;
+    mAssignedClientName        = QLatin1String("");
+
+    // Create and setup JackTrip Object
+    // JackTrip jacktrip(JackTrip::SERVER, JackTrip::UDP, mNumChans, 2);
+    if (gVerboseFlag)
+        cout << "---> JackTripWorker: Creating jacktrip objects..." << endl;
+
+#ifdef WAIR  // WAIR
+             // forces    BufferQueueLength to 2
+             // need to parse numNetChans from incoming header
+             // but force to 16 for now
+#define FORCEBUFFERQ 2
+    if (mUdpHubListener->isWAIR()) {  // invoked with -Sw
+        mWAIR           = true;
+        mNumNetRevChans = NUMNETREVCHANSbecauseNOTINRECEIVEDheader;
+    } else {
+    };
+#endif  // endwhere
+
+#ifndef __JAMTEST__
+#ifdef WAIR  // WAIR
+    //        bool tmp = mJTWorkers->at(id)->isWAIR();
+    //        qDebug() << "is WAIR?" <<  tmp ;
+    qDebug() << "mNumNetRevChans" << mNumNetRevChans;
+
+    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;
+        cout << gPrintSeparator << std::endl;
+        switch (mNumNetRevChans) {
+        case 16:  // freeverb
+            mJackTrip->appendProcessPluginFromNetwork(
+                new dcblock2gain(1));  // plugin slot 0
+            ///////////////
+            //            mJackTrip->appendProcessPlugin(new comb16server(mNumNetChans));
+            // -S LAIR no AP  mJackTrip->appendProcessPlugin(new AP8(mNumChans));
+            break;
+        default:
+            throw std::invalid_argument(
+                "Settings: mNumNetChans doesn't correspond to Faust plugin");
+            break;
+        }
+    }
+#else   // endwhere
+    mJackTrip.reset(new JackTrip(JackTrip::SERVERPINGSERVER, JackTrip::UDP, 0, 1, 0, 1,
+                                 AudioInterface::MIX_UNSET, mBufferQueueLength));
+#endif  // not wair
+#endif  // ifndef __JAMTEST__
+
+#ifdef __JAMTEST__
+    mJackTrip.reset(new JamTest(
+        JackTrip::SERVERPINGSERVER));  // ########### JamTest #################
+    // JackTrip jacktrip(JackTrip::SERVERPINGSERVER, JackTrip::UDP, mNumChans, 2);
+#endif
+}
+
+//*******************************************************************************
+void JackTripWorker::start()
+{
+    QMutexLocker lock(&mMutex);
+    if (!mSpawning) {
+        // Something else has aborted the connection.
+        return;
+    }
+
+    mJackTrip->setConnectDefaultAudioPorts(m_connectDefaultAudioPorts);
+
+    // Set our underrun mode and bit resolution
+    mJackTrip->setUnderRunMode(mUnderRunMode);
+    mJackTrip->setAudioBitResolution(mAudioBitResolution);
+    if (mIOStatTimeout > 0) {
+        mJackTrip->setIOStatTimeout(mIOStatTimeout);
+        mJackTrip->setIOStatStream(mIOStatStream);
+    }
+
+    if (!mClientName.isEmpty()) {
+        mJackTrip->setClientName(mClientName);
+    }
+
+    // ClientAddress.setAddress(mClientAddress);
+    // If I don't type this line, I get a bus error in the next line.
+    // I still haven't figure out why
+    // ClientAddress.toString().toLatin1().constData();
+    // jacktrip.setPeerAddress(ClientAddress.toString().toLatin1().constData());
+    if (mAppendThreadID) {
+        mJackTrip->setID(mID + 1);
+    }
+    mJackTrip->setPeerAddress(mClientAddress);
+    mJackTrip->setBindPorts(mServerPort);
+    // jacktrip.setPeerPorts(mClientPort);
+    mJackTrip->setBufferStrategy(mBufferStrategy);
+    mJackTrip->setNetIssuesSimulation(mSimulatedLossRate, mSimulatedJitterRate,
+                                      mSimulatedDelayRel);
+    mJackTrip->setBroadcast(mBroadcastQueue);
+    mJackTrip->setUseRtUdpPriority(mUseRtUdpPriority);
+
+    mTimeoutTimer.setInterval(mSleepTime);
+    connect(&mTimeoutTimer, &QTimer::timeout, this, &JackTripWorker::udpTimerTick);
+    mElapsedTime = 0;
+    mTimeoutTimer.start();
+    if (gVerboseFlag)
+        cout << "---> JackTripWorker: setJackTripFromClientHeader..." << endl;
+    if (!mUdpSockTemp.bind(QHostAddress::Any, mServerPort,
+                           QUdpSocket::DefaultForPlatform)) {
+        std::cerr
+            << "in JackTripWorker: Could not bind UDP socket. It may already be bound."
+            << endl;
+        throw std::runtime_error("Could not bind UDP socket. It may already be bound.");
+    }
+}
+
+//*******************************************************************************
+void JackTripWorker::stopThread()
+{
+    QMutexLocker locker(&mMutex);
+    if (mRunning) {
+        mRunning = false;
+        mJackTrip->slotStopProcesses();
+#ifndef NO_JACK
+        if (mPatched) {
+            mUdpHubListener->unregisterClientWithPatcher(mAssignedClientName);
+            mPatched = false;
+        }
+#endif
+    } else if (mSpawning) {
+        mSpawning = false;
+        mUdpSockTemp.close();
+        mTimeoutTimer.stop();
+    }
+}
+
+void JackTripWorker::receivedDataUDP()
+{
+    QMutexLocker lock(&mMutex);
+
+    if (!mSpawning || mUdpSockTemp.state() != QAbstractSocket::BoundState) {
+        // Check if something has interrupted the process.
+        return;
+    }
+    mTimeoutTimer.stop();
+
+    // Set our jacktrip parameters from the received header data.
+    quint16 port;
+    int packet_size     = mUdpSockTemp.pendingDatagramSize();
+    int8_t* full_packet = new int8_t[packet_size];
+    mUdpSockTemp.readDatagram(reinterpret_cast<char*>(full_packet), packet_size, nullptr,
+                              &port);
+    mUdpSockTemp.close();  // close the socket
+
+    // Alert the hub listener of the actual client port for incoming packets.
+    // This will remove any old worker objects, and will set the client port member
+    // variable on this object.
+    mUdpHubListener->releaseDuplicateThreads(this, port);
+
+    int PeerBufferSize          = mJackTrip->getPeerBufferSize(full_packet);
+    int PeerSamplingRate        = mJackTrip->getPeerSamplingRate(full_packet);
+    int PeerBitResolution       = mJackTrip->getPeerBitResolution(full_packet);
+    int PeerNumIncomingChannels = mJackTrip->getPeerNumIncomingChannels(full_packet);
+    int PeerNumOutgoingChannels = mJackTrip->getPeerNumOutgoingChannels(full_packet);
+    delete[] full_packet;
+
+    if (gVerboseFlag) {
+        cout << "JackTripWorker: getPeerBufferSize       = " << PeerBufferSize << "\n"
+             << "JackTripWorker: getPeerSamplingRate     = " << PeerSamplingRate << "\n"
+             << "JackTripWorker: getPeerBitResolution    = " << PeerBitResolution << "\n"
+             << "JackTripWorker: PeerNumIncomingChannels = " << PeerNumIncomingChannels
+             << "\n"
+             << "JackTripWorker: PeerNumOutgoingChannels = " << PeerNumOutgoingChannels
+             << "\n";
+    }
+
+    // The header field for NumOutgoingChannels was used for the ConnectionMode.
+    // Only the first Mode was used (NORMAL == 0). If this field is set to 0, we
+    // can assume the peer is using an old version, and the last field doesn't reflect the
+    // number of Outgoing Channels.
+    // The maximum of this field will be used as 0.
+    if (JackTrip::NORMAL == PeerNumOutgoingChannels) {
+        mJackTrip->setNumInputChannels(PeerNumIncomingChannels);
+        mJackTrip->setNumOutputChannels(PeerNumIncomingChannels);
+    } else if ((std::numeric_limits<uint8_t>::max)() == PeerNumOutgoingChannels) {
+        mJackTrip->setNumInputChannels(PeerNumIncomingChannels);
+        mJackTrip->setNumOutputChannels(0);
+    } else {
+        mJackTrip->setNumInputChannels(PeerNumIncomingChannels);
+        mJackTrip->setNumOutputChannels(PeerNumOutgoingChannels);
+    }
+
+    if (PeerNumOutgoingChannels == -1) {
+        // Shut it down
+        mSpawning = false;
+        mUdpHubListener->releaseThread(mID);
+    }
+
+    // Connect signals and slots
+    // -------------------------
+    if (gVerboseFlag)
+        cout << "---> JackTripWorker: Connecting signals and slots..." << endl;
+    // Connection to terminate JackTrip when packets haven't arrive for
+    // a certain amount of time
+    connect(mJackTrip.data(), &JackTrip::signalNoUdpPacketsForSeconds, mJackTrip.data(),
+            &JackTrip::slotStopProcesses, Qt::QueuedConnection);
+    connect(mJackTrip.data(), &JackTrip::signalProcessesStopped, this,
+            &JackTripWorker::jacktripStopped, Qt::QueuedConnection);
+    connect(mJackTrip.data(), &JackTrip::signalError, this,
+            &JackTripWorker::jacktripStopped, Qt::QueuedConnection);
+#ifndef NO_JACK
+    connect(mJackTrip.data(), &JackTrip::signalAudioStarted, this,
+            &JackTripWorker::alertPatcher, Qt::QueuedConnection);
+#endif
+    connect(this, &JackTripWorker::signalRemoveThread, mJackTrip.data(),
+            &JackTrip::slotStopProcesses, Qt::QueuedConnection);
+
+    if (gVerboseFlag)
+        cout << "---> JackTripWorker: startProcess..." << endl;
+    mJackTrip->startProcess(
+#ifdef WAIRTOHUB  // wair
+        mID
+#endif  // endwhere
+    );
+    mRunning  = true;
+    mSpawning = false;
+    // if (gVerboseFlag) cout << "---> JackTripWorker: start..." << endl;
+    // jacktrip.start(); // ########### JamTest Only #################
+}
+
+void JackTripWorker::udpTimerTick()
+{
+    QMutexLocker lock(&mMutex);
+    if (!mSpawning) {
+        mTimeoutTimer.stop();
+        return;
+    }
+    mElapsedTime += mSleepTime;
+    if (gVerboseFlag)
+        cout << "---------> ELAPSED TIME: " << mElapsedTime << endl;
+    // Check if we've timed out.
+    if (gTimeOutMultiThreadedServer > 0 && mElapsedTime >= gTimeOutMultiThreadedServer) {
+        std::cerr << "--->JackTripWorker: is not receiving Datagrams (timeout)" << endl;
+        mTimeoutTimer.stop();
+        mUdpSockTemp.close();
+        mSpawning = false;
+        mUdpHubListener->releaseThread(mID);
+    }
+}
+
+void JackTripWorker::jacktripStopped()
+{
+    QMutexLocker lock(&mMutex);
+    if (mSpawning || !mRunning) {
+        // This has already been taken care of elsewhere.
+        return;
+    }
+    mRunning = false;
+#ifndef NO_JACK
+    if (mPatched) {
+        mUdpHubListener->unregisterClientWithPatcher(mAssignedClientName);
+        mPatched = false;
+    }
+#endif
+    mUdpHubListener->releaseThread(mID);
+}
+
+void JackTripWorker::alertPatcher()
+{
+#ifndef NO_JACK
+    QMutexLocker lock(&mMutex);
+    if (mRunning) {
+        mAssignedClientName = mJackTrip->getAssignedClientName();
+        mUdpHubListener->registerClientWithPatcher(mAssignedClientName);
+        mPatched = true;
+    }
+#endif
+}
diff --git a/src/JackTripWorker.h b/src/JackTripWorker.h
new file mode 100644 (file)
index 0000000..dc31a15
--- /dev/null
@@ -0,0 +1,193 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file JackTripWorker.h
+ * \author Juan-Pablo Caceres
+ * \date September 2008
+ */
+
+#ifndef __JACKTRIPWORKER_H__
+#define __JACKTRIPWORKER_H__
+
+#include <QHostAddress>
+#include <QMutex>
+#include <QObject>
+#include <QUdpSocket>
+#include <iostream>
+
+#include "JackTrip.h"
+#include "jacktrip_globals.h"
+#ifdef __JAMTEST__
+#include "JamTest.h"
+#endif
+
+// class JackTrip; // forward declaration
+class UdpHubListener;  // forward declaration
+
+/** \brief Prototype of the worker class that will be cloned through sending threads to
+ * the Thread Pool
+ *
+ * This class can be send to the ThreadPool using the start() method. Each time
+ * it is sent, it'll became "independent" of the prototype, which means
+ * that the prototype state can be changed, and used to send and start
+ * another thread into the pool. setAutoDelete must be set to false
+ * in order for this to work.
+ */
+// Note that it is not possible to start run() as an event loop. That has to be
+// implemented inside a QThread
+class JackTripWorker : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    /// \brief The class constructor
+    JackTripWorker(
+        UdpHubListener* udphublistener, int BufferQueueLength = gDefaultQueueLength,
+        JackTrip::underrunModeT UnderRunMode                   = JackTrip::WAVETABLE,
+        AudioInterface::audioBitResolutionT AudioBitResolution = AudioInterface::BIT16,
+        const QString& clientName                              = QLatin1String(""));
+    /// \brief The class destructor
+    virtual ~JackTripWorker() { stopThread(); }
+
+    /// \brief Starts the jacktrip process
+    void start();
+    /// \brief Check if the Thread is Spawning
+    /// \return true is it is spawning, false if it's already running
+    bool isSpawning()
+    {
+        QMutexLocker lock(&mMutex);
+        return mSpawning;
+    }
+    /// \brief Check if jacktrip is running
+    /// \return true if it is running, false if not
+    bool isRunning()
+    {
+        QMutexLocker lock(&mMutex);
+        return mRunning;
+    }
+    /// \brief Sets the JackTripWorker properties
+    /// \param id ID number
+    /// \param address
+    void setJackTrip(int id, const QString& client_address, uint16_t server_port,
+                     uint16_t client_port, bool connectDefaultAudioPorts);
+    /// Stop thread
+    void stopThread();
+    int getID() { return mID; }
+
+    void setBufferStrategy(int BufferStrategy) { mBufferStrategy = BufferStrategy; }
+    void setNetIssuesSimulation(double loss, double jitter, double delay_rel)
+    {
+        mSimulatedLossRate   = loss;
+        mSimulatedJitterRate = jitter;
+        mSimulatedDelayRel   = delay_rel;
+    }
+    void setBroadcast(int broadcast_queue) { mBroadcastQueue = broadcast_queue; }
+    void setUseRtUdpPriority(bool use) { mUseRtUdpPriority = use; }
+
+    void setIOStatTimeout(int timeout) { mIOStatTimeout = timeout; }
+    void setIOStatStream(QSharedPointer<std::ostream> statStream)
+    {
+        mIOStatStream = statStream;
+    }
+
+    bool mAppendThreadID;
+
+    void setClientPort(uint16_t port) { mClientPort = port; }
+    QString getAssignedClientName() { return mAssignedClientName; }
+    uint16_t getServerPort() { return mServerPort; }
+    uint16_t getClientPort() { return mClientPort; }
+    QString getClientAddress() { return mClientAddress; }
+
+   private slots:
+    void slotTest() { std::cout << "--- JackTripWorker TEST SLOT ---" << std::endl; }
+    void receivedDataUDP();
+    void udpTimerTick();
+    void jacktripStopped();
+    void alertPatcher();
+
+   signals:
+    void signalRemoveThread();
+
+   private:
+    JackTrip::connectionModeT getConnectionModeFromHeader();
+
+    QUdpSocket mUdpSockTemp;
+    QTimer mTimeoutTimer;
+    int mSleepTime;
+    int mElapsedTime;
+#ifdef __JAMTEST__
+    QScopedPointer<JamTest> mJackTrip;
+#else
+    QScopedPointer<JackTrip> mJackTrip;
+#endif
+
+    UdpHubListener* mUdpHubListener;  ///< Hub Listener Socket
+    // QHostAddress mClientAddress; ///< Client Address
+    QString mClientAddress;
+    uint16_t mServerPort;  ///< Server Ephemeral Incoming Port to use with Client
+    bool m_connectDefaultAudioPorts = false;
+
+    /// Client Outgoing Port. By convention, the receiving port will be <tt>mClientPort
+    /// -1</tt>
+    uint16_t mClientPort;
+
+    int mBufferQueueLength;
+    JackTrip::underrunModeT mUnderRunMode;
+    AudioInterface::audioBitResolutionT mAudioBitResolution;
+    QString mClientName;
+    QString mAssignedClientName;
+
+    /// Thread spawning internal lock.
+    /// If true, the prototype is working on creating (spawning) a new thread
+    volatile bool mSpawning = false;
+    volatile bool mRunning  = false;
+    volatile bool mPatched  = false;
+    QMutex mMutex;  ///< Mutex to protect mSpawning
+
+    int mID = 0;  ///< ID thread number
+
+    int mBufferStrategy         = 1;
+    int mBroadcastQueue         = 0;
+    double mSimulatedLossRate   = 0.0;
+    double mSimulatedJitterRate = 0.0;
+    double mSimulatedDelayRel   = 0.0;
+    bool mUseRtUdpPriority      = false;
+    int mIOStatTimeout          = 0;
+
+    QSharedPointer<std::ostream> mIOStatStream;
+#ifdef WAIR                   // wair
+    int mNumNetRevChans = 0;  ///< Number of Net Channels = net combs
+    bool mWAIR          = false;
+#endif  // endwhere
+};
+
+#endif  //__JACKTRIPWORKER_H__
diff --git a/src/JitterBuffer.cpp b/src/JitterBuffer.cpp
new file mode 100644 (file)
index 0000000..99b1121
--- /dev/null
@@ -0,0 +1,387 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2020 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 JitterBuffer.cpp
+ * \author Anton Runov
+ * \date June 2020
+ */
+
+#include "JitterBuffer.h"
+
+#include <cmath>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <stdexcept>
+
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+JitterBuffer::JitterBuffer(int buf_samples, int qlen, int sample_rate, int strategy,
+                           int bcast_qlen, int channels, int bit_res)
+    : RingBuffer(0, 0)
+{
+    int total_size = sample_rate * channels * bit_res * 2;  // 2 secs of audio
+    int slot_size  = buf_samples * channels * bit_res;
+    mSlotSize      = slot_size;
+    mInSlotSize    = slot_size;
+    if (0 < qlen) {
+        mMaxLatency = qlen * slot_size;
+        mAutoQueue  = 0;
+    } else {
+        // AutoQueue
+        mMaxLatency = 3 * slot_size;
+        mAutoQueue  = 1;
+    }
+    mTotalSize        = total_size;
+    mBroadcastLatency = bcast_qlen * mSlotSize;
+    mNumChannels      = channels;
+    mAudioBitRes      = bit_res;
+    mMinStepSize      = channels * bit_res;
+    mFPP              = buf_samples;
+    mSampleRate       = sample_rate;
+    mActive           = false;
+
+    // Defaults for zero strategy
+    mUnderrunIncTolerance = -10 * mSlotSize;
+    mCorrIncTolerance =
+        100 * mMaxLatency;  // should be greater than mUnderrunIncTolerance
+    mOverflowDecTolerance  = 100 * mMaxLatency;
+    mWritePosition         = mMaxLatency;
+    mStatUnit              = mSlotSize;
+    mLevelDownRate         = std::min<int>(256, mFPP) / (5.0 * sample_rate) * mSlotSize;
+    mOverflowDropStep      = mMaxLatency / 2;
+    mLevelCur              = mMaxLatency;
+    mLevel                 = mLevelCur;
+    mMinLevelThreshold     = 1.9 * mSlotSize;
+    mBroadcastPosition     = 0;
+    mBroadcastPositionCorr = 0.0;
+    mLastCorrCounter       = 0;
+    mLastCorrDirection     = 0;
+
+    switch (strategy) {
+    case 1:
+        mOverflowDropStep = mSlotSize;
+        break;
+    case 2:
+        mUnderrunIncTolerance = 1.1 * mSlotSize;
+        mCorrIncTolerance =
+            1.9 * mSlotSize;  // should be greater than mUnderrunIncTolerance
+        mOverflowDecTolerance = 0.1 * mSlotSize;
+        mOverflowDropStep     = mSlotSize;
+        break;
+    }
+
+    mRingBuffer = new int8_t[mTotalSize];
+    std::memset(mRingBuffer, 0, mTotalSize);
+
+    mAutoQueueCorr = 2 * mSlotSize;
+    if (0 > qlen) {
+        mAutoQFactor = 1.0 / -qlen;
+    } else {
+        mAutoQFactor = 1.0 / 500;
+    }
+    mAutoQRate      = mSlotSize * 0.5;
+    mAutoQRateMin   = mSlotSize * 0.0005;
+    mAutoQRateDecay = 1.0 - std::min<double>(mFPP * 1.2e-6, 0.0005);
+}
+
+//*******************************************************************************
+bool JitterBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen,
+                                         [[maybe_unused]] int seq_num)
+{
+    if (0 == len) {
+        len = mSlotSize;
+    }
+    QMutexLocker locker(&mMutex);
+    mInSlotSize = len;
+    if (!mActive) {
+        mActive = true;
+    }
+    if (mMaxLatency < len + mSlotSize) {
+        mMaxLatency = len + mSlotSize;
+    }
+    if (0 < lostLen) {
+        processPacketLoss(lostLen);
+    }
+    mSkewRaw += mReadsNew - len;
+    mReadsNew = 0;
+    mUnderruns += mUnderrunsNew;
+    mUnderrunsNew = 0;
+    mLevel        = mSlotSize * std::ceil(mLevelCur / mSlotSize);
+
+    // Update positions if necessary
+    int32_t available = mWritePosition - mReadPosition;
+
+    int delta = 0;
+    if (available < -10 * mMaxLatency) {
+        delta = available;
+        mBufIncUnderrun += -delta;
+        mLevelCur = len;
+        // cout << "reset" << endl;
+    } else if (available + len > mMaxLatency) {
+        delta = mOverflowDropStep;
+        mOverflows += delta;
+        mBufDecOverflow += delta;
+        mLevelCur = mMaxLatency;
+    } else if (0 > available
+               && mLevelCur < std::max<double>(mInSlotSize + mMinLevelThreshold,
+                                               mMaxLatency - mUnderrunIncTolerance
+                                                   - 2 * mSlotSize * lastCorrFactor())) {
+        delta = -std::min<int32_t>(-available, mSlotSize);
+        mBufIncUnderrun += -delta;
+    } else if (mLevelCur
+               < mMaxLatency - mCorrIncTolerance - 6 * mSlotSize * lastCorrFactor()) {
+        delta = -mSlotSize;
+        mUnderruns += -delta;
+        mBufIncCompensate += -delta;
+    }
+
+    if (0 != delta) {
+        mReadPosition += delta;
+        mLastCorrCounter   = 0;
+        mLastCorrDirection = 0 < delta ? 1 : -1;
+    } else {
+        ++mLastCorrCounter;
+    }
+
+    int wpos = mWritePosition % mTotalSize;
+    int n    = std::min<int>(mTotalSize - wpos, len);
+    std::memcpy(mRingBuffer + wpos, ptrToSlot, n);
+    if (n < len) {
+        // cout << "split write: " << len << "-" << n << endl;
+        std::memcpy(mRingBuffer, ptrToSlot + n, len - n);
+    }
+    mWritePosition += len;
+
+    return true;
+}
+
+//*******************************************************************************
+void JitterBuffer::readSlotNonBlocking(int8_t* ptrToReadSlot)
+{
+    int len = mSlotSize;
+    QMutexLocker locker(&mMutex);
+    if (!mActive) {
+        std::memset(ptrToReadSlot, 0, len);
+        return;
+    }
+    mReadsNew += len;
+    int32_t available = mWritePosition - mReadPosition;
+    if (available < mLevelCur) {
+        mLevelCur = std::max<double>((double)available, mLevelCur - mLevelDownRate);
+    } else {
+        mLevelCur = available;
+    }
+
+    // auto queue correction
+    if (0 > available + mAutoQueueCorr - mLevelCur) {
+        mAutoQueueCorr += mAutoQRate;
+    } else if (mInSlotSize + mSlotSize < mAutoQueueCorr) {
+        mAutoQueueCorr -= mAutoQRate * mAutoQFactor;
+    }
+    if (mAutoQRate > mAutoQRateMin) {
+        mAutoQRate *= mAutoQRateDecay;
+    }
+    if (0 != mAutoQueue) {
+        int PPS = mSampleRate / mFPP;
+        if (2 * PPS == mAutoQueue++ % (4 * PPS)) {
+            double k = 1.0 + 1e-5 / mAutoQFactor;
+            if (12 * PPS > mAutoQueue
+                || std::abs(mAutoQueueCorr * k - mMaxLatency + mSlotSize / 2)
+                       > 0.6 * mSlotSize) {
+                mMaxLatency = mSlotSize * std::ceil(mAutoQueueCorr * k / mSlotSize);
+                cout << "AutoQueue: " << mMaxLatency / mSlotSize << endl;
+                if (mJackTrip != nullptr) {
+                    mJackTrip->queueLengthChanged(mMaxLatency / mSlotSize);
+                }
+            }
+        }
+    }
+
+    int read_len = qBound(0, available, len);
+    int rpos     = mReadPosition % mTotalSize;
+    int n        = std::min<int>(mTotalSize - rpos, read_len);
+    std::memcpy(ptrToReadSlot, mRingBuffer + rpos, n);
+    if (n < read_len) {
+        // cout << "split read: " << read_len << "-" << n << endl;
+        std::memcpy(ptrToReadSlot + n, mRingBuffer, read_len - n);
+    }
+    if (read_len < len) {
+        std::memset(ptrToReadSlot + read_len, 0, len - read_len);
+        mUnderrunsNew += len - read_len;
+    }
+    mReadPosition += len;
+}
+
+//*******************************************************************************
+void JitterBuffer::readBroadcastSlot(int8_t* ptrToReadSlot)
+{
+    int len = mSlotSize;
+    QMutexLocker locker(&mMutex);
+    if (mBroadcastLatency + len > mReadPosition) {
+        std::memset(ptrToReadSlot, 0, len);
+        return;
+    }
+    // latency correction
+    int32_t d = mReadPosition - mBroadcastLatency - mBroadcastPosition - len;
+    if (static_cast<uint32_t>(std::abs(d)) > mBroadcastLatency / 2) {
+        mBroadcastPosition     = mReadPosition - mBroadcastLatency - len;
+        mBroadcastPositionCorr = 0.0;
+        mBroadcastSkew += d / mMinStepSize;
+    } else {
+        mBroadcastPositionCorr += 0.0003 * d;
+        int delta = mBroadcastPositionCorr / mMinStepSize;
+        if (0 != delta) {
+            mBroadcastPositionCorr -= delta * mMinStepSize;
+            if (2 == mAudioBitRes
+                && (int32_t)(mWritePosition - mBroadcastPosition) > len) {
+                // interpolate
+                len += delta * mMinStepSize;
+            } else {
+                // skip
+                mBroadcastPosition += delta * mMinStepSize;
+            }
+            mBroadcastSkew += delta;
+        }
+    }
+    mBroadcastDelta   = d / mMinStepSize;
+    int32_t available = mWritePosition - mBroadcastPosition;
+    int read_len      = qBound(0, available, len);
+    if (len == mSlotSize) {
+        int rpos = mBroadcastPosition % mTotalSize;
+        int n    = std::min<int>(mTotalSize - rpos, read_len);
+        std::memcpy(ptrToReadSlot, mRingBuffer + rpos, n);
+        if (n < read_len) {
+            // cout << "split read: " << read_len << "-" << n << endl;
+            std::memcpy(ptrToReadSlot + n, mRingBuffer, read_len - n);
+        }
+        if (read_len < len) {
+            std::memset(ptrToReadSlot + read_len, 0, len - read_len);
+        }
+    } else {
+        // interpolation len => mSlotSize
+        double K = 1.0 * len / mSlotSize;
+        for (int c = 0; c < mMinStepSize; c += sizeof(int16_t)) {
+            for (int j = 0; j < mSlotSize / mMinStepSize; ++j) {
+                int j1     = std::floor(j * K);
+                double a   = j * K - j1;
+                int rpos   = (mBroadcastPosition + j1 * mMinStepSize + c) % mTotalSize;
+                int16_t v1 = *(int16_t*)(mRingBuffer + rpos);
+                rpos       = (rpos + mMinStepSize) % mTotalSize;
+                int16_t v2 = *(int16_t*)(mRingBuffer + rpos);
+                *(int16_t*)(ptrToReadSlot + j * mMinStepSize + c) =
+                    std::round((1 - a) * v1 + a * v2);
+            }
+        }
+    }
+    mBroadcastPosition += len;
+}
+
+//*******************************************************************************
+void JitterBuffer::processPacketLoss(int lostLen)
+{
+    mSkewRaw -= lostLen;
+
+    int32_t available = mWritePosition - mReadPosition;
+    int delta =
+        std::min<int32_t>(available + mInSlotSize + lostLen - mMaxLatency, lostLen);
+    if (0 < delta) {
+        lostLen -= delta;
+        mBufDecPktLoss += delta;
+        mLevelCur          = mMaxLatency;
+        mLastCorrCounter   = 0;
+        mLastCorrDirection = 1;
+    } else if (mSlotSize < available + lostLen
+               && (mOverflowDecTolerance > mMaxLatency  // for strategies 0,1
+                   || (0 < mLastCorrDirection
+                       && mLevelCur > mMaxLatency
+                                          - mOverflowDecTolerance
+                                                * (1.1 - lastCorrFactor())))) {
+        delta = std::min<int>(lostLen, mSlotSize);
+        lostLen -= delta;
+        mBufDecPktLoss += delta;
+        mLevelCur -= delta;
+        mLastCorrCounter   = 0;
+        mLastCorrDirection = 1;
+    }
+    if (lostLen >= mTotalSize) {
+        std::memset(mRingBuffer, 0, mTotalSize);
+        mUnderruns += std::max<int>(0, lostLen - std::max<int>(0, -available));
+    } else if (0 < lostLen) {
+        int wpos = mWritePosition % mTotalSize;
+        int n    = std::min<int>(mTotalSize - wpos, lostLen);
+        std::memset(mRingBuffer + wpos, 0, n);
+        if (n < lostLen) {
+            // cout << "split write: " << lostLen << "-" << n << endl;
+            std::memset(mRingBuffer, 0, lostLen - n);
+        }
+        mUnderruns += std::max<int>(0, lostLen - std::max<int>(0, -available));
+    }
+    mWritePosition += lostLen;
+}
+
+//*******************************************************************************
+bool JitterBuffer::getStats(RingBuffer::IOStat* stat, bool reset)
+{
+    QMutexLocker locker(&mMutex);
+    if (reset) {
+        mUnderruns        = 0;
+        mOverflows        = 0;
+        mSkew0            = mLevel;
+        mSkewRaw          = 0;
+        mBufDecOverflow   = 0;
+        mBufDecPktLoss    = 0;
+        mBufIncUnderrun   = 0;
+        mBufIncCompensate = 0;
+        mBroadcastSkew    = 0;
+    }
+    stat->underruns = mUnderruns / mStatUnit;
+    stat->overflows = mOverflows / mStatUnit;
+    stat->skew      = (int32_t)((mSkew0 - mLevel + mBufIncUnderrun + mBufIncCompensate
+                            - mBufDecOverflow - mBufDecPktLoss))
+                 / mStatUnit;
+    stat->skew_raw = mSkewRaw / mStatUnit;
+    stat->level    = mLevel / mStatUnit;
+
+    stat->buf_dec_overflows  = mBufDecOverflow / mStatUnit;
+    stat->buf_dec_pktloss    = mBufDecPktLoss / mStatUnit;
+    stat->buf_inc_underrun   = mBufIncUnderrun / mStatUnit;
+    stat->buf_inc_compensate = mBufIncCompensate / mStatUnit;
+    stat->broadcast_skew     = mBroadcastSkew;
+    stat->broadcast_delta    = mBroadcastDelta;
+
+    stat->autoq_corr = mAutoQueueCorr / mStatUnit * 10;
+    stat->autoq_rate = mAutoQRate / mStatUnit * 1000;
+    return true;
+}
diff --git a/src/JitterBuffer.h b/src/JitterBuffer.h
new file mode 100644 (file)
index 0000000..a4ca931
--- /dev/null
@@ -0,0 +1,98 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2020 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 JitterBuffer.h
+ * \author Anton Runov
+ * \date June 2020
+ */
+
+#ifndef __JITTERBUFFER_H__
+#define __JITTERBUFFER_H__
+
+#include "JackTrip.h"
+#include "RingBuffer.h"
+
+class JitterBuffer : public RingBuffer
+{
+   public:
+    JitterBuffer(int buf_samples, int qlen, int sample_rate, int strategy, int bcast_qlen,
+                 int channels, int bit_res);
+    virtual ~JitterBuffer() {}
+
+    virtual bool insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen,
+                                       int seq_num);
+    virtual void readSlotNonBlocking(int8_t* ptrToReadSlot);
+    virtual void readBroadcastSlot(int8_t* ptrToReadSlot);
+
+    virtual bool getStats(IOStat* stat, bool reset);
+
+    void setJackTrip(JackTrip* jackTrip) { mJackTrip = jackTrip; }
+
+   protected:
+    void processPacketLoss(int lostLen);
+
+   protected:
+    int mMaxLatency;
+    int mNumChannels;
+    int mAudioBitRes;
+    int mMinStepSize;
+    int mFPP;
+    int mSampleRate;
+    int mInSlotSize;
+    bool mActive;
+    uint32_t mBroadcastLatency;
+    uint32_t mBroadcastPosition;
+    double mBroadcastPositionCorr;
+
+    double mUnderrunIncTolerance;
+    double mCorrIncTolerance;
+    double mOverflowDecTolerance;
+    int mOverflowDropStep;
+    uint32_t mLastCorrCounter;
+    int mLastCorrDirection;
+    double mMinLevelThreshold;
+    double lastCorrFactor() const
+    {
+        return 500.0 / std::max<uint32_t>(500U, mLastCorrCounter);
+    }
+
+    int mAutoQueue;
+    double mAutoQueueCorr;
+    double mAutoQFactor;
+    double mAutoQRate;
+    double mAutoQRateMin;
+    double mAutoQRateDecay;
+
+    JackTrip* mJackTrip;
+};
+
+#endif  //__JITTERBUFFER_H__
diff --git a/src/Limiter.cpp b/src/Limiter.cpp
new file mode 100644 (file)
index 0000000..8796075
--- /dev/null
@@ -0,0 +1,136 @@
+//*****************************************************************
+/*
+  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 Limiter.cpp
+ * \author Julius Smith, based on LoopBack.h
+ * \date May-Nov 2020
+ * \license MIT
+ */
+
+#include "Limiter.h"
+
+#include "jacktrip_types.h"
+#include "limiterdsp.h"
+
+//*******************************************************************************
+Limiter::Limiter(int numchans, int numclients, bool verboseFlag)
+    : mNumChannels(numchans)
+    , mNumClients(numclients)
+    , warningAmp(0.0)
+    , warnCount(0)
+    , peakMagnitude(0.0)
+    , nextWarning(1)
+{
+    setVerbose(verboseFlag);
+    for (int i = 0; i < mNumChannels; i++) {
+        limiterdsp* dsp_ptr = new limiterdsp;
+        APIUI* ui_ptr       = new APIUI;
+        limiterP.push_back(dsp_ptr);
+        limiterUIP.push_back(ui_ptr);  // #included in limiterdsp.h
+        dsp_ptr->buildUserInterface(ui_ptr);
+#ifdef SINE_TEST
+        limitertest* test_ptr = new limitertest;
+        ui_ptr                = new APIUI;
+        limiterTestP.push_back(test_ptr);
+        limiterTestUIP.push_back(ui_ptr);  // #included in limitertest.h
+        test_ptr->buildUserInterface(ui_ptr);
+#endif
+    }
+    //    std::cout << "Limiter: constructed for "
+    // << mNumChannels << " channels and "
+    // << mNumClients << " assumed clients\n";
+}
+
+//*******************************************************************************
+Limiter::~Limiter()
+{
+    for (int i = 0; i < mNumChannels; i++) {
+        delete static_cast<limiterdsp*>(limiterP[i]);
+        delete static_cast<APIUI*>(limiterUIP[i]);
+    }
+    limiterP.clear();
+    limiterUIP.clear();
+}
+
+//*******************************************************************************
+void Limiter::init(int samplingRate, int bufferSize)
+{
+    ProcessPlugin::init(samplingRate, bufferSize);
+    fs = float(fSamplingFreq);
+    for (int i = 0; i < mNumChannels; i++) {
+        static_cast<limiterdsp*>(limiterP[i])
+            ->init(fs);  // compression filter parameters depend on sampling rate
+        APIUI* ui_ptr = static_cast<APIUI*>(limiterUIP[i]);
+        int ndx       = ui_ptr->getParamIndex("NumClientsAssumed");
+        ui_ptr->setParamValue(ndx, mNumClients);
+#ifdef SINE_TEST
+        static_cast<limitertest*>(limiterTestP[i])
+            ->init(fs);  // oscillator parameters depend on sampling rate
+        ui_ptr = static_cast<APIUI*>(limiterTestUIP[i]);
+        ndx    = ui_ptr->getParamIndex("Amp");
+        ui_ptr->setParamValue(ndx, 0.2);
+        ndx = ui_ptr->getParamIndex("Freq");
+        float sineFreq =
+            110.0 * pow(1.5, double(i))
+            * (mNumClients > 1 ? 1.25 : 1.0);  // Maj 7 chord for stereo in & out
+        ui_ptr->setParamValue(ndx, sineFreq);
+#endif
+    }
+    inited = true;
+}
+
+//*******************************************************************************
+void Limiter::compute(int nframes, float** inputs, float** outputs)
+{
+    if (!inited) {
+        std::cerr << "*** Limiter " << this << ": init never called! Doing it now.\n";
+        init(0, 0);
+    }
+#ifdef SINE_TEST
+    float sineTestOut[nframes];
+    float* faustSigs[1]{sineTestOut};
+#endif
+    for (int i = 0; i < mNumChannels; i++) {
+        if (warningAmp > 0.0) {
+            checkAmplitudes(nframes,
+                            inputs[i]);  // we presently do one check across all channels
+        }
+        static_cast<limiterdsp*>(limiterP[i])->compute(nframes, &inputs[i], &outputs[i]);
+#ifdef SINE_TEST
+        static_cast<limitertest*>(limiterTestP[i])
+            ->compute(nframes, faustSigs, faustSigs);
+        for (int n = 0; n < nframes; n++) {
+            outputs[i][n] = outputs[i][n] + sineTestOut[n];
+        }
+#endif
+    }
+}
diff --git a/src/Limiter.h b/src/Limiter.h
new file mode 100644 (file)
index 0000000..e55a92d
--- /dev/null
@@ -0,0 +1,150 @@
+//*****************************************************************
+/*
+  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 Limiter.h
+ * \author Julius Smith, based on LoopBack.h
+ * \date May-Nov 2020
+ * \license MIT
+ */
+
+/** \brief Applies limiter_lad_mono from the faustlibraries distribution, compressors.lib
+ *
+ */
+#ifndef __LIMITER_H__
+#define __LIMITER_H__
+
+// #define SINE_TEST
+
+#ifdef SINE_TEST
+#include "limitertest.h"
+#endif
+
+#include <cassert>
+#include <cmath>
+#include <iostream>
+#include <vector>
+
+#include "ProcessPlugin.h"
+
+/** \brief The Limiter class confines the output dynamic range to a
+ *  "dynamic range lane" determined by the assumed number of clients.
+ */
+class Limiter : public ProcessPlugin
+{
+   public:
+    /// \brief The class constructor sets the number of channels to limit
+    Limiter(int numchans, int numclients, bool verboseFlag = false);
+
+    /// \brief The class destructor
+    virtual ~Limiter();
+
+    void init(int samplingRate, int bufferSize) override;
+    int getNumInputs() override { return (mNumChannels); }
+    int getNumOutputs() override { return (mNumChannels); }
+    void compute(int nframes, float** inputs, float** outputs) override;
+    const char* getName() const override { return "Limiter"; }
+
+    void setWarningAmplitude(double wa)
+    {  // setting to 0 turns off warnings
+        warningAmp = std::max<double>(0.0, std::min<double>(1.0, wa));
+    }
+
+   private:
+    void checkAmplitudes(int nframes, float* buf)
+    {
+        const int maxWarningInterval{10000};  // this could become an option
+        assert(warningAmp > 0.0);
+        assert(mNumClients > 0);
+        for (int i = 0; i < nframes; i++) {
+            double tmp_sample = double(buf[i]);
+            double limiterAmp =
+                fabs(tmp_sample)
+                / sqrt(double(mNumClients));  // KEEP IN SYNC with gain in
+                                              // ../faust-src/limiterdsp.dsp
+            if (limiterAmp >= warningAmp) {
+                warnCount++;
+                peakMagnitude = std::max<double>(peakMagnitude, limiterAmp);
+                if (warnCount == nextWarning) {
+                    double peakMagnitudeDB = 20.0 * std::log10(peakMagnitude);
+                    double warningAmpDB    = 20.0 * std::log10(warningAmp);
+                    if (warnCount == 1) {
+                        if (warningAmp == 1.0) {
+                            std::cerr << "*** Limiter.cpp: Audio HARD-CLIPPED!\n";
+                            fprintf(stderr,
+                                    "\tReduce your audio input level(s) by %0.1f dB to "
+                                    "avoid this.\n",
+                                    peakMagnitudeDB);
+                        } else {
+                            fprintf(stderr,
+                                    "*** Limiter.cpp: Amplitude levels must stay below "
+                                    "%0.1f dBFS to avoid compression.\n",
+                                    warningAmpDB);
+                            fprintf(
+                                stderr,
+                                "\tReduce input level(s) by %0.1f dB to achieve this.\n",
+                                peakMagnitudeDB - warningAmpDB);
+                        }
+                    } else {
+                        fprintf(stderr,
+                                "\tReduce audio input level(s) by %0.1f dB to avoid "
+                                "limiter compression distortion.\n",
+                                peakMagnitudeDB - warningAmpDB);
+                    }
+                    peakMagnitude = 0.0;  // reset for next group measurement
+                    if (nextWarning < maxWarningInterval) {  // don't let it stop
+                                                             // reporting for too long
+                        nextWarning *= 10;
+                    } else {
+                        warnCount = 0;
+                    }
+                }  // warnCount==nextWarning
+            }      // above warningAmp
+        }          // loop over frames
+    }              // checkAmplitudes()
+
+   private:
+    float fs;
+    int mNumChannels;
+    int mNumClients;
+    std::vector<void*> limiterP;
+    std::vector<void*> limiterUIP;
+#ifdef SINE_TEST
+    std::vector<void*> limiterTestP;
+    std::vector<void*> limiterTestUIP;
+#endif
+    double warningAmp;
+    uint32_t warnCount;
+    double peakMagnitude;
+    uint32_t nextWarning;
+};
+
+#endif
diff --git a/src/LoopBack.cpp b/src/LoopBack.cpp
new file mode 100644 (file)
index 0000000..f17d181
--- /dev/null
@@ -0,0 +1,57 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file LoopBack.cpp
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+#include "LoopBack.h"
+
+#include <cstring>  // for memcpy
+#include <iostream>
+
+#include "jacktrip_types.h"
+
+using std::cout;
+using std::endl;
+// using namespace JackTripNamespace;
+
+//*******************************************************************************
+void LoopBack::compute(int nframes, float** inputs, float** outputs)
+{
+    for (int i = 0; i < getNumInputs(); i++) {
+        // Everything that comes out, copy back to inputs
+        // memcpy(inputs[i], outputs[i], sizeof(sample_t) * nframes);
+        memcpy(outputs[i], inputs[i], sizeof(sample_t) * nframes);
+    }
+}
diff --git a/src/LoopBack.h b/src/LoopBack.h
new file mode 100644 (file)
index 0000000..763da69
--- /dev/null
@@ -0,0 +1,68 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file LoopBack.h
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+/** \brief Connect Inputs to Outputs
+ *
+ */
+#ifndef __LOOPBACK_H__
+#define __LOOPBACK_H__
+
+#include "ProcessPlugin.h"
+
+/** \brief This Class just copy audio from its inputs to its outputs.
+ *
+ * It can be use to do loopback without the need to externally connect channels
+ * in JACK. Note that if you <EM>do</EM> connect the channels in jack, you'll
+ * be effectively multiplying the signal by 2.
+ */
+class LoopBack : public ProcessPlugin
+{
+   public:
+    /// \brief The class constructor sets the number of channels to connect as loopback
+    LoopBack(int numchans) { mNumChannels = numchans; };
+    /// \brief The class destructor
+    virtual ~LoopBack(){};
+
+    virtual int getNumInputs() { return (mNumChannels); };
+    virtual int getNumOutputs() { return (mNumChannels); };
+    virtual void compute(int nframes, float** inputs, float** outputs);
+
+   private:
+    int mNumChannels;
+};
+
+#endif
diff --git a/src/Meter.cpp b/src/Meter.cpp
new file mode 100644 (file)
index 0000000..9130f15
--- /dev/null
@@ -0,0 +1,195 @@
+//*****************************************************************
+/*
+  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 Meter.cpp
+ * \author Dominick Hing
+ * \date August 2022
+ * \license MIT
+ */
+
+#include "Meter.h"
+
+#include <algorithm>
+#include <iostream>
+
+#include "jacktrip_types.h"
+#include "meterdsp.h"
+
+//*******************************************************************************
+Meter::Meter(int numchans, bool verboseFlag) : mNumChannels(numchans)
+{
+    setVerbose(verboseFlag);
+    for (int i = 0; i < mNumChannels; i++) {
+        meterP.push_back(new meterdsp);
+    }
+}
+
+//*******************************************************************************
+Meter::~Meter()
+{
+    mTimer.stop();
+    for (int i = 0; i < mNumChannels; i++) {
+        delete static_cast<meterdsp*>(meterP[i]);
+    }
+    meterP.clear();
+    if (mValues) {
+        delete mValues;
+    }
+    if (mOutValues) {
+        delete mOutValues;
+    }
+    if (mBuffer) {
+        delete mBuffer;
+    }
+}
+
+//*******************************************************************************
+void Meter::init(int samplingRate, int bufferSize)
+{
+    ProcessPlugin::init(samplingRate, bufferSize);
+
+    fs = float(fSamplingFreq);
+    for (int i = 0; i < mNumChannels; i++) {
+        static_cast<meterdsp*>(meterP[i])->init(fs);
+    }
+
+    /* Set meter values to the default floor */
+    setupValues();
+
+    /* Start timer */
+    int timeout_ms = 100;
+    connect(&mTimer, &QTimer::timeout, this, &Meter::onTick);
+    mTimer.setTimerType(Qt::PreciseTimer);
+    mTimer.setInterval(timeout_ms);
+    mTimer.setSingleShot(false);
+    mTimer.start();
+
+    inited = true;
+}
+
+//*******************************************************************************
+void Meter::compute(int nframes, float** inputs, float** outputs)
+{
+    if (!inited) {
+        std::cerr << "*** Meter " << this << ": init never called! Doing it now.\n";
+        init(0, 0);
+    }
+
+    // Will measure inputs by default unless mMeasureOutputBuffer = true,
+    // in which case the plugin will measure from the outputs. This is useful when
+    // measuring with a monitor, since AudioInterface.cpp expects monitoring plugins
+    // to behave differently than input and output chain plugins
+    float** measuringBuffer = inputs;
+    if (mIsMonitoringMeter) {
+        measuringBuffer = outputs;
+    }
+
+    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  */
+        static_cast<meterdsp*>(meterP[i])->compute(nframes, &measuringBuffer[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 maxSample = *(std::max_element)(mBuffer, mBuffer + nframes);
+
+        /* Update mValues */
+        mValues[i] = std::max<float>(mValues[i], maxSample);
+    }
+
+    /* Set processed audio flag */
+    hasProcessedAudio = true;
+}
+
+//*******************************************************************************
+void Meter::updateNumChannels(int nChansIn, int nChansOut)
+{
+    // this should only be called before init!
+    if (inited) {
+        return;
+    }
+
+    if (outgoingPluginToNetwork) {
+        mNumChannels = nChansIn;
+    } else {
+        mNumChannels = nChansOut;
+    }
+
+    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 */
+        emit onComputedVolumeMeasurements(mOutValues, mNumChannels);
+    }
+}
diff --git a/src/Meter.h b/src/Meter.h
new file mode 100644 (file)
index 0000000..7fac7ea
--- /dev/null
@@ -0,0 +1,99 @@
+//*****************************************************************
+/*
+  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 Meter.h
+ * \author Dominick Hing
+ * \date August 2022
+ * \license MIT
+ */
+
+#ifndef __METER_H__
+#define __METER_H__
+
+#include <QObject>
+#include <QTimer>
+#include <vector>
+
+#include "ProcessPlugin.h"
+
+/** \brief The Meter class measures the live audio loudness level
+ */
+class Meter : public ProcessPlugin
+{
+    Q_OBJECT;
+
+   public:
+    /// \brief The class constructor sets the number of channels to measure
+    Meter(int numchans, bool verboseFlag = false);
+
+    /// \brief The class destructor
+    virtual ~Meter();
+
+    void init(int samplingRate, int bufferSize) override;
+    int getNumInputs() override { return (mNumChannels); }
+    int getNumOutputs() override { return (mNumChannels); }
+    void compute(int nframes, float** inputs, float** outputs) override;
+    const char* getName() const override { return "VU Meter"; };
+
+    void updateNumChannels(int nChansIn, int nChansOut) override;
+
+    void setIsMonitoringMeter(bool isMonitoringMeter)
+    {
+        mIsMonitoringMeter = isMonitoringMeter;
+    };
+    bool getIsMonitoringMeter() { return mIsMonitoringMeter; };
+
+   private:
+    void setupValues();
+
+    float fs;
+    bool mIsMonitoringMeter = false;
+
+    int mNumChannels;
+    float threshold = -80.0;
+    std::vector<void*> meterP;
+    bool hasProcessedAudio = false;
+
+    QTimer mTimer;
+    float* mValues    = nullptr;
+    float* mOutValues = nullptr;
+    float* mBuffer    = nullptr;
+    int mBufSize      = 0;
+
+   private slots:
+    void onTick();
+
+   signals:
+    void onComputedVolumeMeasurements(float* values, int n);
+};
+
+#endif
diff --git a/src/Monitor.cpp b/src/Monitor.cpp
new file mode 100644 (file)
index 0000000..fb17e16
--- /dev/null
@@ -0,0 +1,144 @@
+//*****************************************************************
+/*
+  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 Monitor.cpp
+ * \author Dominick Hing
+ * \date May 2023
+ * \license MIT
+ */
+
+#include "Monitor.h"
+
+#include <iostream>
+
+#include "jacktrip_types.h"
+#include "monitordsp.h"
+
+//*******************************************************************************
+Monitor::Monitor(int numchans, bool verboseFlag) : mNumChannels(numchans)
+{
+    setVerbose(verboseFlag);
+    for (int i = 0; i < mNumChannels; i++) {
+        monitordsp* dsp_ptr = new monitordsp;
+        APIUI* ui_ptr       = new APIUI;
+        monitorP.push_back(dsp_ptr);
+        monitorUIP.push_back(ui_ptr);  // #included in monitordsp.h
+        dsp_ptr->buildUserInterface(ui_ptr);
+    }
+}
+
+//*******************************************************************************
+Monitor::~Monitor()
+{
+    for (int i = 0; i < mNumChannels; i++) {
+        delete static_cast<monitordsp*>(monitorP[i]);
+        delete static_cast<APIUI*>(monitorUIP[i]);
+    }
+    monitorP.clear();
+    monitorUIP.clear();
+}
+
+//*******************************************************************************
+void Monitor::init(int samplingRate, int bufferSize)
+{
+    ProcessPlugin::init(samplingRate, bufferSize);
+    fs = float(fSamplingFreq);
+
+    for (int i = 0; i < mNumChannels; i++) {
+        static_cast<monitordsp*>(monitorP[i])
+            ->init(fs);  // compression filter parameters depend on sampling rate
+        APIUI* ui_ptr = static_cast<APIUI*>(monitorUIP[i]);
+        int ndx       = ui_ptr->getParamIndex("Volume");
+        ui_ptr->setParamValue(ndx, mVolMultiplier);
+        ndx = ui_ptr->getParamIndex("Mute");
+        ui_ptr->setParamValue(ndx, 0);
+    }
+    inited = true;
+}
+
+//*******************************************************************************
+void Monitor::compute(int nframes, float** inputs, float** outputs)
+{
+    if (!inited) {
+        std::cerr << "*** Monitor " << this << ": init never called! Doing it now.\n";
+        init(0, 0);
+    }
+
+    if (mBufSize < nframes) {
+        if (mOutBufferInput) {
+            delete mOutBufferInput;
+        }
+
+        if (mInBufferInput) {
+            delete mInBufferInput;
+        }
+
+        mBufSize        = nframes;
+        mOutBufferInput = new float[mBufSize];
+        mInBufferInput  = new float[mBufSize];
+    }
+
+    std::vector<float*> buffer{mInBufferInput, mOutBufferInput};
+    for (int i = 0; i < mNumChannels; i++) {
+        // copy inputs and outputs into a separate memory buffer
+        memcpy(mInBufferInput, inputs[i], nframes * sizeof(float));
+        memcpy(mOutBufferInput, outputs[i], nframes * sizeof(float));
+
+        /* Run the signal through Faust  */
+        static_cast<monitordsp*>(monitorP[i])
+            ->compute(nframes, buffer.data(), &outputs[i]);
+    }
+}
+
+//*******************************************************************************
+void Monitor::updateNumChannels(int nChansIn, int nChansOut)
+{
+    if (outgoingPluginToNetwork) {
+        mNumChannels = nChansIn;
+    } else {
+        mNumChannels = nChansOut;
+    }
+}
+
+//*******************************************************************************
+void Monitor::volumeUpdated(float multiplier)
+{
+    // maps 0.0-1.0 to a -40 dB to 0 dB range
+    // update if volumedsp.dsp and/or volumedsp.h
+    // change their ranges
+    mVolMultiplier = 40.0 * multiplier - 40.0;
+    for (int i = 0; i < mNumChannels; i++) {
+        APIUI* ui_ptr = static_cast<APIUI*>(monitorUIP[i]);
+        int ndx       = ui_ptr->getParamIndex("Volume");
+        ui_ptr->setParamValue(ndx, mVolMultiplier);
+    }
+}
\ No newline at end of file
diff --git a/src/Monitor.h b/src/Monitor.h
new file mode 100644 (file)
index 0000000..3a41763
--- /dev/null
@@ -0,0 +1,84 @@
+//*****************************************************************
+/*
+  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 Monitor.h
+ * \author Dominick Hing
+ * \date May 2023
+ * \license MIT
+ */
+
+#ifndef __MONITOR_H__
+#define __MONITOR_H__
+
+#include <QObject>
+#include <vector>
+
+#include "ProcessPlugin.h"
+
+/** \brief The Monitor plugin adds a portion of the input signal multiplied by a
+ *  constant factor to the output signal
+ */
+class Monitor : public ProcessPlugin
+{
+    Q_OBJECT;
+
+   public:
+    /// \brief The class constructor sets the number of channels to use
+    Monitor(int numchans, bool verboseFlag = false);
+
+    /// \brief The class destructor
+    virtual ~Monitor();
+
+    void init(int samplingRate, int bufferSize) override;
+    int getNumInputs() override { return (mNumChannels); }
+    int getNumOutputs() override { return (mNumChannels); }
+    void compute(int nframes, float** inputs, float** outputs) override;
+    const char* getName() const override { return "Monitor"; };
+
+    void updateNumChannels(int nChansIn, int nChansOut) override;
+
+   public slots:
+    void volumeUpdated(float multiplier);
+
+   private:
+    std::vector<void*> monitorP;
+    std::vector<void*> monitorUIP;
+    float fs;
+    int mNumChannels;
+    float mVolMultiplier = 0.0;
+
+    float* mOutBufferInput = nullptr;
+    float* mInBufferInput  = nullptr;
+    int mBufSize           = 0;
+};
+
+#endif
\ No newline at end of file
diff --git a/src/PacketHeader.cpp b/src/PacketHeader.cpp
new file mode 100644 (file)
index 0000000..5792456
--- /dev/null
@@ -0,0 +1,356 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file PacketHeader.cpp
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+#include "PacketHeader.h"
+
+#ifndef _WIN32
+#include <sys/time.h>
+#endif
+
+#include <cstdlib>
+#include <iostream>
+#include <limits>
+#include <stdexcept>
+
+#include "JackTrip.h"
+
+using std::cout;
+using std::endl;
+
+// below is the gettimeofday definition for windows: this function is not defined in
+// sys/time.h as it is in unix for more info check:
+// http://www.halcode.com/archives/2008/08/26/retrieving-system-time-gettimeofday/
+#if defined _WIN32
+#ifdef __cplusplus
+// void GetSystemTimeAsFileTime(FILETIME*);
+inline int gettimeofday(struct timeval* p, [[maybe_unused]] void* tz)
+{
+    union {
+        long long ns100; /*time since 1 Jan 1601 in 100ns units */
+        FILETIME ft;
+    } now;
+    GetSystemTimeAsFileTime(&(now.ft));
+    p->tv_usec = (long)((now.ns100 / 10LL) % 1000000LL);
+    p->tv_sec  = (long)((now.ns100 - (116444736000000000LL)) / 10000000LL);
+    return 0;
+}
+#endif
+#endif
+
+// #######################################################################
+// ####################### PacketHeader ##################################
+// #######################################################################
+//***********************************************************************
+PacketHeader::PacketHeader(JackTrip* jacktrip)
+    : mBufferRequiresSameSettings(false), mJackTrip(jacktrip), mSeqNumber(0)
+{
+}
+
+//***********************************************************************
+uint64_t PacketHeader::usecTime()
+{
+    struct timeval tv;
+    gettimeofday(&tv, NULL);
+    return (
+        (tv.tv_sec * 1000000) +  // seconds
+        (tv.tv_usec));  // plus the microseconds. Type suseconds_t, range [-1, 1000000]
+}
+
+// #######################################################################
+// ####################### DefaultHeader #################################
+// #######################################################################
+//***********************************************************************
+DefaultHeader::DefaultHeader(JackTrip* jacktrip) : PacketHeader(jacktrip)
+{
+    mHeader.TimeStamp                  = 0;
+    mHeader.SeqNumber                  = 0;
+    mHeader.BufferSize                 = 0;
+    mHeader.SamplingRate               = 0;
+    mHeader.BitResolution              = 0;
+    mHeader.NumIncomingChannelsFromNet = 0;
+    mHeader.NumOutgoingChannelsToNet   = 0;
+}
+
+//***********************************************************************
+void DefaultHeader::fillHeaderCommonFromAudio()
+{
+    mHeader.TimeStamp                  = PacketHeader::usecTime();
+    mHeader.BufferSize                 = mJackTrip->getBufferSizeInSamples();
+    mHeader.SamplingRate               = mJackTrip->getSampleRateType();
+    mHeader.BitResolution              = mJackTrip->getAudioBitResolution();
+    mHeader.NumIncomingChannelsFromNet = mJackTrip->getNumOutputChannels();
+
+    if (mJackTrip->getNumInputChannels() == mJackTrip->getNumOutputChannels()) {
+        mHeader.NumOutgoingChannelsToNet = 0;
+    } else if (0 == mJackTrip->getNumInputChannels()) {
+        mHeader.NumOutgoingChannelsToNet = (std::numeric_limits<uint8_t>::max)();
+    } else {
+        mHeader.NumOutgoingChannelsToNet = mJackTrip->getNumInputChannels();
+    }
+}
+
+//***********************************************************************
+bool DefaultHeader::checkPeerSettings(int8_t* full_packet)
+{
+    bool error = false;
+    QString report;
+
+    DefaultHeaderStruct* peer_header;
+    peer_header = reinterpret_cast<DefaultHeaderStruct*>(full_packet);
+
+    // Check Buffer Size
+    if (peer_header->BufferSize != mHeader.BufferSize) {
+        if (mBufferRequiresSameSettings) {
+            std::cerr << "ERROR: Peer Buffer Size is  : " << peer_header->BufferSize
+                      << endl;
+            std::cerr << "       Local Buffer Size is : " << mHeader.BufferSize << endl;
+            std::cerr << "Make sure both machines use same buffer size" << endl;
+            std::cerr << gPrintSeparator << endl;
+            error = true;
+            report.append(
+                QStringLiteral("\n\nPeer Buffer Size is %1\nLocal Buffer Size is "
+                               "%2\nMake sure both machines use the same Buffer Size")
+                    .arg(peer_header->BufferSize)
+                    .arg(mHeader.BufferSize));
+        } else {
+            std::cerr << "WARNING: Peer Buffer Size is  : " << peer_header->BufferSize
+                      << endl;
+            std::cerr << "         Local Buffer Size is : " << mHeader.BufferSize << endl;
+        }
+    }
+
+    // Check Sampling Rate
+    if (peer_header->SamplingRate != mHeader.SamplingRate) {
+        int peerRate = AudioInterface::getSampleRateFromType(
+            static_cast<AudioInterface::samplingRateT>(peer_header->SamplingRate));
+        int localRate = AudioInterface::getSampleRateFromType(
+            static_cast<AudioInterface::samplingRateT>(mHeader.SamplingRate));
+        std::cerr << "ERROR: Peer Sampling Rate is   : " << peerRate << endl;
+        std::cerr << "       Local Sampling Rate is  : " << localRate << endl;
+        std::cerr << "Make sure both machines use the same Sampling Rate" << endl;
+        std::cerr << gPrintSeparator << endl;
+        error = true;
+        report.append(
+            QStringLiteral("\n\nPeer Sampling Rate is %1\nLocal Sampling Rate is "
+                           "%2\nMake sure both machines use the same Sampling Rate")
+                .arg(peerRate)
+                .arg(localRate));
+    }
+
+    // Check Audio Bit Resolution
+    if (peer_header->BitResolution != mHeader.BitResolution) {
+        std::cerr << "ERROR: Peer Audio Bit Resolution is  : "
+                  << static_cast<int>(peer_header->BitResolution) << endl;
+        std::cerr << "       Local Audio Bit Resolution is : "
+                  << static_cast<int>(mHeader.BitResolution) << endl;
+        std::cerr << "Make sure both machines use the same Bit Resolution" << endl;
+        std::cerr << gPrintSeparator << endl;
+        error = true;
+        report.append(
+            QStringLiteral(
+                "\n\nPeer Audio Bit Resolution is %1\nLocal Audio Bit Resolution is "
+                "%2\nMake sure both machines use the same Bit Resolution")
+                .arg(peer_header->BitResolution)
+                .arg(mHeader.BitResolution));
+    }
+
+    // Exit program if error
+    if (error) {
+        // std::cerr << "Exiting program..." << endl;
+        // std::exit(1);
+        // throw std::logic_error("Local and Peer Settings don't match");
+        emit signalError(
+            QStringLiteral("Local and Peer Settings don't match").append(report));
+    }
+
+    return !error;
+    /// \todo Check number of channels and other parameters
+}
+
+//***********************************************************************
+void DefaultHeader::printHeader() const
+{
+    cout << "Default Packet Header:" << endl;
+    cout << "Buffer Size               = " << static_cast<int>(mHeader.BufferSize)
+         << endl;
+    // Get the sample rate in Hz form the AudioInterface::samplingRateT
+    int sample_rate = AudioInterface::getSampleRateFromType(
+        static_cast<AudioInterface::samplingRateT>(mHeader.SamplingRate));
+    // clang-format off
+    cout << "Sampling Rate               = " << sample_rate << "\n"
+            "Audio Bit Resolutions       = " << static_cast<int>(mHeader.BitResolution) << "\n"
+            "Number of Incoming Channels = " << static_cast<int>(mHeader.NumIncomingChannelsFromNet) << "\n"
+            "Number of Outgoing Channels = " << static_cast<int>(mHeader.NumOutgoingChannelsToNet) << "\n"
+            "Sequence Number             = " << static_cast<int>(mHeader.SeqNumber) << "\n"
+            "Time Stamp                  = " << mHeader.TimeStamp << "\n"
+            << gPrintSeparator << "\n";
+    // clang-format on
+}
+
+//***********************************************************************
+uint64_t DefaultHeader::getPeerTimeStamp(int8_t* full_packet) const
+{
+    DefaultHeaderStruct* peer_header;
+    peer_header = reinterpret_cast<DefaultHeaderStruct*>(full_packet);
+    return peer_header->TimeStamp;
+}
+
+//***********************************************************************
+uint16_t DefaultHeader::getPeerSequenceNumber(int8_t* full_packet) const
+{
+    DefaultHeaderStruct* peer_header;
+    peer_header = reinterpret_cast<DefaultHeaderStruct*>(full_packet);
+    return peer_header->SeqNumber;
+}
+
+//***********************************************************************
+uint16_t DefaultHeader::getPeerBufferSize(int8_t* full_packet) const
+{
+    DefaultHeaderStruct* peer_header;
+    peer_header = reinterpret_cast<DefaultHeaderStruct*>(full_packet);
+    return peer_header->BufferSize;
+}
+
+//***********************************************************************
+uint8_t DefaultHeader::getPeerSamplingRate(int8_t* full_packet) const
+{
+    DefaultHeaderStruct* peer_header;
+    peer_header = reinterpret_cast<DefaultHeaderStruct*>(full_packet);
+    return peer_header->SamplingRate;
+}
+
+//***********************************************************************
+uint8_t DefaultHeader::getPeerBitResolution(int8_t* full_packet) const
+{
+    DefaultHeaderStruct* peer_header;
+    peer_header = reinterpret_cast<DefaultHeaderStruct*>(full_packet);
+    return peer_header->BitResolution;
+}
+
+//***********************************************************************
+uint8_t DefaultHeader::getPeerNumIncomingChannels(int8_t* full_packet) const
+{
+    DefaultHeaderStruct* peer_header;
+    peer_header = reinterpret_cast<DefaultHeaderStruct*>(full_packet);
+    return peer_header->NumIncomingChannelsFromNet;
+}
+
+uint8_t DefaultHeader::getPeerNumOutgoingChannels(int8_t* full_packet) const
+{
+    DefaultHeaderStruct* peer_header;
+    peer_header = reinterpret_cast<DefaultHeaderStruct*>(full_packet);
+    return peer_header->NumOutgoingChannelsToNet;
+}
+
+// #######################################################################
+// ####################### JamLinkHeader #################################
+// #######################################################################
+//***********************************************************************
+JamLinkHeader::JamLinkHeader(JackTrip* jacktrip) : PacketHeader(jacktrip)
+{
+    mHeader.Common    = 0;
+    mHeader.SeqNumber = 0;
+    mHeader.TimeStamp = 0;
+}
+
+//***********************************************************************
+void JamLinkHeader::fillHeaderCommonFromAudio()
+{
+    // Check number of channels
+    int num_inchannels = mJackTrip->getNumInputChannels();
+    if (num_inchannels != 1) {
+        // std::cerr << "ERROR: JamLink only support ONE channel. Run JackTrip using only
+        // one channel"
+        //           << endl;
+        // std::exit(1);
+        // std::cerr << "WARNING: JamLink only support ONE channel. Run JackTrip using
+        // only one channel" << endl; throw std::logic_error("JamLink only support ONE
+        // channel. Run JackTrip using only one channel");
+        emit signalError(QStringLiteral(
+            "JamLink only supports ONE channel. Run JackTrip using only one channel"));
+    }
+
+    // Sampling Rate
+    int rate_type = mJackTrip->getSampleRateType();
+    if (rate_type != AudioInterface::SR48) {
+        // std::cerr << "WARNING: JamLink only support 48kHz for communication with
+        // JackTrip at the moment." << endl; throw std::logic_error("ERROR: JamLink only
+        // support 48kHz for communication with JackTrip at the moment.");
+        emit signalError(
+            QStringLiteral("JamLink only supports 48kHz for communication with JackTrip "
+                           "at the moment."));
+    }
+
+    // Check Buffer Size
+    int buf_size = mJackTrip->getBufferSizeInSamples();
+    if (buf_size != 64) {
+        // std::cerr << "WARNING: JamLink only support 64 buffer size for communication
+        // with JackTrip at the moment." << endl; throw std::logic_error("ERROR: JamLink
+        // only support 64 buffer size for communication with JackTrip at the moment.");
+        emit signalError(QStringLiteral(
+            "JamLink only supports a buffer size of 64 for communication with JackTrip "
+            "at the moment."));
+    }
+
+    mHeader.Common = (ETX_MONO | ETX_16BIT | ETX_XTND) + 64;
+    switch (rate_type) {
+    case AudioInterface::SR48:
+        mHeader.Common = (mHeader.Common | ETX_48KHZ);
+        break;
+    case AudioInterface::SR44:
+        mHeader.Common = (mHeader.Common | ETX_44KHZ);
+        break;
+    case AudioInterface::SR32:
+        mHeader.Common = (mHeader.Common | ETX_32KHZ);
+        break;
+    case AudioInterface::SR22:
+        mHeader.Common = (mHeader.Common | ETX_22KHZ);
+        break;
+    default:
+        // std::cerr << "ERROR: Sample rate not supported by JamLink" << endl;
+        // std::exit(1);
+        // throw std::out_of_range("Sample rate not supported by JamLink");
+        emit signalError(QStringLiteral("Sample rate not supported by JamLink."));
+        break;
+    }
+}
+
+// #######################################################################
+// ####################### EmptyHeader #################################
+// #######################################################################
+//***********************************************************************
+EmptyHeader::EmptyHeader(JackTrip* jacktrip) : PacketHeader(jacktrip) {}
diff --git a/src/PacketHeader.h b/src/PacketHeader.h
new file mode 100644 (file)
index 0000000..84c0351
--- /dev/null
@@ -0,0 +1,328 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file PacketHeader.h
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+#ifndef __PACKETHEADER_H__
+#define __PACKETHEADER_H__
+
+#include <iostream>
+// #include <tr1/memory> // for shared_ptr
+#include <QObject>
+#include <QString>
+#include <cstring>
+
+#include "jacktrip_globals.h"
+#include "jacktrip_types.h"
+class JackTrip;  // Forward Declaration
+
+/// \brief Abstract Header Struct, Header Structs should subclass it
+struct HeaderStruct {
+};
+
+/// \brief Default Header Struct
+struct DefaultHeaderStruct : public HeaderStruct {
+   public:
+    // watch out for alignment...
+    uint64_t TimeStamp;     ///< Time Stamp
+    uint16_t SeqNumber;     ///< Sequence Number
+    uint16_t BufferSize;    ///< Buffer Size in Samples
+    uint8_t SamplingRate;   ///< Sampling Rate in JackAudioInterface::samplingRateT
+    uint8_t BitResolution;  ///< Audio Bit Resolution
+    uint8_t NumIncomingChannelsFromNet;  ///< Number of incoming Channels from the network
+    uint8_t NumOutgoingChannelsToNet;    ///< Number of outgoing Channels to the network
+};
+
+//---------------------------------------------------------
+// JamLink UDP Header:
+/************************************************************************/
+/* values for the UDP stream type                                       */
+/* streamType is a 16-bit value at the head of each UDP stream          */
+/* Its bit map is as follows:  (b15-msb)                                */
+/* B15:reserved, B14:extended header, B13 Stereo, B12 not 16-bit        */
+/* B11-B9: 0-48 Khz, 1-44 Khz, 2-32 Khz, 3-24 Khz,                      */
+/*         4-22 Khz, 5-16 Khz, 6-11 Khz, 7-8 Khz                        */
+/* B8-0: Samples in packet                                              */
+/************************************************************************/
+const unsigned short ETX_RSVD   = (0 << 15);
+const unsigned short ETX_XTND   = (1 << 14);
+const unsigned short ETX_STEREO = (1 << 13);
+const unsigned short ETX_MONO   = (0 << 13);
+const unsigned short ETX_16BIT  = (0 << 12);
+// inline unsigned short ETX_RATE_MASK(const unsigned short a) { a&(0x7<<9); }
+const unsigned short ETX_48KHZ = (0 << 9);
+const unsigned short ETX_44KHZ = (1 << 9);
+const unsigned short ETX_32KHZ = (2 << 9);
+const unsigned short ETX_24KHZ = (3 << 9);
+const unsigned short ETX_22KHZ = (4 << 9);
+const unsigned short ETX_16KHZ = (5 << 9);
+const unsigned short ETX_11KHZ = (6 << 9);
+const unsigned short ETX_8KHZ  = (7 << 9);
+// able to express up to 512 SPP
+// inline unsigned short  ETX_SPP(const unsigned short a) { (a&0x01FF); }
+
+/// \brief JamLink Header Struct
+struct JamLinkHeaderStuct : public HeaderStruct {
+    // watch out for alignment -- need to be on 4 byte chunks
+    uint16_t Common;     ///< Common part of the header, 16 bit
+    uint16_t SeqNumber;  ///< Sequence Number
+    uint32_t TimeStamp;  ///< Time Stamp
+};
+
+// #######################################################################
+// ####################### PacketHeader ##################################
+// #######################################################################
+/** \brief Base class for header type. Subclass this struct to
+ * create a new header.
+ */
+class PacketHeader : public QObject
+{
+    Q_OBJECT
+
+   public:
+    /// \brief The class Constructor
+    PacketHeader(JackTrip* jacktrip);
+    /// \brief The class Destructor
+    virtual ~PacketHeader() {}
+
+    /// \brief Return a time stamp in microseconds
+    /// \return Time stamp: microseconds since midnight (0 hour), January 1, 1970
+    static uint64_t usecTime();
+    /// \todo Implement this using a JackTrip Method (Mediator) member instead of the
+    /// reference to JackAudio
+    virtual void fillHeaderCommonFromAudio() = 0;
+    /// \brief Parse the packet header and take appropriate measures (like change
+    /// settings, or quit the program if peer settings don't match)
+    virtual void parseHeader() = 0;
+    /// \brief Check that the settings in the supplied packet header match the server's
+    /// settings \return True if settings match, false otherwise
+    virtual bool checkPeerSettings(int8_t* full_packet) = 0;
+
+    virtual uint64_t getPeerTimeStamp(int8_t* full_packet) const          = 0;
+    virtual uint16_t getPeerSequenceNumber(int8_t* full_packet) const     = 0;
+    virtual uint16_t getPeerBufferSize(int8_t* full_packet) const         = 0;
+    virtual uint8_t getPeerSamplingRate(int8_t* full_packet) const        = 0;
+    virtual uint8_t getPeerBitResolution(int8_t* full_packet) const       = 0;
+    virtual uint8_t getPeerNumIncomingChannels(int8_t* full_packet) const = 0;
+    virtual uint8_t getPeerNumOutgoingChannels(int8_t* full_packet) const = 0;
+
+    /// \brief Increase sequence number for counter, a 16bit number
+    virtual void increaseSequenceNumber() { mSeqNumber++; }
+    /// \brief Returns the current sequence number
+    /// \return 16bit Sequence number
+    virtual uint16_t getSequenceNumber() const { return mSeqNumber; }
+    /// \brief Get the header size in bytes
+    virtual int getHeaderSizeInBytes() const = 0;
+    virtual void putHeaderInPacketBaseClass(int8_t* full_packet,
+                                            const HeaderStruct& header_struct)
+    {
+        std::memcpy(full_packet, reinterpret_cast<const void*>(&header_struct),
+                    getHeaderSizeInBytes());
+    }
+    /// \brief Put the header in buffer pointed by full_packet
+    /// \param full_packet Pointer to full packet (audio+header). Size must be
+    /// sizeof(header part) + sizeof(audio part)
+    virtual void putHeaderInPacket(int8_t* full_packet) = 0;
+    void setBufferRequiresSameSettings(bool sameSettings)
+    {
+        mBufferRequiresSameSettings = sameSettings;
+    }
+
+   signals:
+    void signalError(const QString& error_message);
+
+   protected:
+    bool mBufferRequiresSameSettings;
+    JackTrip* mJackTrip;  ///< JackTrip mediator class
+
+   private:
+    uint16_t mSeqNumber;
+};
+
+// #######################################################################
+// ####################### DefaultHeader #################################
+// #######################################################################
+/** \brief Default Header
+ */
+class DefaultHeader : public PacketHeader
+{
+    Q_OBJECT
+
+   public:
+    DefaultHeader(JackTrip* jacktrip);
+    virtual ~DefaultHeader() {}
+
+    virtual void fillHeaderCommonFromAudio() override;
+    virtual void parseHeader() override {}
+    virtual bool checkPeerSettings(int8_t* full_packet) override;
+    virtual void increaseSequenceNumber() override { mHeader.SeqNumber++; }
+    virtual uint16_t getSequenceNumber() const override { return mHeader.SeqNumber; }
+    virtual int getHeaderSizeInBytes() const override { return sizeof(mHeader); }
+    virtual void putHeaderInPacket(int8_t* full_packet) override
+    {
+        putHeaderInPacketBaseClass(full_packet, mHeader);
+    }
+    void printHeader() const;
+    uint8_t getNumIncomingChannelsFromNet() const
+    {
+        return mHeader.NumIncomingChannelsFromNet;
+    }
+    uint8_t getNumOutgoingChannelsToNet() const
+    {
+        return mHeader.NumOutgoingChannelsToNet;
+    }
+    virtual uint64_t getPeerTimeStamp(int8_t* full_packet) const override;
+    virtual uint16_t getPeerSequenceNumber(int8_t* full_packet) const override;
+    virtual uint16_t getPeerBufferSize(int8_t* full_packet) const override;
+    virtual uint8_t getPeerSamplingRate(int8_t* full_packet) const override;
+    virtual uint8_t getPeerBitResolution(int8_t* full_packet) const override;
+    uint8_t getPeerNumIncomingChannels(int8_t* full_packet) const override;
+    uint8_t getPeerNumOutgoingChannels(int8_t* full_packet) const override;
+
+   private:
+    DefaultHeaderStruct mHeader;  ///< Default Header Struct
+};
+
+// #######################################################################
+// ####################### JamLinkHeader #################################
+// #######################################################################
+
+/** \brief JamLink Header
+ */
+class JamLinkHeader : public PacketHeader
+{
+    Q_OBJECT
+
+   public:
+    JamLinkHeader(JackTrip* jacktrip);
+    virtual ~JamLinkHeader() {}
+
+    virtual void fillHeaderCommonFromAudio() override;
+    virtual void parseHeader() override {}
+    virtual bool checkPeerSettings(int8_t* /*full_packet*/) override { return true; }
+
+    virtual uint64_t getPeerTimeStamp(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    virtual uint16_t getPeerSequenceNumber(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    virtual uint16_t getPeerBufferSize(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    virtual uint8_t getPeerSamplingRate(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    virtual uint8_t getPeerBitResolution(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    uint8_t getPeerNumIncomingChannels(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    uint8_t getPeerNumOutgoingChannels(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+
+    virtual void increaseSequenceNumber() override {}
+    virtual int getHeaderSizeInBytes() const override { return sizeof(mHeader); }
+    virtual void putHeaderInPacket(int8_t* full_packet) override
+    {
+        putHeaderInPacketBaseClass(full_packet, mHeader);
+    }
+
+   private:
+    JamLinkHeaderStuct mHeader;  ///< JamLink Header Struct
+};
+
+// #######################################################################
+// ####################### EmptyHeader #################################
+// #######################################################################
+
+/** \brief Empty Header to use with systems that don't include a header.
+ */
+class EmptyHeader : public PacketHeader
+{
+    Q_OBJECT
+
+   public:
+    EmptyHeader(JackTrip* jacktrip);
+    virtual ~EmptyHeader() {}
+
+    virtual void fillHeaderCommonFromAudio() override {}
+    virtual void parseHeader() override {}
+    virtual bool checkPeerSettings(int8_t* /*full_packet*/) override { return true; }
+    virtual void increaseSequenceNumber() override {}
+    virtual int getHeaderSizeInBytes() const override { return 0; }
+
+    virtual uint64_t getPeerTimeStamp(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    virtual uint16_t getPeerSequenceNumber(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    virtual uint16_t getPeerBufferSize(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    virtual uint8_t getPeerSamplingRate(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    virtual uint8_t getPeerBitResolution(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    uint8_t getPeerNumIncomingChannels(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+    uint8_t getPeerNumOutgoingChannels(int8_t* /*full_packet*/) const override
+    {
+        return 0;
+    }
+
+    virtual void putHeaderInPacket(int8_t* /*full_packet*/) override {}
+};
+
+#endif  //__PACKETHEADER_H__
diff --git a/src/Patcher.cpp b/src/Patcher.cpp
new file mode 100644 (file)
index 0000000..f4aa065
--- /dev/null
@@ -0,0 +1,200 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file Patcher.cpp
+ * \author Aaron Wyatt
+ * \date September 2020
+ */
+
+#include "Patcher.h"
+
+#include <QVector>
+
+void Patcher::setPatchMode(JackTrip::hubConnectionModeT patchMode)
+{
+    QMutexLocker locker(&m_connectionMutex);
+    m_fan = patchMode == JackTrip::CLIENTFOFI || patchMode == JackTrip::FULLMIX
+            || patchMode == JackTrip::SERVFOFI || patchMode == JackTrip::SERVFULLMIX;
+    m_loop = patchMode == JackTrip::CLIENTECHO || patchMode == JackTrip::FULLMIX
+             || patchMode == JackTrip::SERVFULLMIX;
+    m_includeServer = patchMode == JackTrip::SERVERTOCLIENT
+                      || patchMode == JackTrip::SERVFOFI
+                      || patchMode == JackTrip::SERVFULLMIX;
+}
+
+void Patcher::setStereoUpmix(bool upmix)
+{
+    m_steroUpmix = upmix;
+}
+
+void Patcher::registerClient(const QString& clientName)
+{
+    QMutexLocker locker(&m_connectionMutex);
+
+    // If our jack client isn't running, start it.
+    if (!m_jackClient) {
+        m_jackClient = jack_client_open("jthubpatcher", JackNoStartServer, &m_status);
+        if (!m_jackClient) {
+            std::cout << "Unable to start patcher JACK client: Patching disabled\n"
+                      << std::endl;
+            return;
+        } else {
+            jack_on_shutdown(m_jackClient, &Patcher::shutdownCallback, this);
+        }
+    }
+
+    const char **outPorts, **inPorts;
+    outPorts = jack_get_ports(m_jackClient, NULL, NULL, JackPortIsOutput);
+    inPorts  = jack_get_ports(m_jackClient, NULL, NULL, JackPortIsInput);
+
+    // Find the ports belonging to our client.
+    QVector<const char*> clientOutPorts;
+    QVector<const char*> clientInPorts;
+
+    for (int i = 0; outPorts[i]; i++) {
+        // Exclude broadcast ports.
+        if (QString(outPorts[i]).section(QStringLiteral(":"), 0, 0) == clientName
+            && !QString(outPorts[i]).contains(QLatin1String("broadcast"))) {
+            clientOutPorts.append(outPorts[i]);
+        }
+    }
+
+    for (int i = 0; inPorts[i]; i++) {
+        if (QString(inPorts[i]).section(QStringLiteral(":"), 0, 0) == clientName) {
+            clientInPorts.append(inPorts[i]);
+        }
+    }
+
+    bool clientIsMono = (clientOutPorts.count() == 1);
+
+    if (m_includeServer && clientIsMono && m_steroUpmix) {
+        // Most connections in server to client modes are created already by the code in
+        // JackAudioInterface. We only need to handle any upmixing of mono clients here.
+        const char** systemInPorts = jack_get_ports(m_jackClient, NULL, NULL,
+                                                    JackPortIsPhysical | JackPortIsInput);
+        if (systemInPorts[0] && systemInPorts[1]) {
+            jack_connect(m_jackClient, clientOutPorts.at(0), systemInPorts[1]);
+        }
+        jack_free(systemInPorts);
+    }
+
+    if (m_fan || m_loop) {
+        // Start with our receiving ports.
+        for (int i = 0; i < clientOutPorts.count(); i++) {
+            QString channel =
+                QString(clientOutPorts.at(i)).section(QStringLiteral("_"), -1, -1);
+            for (int j = 0; inPorts[j]; j++) {
+                QString otherClient =
+                    QString(inPorts[j]).section(QStringLiteral(":"), 0, 0);
+                QString otherChannel =
+                    QString(inPorts[j]).section(QStringLiteral("_"), -1, -1);
+
+                // First check if this is one of our other clients. (Fan out/in and full
+                // mix.)
+                if (m_fan) {
+                    if (m_clients.contains(otherClient) && otherChannel == channel) {
+                        jack_connect(m_jackClient, clientOutPorts.at(i), inPorts[j]);
+                    } else if (m_steroUpmix && clientIsMono) {
+                        // Deal with the special case of stereo upmix
+                        if (m_clients.contains(otherClient)
+                            && otherChannel == QLatin1String("2")) {
+                            jack_connect(m_jackClient, clientOutPorts.at(i), inPorts[j]);
+                        }
+                    }
+                }
+
+                // Then check if it's our registering client. (Client Echo and full mix.)
+                if (m_loop) {
+                    if (otherClient == clientName && otherChannel == channel) {
+                        jack_connect(m_jackClient, clientOutPorts.at(i), inPorts[j]);
+                    } else if (m_steroUpmix && clientIsMono) {
+                        if (otherClient == clientName
+                            && otherChannel == QLatin1String("2")) {
+                            jack_connect(m_jackClient, clientOutPorts.at(i), inPorts[j]);
+                        }
+                    }
+                }
+            }
+        }
+
+        // Then our sending ports. We only need to check for other clients here.
+        // (Any loopback connections will have been made in the previous loop.)
+        if (m_fan) {
+            for (int i = 0; i < clientInPorts.count(); i++) {
+                QString channel =
+                    QString(clientInPorts.at(i)).section(QStringLiteral("_"), -1, -1);
+                for (int j = 0; outPorts[j]; j++) {
+                    QString otherClient =
+                        QString(outPorts[j]).section(QStringLiteral(":"), 0, 0);
+                    QString otherChannel =
+                        QString(outPorts[j]).section(QStringLiteral("_"), -1, -1);
+                    if (m_clients.contains(otherClient)
+                        && !QString(outPorts[j]).contains(QLatin1String("broadcast"))) {
+                        if (otherChannel == channel
+                            || (m_steroUpmix && channel == QLatin1String("2")
+                                && m_monoClients.contains(otherClient))) {
+                            jack_connect(m_jackClient, outPorts[j], clientInPorts.at(i));
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    m_clients.append(clientName);
+    if (clientIsMono) {
+        m_monoClients.append(clientName);
+    }
+    jack_free(outPorts);
+    jack_free(inPorts);
+}
+
+void Patcher::unregisterClient(const QString& clientName)
+{
+    QMutexLocker locker(&m_connectionMutex);
+    m_clients.removeAll(clientName);
+    m_monoClients.removeAll(clientName);
+}
+
+void Patcher::shutdownCallback(void* arg)
+{
+    Patcher* patcher = static_cast<Patcher*>(arg);
+    jack_client_close(patcher->m_jackClient);
+    patcher->m_jackClient = nullptr;
+}
+
+Patcher::~Patcher()
+{
+    if (m_jackClient) {
+        jack_client_close(m_jackClient);
+    }
+}
diff --git a/src/Patcher.h b/src/Patcher.h
new file mode 100644 (file)
index 0000000..0f2f5b0
--- /dev/null
@@ -0,0 +1,82 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file Patcher.h
+ * \author Aaron Wyatt
+ * \date September 2020
+ */
+
+#ifndef __PATCHER_H__
+#define __PATCHER_H__
+
+#include "JackTrip.h"
+#ifdef USE_WEAK_JACK
+#include "weak_libjack.h"
+#else
+#include <jack/jack.h>
+#endif
+#include <QMutex>
+#include <QStringList>
+
+class Patcher : public QObject
+{
+    Q_OBJECT
+
+   public:
+    Patcher(QObject* parent = nullptr) : QObject(parent) {}
+    virtual ~Patcher();
+
+    void setPatchMode(JackTrip::hubConnectionModeT patchMode);
+    void setStereoUpmix(bool upmix);
+
+    void registerClient(const QString& clientName);
+    void unregisterClient(const QString& clientName);
+
+    // Jack shutdown callback
+    static void shutdownCallback(void* arg);
+
+   private:
+    QStringList m_clients;
+    QStringList m_monoClients;
+
+    bool m_fan           = false;
+    bool m_loop          = false;
+    bool m_includeServer = true;
+    bool m_steroUpmix    = false;
+
+    jack_client_t* m_jackClient = nullptr;
+    jack_status_t m_status;
+
+    QMutex m_connectionMutex;
+};
+
+#endif  // __PATCHER_H__
diff --git a/src/ProcessPlugin.cpp b/src/ProcessPlugin.cpp
new file mode 100644 (file)
index 0000000..4dcc4bd
--- /dev/null
@@ -0,0 +1,31 @@
+// #include "ProcessPlugin.h"
+
+/*
+//----------------------------------------------------------------------------
+// Jack Callbacks
+//----------------------------------------------------------------------------
+
+int srate(jack_nframes_t nframes, void *arg)
+{
+  printf("the sample rate is now %u/sec\n", nframes);
+  return 0;
+}
+
+void jack_shutdown(void *arg)
+{
+  std::cout << "" << std::endl;
+  std::exit(1);
+}
+
+int process (jack_nframes_t nframes, void *arg)
+{
+  for (int i = 0; i < gNumInChans; i++) {
+    gInChannel[i] = (float *)jack_port_get_buffer(input_ports[i], nframes);
+  }
+  for (int i = 0; i < gNumOutChans; i++) {
+    gOutChannel[i] = (float *)jack_port_get_buffer(output_ports[i], nframes);
+  }
+  DSP.compute(nframes, gInChannel, gOutChannel);
+  return 0;
+}
+*/
diff --git a/src/ProcessPlugin.h b/src/ProcessPlugin.h
new file mode 100644 (file)
index 0000000..e4af0ed
--- /dev/null
@@ -0,0 +1,120 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file ProcessPlugin.h
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+#ifndef __PROCESSPLUGIN_H__
+#define __PROCESSPLUGIN_H__
+
+#include <QObject>
+#include <QThread>
+
+/** \brief Interface for the process plugins to add to the JACK callback process in
+ * JackAudioInterface
+ *
+ * This class contains the same methods of the FAUST dsp class. A mydsp class can inherit
+ * from this class the same way it inherits from dsp. Subclass should implement all
+ * methods except init, which is optional for processing that are sampling rate dependent
+ * or that need specific initialization.
+ */
+class ProcessPlugin : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    /// \brief The Class Constructor
+    ProcessPlugin(){};
+    /// \brief The Class Destructor
+    virtual ~ProcessPlugin(){};
+
+    /// \brief Return Number of Input Channels
+    virtual int getNumInputs() = 0;
+    /// \brief Return Number of Output Channels
+    virtual int getNumOutputs() = 0;
+
+    // virtual void buildUserInterface(UI* interface) = 0;
+
+    virtual const char* getName() const = 0;  // get name of DERIVED class
+
+    /** \brief Do proper Initialization of members and class instances. By default this
+     * initializes the Sampling Frequency. If a class instance depends on the
+     * sampling frequency, it should be initialize here.
+     */
+    virtual void init(int samplingRate, int bufferSize)
+    {
+        if (samplingRate <= 0) {
+            samplingRate = 48000;
+            printf("%s: *** HAD TO GUESS the sampling rate (chose 48000 Hz) ***\n",
+                   getName());
+        }
+        if (bufferSize <= 0) {
+            bufferSize = 128;
+            printf("%s: *** HAD TO GUESS the buffer size (chose 128) ***\n", getName());
+        }
+        fSamplingFreq = samplingRate;
+        mBufferSize   = bufferSize;
+        if (verbose) {
+            printf("%s: init(%d, %d)\n", getName(), samplingRate, bufferSize);
+        }
+    }
+    virtual bool getInited() { return inited; }
+    virtual void setVerbose(bool v) { verbose = v; }
+
+    virtual void setOutgoingToNetwork(bool toNetwork)
+    {
+        outgoingPluginToNetwork = toNetwork;
+    }
+
+    /// \brief Compute process
+    virtual void compute(int nframes, float** inputs, float** outputs) = 0;
+
+    /**
+     * @brief This function may optionally be used by plugins. This is useful
+     * if the number of audio channels in the parent audio interface has changed
+     * after the plugin instance was instantiated, to tell the plugin to modify its
+     * functionality.
+     */
+    virtual void updateNumChannels(int /*nChansIn*/, int /*nChansOut*/) { return; };
+
+   protected:
+    int fSamplingFreq;  //< Faust Data member, Sampling Rate
+    int mBufferSize;    //< expected number of samples per compute callbacks
+    bool inited                  = false;
+    bool verbose                 = false;
+    bool outgoingPluginToNetwork = false;  //< Tells the plugin if it processes audio
+                                           // going into or out of the network
+};
+
+#endif
diff --git a/src/QtStaticPlugins.cpp b/src/QtStaticPlugins.cpp
new file mode 100644 (file)
index 0000000..8e9e079
--- /dev/null
@@ -0,0 +1,4 @@
+#include <QtPlugin>
+
+// used to load tls backend for linux static builds
+Q_IMPORT_PLUGIN(QTlsBackendOpenSSL)
diff --git a/src/Regulator.cpp b/src/Regulator.cpp
new file mode 100644 (file)
index 0000000..a92a724
--- /dev/null
@@ -0,0 +1,1092 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024 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 Regulator.cpp
+ * \author Chris Chafe
+ * \date May 2021 - May 2024
+ */
+
+// EXPERIMENTAL for testing in JackTrip v1.5.<n>
+// server and client can have different FPP (tested from FPP 16 to 1024)
+// stress tested by repeatedly starting & stopping across range of FPP's
+// server and client can have different in / out channel count
+// large FPP, for example 512, should not be run with --udprt as PLC audio callback
+// completion gets delayed auto mode -- use -q auto3 or for manual setting of initial
+// mMsecTolerance -- use -q auto<msec> gathers data for 6 sec and then goes full auto
+
+// example WAN test
+// ./jacktrip -S --udprt -p1 --bufstrategy 3 -q auto
+// PIPEWIRE_LATENCY=32/48000 ./jacktrip -C <SERV> --udprt --bufstrategy 3 -q auto
+
+// (mono : mono : mono)
+// ./jacktrip -S -p1 --bufstrategy 3 -q auto3 -u --receivechannels 1 --sendchannels 1
+// --udprt  -I 1
+// ./jacktrip -C <SERV> --receivechannels 1 -u --sendchannels 1 --bufstrategy 3 -q auto3
+// -I 1 --udprt
+
+// latest (mono <: stereo : stereo)
+// ./jacktrip -S -p1 --bufstrategy 3 -q auto3 -u --receivechannels 1 --sendchannels 2
+// --udprt  -I 1
+// ./jacktrip -C <SERV> --receivechannels 2 -u --sendchannels 1 --bufstrategy 3 -q auto3
+// -I 1 --udprt
+
+// example WAN test
+// at 48000 / 32 = 2.667 ms total roundtrip latency
+// local loopback test with 4 terminals running and a jmess file
+// jacktrip -S --udprt --nojackportsconnect -q1 --bufstrategy 3
+// jacktrip -C localhost --udprt --nojackportsconnect -q1  --bufstrategy 3
+
+// tested outgoing loss impairments with (replace lo with relevant network interface)
+// sudo tc qdisc add dev lo root netem loss 5%
+// sudo tc qdisc del dev lo root netem loss 5%
+// or very revealing
+// sudo tc qdisc add dev lo root netem loss 20%
+// sudo tc qdisc del dev lo root netem loss 20%
+// tested jitter impairments with
+// wifi simulation
+// sudo tc qdisc add dev lo root netem slot distribution pareto 0.1ms 3.0ms
+// sudo tc qdisc del dev lo root netem slot distribution pareto 0.1ms 3.0ms
+// ugly wired simulation
+// sudo tc qdisc add dev lo root netem slot distribution pareto 0.2ms 0.3ms
+// sudo tc qdisc del dev lo root netem slot distribution pareto 0.2ms 0.3ms
+
+#include "Regulator.h"
+
+#include <cfloat>
+#include <iomanip>
+
+#include "JitterBuffer.h"  // for broadcast
+
+using std::cout;
+using std::endl;
+using std::setw;
+
+// constants...
+constexpr bool PrintDirect = false;  // print stats direct from -V not -I
+constexpr int HIST         = 2;      // mPacketsInThePast setting for
+constexpr int HISTFPP      = 128;    // default FPP when calibrating burg window
+
+constexpr int NumSlots   = 4096;   // NumSlots looped for recent arrivals
+constexpr double AutoMax = 250.0;  // msec bounds on insane IPI, like ethernet unplugged
+constexpr double AutoInitDur = 3000.0;  // kick in auto after this many msec
+constexpr double AutoInitValFactor =
+    0.5;  // scale for initial mMsecTolerance during init phase if unspecified
+
+// tweak
+constexpr int WindowDivisor = 8;  // for faster auto tracking
+constexpr double AutoHeadroomGlitchTolerance =
+    0.02;  // Acceptable rate of skips before auto headroom is increased (2.0%)
+constexpr double AutoHistoryWindow =
+    60;  // rolling window of time (in seconds) over which auto tolerance roughly adjusts
+constexpr double AutoSmoothingFactor =
+    1.0
+    / (WindowDivisor * AutoHistoryWindow);  // EWMA smoothing factor for auto tolerance
+
+BurgAlgorithm::BurgAlgorithm(int size)  // mUpToNow = mPacketsInThePast * fpp
+{
+    // GET SIZE FROM INPUT VECTORS
+    m = N      = size - 1;
+    this->size = size;
+    if (size < m)
+        cout << "time_series should have more elements than the AR order is" << endl;
+    Ak.resize(size);
+    for (int i = 0; i < size; i++)
+        Ak[i] = 0.0;
+    AkReset.resize(size);
+    AkReset    = Ak;
+    AkReset[0] = 1.0;
+
+    f.resize(size);
+    b.resize(size);
+}
+
+void BurgAlgorithm::train(std::vector<float>& coeffs, const std::vector<float>& x,
+                          int size)
+{
+    // INITIALIZE Ak
+    Ak = AkReset;
+
+    // INITIALIZE f and b
+    for (int i = 0; i < size; i++)
+        f[i] = b[i] = x[i];
+
+    // INITIALIZE Dk
+    float Dk = 0.0;
+
+    for (int j = 0; j <= N; j++)
+        Dk += 2.00002 * f[j] * f[j];  // needs more damping than orig 2.0
+
+    Dk -= f[0] * f[0] + b[N] * b[N];
+
+    // BURG RECURSION
+    for (int k = 0; k < m; k++) {
+        // COMPUTE MU
+        float mu = 0.0;
+        for (int n = 0; n <= N - k - 1; n++) {
+            mu += f[n + k + 1] * b[n];
+        }
+
+        if (Dk == 0.0)
+            Dk = FLT_EPSILON;  // 0.0000001 from online testing when it was a double
+        mu *= -2.0 / Dk;
+
+        // UPDATE Ak
+        for (int n = 0; n <= (k + 1) / 2; n++) {
+            float t1      = Ak[n] + mu * Ak[k + 1 - n];
+            float t2      = Ak[k + 1 - n] + mu * Ak[n];
+            Ak[n]         = t1;
+            Ak[k + 1 - n] = t2;
+        }
+
+        // UPDATE f and b
+        for (int n = 0; n <= N - k - 1; n++) {
+            float t1     = f[n + k + 1] + mu * b[n];  // were double
+            float t2     = b[n] + mu * f[n + k + 1];
+            f[n + k + 1] = t1;
+            b[n]         = t2;
+        }
+
+        // UPDATE Dk
+        Dk = (1.0 - mu * mu) * Dk - f[k + 1] * f[k + 1] - b[N - k - 1] * b[N - k - 1];
+    }
+    // ASSIGN COEFFICIENTS
+    coeffs.assign(++Ak.begin(), Ak.end());
+}
+
+void BurgAlgorithm::predict(std::vector<float>& coeffs, std::vector<float>& tail,
+                            int size)
+{
+    for (int i = m; i < size; i++) {
+        tail[i] = 0.0;
+        for (int j = 0; j < m; j++) {
+            tail[i] -= coeffs[j] * tail[i - 1 - j];
+        }
+    }
+}
+
+//*******************************************************************************
+Channel::Channel(int fpp, int upToNow, int packetsInThePast)  // operates at peer FPP
+{
+    predictedNowPacket.resize(fpp);
+    realNowPacket.resize(fpp);
+    outputNowPacket.resize(fpp);
+    futurePredictedPacket.resize(fpp);
+    mTmpFloatBuf.resize(fpp);
+    mZeros.resize(fpp);
+    for (int i = 0; i < fpp; i++)
+        predictedNowPacket[i] = realNowPacket[i] = outputNowPacket[i] =
+            futurePredictedPacket[i] = mTmpFloatBuf[i] = mZeros[i] = 0.0;
+
+    realPast.resize(upToNow);
+    for (int i = 0; i < upToNow; i++)
+        realPast[i] = 0.0;
+    zeroPast.resize(upToNow);
+    for (int i = 0; i < upToNow; i++)
+        zeroPast[i] = 1.0;
+
+    for (int i = 0; i < packetsInThePast; i++) {  // don't resize, using push_back
+        std::vector<float> tmp(fpp);
+        for (int j = 0; j < fpp; j++)
+            tmp[j] = 0.0;
+        predictedPast.push_back(tmp);
+    }
+
+    mCoeffsSize = upToNow - 1;
+    coeffs.resize(mCoeffsSize);
+    for (int i = 0; i < mCoeffsSize; i++) {
+        coeffs[i] = 0.0;
+    }
+
+    mTailSize = upToNow + fpp * 2;
+    prediction.resize(mTailSize);
+    for (int i = 0; i < mTailSize; i++) {
+        prediction[i] = 0.0;
+    }
+
+    // set up ring buffer
+    mRing = packetsInThePast;
+    mWptr = mRing / 2;
+    for (int i = 0; i < mRing; i++) {  // don't resize, using push_back
+        std::vector<float> tmp(fpp);
+        for (int j = 0; j < fpp; j++)
+            tmp[j] = 0.0;
+        mPacketRing.push_back(tmp);
+    }
+    lastWasGlitch = false;
+}
+
+// push received packet to ring
+void Channel::ringBufferPush()
+{
+    mPacketRing[mWptr % mRing] = mTmpFloatBuf;
+    mWptr++;
+    mWptr %= mRing;
+}
+
+// pull numbered packet from ring
+void Channel::ringBufferPull(int past)
+{
+    int pastPtr = mWptr - past;
+    if (pastPtr < 0)
+        pastPtr += mRing;
+    mTmpFloatBuf = mPacketRing[pastPtr];
+}
+
+//*******************************************************************************
+Regulator::Regulator(int rcvChannels, int bit_res, int localFPP, int qLen, int bqLen,
+                     int sample_rate)
+    : RingBuffer(0, 0)
+    , mInitialized(false)
+    , mNumChannels(rcvChannels)
+    , mAudioBitRes(bit_res)
+    , mLocalFPP(localFPP)
+    , mSampleRate(sample_rate)
+    , mMsecTolerance((double)qLen)  // handle non-auto mode, expects positive qLen
+    , m_b_BroadcastQueueLength(bqLen)
+{
+    switch (mAudioBitRes) {  // int from JitterBuffer to AudioInterface enum
+    case 1:
+        mBitResolutionMode = AudioInterface::audioBitResolutionT::BIT8;
+        break;
+    case 2:
+        mBitResolutionMode = AudioInterface::audioBitResolutionT::BIT16;
+        break;
+    case 3:
+        mBitResolutionMode = AudioInterface::audioBitResolutionT::BIT24;
+        break;
+    case 4:
+        mBitResolutionMode = AudioInterface::audioBitResolutionT::BIT32;
+        break;
+    }
+    mScale    = pow(2.0, (((mBitResolutionMode * 8) - 1.0))) - 1.0;
+    mInvScale = 1.0 / mScale;
+
+    if (gVerboseFlag)
+        cout << "mBitResolutionMode = " << mBitResolutionMode << " scale = " << mScale
+             << endl;
+
+    mIncomingTiming.resize(NumSlots);
+    for (int i = 0; i < NumSlots; i++) {
+        mIncomingTiming[i] = 0.0;
+    }
+
+    mLocalFPPdurMsec = 1000.0 * mLocalFPP / mSampleRate;
+    mLocalBytes      = mLocalFPP * mNumChannels * mBitResolutionMode;
+
+    //    mPhasor.resize(mNumChannels, 0.0);
+}
+
+Regulator::~Regulator()
+{
+    delete[] mXfrBuffer;
+    delete[] mBroadcastBuffer;
+    delete[] mSlotBuf;
+    delete pushStat;
+    delete pullStat;
+    for (int i = 0; i < mNumChannels; i++)
+        delete mChanData[i];
+    if (m_b_BroadcastRingBuffer)
+        delete m_b_BroadcastRingBuffer;
+    delete mTime;
+    delete ba;
+    if (mWorkerThreadPtr != nullptr) {
+        mWorkerThreadPtr->quit();
+        mWorkerThreadPtr->wait();
+        delete mWorkerThreadPtr;
+    }
+    if (mRegulatorWorkerPtr)
+        delete mRegulatorWorkerPtr;
+    if (mWorkerBuffer)
+        delete[] mWorkerBuffer;
+}
+
+//*******************************************************************************
+void Regulator::setFPPratio(int len)
+{
+    // only for first peer packet
+    if (mInitialized) {
+        return;
+    }
+
+    mPeerBytes           = len;
+    mPeerFPP             = len / (mNumChannels * mBitResolutionMode);
+    mPeerFPPdurMsec      = 1000.0 * mPeerFPP / mSampleRate;
+    mFPPratioNumerator   = 1;
+    mFPPratioDenominator = 1;
+
+    //////////////////////////////////////
+
+    mPacketsInThePast = HIST;  // HIST is the setting for HISTFPP
+
+    if (mPeerFPP < HISTFPP)
+        mPacketsInThePast *= (HISTFPP / mPeerFPP);  // but don't go below 2
+    else if (mPeerFPP > (HISTFPP * 2))
+        mPacketsInThePast = 1;  // 1 is enough history @ 512 or greater
+
+    if (gVerboseFlag)
+        cout << "mPacketsInThePast = " << mPacketsInThePast << " at " << mPeerFPP << " / "
+             << mLocalFPP << endl;
+
+    mPcnt = 0;
+    mTime = new Time();
+    mTime->start();
+    mUpToNow   = mPacketsInThePast * mPeerFPP;        // duration
+    mBeyondNow = (mPacketsInThePast + 1) * mPeerFPP;  // duration
+
+    ba              = new BurgAlgorithm(mUpToNow);
+    mPeerFPPdurMsec = 1000.0 * mPeerFPP / mSampleRate;
+    mPeerBytes      = mPeerFPP * mNumChannels * mBitResolutionMode;
+
+    //////////////////////////////////////
+
+    if (mPeerFPP != mLocalFPP) {
+        if (mPeerFPP > mLocalFPP)
+            mFPPratioDenominator = mPeerFPP / mLocalFPP;
+        else
+            mFPPratioNumerator = mLocalFPP / mPeerFPP;
+    }
+
+    // bufstrategy 1 autoq mode overloads qLen with negative val
+    // creates this ugly code
+    if (mMsecTolerance <= 0) {  // handle -q auto or, for example, -q auto10
+        mAuto = true;
+        // default is -500 from bufstrategy 1 autoq mode
+        // use mMsecTolerance to set headroom
+        if (mMsecTolerance == -500.0) {
+            mAutoHeadroom = -1;
+            cout << "PLC is in auto mode and has been set with variable headroom" << endl;
+        } else {
+            mAutoHeadroom = std::abs(mMsecTolerance);
+            cout << "PLC is in auto mode and has been set with " << mAutoHeadroom
+                 << "ms headroom" << endl;
+            if (mAutoHeadroom > 50.0)
+                cout << "That's a very large value and should be less than, "
+                        "for example, 50ms"
+                     << endl;
+        }
+        // found an interesting relationship between mPeerFPP and initial
+        // mMsecTolerance mPeerFPP*0.5 is pretty good though that's an oddball
+        // conversion of bufsize directly to msec
+        mMsecTolerance = (mPeerFPP * AutoInitValFactor);
+    } else {
+        cout << "PLC is using a fixed tolerance of " << mMsecTolerance << "ms" << endl;
+    }
+
+    mXfrBuffer = new int8_t[mPeerBytes];
+    memset(mXfrBuffer, 0, mPeerBytes);
+    mBroadcastBuffer = new int8_t[mPeerBytes];
+    memset(mBroadcastBuffer, 0, mPeerBytes);
+
+    mFadeUp.resize(mPeerFPP, 0.0);
+    mFadeDown.resize(mPeerFPP, 0.0);
+    for (int i = 0; i < mPeerFPP; i++) {
+        mFadeUp[i]   = (double)i / (double)mPeerFPP;
+        mFadeDown[i] = 1.0 - mFadeUp[i];
+    }
+
+    mSlots      = new int8_t*[NumSlots];
+    mSlotBuf    = new int8_t[NumSlots * mPeerBytes];
+    int8_t* tmp = mSlotBuf;
+    for (int i = 0; i < NumSlots; i++) {
+        mSlots[i] = tmp;
+        tmp += mPeerBytes;
+    }
+
+    for (int i = 0; i < mNumChannels; i++) {
+        Channel* tmp = new Channel(mPeerFPP, mUpToNow, mPacketsInThePast);
+        mChanData.push_back(tmp);
+    }
+
+    mIncomingTimer.start();
+    mLastSeqNumIn.store(-1, std::memory_order_relaxed);
+    if (m_b_BroadcastQueueLength) {
+        m_b_BroadcastRingBuffer =
+            new JitterBuffer(mPeerFPP, 10, mSampleRate, 1, m_b_BroadcastQueueLength,
+                             mNumChannels, mAudioBitRes);
+        cout << "Broadcast started in Regulator with packet queue of "
+             << m_b_BroadcastQueueLength << endl;
+        // have not implemented the mJackTrip->queueLengthChanged functionality
+    }
+
+    // number of stats tick calls per sec depends on FPP
+    pushStat =
+        new StdDev(1, &mIncomingTimer, (int)(floor(mSampleRate / (double)mPeerFPP)));
+    pullStat =
+        new StdDev(2, &mIncomingTimer, (int)(floor(mSampleRate / (double)mLocalFPP)));
+
+    mInitialized = true;
+}
+
+//*******************************************************************************
+bool Regulator::enableWorker()
+{
+    // enable worker thread & queue if a prediction takes longer than
+    // our local audio callback interval (too slow to keep up)
+    const double maxPLCdspAllowed = mLocalFPPdurMsec * 0.7;  // 70%
+    if (mStatsMaxPLCdspElapsed >= maxPLCdspAllowed && !isWorkerEnabled()) {
+        cout << "PLC dsp " << mStatsMaxPLCdspElapsed
+             << " is too slow (max=" << maxPLCdspAllowed << "), enabling worker" << endl;
+        mWorkerBuffer = new int8_t[mPeerBytes];
+        memset(mWorkerBuffer, 0, mPeerBytes);
+        mWorkerThreadPtr = new QThread();
+        mWorkerThreadPtr->setObjectName("RegulatorThread");
+        mWorkerThreadPtr->start();
+        mRegulatorWorkerPtr = new RegulatorWorker(this);
+        mRegulatorWorkerPtr->moveToThread(mWorkerThreadPtr);
+        mWorkerEnabled = true;
+    }
+    return mWorkerEnabled;
+}
+
+//*******************************************************************************
+void Regulator::updateTolerance(int glitches, int skipped)
+{
+    // update headroom
+    if (mAutoHeadroom < 0) {
+        // variable headroom: automatically increase to minimize glitch counts
+        // only increase headroom if doing so would have reduced the number of
+        // glitches that occured over the past two seconds by 2% or more.
+        // prevent headroom from growing beyond rolling average of max.
+        const int skipsAllowed =
+            static_cast<int>(AutoHeadroomGlitchTolerance * mSampleRate / mPeerFPP);
+        if (glitches > 0 && skipped > skipsAllowed
+            && mCurrentHeadroom + 1 <= pushStat->longTermMax) {
+            if (mSkipAutoHeadroom) {
+                mSkipAutoHeadroom = false;
+            } else {
+                mSkipAutoHeadroom = true;
+                ++mCurrentHeadroom;
+                cout << "PLC glitches=" << glitches << " skipped=" << skipped << ">"
+                     << skipsAllowed << ", increasing headroom to " << mCurrentHeadroom
+                     << " (max=" << pushStat->longTermMax << ")" << endl;
+            }
+        } else {
+            mSkipAutoHeadroom = true;
+        }
+    } else {
+        // fixed headroom
+        mCurrentHeadroom = mAutoHeadroom;
+    }
+
+    // pushes happen when we have new packets received from peer
+    // pulls happen when our audio interface triggers a callback
+    const double pushStatTol = pushStat->calcAuto();
+    const double pullStatTol = pullStat->calcAuto();
+    double newTolerance = std::max<double>(pushStatTol + mCurrentHeadroom, pullStatTol);
+    if (newTolerance > AutoMax)
+        newTolerance = AutoMax;
+    if (newTolerance < mLocalFPPdurMsec)
+        newTolerance = mLocalFPPdurMsec;
+    if (newTolerance < mPeerFPPdurMsec)
+        newTolerance = mPeerFPPdurMsec;
+    mMsecTolerance = newTolerance;
+}
+
+//*******************************************************************************
+void Regulator::updatePushStats(int seq_num)
+{
+    // use time of last packet pulled as a baseline time for previous packet
+    // this avoids having to search for a previous packet that wasn't missing
+    // pkts is distance of previous packet from mLastSeqNumOut
+    int pkts = seq_num - 1 - mLastSeqNumOut;
+    if (pkts < 0)
+        pkts += NumSlots;
+    double prev_time = mIncomingTiming[mLastSeqNumOut] + (pkts * mPeerFPPdurMsec);
+    if (prev_time >= mIncomingTiming[seq_num])
+        return;  // skip edge case where mLastSeqNumOut was very late
+
+    // update push stats
+    bool pushStatsUpdated = pushStat->tick(prev_time, mIncomingTiming[seq_num]);
+    if (pushStatsUpdated && pushStat->longTermCnt % WindowDivisor == 0) {
+        // once per second
+        const int totalGlitches = pullStat->plcUnderruns + pullStat->plcOverruns;
+        const int totalSkipped  = mSkipped;
+        const int newGlitches   = totalGlitches - mLastGlitches;
+        const int newSkipped    = totalSkipped - mLastSkipped;
+        mLastGlitches           = totalGlitches;
+        mLastSkipped            = totalSkipped;
+        if (mAuto && pushStat->lastTime > AutoInitDur) {
+            // after AutoInitDur: update auto tolerance once per second
+            updateTolerance(newGlitches, newSkipped);
+        }
+    }
+}
+
+//*******************************************************************************
+void Regulator::pushPacket(const int8_t* buf, int seq_num)
+{
+    if (m_b_BroadcastRingBuffer != NULL)
+        m_b_BroadcastRingBuffer->insertSlotNonBlocking(buf, mPeerBytes, 0, seq_num);
+    seq_num %= NumSlots;
+    // if (seq_num==0) return;   // impose regular loss
+    mIncomingTiming[seq_num] = (double)mIncomingTimer.nsecsElapsed() / 1000000.0;
+    memcpy(mSlots[seq_num], buf, mPeerBytes);
+    mLastSeqNumIn.store(seq_num, std::memory_order_release);
+};
+
+//*******************************************************************************
+bool Regulator::pullPacket()
+{
+    const double now       = (double)mIncomingTimer.nsecsElapsed() / 1000000.0;
+    const int lastSeqNumIn = mLastSeqNumIn.load(std::memory_order_acquire);
+    int skipped            = 0;
+
+    if ((lastSeqNumIn == -1) || (!mInitialized)) {
+        goto ZERO_OUTPUT;
+    } else if (lastSeqNumIn == mLastSeqNumOut) {
+        goto UNDERRUN;
+    } else {
+        // calculate how many new packets we want to look at to
+        // find the next packet to pull
+        int new_pkts = lastSeqNumIn - mLastSeqNumOut;
+        if (new_pkts < 0)
+            new_pkts += NumSlots;
+
+        // iterate through each new packet
+        for (int i = new_pkts - 1; i >= 0; i--) {
+            int next = lastSeqNumIn - i;
+            if (next < 0)
+                next += NumSlots;
+            if (mLastSeqNumOut != -1) {
+                // account for missing packets
+                if (mIncomingTiming[next] < mIncomingTiming[mLastSeqNumOut]
+                    && mIncomingTiming[mLastSeqNumOut] - mIncomingTiming[next]
+                           > mMsecTolerance)
+                    continue;
+                updatePushStats(next);
+                // count how many we have skipped
+                skipped = next - mLastSeqNumOut - 1;
+                if (skipped < 0)
+                    skipped += NumSlots;
+            }
+            if (mIncomingTiming[next] + mMsecTolerance >= now) {
+                // next is the best candidate
+                memcpy(mXfrBuffer, mSlots[next], mPeerBytes);
+                mLastSeqNumOut = next;
+                goto PACKETOK;
+            }
+            // track how many good packets we skipped due to tolerance < 1ms
+            if (mIncomingTiming[next] + mMsecTolerance + 1 >= now) {
+                ++mSkipped;
+            }
+        }
+
+        // no viable candidate
+        goto UNDERRUN;
+    }
+
+PACKETOK : {
+    if (skipped) {
+        processPacket(true);
+        pullStat->plcOverruns += skipped;
+        return true;
+    } else
+        processPacket(false);
+    goto OUTPUT;
+}
+
+UNDERRUN : {
+    pullStat->plcUnderruns++;  // count late
+    if ((mLastSeqNumOut == lastSeqNumIn)
+        && ((now - mIncomingTiming[mLastSeqNumOut]) > gUdpWaitTimeout)) {
+        goto ZERO_OUTPUT;
+    }
+    // "good underrun", not a stuck client
+    processPacket(true);
+    return true;
+}
+
+ZERO_OUTPUT:
+    memset(mXfrBuffer, 0, mPeerBytes);
+
+OUTPUT:
+    return false;
+};
+
+//*******************************************************************************
+void Regulator::processPacket(bool glitch)
+{
+    double tmp = 0.0;
+    if (glitch)
+        tmp = (double)mIncomingTimer.nsecsElapsed();
+
+    zeroTmpFloatBuf();  // ahead of either call to burg
+    xfrBufferToFloatBuf();
+    burg(glitch);
+    floatBufToXfrBuffer();
+
+    if (glitch) {
+        double tmp2 = (double)mIncomingTimer.nsecsElapsed() - tmp;
+        tmp2 /= 1000000.0;
+        if (tmp2 > mStatsMaxPLCdspElapsed)
+            mStatsMaxPLCdspElapsed = tmp2;
+    }
+}
+
+//*******************************************************************************
+// copped from AudioInterface.cpp
+
+sample_t Regulator::bitsToSample(int ch, int frame)
+{
+    sample_t sample = 0.0;
+    AudioInterface::fromBitToSampleConversion(
+        &mXfrBuffer[(frame * mBitResolutionMode * mNumChannels)
+                    + (ch * mBitResolutionMode)],
+        &sample, mBitResolutionMode);
+    return sample;
+}
+
+void Regulator::sampleToBits(sample_t sample, int ch, int frame)
+{
+    AudioInterface::fromSampleToBitConversion(
+        &sample,
+        &mXfrBuffer[(frame * mBitResolutionMode * mNumChannels)
+                    + (ch * mBitResolutionMode)],
+        mBitResolutionMode);
+}
+
+/*
+void Regulator::sineToXfrBuffer()
+{
+    for (int ch = 0; ch < mNumChannels; ch++)
+        for (int s = 0; s < mPeerFPP; s++) {
+            sampleToBits(0.7 * sin(mPhasor[ch]), ch, s);
+            mPhasor[ch] += (!ch) ? 0.1 : 0.11;
+        }
+};
+*/
+
+void Regulator::floatBufToXfrBuffer()
+{
+    for (int ch = 0; ch < mNumChannels; ch++)
+        for (int s = 0; s < mPeerFPP; s++) {
+            double tmpOut = mChanData[ch]->mTmpFloatBuf[s];
+            //              if (tmpOut > 1.0) tmpOut = 1.0;
+            //              if (tmpOut < -1.0) tmpOut = -1.0;
+            sampleToBits(tmpOut, ch, s);
+        }
+};
+
+void Regulator::xfrBufferToFloatBuf()
+{
+    for (int ch = 0; ch < mNumChannels; ch++)
+        for (int s = 0; s < mPeerFPP; s++) {
+            double tmpIn                   = bitsToSample(ch, s);
+            mChanData[ch]->mTmpFloatBuf[s] = tmpIn;
+        }
+};
+
+void Regulator::toFloatBuf(qint16* in)
+{
+    for (int ch = 0; ch < mNumChannels; ch++)
+        for (int i = 0; i < mPeerFPP; i++) {
+            double tmpIn                   = ((qint16)*in++) * mInvScale;
+            mChanData[ch]->mTmpFloatBuf[i] = tmpIn;
+        }
+}
+
+void Regulator::fromFloatBuf(qint16* out)
+{
+    for (int ch = 0; ch < mNumChannels; ch++)
+        for (int i = 0; i < mPeerFPP; i++) {
+            double tmpOut = mChanData[ch]->mTmpFloatBuf[i];
+            if (tmpOut > 1.0)
+                tmpOut = 1.0;
+            if (tmpOut < -1.0)
+                tmpOut = -1.0;
+            *out++ = (qint16)(tmpOut * mScale);
+        }
+}
+
+void Regulator::zeroTmpFloatBuf()
+{
+    for (int ch = 0; ch < mNumChannels; ch++)
+        mChanData[ch]->mTmpFloatBuf = mChanData[ch]->mZeros;
+}
+
+//*******************************************************************************
+StdDev::StdDev(int id, QElapsedTimer* timer, int w) : mId(id), mTimer(timer), window(w)
+{
+    window /= WindowDivisor;
+    reset();
+    longTermStdDev    = 0.0;
+    longTermStdDevAcc = 0.0;
+    longTermCnt       = 0;
+    lastMean          = 0.0;
+    lastMin           = 0.0;
+    lastMax           = 0.0;
+    longTermMax       = 0.0;
+    longTermMaxAcc    = 0.0;
+    lastTime          = 0.0;
+    plcOverruns       = 0;
+    plcUnderruns      = 0;
+    data.resize(w, 0.0);
+}
+
+void StdDev::reset()
+{
+    ctr  = 0;
+    mean = 0.0;
+    acc  = 0.0;
+    min  = 999999.0;
+    max  = -999999.0;
+};
+
+double StdDev::calcAuto()
+{
+    if ((longTermStdDev == 0.0) || (longTermMax == 0.0))
+        return AutoMax;
+    double tmp = longTermStdDev + ((longTermMax > AutoMax) ? AutoMax : longTermMax);
+    return tmp;
+};
+
+double StdDev::smooth(double avg, double current)
+{
+    // use exponential weighted moving average (EWMA) for long term calculations
+    // See https://en.wikipedia.org/wiki/Exponential_smoothing
+    return avg + AutoSmoothingFactor * (current - avg);
+}
+
+bool StdDev::tick(double prevTime, double curTime)
+{
+    double msElapsed = curTime - prevTime;
+    if (msElapsed > 0) {
+        lastTime = curTime;
+    } else {
+        double now = (double)mTimer->nsecsElapsed() / 1000000.0;
+        msElapsed  = now - lastTime;
+        lastTime   = now;
+    }
+
+    // discard measurements that exceed the max wait time
+    // this prevents temporary outages from skewing jitter metrics
+    if (msElapsed > gUdpWaitTimeout)
+        return false;
+
+    data[ctr] = msElapsed;
+    if (msElapsed < min)
+        min = msElapsed;
+    else if (msElapsed > max)
+        max = msElapsed;
+    acc += msElapsed;
+    if (ctr == 0 && longTermCnt % WindowDivisor == 0) {
+        lastMin = msElapsed;
+        lastMax = msElapsed;
+    } else {
+        if (msElapsed < lastMin)
+            lastMin = msElapsed;
+        if (msElapsed > lastMax)
+            lastMax = msElapsed;
+    }
+    if (++ctr < window)
+        return false;
+    ctr = 0;
+
+    // calculate mean and standard deviation
+    mean       = (double)acc / (double)window;
+    double var = 0.0;
+    for (int i = 0; i < window; i++) {
+        double tmp = data[i] - mean;
+        var += (tmp * tmp);
+    }
+    var /= (double)window;
+    double stdDevTmp = sqrt(var);
+
+    if (longTermCnt <= 3) {
+        if (longTermCnt == 0 && gVerboseFlag && PrintDirect) {
+            cout << "printing directly from Regulator->stdDev->tick:" << endl
+                 << " (mean / min / max / stdDev / longTermMax / longTermStdDev)" << endl;
+        }
+        // ignore first few stats because they are unreliable
+        longTermMax       = max;
+        longTermMaxAcc    = max;
+        longTermStdDev    = stdDevTmp;
+        longTermStdDevAcc = stdDevTmp;
+    } else {
+        longTermStdDevAcc += stdDevTmp;
+        longTermMaxAcc += max;
+        if (longTermCnt <= (WindowDivisor * AutoHistoryWindow)) {
+            // use simple average for startup to establish baseline
+            longTermStdDev = longTermStdDevAcc / (longTermCnt - 3);
+            longTermMax    = longTermMaxAcc / (longTermCnt - 3);
+        } else {
+            // use EWMA after startup to allow for adjustments
+            longTermStdDev = smooth(longTermStdDev, stdDevTmp);
+            longTermMax    = smooth(longTermMax, max);
+        }
+    }
+
+    if (gVerboseFlag && PrintDirect) {
+        cout << setw(10) << mean << setw(10) << min << setw(10) << max << setw(10)
+             << stdDevTmp << setw(10) << longTermMax << setw(10) << longTermStdDev << " "
+             << mId << endl;
+    }
+
+    longTermCnt++;
+    lastMean   = mean;
+    lastStdDev = stdDevTmp;
+    reset();
+
+    return true;
+}
+
+void Regulator::readSlotNonBlocking(int8_t* ptrToReadSlot)
+{
+    if (!mInitialized) {
+        // audio callback before receiving first packet from peer
+        // nothing is initialized yet, so just return silence
+        memset(ptrToReadSlot, 0, mLocalBytes);
+        return;
+    }
+
+    pullStat->tick();
+
+    bool prediction = false;
+    if (mFPPratioNumerator == mFPPratioDenominator) {
+        // local FPP matches peer
+        if (isWorkerEnabled()) {
+            // we need to use a different buffer here since mXfrBuffer
+            // is used to pass data between the worker and PLC backend
+            mRegulatorWorkerPtr->pop(mWorkerBuffer);
+            memcpy(ptrToReadSlot, mWorkerBuffer, mLocalBytes);
+        } else {
+            prediction = pullPacket();
+            memcpy(ptrToReadSlot, mXfrBuffer, mLocalBytes);
+            if (prediction)
+                enableWorker();
+            // nothing special to do if enabled
+        }
+        return;
+    }
+
+    if (mFPPratioNumerator > 1) {
+        // 2/1, 4/1 peer FPP is lower, (local/peer)/1
+        if (isWorkerEnabled()) {
+            for (int i = 0; i < mFPPratioNumerator; i++) {
+                mRegulatorWorkerPtr->pop(mWorkerBuffer);
+                memcpy(ptrToReadSlot, mWorkerBuffer, mPeerBytes);
+                ptrToReadSlot += mPeerBytes;
+            }
+        } else {
+            for (int i = 0; i < mFPPratioNumerator; i++) {
+                if (pullPacket())
+                    prediction = true;
+                memcpy(ptrToReadSlot, mXfrBuffer, mPeerBytes);
+                ptrToReadSlot += mPeerBytes;
+            }
+            if (prediction)
+                enableWorker();
+            // nothing special to do if enabled
+        }
+        return;
+    }
+
+    // 1/2, 1/4 peer FPP is higher, 1/(peer/local)
+    if (isWorkerEnabled()) {
+        if (mXfrPullPtr == NULL || mXfrPullPtr >= (mWorkerBuffer + mPeerBytes)) {
+            mRegulatorWorkerPtr->pop(mWorkerBuffer);
+            mXfrPullPtr = mWorkerBuffer;
+        }
+    } else {
+        if (mXfrPullPtr == NULL || mXfrPullPtr >= (mXfrBuffer + mPeerBytes)) {
+            prediction  = pullPacket();
+            mXfrPullPtr = mXfrBuffer;
+            if (prediction) {
+                if (enableWorker()) {
+                    // copy result to worker buffer
+                    memcpy(mWorkerBuffer, mXfrBuffer, mPeerBytes);
+                    mXfrPullPtr = mWorkerBuffer;
+                }
+            }
+        }
+    }
+
+    memcpy(ptrToReadSlot, mXfrPullPtr, mLocalBytes);
+    mXfrPullPtr += mLocalBytes;
+}
+
+void Regulator::readBroadcastSlot(int8_t* ptrToReadSlot)
+{
+    if (!mInitialized || m_b_BroadcastRingBuffer == NULL) {
+        // audio callback before receiving first packet from peer
+        // nothing is initialized yet, so just return silence
+        memset(ptrToReadSlot, 0, mLocalBytes);
+        return;
+    }
+
+    if (mFPPratioNumerator == mFPPratioDenominator) {
+        // local FPP matches peer
+        m_b_BroadcastRingBuffer->readBroadcastSlot(ptrToReadSlot);
+        return;
+    }
+
+    if (mFPPratioNumerator > 1) {
+        // 2/1, 4/1 peer FPP is lower, (local/peer)/1
+        for (int i = 0; i < mFPPratioNumerator; i++) {
+            m_b_BroadcastRingBuffer->readBroadcastSlot(ptrToReadSlot);
+            ptrToReadSlot += mPeerBytes;
+        }
+        return;
+    }
+
+    // 1/2, 1/4 peer FPP is higher, 1/(peer/local)
+    if (mBroadcastPullPtr == NULL
+        || mBroadcastPullPtr >= (mBroadcastBuffer + mPeerBytes)) {
+        m_b_BroadcastRingBuffer->readBroadcastSlot(mBroadcastBuffer);
+        mBroadcastPullPtr = mBroadcastBuffer;
+    }
+    memcpy(ptrToReadSlot, mBroadcastPullPtr, mLocalBytes);
+    mBroadcastPullPtr += mLocalBytes;
+}
+
+//*******************************************************************************
+void Regulator::burg(bool glitch)
+{  // generate next bufferfull and convert to short int
+    bool primed = mPcnt > mPacketsInThePast;
+    for (int ch = 0; ch < mNumChannels; ch++) {
+        Channel* c = mChanData[ch];
+        //////////////////////////////////////
+        if (glitch)
+            mTime->trigger(ch);  // if ch == 0, incr glitchCnt
+
+        for (int s = 0; s < mPeerFPP; s++)
+            c->realNowPacket[s] = (!glitch) ? c->mTmpFloatBuf[s] : 0.0;
+
+        if (!glitch) {
+            for (int s = 0; s < mPeerFPP; s++)
+                c->mTmpFloatBuf[s] = c->realNowPacket[s];
+            c->ringBufferPush();
+        }
+
+        if (primed) {
+            int offset = 0;
+            for (int i = 0; i < mPacketsInThePast; i++) {
+                c->ringBufferPull(mPacketsInThePast - i);
+                for (int s = 0; s < mPeerFPP; s++)
+                    c->realPast[s + offset] = c->mTmpFloatBuf[s];
+                offset += mPeerFPP;
+            }
+        }
+
+        if (glitch) {
+            for (int s = 0; s < mUpToNow; s++)
+                c->prediction[s] = c->predictedPast[s / mPeerFPP][s % mPeerFPP];
+
+            ba->train(c->coeffs, c->prediction, mUpToNow);
+
+            ba->predict(c->coeffs, c->prediction, c->mTailSize);
+
+            for (int s = 0; s < mPeerFPP; s++)
+                c->predictedNowPacket[s] = c->prediction[mUpToNow + s];
+        }
+
+        for (int s = 0; s < mPeerFPP; s++)
+            c->mTmpFloatBuf[s] = c->outputNowPacket[s] =
+                ((glitch)
+                     ? ((primed) ? c->predictedNowPacket[s] : 0.0)
+                     : ((c->lastWasGlitch) ? (mFadeDown[s] * c->futurePredictedPacket[s]
+                                              + mFadeUp[s] * c->realNowPacket[s])
+                                           : c->realNowPacket[s]));
+
+        for (int s = 0; s < mPeerFPP; s++)
+            c->mTmpFloatBuf[s] = c->outputNowPacket[s];
+
+        c->lastWasGlitch = glitch;
+
+        for (int i = 0; i < mPacketsInThePast - 1; i++) {
+            for (int s = 0; s < mPeerFPP; s++)
+                c->predictedPast[i][s] = c->predictedPast[i + 1][s];
+        }
+        for (int s = 0; s < mPeerFPP; s++)
+            c->predictedPast[mPacketsInThePast - 1][s] = c->outputNowPacket[s];
+
+        for (int s = 0; s < mPeerFPP; s++)
+            c->futurePredictedPacket[s] = c->prediction[mBeyondNow + s - 0];
+
+        if (glitch)
+            mTime->collect();
+        //////////////////////////////////////
+    }
+
+    if ((!(mPcnt % 300)) && (gVerboseFlag))
+        cout << "PLC avg " << mTime->avg() << " glitches " << mTime->glitches()
+             << " skipped " << (mSkipped - mLastSkipped) << " tolerance "
+             << (mMsecTolerance - mCurrentHeadroom) << " +" << mCurrentHeadroom << endl;
+    mPcnt++;
+    // 32 bit is good for days:  (/ (* (- (expt 2 32) 1) (/ 32 48000.0)) (* 60 60 24))
+}
+
+//*******************************************************************************
+bool Regulator::getStats(RingBuffer::IOStat* stat, bool reset)
+{
+    if (!mInitialized) {
+        return false;
+    }
+
+    if (reset) {  // all are unused, this is copied from superclass
+        mUnderruns        = 0;
+        mOverflows        = 0;
+        mSkew0            = mLevel;
+        mSkewRaw          = 0;
+        mBufDecOverflow   = 0;
+        mBufDecPktLoss    = 0;
+        mBufIncUnderrun   = 0;
+        mBufIncCompensate = 0;
+        mBroadcastSkew    = 0;
+    }
+
+    if (isWorkerEnabled() && mRegulatorWorkerPtr != nullptr) {
+        mRegulatorWorkerPtr->getStats();
+    }
+
+    // hijack  of  struct IOStat {
+    stat->underruns = mLastGlitches - mStatsGlitches;
+#define FLOATFACTOR 1000.0
+    stat->overflows = FLOATFACTOR * pushStat->longTermStdDev;
+    stat->skew      = FLOATFACTOR * pushStat->lastMean;
+    stat->skew_raw  = FLOATFACTOR * pushStat->lastMin;
+    stat->level     = FLOATFACTOR * pushStat->lastMax;
+    //    stat->level              = FLOATFACTOR * pushStat->longTermMax;
+    stat->buf_dec_overflows  = FLOATFACTOR * pushStat->lastStdDev;
+    stat->autoq_corr         = FLOATFACTOR * mMsecTolerance;
+    stat->buf_dec_pktloss    = FLOATFACTOR * pullStat->longTermStdDev;
+    stat->buf_inc_underrun   = FLOATFACTOR * pullStat->lastMean;
+    stat->buf_inc_compensate = FLOATFACTOR * pullStat->lastMin;
+    stat->broadcast_skew     = FLOATFACTOR * pullStat->lastMax;
+    stat->broadcast_delta    = FLOATFACTOR * pullStat->lastStdDev;
+    stat->autoq_rate         = FLOATFACTOR * mStatsMaxPLCdspElapsed;
+    // reset a few stats for next time
+    mStatsGlitches         = mLastGlitches;
+    mStatsMaxPLCdspElapsed = 0.0;
+    // none are unused
+    return true;
+}
diff --git a/src/Regulator.h b/src/Regulator.h
new file mode 100644 (file)
index 0000000..c65c3d2
--- /dev/null
@@ -0,0 +1,443 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024 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 Regulator.h
+ * \author Chris Chafe
+ * \date May 2021 - May 2024
+ */
+
+// Initial references and starter code to bring up Burg's recursion
+// http://www.emptyloop.com/technotes/A%20tutorial%20on%20Burg's%20method,%20algorithm%20and%20recursion.pdf
+// https://metacpan.org/source/SYP/Algorithm-Burg-0.001/README
+
+#ifndef __REGULATOR_H__
+#define __REGULATOR_H__
+
+#include <math.h>
+
+#include <QDebug>
+#include <QElapsedTimer>
+#include <QThread>
+#include <atomic>
+#include <cstring>
+#include <vector>
+
+#include "AudioInterface.h"
+#include "RingBuffer.h"
+#include "WaitFreeFrameBuffer.h"
+#include "jacktrip_globals.h"
+
+// forward declaration
+class RegulatorWorker;
+
+class BurgAlgorithm
+{
+   public:
+    BurgAlgorithm(int size);
+    void train(std::vector<float>& coeffs, const std::vector<float>& x, int size);
+    void predict(std::vector<float>& coeffs, std::vector<float>& predicted, int size);
+
+   private:
+    int m;
+    int N;
+    int size;
+    std::vector<float> Ak;
+    std::vector<float> AkReset;
+    std::vector<float> f;
+    std::vector<float> b;
+};
+
+class Time
+{
+    double accum   = 0.0;
+    int cnt        = 0;
+    int glitchCnt  = 0;
+    double tmpTime = 0.0;
+
+   public:
+    QElapsedTimer mCallbackTimer;  // for rcvElapsedTime
+    void collect()
+    {
+        double tmp = (mCallbackTimer.nsecsElapsed() - tmpTime) / 1000000.0;
+        accum += tmp;
+        cnt++;
+    }
+    double instantElapsed()
+    {
+        return (mCallbackTimer.nsecsElapsed() - tmpTime) / 1000000.0;
+    }
+    double instantAbsolute() { return (mCallbackTimer.nsecsElapsed()) / 1000000.0; }
+    double avg()
+    {
+        if (!cnt)
+            return 0.0;
+        double tmp = accum / (double)cnt;
+        accum      = 0.0;
+        cnt        = 0;
+        return tmp;
+    }
+    void start() { mCallbackTimer.start(); }
+    void trigger(int ch)
+    {
+        tmpTime = mCallbackTimer.nsecsElapsed();
+        if (!ch)
+            glitchCnt++;
+    }
+    int glitches()
+    {
+        int tmp   = glitchCnt;
+        glitchCnt = 0;
+        return tmp;
+    }
+};
+
+class Channel
+{
+   public:
+    Channel(int fpp, int upToNow, int packetsInThePast);
+    void ringBufferPush();
+    void ringBufferPull(int past);
+    std::vector<float>
+        mTmpFloatBuf;  // one bufferfull of audio, used for rcv and send operations
+    std::vector<float> prediction;
+    std::vector<float> predictedNowPacket;
+    std::vector<float> realNowPacket;
+    std::vector<float> outputNowPacket;
+    std::vector<float> futurePredictedPacket;
+    std::vector<float> realPast;
+    std::vector<float> zeroPast;
+    std::vector<std::vector<float>> predictedPast;
+    std::vector<float> coeffs;
+    std::vector<std::vector<float>> mPacketRing;
+    int mWptr;
+    int mRing;
+    std::vector<float> mZeros;
+    bool lastWasGlitch;
+    int mCoeffsSize;
+    int mTailSize;
+};
+
+class StdDev
+{
+   public:
+    StdDev(int id, QElapsedTimer* timer, int w);
+    bool tick(double prevTime = 0,
+              double curTime  = 0);  // returns true if stats were updated
+    double calcAuto();
+    int mId;
+    int plcOverruns;
+    int plcUnderruns;
+    double lastTime;
+    double lastMean;
+    double lastMin;
+    double lastMax;
+    double lastStdDev;
+    double longTermStdDev;
+    double longTermStdDevAcc;
+    double longTermMax;
+    double longTermMaxAcc;
+    int longTermCnt;
+
+   private:
+    double smooth(double avg, double current);
+    void reset();
+    QElapsedTimer* mTimer = nullptr;
+    std::vector<double> data;
+    double mean;
+    int window;
+    double acc;
+    double min;
+    double max;
+    int ctr;
+};
+
+class Regulator : public RingBuffer
+{
+   public:
+    /// construct a new regulator
+    Regulator(int rcvChannels, int bit_res, int localFPP, int qLen, int bqLen,
+              int sample_rate);
+
+    // virtual destructor
+    virtual ~Regulator();
+
+    // can hijack unused2 to propagate incoming seq num if needed
+    // option is in UdpDataProtocol
+    // if (!mJackTrip->writeAudioBuffer(src, host_buf_size, last_seq_num))
+    // instead of
+    // if (!mJackTrip->writeAudioBuffer(src, host_buf_size, gap_size))
+    virtual bool insertSlotNonBlocking(const int8_t* ptrToSlot, int len,
+                                       [[maybe_unused]] int lostLen, int seq_num)
+    {
+        if (seq_num == -1)
+            return true;
+        setFPPratio(len);
+        pushPacket(ptrToSlot, seq_num);
+        return (true);
+    }
+
+    /// @brief called by audio interface to get the next buffer of samples
+    /// @param ptrToReadSlot new samples will be copied to this memory block
+    virtual void readSlotNonBlocking(int8_t* ptrToReadSlot);
+
+    /// @brief called by broadcast ports to get the next buffer of samples
+    /// @param ptrToReadSlot new samples will be copied to this memory block
+    virtual void readBroadcastSlot(int8_t* ptrToReadSlot);
+
+    /// @brief returns sample rate
+    inline int getSampleRate() const { return mSampleRate; }
+
+    /// @brief returns number of bytes in an audio "packet"
+    inline int getPacketSize() const { return mLocalBytes; }
+
+    /// @brief returns number of samples, or frames per callback period
+    inline int getBufferSizeInSamples() const { return mLocalFPP; }
+
+    /// @brief returns true if worker thread & queue is enabled
+    inline bool isWorkerEnabled() const { return mWorkerEnabled; }
+
+    //    virtual QString getStats(uint32_t statCount, uint32_t lostCount);
+    virtual bool getStats(IOStat* stat, bool reset);
+
+   private:
+    void pushPacket(const int8_t* buf, int seq_num);
+    void updatePushStats(int seq_num);
+    bool pullPacket();    // returns true if PLC prediction
+    bool enableWorker();  // returns true if worker was enabled
+    void updateTolerance(int glitches, int skipped);
+    void setFPPratio(int len);
+    void processPacket(bool glitch);
+    void burg(bool glitch);
+    sample_t bitsToSample(int ch, int frame);
+    void sampleToBits(sample_t sample, int ch, int frame);
+    void floatBufToXfrBuffer();
+    void xfrBufferToFloatBuf();
+    void toFloatBuf(qint16* in);
+    void fromFloatBuf(qint16* out);
+    void zeroTmpFloatBuf();
+
+    /*
+       void sineToXfrBuffer(); // keep this around, handy for signal test points
+        std::vector<double> mPhasor;
+        */
+
+    int mPacketsInThePast;
+    bool mInitialized;
+    int mNumChannels;
+    int mAudioBitRes;
+    int mLocalFPP;
+    int mPeerFPP;
+    int mSampleRate;
+    int mPcnt;
+    std::vector<float> mTmpFloatBuf;
+    std::vector<Channel*> mChanData;
+    BurgAlgorithm* ba = nullptr;
+    int mUpToNow;
+    int mBeyondNow;
+    std::vector<float> mFadeUp;
+    std::vector<float> mFadeDown;
+    float mScale;
+    float mInvScale;
+    uint32_t mLastLostCount = 0;
+    AudioInterface::audioBitResolutionT mBitResolutionMode;
+    int mLocalBytes;
+    int mPeerBytes;
+    double mLocalFPPdurMsec;
+    double mPeerFPPdurMsec;
+    int8_t* mXfrBuffer        = nullptr;
+    int8_t* mXfrPullPtr       = nullptr;
+    int8_t* mBroadcastBuffer  = nullptr;
+    int8_t* mBroadcastPullPtr = nullptr;
+    int8_t** mSlots           = nullptr;
+    int8_t* mSlotBuf          = nullptr;
+    StdDev* pushStat          = nullptr;
+    StdDev* pullStat          = nullptr;
+    double mMsecTolerance     = 64;
+    int mLastSeqNumOut        = -1;
+    std::atomic<int> mLastSeqNumIn;
+    QElapsedTimer mIncomingTimer;
+    std::vector<double> mIncomingTiming;
+    int mFPPratioNumerator;
+    int mFPPratioDenominator;
+    bool mAuto                    = false;
+    bool mSkipAutoHeadroom        = true;
+    int mSkipped                  = 0;
+    int mLastSkipped              = 0;
+    int mLastGlitches             = 0;
+    int mStatsGlitches            = 0;
+    double mStatsMaxPLCdspElapsed = 0;
+    double mCurrentHeadroom       = 0;
+    double mAutoHeadroom          = -1;
+    Time* mTime                   = nullptr;
+
+    /// Pointer for the Broadcast RingBuffer
+    RingBuffer* m_b_BroadcastRingBuffer = nullptr;
+    int m_b_BroadcastQueueLength;
+
+    /// worker thread used to manage queue
+    int8_t* mWorkerBuffer                = nullptr;
+    QThread* mWorkerThreadPtr            = nullptr;
+    RegulatorWorker* mRegulatorWorkerPtr = nullptr;
+    bool mWorkerEnabled                  = false;
+    friend class RegulatorWorker;
+};
+
+class RegulatorWorker : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    RegulatorWorker(Regulator* rPtr)
+        : mRegulatorPtr(rPtr)
+        , mPacketQueue(rPtr->mPeerBytes)
+        , mPacketQueueTarget(1)
+        , mLastUnderrun(0)
+        , mSkipQueueUpdate(true)
+        , mUnderrun(false)
+        , mStarted(false)
+    {
+        // wire up signals
+        QObject::connect(this, &RegulatorWorker::startup, this,
+                         &RegulatorWorker::setRealtimePriority, Qt::QueuedConnection);
+        QObject::connect(this, &RegulatorWorker::signalPullPacket, this,
+                         &RegulatorWorker::pullPacket, Qt::QueuedConnection);
+        // set thread to realtime priority
+        emit startup();
+    }
+
+    virtual ~RegulatorWorker() {}
+
+    bool pop(int8_t* pktPtr)
+    {
+        // start pulling more packets to maintain target
+        emit signalPullPacket();
+
+        if (mPacketQueue.pop(pktPtr))
+            return true;
+
+        // use silence for underruns
+        ::memset(pktPtr, 0, mPacketQueue.getBytesPerFrame());
+
+        // trigger underrun to re-evaluate queue target
+        mUnderrun.store(true, std::memory_order_relaxed);
+
+        return false;
+    }
+
+    void getStats()
+    {
+        std::cout << "PLC worker queue: size=" << mPacketQueue.size()
+                  << " target=" << mPacketQueueTarget
+                  << " underruns=" << mPacketQueue.getUnderruns()
+                  << " overruns=" << mPacketQueue.getOverruns() << std::endl;
+        mPacketQueue.clearStats();
+    }
+
+   signals:
+    void signalPullPacket();
+    void signalMaxQueueSize();
+    void startup();
+
+   public slots:
+    void pullPacket()
+    {
+        if (mUnderrun.load(std::memory_order_relaxed)) {
+            if (mStarted) {
+                double now =
+                    (double)mRegulatorPtr->mIncomingTimer.nsecsElapsed() / 1000000.0;
+                // only adjust target at most once per 1.0 seconds
+                if (now - mLastUnderrun >= 1000.0) {
+                    if (mSkipQueueUpdate) {
+                        // require consecutive underruns periods to bump target
+                        mSkipQueueUpdate = false;
+                    } else if (now - mLastUnderrun < 2000.0) {
+                        // previous period had underruns
+                        updateQueueTarget();
+                        mSkipQueueUpdate = true;
+                    }  // else, skip this period but not the next one
+                    mLastUnderrun = now;
+                }
+                mUnderrun.store(false, std::memory_order_relaxed);
+            } else {
+                mStarted = true;
+            }
+        }
+        std::size_t qSize = mPacketQueue.size();
+        while (qSize < mPacketQueueTarget) {
+            mRegulatorPtr->pullPacket();
+            qSize = mPacketQueue.push(mRegulatorPtr->mXfrBuffer);
+        }
+    }
+    void setRealtimePriority() { setRealtimeProcessPriority(); }
+
+   private:
+    void updateQueueTarget()
+    {
+        // sanity check
+        const std::size_t maxPackets = mPacketQueue.capacity() / 2;
+        if (mPacketQueueTarget > maxPackets)
+            return;
+        // adjust queue target
+        ++mPacketQueueTarget;
+        std::cout << "PLC worker queue: adjusting target=" << mPacketQueueTarget
+                  << " (max=" << maxPackets
+                  << ", lastDspElapsed=" << mRegulatorPtr->mStatsMaxPLCdspElapsed << ")"
+                  << std::endl;
+        if (mPacketQueueTarget == maxPackets) {
+            emit signalMaxQueueSize();
+            std::cout << "PLC worker queue: reached MAX target!" << std::endl;
+        }
+    }
+
+    /// pointer to Regulator for pulling packets
+    Regulator* mRegulatorPtr;
+
+    /// queue of ready packets (if mBufferStrategy==3)
+    WaitFreeFrameBuffer<> mPacketQueue;
+
+    /// target size for the packet queue
+    std::size_t mPacketQueueTarget;
+
+    /// time of last underrun, in milliseconds
+    double mLastUnderrun;
+
+    /// true if the next packet queue update should be skipped
+    bool mSkipQueueUpdate;
+
+    /// last value of packet queue underruns
+    std::atomic<bool> mUnderrun;
+
+    /// will be true after first packet is pushed
+    bool mStarted;
+};
+
+#endif  //__REGULATOR_H__
diff --git a/src/Reverb.cpp b/src/Reverb.cpp
new file mode 100644 (file)
index 0000000..c8f3b45
--- /dev/null
@@ -0,0 +1,155 @@
+//*****************************************************************
+/*
+  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 Reverb.cpp
+ * \author Julius Smith, based on Limiter.h
+ * \date August 2020
+ */
+
+#include "Reverb.h"
+
+#include "freeverbdsp.h"  // stereo in and out
+#include "freeverbmonodsp.h"  // mono in and out (there is no mono to stereo case in jacktrip as yet)
+#include "jacktrip_types.h"
+#include "zitarevdsp.h"      // stereo in and out
+#include "zitarevmonodsp.h"  // mono in and out
+
+//*******************************************************************************
+Reverb::Reverb(int numInChans, int numOutChans, float reverbLevel, bool verboseFlag)
+    : mNumInChannels(numInChans), mNumOutChannels(numOutChans), mReverbLevel(reverbLevel)
+{
+    setVerbose(verboseFlag);
+    if (mNumInChannels < 1) {
+        std::cerr << "*** Reverb.h: must have at least one input audio channels\n";
+        mNumInChannels = 1;
+    }
+    if (mNumInChannels > 2) {
+        std::cerr << "*** Reverb.h: limiting number of audio output channels to 2\n";
+        mNumInChannels = 2;
+    }
+#if 0
+std::cout << "Reverb: constructed for "
+            << mNumInChannels << " input channels and "
+            << mNumOutChannels << " output channels with reverb level = "
+            << mReverbLevel << "\n";
+#endif
+
+    if (mReverbLevel <= 1.0) {                    // freeverb:
+        freeverbStereoP   = new freeverbdsp;      // stereo input and output
+        freeverbMonoP     = new freeverbmonodsp;  // mono input, stereo output
+        freeverbStereoUIP = new APIUI;            // #included in *dsp.h
+        freeverbMonoUIP   = new APIUI;
+        static_cast<freeverbdsp*>(freeverbStereoP)
+            ->buildUserInterface(static_cast<APIUI*>(freeverbStereoUIP));
+        static_cast<freeverbmonodsp*>(freeverbMonoP)
+            ->buildUserInterface(static_cast<APIUI*>(freeverbMonoUIP));
+        // std::cout << "Using freeverb\n";
+    } else {
+        zitarevStereoP   = new zitarevdsp;      // stereo input and output
+        zitarevMonoP     = new zitarevmonodsp;  // mono input, stereo output
+        zitarevStereoUIP = new APIUI;
+        zitarevMonoUIP   = new APIUI;
+        static_cast<zitarevdsp*>(zitarevStereoP)
+            ->buildUserInterface(static_cast<APIUI*>(zitarevStereoUIP));
+        static_cast<zitarevmonodsp*>(zitarevMonoP)
+            ->buildUserInterface(static_cast<APIUI*>(zitarevMonoUIP));
+        // std::cout << "Using zitarev\n";
+    }
+}
+
+//*******************************************************************************
+Reverb::~Reverb()
+{
+    if (mReverbLevel <= 1.0) {  // freeverb:
+        delete static_cast<freeverbdsp*>(freeverbStereoP);
+        delete static_cast<freeverbmonodsp*>(freeverbMonoP);
+        delete static_cast<APIUI*>(freeverbStereoUIP);
+        delete static_cast<APIUI*>(freeverbMonoUIP);
+    } else {
+        delete static_cast<zitarevdsp*>(zitarevStereoP);
+        delete static_cast<zitarevmonodsp*>(zitarevMonoP);
+        delete static_cast<APIUI*>(zitarevStereoUIP);
+        delete static_cast<APIUI*>(zitarevMonoUIP);
+    }
+}
+
+//*******************************************************************************
+void Reverb::init(int samplingRate, int bufferSize)
+{
+    ProcessPlugin::init(samplingRate, bufferSize);
+    fs = float(fSamplingFreq);
+    if (mReverbLevel <= 1.0) {  // freeverb:
+        static_cast<freeverbdsp*>(freeverbStereoP)
+            ->init(fs);  // compression filter parameters depend on sampling rate
+        static_cast<freeverbmonodsp*>(freeverbMonoP)
+            ->init(fs);  // compression filter parameters depend on sampling rate
+        int ndx = static_cast<APIUI*>(freeverbStereoUIP)->getParamIndex("Wet");
+        static_cast<APIUI*>(freeverbStereoUIP)->setParamValue(ndx, mReverbLevel);
+        static_cast<APIUI*>(freeverbMonoUIP)->setParamValue(ndx, mReverbLevel);
+    } else {  // zitarev:
+        static_cast<zitarevdsp*>(zitarevStereoP)
+            ->init(fs);  // compression filter parameters depend on sampling rate
+        static_cast<zitarevmonodsp*>(zitarevMonoP)
+            ->init(fs);  // compression filter parameters depend on sampling rate
+        int ndx = static_cast<APIUI*>(zitarevStereoUIP)->getParamIndex("Wet");
+        float zitaLevel =
+            mReverbLevel - 1.0f;  // range within zitarev is 0 to 1 (our version only)
+        static_cast<APIUI*>(zitarevStereoUIP)->setParamValue(ndx, zitaLevel);
+        static_cast<APIUI*>(zitarevMonoUIP)->setParamValue(ndx, zitaLevel);
+    }
+    inited = true;
+}
+
+//*******************************************************************************
+void Reverb::compute(int nframes, float** inputs, float** outputs)
+{
+    if (!inited) {
+        std::cerr << "*** Reverb " << this << ": init never called! Doing it now.\n";
+        init(0, 0);
+    }
+    if (mReverbLevel <= 1.0) {
+        if (mNumInChannels == 1) {
+            static_cast<freeverbmonodsp*>(freeverbMonoP)
+                ->compute(nframes, inputs, outputs);
+        } else {
+            assert(mNumInChannels == 2);
+            static_cast<freeverbdsp*>(freeverbStereoP)->compute(nframes, inputs, outputs);
+        }
+    } else {
+        if (mNumInChannels == 1) {
+            static_cast<zitarevmonodsp*>(zitarevMonoP)->compute(nframes, inputs, outputs);
+        } else {
+            assert(mNumInChannels == 2);
+            static_cast<zitarevdsp*>(zitarevStereoP)->compute(nframes, inputs, outputs);
+        }
+    }
+}
diff --git a/src/Reverb.h b/src/Reverb.h
new file mode 100644 (file)
index 0000000..d608dfa
--- /dev/null
@@ -0,0 +1,85 @@
+//*****************************************************************
+/*
+  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 Reverb.h
+ * \author Julius Smith, based on Limiter.h
+ * \date August 2020
+ */
+
+/** \brief Applies freeverb or zitarev from the faustlibraries distribution: reverbs.lib
+ *
+ */
+#ifndef __REVERB_H__
+#define __REVERB_H__
+
+#include <iostream>
+
+#include "ProcessPlugin.h"
+
+/** \brief A Reverb is an echo-based delay effect,
+ *  providing a virtual acoustic listening space.
+ */
+class Reverb : public ProcessPlugin
+{
+   public:
+    /// \brief The class constructor sets the number of channels to limit
+    Reverb(int numInChans, int numOutChans, float reverbLevel = 1.0,
+           bool verboseFlag = false);
+
+    /// \brief The class destructor
+    virtual ~Reverb();
+
+    void init(int samplingRate, int bufferSize) override;
+    int getNumInputs() override { return (mNumInChannels); }
+    int getNumOutputs() override { return (mNumOutChannels); }
+    void compute(int nframes, float** inputs, float** outputs) override;
+    const char* getName() const override { return "Reverb"; }
+
+   private:
+    float fs;
+    int mNumInChannels;
+    int mNumOutChannels;
+
+    float mReverbLevel;
+
+    void* freeverbStereoP;
+    void* freeverbMonoP;
+    void* freeverbStereoUIP;
+    void* freeverbMonoUIP;
+
+    void* zitarevStereoP;
+    void* zitarevMonoP;
+    void* zitarevStereoUIP;
+    void* zitarevMonoUIP;
+};
+
+#endif
diff --git a/src/RingBuffer.cpp b/src/RingBuffer.cpp
new file mode 100644 (file)
index 0000000..370355e
--- /dev/null
@@ -0,0 +1,321 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file RingBuffer.cpp
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+#include "RingBuffer.h"
+
+#include <cmath>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <stdexcept>
+
+#include "JackTrip.h"
+
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+RingBuffer::RingBuffer(int SlotSize, int NumSlots)
+    : mSlotSize(SlotSize)
+    , mNumSlots(NumSlots)
+    , mTotalSize(mSlotSize * mNumSlots)
+    , mReadPosition(0)
+    , mWritePosition(0)
+    , mFullSlots(0)
+    , mRingBuffer(NULL)
+    , mLastReadSlot(NULL)
+{
+    if (0 < mTotalSize) {
+        mRingBuffer   = new int8_t[mTotalSize];
+        mLastReadSlot = new int8_t[mSlotSize];
+        std::memset(mRingBuffer, 0, mTotalSize);   // set buffer to 0
+        std::memset(mLastReadSlot, 0, mSlotSize);  // set buffer to 0
+
+        mWritePosition = ((NumSlots / 2) * SlotSize) % mTotalSize;
+    }
+
+    // Advance write position to half of the RingBuffer
+    // Update Full Slots accordingly
+    mFullSlots        = (NumSlots / 2);
+    mLevelDownRate    = 0.01;
+    mStatUnit         = 1;
+    mUnderruns        = 0;
+    mOverflows        = 0;
+    mSkew0            = 0;
+    mSkewRaw          = 0;
+    mLevelCur         = mFullSlots;
+    mLevel            = mLevelCur;
+    mBufDecOverflow   = 0;
+    mBufDecPktLoss    = 0;
+    mBufIncUnderrun   = 0;
+    mBufIncCompensate = 0;
+    mBroadcastSkew    = 0;
+    mBroadcastDelta   = 0;
+}
+
+//*******************************************************************************
+RingBuffer::~RingBuffer()
+{
+    delete[] mRingBuffer;  // Free memory
+    mRingBuffer = NULL;    // Clear to prevent using invalid memory reference
+    delete[] mLastReadSlot;
+    mLastReadSlot = NULL;
+}
+
+//*******************************************************************************
+void RingBuffer::insertSlotBlocking(const int8_t* ptrToSlot)
+{
+    QMutexLocker locker(&mMutex);  // lock the mutex
+    updateReadStats();
+
+    // Check if there is space available to write a slot
+    // If the Ringbuffer is full, it waits for the bufferIsNotFull condition
+    while (mFullSlots == mNumSlots) {
+        // std::cout << "OUTPUT OVERFLOW BLOCKING" << std::endl;
+        mBufferIsNotFull.wait(&mMutex);
+    }
+
+    // Copy mSlotSize bytes to mRingBuffer
+    std::memcpy(mRingBuffer + mWritePosition, ptrToSlot, mSlotSize);
+    // Update write position
+    mWritePosition = (mWritePosition + mSlotSize) % mTotalSize;
+    mFullSlots++;  // update full slots
+    // Wake threads waitng for bufferIsNotFull condition
+    mBufferIsNotEmpty.wakeAll();
+}
+
+//*******************************************************************************
+void RingBuffer::readSlotBlocking(int8_t* ptrToReadSlot)
+{
+    QMutexLocker locker(&mMutex);  // lock the mutex
+    ++mReadsNew;
+
+    // Check if there are slots available to read
+    // If the Ringbuffer is empty, it waits for the bufferIsNotEmpty condition
+    while (mFullSlots == 0) {
+        // std::cerr << "READ UNDER-RUN BLOCKING before" << endl;
+        mBufferIsNotEmpty.wait(&mMutex, 200);
+        if (JackTrip::sAudioStopped) {
+            return;
+        }
+    }
+
+    // Copy mSlotSize bytes to ReadSlot
+    std::memcpy(ptrToReadSlot, mRingBuffer + mReadPosition, mSlotSize);
+    // Always save memory of the last read slot
+    std::memcpy(mLastReadSlot, mRingBuffer + mReadPosition, mSlotSize);
+    // Update write position
+    mReadPosition =
+        (0 == mTotalSize) ? mReadPosition : (mReadPosition + mSlotSize) % mTotalSize;
+    mFullSlots--;  // update full slots
+    // Wake threads waitng for bufferIsNotFull condition
+    mBufferIsNotFull.wakeAll();
+}
+
+//*******************************************************************************
+bool RingBuffer::insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen,
+                                       [[maybe_unused]] int seq_num)
+{
+    if (len != mSlotSize && 0 != len) {
+        // RingBuffer does not support mixed buf sizes
+        return false;
+    }
+    QMutexLocker locker(&mMutex);  // lock the mutex
+    if (0 < lostLen) {
+        int lostCount = lostLen / mSlotSize;
+        mBufDecPktLoss += lostCount;
+        mSkewRaw -= lostCount;
+        mLevelCur -= lostCount;
+    }
+    updateReadStats();
+
+    // Check if there is space available to write a slot
+    // If the Ringbuffer is full, it returns without writing anything
+    // and resets the buffer
+    /// \todo It may be better here to insert the slot anyways,
+    /// instead of not writing anything
+    if (mFullSlots == mNumSlots) {
+        // std::cout << "OUTPUT OVERFLOW NON BLOCKING = " << mNumSlots << std::endl;
+        overflowReset();
+        return true;
+    }
+
+    // Copy mSlotSize bytes to mRingBuffer
+    std::memcpy(mRingBuffer + mWritePosition, ptrToSlot, mSlotSize);
+    // Update write position
+    mWritePosition = (mWritePosition + mSlotSize) % mTotalSize;
+    mFullSlots++;  // update full slots
+    // Wake threads waitng for bufferIsNotFull condition
+    mBufferIsNotEmpty.wakeAll();
+    return true;
+}
+
+//*******************************************************************************
+void RingBuffer::readSlotNonBlocking(int8_t* ptrToReadSlot)
+{
+    QMutexLocker locker(&mMutex);  // lock the mutex
+    ++mReadsNew;
+    if (mFullSlots < mLevelCur) {
+        mLevelCur = std::max<double>((double)mFullSlots, mLevelCur - mLevelDownRate);
+    } else {
+        mLevelCur = mFullSlots;
+    }
+
+    // Check if there are slots available to read
+    // If the Ringbuffer is empty, it returns a buffer of zeros and rests the buffer
+    if (mFullSlots <= 0) {
+        // Returns a buffer of zeros if there's nothing to read
+        // std::cerr << "READ UNDER-RUN NON BLOCKING = " << mNumSlots << endl;
+        // std::memset(ptrToReadSlot, 0, mSlotSize);
+        setUnderrunReadSlot(ptrToReadSlot);
+        underrunReset();
+        return;
+    }
+
+    // Copy mSlotSize bytes to ReadSlot
+    std::memcpy(ptrToReadSlot, mRingBuffer + mReadPosition, mSlotSize);
+    // Always save memory of the last read slot
+    std::memcpy(mLastReadSlot, mRingBuffer + mReadPosition, mSlotSize);
+    // Update write position
+    mReadPosition = (mReadPosition + mSlotSize) % mTotalSize;
+    mFullSlots--;  // update full slots
+    // Wake threads waitng for bufferIsNotFull condition
+    mBufferIsNotFull.wakeAll();
+}
+
+//*******************************************************************************
+// Not supported in RingBuffer
+void RingBuffer::readBroadcastSlot(int8_t* ptrToReadSlot)
+{
+    std::memset(ptrToReadSlot, 0, mSlotSize);
+}
+
+//*******************************************************************************
+void RingBuffer::setUnderrunReadSlot(int8_t* ptrToReadSlot)
+{
+    std::memset(ptrToReadSlot, 0, mSlotSize);
+}
+
+//*******************************************************************************
+void RingBuffer::setMemoryInReadSlotWithLastReadSlot(int8_t* ptrToReadSlot)
+{
+    std::memcpy(ptrToReadSlot, mLastReadSlot, mSlotSize);
+}
+
+//*******************************************************************************
+// Under-run happens when there's nothing to read.
+void RingBuffer::underrunReset()
+{
+    // Advance the write pointer 1/2 the ring buffer
+    // mWritePosition = ( mReadPosition + ( (mNumSlots/2) * mSlotSize ) ) % mTotalSize;
+    // mWritePosition = ( mWritePosition + ( (mNumSlots/2) * mSlotSize ) ) % mTotalSize;
+    // mFullSlots += mNumSlots/2;
+    // There's nothing new to read, so we clear the whole buffer (Set the entire buffer to
+    // 0)
+    std::memset(mRingBuffer, 0, mTotalSize);
+    ++mUnderrunsNew;
+}
+
+//*******************************************************************************
+// Over-flow happens when there's no space to write more slots.
+void RingBuffer::overflowReset()
+{
+    // Advance the read pointer 1/2 the ring buffer
+    // mReadPosition = ( mWritePosition + ( (mNumSlots/2) * mSlotSize ) ) % mTotalSize;
+    int d         = mNumSlots / 2;
+    mReadPosition = (mReadPosition + (d * mSlotSize)) % mTotalSize;
+    mFullSlots -= d;
+    mOverflows += d + 1;
+    mBufDecOverflow += d + 1;
+    mLevelCur -= d;
+}
+
+//*******************************************************************************
+void RingBuffer::debugDump() const
+{
+    cout << "mTotalSize = " << mTotalSize << endl;
+    cout << "mReadPosition = " << mReadPosition << endl;
+    cout << "mWritePosition = " << mWritePosition << endl;
+    cout << "mFullSlots = " << mFullSlots << endl;
+}
+
+//*******************************************************************************
+bool RingBuffer::getStats(RingBuffer::IOStat* stat, bool reset)
+{
+    QMutexLocker locker(&mMutex);
+    if (reset) {
+        mUnderruns        = 0;
+        mOverflows        = 0;
+        mSkew0            = mLevel;
+        mSkewRaw          = 0;
+        mBufDecOverflow   = 0;
+        mBufDecPktLoss    = 0;
+        mBufIncUnderrun   = 0;
+        mBufIncCompensate = 0;
+        mBroadcastSkew    = 0;
+    }
+    stat->underruns = mUnderruns / mStatUnit;
+    stat->overflows = mOverflows / mStatUnit;
+    stat->skew      = (int32_t)((mSkew0 - mLevel + mBufIncUnderrun + mBufIncCompensate
+                            - mBufDecOverflow - mBufDecPktLoss))
+                 / mStatUnit;
+    stat->skew_raw = mSkewRaw / mStatUnit;
+    stat->level    = mLevel / mStatUnit;
+
+    stat->buf_dec_overflows  = mBufDecOverflow / mStatUnit;
+    stat->buf_dec_pktloss    = mBufDecPktLoss / mStatUnit;
+    stat->buf_inc_underrun   = mBufIncUnderrun / mStatUnit;
+    stat->buf_inc_compensate = mBufIncCompensate / mStatUnit;
+    stat->broadcast_skew     = mBroadcastSkew;
+    stat->broadcast_delta    = mBroadcastDelta;
+
+    stat->autoq_corr = 0;
+    stat->autoq_rate = 0;
+    return true;
+}
+
+//*******************************************************************************
+void RingBuffer::updateReadStats()
+{
+    --mSkewRaw;
+    mSkewRaw += mReadsNew;
+    mReadsNew = 0;
+    mUnderruns += mUnderrunsNew;
+    mBufIncUnderrun += mUnderrunsNew;
+    mUnderrunsNew = 0;
+    mLevel        = std::ceil(mLevelCur);
+}
diff --git a/src/RingBuffer.h b/src/RingBuffer.h
new file mode 100644 (file)
index 0000000..a35f0b2
--- /dev/null
@@ -0,0 +1,183 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file RingBuffer.h
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+#ifndef __RINGBUFFER_H__
+#define __RINGBUFFER_H__
+
+#include <QMutex>
+#include <QMutexLocker>
+#include <QWaitCondition>
+#include <atomic>
+
+#include "jacktrip_types.h"
+
+// using namespace JackTripNamespace;
+
+/** \brief Provides a ring-buffer (or circular-buffer) that can be written to and read
+ * from asynchronously (blocking) or synchronously (non-blocking).
+ *
+ * The RingBuffer is an array of \b NumSlots slots of memory
+ * each of which is of size \b SlotSize bytes (8-bits). Slots can be read and
+ * written asynchronously/synchronously by multiple threads.
+ */
+class RingBuffer
+{
+   public:
+    /** \brief The class constructor
+     * \param SlotSize Size of one slot in bytes
+     * \param NumSlots Number of slots
+     */
+    RingBuffer(int SlotSize, int NumSlots);
+
+    /** \brief The class destructor
+     */
+    virtual ~RingBuffer();
+
+    /** \brief Insert a slot into the RingBuffer from ptrToSlot. This method will block
+     * until there's space in the buffer.
+     *
+     * The caller is responsible to make sure sizeof(WriteSlot) = SlotSize. This
+     * method should be use when the caller can block against its output, like
+     * sending/receiving UDP packets. It shouldn't be used by audio. For that, use the
+     * insertSlotNonBlocking.
+     * \param ptrToSlot Pointer to slot to insert into the RingBuffer
+     */
+    void insertSlotBlocking(const int8_t* ptrToSlot);
+
+    /** \brief Read a slot from the RingBuffer into ptrToReadSlot. This method will block
+     * until there's space in the buffer.
+     *
+     * The caller is responsible to make sure sizeof(ptrToReadSlot) = SlotSize. This
+     * method should be use when the caller can block against its input, like
+     * sending/receiving UDP packets. It shouldn't be used by audio. For that, use the
+     * readSlotNonBlocking.
+     * \param ptrToReadSlot Pointer to read slot from the RingBuffer
+     */
+    void readSlotBlocking(int8_t* ptrToReadSlot);
+
+    /** \brief Same as insertSlotBlocking but non-blocking (asynchronous)
+     * \param ptrToSlot Pointer to slot to insert into the RingBuffer
+     */
+    virtual bool insertSlotNonBlocking(const int8_t* ptrToSlot, int len, int lostLen,
+                                       int seq_num);
+
+    /** \brief Same as readSlotBlocking but non-blocking (asynchronous)
+     * \param ptrToReadSlot Pointer to read slot from the RingBuffer
+     */
+    virtual void readSlotNonBlocking(int8_t* ptrToReadSlot);
+    virtual void readBroadcastSlot(int8_t* ptrToReadSlot);
+
+    struct IOStat {
+        uint32_t underruns;
+        uint32_t overflows;
+        int32_t skew;
+        int32_t skew_raw;
+        int32_t level;
+        uint32_t buf_dec_overflows;
+        uint32_t buf_dec_pktloss;
+        uint32_t buf_inc_underrun;
+        uint32_t buf_inc_compensate;
+        int32_t broadcast_skew;
+        int32_t broadcast_delta;
+
+        int32_t autoq_corr;
+        int32_t autoq_rate;
+    };
+    virtual bool getStats(IOStat* stat, bool reset);
+
+   protected:
+    /** \brief Sets the memory in the Read Slot when uderrun occurs. By default,
+     * this sets it to 0. Override this method in a subclass for a different behavior.
+     * \param ptrToReadSlot Pointer to read slot from the RingBuffer
+     */
+    virtual void setUnderrunReadSlot(int8_t* ptrToReadSlot);
+
+    /** \brief Uses the last read slot to set the memory in the Read Slot.
+     *
+     * The last read slot is the last packet that arrived, so if no new packets are
+     * received, it keeps looping the same packet. \param ptrToReadSlot Pointer to read
+     * slot from the RingBuffer
+     */
+    virtual void setMemoryInReadSlotWithLastReadSlot(int8_t* ptrToReadSlot);
+
+    /// \brief Resets the ring buffer for reads under-runs non-blocking
+    void underrunReset();
+    /// \brief Resets the ring buffer for writes over-flows non-blocking
+    void overflowReset();
+    /// \brief Helper method to debug, prints member variables to terminal
+    void debugDump() const;
+    void updateReadStats();
+
+    /*const*/ int mSlotSize;   ///< The size of one slot in byes
+    /*const*/ int mNumSlots;   ///< Number of Slots
+    /*const*/ int mTotalSize;  ///< Total size of the mRingBuffer = mSlotSize*mNumSlotss
+    uint32_t mReadPosition;    ///< Read Positions in the RingBuffer (Tail)
+    uint32_t mWritePosition;   ///< Write Position in the RingBuffer (Head)
+    int mFullSlots;            ///< Number of used (full) slots, in slot-size
+    int8_t* mRingBuffer;       ///< 8-bit array of data (1-byte)
+    int8_t* mLastReadSlot;     ///< Last slot read
+
+    // Thread Synchronization Private Members
+    QMutex mMutex;                     ///< Mutex to protect read and write operations
+    QWaitCondition mBufferIsNotFull;   ///< Buffer not full condition to monitor threads
+    QWaitCondition mBufferIsNotEmpty;  ///< Buffer not empty condition to monitor threads
+
+    // IO stat
+    int mStatUnit;
+    uint32_t mUnderruns;
+    uint32_t mOverflows;
+    int32_t mSkewRaw;
+    double mLevelCur;
+    double mLevelDownRate;
+    int32_t mLevel;
+
+    uint32_t mBufDecOverflow;
+    uint32_t mBufDecPktLoss;
+    uint32_t mBufIncUnderrun;
+    uint32_t mBufIncCompensate;
+
+    // temp counters for reads
+    uint32_t mReadsNew;
+    uint32_t mUnderrunsNew;
+    int32_t mSkew0;
+
+    // broadcast counters
+    int32_t mBroadcastSkew;
+    int32_t mBroadcastDelta;
+};
+
+#endif
diff --git a/src/RingBufferWavetable.h b/src/RingBufferWavetable.h
new file mode 100644 (file)
index 0000000..9412f5f
--- /dev/null
@@ -0,0 +1,68 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file RingBufferWavetable.h
+ * \author Juan-Pablo Caceres
+ * \date September 2008
+ */
+
+#ifndef __RINGBUFFERWAVETABLE_H__
+#define __RINGBUFFERWAVETABLE_H__
+
+/** \brief Same as RingBuffer, except that it uses the Wavetable mode for
+ * lost or late packets.
+ */
+class RingBufferWavetable : public RingBuffer
+{
+   public:
+    /** \brief The class constructor
+     * \param SlotSize Size of one slot in bytes
+     * \param NumSlots Number of slots
+     */
+    RingBufferWavetable(int SlotSize, int NumSlots) : RingBuffer(SlotSize, NumSlots) {}
+
+    /** \brief The class destructor
+     */
+    virtual ~RingBufferWavetable() {}
+
+   protected:
+    /** \brief Sets the memory in the Read Slot when uderrun occurs. This loops as a
+     * wavetable in the last received packet.
+     * \param ptrToReadSlot Pointer to read slot from the RingBuffer
+     */
+    virtual void setUnderrunReadSlot(int8_t* ptrToReadSlot)
+    {
+        setMemoryInReadSlotWithLastReadSlot(ptrToReadSlot);
+    }
+};
+
+#endif  //__RINGBUFFERWAVETABLE_H__
diff --git a/src/RtAudioInterface.cpp b/src/RtAudioInterface.cpp
new file mode 100644 (file)
index 0000000..fc17538
--- /dev/null
@@ -0,0 +1,785 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file RtAudioInterface.cpp
+ * \author Juan-Pablo Caceres
+ * \date July 2009
+ */
+
+#include "RtAudioInterface.h"
+
+#include <QString>
+#include <QTextStream>
+#include <cstdlib>
+
+#include "JackTrip.h"
+#include "StereoToMono.h"
+#include "jacktrip_globals.h"
+
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+void RtAudioDevice::print() const
+{
+    std::cout << "[" << RtAudio::getApiDisplayName(this->api) << " - " << this->ID << "]"
+              << ": \"";
+    std::cout << this->name << "\" ";
+    std::cout << "(" << this->inputChannels << " ins, " << this->outputChannels
+              << " outs)" << endl;
+}
+
+//*******************************************************************************
+void RtAudioDevice::printVerbose() const
+{
+    cout << "Audio Device  [" << RtAudio::getApiDisplayName(this->api) << " - "
+         << this->ID << "] : " << this->name << endl;
+    cout << "  Output Channels : " << this->outputChannels << endl;
+    cout << "  Input Channels  : " << this->inputChannels << endl;
+    cout << "  Supported Sampling Rates: ";
+    for (unsigned int i = 0; i < this->sampleRates.size(); i++) {
+        cout << this->sampleRates[i] << " ";
+    }
+    cout << endl;
+    if (this->isDefaultOutput) {
+        cout << "  --Default Output Device--" << endl;
+    }
+    if (this->isDefaultInput) {
+        cout << "  --Default Input Device--" << endl;
+    }
+#if RTAUDIO_VERSION_MAJOR < 6
+    if (this->probed) {
+        cout << "  --Probed Successful--" << endl;
+    }
+#endif
+}
+
+//*******************************************************************************
+bool RtAudioDevice::checkSampleRate(unsigned int srate) const
+{
+    for (unsigned int i = 0; i < this->sampleRates.size(); i++) {
+        if (this->sampleRates[i] == srate)
+            return true;
+    }
+    return false;
+}
+
+//*******************************************************************************
+RtAudioDevice& RtAudioDevice::operator=(const RtAudio::DeviceInfo& info)
+{
+    RtAudio::DeviceInfo::operator=(info);
+    return *this;
+}
+
+//*******************************************************************************
+RtAudioInterface::RtAudioInterface(QVarLengthArray<int> InputChans,
+                                   QVarLengthArray<int> OutputChans,
+                                   inputMixModeT InputMixMode,
+                                   audioBitResolutionT AudioBitResolution,
+                                   bool processWithNetwork, JackTrip* jacktrip)
+    : AudioInterface(InputChans, OutputChans, InputMixMode, AudioBitResolution,
+                     processWithNetwork, jacktrip)
+{
+}
+
+//*******************************************************************************
+RtAudioInterface::~RtAudioInterface() {}
+
+//*******************************************************************************
+void RtAudioInterface::setup(bool verbose)
+{
+    // Initialize Buffer array to read and write audio and members
+    QVarLengthArray<int> in_chans  = getInputChannels();
+    QVarLengthArray<int> out_chans = getOutputChannels();
+
+    uint32_t in_chans_num   = in_chans.size();
+    uint32_t out_chans_num  = out_chans.size();
+    uint32_t in_chans_base  = 0;
+    uint32_t out_chans_base = 0;
+
+    if (in_chans.size() >= 1) {
+        int min = in_chans.at(0);
+        for (int i = 0; i < in_chans.size(); i++) {
+            if (in_chans.at(i) < min) {
+                min = in_chans.at(i);
+            }
+        }
+        if (min >= 0) {
+            in_chans_base = min;
+        }
+    }
+
+    if (out_chans.size() >= 1) {
+        int min = out_chans.at(0);
+        for (int i = 0; i < out_chans.size(); i++) {
+            if (out_chans.at(i) < min) {
+                min = in_chans.at(i);
+            }
+        }
+        if (min >= 0) {
+            out_chans_base = min;
+        }
+    }
+
+    cout << "Setting Up RtAudio Interface" << endl;
+    cout << gPrintSeparator << endl;
+
+    AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_NONE);
+    AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NONE);
+
+    if (mDevices.empty())
+        scanDevices(mDevices);
+
+    RtAudioDevice in_device;
+    RtAudioDevice out_device;
+
+    if (getNumInputDevices() == 0) {
+        AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NO_DEVICES);
+        throw std::runtime_error("no input audio devices found");
+    }
+
+    if (getNumOutputDevices() == 0) {
+        AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NO_DEVICES);
+        throw std::runtime_error("no output audio devices found");
+    }
+
+    // Locate the selected input audio device
+    auto in_name = getInputDevice();
+    if (in_name.empty()) {
+        long default_device_id = getDefaultDevice(true);
+        if (!getDeviceInfoFromId(default_device_id, in_device, true))
+            throw std::runtime_error("default input device not found");
+        cout << "Selected default INPUT device" << endl;
+    } else {
+        if (!getDeviceInfoFromName(in_name, in_device, true)) {
+            throw std::runtime_error("Requested input device \"" + in_name
+                                     + "\" not found.");
+        }
+        cout << "Selected INPUT device " << in_name << endl;
+    }
+
+    // Locate the selected output audio device
+    auto out_name = getOutputDevice();
+    if (out_name.empty()) {
+        long default_device_id = getDefaultDevice(false);
+        if (!getDeviceInfoFromId(default_device_id, out_device, false))
+            throw std::runtime_error("default output device not found");
+        cout << "Selected default OUTPUT device" << endl;
+    } else {
+        if (!getDeviceInfoFromName(out_name, out_device, false)) {
+            throw std::runtime_error("Requested output device \"" + out_name
+                                     + "\" not found.");
+        }
+        cout << "Selected OUTPUT device " << out_name << endl;
+    }
+
+    if (in_device.ID == out_device.ID) {
+        mRtAudioInput.reset(new RtAudio(in_device.api));
+        mRtAudioOutput.reset();
+        mDuplexMode = true;
+    } else {
+        mRtAudioInput.reset(new RtAudio(in_device.api));
+        mRtAudioOutput.reset(new RtAudio(out_device.api));
+        mDuplexMode = false;
+    }
+
+    if (in_chans_base + in_chans_num > in_device.inputChannels) {
+        in_chans_base = 0;
+        in_chans_num  = 2;
+        if (in_device.inputChannels < 2) {
+            in_chans_num = 1;
+        }
+    }
+
+    if (out_chans_base + out_chans_num > out_device.outputChannels) {
+        out_chans_base = 0;
+        out_chans_num  = 2;
+        if (out_device.outputChannels < 2) {
+            out_chans_num = 1;
+        }
+    }
+
+    if (verbose) {
+        if (mDuplexMode) {
+            cout << "DUPLEX DEVICE:" << endl;
+        } else {
+            cout << "INPUT DEVICE:" << endl;
+        }
+        in_device.printVerbose();
+        cout << gPrintSeparator << endl;
+        if (!mDuplexMode) {
+            cout << "OUTPUT DEVICE:" << endl;
+            out_device.printVerbose();
+            cout << gPrintSeparator << endl;
+        }
+    }
+
+    if (!in_device.checkSampleRate(getSampleRate())) {
+        QString errorMsg;
+        QTextStream(&errorMsg) << "Input device \"" << QString::fromStdString(in_name)
+                               << "\" does not support sample rate of "
+                               << getSampleRate();
+        throw std::runtime_error(errorMsg.toStdString());
+    }
+    if (!out_device.checkSampleRate(getSampleRate())) {
+        QString errorMsg;
+        QTextStream(&errorMsg) << "Output device \"" << QString::fromStdString(out_name)
+                               << "\" does not support sample rate of "
+                               << getSampleRate();
+        throw std::runtime_error(errorMsg.toStdString());
+    }
+
+    if (in_device.api == out_device.api) {
+#ifdef _WIN32
+        if (in_device.api != RtAudio::WINDOWS_ASIO) {
+            AudioInterface::setDevicesWarningMsg(
+                AudioInterface::DEVICE_WARN_ASIO_LATENCY);
+            AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NONE);
+        } else if (in_device.api == RtAudio::WINDOWS_ASIO
+                   && in_device.ID != out_device.ID) {
+            AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_NONE);
+            AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_SAME_ASIO);
+        }
+#else
+        if (in_device.api == RtAudio::LINUX_PULSE
+            || in_device.api == RtAudio::LINUX_OSS) {
+            AudioInterface::setDevicesWarningMsg(
+                AudioInterface::DEVICE_WARN_ALSA_LATENCY);
+            AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NONE);
+        }
+#endif
+    } else {
+        AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_NONE);
+        AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_INCOMPATIBLE);
+    }
+
+    RtAudio::StreamParameters in_params, out_params;
+    in_params.deviceId      = in_device.ID;
+    out_params.deviceId     = out_device.ID;
+    in_params.nChannels     = in_chans_num;
+    out_params.nChannels    = out_chans_num;
+    in_params.firstChannel  = in_chans_base;
+    out_params.firstChannel = out_chans_base;
+
+    RtAudio::StreamOptions options;
+    // The second flag affects linux and mac only
+    options.flags = RTAUDIO_NONINTERLEAVED | RTAUDIO_SCHEDULE_REALTIME;
+#ifdef _WIN32
+    options.flags = options.flags | RTAUDIO_MINIMIZE_LATENCY;
+#endif
+    // linux only
+    options.priority   = 30;
+    options.streamName = gJackDefaultClientName;
+
+    // Update parent class
+    QVarLengthArray<int> updatedInputChannels;
+    QVarLengthArray<int> updatedOutputChannels;
+    updatedInputChannels.resize(in_chans_num);
+    updatedOutputChannels.resize(out_chans_num);
+    for (uint32_t i = 0; i < in_chans_num; i++) {
+        updatedInputChannels[i] = in_chans_base + i;
+    }
+    for (uint32_t i = 0; i < out_chans_num; i++) {
+        updatedOutputChannels[i] = out_chans_base + i;
+    }
+    setInputChannels(updatedInputChannels);
+    setOutputChannels(updatedOutputChannels);
+
+    // Setup buffers
+    mInBuffer.resize(in_chans_num);
+    mOutBuffer.resize(out_chans_num);
+
+    unsigned int sampleRate   = getSampleRate();           // mSamplingRate;
+    unsigned int bufferFrames = getBufferSizeInSamples();  // mBufferSize;
+
+    if (in_device.api != out_device.api)
+        return;
+
+    std::string errorText;
+
+    // IMPORTANT NOTE: It's VERY important to remember to pass "this"
+    // to the user data for the process callback, otherwise member won't
+    // be accessible
+
+#if RTAUDIO_VERSION_MAJOR < 6
+    // function pointers used before v6, and lambda conversion to function
+    // pointers does not support capture
+    RtAudioErrorCallback errorFunc = [](RtAudioError::Type type,
+                                        const std::string& errorText) {
+        errorCallback(type, errorText, nullptr);
+    };
+    try {
+        if (mDuplexMode) {
+            mRtAudioInput->openStream(
+                &out_params, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                &RtAudioInterface::wrapperRtAudioCallback, this, &options, errorFunc);
+        } else {
+            mRtAudioInput->openStream(
+                nullptr, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                &RtAudioInterface::wrapperRtAudioCallback, this, &options, errorFunc);
+            const unsigned int inputBufferFrames = bufferFrames;
+            mRtAudioOutput->openStream(
+                &out_params, nullptr, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                &RtAudioInterface::wrapperRtAudioCallback, this, &options, errorFunc);
+            if (inputBufferFrames != bufferFrames) {
+                // output device doesn't support the same buffer size
+                // try to reopen the input device with new size
+                const unsigned int outputBufferFrames = bufferFrames;
+                mRtAudioInput->closeStream();
+                mRtAudioInput->openStream(
+                    nullptr, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                    &RtAudioInterface::wrapperRtAudioCallback, this, &options, errorFunc);
+                if (outputBufferFrames != bufferFrames) {
+                    // just give up if this still doesn't work
+                    errorText = "The two devices selected are incompatible";
+                }
+            }
+        }
+    } catch (RtAudioError& e) {
+        errorText = e.getMessage();
+    }
+#else
+    // we need a wrapper since RtAudio doesn't support void* arguments
+    RtAudioErrorCallback errorFunc = [this](RtAudioErrorType type,
+                                            const std::string& errorText) {
+        errorCallback(type, errorText, this);
+    };
+    mRtAudioInput->setErrorCallback(errorFunc);
+    if (mDuplexMode) {
+        if (RTAUDIO_NO_ERROR
+            != mRtAudioInput->openStream(
+                &out_params, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                &RtAudioInterface::wrapperRtAudioCallback, this, &options)) {
+            errorText = mRtAudioInput->getErrorText();
+        }
+    } else {
+        mRtAudioOutput->setErrorCallback(errorFunc);
+        if (RTAUDIO_NO_ERROR
+            != mRtAudioInput->openStream(
+                nullptr, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                &RtAudioInterface::wrapperRtAudioCallback, this, &options)) {
+            errorText = mRtAudioInput->getErrorText();
+        } else {
+            const unsigned int inputBufferFrames = bufferFrames;
+            if (RTAUDIO_NO_ERROR
+                != mRtAudioOutput->openStream(
+                    &out_params, nullptr, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                    &RtAudioInterface::wrapperRtAudioCallback, this, &options)) {
+                errorText = mRtAudioOutput->getErrorText();
+            } else if (inputBufferFrames != bufferFrames) {
+                // output device doesn't support the same buffer size
+                // try to reopen the input device with new size
+                const unsigned int outputBufferFrames = bufferFrames;
+                mRtAudioInput->closeStream();
+                if (RTAUDIO_NO_ERROR
+                    != mRtAudioInput->openStream(
+                        nullptr, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                        &RtAudioInterface::wrapperRtAudioCallback, this, &options)) {
+                    errorText = mRtAudioInput->getErrorText();
+                } else if (outputBufferFrames != bufferFrames) {
+                    // just give up if this still doesn't work
+                    errorText = "The two devices selected are incompatible";
+                }
+            }
+        }
+    }
+#endif
+
+    if (!errorText.empty()) {
+        std::cerr << "RtAudioInterface failed to open stream: " << errorText << '\n'
+                  << std::endl;
+        mRtAudioInput.reset();
+        mRtAudioOutput.reset();
+        throw std::runtime_error(errorText);
+    }
+
+    // RtAudio::openStream() can return a different buffer size
+    // if the audio interface doesn't support the one that was requested
+    if (bufferFrames != getBufferSizeInSamples()) {
+        std::cout << "RtAudioInterface updated buffer size to " << bufferFrames
+                  << " samples" << std::endl;
+        setBufferSize(bufferFrames);
+    }
+
+    if (highLatencyBufferSize() && getDevicesWarningMsg().empty()) {
+        setDevicesWarningMsg(AudioInterface::DEVICE_WARN_BUFFER_LATENCY);
+    }
+
+    // Setup parent class
+    // This MUST be after buffer size is finalized, so that plugins
+    // are initialized with the correct settings
+    AudioInterface::setup(verbose);
+
+    // Setup StereoToMonoMixer
+    // This MUST be after RtAudio::openSteram in case bufferFrames changes
+    mStereoToMonoMixerPtr.reset(new StereoToMono());
+    mStereoToMonoMixerPtr->init(sampleRate, bufferFrames);
+}
+
+//*******************************************************************************
+void RtAudioInterface::printDevices()
+{
+    QVector<RtAudioDevice> devices;
+    scanDevices(devices);
+}
+
+//*******************************************************************************
+unsigned int RtAudioInterface::getNumInputDevices() const
+{
+    unsigned int deviceCount = 0;
+    for (const RtAudioDevice& d : mDevices) {
+        if (d.inputChannels > 0) {
+            ++deviceCount;
+        }
+    }
+    return deviceCount;
+}
+
+//*******************************************************************************
+unsigned int RtAudioInterface::getNumOutputDevices() const
+{
+    unsigned int deviceCount = 0;
+    for (const RtAudioDevice& d : mDevices) {
+        if (d.outputChannels > 0) {
+            ++deviceCount;
+        }
+    }
+    return deviceCount;
+}
+
+//*******************************************************************************
+void RtAudioInterface::getDeviceIds(RtAudio& rtaudio, std::vector<unsigned int>& ids)
+{
+#if RTAUDIO_VERSION_MAJOR < 6
+    for (unsigned int i = 0; i < rtaudio.getDeviceCount(); i++) {
+        ids.push_back(i);
+    }
+#else
+    ids = rtaudio.getDeviceIds();
+#endif
+}
+
+//*******************************************************************************
+long RtAudioInterface::getDefaultDevice(bool isInput)
+{
+    RtAudio rtaudio;
+
+#if RTAUDIO_VERSION_MAJOR < 6
+    if (rtaudio.getCurrentApi() == RtAudio::LINUX_PULSE) {
+        return getDefaultDeviceForLinuxPulseAudio(isInput);
+    }
+#endif
+
+    long defaultId =
+        isInput ? rtaudio.getDefaultInputDevice() : rtaudio.getDefaultOutputDevice();
+
+#if RTAUDIO_VERSION_MAJOR >= 6
+    // In RtAudio v6, 0 is an invalid device id and used to indicate that no devices are
+    // available
+    if (defaultId == 0)
+        defaultId = -1;
+#endif
+
+    return defaultId;
+}
+
+//*******************************************************************************
+// Use this for getting the default device with PulseAudio
+// At the time of writing this, the latest RtAudio release did not properly
+// select default devices with PulseAudio
+// Once this functinoality is provided upstream and in the distributions'
+// package managers, the following function can be removed and the default device
+// can be obtained by calls to getDefaultInputDevice() / getDefaultOutputDevice()
+long RtAudioInterface::getDefaultDeviceForLinuxPulseAudio(bool isInput)
+{
+    // Iterate devices to find defaults
+    for (const RtAudioDevice& d : mDevices) {
+#if RTAUDIO_VERSION_MAJOR < 6
+        // probed was removed from DeviceInfo in 6.0
+        if (d.probed == false)
+            continue;
+#endif
+        if (d.isDefaultInput && isInput) {
+            return d.ID;
+        } else if (d.isDefaultOutput && !isInput) {
+            return d.ID;
+        }
+    }
+
+    // return the first device if default was not found
+    // this is consistent with RtAudio API
+    return 0;
+}
+
+//*******************************************************************************
+int RtAudioInterface::RtAudioCallback(void* outputBuffer, void* inputBuffer,
+                                      unsigned int nFrames, double /*streamTime*/,
+                                      RtAudioStreamStatus /*status*/)
+{
+    sample_t* inputBuffer_sample  = static_cast<sample_t*>(inputBuffer);
+    sample_t* outputBuffer_sample = static_cast<sample_t*>(outputBuffer);
+    int in_chans_num              = getNumInputChannels();
+
+    if (mDuplexMode) {
+        if (inputBuffer_sample == NULL || outputBuffer_sample == NULL) {
+            return 0;
+        }
+    } else if (inputBuffer_sample == NULL && outputBuffer_sample == NULL) {
+        return 0;
+    }
+
+    // process input before output to minimize monitor latency on duplex devices
+    if (inputBuffer_sample != NULL) {
+        // copy samples to input buffer
+        for (int i = 0; i < mInBuffer.size(); i++) {
+            // Input Ports are READ ONLY
+            mInBuffer[i] = inputBuffer_sample + (nFrames * i);
+        }
+        if (in_chans_num == 2 && mInBuffer.size() == in_chans_num
+            && mInputMixMode == AudioInterface::MIXTOMONO) {
+            mStereoToMonoMixerPtr->compute(nFrames, mInBuffer.data(), mInBuffer.data());
+        }
+        AudioInterface::audioInputCallback(mInBuffer, nFrames);
+    }
+
+    if (outputBuffer_sample != NULL) {
+        // copy samples to output buffer
+        for (int i = 0; i < mOutBuffer.size(); i++) {
+            // Output Ports are WRITABLE
+            mOutBuffer[i] = outputBuffer_sample + (nFrames * i);
+        }
+        AudioInterface::audioOutputCallback(mOutBuffer, nFrames);
+    }
+
+    return 0;
+}
+
+//*******************************************************************************
+int RtAudioInterface::wrapperRtAudioCallback(void* outputBuffer, void* inputBuffer,
+                                             unsigned int nFrames, double streamTime,
+                                             RtAudioStreamStatus status, void* userData)
+{
+    RtAudioInterface* interface = static_cast<RtAudioInterface*>(userData);
+    return interface->RtAudioCallback(outputBuffer, inputBuffer, nFrames, streamTime,
+                                      status);
+}
+
+//*******************************************************************************
+void RtAudioInterface::errorCallback(RtAudioErrorType errorType,
+                                     const std::string& errorText, void* arg)
+{
+#if RTAUDIO_VERSION_MAJOR < 6
+    if (errorType == RtAudioError::WARNING || errorType == RtAudioError::DEBUG_WARNING)
+        return;
+#else
+    if (errorType == RTAUDIO_WARNING)
+        return;
+#endif
+    std::string errorMsg = "RtAudio Error";
+    if (!errorText.empty()) {
+        errorMsg += ": ";
+        errorMsg += errorText;
+    }
+    if (arg != nullptr) {
+        RtAudioInterface* ifPtr = static_cast<RtAudioInterface*>(arg);
+        ifPtr->mErrorMsg        = errorText;
+        if (ifPtr->mErrorCallback) {
+            ifPtr->mErrorCallback(errorText);
+        }
+    }
+    std::cerr << errorMsg << std::endl;
+    JackTrip::sAudioStopped = true;
+}
+
+//*******************************************************************************
+int RtAudioInterface::startProcess()
+{
+    if (mRtAudioInput.isNull())
+        return 0;
+    if (!mDuplexMode && mRtAudioOutput.isNull())
+        return 0;
+
+    std::string errorText;
+
+#if RTAUDIO_VERSION_MAJOR < 6
+    try {
+        mRtAudioInput->startStream();
+        if (!mDuplexMode) {
+            mRtAudioOutput->startStream();
+        }
+    } catch (RtAudioError& e) {
+        errorText = e.getMessage();
+    }
+#else
+    if (RTAUDIO_NO_ERROR != mRtAudioInput->startStream()) {
+        errorText = mRtAudioInput->getErrorText();
+    } else if (!mDuplexMode && RTAUDIO_NO_ERROR != mRtAudioOutput->startStream()) {
+        errorText = mRtAudioOutput->getErrorText();
+    }
+#endif
+
+    if (!errorText.empty()) {
+        std::cerr << "RtAudioInterface failed to start stream: " << errorText
+                  << std::endl;
+        mRtAudioInput.reset();
+        mRtAudioOutput.reset();
+        return (-1);
+    }
+
+    return (0);
+}
+
+//*******************************************************************************
+int RtAudioInterface::stopProcess()
+{
+    if (mRtAudioInput.isNull())
+        return 0;
+    if (!mDuplexMode && mRtAudioOutput.isNull())
+        return 0;
+
+    std::string errorText;
+
+#if RTAUDIO_VERSION_MAJOR < 6
+    try {
+        mRtAudioInput->closeStream();
+        // this causes it to crash for some reason
+        // mRtAudio->abortStream();
+        if (!mDuplexMode) {
+            mRtAudioOutput->closeStream();
+        }
+    } catch (RtAudioError& e) {
+        errorText = e.getMessage();
+    }
+#else
+    if (RTAUDIO_NO_ERROR != mRtAudioInput->abortStream()) {
+        errorText = mRtAudioInput->getErrorText();
+    } else if (!mDuplexMode && RTAUDIO_NO_ERROR != mRtAudioOutput->abortStream()) {
+        errorText = mRtAudioOutput->getErrorText();
+    } else {
+        mRtAudioInput->closeStream();
+        if (!mDuplexMode) {
+            mRtAudioOutput->closeStream();
+        }
+    }
+#endif
+
+    mRtAudioInput.reset();
+    mRtAudioOutput.reset();
+
+    if (!errorText.empty()) {
+        std::cerr << errorText << '\n' << std::endl;
+        return (-1);
+    }
+
+    AudioInterface::setDevicesWarningMsg(AudioInterface::DEVICE_WARN_NONE);
+    AudioInterface::setDevicesErrorMsg(AudioInterface::DEVICE_ERR_NONE);
+
+    return 0;
+}
+
+//*******************************************************************************
+bool RtAudioInterface::getDeviceInfoFromName(const std::string& deviceName,
+                                             RtAudioDevice& device, bool isInput) const
+{
+    // convert names to QString to gracefully handle invalid
+    // utf8 character sequences, such as "RØDE Microphone"
+    const QString utf8Name(QString::fromStdString(deviceName));
+    for (const RtAudioDevice& d : mDevices) {
+        if (utf8Name == QString::fromStdString(d.name)) {
+            if ((isInput && d.inputChannels > 0) || (!isInput && d.outputChannels > 0)) {
+                device = d;
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+//*******************************************************************************
+bool RtAudioInterface::getDeviceInfoFromId(const long deviceId, RtAudioDevice& device,
+                                           [[maybe_unused]] bool isInput) const
+{
+#if RTAUDIO_VERSION_MAJOR < 6
+    if (mDevices.size() > deviceId) {
+        device = mDevices[deviceId];
+        return true;
+    }
+#else
+    if (deviceId < 0)
+        return false;
+    for (const RtAudioDevice& d : mDevices) {
+        if (deviceId == d.ID) {
+            if ((isInput && d.inputChannels > 0) || (!isInput && d.outputChannels > 0)) {
+                device = d;
+                return true;
+            }
+        }
+    }
+#endif
+    return false;
+}
+
+//*******************************************************************************
+void RtAudioInterface::scanDevices(QVector<RtAudioDevice>& devices)
+{
+    std::vector<RtAudio::Api> apis;
+    RtAudio::getCompiledApi(apis);
+    devices.clear();
+
+    std::cout << "RTAudio: scanning devices..." << std::endl;
+
+    for (uint32_t i = 0; i < apis.size(); i++) {
+#ifdef _WIN32
+        if (apis.at(i) == RtAudio::UNIX_JACK) {
+            continue;
+        }
+#endif
+        RtAudio rtaudio(apis.at(i));
+        std::vector<unsigned int> ids;
+        getDeviceIds(rtaudio, ids);
+        for (unsigned int deviceId : ids) {
+            RtAudioDevice device;
+            device     = rtaudio.getDeviceInfo(deviceId);
+            device.api = rtaudio.getCurrentApi();
+#if RTAUDIO_VERSION_MAJOR < 6
+            device.ID = deviceId;
+            // probed was removed from DeviceInfo in 6.0
+            if (device.probed == false)
+                continue;
+#endif
+            if (device.inputChannels == 0 && device.outputChannels == 0)
+                continue;
+            devices.push_back(device);
+            device.print();
+        }
+    }
+}
diff --git a/src/RtAudioInterface.h b/src/RtAudioInterface.h
new file mode 100644 (file)
index 0000000..b58125c
--- /dev/null
@@ -0,0 +1,160 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file RtAudioInterface.h
+ * \author Juan-Pablo Caceres
+ * \date July 2009
+ */
+
+#ifndef __RTAUDIOINTERFACE_H__
+#define __RTAUDIOINTERFACE_H__
+
+#include <RtAudio.h>
+
+#if RTAUDIO_VERSION_MAJOR < 6
+#define RtAudioErrorType RtAudioError::Type
+#endif
+
+#include <QQueue>
+#include <QSharedPointer>
+#include <QString>
+#include <QVector>
+
+#include "AudioInterface.h"
+#include "StereoToMono.h"
+#include "jacktrip_globals.h"
+class JackTrip;  // Forward declaration
+
+/// \brief Simple Class that represents an audio interface available via RtAudio
+class RtAudioDevice : public RtAudio::DeviceInfo
+{
+   public:
+#if RTAUDIO_VERSION_MAJOR < 6
+    unsigned int ID;
+#endif
+    RtAudio::Api api;
+    void print() const;
+    void printVerbose() const;
+    bool checkSampleRate(unsigned int srate) const;
+    RtAudioDevice& operator=(const RtAudio::DeviceInfo& info);
+};
+
+/// \brief Base Class that provides an interface with RtAudio
+class RtAudioInterface : public AudioInterface
+{
+   public:
+    /** \brief The class constructor
+     * \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(QVarLengthArray<int> InputChans, QVarLengthArray<int> OutputChans,
+                     inputMixModeT InputMixMode             = AudioInterface::MIX_UNSET,
+                     audioBitResolutionT AudioBitResolution = BIT16,
+                     bool processWithNetwork = false, JackTrip* jacktrip = nullptr);
+    /// \brief The class destructor
+    virtual ~RtAudioInterface();
+
+    /// \brief List all available audio interfaces, with its properties
+    static void printDevices();
+    virtual void setup(bool verbose = true);
+    virtual int startProcess();
+    virtual int stopProcess();
+    /// \brief This has no effect in RtAudio
+    virtual void connectDefaultPorts() {}
+
+    // returns number of available input audio devices
+    unsigned int getNumInputDevices() const;
+
+    // returns number of available output audio devices
+    unsigned int getNumOutputDevices() const;
+
+    // populates the ids vector with ids for all known devices
+    // for RtAudio v5 these are just incrementing numbers starting at 0,
+    // while RtAudio v6 uses unique ids for each device that may not correspond with
+    // the index location within the vector
+    static void getDeviceIds(RtAudio& rtaudio, std::vector<unsigned int>& ids);
+
+    // populates devices with all available audio interfaces
+    static void scanDevices(QVector<RtAudioDevice>& devices);
+
+    // sets devices to available audio interfaces
+    void setRtAudioDevices(QVector<RtAudioDevice>& devices) { mDevices = devices; }
+
+    // returns all available audio devices
+    inline void getRtAudioDevices(QVector<RtAudioDevice>& d) const { d = mDevices; }
+
+    //--------------SETTERS---------------------------------------------
+    /// \brief This has no effect in RtAudio
+    virtual void setClientName(const QString& /*ClientName*/) {}
+    //------------------------------------------------------------------
+
+    //--------------GETTERS---------------------------------------------
+    //------------------------------------------------------------------
+   private:
+    int RtAudioCallback(void* outputBuffer, void* inputBuffer, unsigned int nFrames,
+                        double streamTime, RtAudioStreamStatus status);
+    static int wrapperRtAudioCallback(void* outputBuffer, void* inputBuffer,
+                                      unsigned int nFrames, double streamTime,
+                                      RtAudioStreamStatus status, void* userData);
+    static void errorCallback(RtAudioErrorType type, const std::string& errorText,
+                              void* arg = nullptr);
+
+    // retrieves info about an audio device by search for its name
+    // updates device and returns true if found
+    bool getDeviceInfoFromName(const std::string& deviceName, RtAudioDevice& device,
+                               bool isInput) const;
+
+    // retrieves info about an audio device by search for its id
+    // updates device and returns true if found
+    bool getDeviceInfoFromId(const long deviceId, RtAudioDevice& device,
+                             bool isInput) const;
+    long getDefaultDevice(bool isInput);
+    long getDefaultDeviceForLinuxPulseAudio(bool isInput);
+
+    QVarLengthArray<float*>
+        mInBuffer;  ///< Vector of Input buffers/channel read from JACK
+    QVarLengthArray<float*>
+        mOutBuffer;  ///< Vector of Output buffer/channel to write to JACK
+    QVector<RtAudioDevice>
+        mDevices;  ///< Vector of audio interfaces available via RTAudio
+    QSharedPointer<RtAudio> mRtAudioInput;   ///< RtAudio class for the input device
+    QSharedPointer<RtAudio> mRtAudioOutput;  ///< RtAudio class for the output device
+                                             ///< (null if using duplex mode)
+    bool mDuplexMode;  ///< true if using duplex stream mode (input device == output
+                       ///< device)
+    QScopedPointer<StereoToMono> mStereoToMonoMixerPtr;
+};
+
+#endif  // __RTAUDIOINTERFACE_H__
diff --git a/src/Settings.cpp b/src/Settings.cpp
new file mode 100644 (file)
index 0000000..28ffb13
--- /dev/null
@@ -0,0 +1,1297 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file Settings.cpp
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+#include "Settings.h"
+
+#include "LoopBack.h"
+// #include "NetKS.h"
+#include "Effects.h"
+
+#ifdef WAIR  // wair
+#include "Stk16.dsp.h"
+#include "ap8x2.dsp.h"
+#endif  // endwhere
+
+// #include "JackTripWorker.h"
+#include <getopt.h>  // for command line parsing
+
+#include <cassert>
+#include <cctype>
+#include <cstdlib>
+#include <iostream>
+
+#include "jacktrip_globals.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#else
+#include <termios.h>
+#include <unistd.h>
+#endif
+
+#ifdef RT_AUDIO
+#include "RtAudioInterface.h"
+#endif
+
+#ifdef JACKTRIP_BUILD_INFO
+#define STR(s)           #s
+#define TO_STRING(s)     STR(s)
+#define PRINT_BUILD_INFO cout << "Build Info: " << TO_STRING(JACKTRIP_BUILD_INFO) << endl;
+#endif
+
+// #include "ThreadPoolTest.h"
+
+using std::cout;
+using std::endl;
+
+int gVerboseFlag = 0;
+
+enum JTLongOptIDS {
+    OPT_BUFSTRATEGY = 1001,
+    OPT_SIMLOSS,
+    OPT_SIMJITTER,
+    OPT_BROADCAST,
+    OPT_RTUDPPRIORITY,
+    OPT_AUTHCERT,
+    OPT_AUTHKEY,
+    OPT_AUTHCREDS,
+    OPT_AUTHUSER,
+    OPT_AUTHPASS,
+    OPT_NUMRECEIVE,
+    OPT_NUMSEND,
+    OPT_APPENDTHREADID,
+    OPT_LISTDEVICES,
+    OPT_AUDIODEVICE,
+    OPT_AUDIOINPUTDEVICE,
+    OPT_AUDIOOUTPUTDEVICE,
+    OPT_GUI,
+    OPT_CLASSIC_GUI,
+    OPT_DEEPLINK
+};
+
+//*******************************************************************************
+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 && !mGuiEnabled) {
+        printUsage();
+        std::exit(0);
+    }
+
+    // Usage example at:
+    // http://www.gnu.org/software/libc/manual/html_node/Getopt-Long-Option-Example.html#Getopt-Long-Option-Example
+    // options descriptor
+    //----------------------------------------------------------------------------
+    static struct option longopts[] = {
+        // These options don't set a flag.
+        {"numchannels", required_argument, NULL,
+         'n'},  // Number of input and output channels
+        {"receivechannels", required_argument, NULL,
+         OPT_NUMRECEIVE},  // Number of incoming channels
+        {"sendchannels", required_argument, NULL,
+         OPT_NUMSEND},                     // Number of outgoing channels
+#ifdef WAIR                                // WAIR
+        {"wair", no_argument, NULL, 'w'},  // Run in LAIR mode, sets numnetrevchannels
+        {"addcombfilterlength", required_argument, NULL,
+         'N'},                                                 // added comb filter length
+        {"combfilterfeedback", required_argument, NULL, 'H'},  // comb filter feedback
+#endif                                                         // endwhere
+        {"server", no_argument, NULL, 's'},                    // Run in P2P server mode
+        {"client", required_argument, NULL,
+         'c'},  // Run in P2P client mode, set server IP address
+        {"localaddress", required_argument, NULL,
+         'L'},  // set local address e.g., 127.0.0.2 for second instance on same host
+        {"jacktripserver", no_argument, NULL, 'S'},  // Run in JamLink mode
+        {"pingtoserver", required_argument, NULL,
+         'C'},  // Run in ping to server mode, set server IP address
+        {"portoffset", required_argument, NULL, 'o'},  // Port Offset from 4464
+        {"bindport", required_argument, NULL, 'B'},    // Port Offset from 4464
+        {"peerport", required_argument, NULL, 'P'},    // Port Offset from 4464
+        {"udpbaseport", required_argument, NULL,
+         'U'},  // Server udp base port (defaults to 61002)
+        {"queue", required_argument, NULL, 'q'},       // Queue Length
+        {"redundancy", required_argument, NULL, 'r'},  // Redundancy
+        {"bitres", required_argument, NULL, 'b'},      // Audio Bit Resolution
+        {"zerounderrun", no_argument, NULL, 'z'},      // Use Underrun to Zeros Mode
+        {"timeout", no_argument, NULL, 't'},      // Quit after 10 second network timeout
+        {"loopback", no_argument, NULL, 'l'},     // Run in loopback mode
+        {"jamlink", no_argument, NULL, 'j'},      // Run in JamLink mode
+        {"emptyheader", no_argument, NULL, 'e'},  // Run in JamLink mode
+        {"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
+        {"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
+        {"deviceid", required_argument, NULL,
+         'd'},  // Set RTAudio device id to use (DEPRECATED)
+        {"audiodevice", required_argument, NULL,
+         OPT_AUDIODEVICE},  // Set RTAudio devices by name
+        {"audioinputdevice", required_argument, NULL,
+         OPT_AUDIOINPUTDEVICE},  // Set RTAudio input device by name
+        {"audiooutputdevice", required_argument, NULL,
+         OPT_AUDIOOUTPUTDEVICE},  // Set RTAudio output device by name
+        {"listdevices", no_argument, NULL,
+         OPT_LISTDEVICES},  // Set RTAudio device id to use
+#endif
+        {"nojackportsconnect", no_argument, NULL,
+         'D'},                                // Don't connect default Audio Ports
+        {"version", no_argument, NULL, 'v'},  // Version Number
+        {"verbose", no_argument, NULL, 'V'},  // Verbose mode
+        {"hubpatch", required_argument, NULL,
+         'p'},  // Set hubConnectionMode for auto patch in Jack
+        {"includeserver", no_argument, NULL, 'i'},    // Include server audio in patch
+        {"upmix", no_argument, NULL, 'u'},            // Upmix mono clients when patching
+        {"iostat", required_argument, NULL, 'I'},     // Set IO stat timeout
+        {"iostatlog", required_argument, NULL, 'G'},  // Set IO stat log file
+        {"effects", required_argument, NULL,
+         'f'},  // Turn on outgoing compressor and incoming reverb, reverbLevel arg
+        {"overflowlimiting", required_argument, NULL,
+         'O'},  // Turn On limiter, cases 'i', 'o', 'io'
+        {"assumednumclients", required_argument, NULL,
+         'a'},  // assumed number of clients (sound sources) (otherwise 2)
+        {"bufstrategy", required_argument, NULL, OPT_BUFSTRATEGY},  // Set bufstrategy
+        {"simloss", required_argument, NULL, OPT_SIMLOSS},
+        {"simjitter", required_argument, NULL, OPT_SIMJITTER},
+        {"broadcast", required_argument, NULL, OPT_BROADCAST},
+        {"udprt", no_argument, NULL, OPT_RTUDPPRIORITY},
+        {"auth", no_argument, NULL,
+         'A'},  // Enable authentication between hub client and hub server
+        {"certfile", required_argument, NULL,
+         OPT_AUTHCERT},  // Certificate for server authentication
+        {"keyfile", required_argument, NULL,
+         OPT_AUTHKEY},  // Private key for server authentication
+        {"credsfile", required_argument, NULL,
+         OPT_AUTHCREDS},  // Username and password store for server authentication
+        {"username", optional_argument, NULL,
+         OPT_AUTHUSER},  // Username when using authentication as a hub client
+        {"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
+        {"classic-gui", no_argument, NULL, OPT_CLASSIC_GUI},  // Force Classic Mode GUI
+        {"deeplink", optional_argument, NULL,
+         OPT_DEEPLINK},  // Deeplink URL (should be in the form jacktrip://...)
+        {NULL, 0, NULL, 0}};
+
+    // Parse Command Line Arguments
+    //----------------------------------------------------------------------------
+    /// \todo Specify mandatory arguments
+    int ch;
+    while ((ch = getopt_long(
+                argc, argv,
+                "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) {
+        switch (ch) {
+        case OPT_NUMRECEIVE:
+            if (0 < atoi(optarg)) {
+                mNumAudioOutputChans = atoi(optarg);
+            } else {
+                std::cerr << "--receivechannels ERROR: Number of channels must be "
+                             "greater than 0\n";
+                std::exit(1);
+            }
+            break;
+        case OPT_NUMSEND:
+            if (0 < atoi(optarg)) {
+                mNumAudioInputChans = atoi(optarg);
+            } else {
+                std::cerr << "--sendchannels ERROR: Number of channels must be greater "
+                             "than 0\n";
+                std::exit(1);
+            }
+            break;
+        case 'n':  // Number of input and output channels
+            //-------------------------------------------------------
+            if (0 < atoi(optarg)) {
+                mNumAudioInputChans  = atoi(optarg);
+                mNumAudioOutputChans = atoi(optarg);
+            } else {
+                std::cerr << "-n --numchannels ERROR: Number of channels must be greater "
+                             "than 0\n";
+                std::exit(1);
+            }
+            break;
+        case 'U':  // UDP Bind Port
+            mServerUdpPortNum = atoi(optarg);
+            break;
+#ifdef WAIR
+        case 'w':
+            //-------------------------------------------------------
+            mWAIR = true;
+            mNumNetRevChans =
+                gDefaultNumNetRevChannels;  // fixed amount sets number of network
+                                            // channels and comb filters for WAIR
+            break;
+        case 'N':
+            //-------------------------------------------------------
+            mClientAddCombLen = atoi(optarg);  // cmd line comb length adjustment
+            break;
+        case 'H':  // comb feedback adjustment
+            //-------------------------------------------------------
+            mClientRoomSize = atof(optarg);  // cmd line comb feedback adjustment
+            break;
+#endif             // endwhere
+        case 's':  // Run in P2P server mode
+            //-------------------------------------------------------
+            mJackTripMode = JackTrip::SERVER;
+            checkMode();
+            break;
+        case 'S':  // Run in Hub server mode
+            //-------------------------------------------------------
+            mJackTripMode = JackTrip::SERVERPINGSERVER;
+            checkMode();
+            break;
+        case 'c':  // P2P client mode
+            //-------------------------------------------------------
+            mJackTripMode = JackTrip::CLIENT;
+            mPeerAddress  = optarg;
+            checkMode();
+            break;
+        case 'L':  // set optional local host address
+            //-------------------------------------------------------
+            mGuiIgnoresArguments = true;
+            mLocalAddress        = optarg;
+            break;
+        case 'C':  // Ping to server
+            //-------------------------------------------------------
+            mJackTripMode = JackTrip::CLIENTTOPINGSERVER;
+            mPeerAddress  = optarg;
+            checkMode();
+            break;
+        case 'o':  // Port Offset
+            //-------------------------------------------------------
+            mBindPortNum += atoi(optarg);
+            mPeerPortNum += atoi(optarg);
+            if (gVerboseFlag)
+                std::cout << "SETTINGS: argument parsed for TCP Bind Port: "
+                          << mBindPortNum << std::endl;
+            if (gVerboseFlag)
+                std::cout << "SETTINGS: argument parsed for TCP Peer Port: "
+                          << mPeerPortNum << std::endl;
+            break;
+        case 'B':  // Bind Port
+            //-------------------------------------------------------
+            mBindPortNum = atoi(optarg);
+            if (gVerboseFlag)
+                std::cout << "SETTINGS: argument parsed for TCP Bind Port: "
+                          << mBindPortNum << std::endl;
+            break;
+        case 'P':  // Peer Port
+            //-------------------------------------------------------
+            mPeerPortNum = atoi(optarg);
+            if (gVerboseFlag)
+                std::cout << "SETTINGS: argument parsed for TCP Peer Port: "
+                          << mPeerPortNum << std::endl;
+            break;
+        case 'b':
+            //-------------------------------------------------------
+            if (atoi(optarg) == 8) {
+                mAudioBitResolution = AudioInterface::BIT8;
+            } else if (atoi(optarg) == 16) {
+                mAudioBitResolution = AudioInterface::BIT16;
+            } else if (atoi(optarg) == 24) {
+                mAudioBitResolution = AudioInterface::BIT24;
+            } else if (atoi(optarg) == 32) {
+                mAudioBitResolution = AudioInterface::BIT32;
+            } else {
+                printUsage();
+                std::cerr << "--bitres ERROR: Bit resolution: " << atoi(optarg)
+                          << " is not supported." << endl;
+                std::exit(1);
+            }
+            break;
+        case 'q':
+            //-------------------------------------------------------
+            if (0 == strncmp(optarg, "auto", 4)) {
+                if (optarg[4] == 0) {
+                    mBufferQueueLength = -500;
+                } else {
+                    mBufferQueueLength = -atoi(optarg + 4);
+                }
+            } else if (atoi(optarg) <= 0) {
+                printUsage();
+                std::cerr << "--queue ERROR: The queue has to be equal or greater than 2"
+                          << endl;
+                std::exit(1);
+            } else {
+                mBufferQueueLength = atoi(optarg);
+            }
+            break;
+        case 'r':
+            //-------------------------------------------------------
+            if (atoi(optarg) <= 0) {
+                printUsage();
+                std::cerr
+                    << "--redundancy ERROR: The redundancy has to be a positive integer"
+                    << endl;
+                std::exit(1);
+            } else {
+                mRedundancy = atoi(optarg);
+            }
+            break;
+        case 'z':  // underrun to zero
+            //-------------------------------------------------------
+            mUnderrunMode = JackTrip::ZEROS;
+            break;
+        case 't':  // quit on timeout
+            mStopOnTimeout = true;
+            break;
+        case 'l':  // loopback
+            //-------------------------------------------------------
+            mGuiIgnoresArguments = true;
+            mLoopBack            = true;
+            break;
+        case 'e':  // jamlink
+            //-------------------------------------------------------
+            mGuiIgnoresArguments = true;
+            mEmptyHeader         = true;
+            break;
+        case 'j':  // jamlink
+            //-------------------------------------------------------
+            mGuiIgnoresArguments = true;
+            mJamLink             = true;
+            break;
+        case 'J':  // Set client Name
+            //-------------------------------------------------------
+            mClientName = optarg;
+            break;
+        case 'K':  // Set Remote client Name
+            //-------------------------------------------------------
+            mRemoteClientName = optarg;
+            break;
+        case OPT_APPENDTHREADID:
+            mAppendThreadID = true;
+            break;
+        case 'T':  // Sampling Rate
+            //-------------------------------------------------------
+            mGuiIgnoresArguments = true;
+            mChangeDefaultSR     = true;
+            mSampleRate          = atoi(optarg);
+            break;
+        case 'F':  // Buffer Size
+            //-------------------------------------------------------
+            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 OPT_AUDIODEVICE:  // Set audio device
+            //-------------------------------------------------------
+            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:
+            mGuiIgnoresArguments = true;
+            mInputDeviceName     = optarg;
+            break;
+        case OPT_AUDIOOUTPUTDEVICE:
+            mGuiIgnoresArguments = true;
+            mOutputDeviceName    = optarg;
+            break;
+        case OPT_LISTDEVICES:  // List audio devices
+            //-------------------------------------------------------
+            mGuiIgnoresArguments = true;
+            if (!mGuiEnabled) {
+                RtAudioInterface::printDevices();
+                std::exit(0);
+            }
+            break;
+#endif
+        case 'D':
+            //-------------------------------------------------------
+            mConnectDefaultAudioPorts = false;
+            break;
+        case 'v':
+            //-------------------------------------------------------
+            cout << "JackTrip VERSION: " << gVersion << endl;
+#ifdef JACKTRIP_BUILD_INFO
+            PRINT_BUILD_INFO
+#endif
+            cout << "Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe." << endl;
+            cout << "SoundWIRE group at CCRMA, Stanford University" << endl;
+#ifdef QT_OPENSOURCE
+            cout << "This build of JackTrip is subject to LGPL license." << endl;
+#endif
+            cout << "JackTrip source code is released under MIT and GPL licenses."
+                 << endl;
+            cout << "See LICENSE.md file for more information." << endl;
+            cout << "" << endl;
+            std::exit(0);
+            break;
+        case 'V':
+            //-------------------------------------------------------
+            gVerboseFlag = true;
+            if (gVerboseFlag)
+                std::cout << "Verbose mode" << std::endl;
+            mEffects.setVerboseFlag(gVerboseFlag);
+            break;
+        case 'p':
+            //-------------------------------------------------------
+            if (atoi(optarg) == 0) {
+                mHubConnectionMode = JackTrip::SERVERTOCLIENT;
+            } else if (atoi(optarg) == 1) {
+                mHubConnectionMode = JackTrip::CLIENTECHO;
+            } else if (atoi(optarg) == 2) {
+                mHubConnectionMode = JackTrip::CLIENTFOFI;
+            } else if (atoi(optarg) == 3) {
+                mHubConnectionMode = JackTrip::RESERVEDMATRIX;
+            } else if (atoi(optarg) == 4) {
+                mHubConnectionMode = JackTrip::FULLMIX;
+            } else if (atoi(optarg) == 5) {
+                mHubConnectionMode = JackTrip::NOAUTO;
+            } else {
+                printUsage();
+                std::cerr << "-p ERROR: Wrong HubConnectionMode: " << atoi(optarg)
+                          << " is not supported." << endl;
+                std::exit(1);
+            }
+            break;
+        case 'i':
+            mPatchServerAudio = true;
+            break;
+        case 'u':
+            mStereoUpmix = true;
+            break;
+        case 'I':  // IO Stat timeout
+            //-------------------------------------------------------
+            mGuiIgnoresArguments = true;
+            mIOStatTimeout       = atoi(optarg);
+            if (0 > mIOStatTimeout && !mGuiEnabled) {
+                printUsage();
+                std::cerr << "--iostat ERROR: negative timeout." << endl;
+                std::exit(1);
+            }
+            break;
+        case 'G':  // IO Stat log file
+            //-------------------------------------------------------
+            {
+                mGuiIgnoresArguments = true;
+                if (mGuiEnabled) {
+                    break;
+                }
+                std::ofstream* outStream = new std::ofstream(optarg);
+                if (!outStream->is_open()) {
+                    printUsage();
+                    std::cerr << "--iostatlog FAILED to open " << optarg
+                              << " for writing." << endl;
+                    std::exit(1);
+                }
+                mIOStatStream.reset(outStream);
+            }
+            break;
+        case OPT_BUFSTRATEGY:  // Buf strategy
+            mBufferStrategy = atoi(optarg);
+            if (-1 > mBufferStrategy || 4 < mBufferStrategy) {
+                std::cerr << "Unsupported buffer strategy " << optarg << endl;
+                printUsage();
+                std::exit(1);
+            }
+            break;
+        case OPT_SIMLOSS:  // Simulate packet loss
+            mGuiIgnoresArguments = true;
+            mSimulatedLossRate   = atof(optarg);
+            break;
+        case OPT_SIMJITTER:  // Simulate jitter
+            mGuiIgnoresArguments = true;
+            char* endp;
+            mSimulatedJitterRate = strtod(optarg, &endp);
+            if (0 == *endp) {
+                mSimulatedDelayRel = 1.0;
+            } else {
+                mSimulatedDelayRel = atof(endp + 1);
+            }
+            break;
+        case OPT_BROADCAST:  // Broadcast output
+            mBroadcastQueue = atoi(optarg);
+            break;
+        case OPT_RTUDPPRIORITY:  // Use RT priority for UDPDataProtocol thread
+            mUseRtUdpPriority = true;
+            break;
+        case 'h':
+            //-------------------------------------------------------
+            printUsage();
+            std::exit(0);
+            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);
+            }
+            int returnCode = mEffects.parseLimiterOptArg(cmd, optarg);
+            if (returnCode > 1) {
+                mEffects.printHelp(cmd, ch);
+                std::cerr << cmd << " required argument `" << optarg
+                          << "' is malformed\n";
+                std::exit(1);
+            } else if (returnCode == 1) {
+                std::exit(0);  // benign but not continuing such as "help"
+            }
+            break;
+        }
+        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);
+            }
+            int returnCode = mEffects.parseAssumedNumClientsOptArg(cmd, optarg);
+            if (returnCode > 1) {
+                mEffects.printHelp(cmd, ch);
+                std::cerr << cmd << " required argument `" << optarg
+                          << "' is malformed\n";
+                std::exit(1);
+            } else if (returnCode == 1) {
+                std::exit(0);  // help printed
+            }
+            break;
+        }
+        case 'f': {  // --effects (-f) effectsSpecArg
+            //-------------------------------------------------------
+            mGuiIgnoresArguments = true;
+            if (mGuiEnabled) {
+                break;
+            }
+            char cmd[]{"--effects (-f)"};
+            int returnCode = mEffects.parseEffectsOptArg(cmd, optarg);
+            if (returnCode > 1) {
+                mEffects.printHelp(cmd, ch);
+                std::cerr << cmd << " required argument `" << optarg
+                          << "' is malformed\n";
+                std::exit(1);
+            } else if (returnCode == 1) {
+                std::exit(0);  // something benign but non-continuing like "help"
+            }
+            break;
+        }
+        case 'A':
+            mAuth = true;
+            break;
+        case OPT_AUTHCERT:
+            mCertFile = optarg;
+            break;
+        case OPT_AUTHKEY:
+            mKeyFile = optarg;
+            break;
+        case OPT_AUTHCREDS:
+            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);
+                std::exit(0);
+            }
+            mAudioTester->setEnabled(true);
+            if (optarg == 0 || optarg[0] == '-'
+                || optarg[0] == 0) {  // happens when no -f argument specified
+                printUsage();
+                std::cerr << cmd
+                          << " ERROR: Print-interval argument REQUIRED (set to 0.0 to "
+                             "see every delay)\n";
+                std::exit(1);
+            }
+            mAudioTester->setPrintIntervalSec(atof(optarg));
+            break;
+        }
+        // The following option needs to be handled earlier, so are all parsed in
+        // main. Included here so that we don't get an unrecognized option error.
+        case OPT_GUI:
+            break;
+        case OPT_CLASSIC_GUI:
+            mGuiForceClassicMode = true;
+            break;
+        case OPT_DEEPLINK:
+            if (optarg == NULL && optind < argc && argv[optind][0] != '-') {
+                optarg = argv[optind++];
+            }
+            mDeeplink = optarg;
+            break;
+        case ':': {
+            printUsage();
+            printf("*** Missing option argument *** see above for usage\n\n");
+            break;
+        }
+        case '?': {
+            printUsage();
+            printf(
+                "*** Unknown, missing, or ambiguous option argument *** see above for "
+                "usage\n\n");
+            std::exit(1);
+            break;
+        }
+        default: {
+            //-------------------------------------------------------
+            printUsage();
+            printf("*** Unrecognized option -%c *** see above for usage\n", ch);
+            std::exit(1);
+            break;
+        }
+        }
+    }
+
+    // allow deeplink in command line without option
+    if (optind < argc && strncmp(argv[optind], "jacktrip://", 11) == 0) {
+        mDeeplink = argv[optind];
+        optind++;
+    }
+
+    // Warn user if undefined options where entered
+    //----------------------------------------------------------------------------
+    if (optind < argc) {
+        if (strncmp(argv[optind], "help", 4) != 0) {
+            cout << gPrintSeparator << endl;
+            cout << "*** Unexpected command-line argument(s): ";
+            for (; optind < argc; optind++) {
+                cout << argv[optind] << " ";
+            }
+            cout << endl << gPrintSeparator << endl;
+        }
+        printUsage();
+        std::exit(1);
+    }
+
+    if (true == mAudioTester->getEnabled()) {
+        assert(mNumAudioInputChans > 0);
+        mAudioTester->setSendChannel(mNumAudioInputChans
+                                     - 1);  // use last channel for latency testing
+        // Originally, testing only in the last channel was adopted
+        // because channel 0 ("left") was a clap track on CCRMA loopback
+        // servers.  Now, however, we also do it in order to easily keep
+        // effects in all but the last channel, enabling silent testing
+        // in the last channel in parallel with normal operation of the others.
+    }
+    // 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) {
+        std::cerr << "*** --effects (-f) ERROR: Effects not yet supported server modes "
+                     "(-S and -s).\n\n";
+        std::exit(1);
+    }
+    if (mEffects.getHaveLimiter() && haveSomeServerMode) {
+        if (mEffects.getLimit()
+            != Effects::LIMITER_MODE::LIMITER_OUTGOING) {  // default case
+            std::cerr << "*** --overflowlimiting (-O) ERROR: Limiters not yet supported "
+                         "server modes (-S and -s).\n\n";
+        }
+        mEffects.setNoLimiters();
+        // don't exit since an outgoing limiter should be the default (could exit for
+        // incoming case): std::exit(1);
+    }
+    if (mAudioTester->getEnabled() && haveSomeServerMode) {
+        std::cerr << "*** --examine-audio-delay (-x) ERROR: Audio latency measurement "
+                     "not supported in server modes (-S and -s)\n\n";
+        std::exit(1);
+    }
+    if (mAudioTester->getEnabled() && (mAudioBitResolution != AudioInterface::BIT16)
+        && (mAudioBitResolution
+            != AudioInterface::BIT32)) {  // BIT32 not tested but should be ok
+        // BIT24 should work also, but there's a comment saying it's broken right now, so
+        // exclude it
+        std::cerr << "*** --examine-audio-delay (-x) ERROR: Only --bitres (-b) 16 and 32 "
+                     "presently supported for audio latency measurement.\n\n";
+        std::exit(1);
+    }
+}
+
+//*******************************************************************************
+void Settings::printUsage()
+{
+    // clang-format off
+    cout << "" << endl;
+    cout << "JackTrip: A System for High-Quality Audio Network Performance" << endl;
+    cout << "over the Internet" << endl;
+    cout << "Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe." << endl;
+    cout << "SoundWIRE group at CCRMA, Stanford University" << endl;
+#ifdef QT_OPENSOURCE
+    cout << "This build of JackTrip is subject to LGPL license." << endl;
+#endif
+    cout << "JackTrip source code is released under MIT and GPL licenses." << endl;
+    cout << "See LICENSE.md file for more information." << endl;
+    cout << "VERSION: " << gVersion << endl;
+    cout << "" << endl;
+    cout << "Usage: jacktrip [-s|-c|-S|-C hostIPAddressOrURL] [options]" << endl;
+    cout << "" << endl;
+    cout << "Options: " << endl;
+    cout << "REQUIRED ARGUMENTS: One of:" << endl;
+    cout << " -s, --server                             Run in P2P Server Mode" << endl;
+    cout << " -c, --client <peer_hostname_or_IP_num>   Run in P2P Client Mode" << endl;
+    cout << " -S, --jacktripserver                     Run in Hub Server Mode" << endl;
+    cout << " -C, --pingtoserver <peer_name_or_IP>     Run in Hub Client Mode" << endl;
+    cout << endl;
+    cout << "OPTIONAL ARGUMENTS: " << endl;
+    cout << " -n, --numchannels #                      Number of Input and Output "
+            "Channels (# greater than 0, default: "
+         << 2 << ")" << endl;
+    cout << "     --receivechannels #                       Number of receive Channels from "
+            "the network (# greater than 0)\n";
+    cout << "     --sendchannels #                          Number of send Channels to "
+            "the network (# greater than 0)\n";
+#ifdef WAIR  // WAIR
+    cout << " -w, --wair                               Run in WAIR Mode" << endl;
+    cout << " -N, --addcombfilterlength #              comb length adjustment for WAIR "
+            "(default "
+         << gDefaultAddCombFilterLength << ")" << endl;
+    cout << " -H, --combfilterfeedback # (roomSize)    comb feedback adjustment for WAIR "
+            "(default "
+         << gDefaultCombFilterFeedback << ")" << endl;
+#endif  // endwhere
+    cout << " -q, --queue # (2 or more)                Queue Buffer Length, in Packet "
+            "Size (default: "
+         << gDefaultQueueLength << ")" << endl;
+    cout << " -r, --redundancy # (1 or more)           Packet Redundancy to avoid "
+            "glitches with packet losses (default: 1)"
+         << endl;
+    cout << " -o, --portoffset #                       Receiving bind port and peer port "
+            "offset from default "
+         << gDefaultPort << endl;
+    cout << " -B, --bindport #                         Set only the bind port number "
+            "(default: "
+         << gDefaultPort << ")" << endl;
+    cout << " -P, --peerport #                         Set only the peer port number "
+            "(default: "
+         << gDefaultPort << ")" << endl;
+    cout << " -U, --udpbaseport                        Set only the server udp base port "
+            "number (default: 61002)"
+         << endl;
+    cout << " -b, --bitres # (8, 16, 24, 32)           Audio Bit Rate Resolutions "
+            "(default: 16, 32 uses floating-point)"
+         << endl;
+    cout << " -z, --zerounderrun                       Set buffer to zeros when underrun "
+            "occurs (default: wavetable)"
+         << endl;
+    cout << " -t, --timeout                            Quit after 10 seconds of no "
+            "network activity"
+         << endl;
+    cout << " -l, --loopback                           Run in Loop-Back Mode" << endl;
+    cout << " -j, --jamlink                            Run in JamLink Mode (Connect to a "
+            "JamLink Box)"
+         << endl;
+    cout << " -J, --clientname                         Change default client name "
+            "(default: JackTrip)"
+         << endl;
+    cout << " -K, --remotename                         Change default remote client name "
+            "when connecting to a hub server (the default is derived from this "
+            "computer's external facing IP address)"
+         << endl;
+    cout << " --appendthreadid                         Append thread ID to client names\n";
+    cout << " -L, --localaddress                       Change default local host IP "
+            "address (default: 127.0.0.1)"
+         << endl;
+    cout << " -D, --nojackportsconnect                 Don't connect default audio ports "
+            "in jack"
+         << endl;
+    cout << " --bufstrategy # (0, 1, 2, 3, 4)          Use alternative jitter buffer"
+         << endl;
+    cout << " --broadcast <broadcast_queue>            Duplicate receive ports with the specified broadcast_queue length. "
+                                                       "Broadcast outputs have higher latency but less packet loss.\n";
+    cout << " --udprt                                  Use RT thread priority for "
+            "network I/O"
+         << endl;
+    cout << endl;
+    cout << "OPTIONS FOR AUDIO PATCHING IN HUB SERVER MODE:" << endl;
+    cout << " -p, --hubpatch # (0, 1, 2, 3, 4, 5)      Hub auto audio patch, only has "
+            "effect if running HUB SERVER mode, 0=server-to-clients, 1=client loopback, "
+            "2=client fan out/in but not loopback, 3=reserved for TUB, 4=full mix, 5=no "
+            "auto patching (default: 0)"
+         << endl;
+    cout << " -i, --includeserver                      Include audio to and from the "
+            "server in the mix when patching. Only affects -p 2 (client fan out/in "
+            "but not loopback) and -p 4 (full mix) patch modes."
+         << endl;
+    cout << " -u, --upmix                              Upmix mono clients to stereo when "
+            "patching"
+         << endl;
+    cout << endl;
+    cout << "OPTIONAL SIGNAL PROCESSING: " << endl;
+    cout << " -f, --effects # | paramString | help     Turn on incoming and/or outgoing "
+            "compressor and/or reverb in Client - see `-f help' for details"
+         << endl;
+    cout << " -O, --overflowlimiting i|o[w]|io[w]|n|help" << endl;
+    cout << "                                          Use audio limiter(s) in Client, "
+            "i=incoming from network, o=outgoing to network, io=both, n=no limiters, "
+            "w=warn if limiting (default=n). Say -O help for more."
+         << endl;
+    cout << " -a, --assumednumclients help|# (1,2,..)  Assumed number of Clients "
+            "(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 << " --audiodevice \"input-output device name\"" << endl;
+    cout << " --audiodevice \"input device name\",\"output device name\"" << endl;
+    cout << " --audioinputdevice \"input device name\"" << endl;
+    cout << " --audiooutputdevice \"output device name\"" << endl;
+    cout << "                                          Set audio device to use; if not set, "
+            "the default device will be used"
+         << endl;
+    cout << " --listdevices                            List available audio devices"
+         << endl;
+    cout << " -d, --deviceid #                         Set rtaudio device id (DEPRECATED, "
+            "use --audiodevice instead)"
+         << endl;
+    cout << endl;
+#endif
+    cout << "ARGUMENTS TO DISPLAY IO STATISTICS:" << endl;
+    cout << " -I, --iostat <time_in_secs>              Turn on IO stat reporting with "
+            "specified interval (in seconds)"
+         << endl;
+    cout << " -G, --iostatlog <log_file>               Save stat log into a file "
+            "(default: print in stdout)"
+         << endl;
+    cout << " -x, --examine-audio-delay <print_interval_in_secs> | help\n";
+    cout << "                                          Print round-trip audio delay "
+            "statistics. See `-x help' for details."
+         << endl;
+    cout << endl;
+    cout << "ARGUMENTS TO SIMULATE NETWORK ISSUES:" << endl;
+    cout << " --simloss <rate>                         Simulate packet loss" << endl;
+    cout << " --simjitter <rate>,<d>                   Simulate jitter, d is max delay "
+            "in packets"
+         << endl;
+    cout << endl;
+    cout << "ARGUMENTS FOR HUB CLIENT/SERVER AUTHENTICATION:" << endl;
+    cout << " -A, --auth                               Use authentication on the client side, or require it on the server side" << endl;
+    cout << " --certfile                               The certificate file to use on the hub server" << endl;
+    cout << " --keyfile                                The private key file to use on the hub server" << endl;
+    cout << " --credsfile                              The file containing the stored usernames and passwords" << endl;
+    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 << " --classic-gui                            Force JackTrip to run with the Classic Mode GUI." << endl;
+    cout << " --deeplink                               Handle a deeplink URL in the format jacktrip://join/<studio_id> by connecting as a hub client" << endl;
+    cout << endl;
+    cout << "HELP ARGUMENTS: " << endl;
+    cout << " -v, --version                            Prints Version Number" << endl;
+    cout << " -V, --verbose                            Verbose mode, prints debug messages"
+         << endl;
+    cout << " -h, --help                               Prints this Help" << endl;
+    cout << "" << endl;
+    // clang-format on
+}
+
+#ifdef RT_AUDIO
+void Settings::setDevicesByString(std::string nameArg)
+{
+    size_t commaPos;
+    char delim = ',';
+    if (std::count(nameArg.begin(), nameArg.end(), delim) > 1) {
+        throw std::invalid_argument(
+            "Found multiple commas in the --audiodevice argument, cannot parse "
+            "reliably.");
+    }
+    commaPos = nameArg.rfind(delim);
+    if (commaPos || nameArg[0] == delim) {
+        mInputDeviceName  = nameArg.substr(0, commaPos);
+        mOutputDeviceName = nameArg.substr(commaPos + 1);
+    } else {
+        mInputDeviceName = mOutputDeviceName = nameArg;
+    }
+}
+#endif
+
+//*******************************************************************************
+UdpHubListener* Settings::getConfiguredHubServer()
+{
+    if ((mBindPortNum < gBindPortLow) || (mBindPortNum > gBindPortHigh))
+        std::cout << "BindPort: " << mBindPortNum << " outside range" << std::endl;
+
+    if (gVerboseFlag)
+        std::cout << "JackTrip HUB SERVER TCP Bind Port: " << mBindPortNum << std::endl;
+    UdpHubListener* udpHub = new UdpHubListener(mBindPortNum, mServerUdpPortNum);
+    // udpHub->setSettings(this);
+#ifdef WAIR  // WAIR
+    udpHub->setWAIR(mWAIR);
+#endif  // endwhere
+    if (mPatchServerAudio) {
+        if (mHubConnectionMode == JackTrip::CLIENTFOFI) {
+            mHubConnectionMode = JackTrip::SERVFOFI;
+        } else if (mHubConnectionMode == JackTrip::FULLMIX) {
+            mHubConnectionMode = JackTrip::SERVFULLMIX;
+        } else {
+            std::cout << "WARNING: The -i (--includeserver) flag has no effect in this "
+                         "patch mode and will be ignored."
+                      << std::endl;
+        }
+    }
+    udpHub->setHubPatch(mHubConnectionMode);
+    udpHub->setStereoUpmix(mStereoUpmix);
+    // Connect default audio ports must be set after the connection mode.
+    udpHub->setConnectDefaultAudioPorts(mConnectDefaultAudioPorts);
+    // Set buffers to zero when underrun
+    if (mUnderrunMode == JackTrip::ZEROS) {
+        cout << "Setting buffers to zero when underrun..." << endl;
+        cout << gPrintSeparator << std::endl;
+        udpHub->setUnderRunMode(mUnderrunMode);
+    }
+    udpHub->setAudioBitResolution(mAudioBitResolution);
+    udpHub->setBufferQueueLength(mBufferQueueLength);
+
+    udpHub->setBufferStrategy(mBufferStrategy);
+    udpHub->setNetIssuesSimulation(mSimulatedLossRate, mSimulatedJitterRate,
+                                   mSimulatedDelayRel);
+    udpHub->setBroadcast(mBroadcastQueue);
+    udpHub->setUseRtUdpPriority(mUseRtUdpPriority);
+
+    if (true == mAppendThreadID) {
+        udpHub->mAppendThreadID = true;
+    }
+
+    if (mIOStatTimeout > 0) {
+        udpHub->setIOStatTimeout(mIOStatTimeout);
+        udpHub->setIOStatStream(mIOStatStream);
+    }
+
+    if (mAuth) {
+        //(We don't need to check the validity of these files because it's done by the
+        // UdpHubListener.)
+        udpHub->setRequireAuth(mAuth);
+        udpHub->setCertFile(mCertFile);
+        udpHub->setKeyFile(mKeyFile);
+        udpHub->setCredsFile(mCredsFile);
+    }
+    return udpHub;
+}
+
+JackTrip* Settings::getConfiguredJackTrip()
+{
+#ifdef WAIR  // WAIR
+    if (gVerboseFlag)
+        std::cout << "Settings:startJackTrip mNumNetRevChans = " << mNumNetRevChans
+                  << std::endl;
+#endif  // endwhere
+    if (gVerboseFlag)
+        std::cout << "Settings:startJackTrip before new JackTrip" << std::endl;
+    JackTrip* jackTrip = new JackTrip(
+        mJackTripMode, mDataProtocol, mBaseAudioInputChanNum, mNumAudioInputChans,
+        mBaseAudioOutputChanNum, mNumAudioOutputChans, AudioInterface::MIX_UNSET,
+#ifdef WAIR  // wair
+        mNumNetRevChans,
+#endif  // endwhere
+        mBufferQueueLength, mRedundancy, mAudioBitResolution,
+        /*DataProtocol::packetHeaderTypeT PacketHeaderType = */ DataProtocol::DEFAULT,
+        /*underrunModeT UnderRunMode = */ mUnderrunMode,
+        /* int receiver_bind_port = */ mBindPortNum,
+        /*int sender_bind_port = */ mBindPortNum,
+        /*int receiver_peer_port = */ mPeerPortNum,
+        /* int sender_peer_port = */ mPeerPortNum, mPeerPortNum);
+    // Set connect or not default audio ports. Only work for jack
+    jackTrip->setConnectDefaultAudioPorts(mConnectDefaultAudioPorts);
+
+    // Change client name if different from default
+    if (!mClientName.isEmpty()) {
+        jackTrip->setClientName(mClientName);
+    }
+
+    if (!mRemoteClientName.isEmpty() && (mJackTripMode == JackTrip::CLIENTTOPINGSERVER)) {
+        jackTrip->setRemoteClientName(mRemoteClientName);
+    }
+
+    // Set buffers to zero when underrun (Actual setting is handled in constructor.)
+    if (mUnderrunMode == JackTrip::ZEROS) {
+        cout << "Setting buffers to zero when underrun..." << endl;
+        cout << gPrintSeparator << std::endl;
+    }
+
+    jackTrip->setStopOnTimeout(mStopOnTimeout);
+
+    // Set peer address in server mode
+    if (mJackTripMode == JackTrip::CLIENT
+        || mJackTripMode == JackTrip::CLIENTTOPINGSERVER) {
+        jackTrip->setPeerAddress(mPeerAddress);
+    }
+
+    // Set in JamLink Mode
+    if (mJamLink) {
+        cout << "Running in JamLink Mode..." << endl;
+        cout << gPrintSeparator << std::endl;
+        jackTrip->setPacketHeaderType(DataProtocol::JAMLINK);
+    }
+
+    // Set in EmptyHeader Mode
+    if (mEmptyHeader) {
+        cout << "Running in EmptyHeader Mode..." << endl;
+        cout << gPrintSeparator << std::endl;
+        jackTrip->setPacketHeaderType(DataProtocol::EMPTY);
+    }
+
+    // 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) {
+        AudioInterface::setPipewireLatency(mAudioBufferSize, mSampleRate);
+    }
+#endif
+
+    // Set RtAudio
+#ifdef RT_AUDIO
+    if (!mUseJack) {
+        jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO);
+    }
+
+    // Change default device ID
+    if (mChangeDefaultID) {
+        jackTrip->setDeviceID(mDeviceID);
+    }
+
+    // Set device names
+    jackTrip->setInputDevice(mInputDeviceName);
+    jackTrip->setOutputDevice(mOutputDeviceName);
+#endif
+
+    jackTrip->setBufferStrategy(mBufferStrategy);
+    jackTrip->setNetIssuesSimulation(mSimulatedLossRate, mSimulatedJitterRate,
+                                     mSimulatedDelayRel);
+    jackTrip->setBroadcast(mBroadcastQueue);
+    jackTrip->setUseRtUdpPriority(mUseRtUdpPriority);
+
+    // Set auth details if we're in hub client mode
+    if (mAuth && mJackTripMode == JackTrip::CLIENTTOPINGSERVER) {
+        jackTrip->setUseAuth(true);
+        if (mUsername.isEmpty()) {
+            std::cout << "Username: ";
+            std::string username;
+            std::cin >> username;
+            mUsername = QString(username.c_str());
+        }
+        if (mPassword.isEmpty()) {
+            std::cout << "Password: ";
+            disableEcho(true);
+            std::string password;
+            std::cin >> password;
+            mPassword = QString(password.c_str());
+            disableEcho(false);
+        }
+        jackTrip->setUsername(mUsername);
+        jackTrip->setPassword(mPassword);
+    }
+
+    // Add Plugins
+    if (mLoopBack) {
+        cout << "Running in Loop-Back Mode..." << endl;
+        cout << gPrintSeparator << std::endl;
+        // std::tr1::shared_ptr<LoopBack> loopback(new LoopBack(mNumChans));
+        // mJackTrip->appendProcessPlugin(loopback.get());
+
+#if 0  // previous technique:
+        LoopBack* loopback = new LoopBack(mNumChans);
+        jackTrip->appendProcessPlugin(loopback);
+#else  // simpler method ( see AudioInterface.cpp callback() ):
+        jackTrip->setLoopBack(true);
+#endif
+
+        // ----- Test Karplus Strong -----------------------------------
+        // std::tr1::shared_ptr<NetKS> loopback(new NetKS());
+        // mJackTrip->appendProcessPlugin(loopback);
+        // loopback->play();
+        // NetKS* netks = new NetKS;
+        // mJackTrip->appendProcessPlugin(netks);
+        // netks->play();
+        // -------------------------------------------------------------
+    }
+
+    if (mIOStatTimeout > 0) {
+        jackTrip->setIOStatTimeout(mIOStatTimeout);
+        jackTrip->setIOStatStream(mIOStatStream);
+    }
+
+    jackTrip->setAudioTesterP(mAudioTester);
+
+    // Allocate audio effects in client, if any:
+    int nReservedChans =
+        mAudioTester->getEnabled() ? 1 : 0;  // no fx allowed on tester channel
+
+    std::vector<ProcessPlugin*> outgoingEffects =
+        mEffects.allocateOutgoingEffects(mNumAudioInputChans - nReservedChans);
+    for (auto p : outgoingEffects) {
+        jackTrip->appendProcessPluginToNetwork(p);
+    }
+
+    std::vector<ProcessPlugin*> incomingEffects =
+        mEffects.allocateIncomingEffects(mNumAudioOutputChans - nReservedChans);
+    for (auto p : incomingEffects) {
+        jackTrip->appendProcessPluginFromNetwork(p);
+    }
+
+#ifdef WAIR  // WAIR
+    if (mWAIR) {
+        cout << "Running in WAIR Mode..." << endl;
+        cout << gPrintSeparator << std::endl;
+        switch (mNumNetRevChans) {
+        case 16: {
+            jackTrip->appendProcessPluginFromNetwork(
+                new ap8x2(mNumChansOut));  // plugin slot 0
+            /////////////////////////////////////////////////////////
+            Stk16* plugin = new Stk16(mNumNetRevChans);
+            plugin->Stk16::initCombClient(mClientAddCombLen, mClientRoomSize);
+            jackTrip->appendProcessPluginFromNetwork(plugin);  // plugin slot 1
+        } break;
+        default:
+            throw std::invalid_argument(
+                "Settings: mNumNetRevChans doesn't correspond to Faust plugin");
+            break;
+        }
+    }
+#endif  // endwhere
+
+    return jackTrip;
+}
+
+void Settings::disableEcho(bool disabled)
+{
+#ifdef _WIN32
+    HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
+    DWORD mode;
+    GetConsoleMode(hStdin, &mode);
+
+    if (disabled) {
+        mode &= ~ENABLE_ECHO_INPUT;
+    } else {
+        mode |= ENABLE_ECHO_INPUT;
+    }
+    SetConsoleMode(hStdin, mode);
+#else
+    struct termios tty;
+    tcgetattr(STDIN_FILENO, &tty);
+    if (disabled) {
+        tty.c_lflag &= ~ECHO;
+    } else {
+        tty.c_lflag |= ECHO;
+    }
+    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
new file mode 100644 (file)
index 0000000..e30f7ca
--- /dev/null
@@ -0,0 +1,200 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file Settings.h
+ * \author Juan-Pablo Caceres
+ * \date July 2008
+ */
+
+#ifndef __SETTINGS_H__
+#define __SETTINGS_H__
+
+#include <cstdlib>
+#include <fstream>
+#include <vector>
+
+#include "DataProtocol.h"
+
+#ifndef NO_JACK
+#include "JackAudioInterface.h"
+#endif  // NO_JACK
+
+#include "AudioTester.h"
+#include "Effects.h"
+#include "JackTrip.h"
+#include "UdpHubListener.h"
+
+/** \brief Class to set usage options and parse settings from input
+ */
+class Settings : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    Settings(bool guiEnabled = false, QObject* parent = nullptr)
+        : QObject(parent), mGuiEnabled(guiEnabled), mAudioTester(new AudioTester)
+    {
+#ifdef NO_GUI
+        mGuiEnabled = false;
+#endif
+    }
+
+    /// \brief Parses command line input
+    void parseInput(int argc, char** argv);
+
+    UdpHubListener* getConfiguredHubServer();
+    JackTrip* getConfiguredJackTrip();
+
+    /// \brief Prints usage help
+    void printUsage();
+#ifdef RT_AUDIO
+    void setDevicesByString(std::string nameArg);
+#endif
+
+    bool getLoopBack() { return mLoopBack; }
+    bool isHubServer() { return mJackTripMode == JackTrip::SERVERPINGSERVER; }
+    bool guiIgnoresArguments() { return mGuiIgnoresArguments; }
+    bool guiForceClassicMode() { return mGuiForceClassicMode; }
+    bool isModeSet() { return mModeSet; }
+
+    JackTrip::jacktripModeT getJackTripMode() { return mJackTripMode; }
+    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; }
+    int getIOStatTimeout() { return mIOStatTimeout; }
+    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; }
+    const QString& getDeeplink() const { return mDeeplink; }
+
+   private:
+    void disableEcho(bool disabled);
+    void checkMode();
+
+    bool mGuiEnabled          = false;
+    bool mGuiIgnoresArguments = false;
+    bool mGuiForceClassicMode = false;
+
+    JackTrip::jacktripModeT mJackTripMode =
+        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;
+    QString mPeerAddress;  ///< Peer Address to use in jacktripModeT::CLIENT Mode
+    int mBindPortNum      = gDefaultPort;  ///< Bind Port Number
+    int mPeerPortNum      = gDefaultPort;  ///< Peer Port Number
+    int mServerUdpPortNum = 0;
+    QString mClientName;  ///< JackClient Name
+    QString mRemoteClientName;
+    bool mAppendThreadID{false};
+    JackTrip::underrunModeT mUnderrunMode{JackTrip::WAVETABLE};  ///< Underrun mode
+    bool mStopOnTimeout{false};  /// < Stop jacktrip after 10 second network timeout
+    int mBufferStrategy{1};
+
+#ifdef WAIR                   // wair
+    int mNumNetRevChans = 0;  ///< Number of Network Audio Channels (net comb filters)
+    int mClientAddCombLen;    ///< cmd line adjustment of net comb
+    double mClientRoomSize;   ///< freeverb room size
+    bool mWAIR = false;       ///< WAIR mode
+#endif                        // endwhere
+
+    bool mLoopBack           = false;                 ///< Loop-back mode
+    bool mJamLink            = false;                 ///< JamLink mode
+    bool mEmptyHeader        = false;                 ///< EmptyHeader 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
+
+    unsigned int mSampleRate;
+    unsigned int mAudioBufferSize;
+
+#ifdef RT_AUDIO
+    unsigned int mDeviceID;
+    std::string mInputDeviceName, mOutputDeviceName;
+#endif
+    unsigned int mHubConnectionMode = JackTrip::SERVERTOCLIENT;
+    bool mPatchServerAudio          = false;
+    bool mStereoUpmix               = false;
+    bool mConnectDefaultAudioPorts  = true;  ///< Connect or not jack audio ports
+    int mIOStatTimeout              = 0;
+    QSharedPointer<std::ostream> mIOStatStream;
+    Effects mEffects            = false;  // outgoing limiter OFF by default
+    double mSimulatedLossRate   = 0.0;
+    double mSimulatedJitterRate = 0.0;
+    double mSimulatedDelayRel   = 0.0;
+    int mBroadcastQueue         = 0;
+    bool mUseRtUdpPriority      = false;
+
+    bool mAuth = false;
+    QString mCertFile;
+    QString mKeyFile;
+    QString mCredsFile;
+    QString mUsername;
+    QString mPassword;
+    QString mDeeplink;
+
+    QSharedPointer<AudioTester> mAudioTester;
+};
+
+#endif
diff --git a/src/SslServer.cpp b/src/SslServer.cpp
new file mode 100644 (file)
index 0000000..5b2908d
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file SslServer.cpp
+ * \author Aaron Wyatt
+ * \date September 2020
+ */
+
+#include "SslServer.h"
+
+#include <QSslSocket>
+
+SslServer::SslServer(QObject* parent) : QTcpServer(parent) {}
+
+void SslServer::incomingConnection(qintptr socketDescriptor)
+{
+    // Override of the QTcpServer incomingConnection function to create
+    // an SslSocket rather than a regular socket.
+    QSslSocket* sslSocket = new QSslSocket(this);
+    sslSocket->setSocketDescriptor(socketDescriptor);
+    sslSocket->setLocalCertificate(m_certificate);
+    sslSocket->setPrivateKey(m_privateKey);
+    this->addPendingConnection(sslSocket);
+}
+
+void SslServer::setCertificate(const QSslCertificate& certificate)
+{
+    m_certificate = certificate;
+}
+
+void SslServer::setPrivateKey(const QSslKey& key)
+{
+    m_privateKey = key;
+}
+
+SslServer::~SslServer() = default;
diff --git a/src/SslServer.h b/src/SslServer.h
new file mode 100644 (file)
index 0000000..f3fd1fa
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file SslServer.h
+ * \author Aaron Wyatt
+ * \date September 2020
+ */
+
+#ifndef __SSLSERVER_H__
+#define __SSLSERVER_H__
+
+#include <QSslCertificate>
+#include <QSslKey>
+#include <QTcpServer>
+
+class SslServer : public QTcpServer
+{
+    Q_OBJECT
+
+   public:
+    SslServer(QObject* parent);
+    ~SslServer();
+
+    void incomingConnection(qintptr socketDescriptor) override;
+    void setCertificate(const QSslCertificate& certificate);
+    void setPrivateKey(const QSslKey& key);
+
+   private:
+    QSslCertificate m_certificate;
+    QSslKey m_privateKey;
+};
+
+#endif  // __SSLSERVER_H__
diff --git a/src/StereoToMono.cpp b/src/StereoToMono.cpp
new file mode 100644 (file)
index 0000000..fa99c33
--- /dev/null
@@ -0,0 +1,78 @@
+//*****************************************************************
+/*
+  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 <iostream>
+
+#include "jacktrip_types.h"
+#include "stereotomonodsp.h"
+
+//*******************************************************************************
+StereoToMono::StereoToMono(bool verboseFlag)
+{
+    setVerbose(verboseFlag);
+    stereoToMonoP = new stereotomonodsp;
+}
+
+//*******************************************************************************
+StereoToMono::~StereoToMono()
+{
+    delete static_cast<stereotomonodsp*>(stereoToMonoP);
+}
+
+//*******************************************************************************
+void StereoToMono::init(int samplingRate, int bufferSize)
+{
+    ProcessPlugin::init(samplingRate, bufferSize);
+
+    fs = float(fSamplingFreq);
+    static_cast<stereotomonodsp*>(stereoToMonoP)->init(fs);
+
+    inited = true;
+}
+
+//*******************************************************************************
+void StereoToMono::compute(int nframes, float** inputs, float** outputs)
+{
+    if (!inited) {
+        std::cerr << "*** Stereo-to-Mono " << this
+                  << ": init never called! Doing it now.\n";
+        init(0, 0);
+    }
+    static_cast<stereotomonodsp*>(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 (file)
index 0000000..690a89a
--- /dev/null
@@ -0,0 +1,70 @@
+//*****************************************************************
+/*
+  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 <QObject>
+
+#include "ProcessPlugin.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);
+
+    /// \brief The class destructor
+    virtual ~StereoToMono();
+
+    void init(int samplingRate, int bufferSize) 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;
+    void* stereoToMonoP;
+};
+
+#endif
\ No newline at end of file
diff --git a/src/Tone.cpp b/src/Tone.cpp
new file mode 100644 (file)
index 0000000..b003ebe
--- /dev/null
@@ -0,0 +1,115 @@
+//*****************************************************************
+/*
+  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 Tone.cpp
+ * \author Nelson Wang
+ * \date October 2022
+ * \license MIT
+ */
+
+#include "Tone.h"
+
+#include <iostream>
+
+#include "jacktrip_types.h"
+#include "tonedsp.h"
+
+//*******************************************************************************
+Tone::Tone(int numchans, bool verboseFlag) : mNumChannels(numchans)
+{
+    setVerbose(verboseFlag);
+    for (int i = 0; i < mNumChannels; i++) {
+        tonedsp* dsp_ptr = new tonedsp;
+        APIUI* ui_ptr    = new APIUI;
+        toneP.push_back(dsp_ptr);
+        toneUIP.push_back(ui_ptr);
+        dsp_ptr->buildUserInterface(ui_ptr);
+    }
+}
+
+//*******************************************************************************
+Tone::~Tone()
+{
+    for (int i = 0; i < mNumChannels; i++) {
+        delete static_cast<tonedsp*>(toneP[i]);
+        delete static_cast<APIUI*>(toneUIP[i]);
+    }
+    toneP.clear();
+    toneUIP.clear();
+}
+
+//*******************************************************************************
+void Tone::init(int samplingRate, int bufferSize)
+{
+    ProcessPlugin::init(samplingRate, bufferSize);
+    fs = float(fSamplingFreq);
+
+    for (int i = 0; i < mNumChannels; i++) {
+        static_cast<tonedsp*>(toneP[i])->init(
+            fs);  // compression filter parameters depend on sampling rate
+    }
+    inited = true;
+}
+
+//*******************************************************************************
+void Tone::compute(int nframes, float** inputs, float** outputs)
+{
+    if (!inited) {
+        std::cerr << "*** Tone " << this << ": init never called! Doing it now.\n";
+        init(0, 0);
+    }
+
+    for (int i = 0; i < mNumChannels; i++) {
+        /* Run the signal through Faust  */
+        static_cast<tonedsp*>(toneP[i])->compute(nframes, &inputs[i], &outputs[i]);
+    }
+}
+
+//*******************************************************************************
+void Tone::updateNumChannels(int nChansIn, int nChansOut)
+{
+    if (outgoingPluginToNetwork) {
+        mNumChannels = nChansIn;
+    } else {
+        mNumChannels = nChansOut;
+    }
+}
+
+void Tone::triggerPlayback()
+{
+    for (int i = 0; i < mNumChannels; i++) {
+        APIUI* ui_ptr = static_cast<APIUI*>(toneUIP[i]);
+        int ndx       = ui_ptr->getParamIndex("gate");
+        int v         = ui_ptr->getParamValue(ndx);
+        ui_ptr->setParamValue(ndx, v + 1);
+    }
+}
diff --git a/src/Tone.h b/src/Tone.h
new file mode 100644 (file)
index 0000000..98d8dba
--- /dev/null
@@ -0,0 +1,78 @@
+//*****************************************************************
+/*
+  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 Tone.h
+ * \author Nelson Wang
+ * \date October 2022
+ * \license MIT
+ */
+
+#ifndef __TONE_H__
+#define __TONE_H__
+
+#include <QObject>
+#include <vector>
+
+#include "ProcessPlugin.h"
+
+/** \brief The Tone plugin plays some arbitrary sample audio
+ */
+class Tone : public ProcessPlugin
+{
+    Q_OBJECT;
+
+   public:
+    /// \brief The class constructor sets the number of channels to measure
+    Tone(int numchans, bool verboseFlag = false);
+
+    /// \brief The class destructor
+    virtual ~Tone();
+
+    void init(int samplingRate, int bufferSize) override;
+    int getNumInputs() override { return (mNumChannels); }
+    int getNumOutputs() override { return (mNumChannels); }
+    void compute(int nframes, float** inputs, float** outputs) override;
+    const char* getName() const override { return "Tone"; };
+
+    void updateNumChannels(int nChansIn, int nChansOut) override;
+
+   public slots:
+    void triggerPlayback();
+
+   private:
+    std::vector<void*> toneP;
+    std::vector<void*> toneUIP;
+    float fs;
+    int mNumChannels;
+};
+
+#endif
\ No newline at end of file
diff --git a/src/UdpDataProtocol.cpp b/src/UdpDataProtocol.cpp
new file mode 100644 (file)
index 0000000..417ee82
--- /dev/null
@@ -0,0 +1,1069 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file UdpDataProtocol.cpp
+ * \author Juan-Pablo Caceres
+ * \date June 2008
+ */
+
+// #define MANUAL_POLL
+
+#include "UdpDataProtocol.h"
+
+#include <QHostInfo>
+#include <cerrno>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <stdexcept>
+
+#include "JackTrip.h"
+#include "jacktrip_globals.h"
+#ifdef _WIN32
+// #include <winsock.h>
+#include <stdio.h>
+#include <winsock2.h>  //cc need SD_SEND
+#pragma comment(lib, "ws2_32.lib")
+#define SIO_UDP_CONNRESET _WSAIOW(IOC_VENDOR, 12)
+#else
+#include <fcntl.h>
+#include <sys/socket.h>  // for POSIX Sockets
+#include <unistd.h>
+#ifndef MANUAL_POLL
+#ifdef __linux__
+#include <sys/epoll.h>
+#else
+#include <sys/event.h>
+#endif  // __linux__
+#endif  // MANUAL_POLL
+#endif  // _WIN32
+
+using std::cout;
+using std::endl;
+
+// NOTE: It's better not to use
+// using namespace std;
+// because some functions (like exit()) get confused with QT functions
+
+// sJackMutex definition
+QMutex UdpDataProtocol::sUdpMutex;
+
+//*******************************************************************************
+UdpDataProtocol::UdpDataProtocol(JackTrip* jacktrip, const runModeT runmode,
+                                 int bind_port, int peer_port,
+                                 unsigned int udp_redundancy_factor)
+    : DataProtocol(jacktrip, runmode, bind_port, peer_port)
+    , mBindPort(bind_port)
+    , mPeerPort(peer_port)
+    , mRunMode(runmode)
+#if defined(_WIN32)
+    , mSocket(INVALID_SOCKET)
+#else
+    , mSocket(-1)
+#endif
+    , mAudioPacket(NULL)
+    , mFullPacket(NULL)
+    , mUdpRedundancyFactor(udp_redundancy_factor)
+    , mControlPacketSize(63)
+    , mStopSignalSent(false)
+{
+    mIPv6 = false;
+    std::memset(&mPeerAddr, 0, sizeof(mPeerAddr));
+    std::memset(&mPeerAddr6, 0, sizeof(mPeerAddr6));
+    mPeerAddr.sin_port   = htons(mPeerPort);
+    mPeerAddr6.sin6_port = htons(mPeerPort);
+
+    if (mRunMode == RECEIVER) {
+        QObject::connect(this, &UdpDataProtocol::signalWaitingTooLong, jacktrip,
+                         &JackTrip::slotUdpWaitingTooLongClientGoneProbably,
+                         Qt::QueuedConnection);
+    }
+    mSimulatedLossRate       = 0.0;
+    mSimulatedJitterRate     = 0.0;
+    mSimulatedJitterMaxDelay = 0.0;
+}
+
+//*******************************************************************************
+UdpDataProtocol::~UdpDataProtocol()
+{
+    delete[] mAudioPacket;
+    delete[] mFullPacket;
+    closeSocket();
+    wait();
+}
+
+//*******************************************************************************
+void UdpDataProtocol::stop()
+{
+    closeSocket();
+    DataProtocol::stop();
+}
+
+//*******************************************************************************
+void UdpDataProtocol::closeSocket()
+{
+    if (mRunMode != RECEIVER) {
+        return;
+    }
+#if defined(_WIN32)
+    if (mSocket != INVALID_SOCKET) {
+        closesocket(mSocket);
+        mSocket = INVALID_SOCKET;
+    }
+#else
+    if (mSocket != -1) {
+        ::close(mSocket);
+        mSocket = -1;
+    }
+#endif
+}
+
+//*******************************************************************************
+void UdpDataProtocol::setPeerAddress(const char* peerHostOrIP)
+{
+    // Get DNS Address
+    if (!mPeerAddress.setAddress(peerHostOrIP)) {
+        QHostInfo info = QHostInfo::fromName(peerHostOrIP);
+        if (!info.addresses().isEmpty()) {
+            // use the first IP address
+            mPeerAddress = info.addresses().constFirst();
+        }
+        // cout << "UdpDataProtocol::setPeerAddress IP Address Number: "
+        //    << mPeerAddress.toString().toStdString() << endl;
+    }
+
+    // check if the ip address is valid
+    if (mPeerAddress.protocol() == QAbstractSocket::IPv6Protocol) {
+        mIPv6 = true;
+    } else if (mPeerAddress.protocol() != QAbstractSocket::IPv4Protocol) {
+        QString error_message =
+            QStringLiteral("Incorrect presentation format address\n'");
+        error_message.append(peerHostOrIP);
+        error_message.append("' is not a valid IP address or Host Name");
+        // std::cerr << "ERROR: Incorrect presentation format address" << endl;
+        // std::cerr << "'" << peerHostOrIP <<"' does not seem to be a valid IP address"
+        // << endl; throw std::invalid_argument("Incorrect presentation format address");
+        throw std::invalid_argument(error_message.toStdString());
+    }
+    /*
+    else {
+        std::cout << "Peer Address set to: "
+            << mPeerAddress.toString().toStdString() << std::endl;
+        cout << gPrintSeparator << endl;
+        usleep(100);
+    }
+    */
+
+    // Save our address as an appropriate address structure
+    if (mIPv6) {
+        mPeerAddr6.sin6_family = AF_INET6;
+        ::inet_pton(AF_INET6, mPeerAddress.toString().toLatin1().constData(),
+                    &mPeerAddr6.sin6_addr);
+    } else {
+        mPeerAddr.sin_family = AF_INET;
+        ::inet_pton(AF_INET, mPeerAddress.toString().toLatin1().constData(),
+                    &mPeerAddr.sin_addr);
+    }
+}
+
+#if defined(_WIN32)
+void UdpDataProtocol::setSocket(SOCKET& socket)
+#else
+void UdpDataProtocol::setSocket(int& socket)
+#endif
+{
+    // If we haven't been passed a valid socket, then we should bind one.
+#if defined(_WIN32)
+    if (socket == INVALID_SOCKET) {
+#else
+    if (socket == -1) {
+#endif
+        try {
+            if (gVerboseFlag)
+                std::cout << "    UdpDataProtocol:run" << mRunMode << " before bindSocket"
+                          << std::endl;
+            socket = bindSocket();  // Bind Socket
+        } catch (const std::exception& e) {
+            emit signalError(e.what());
+            return;
+        }
+    }
+    mSocket = socket;
+}
+
+//*******************************************************************************
+#if defined(_WIN32)
+SOCKET UdpDataProtocol::bindSocket()
+#else
+int UdpDataProtocol::bindSocket()
+#endif
+{
+    QMutexLocker locker(&sUdpMutex);
+
+#ifdef _WIN32
+    WORD wVersionRequested;
+    WSADATA wsaData;
+    int err;
+
+    wVersionRequested = MAKEWORD(2, 2);
+
+    err = WSAStartup(wVersionRequested, &wsaData);
+    if (err != 0) {
+        // Tell the user that we couldn't find a usable
+        // winsock.dll.
+
+        return INVALID_SOCKET;
+    }
+
+    // Confirm that the Windows Sockets DLL supports 1.1. or higher
+
+    if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
+        // Tell the user that we couldn't find a usable
+        // winsock.dll.
+        WSACleanup();
+        return INVALID_SOCKET;
+    }
+
+    SOCKET sock_fd;
+#else
+    int sock_fd;
+#endif
+
+    // Set local IPv4 or IPv6 Address
+    struct sockaddr_in local_addr;
+    struct sockaddr_in6 local_addr6;
+
+    // Create socket descriptor
+    if (mIPv6) {
+        sock_fd = socket(AF_INET6, SOCK_DGRAM, 0);
+        std::memset(&local_addr6, 0, sizeof(local_addr6));
+        local_addr6.sin6_family = AF_INET6;
+        local_addr6.sin6_addr   = in6addr_any;
+        local_addr6.sin6_port   = htons(mBindPort);
+    } else {
+        sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
+
+        //::bzero(&local_addr, sizeof(local_addr));
+        std::memset(&local_addr, 0, sizeof(local_addr));  // set buffer to 0
+        local_addr.sin_family = AF_INET;                  // AF_INET: IPv4 Protocol
+        local_addr.sin_addr.s_addr =
+            htonl(INADDR_ANY);  // INADDR_ANY: let the kernel decide the active address
+        local_addr.sin_port = htons(mBindPort);  // set local port
+    }
+
+    // Prevent WSAECONNRESET errors that occur on Windows due to async UDP port setup
+#if defined(_WIN32)
+    BOOL bNewBehavior     = FALSE;
+    DWORD dwBytesReturned = 0;
+    WSAIoctl(sock_fd, SIO_UDP_CONNRESET, &bNewBehavior, sizeof bNewBehavior, NULL, 0,
+             &dwBytesReturned, NULL, NULL);
+#endif
+
+    // Set socket to be reusable, this is platform dependent
+    int one = 1;
+#if defined(_WIN32)
+    // make address/port reusable
+    setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, (char*)&one, sizeof(one));
+#elif defined(__linux__)
+    ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
+#else
+    // This option is not available on Linux, and without it MAC OS X
+    // has problems rebinding a socket
+    ::setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
+#endif
+
+#if defined(_WIN32)
+    // TODO: these don't seem to work on windows. we likely need to use qWAVE or qos2
+#elif defined(__APPLE__)
+    // set service type "Interactive Voice"
+    // TODO: this is supposed to be the right thing to do on OSX, but doesn't seem to do
+    // anything
+    const int val = NET_SERVICE_TYPE_VO;
+    ::setsockopt(sock_fd, SOL_SOCKET, SO_NET_SERVICE_TYPE, &val, sizeof(val));
+#else
+    // Set ToS to DSCP Expedited Forwarding (EF), recommended for Audio
+    // See RFC2474 https://datatracker.ietf.org/doc/html/rfc2474
+    // See also
+    // https://www.slashroot.in/understanding-differentiated-services-tos-field-internet-protocol-header
+    const char tos = 0xB8;  // 10111000
+    if (mIPv6) {
+        ::setsockopt(sock_fd, IPPROTO_IPV6, IPV6_TCLASS, &tos, sizeof(tos));
+    } else {
+        ::setsockopt(sock_fd, IPPROTO_IP, IP_TOS, &tos, sizeof(tos));
+    }
+
+    // Set 802.1q QoS priority
+    int priority = 6;
+    ::setsockopt(sock_fd, SOL_SOCKET, SO_PRIORITY, &priority, sizeof(priority));
+#endif
+
+    // Bind the Socket
+    if (mIPv6) {
+        if ((::bind(sock_fd, (struct sockaddr*)&local_addr6, sizeof(local_addr6))) < 0) {
+            throw std::runtime_error("ERROR: UDP Socket Bind Error");
+        }
+    } else {
+        if ((::bind(sock_fd, (struct sockaddr*)&local_addr, sizeof(local_addr))) < 0) {
+            throw std::runtime_error("ERROR: UDP Socket Bind Error");
+        }
+    }
+
+    // Return our file descriptor so the socket can be shared for a
+    // full duplex connection.
+    return sock_fd;
+}
+
+void UdpDataProtocol::processControlPacket(const char* buf)
+{
+    // Control signal (currently just check for exit packet);
+    bool exit = true;
+    for (int i = 0; i < mControlPacketSize; i++) {
+        if (buf[i] != char(0xff)) {
+            exit = false;
+            i    = mControlPacketSize;
+        }
+    }
+    if (exit && !mStopSignalSent) {
+        mStopSignalSent = true;
+        emit signalCeaseTransmission(QStringLiteral("Peer Stopped"));
+        std::cout << "Peer Stopped" << std::endl;
+    }
+}
+
+//*******************************************************************************
+int UdpDataProtocol::receivePacket(char* buf, const size_t n)
+{
+    int n_bytes = ::recv(mSocket, buf, int(n), 0);
+    if (n_bytes == mControlPacketSize) {
+        processControlPacket(buf);
+        return 0;
+    }
+    return n_bytes;
+}
+
+//*******************************************************************************
+int UdpDataProtocol::sendPacket(const char* buf, const size_t n)
+{
+    /*#if defined (_WIN32)
+    //Alternative windows specific code that uses winsock equivalents of the bsd socket
+functions. DWORD n_bytes; WSABUF buffer; int error; buffer.len = n; buffer.buf = (char
+*)buf;
+
+    if (mIPv6) {
+        error = WSASendTo(mSocket, &buffer, 1, &n_bytes, 0, (struct sockaddr *)
+&mPeerAddr6, sizeof(mPeerAddr6), 0, 0); } else { error = WSASend(mSocket, &buffer, 1,
+&n_bytes, 0, 0, 0);
+    }
+    if (error == SOCKET_ERROR) {
+        cout << "Socket Error: " << WSAGetLastError() << endl;
+    }
+    return (int)n_bytes;
+#else*/
+    int n_bytes;
+    if (mIPv6) {
+        n_bytes = ::sendto(mSocket, buf, int(n), 0, (struct sockaddr*)&mPeerAddr6,
+                           sizeof(mPeerAddr6));
+    } else {
+        n_bytes = ::sendto(mSocket, buf, int(n), 0, (struct sockaddr*)&mPeerAddr,
+                           sizeof(mPeerAddr));
+    }
+    return n_bytes;
+    // #endif
+}
+
+//*******************************************************************************
+// void UdpDataProtocol::getPeerAddressFromFirstPacket(QHostAddress& peerHostAddress,
+//                                                     uint16_t& port)
+// {
+//     while (!datagramAvailable()) {
+//         msleep(100);
+//     }
+//     char buf[1];
+
+//     struct sockaddr_storage addr;
+//     std::memset(&addr, 0, sizeof(addr));
+//     socklen_t sa_len = sizeof(addr);
+//     ::recvfrom(mSocket, buf, 1, 0, (struct sockaddr*)&addr, &sa_len);
+//     peerHostAddress.setAddress((struct sockaddr*)&addr);
+//     if (mIPv6) {
+//         port = ((struct sockaddr_in6*)&addr)->sin6_port;
+//     } else {
+//         port = ((struct sockaddr_in*)&addr)->sin_port;
+//     }
+// }
+
+//*******************************************************************************
+void UdpDataProtocol::run()
+{
+    if (gVerboseFlag)
+        switch (mRunMode) {
+        case RECEIVER: {
+            std::cout << "step 3" << std::endl;
+            break;
+        }
+
+        case SENDER: {
+            std::cout << "step 4" << std::endl;
+            break;
+        }
+        }
+
+    // QObject::connect(this, SIGNAL(signalError(const char*)),
+    //                 mJackTrip, SLOT(slotStopProcesses()),
+    //                 Qt::QueuedConnection);
+
+    if (mRunMode == RECEIVER) {
+        cout << "UDP Socket Receiving in Port: " << mBindPort << endl;
+        cout << gPrintSeparator << endl;
+        // Make sure our socket is in non-blocking mode.
+#ifdef _WIN32
+        u_long nonblock = 1;
+        ioctlsocket(mSocket, FIONBIO, &nonblock);
+#else
+        int flags = ::fcntl(mSocket, F_GETFL, 0);
+        ::fcntl(mSocket, F_SETFL, flags | O_NONBLOCK);
+#endif
+    }
+
+    if (gVerboseFlag)
+        std::cout << "    UdpDataProtocol:run" << mRunMode
+                  << " before Setup Audio Packet buffer, Full Packet buffer, Redundancy "
+                     "Variables"
+                  << std::endl;
+
+    // Setup Audio Packet buffer
+    size_t audio_packet_size = getAudioPacketSizeInBites();
+    // cout << "audio_packet_size: " << audio_packet_size << endl;
+    mAudioPacket = new int8_t[audio_packet_size];
+    std::memset(mAudioPacket, 0, audio_packet_size);  // set buffer to 0
+    mBuffer.resize(audio_packet_size, 0);
+
+    int full_packet_size;
+    mSmplSize = mJackTrip->getAudioBitResolution() / 8;
+
+    if (mRunMode == RECEIVER) {
+        mChans = mJackTrip->getNumOutputChannels();
+        if (0 == mChans)
+            return;
+        full_packet_size = mJackTrip->getReceivePacketSizeInBytes();
+        mFullPacket      = new int8_t[full_packet_size];
+        std::memset(mFullPacket, 0, full_packet_size);  // set buffer to 0
+        // Put header in first packet
+        mJackTrip->putHeaderInIncomingPacket(mFullPacket, mAudioPacket);
+
+    } else {
+        mChans = mJackTrip->getNumInputChannels();
+        if (0 == mChans)
+            return;
+        full_packet_size = mJackTrip->getSendPacketSizeInBytes();
+        mFullPacket      = new int8_t[full_packet_size];
+        std::memset(mFullPacket, 0, full_packet_size);  // set buffer to 0
+        // Put header in first packet
+        mJackTrip->putHeaderInOutgoingPacket(mFullPacket, mAudioPacket);
+    }
+
+    // Redundancy Variables
+    // (Algorithm explained at the end of this file)
+    // ---------------------------------------------
+    int full_redundant_packet_size = full_packet_size * mUdpRedundancyFactor;
+    int8_t* full_redundant_packet  = NULL;
+
+    // Set realtime priority (function in jacktrip_globals.h)
+    if (gVerboseFlag)
+        std::cout << "    UdpDataProtocol:run" << mRunMode
+                  << " before setRealtimeProcessPriority()" << std::endl;
+    // std::endl;
+    // Anton Runov: making setRealtimeProcessPriority optional
+    if (mUseRtPriority) {
+#if defined(__APPLE__)
+        setRealtimeProcessPriority(mJackTrip->getBufferSizeInSamples(),
+                                   mJackTrip->getSampleRate());
+#else
+        setRealtimeProcessPriority();
+#endif
+    }
+
+    // clang-format off
+    /////////////////////
+    // to see thread priorities
+    // sudo ps -eLo pri,rtprio,cls,pid,nice,cmd | grep -E 'jackd|jacktrip|rtc|RTPRI' | sort -r
+
+    // from David Runge
+
+    //  It seems that it tries to apply the highest available SCHED_FIFO to
+    //  jacktrip or half of it (?) [1] (although that's not what you would want,
+    //  as this would mean assigning a higher priority to jacktrip than e.g. to
+    //  the audio interface and e.g. IRQs that need to be taken care of).
+
+    //  The version on github [2] (current 1.1) is actually worse off, as it
+    //  just hardcodes RTPRIO 99 (which means jacktrip will compete with the
+    //  Linux kernel watchdog, if the user trying to launch jacktrip is even
+    //  allowed to use that high of a priority!).
+    //  On most systems this will not work at all (aside from it being outright
+    //  dangerous). On Arch (and also Ubuntu) the sane default is to allow
+    //  rtprio 95 to a privileged user group (e.g. 'realtime' or 'audio', etc.)
+
+    //  It would be very awesome, if setting the priority would be dealt with by
+    //  a command line flag to jacktrip (e.g. `jacktrip --priority=50`) and
+    //  otherwise defaulting to a much much lower number (e.g. 10), so the
+    //  application can be run out-of-the-box (even without being in a
+    //  privileged group).
+
+    // from Nando
+
+    //  You should actually be using the priority that jack gives you when you
+    //  create the realtime thread, that puts your process "behind" - so to
+    //  speak - the processing that jack does on behalf of all its clients, and
+    //  behind (in a properly configured system) the audio interface processing
+    //  interrupt. No need to select a priority yourself.
+
+    //  In a Fedora system I run jack with a priority of 65 (the Fedora packages
+    //  changed the default to a much lower one which is a big no-no). The
+    //  clients inherit 60, I think. Some clients that have their own internal
+    //  structure of processes (jconvolver) run multiple threads and use
+    //  priorities below 60 for them (ie: they start with what jack gave them).
+
+    //  If you need to run a thread (not the audio thread) with higher priority
+    //  you could retrieve the priority that jack gave you and add some magic
+    //  number to get it to be above jack itself (10 would be fine in my
+    //  experience).
+
+    //without setting it
+    //        PRI RTPRIO CLS   PID  NI CMD
+    //         60     20  FF  4348   - /usr/bin/jackd -dalsa -dhw:CODEC -r48000 -p128 -n2 -Xseq
+    //         55     15  FF  9835   - ./jacktrip -s
+    //         19      -  TS  9835   0 ./jacktrip -s
+    //         19      -  TS  9835   0 ./jacktrip -s
+    //         19      -  TS  9835   0 ./jacktrip -s
+    //         19      -  TS  9835   0 ./jacktrip -s
+    //         19      -  TS  9835   0 ./jacktrip -s
+    //         19      -  TS  4348   0 /usr/bin/jackd -dalsa -dhw:CODEC -r48000 -p128 -n2 -Xseq
+    //         19      -  TS  4348   0 /usr/bin/jackd -dalsa -dhw:CODEC -r48000 -p128 -n2 -Xseq
+    //         19      -  TS  4348   0 /usr/bin/jackd -dalsa -dhw:CODEC -r48000 -p128 -n2 -Xseq
+    //         19      -  TS  4348   0 /usr/bin/jackd -dalsa -dhw:CODEC -r48000 -p128 -n2 -Xseq
+
+    // jack puts its clients in FF at 5 points below itself
+    //
+    // clang-format off
+
+    threadHasStarted();
+
+    switch (mRunMode) {
+    case RECEIVER: {
+        // Connect signals and slots for packets arriving too late notifications
+        QObject::connect(this, &UdpDataProtocol::signalWaitingTooLong, this,
+                         &UdpDataProtocol::printUdpWaitedTooLong, Qt::QueuedConnection);
+        //-----------------------------------------------------------------------------------
+        // Wait for the first packet to be ready and obtain address
+        // from that packet
+        if (gVerboseFlag)
+            std::cout << "    UdpDataProtocol:run" << mRunMode
+                      << " before !UdpSocket.hasPendingDatagrams()" << std::endl;
+        std::cout << "Waiting for Peer..." << std::endl;
+        // This blocks waiting for the first packet
+        while (!datagramAvailable()) {
+            if (mStopped) { return; }
+            QThread::msleep(100);
+            if (gVerboseFlag) std::cout << "100ms  " << std::flush;
+        }
+        full_redundant_packet_size = 0x10000;  // max UDP datagram size
+        full_redundant_packet      = new int8_t[full_redundant_packet_size];
+        full_redundant_packet_size = receivePacket(
+            reinterpret_cast<char*>(full_redundant_packet), full_redundant_packet_size);
+        // Check that peer has the same audio settings
+        if (gVerboseFlag)
+            std::cout << std::endl
+                      << "    UdpDataProtocol:run" << mRunMode
+                      << " before mJackTrip->checkPeerSettings()" << std::endl;
+        if (!mJackTrip->checkPeerSettings(full_redundant_packet)) {
+            // If our peer settings aren't compatible, don't continue.
+            // (The checkPeerSettings function needs to signal the JackTrip instance with the exact error message.)
+            delete[] full_redundant_packet;
+            full_redundant_packet = nullptr;
+            return;
+        }
+
+        int peer_chans   = mJackTrip->getPeerNumOutgoingChannels(full_redundant_packet);
+        full_packet_size = mJackTrip->getHeaderSizeInBytes()
+                           + mJackTrip->getPeerBufferSize(full_redundant_packet)
+                                 * peer_chans * mSmplSize;
+        /*
+        cout << "peer sizes: " << mJackTrip->getHeaderSizeInBytes()
+             << " + " << mJackTrip->getPeerBufferSize(full_redundant_packet)
+             << " * " << mJackTrip->getNumChannels() << " * " << (int)mJackTrip->getAudioBitResolution()/8 << endl;
+        cout << "full_packet_size: " << full_packet_size << " / " << mJackTrip->getPacketSizeInBytes() << endl;
+        cout << "full_redundant_packet_size: " << full_redundant_packet_size << endl;
+        // */
+
+        if (gVerboseFlag) std::cout << "step 7" << std::endl;
+        if (gVerboseFlag)
+            std::cout << "    UdpDataProtocol:run" << mRunMode
+                      << " before mJackTrip->parseAudioPacket()" << std::endl;
+        std::cout << "Received Connection from Peer!" << std::endl;
+        emit signalReceivedConnectionFromPeer();
+
+        // Redundancy Variables
+        // --------------------
+        // NOTE: These types need to be the same unsigned integer as the sequence
+        // number in the header. That way, they wrap around in the "same place"
+        uint16_t current_seq_num = 0;  // Store current sequence number
+        uint16_t last_seq_num    = 0;  // Store last package sequence number
+        uint16_t newer_seq_num   = 0;  // Store newer sequence number
+        mTotCount                = 0;
+        mLostCount               = 0;
+        mOutOfOrderCount         = 0;
+        mLastOutOfOrderCount     = 0;
+        mInitialState            = true;
+        mRevivedCount            = 0;
+        mStatCount               = 0;
+
+        //Set up our platform specific polling mechanism. (kqueue, epoll, overlapped I/O)
+#if !defined (MANUAL_POLL)
+#if defined (__linux__)
+        int epollfd = epoll_create1(0);
+        struct epoll_event change, event;
+        change.events = EPOLLIN;
+        change.data.fd = mSocket;
+        epoll_ctl(epollfd, EPOLL_CTL_ADD, mSocket, &change);
+#elif defined (_WIN32)
+        WSAEVENT eventArray[WSA_MAXIMUM_WAIT_EVENTS];
+        WSAOVERLAPPED socketOverlapped;
+        WSABUF dataBuf;
+        dataBuf.len = full_redundant_packet_size;
+        dataBuf.buf = reinterpret_cast<char *>(full_redundant_packet);
+        DWORD recvBytes = 0, flags = 0, index, bytesTransferred = 0;
+
+        eventArray[0] = WSACreateEvent();
+        if (eventArray == WSA_INVALID_EVENT) {
+            emit signalError("Unable to set up network event monitoring");
+            cout << "ERROR: Unable to set up network event monitoring" << endl;
+            mStopped = true;
+        }
+        ZeroMemory(&socketOverlapped, sizeof(WSAOVERLAPPED));
+        socketOverlapped.hEvent = eventArray[0];
+
+        if (WSARecv(mSocket, &dataBuf, 1, &recvBytes, &flags, &socketOverlapped, NULL) == SOCKET_ERROR) {
+            int result = WSAGetLastError();
+            if (result != WSA_IO_PENDING) {
+                emit signalError("Unable to listen for incoming network packets");
+                cout << "ERROR: Unable to listen for incoming network packets" << endl;
+                mStopped = true;
+            }
+        }
+#else
+        int kq = kqueue();
+        struct kevent change;
+        struct kevent event;
+        EV_SET(&change, mSocket, EVFILT_READ, EV_ADD, 0, 0, NULL);
+        struct timespec timeout;
+        timeout.tv_sec = 0;
+        timeout.tv_nsec = 10000000;
+#endif
+        int waitTime = 0;
+#endif // MANUAL_POLL
+
+        if (gVerboseFlag) std::cout << "step 8" << std::endl;
+        while (!mStopped) {
+            // Timer to report packets arriving too late
+            // This QT method gave me a lot of trouble, so I replaced it with my own 'waitForReady'
+            // that uses signals and slots and can also report with packets have not
+            // arrive for a longer time
+            //timeout = UdpSocket.waitForReadyRead(gUdpWaitTimeout);
+            //        timeout = cc unused!
+#if defined (MANUAL_POLL)
+            waitForReady(60000); //60 seconds
+            if (receivePacket(reinterpret_cast<char *>(full_redundant_packet), full_redundant_packet_size) > 0) {
+                receivePacketRedundancy(full_redundant_packet, full_redundant_packet_size,
+                                        full_packet_size, current_seq_num, last_seq_num,
+                                        newer_seq_num);
+            }
+        }
+#else
+
+            // OLD CODE WITHOUT REDUNDANCY----------------------------------------------------
+            /*
+        // This is blocking until we get a packet...
+        receivePacket( UdpSocket, reinterpret_cast<char*>(mFullPacket), full_packet_size);
+
+        mJackTrip->parseAudioPacket(mFullPacket, mAudioPacket);
+
+        // ...so we want to send the packet to the buffer as soon as we get in from
+        // the socket, i.e., non-blocking
+        //mRingBuffer->insertSlotNonBlocking(mAudioPacket);
+        mJackTrip->writeAudioBuffer(mAudioPacket);
+        */
+            //----------------------------------------------------------------------------------
+
+#if defined(_WIN32)
+            index = WSAWaitForMultipleEvents(1, eventArray, FALSE, 10, FALSE);
+            if (index == WSA_WAIT_TIMEOUT) {
+                waitTime += 10;
+                emit signalWaitingTooLong(waitTime);
+            } else {
+                waitTime = 0;
+                WSAResetEvent(eventArray[index - WSA_WAIT_EVENT_0]);
+                WSAGetOverlappedResult(mSocket, &socketOverlapped, &bytesTransferred, FALSE, &flags);
+                if (bytesTransferred == mControlPacketSize) {
+                    processControlPacket(reinterpret_cast<char *>(full_redundant_packet));
+                } else if (bytesTransferred > 0 ){
+                    receivePacketRedundancy(full_redundant_packet, full_redundant_packet_size,
+                                        full_packet_size, current_seq_num, last_seq_num,
+                                        newer_seq_num);
+                }
+                WSARecv(mSocket, &dataBuf, 1, &recvBytes, &flags, &socketOverlapped, NULL);
+            }
+#else
+#if defined(__linux__)
+            int n = epoll_wait(epollfd, &event, 1, 10);
+#else
+            int n = kevent(kq, &change, 1, &event, 1, &timeout);
+#endif
+            if (n > 0) {
+                waitTime = 0;
+                if (receivePacket(reinterpret_cast<char *>(full_redundant_packet), full_redundant_packet_size) > 0) {
+                    receivePacketRedundancy(full_redundant_packet, full_redundant_packet_size,
+                                            full_packet_size, current_seq_num, last_seq_num,
+                                            newer_seq_num);
+                }
+            } else {
+                waitTime += 10;
+                emit signalWaitingTooLong(waitTime);
+            }
+#endif
+        }
+#if defined(__linux__)
+        close(epollfd);
+#elif defined(_WIN32)
+        WSACloseEvent(eventArray);
+#else
+        close(kq);
+#endif
+#endif // MANUAL_POLL
+        break; }
+
+    case SENDER : {
+        delete[] full_redundant_packet;
+        full_redundant_packet = new int8_t[full_redundant_packet_size];
+        std::memset(full_redundant_packet, 0,
+                    full_redundant_packet_size); // Initialize to 0
+        while (!mStopped && !JackTrip::sSigInt && !JackTrip::sAudioStopped) {
+            sendPacketRedundancy(full_redundant_packet, full_redundant_packet_size,
+                                 full_packet_size);
+        }
+
+        // Send exit packet (with 1 redundant packet).
+        cout << "sending exit packet" << endl;
+        QByteArray exitPacket = QByteArray(mControlPacketSize, static_cast<char>(0xff));
+        sendPacket(exitPacket.constData(), mControlPacketSize);
+        sendPacket(exitPacket.constData(), mControlPacketSize);
+        emit signalCeaseTransmission();
+        break; }
+    }
+
+    if (NULL != full_redundant_packet) {
+        delete[] full_redundant_packet;
+        full_redundant_packet = NULL;
+    }
+}
+
+//*******************************************************************************
+//bool
+void UdpDataProtocol::waitForReady(int timeout_msec)
+{
+    int loop_resolution_usec = 100;    // usecs to wait on each loop
+    int emit_resolution_usec = 10000;  // 10 milliseconds
+    int timeout_usec         = timeout_msec * 1000;
+    int elapsed_time_usec    = 0;  // Elapsed time in milliseconds
+
+    while (!datagramAvailable() && (elapsed_time_usec <= timeout_usec) && !mStopped) {
+        //    if (mStopped) { return false; }
+        QThread::usleep(loop_resolution_usec);
+        elapsed_time_usec += loop_resolution_usec;
+
+        if (!(elapsed_time_usec % emit_resolution_usec)) {
+            emit signalWaitingTooLong(static_cast<int>(elapsed_time_usec / 1000));
+        }
+    }
+    // cc under what condition?
+    //  if ( elapsed_time_usec >= timeout_usec )
+    //  {
+    //    emit signalWaitingTooLong(elapsed_time_usec/1000);
+    //    return false;
+    //  }
+    //  return true;
+}
+
+//*******************************************************************************
+void UdpDataProtocol::printUdpWaitedTooLong(int wait_msec)
+{
+    if (!(wait_msec % gUdpWaitTimeout)) {
+        // only log error once per gap in audio, rather than every 30ms
+        if (wait_msec <= gUdpWaitTimeout) {
+            std::cerr << "UDP waiting too long (more than " << gUdpWaitTimeout << "ms) for "
+                    << mPeerAddress.toString().toStdString() << "..." << endl;
+        }
+        emit signalUdpWaitingTooLong();
+    }
+}
+
+//*******************************************************************************
+void UdpDataProtocol::receivePacketRedundancy(
+    int8_t* full_redundant_packet, [[maybe_unused]] int full_redundant_packet_size, int full_packet_size,
+    uint16_t& current_seq_num, uint16_t& last_seq_num, uint16_t& newer_seq_num)
+{
+    if (0.0 < mSimulatedLossRate || 0.0 < mSimulatedJitterRate) {
+        double x = mUniformDist(mRndEngine);
+        // Drop packets
+        x -= mSimulatedLossRate;
+        if (0 > x) { return; }
+        // Delay packets
+        x -= mSimulatedJitterRate;
+        if (0 > x) { usleep(mUniformDist(mRndEngine) * mSimulatedJitterMaxDelay * 1e6); }
+    }
+
+    // Get Packet Sequence Number
+    newer_seq_num   = mJackTrip->getPeerSequenceNumber(full_redundant_packet);
+    current_seq_num = newer_seq_num;
+
+    int16_t lost = 0;
+    if (!mInitialState) {
+        lost = newer_seq_num - last_seq_num - 1;
+        if (0 > lost || 1000 < lost) {
+            // Out of order packet, should be ignored
+            ++mOutOfOrderCount;
+            if (5 < ++mLastOutOfOrderCount) {
+                mInitialState = true;
+                mStatCount    = 0;
+                mTotCount     = 0;
+            }
+            return;
+        } else if (0 != lost) {
+            mLostCount += lost;
+        }
+        mTotCount += 1 + lost;
+    }
+    mLastOutOfOrderCount = 0;
+    mInitialState        = false;
+
+    //cout << current_seq_num << " ";
+    int redun_last_index = 0;
+    for (unsigned int i = 1; i < mUdpRedundancyFactor; i++) {
+        // Check if the package we receive is the next one expected, i.e.,
+        // current_seq_num == (last_seq_num+1)
+        if (current_seq_num == (last_seq_num + 1)) { break; }
+
+        // if it's not, check the next one until it is the corresponding packet
+        // or there aren't more available packets
+        redun_last_index = i;  // index of packet to use in the redundant packet
+        current_seq_num  = mJackTrip->getPeerSequenceNumber(full_redundant_packet
+                                                           + (i * full_packet_size));
+        //cout << current_seq_num << " ";
+    }
+    mRevivedCount += redun_last_index;
+    //cout << endl;
+
+    int peer_chans    = mJackTrip->getPeerNumOutgoingChannels(full_redundant_packet);
+    int N             = mJackTrip->getPeerBufferSize(full_redundant_packet);
+    int host_buf_size = N * mChans * mSmplSize;
+    int hdr_size      = mJackTrip->getHeaderSizeInBytes();
+    int gap_size      = mInitialState ? 0 : (lost - redun_last_index) * host_buf_size;
+
+    last_seq_num = newer_seq_num;  // Save last read packet
+
+    if ((int)mBuffer.size() < host_buf_size) { mBuffer.resize(host_buf_size, 0); }
+    // Send to audio all available audio packets, in order
+    for (int i = redun_last_index; i >= 0; i--) {
+        int8_t* src = full_redundant_packet + (i * full_packet_size) + hdr_size;
+        if (1 != mChans) {
+            // Convert packet's non-interleaved layout to interleaved one used internally
+            int8_t* dst = mBuffer.data();
+            int C       = qMin(mChans, peer_chans);
+            for (int n = 0; n < N; ++n) {
+                for (int c = 0; c < C; ++c) {
+                    memcpy(dst + (n * mChans + c) * mSmplSize,
+                           src + (n + c * N) * mSmplSize, mSmplSize);
+                }
+            }
+            src = dst;
+        }
+        int ok = true; // send audio buf to
+        ok = mJackTrip->writeAudioBuffer(src, host_buf_size, gap_size, last_seq_num);
+        if (!ok) {
+            emit signalError("Local and Peer buffer settings are incompatible");
+            cout << "ERROR: Local and Peer buffer settings are incompatible" << endl;
+            mStopped = true;
+            break;
+        }
+        gap_size = 0;
+    }
+}
+
+//*******************************************************************************
+bool UdpDataProtocol::getStats(DataProtocol::PktStat* stat)
+{
+    if (0 == mStatCount) {
+        mLostCount       = 0;
+        mOutOfOrderCount = 0;
+        mRevivedCount    = 0;
+    }
+    stat->tot        = mTotCount;
+    stat->lost       = mLostCount;
+    stat->outOfOrder = mOutOfOrderCount;
+    stat->revived    = mRevivedCount;
+    stat->statCount  = mStatCount++;
+    return true;
+}
+
+//*******************************************************************************
+void UdpDataProtocol::setIssueSimulation(double loss, double jitter, double max_delay)
+{
+    mSimulatedLossRate       = loss;
+    mSimulatedJitterRate     = jitter;
+    mSimulatedJitterMaxDelay = max_delay;
+
+    std::random_device r;
+    mRndEngine   = std::default_random_engine(r());
+    mUniformDist = std::uniform_real_distribution<double>(0.0, 1.0);
+
+    cout << "Simulating network issues: "
+            "loss_rate="
+         << loss << ", jitter_rate=" << jitter << ", jitter_max_delay=" << max_delay
+         << endl;
+}
+
+//*******************************************************************************
+void UdpDataProtocol::sendPacketRedundancy(int8_t* full_redundant_packet,
+                                           int full_redundant_packet_size,
+                                           int full_packet_size)
+{
+    mJackTrip->readAudioBuffer(mAudioPacket);
+    int8_t* src = mAudioPacket;
+    if (1 < mChans) {
+        // Convert internal interleaved layout to non-interleaved
+        int N       = int(getAudioPacketSizeInBites() / mChans / mSmplSize);
+        int8_t* dst = mBuffer.data();
+        for (int n = 0; n < N; ++n) {
+            for (int c = 0; c < mChans; ++c) {
+                memcpy(dst + (n + c * N) * mSmplSize, src + (n * mChans + c) * mSmplSize,
+                       mSmplSize);
+            }
+        }
+        src = dst;
+    }
+    mJackTrip->putHeaderInOutgoingPacket(mFullPacket, src);
+
+    // Move older packets to end of array of redundant packets
+    std::memmove(full_redundant_packet + full_packet_size, full_redundant_packet,
+                 full_packet_size * (mUdpRedundancyFactor - 1));
+    // Copy new packet to the beginning of array
+    std::memcpy(full_redundant_packet, mFullPacket, full_packet_size);
+
+    // 10% (or other number) packet lost simulation.
+    // Uncomment the if to activate
+    //---------------------------------------------------------------------------------
+    //int random_integer = rand();
+    //if ( random_integer > (RAND_MAX/10) )
+    //{
+    sendPacket(reinterpret_cast<char*>(full_redundant_packet),
+               full_redundant_packet_size);
+    //}
+    //---------------------------------------------------------------------------------
+
+    mJackTrip->increaseSequenceNumber();
+}
+
+/*
+  The Redundancy Algorythmn works as follows. We send a packet that contains
+  a mUdpRedundancyFactor number of packets (header+audio). This big packet looks
+  as follows
+
+  ----------  ------------       -----------------------------------
+  | UDP[n] |  | UDP[n-1] |  ...  | UDP[n-(mUdpRedundancyFactor-1)] |
+  ----------  ------------       -----------------------------------
+
+  Then, for the new audio buffer, we shift everything to the right and send:
+
+  ----------  ------------       -------------------------------------
+  | UDP[n+1] |  | UDP[n] |  ...  | UDP[n-(mUdpRedundancyFactor-1)+1] |
+  ----------  ------------       -------------------------------------
+
+  etc...
+
+  For a redundancy factor of 4, this will look as follows:
+  ----------  ----------  ----------  ----------
+  | UDP[4] |  | UDP[3] |  | UDP[2] |  | UDP[1] |
+  ----------  ----------  ----------  ----------
+
+  ----------  ----------  ----------  ----------
+  | UDP[5] |  | UDP[4] |  | UDP[3] |  | UDP[2] |
+  ----------  ----------  ----------  ----------
+
+  ----------  ----------  ----------  ----------
+  | UDP[6] |  | UDP[5] |  | UDP[4] |  | UDP[3] |
+  ----------  ----------  ----------  ----------
+
+  etc...
+
+  Then, the receiving end checks if the firs packet in the list is the one it should use,
+  otherwise it continue reading the mUdpRedundancyFactor packets until it finds the one that
+  should come next (this can better perfected by just jumping until the correct packet).
+  If it has more than one packet that it hasn't yet received, it sends it to the soundcard
+  one by one.
+*/
+
+bool UdpDataProtocol::datagramAvailable()
+{
+    //Currently using a simplified version of the way QUdpSocket checks for datagrams.
+    char c;
+#if defined(_WIN32)
+    //Need to use the winsock version of the function for MSG_PEEK
+    WSABUF buffer;
+    buffer.buf  = &c;
+    buffer.len  = sizeof(c);
+    DWORD n     = 0;
+    DWORD flags = MSG_PEEK;
+    int ret     = WSARecv(mSocket, &buffer, 1, &n, &flags, NULL, NULL);
+    if (ret == 0) {
+        //True if no error,
+        return true;
+    } else {
+        //or if our error is that our buffer is too small.
+        int err = WSAGetLastError();
+        return (err == WSAEMSGSIZE);
+    }
+#else
+    ssize_t n;
+    n = ::recv(mSocket, &c, sizeof(c), MSG_PEEK);
+    //We have a datagram if our buffer is too small or if no error.
+    return (n != -1 || errno == EMSGSIZE);
+#endif
+}
diff --git a/src/UdpDataProtocol.h b/src/UdpDataProtocol.h
new file mode 100644 (file)
index 0000000..f49ffae
--- /dev/null
@@ -0,0 +1,247 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file UdpDataProtocol.h
+ * \author Juan-Pablo Caceres
+ * \date June 2008
+ */
+
+#ifndef __UDPDATAPROTOCOL_H__
+#define __UDPDATAPROTOCOL_H__
+
+#include <QHostAddress>
+#include <QMutex>
+#include <QThread>
+#include <atomic>
+#include <random>
+#include <stdexcept>
+#include <vector>
+
+#include "DataProtocol.h"
+#include "jacktrip_globals.h"
+#include "jacktrip_types.h"
+
+/** \brief UDP implementation of DataProtocol class
+ *
+ * The class has a <tt>bind port</tt> and a <tt>peer port</tt>. The meaning of these
+ * depends on the runModeT. If it's a SENDER, <tt>bind port</tt> is the source port and
+ * <tt>peer port</tt> is the destination port for each UDP packet. If it's a RECEIVER,
+ * the <tt>bind port</tt> destination port (for incoming packets) and the <tt>peer
+ * port</tt> is the source port.
+ *
+ * The SENDER and RECEIVER socket can share the same port/address pair (for compatibility
+ * with the JamLink boxes). This is achieved setting
+ * the resusable property in the socket for address and port. You have to
+ * externally check if the port is already binded if you want to avoid re-binding to the
+ * same port.
+ */
+class UdpDataProtocol : public DataProtocol
+{
+    Q_OBJECT;
+
+   public:
+    /** \brief The class constructor
+     * \param jacktrip Pointer to the JackTrip class that connects all classes (mediator)
+     * \param runmode Sets the run mode, use either SENDER or RECEIVER
+     * \param bind_port Port number to bind for this socket (this is the receive or send
+     * port depending on the runmode)
+     * \param peer_port Peer port number (this is the receive or send port depending on
+     * the runmode)
+     * \param udp_redundancy_factor Number of redundant packets
+     */
+    UdpDataProtocol(JackTrip* jacktrip, const runModeT runmode, int bind_port,
+                    int peer_port, unsigned int udp_redundancy_factor = 1);
+
+    /** \brief The class destructor
+     */
+    virtual ~UdpDataProtocol();
+
+    /// \brief Stops the execution of the Thread
+    virtual void stop();
+
+    /** \brief Set the Peer address to connect to
+     * \param peerHostOrIP IPv4 number or host name
+     */
+    void setPeerAddress(const char* peerHostOrIP);
+
+#if defined(_WIN32)
+    void setSocket(SOCKET& socket);
+#else
+    void setSocket(int& socket);
+#endif
+
+    void processControlPacket(const char* buf);
+
+    /** \brief Receives a packet. It blocks until a packet is received
+     *
+     * This function makes sure we receive a complete packet
+     * of size n
+     * \param buf Buffer to store the received packet
+     * \param n size of packet to receive
+     * \return number of bytes read, -1 on error
+     */
+    virtual int receivePacket(char* buf, const size_t n);
+
+    /** \brief Sends a packet
+     *
+     * This function meakes sure we send a complete packet
+     * of size n
+     * \param buf Buffer to send
+     * \param n size of packet to receive
+     * \return number of bytes read, -1 on error
+     */
+    virtual int sendPacket(const char* buf, const size_t n);
+
+    /** \brief Obtains the peer address from the first UDP packet received. This address
+     * is used by the SERVER mode to connect back to the client.
+     * \param peerHostAddress QHostAddress to store the peer address
+     * \param port Receiving port
+     */
+    // virtual void getPeerAddressFromFirstPacket(QHostAddress& peerHostAddress,
+    //                                            uint16_t& port);
+
+    /** \brief Sets the bind port number
+     */
+    void setBindPort(int port) { mBindPort = port; }
+
+    /** \brief Sets the peer port number
+     */
+    void setPeerPort(int port)
+    {
+        mPeerPort            = port;
+        mPeerAddr.sin_port   = htons(mPeerPort);
+        mPeerAddr6.sin6_port = htons(mPeerPort);
+    }
+
+    /** \brief Implements the Thread Loop. To start the thread, call start()
+     * ( DO NOT CALL run() )
+     *
+     * This function creates and binds all the socket and start the connection loop
+     * thread.
+     */
+    virtual void run();
+
+    virtual bool getStats(PktStat* stat);
+    virtual void setIssueSimulation(double loss, double jitter, double max_delay);
+
+   private slots:
+    void printUdpWaitedTooLong(int wait_msec);
+
+   signals:
+
+    /// \brief Signals when waiting every 10 milliseconds, with the total wait on
+    /// wait_msec
+    /// \param wait_msec Total wait in milliseconds
+    void signalWaitingTooLong(int wait_msec);
+    void signalUdpWaitingTooLong();
+
+    // private:
+   protected:
+    /** \brief Binds the UDP socket to the available address and specified port
+     */
+#if defined(_WIN32)
+    SOCKET bindSocket();
+#else
+    int bindSocket();
+#endif
+
+    /** \brief This function blocks until data is available for reading in the
+     * socket. The function will timeout after timeout_msec microseconds.
+     *
+     * This function is intended to replace QAbstractSocket::waitForReadyRead which has
+     * some problems with multithreading.
+     *
+     * \return returns true if there is data available for reading;
+     * otherwise it returns false (if an error occurred or the operation timed out)
+     */
+    void waitForReady(int timeout_msec);
+
+    /** \brief Redundancy algorithm at the receiving end
+     */
+    virtual void receivePacketRedundancy(int8_t* full_redundant_packet,
+                                         int full_redundant_packet_size,
+                                         int full_packet_size, uint16_t& current_seq_num,
+                                         uint16_t& last_seq_num, uint16_t& newer_seq_num);
+
+    /** \brief Redundancy algorithm at the sender's end
+     */
+    virtual void sendPacketRedundancy(int8_t* full_redundant_packet,
+                                      int full_redundant_packet_size,
+                                      int full_packet_size);
+
+   private:
+    void closeSocket();
+    bool datagramAvailable();
+
+    int mBindPort;            ///< Local Port number to Bind
+    int mPeerPort;            ///< Peer Port number
+    const runModeT mRunMode;  ///< Run mode, either SENDER or RECEIVER
+    bool mIPv6;               /// Use IPv6
+
+    QHostAddress mPeerAddress;  ///< The Peer Address
+    struct sockaddr_in mPeerAddr;
+    struct sockaddr_in6 mPeerAddr6;
+#if defined(_WIN32)
+    SOCKET mSocket;
+#else
+    int mSocket;
+#endif
+
+    int8_t* mAudioPacket;  ///< Buffer to store Audio Packets
+    int8_t* mFullPacket;   ///< Buffer to store Full Packet (audio+header)
+    std::vector<int8_t> mBuffer;
+    int mChans;
+    int mSmplSize;
+    int mLastOutOfOrderCount;
+    bool mInitialState;
+
+    unsigned int mUdpRedundancyFactor;  ///< Factor of redundancy
+    static QMutex sUdpMutex;            ///< Mutex to make thread safe the binding process
+
+    std::atomic<uint32_t> mTotCount;
+    std::atomic<uint32_t> mLostCount;
+    std::atomic<uint32_t> mOutOfOrderCount;
+    std::atomic<uint32_t> mRevivedCount;
+    uint32_t mStatCount;
+
+    uint8_t mControlPacketSize;
+    bool mStopSignalSent;
+
+    // packet loss/jitter simulation
+    double mSimulatedLossRate;
+    double mSimulatedJitterRate;
+    double mSimulatedJitterMaxDelay;
+    std::default_random_engine mRndEngine;
+    std::uniform_real_distribution<double> mUniformDist;
+};
+
+#endif  // __UDPDATAPROTOCOL_H__
diff --git a/src/UdpHubListener.cpp b/src/UdpHubListener.cpp
new file mode 100644 (file)
index 0000000..ecd588f
--- /dev/null
@@ -0,0 +1,674 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file UdpHubListener.cpp
+ * \author Juan-Pablo Caceres and Chris Chafe
+ * \date September 2008
+ */
+
+#include "UdpHubListener.h"
+
+#include <QFile>
+#include <QFileInfo>
+#include <QMutexLocker>
+#include <QSslKey>
+#include <QSslSocket>
+#include <QStringList>
+#include <QtEndian>
+#include <cstdlib>
+#include <cstring>
+#include <iostream>
+#include <stdexcept>
+
+#ifndef NO_JACK
+#include "JMess.h"
+#endif
+
+#include "JackTripWorker.h"
+#include "jacktrip_globals.h"
+
+using std::cout;
+using std::endl;
+
+bool UdpHubListener::sSigInt = false;
+
+//*******************************************************************************
+UdpHubListener::UdpHubListener(int server_port, int server_udp_port, QObject* parent)
+    : QObject(parent)
+    , mTcpServer(this)
+    , mServerPort(server_port)
+    , mServerUdpPort(server_udp_port)
+    ,  // final udp base port number
+    mRequireAuth(false)
+    , mStopped(false)
+#ifdef WAIR  // wair
+    , mWAIR(false)
+#endif  // endwhere
+    , mTotalRunningThreads(0)
+    , mHubPatchDescriptions(
+          {"server-to-clients", "client loopback", "client fan out/in but not loopback",
+           "reserved for TUB", "full mix", "no auto patching",
+           "client fan out/in, including server", "full mix, including server"})
+    , m_connectDefaultAudioPorts(false)
+    , mIOStatTimeout(0)
+{
+    // Register JackTripWorker with the hub listener
+    // mJTWorker = new JackTripWorker(this);
+    mJTWorkers = new QVector<JackTripWorker*>;
+    for (int i = 0; i < gMaxThreads; i++) {
+        mJTWorkers->append(nullptr);
+    }
+
+    qDebug() << "mThreadPool default maxThreadCount =" << QThread::idealThreadCount();
+    qDebug() << "mThreadPool maxThreadCount previously set to"
+             << QThread::idealThreadCount() * 16;
+
+    // Set the base dynamic port
+    // The Dynamic and/or Private Ports are those from 49152 through 65535
+    // mBasePort = ( rand() % ( (65535 - gMaxThreads) - 49152 ) ) + 49152;
+
+    // SoundWIRE ports open are UDP 61002-62000
+    // (server_port - gDefaultPort) apply TCP offset to UDP too
+    if (mServerUdpPort != 0) {
+        mBasePort = mServerUdpPort;
+    } else {
+        mBasePort = 61002 + (server_port - gDefaultPort);
+    }
+
+    cout << "JackTrip HUB SERVER: UDP Base Port set to " << mBasePort << endl;
+
+    mUnderRunMode      = JackTrip::WAVETABLE;
+    mBufferQueueLength = gDefaultQueueLength;
+
+    mBufferStrategy      = 1;
+    mBroadcastQueue      = 0;
+    mSimulatedLossRate   = 0.0;
+    mSimulatedJitterRate = 0.0;
+    mSimulatedDelayRel   = 0.0;
+
+    mUseRtUdpPriority = false;
+}
+
+//*******************************************************************************
+UdpHubListener::~UdpHubListener()
+{
+    mStopCheckTimer.stop();
+    QMutexLocker lock(&mMutex);
+    // delete mJTWorker;
+    for (int i = 0; i < gMaxThreads; i++) {
+        delete mJTWorkers->at(i);
+    }
+    delete mJTWorkers;
+}
+
+//*******************************************************************************
+// Now that the first handshake is with TCP server, if the address/peer port of
+// the client is already on the thread pool, it means that a new connection is
+// requested (the old was disconnected). So we have to remove that thread from
+// the pool and then connect again.
+void UdpHubListener::start()
+{
+    mStopped = false;
+
+    // Bind the TCP server
+    // ------------------------------
+    QObject::connect(&mTcpServer, &SslServer::newConnection, this,
+                     &UdpHubListener::receivedNewConnection);
+    if (!mTcpServer.listen(QHostAddress::Any, mServerPort)) {
+        QString error_message = QStringLiteral("TCP Socket Server on Port %1 ERROR: %2")
+                                    .arg(mServerPort)
+                                    .arg(mTcpServer.errorString());
+        emit signalError(error_message);
+        return;
+    }
+
+    if (mRequireAuth) {
+        cout << "JackTrip HUB SERVER: Enabling authentication" << endl;
+        // Check that SSL is available
+        bool error = false;
+        QString error_message;
+        if (!QSslSocket::supportsSsl()) {
+            error = true;
+            error_message =
+                "SSL not supported. Make sure you have the appropriate SSL "
+                "libraries\ninstalled to enable authentication.";
+        }
+
+        if (mCertFile.isEmpty()) {
+            error         = true;
+            error_message = QStringLiteral("No certificate file specified.");
+        } else if (mKeyFile.isEmpty()) {
+            error         = true;
+            error_message = QStringLiteral("No private key file specified.");
+        }
+
+        // Load our certificate and private key
+        if (!error) {
+            QFile certFile(mCertFile);
+            if (certFile.open(QIODevice::ReadOnly)) {
+                QSslCertificate cert(certFile.readAll());
+                if (!cert.isNull()) {
+                    mTcpServer.setCertificate(cert);
+                } else {
+                    error         = true;
+                    error_message = QStringLiteral("Unable to load certificate file.");
+                }
+            } else {
+                error         = true;
+                error_message = QStringLiteral("Could not find certificate file.");
+            }
+        }
+
+        if (!error) {
+            QFile keyFile(mKeyFile);
+            if (keyFile.open(QIODevice::ReadOnly)) {
+                QSslKey key(&keyFile, QSsl::Rsa);
+                if (!key.isNull()) {
+                    mTcpServer.setPrivateKey(key);
+                } else {
+                    error = true;
+                    error_message =
+                        QStringLiteral("Unable to read RSA private key file.");
+                }
+            } else {
+                error         = true;
+                error_message = QStringLiteral("Could not find RSA private key file.");
+            }
+        }
+
+        if (!error) {
+            QFileInfo credsInfo(mCredsFile);
+            if (!credsInfo.exists() || !credsInfo.isFile()) {
+                error         = true;
+                error_message = QStringLiteral("Could not find credentials file.");
+            }
+        }
+
+        if (error) {
+            emit signalError(error_message);
+            return;
+        }
+        mAuth.reset(new Auth(mCredsFile, true));
+    }
+
+    cout << "JackTrip HUB SERVER: Waiting for client connections..." << endl;
+    cout << "JackTrip HUB SERVER: Hub auto audio patch setting = " << mHubPatch << " ("
+         << mHubPatchDescriptions.at(mHubPatch).toStdString() << ")" << endl;
+    cout << "=======================================================" << endl;
+
+    // Start our monitoring timer
+    mStopCheckTimer.setInterval(200);
+    connect(&mStopCheckTimer, &QTimer::timeout, this, &UdpHubListener::stopCheck);
+    mStopCheckTimer.start();
+    emit signalStarted();
+}
+
+void UdpHubListener::receivedNewConnection()
+{
+    QSslSocket* clientSocket =
+        static_cast<QSslSocket*>(mTcpServer.nextPendingConnection());
+    connect(clientSocket, &QAbstractSocket::readyRead, this, [=] {
+        receivedClientInfo(clientSocket);
+    });
+    cout << "JackTrip HUB SERVER: Client Connection Received!" << endl;
+}
+
+void UdpHubListener::receivedClientInfo(QSslSocket* clientConnection)
+{
+    QHostAddress PeerAddress = clientConnection->peerAddress();
+    cout << "JackTrip HUB SERVER: Client Connect Received from Address : "
+         << PeerAddress.toString().toStdString() << endl;
+
+    // Get UDP port from client
+    // ------------------------
+    QString clientName = QString();
+    cout << "JackTrip HUB SERVER: Reading UDP port from Client..." << endl;
+    int peer_udp_port;
+    if (!clientConnection->isEncrypted()) {
+        if (clientConnection->bytesAvailable() < (int)sizeof(qint32)) {
+            // We don't have enough data. Wait for the next readyRead notification.
+            return;
+        }
+        peer_udp_port = readClientUdpPort(clientConnection, clientName);
+        // Use our peer port to check if we need to authenticate our client.
+        // (We use values above the max port number of 65535 to achieve this. Since the
+        // port number was always sent as a 32 bit integer, it meants we can squeeze this
+        // functionality in here without breaking older clients when authentication isn't
+        // required.)
+        if (peer_udp_port == Auth::OK) {
+            if (!mRequireAuth) {
+                // We're not using authentication. Let the client know and close the
+                // connection.
+                cout << "JackTrip HUB SERVER: Client attempting unnecessary "
+                        "authentication. Disconnecting."
+                     << endl;
+                sendUdpPort(clientConnection, Auth::NOTREQUIRED);
+                clientConnection->close();
+                clientConnection->deleteLater();
+                return;
+            }
+            // Initiate the SSL handshake, and wait for more data to arrive once it's been
+            // established.
+            sendUdpPort(clientConnection, Auth::OK);
+            clientConnection->startServerEncryption();
+            return;
+        } else if (mRequireAuth) {
+            // Let our client know we're not accepting connections without authentication.
+            cout << "JackTrip HUB SERVER: Client not authenticating. Disconnecting."
+                 << endl;
+            sendUdpPort(clientConnection, Auth::REQUIRED);
+            clientConnection->close();
+            clientConnection->deleteLater();
+            return;
+        }
+    } else {
+        // This branch executes when our socket is already in SSL mode and we're expecting
+        // to read our authentication data.
+        peer_udp_port = checkAuthAndReadPort(clientConnection, clientName);
+        if (peer_udp_port > 65535) {
+            // Our client hasn't provided valid credentials. Send an error code and close
+            // the connection.
+            cout << "JackTrip HUB SERVER: Authentication failed. Disconnecting." << endl;
+            sendUdpPort(clientConnection, peer_udp_port);
+            clientConnection->close();
+            clientConnection->deleteLater();
+            return;
+        }
+    }
+    // If we haven't received our port, wait for more data to arrive.
+    if (peer_udp_port == 0) {
+        return;
+    }
+
+    // At this stage, we should definitely only be dealing with a 16 bit integer. (Ignore
+    // the upper bytes.)
+    peer_udp_port &= 0xffff;
+    cout << "JackTrip HUB SERVER: Client UDP Port is = " << peer_udp_port << endl;
+
+    // Create a new JackTripWorker, but don't check if this is coming from an existing ip
+    // or port yet. We need to wait until we receive the port value from the UDP header to
+    // accommodate NAT.
+    // -----------------------------
+    int id = getJackTripWorker(PeerAddress.toString(), peer_udp_port, clientName);
+
+    // Assign server port and send it to Client
+    if (id != -1) {
+        cout << "JackTrip HUB SERVER: Sending Final UDP Port to Client: "
+             << mJTWorkers->at(id)->getServerPort() << endl;
+    }
+
+    if (id == -1
+        || sendUdpPort(clientConnection, mJTWorkers->at(id)->getServerPort()) == 0) {
+        clientConnection->close();
+        clientConnection->deleteLater();
+        releaseThread(id);
+        return;
+    }
+
+    // Close and mark socket for deletion
+    // ----------------------------------
+    clientConnection->close();
+    clientConnection->deleteLater();
+    cout << "JackTrip HUB SERVER: Client TCP Connection Closed!" << endl;
+
+    if (mIOStatTimeout > 0) {
+        mJTWorkers->at(id)->setIOStatTimeout(mIOStatTimeout);
+        mJTWorkers->at(id)->setIOStatStream(mIOStatStream);
+    }
+    mJTWorkers->at(id)->setBufferStrategy(mBufferStrategy);
+    mJTWorkers->at(id)->setNetIssuesSimulation(mSimulatedLossRate, mSimulatedJitterRate,
+                                               mSimulatedDelayRel);
+    mJTWorkers->at(id)->setBroadcast(mBroadcastQueue);
+    mJTWorkers->at(id)->setUseRtUdpPriority(mUseRtUdpPriority);
+    cout << "JackTrip HUB SERVER: Starting JackTripWorker..." << endl;
+    mJTWorkers->at(id)->start();
+}
+
+void UdpHubListener::stopCheck()
+{
+    if (mStopped || sSigInt) {
+        cout << "JackTrip HUB SERVER: Stopped" << endl;
+        mStopCheckTimer.stop();
+        mTcpServer.close();
+        stopAllThreads();
+        emit signalStopped();
+    }
+}
+
+//*******************************************************************************
+// Returns 0 on error
+int UdpHubListener::readClientUdpPort(QSslSocket* clientConnection, QString& clientName)
+{
+    if (gVerboseFlag)
+        cout << "Ready To Read From Client!" << endl;
+    // Read UDP Port Number from Server
+    // --------------------------------
+    qint32 udp_port;
+    int size       = sizeof(udp_port);
+    char* port_buf = new char[size];
+    clientConnection->read(port_buf, size);
+    udp_port = qFromLittleEndian<qint32>(port_buf);
+    delete[] port_buf;
+    // std::memcpy(&udp_port, port_buf, size);
+
+    // Check if we have enough data available to set our remote client name
+    // (Optional so that we don't block here with earlier clients that don't send it.)
+    if (clientConnection->bytesAvailable() == gMaxRemoteNameLength) {
+        char name_buf[gMaxRemoteNameLength];
+        clientConnection->read(name_buf, gMaxRemoteNameLength);
+        clientName = QString::fromUtf8((const char*)name_buf);
+    }
+
+    return udp_port;
+}
+
+int UdpHubListener::checkAuthAndReadPort(QSslSocket* clientConnection,
+                                         QString& clientName)
+{
+    if (gVerboseFlag)
+        cout << "Ready To Read Authentication Data From Client!" << endl;
+    // Because we don't know how long our username and password are, we have to peek at
+    // our data to read the expected lengths and know if we have enough to work with.
+
+    // Currently, we expect to receive:
+    // 4 bytes: LE int giving our port number.
+    // 64 bytes: Maximum 63 byte jack client name (with null terminator).
+    // 4 bytes: Username length, not including null terminator.
+    // 4 bytes: Password length, not including null terminator.
+    // Variable length: Our username and password, each with added null terminator.
+
+    int size = gMaxRemoteNameLength + (3 * sizeof(qint32));
+    if (clientConnection->bytesAvailable() < size) {
+        return 0;
+    }
+
+    qint32 usernameLength, passwordLength;
+    char* buf = new char[size];
+    clientConnection->peek(buf, size);
+    usernameLength =
+        qFromLittleEndian<qint32>(buf + gMaxRemoteNameLength + sizeof(qint32));
+    passwordLength =
+        qFromLittleEndian<qint32>(buf + gMaxRemoteNameLength + (2 * sizeof(qint32)));
+    delete[] buf;
+
+    // Check if we have enough data.
+    if (clientConnection->bytesAvailable() < size + usernameLength + passwordLength + 2) {
+        return 0;
+    }
+
+    // Get our port.
+    qint32 udp_port;
+    size           = sizeof(udp_port);
+    char* port_buf = new char[size];
+    clientConnection->read(port_buf, size);
+    udp_port = qFromLittleEndian<qint32>(port_buf);
+
+    // Then our jack client name.
+    char name_buf[gMaxRemoteNameLength];
+    clientConnection->read(name_buf, gMaxRemoteNameLength);
+    clientName = QString::fromUtf8((const char*)name_buf);
+
+    // We can discard our username and password length since we already have them.
+    clientConnection->read(port_buf, size);
+    clientConnection->read(port_buf, size);
+    delete[] port_buf;
+
+    // And then get our username and password.
+    QString username, password;
+    char* username_buf = new char[usernameLength + 1];
+    clientConnection->read(username_buf, usernameLength + 1);
+    username = QString::fromUtf8((const char*)username_buf);
+    delete[] username_buf;
+
+    char* password_buf = new char[passwordLength + 1];
+    clientConnection->read(password_buf, passwordLength + 1);
+    password = QString::fromUtf8((const char*)password_buf);
+    delete[] password_buf;
+
+    // Check if our credentials are valid, and return either an error code or our port.
+    Auth::AuthResponseT response = mAuth->checkCredentials(username, password);
+    if (response == Auth::OK) {
+        return udp_port;
+    } else {
+        return response;
+    }
+}
+
+//*******************************************************************************
+int UdpHubListener::sendUdpPort(QSslSocket* clientConnection, qint32 udp_port)
+{
+    // Send Port Number to Client
+    // --------------------------
+    char port_buf[sizeof(udp_port)];
+    // std::memcpy(port_buf, &udp_port, sizeof(udp_port));
+    qToLittleEndian<qint32>(udp_port, port_buf);
+    if (udp_port < 65536) {
+        std::cout << "Writing port: " << udp_port << std::endl;
+    } else {
+        std::cout << "Writing auth response: " << udp_port << std::endl;
+    }
+    clientConnection->write(port_buf, sizeof(udp_port));
+    while (clientConnection->bytesToWrite() > 0) {
+        if (clientConnection->state() == QAbstractSocket::ConnectedState) {
+            clientConnection->waitForBytesWritten(-1);
+        } else {
+            return 0;
+        }
+    }
+    return 1;
+    // cout << "Port sent to Client" << endl;
+}
+
+//*******************************************************************************
+void UdpHubListener::bindUdpSocket(QUdpSocket& udpsocket, int port)
+{
+    // QHostAddress::Any : let the kernel decide the active address
+    if (!udpsocket.bind(QHostAddress::Any, port, QUdpSocket::DefaultForPlatform)) {
+        // std::cerr << "ERROR: could not bind UDP socket" << endl;
+        // std::exit(1);
+        throw std::runtime_error("Could not bind UDP socket. It may be already binded.");
+    } else {
+        cout << "UDP Socket Receiving in Port: " << port << endl;
+    }
+}
+
+//*******************************************************************************
+int UdpHubListener::getJackTripWorker(const QString& address,
+                                      [[maybe_unused]] uint16_t port, QString& clientName)
+{
+    // Find our first empty slot in our vector of worker object pointers.
+    // Return -1 if we have no space left for additional threads, or the index of the new
+    // JackTripWorker.
+    QMutexLocker lock(&mMutex);
+    int id = -1;
+    for (int i = 0; i < gMaxThreads; i++) {
+        if (mJTWorkers->at(i) == nullptr) {
+            id = i;
+            i  = gMaxThreads;
+        }
+    }
+
+    if (id >= 0) {
+        mTotalRunningThreads++;
+        if (mAppendThreadID) {
+            clientName = clientName + QStringLiteral("_%1").arg(id + 1);
+        }
+        mJTWorkers->replace(id,
+                            new JackTripWorker(this, mBufferQueueLength, mUnderRunMode,
+                                               mAudioBitResolution, clientName));
+        mJTWorkers->at(id)->setJackTrip(
+            id, address, mBasePort + id,
+            0,  // Set client port to 0 initially until we receive a UDP packet.
+            m_connectDefaultAudioPorts);  //
+    }
+
+    return id;
+}
+
+//*******************************************************************************
+int UdpHubListener::getPoolID(const QString& address, uint16_t port)
+{
+    QMutexLocker lock(&mMutex);
+    // for (int id = 0; id<mThreadPool.activeThreadCount(); id++ )
+    for (int id = 0; id < gMaxThreads; id++) {
+        if (mJTWorkers->at(id) != nullptr
+            && address == mJTWorkers->at(id)->getClientAddress()
+            && port == mJTWorkers->at(id)->getServerPort()) {
+            return id;
+        }
+    }
+    return -1;
+}
+
+#ifndef NO_JACK
+void UdpHubListener::registerClientWithPatcher(QString& clientName)
+{
+    cout << "JackTrip HUB SERVER: Total Running Threads:  " << mTotalRunningThreads
+         << endl;
+    cout << "===============================================================" << endl;
+#ifdef WAIR  // WAIR
+    if (isWAIR())
+        connectMesh(true);  // invoked with -Sw
+#endif                      // endwhere
+    // qDebug() << "mPeerAddress" << mActiveAddress[id].address <<
+    // mActiveAddress[id].port;
+    connectPatch(true, clientName);
+}
+
+void UdpHubListener::unregisterClientWithPatcher(QString& clientName)
+{
+#ifdef WAIR  // wair
+    if (isWAIR())
+        connectMesh(false);  // invoked with -Sw
+#endif                       // endwhere
+    connectPatch(false, clientName);
+}
+#endif  // NO_JACK
+
+//*******************************************************************************
+int UdpHubListener::releaseThread(int id)
+{
+    QMutexLocker lock(&mMutex);
+    mTotalRunningThreads--;
+#ifdef WAIR  // wair
+    if (isWAIR())
+        connectMesh(false);  // invoked with -Sw
+#endif                       // endwhere
+    mJTWorkers->at(id)->deleteLater();
+    mJTWorkers->replace(id, nullptr);
+    return 0;  /// \todo Check if we really need to return an argument here
+}
+
+void UdpHubListener::releaseDuplicateThreads(JackTripWorker* worker,
+                                             uint16_t actual_peer_port)
+{
+    QMutexLocker lock(&mMutex);
+    // Now that we have our actual port, remove any duplicate workers.
+    for (int i = 0; i < gMaxThreads; i++) {
+        if (mJTWorkers->at(i) != nullptr
+            && worker->getClientAddress() == mJTWorkers->at(i)->getClientAddress()
+            && actual_peer_port == mJTWorkers->at(i)->getClientPort()) {
+            mJTWorkers->at(i)->stopThread();
+            mJTWorkers->at(i)->deleteLater();
+            mJTWorkers->replace(i, nullptr);
+            mTotalRunningThreads--;
+        }
+    }
+    worker->setClientPort(actual_peer_port);
+}
+
+#ifndef NO_JACK
+#ifdef WAIR  // wair
+//*******************************************************************************
+void UdpHubListener::connectMesh(bool spawn)
+{
+    cout << ((spawn) ? "spawning" : "releasing") << " jacktripWorker so change mesh"
+         << endl;
+    JMess tmp;
+    tmp.connectSpawnedPorts(
+        gDefaultNumInChannels);  // change gDefaultNumInChannels if more than stereo LAIR
+                                 // interconnects
+    //  tmp.disconnectAll();
+    //  enumerateRunningThreadIDs();
+}
+
+//*******************************************************************************
+void UdpHubListener::enumerateRunningThreadIDs()
+{
+    for (int id = 0; id < gMaxThreads; id++) {
+        if (mJTWorkers->at(id) != nullptr) {
+            qDebug() << id;
+        }
+    }
+}
+#endif  // endwhere
+
+void UdpHubListener::connectPatch(bool spawn, const QString& clientName)
+{
+    if ((getHubPatch() == JackTrip::NOAUTO)
+        || (getHubPatch() == JackTrip::SERVERTOCLIENT && !m_connectDefaultAudioPorts)) {
+        cout << ((spawn) ? "spawning" : "releasing")
+             << " jacktripWorker (auto hub patching disabled)" << endl;
+        return;
+    }
+    cout << ((spawn) ? "spawning" : "releasing") << " jacktripWorker so change patch"
+         << endl;
+    if (getHubPatch() == JackTrip::RESERVEDMATRIX) {
+        // This is a special patch for the TU Berlin ensemble.
+        // Use the old JMess mechanism.
+        JMess tmp;
+        tmp.connectTUB(gDefaultNumInChannels);
+        // FIXME: need change to gDefaultNumInChannels if more than stereo
+    } else {
+        if (spawn) {
+            mPatcher.registerClient(clientName);
+        } else {
+            mPatcher.unregisterClient(clientName);
+        }
+    }
+}
+#endif  // NO_JACK
+
+void UdpHubListener::stopAllThreads()
+{
+    QVectorIterator<JackTripWorker*> iterator(*mJTWorkers);
+    while (iterator.hasNext()) {
+        if (iterator.peekNext() != nullptr) {
+            iterator.next()->stopThread();
+        } else {
+            iterator.next();
+        }
+    }
+}
+// TODO:
+// USE bool QAbstractSocket::isValid () const to check if socket is connect. if not, exit
+// loop
diff --git a/src/UdpHubListener.h b/src/UdpHubListener.h
new file mode 100644 (file)
index 0000000..a810025
--- /dev/null
@@ -0,0 +1,274 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file UdpHubListener.h
+ * \author Juan-Pablo Caceres and Chris Chafe
+ * \date September 2008
+ */
+
+#ifndef __UDPHUBLISTENER_H__
+#define __UDPHUBLISTENER_H__
+
+#include <QHostAddress>
+#include <QMutex>
+#include <QThread>
+#include <QThreadPool>
+#include <QUdpSocket>
+#include <fstream>
+#include <iostream>
+#include <stdexcept>
+
+#include "JackTrip.h"
+#include "jacktrip_globals.h"
+#include "jacktrip_types.h"
+#ifndef NO_JACK
+#include "Patcher.h"
+#endif
+#include "Auth.h"
+#include "SslServer.h"
+
+class JackTripWorker;  // forward declaration
+class Settings;
+
+/** \brief Hub UDP listener on the Server.
+ *
+ * This creates a server that will listen on the well know port (the server port) and will
+ * spawn JackTrip threads into the Thread pool. Clients request a connection.
+ */
+class UdpHubListener : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    UdpHubListener(int server_port = gServerUdpPort, int server_udp_port = 0,
+                   QObject* parent = nullptr);
+    virtual ~UdpHubListener();
+
+    /// \brief Starts the TCP server
+    void start();
+
+    /// \brief Stops the execution of the Thread
+    void stop() { mStopped = true; }
+
+#ifndef NO_JACK
+    void registerClientWithPatcher(QString& clientName);
+    void unregisterClientWithPatcher(QString& clientName);
+#endif
+    int releaseThread(int id);
+    void releaseDuplicateThreads(JackTripWorker* worker, uint16_t actual_peer_port);
+
+    void setConnectDefaultAudioPorts(bool connectDefaultAudioPorts)
+    {
+        // Only allow us to override this in the default patching mode. (Or TUB mode.)
+        //(Allows -D to continue to function as a synonym for -p5.)
+        if (mHubPatch == JackTrip::SERVERTOCLIENT
+            || mHubPatch == JackTrip::RESERVEDMATRIX) {
+            m_connectDefaultAudioPorts = connectDefaultAudioPorts;
+        }
+    }
+
+    static void sigIntHandler([[maybe_unused]] int unused)
+    {
+        std::cout << std::endl << "Shutting Down..." << std::endl;
+        sSigInt = true;
+    }
+
+   private slots:
+    void testReceive()
+    {
+        std::cout << "========= TEST RECEIVE SLOT ===========" << std::endl;
+    }
+    void receivedNewConnection();
+    void stopCheck();
+
+   signals:
+    void signalStarted();
+    void signalRemoveThread(int id);
+    void signalStopped();
+    void signalError(const QString& errorMessage);
+
+   private:
+    /** \brief Binds a QUdpSocket. It chooses the available (active) interface.
+     * \param udpsocket a QUdpSocket
+     * \param port Port number
+     */
+    void receivedClientInfo(QSslSocket* clientConnection);
+
+    static void bindUdpSocket(QUdpSocket& udpsocket, int port);
+
+    int readClientUdpPort(QSslSocket* clientConnection, QString& clientName);
+    int checkAuthAndReadPort(QSslSocket* clientConnection, QString& clientName);
+    int sendUdpPort(QSslSocket* clientConnection, qint32 udp_port);
+
+    /**
+     * \brief Send the JackTripWorker to the thread pool. This will run
+     * until it's done. We still have control over the prototype class.
+     * \param id Identification Number
+     */
+    // void sendToPoolPrototype(int id);
+
+    /**
+     * \brief Check if address is already handled and reuse or create
+     * a JackTripWorker as appropriate
+     * \param address as string (IPv4 or IPv6)
+     * \return id number of JackTripWorker
+     */
+    int getJackTripWorker(const QString& address, uint16_t port, QString& clientName);
+
+    /** \brief Returns the ID of the client in the pool. If the client
+     * is not in the pool yet, returns -1.
+     */
+    int getPoolID(const QString& address, uint16_t port);
+
+    void stopAllThreads();
+
+    // QUdpSocket mUdpHubSocket; ///< The UDP socket
+    // QHostAddress mPeerAddress; ///< The Peer Address
+
+    // JackTripWorker* mJTWorker; ///< Class that will be used as prototype
+    QVector<JackTripWorker*>* mJTWorkers;  ///< Vector of JackTripWorkers
+
+    SslServer mTcpServer;
+    int mServerPort;     //< Server known port number
+    int mServerUdpPort;  //< Server udp base port number
+    int mBasePort;
+    // addressPortNameTriple mActiveAddress[gMaxThreads]; ///< Active address pool
+    // addresses QHash<QString, uint16_t> mActiveAddressPortPair;
+
+    bool mRequireAuth;
+    QString mCertFile;
+    QString mKeyFile;
+    QString mCredsFile;
+    QScopedPointer<Auth> mAuth;
+
+    /// Boolean stop the execution of the thread
+    volatile bool mStopped;
+    static bool sSigInt;
+    QTimer mStopCheckTimer;
+    int mTotalRunningThreads;  ///< Number of Threads running in the pool
+    QMutex mMutex;
+    JackTrip::underrunModeT mUnderRunMode;
+    AudioInterface::audioBitResolutionT mAudioBitResolution;
+    int mBufferQueueLength;
+
+    QStringList mHubPatchDescriptions;
+    bool m_connectDefaultAudioPorts;
+#ifndef NO_JACK
+    Patcher mPatcher;
+#endif
+    bool mStereoUpmix;
+
+    int mIOStatTimeout;
+    QSharedPointer<std::ostream> mIOStatStream;
+
+    int mBufferStrategy;
+    int mBroadcastQueue;
+    double mSimulatedLossRate;
+    double mSimulatedJitterRate;
+    double mSimulatedDelayRel;
+    bool mUseRtUdpPriority;
+
+#ifdef WAIR  // wair
+    bool mWAIR;
+    void connectMesh(bool spawn);
+    void enumerateRunningThreadIDs();
+
+   public:
+    void setWAIR(int b) { mWAIR = b; }
+    bool isWAIR() { return mWAIR; }
+#endif  // endwhere
+#ifndef NO_JACK
+    void connectPatch(bool spawn, const QString& clientName);
+#endif
+
+   public:
+    void setRequireAuth(bool requireAuth) { mRequireAuth = requireAuth; }
+    void setCertFile(const QString& certFile) { mCertFile = certFile; }
+    void setKeyFile(const QString& keyFile) { mKeyFile = keyFile; }
+    void setCredsFile(const QString& credsFile) { mCredsFile = credsFile; }
+
+    unsigned int mHubPatch;
+    void setHubPatch(unsigned int p)
+    {
+        mHubPatch = p;
+#ifndef NO_JACK
+        mPatcher.setPatchMode(static_cast<JackTrip::hubConnectionModeT>(p));
+#endif
+        // Set the correct audio port connection setting for our chosen patch mode.
+        if (mHubPatch == JackTrip::SERVERTOCLIENT || mHubPatch == JackTrip::SERVFOFI
+            || mHubPatch == JackTrip::SERVFULLMIX) {
+            m_connectDefaultAudioPorts = true;
+        } else {
+            m_connectDefaultAudioPorts = false;
+        }
+    }
+    unsigned int getHubPatch() { return mHubPatch; }
+
+    void setStereoUpmix([[maybe_unused]] bool upmix)
+    {
+#ifndef NO_JACK
+        mPatcher.setStereoUpmix(upmix);
+#endif
+    }
+
+    void setUnderRunMode(JackTrip::underrunModeT UnderRunMode)
+    {
+        mUnderRunMode = UnderRunMode;
+    }
+    void setAudioBitResolution(AudioInterface::audioBitResolutionT AudioBitResolution)
+    {
+        mAudioBitResolution = AudioBitResolution;
+    }
+    void setBufferQueueLength(int BufferQueueLength)
+    {
+        mBufferQueueLength = BufferQueueLength;
+    }
+
+    void setIOStatTimeout(int timeout) { mIOStatTimeout = timeout; }
+    void setIOStatStream(QSharedPointer<std::ostream> statStream)
+    {
+        mIOStatStream = statStream;
+    }
+
+    void setBufferStrategy(int BufferStrategy) { mBufferStrategy = BufferStrategy; }
+    void setNetIssuesSimulation(double loss, double jitter, double delay_rel)
+    {
+        mSimulatedLossRate   = loss;
+        mSimulatedJitterRate = jitter;
+        mSimulatedDelayRel   = delay_rel;
+    }
+    void setBroadcast(int broadcast_queue) { mBroadcastQueue = broadcast_queue; }
+    void setUseRtUdpPriority(bool use) { mUseRtUdpPriority = use; }
+    bool mAppendThreadID = false;
+};
+
+#endif  //__UDPHUBLISTENER_H__
diff --git a/src/Volume.cpp b/src/Volume.cpp
new file mode 100644 (file)
index 0000000..f15f9aa
--- /dev/null
@@ -0,0 +1,133 @@
+//*****************************************************************
+/*
+  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 Volume.cpp
+ * \author Matt Horton
+ * \date September 2022
+ * \license MIT
+ */
+
+#include "Volume.h"
+
+#include <iostream>
+
+#include "jacktrip_types.h"
+#include "volumedsp.h"
+
+//*******************************************************************************
+Volume::Volume(int numchans, bool verboseFlag) : mNumChannels(numchans)
+{
+    setVerbose(verboseFlag);
+    for (int i = 0; i < mNumChannels; i++) {
+        volumedsp* dsp_ptr = new volumedsp;
+        APIUI* ui_ptr      = new APIUI;
+        volumeP.push_back(dsp_ptr);
+        volumeUIP.push_back(ui_ptr);  // #included in volumedsp.h
+        dsp_ptr->buildUserInterface(ui_ptr);
+    }
+}
+
+//*******************************************************************************
+Volume::~Volume()
+{
+    for (int i = 0; i < mNumChannels; i++) {
+        delete static_cast<volumedsp*>(volumeP[i]);
+        delete static_cast<APIUI*>(volumeUIP[i]);
+    }
+    volumeP.clear();
+    volumeUIP.clear();
+}
+
+//*******************************************************************************
+void Volume::init(int samplingRate, int bufferSize)
+{
+    ProcessPlugin::init(samplingRate, bufferSize);
+    fs = float(fSamplingFreq);
+
+    for (int i = 0; i < mNumChannels; i++) {
+        static_cast<volumedsp*>(volumeP[i])
+            ->init(fs);  // compression filter parameters depend on sampling rate
+        APIUI* ui_ptr = static_cast<APIUI*>(volumeUIP[i]);
+        int ndx       = ui_ptr->getParamIndex("Volume");
+        ui_ptr->setParamValue(ndx, mVolMultiplier);
+        ndx = ui_ptr->getParamIndex("Mute");
+        ui_ptr->setParamValue(ndx, isMuted ? 1 : 0);
+    }
+    inited = true;
+}
+
+//*******************************************************************************
+void Volume::compute(int nframes, float** inputs, float** outputs)
+{
+    if (!inited) {
+        std::cerr << "*** Volume " << this << ": init never called! Doing it now.\n";
+        init(0, 0);
+    }
+
+    for (int i = 0; i < mNumChannels; i++) {
+        /* Run the signal through Faust  */
+        static_cast<volumedsp*>(volumeP[i])->compute(nframes, &inputs[i], &outputs[i]);
+    }
+}
+
+//*******************************************************************************
+void Volume::updateNumChannels(int nChansIn, int nChansOut)
+{
+    if (outgoingPluginToNetwork) {
+        mNumChannels = nChansIn;
+    } else {
+        mNumChannels = nChansOut;
+    }
+}
+
+void Volume::volumeUpdated(float multiplier)
+{
+    // maps 0.0-1.0 to a -40 dB to 0 dB range
+    // update if volumedsp.dsp and/or volumedsp.h
+    // change their ranges
+    mVolMultiplier = 40.0 * multiplier - 40.0;
+    for (int i = 0; i < mNumChannels; i++) {
+        APIUI* ui_ptr = static_cast<APIUI*>(volumeUIP[i]);
+        int ndx       = ui_ptr->getParamIndex("Volume");
+        ui_ptr->setParamValue(ndx, mVolMultiplier);
+    }
+}
+
+void Volume::muteUpdated(bool muted)
+{
+    isMuted = muted;
+    for (int i = 0; i < mNumChannels; i++) {
+        APIUI* ui_ptr = static_cast<APIUI*>(volumeUIP[i]);
+        int ndx       = ui_ptr->getParamIndex("Mute");
+        ui_ptr->setParamValue(ndx, isMuted ? 1 : 0);
+    }
+}
\ No newline at end of file
diff --git a/src/Volume.h b/src/Volume.h
new file mode 100644 (file)
index 0000000..8e8e12f
--- /dev/null
@@ -0,0 +1,81 @@
+//*****************************************************************
+/*
+  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 Volume.h
+ * \author Matt Horton
+ * \date September 2022
+ * \license MIT
+ */
+
+#ifndef __VOLUME_H__
+#define __VOLUME_H__
+
+#include <QObject>
+#include <vector>
+
+#include "ProcessPlugin.h"
+
+/** \brief The Volume plugin adjusts the level of the signal via multiplication
+ */
+class Volume : public ProcessPlugin
+{
+    Q_OBJECT;
+
+   public:
+    /// \brief The class constructor sets the number of channels to measure
+    Volume(int numchans, bool verboseFlag = false);
+
+    /// \brief The class destructor
+    virtual ~Volume();
+
+    void init(int samplingRate, int bufferSize) override;
+    int getNumInputs() override { return (mNumChannels); }
+    int getNumOutputs() override { return (mNumChannels); }
+    void compute(int nframes, float** inputs, float** outputs) override;
+    const char* getName() const override { return "Volume"; };
+
+    void updateNumChannels(int nChansIn, int nChansOut) override;
+
+   public slots:
+    void volumeUpdated(float multiplier);
+    void muteUpdated(bool muted);
+
+   private:
+    std::vector<void*> volumeP;
+    std::vector<void*> volumeUIP;
+    float fs;
+    int mNumChannels;
+    float mVolMultiplier = 0.0;
+    bool isMuted         = false;
+};
+
+#endif
\ No newline at end of file
diff --git a/src/WaitFreeFrameBuffer.h b/src/WaitFreeFrameBuffer.h
new file mode 100644 (file)
index 0000000..41887fa
--- /dev/null
@@ -0,0 +1,87 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+  JackTrip Labs, Inc.
+
+  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 WaitFreeFrameBuffer.h
+ * \author Mike Dickey
+ * \date May 2023
+ */
+
+#ifndef __WAITFREEFRAMEBUFFER_H__
+#define __WAITFREEFRAMEBUFFER_H__
+
+#include <cstring>
+
+#include "WaitFreeRingBuffer.h"
+
+// WaitFreeFrameBuffer is a wait-free FIFO data structure for audio frames
+// that only supports a single producer and a single consumer
+template<std::size_t Size = 64>
+class WaitFreeFrameBuffer : public WaitFreeRingBuffer<int8_t*, Size>
+{
+   public:
+    /// @brief constructor requires number of bytes per frame
+    /// @param bytesPerFrame
+    WaitFreeFrameBuffer(std::size_t bytesPerFrame)
+        : WaitFreeRingBuffer<int8_t*, Size>(), mBytesPerFrame(bytesPerFrame)
+    {
+        for (std::size_t n = 0; n < Size; ++n) {
+            this->mRing[n] = new int8_t[mBytesPerFrame];
+        }
+    }
+
+    /// @brief virtual destructor
+    virtual ~WaitFreeFrameBuffer()
+    {
+        for (std::size_t n = 0; n < Size; ++n) {
+            delete[] this->mRing[n];
+        }
+    }
+
+    /// returns bytes stored in each frame
+    inline std::size_t getBytesPerFrame() const { return mBytesPerFrame; }
+
+   private:
+    virtual void setItem(int8_t*& item, int8_t* const& value)
+    {
+        ::memcpy(item, value, mBytesPerFrame);
+    }
+
+    virtual void getItem(int8_t* const& item, int8_t*& value)
+    {
+        ::memcpy(value, item, mBytesPerFrame);
+    }
+
+    std::size_t mBytesPerFrame;
+};
+
+#endif  // __WAITFREEFRAMEBUFFER_H__
diff --git a/src/WaitFreeRingBuffer.h b/src/WaitFreeRingBuffer.h
new file mode 100644 (file)
index 0000000..1b415d4
--- /dev/null
@@ -0,0 +1,158 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+  JackTrip Labs, Inc.
+
+  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 WaitFreeRingBuffer.h
+ * \author Mike Dickey
+ * \date May 2023
+ */
+
+// Adapted from https://www.boost.org/doc/libs/1_64_0/doc/html/atomic/usage_examples.html
+
+#ifndef __WAITFREERINGBUFFER_H__
+#define __WAITFREERINGBUFFER_H__
+
+#include <atomic>
+
+// WaitFreeRingBuffer is a simple wait-free FIFO data structure
+// that only supports a single producer and a single consumer
+// and loosely tracks xrun stats on a low-cost basis
+template<typename T = int8_t*, std::size_t Size = 64>
+class WaitFreeRingBuffer
+{
+   public:
+    /// @brief default constructor
+    WaitFreeRingBuffer() : mHeadPtr(0), mTailPtr(0), mUnderruns(0), mOverruns(0) {}
+
+    /// @brief virtual destructor
+    virtual ~WaitFreeRingBuffer() {}
+
+    /// @brief push a value into the buffer
+    /// @param value the next free item in buffer is assigned to this
+    /// @return new number of items in the buffer if success, or 0 if overrun
+    std::size_t push(const T& value)
+    {
+        std::size_t tail      = mTailPtr.load(std::memory_order_acquire);
+        std::size_t head      = mHeadPtr.load(std::memory_order_relaxed);
+        std::size_t next_head = next(head);
+        if (next_head == tail) {
+            ++mOverruns;
+            return 0;
+        }
+        setItem(mRing[head], value);
+        mHeadPtr.store(next_head, std::memory_order_release);
+        return (next_head >= tail) ? (next_head - tail) : (next_head + (Size - tail));
+    }
+
+    /// @brief pop a value from the buffer
+    /// @param value will be assigned to the next available item in buffer
+    /// @return false if underrun
+    bool pop(T& value)
+    {
+        std::size_t tail = mTailPtr.load(std::memory_order_relaxed);
+        if (tail == mHeadPtr.load(std::memory_order_acquire)) {
+            ++mUnderruns;
+            return false;
+        }
+        getItem(mRing[tail], value);
+        mTailPtr.store(next(tail), std::memory_order_release);
+        return true;
+    }
+
+    /// @brief clear xrun stats only
+    void clearStats() { mUnderruns = mOverruns = 0; }
+
+    /// @brief clear all frames and xrun stats
+    void clear()
+    {
+        mHeadPtr.store(0, std::memory_order_release);
+        mTailPtr.store(0, std::memory_order_release);
+        clearStats();
+    }
+
+    /// returns the number of items in the buffer
+    std::size_t size() const
+    {
+        std::size_t head = mHeadPtr.load(std::memory_order_relaxed);
+        std::size_t tail = mTailPtr.load(std::memory_order_relaxed);
+        return (head >= tail) ? (head - tail) : (head + (Size - tail));
+    }
+
+    /// returns true if the buffer is empty
+    bool empty() const
+    {
+        std::size_t head = mHeadPtr.load(std::memory_order_relaxed);
+        std::size_t tail = mTailPtr.load(std::memory_order_relaxed);
+        return head == tail;
+    }
+
+    /// returns maximum capacity for the buffer
+    inline std::size_t capacity() const { return Size; }
+
+    /// returns number of times that a pop failed due to it being empty
+    inline std::size_t getUnderruns() const { return mUnderruns; }
+
+    /// returns number of times that a push failed due to it being full
+    inline std::size_t getOverruns() const { return mOverruns; }
+
+   protected:
+    /// @brief assigns an item in the buffer to value
+    /// @param item
+    /// @param value
+    virtual void setItem(T& item, const T& value) { item = value; }
+
+    /// @brief assigns value to an item in the buffer
+    /// @param item
+    /// @param value
+    virtual void getItem(const T& item, T& value) { value = item; }
+
+    /// items stored in the buffer
+    T mRing[Size];
+
+   private:
+    /// returns next item in the buffer
+    std::size_t next(std::size_t current) { return (current + 1) % Size; }
+
+    /// position of the head of the buffer
+    std::atomic<std::size_t> mHeadPtr;
+
+    /// position of the tail of the buffer
+    std::atomic<std::size_t> mTailPtr;
+
+    /// approximate number of underruns
+    std::size_t mUnderruns;
+
+    /// approximate number of overruns
+    std::size_t mOverruns;
+};
+
+#endif  // __WAITFREERINGBUFFER_H__
diff --git a/src/build b/src/build
new file mode 100755 (executable)
index 0000000..4e38032
--- /dev/null
+++ b/src/build
@@ -0,0 +1,20 @@
+#!/bin/bash
+if [[ -t 1 ]]; then
+       echo
+       echo -e "\033[1;31mIMPORTANT\033[0m"
+       echo "The build script is now located in the root jacktrip folder and should"
+       echo "be run from there in future. (This script will eventually be removed and"
+       echo "is included for compatibility during the transition.)"
+       echo
+       echo "In future, after cloning the repository with git use the following commands:"
+       echo -e "\033[1m$ cd jacktrip"
+       echo "$ ./build"
+       echo -e "$ cd builddir\033[0m"
+       echo
+       echo "This will build jacktrip and take you to the build directory."
+       echo
+       read -n1 -rsp "Press any key to continue the build process..."
+       echo
+fi
+cd ../
+./build $@
diff --git a/src/compressordsp.h b/src/compressordsp.h
new file mode 100644 (file)
index 0000000..fc71611
--- /dev/null
@@ -0,0 +1,2256 @@
+/* ------------------------------------------------------------
+author: "Julius Smith"
+license: "MIT Style STK-4.2"
+name: "compressor"
+version: "0.0"
+Code generated with Faust 2.41.1 (https://faust.grame.fr)
+Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn compressordsp -es 1 -mcd
+16 -single -ftz 0
+------------------------------------------------------------ */
+
+#ifndef __compressordsp_H__
+#define __compressordsp_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 <string>
+#include <vector>
+
+/************************************************************************
+ ************************************************************************
+    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.41.1"
+
+// 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<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> 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 <xmmintrin.h>
+#endif
+
+class FAUST_API ScopedNoDenormals
+{
+   private:
+    intptr_t fpsr;
+
+    void setFpStatusRegister(intptr_t fpsr_aux) noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("msr fpcr, %0" : : "ri"(fpsr_aux));
+#elif defined(__SSE__)
+        _mm_setcsr(static_cast<uint32_t>(fpsr_aux));
+#endif
+    }
+
+    void getFpStatusRegister() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("mrs %0, fpcr" : "=r"(fpsr));
+#elif defined(__SSE__)
+        fpsr          = static_cast<intptr_t>(_mm_getcsr());
+#endif
+    }
+
+   public:
+    ScopedNoDenormals() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        intptr_t mask = (1 << 24 /* FZ */);
+#else
+#if defined(__SSE__)
+#if defined(__SSE2__)
+        intptr_t mask = 0x8040;
+#else
+        intptr_t mask = 0x8000;
+#endif
+#else
+        intptr_t mask = 0x0000;
+#endif
+#endif
+        getFpStatusRegister();
+        setFpStatusRegister(fpsr | mask);
+    }
+
+    ~ScopedNoDenormals() noexcept { setFpStatusRegister(fpsr); }
+};
+
+#define AVOIDDENORMALS ScopedNoDenormals();
+
+#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 <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** 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<typename REAL>
+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<FAUSTFLOAT> {
+    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 <algorithm>
+#include <map>
+#include <regex>
+#include <set>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class FAUST_API PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+    std::vector<std::string> fFullPaths;
+    std::map<std::string, std::string> 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<std::string>
+            uniquePaths;  // all full paths transformed but made unique with a prefix
+        std::map<std::string, std::string>
+            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<std::string, int> 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<std::string> collisionSet;
+            std::map<std::string, std::string> 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<char>& 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 <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// 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<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? 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<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(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<double>(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<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(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<double>(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<UpdatableValueConverter*> 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<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> 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<std::string, std::string> 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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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<ZoneControl*>* 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<ZoneControl*>* 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<ZoneControl*>* 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
+    //-------------------------------------------------------------------------------
+    int getParamsCount() { return int(fItems.size()); }
+
+    int getParamIndex(const char* path_aux)
+    {
+        std::string path = std::string(path_aux);
+        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;
+    }
+
+    const char* getParamLabel(int p) { return fItems[uint(p)].fLabel.c_str(); }
+    const char* getParamShortname(int p) { return fItems[uint(p)].fShortname.c_str(); }
+    const char* getParamAddress(int p) { return fItems[uint(p)].fPath.c_str(); }
+
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> metadata = fMetaData[uint(p)];
+        for (const auto& it : metadata) {
+            res[it.first.c_str()] = it.second.c_str();
+        }
+        return res;
+    }
+
+    const char* getMetadata(int p, const char* key)
+    {
+        return (fMetaData[uint(p)].find(key) != fMetaData[uint(p)].end())
+                   ? fMetaData[uint(p)][key].c_str()
+                   : "";
+    }
+    FAUSTFLOAT getParamMin(int p) { return fItems[uint(p)].fMin; }
+    FAUSTFLOAT getParamMax(int p) { return fItems[uint(p)].fMax; }
+    FAUSTFLOAT getParamStep(int p) { return fItems[uint(p)].fStep; }
+    FAUSTFLOAT getParamInit(int p) { return fItems[uint(p)].fInit; }
+
+    FAUSTFLOAT* getParamZone(int p) { return fItems[uint(p)].fZone; }
+
+    FAUSTFLOAT getParamValue(int p) { return *fItems[uint(p)].fZone; }
+    FAUSTFLOAT getParamValue(const char* path)
+    {
+        int index = getParamIndex(path);
+        if (index >= 0) {
+            return getParamValue(index);
+        } else {
+            fprintf(stderr, "getParamValue : '%s' not found\n",
+                    (path == nullptr ? "NULL" : path));
+            return FAUSTFLOAT(0);
+        }
+    }
+
+    void setParamValue(int p, FAUSTFLOAT v) { *fItems[uint(p)].fZone = v; }
+    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
+     * @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
+     * @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
+     * @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
+     * @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; }
+
+    // getScreenColor() : -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/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+#include <math.h>
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifndef FAUSTCLASS
+#define FAUSTCLASS compressordsp
+#endif
+
+#ifdef __APPLE__
+#define exp10f __exp10f
+#define exp10  __exp10
+#endif
+
+#if defined(_WIN32)
+#define RESTRICT __restrict
+#else
+#define RESTRICT __restrict__
+#endif
+
+class compressordsp : public dsp
+{
+   private:
+    FAUSTFLOAT fCheckbox0;
+    FAUSTFLOAT fHslider0;
+    FAUSTFLOAT fHslider1;
+    int fSampleRate;
+    float fConst0;
+    FAUSTFLOAT fHslider2;
+    FAUSTFLOAT fHslider3;
+    float fRec3[2];
+    FAUSTFLOAT fHslider4;
+    float fRec2[2];
+    float fRec1[2];
+    float fRec0[2];
+    FAUSTFLOAT fHbargraph0;
+
+   public:
+    void metadata(Meta* m)
+    {
+        m->declare("analyzers.lib/amp_follower_ar:author",
+                   "Jonatan Liljedahl, revised by Romain Michon");
+        m->declare("analyzers.lib/name", "Faust Analyzer Library");
+        m->declare("analyzers.lib/version", "0.2");
+        m->declare("author", "Julius Smith");
+        m->declare("basics.lib/bypass1:author", "Julius Smith");
+        m->declare("basics.lib/name", "Faust Basic Element Library");
+        m->declare("basics.lib/version", "0.8");
+        m->declare("compile_options",
+                   "-a faust2header.cpp -lang cpp -i -inpl -cn compressordsp -es 1 -mcd "
+                   "16 -single -ftz 0");
+        m->declare("compressors.lib/compression_gain_mono:author", "Julius O. Smith III");
+        m->declare(
+            "compressors.lib/compression_gain_mono:copyright",
+            "Copyright (C) 2014-2020 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("compressors.lib/compression_gain_mono:license",
+                   "MIT-style STK-4.3 license");
+        m->declare("compressors.lib/compressor_lad_mono:author", "Julius O. Smith III");
+        m->declare(
+            "compressors.lib/compressor_lad_mono:copyright",
+            "Copyright (C) 2014-2020 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("compressors.lib/compressor_lad_mono:license",
+                   "MIT-style STK-4.3 license");
+        m->declare("compressors.lib/compressor_mono:author", "Julius O. Smith III");
+        m->declare(
+            "compressors.lib/compressor_mono:copyright",
+            "Copyright (C) 2014-2020 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("compressors.lib/compressor_mono:license",
+                   "MIT-style STK-4.3 license");
+        m->declare("compressors.lib/name", "Faust Compressor Effect Library");
+        m->declare("compressors.lib/version", "0.2");
+        m->declare("description",
+                   "Compressor demo application, adapted from the Faust Library's "
+                   "dm.compressor_demo in demos.lib");
+        m->declare("documentation",
+                   "https://faustlibraries.grame.fr/libs/compressors/#cocompressor_mono");
+        m->declare("filename", "compressordsp.dsp");
+        m->declare("license", "MIT Style STK-4.2");
+        m->declare("maths.lib/author", "GRAME");
+        m->declare("maths.lib/copyright", "GRAME");
+        m->declare("maths.lib/license", "LGPL with exception");
+        m->declare("maths.lib/name", "Faust Math Library");
+        m->declare("maths.lib/version", "2.5");
+        m->declare("name", "compressor");
+        m->declare("platform.lib/name", "Generic Platform Library");
+        m->declare("platform.lib/version", "0.2");
+        m->declare("signals.lib/name", "Faust Signal Routing Library");
+        m->declare("signals.lib/onePoleSwitching:author",
+                   "Jonatan Liljedahl, revised by Dario Sanfilippo");
+        m->declare("signals.lib/onePoleSwitching:licence", "STK-4.3");
+        m->declare("signals.lib/version", "0.3");
+        m->declare("version", "0.0");
+    }
+
+    virtual int getNumInputs() { return 1; }
+    virtual int getNumOutputs() { return 1; }
+
+    static void classInit(int /*sample_rate*/) {}
+
+    virtual void instanceConstants(int sample_rate)
+    {
+        fSampleRate = sample_rate;
+        fConst0 =
+            1.0f / std::min<float>(192000.0f, std::max<float>(1.0f, float(fSampleRate)));
+    }
+
+    virtual void instanceResetUserInterface()
+    {
+        fCheckbox0 = FAUSTFLOAT(0.0f);
+        fHslider0  = FAUSTFLOAT(2.0f);
+        fHslider1  = FAUSTFLOAT(2.0f);
+        fHslider2  = FAUSTFLOAT(40.0f);
+        fHslider3  = FAUSTFLOAT(15.0f);
+        fHslider4  = FAUSTFLOAT(-24.0f);
+    }
+
+    virtual void instanceClear()
+    {
+        for (int l0 = 0; l0 < 2; l0 = l0 + 1) {
+            fRec3[l0] = 0.0f;
+        }
+        for (int l1 = 0; l1 < 2; l1 = l1 + 1) {
+            fRec2[l1] = 0.0f;
+        }
+        for (int l2 = 0; l2 < 2; l2 = l2 + 1) {
+            fRec1[l2] = 0.0f;
+        }
+        for (int l3 = 0; l3 < 2; l3 = l3 + 1) {
+            fRec0[l3] = 0.0f;
+        }
+    }
+
+    virtual void init(int sample_rate)
+    {
+        classInit(sample_rate);
+        instanceInit(sample_rate);
+    }
+    virtual void instanceInit(int sample_rate)
+    {
+        instanceConstants(sample_rate);
+        instanceResetUserInterface();
+        instanceClear();
+    }
+
+    virtual compressordsp* clone() { return new compressordsp(); }
+
+    virtual int getSampleRate() { return fSampleRate; }
+
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        ui_interface->declare(
+            0, "tooltip",
+            "References:                 "
+            "https://faustlibraries.grame.fr/libs/compressors/                 "
+            "http://en.wikipedia.org/wiki/Dynamic_range_compression");
+        ui_interface->openVerticalBox("COMPRESSOR");
+        ui_interface->declare(0, "0", "");
+        ui_interface->openHorizontalBox("0x00");
+        ui_interface->declare(&fCheckbox0, "0", "");
+        ui_interface->declare(
+            &fCheckbox0, "tooltip",
+            "When this is checked, the compressor                 has no effect");
+        ui_interface->addCheckButton("Bypass", &fCheckbox0);
+        ui_interface->declare(&fHbargraph0, "1", "");
+        ui_interface->declare(&fHbargraph0, "tooltip", "Compressor gain in dB");
+        ui_interface->declare(&fHbargraph0, "unit", "dB");
+        ui_interface->addHorizontalBargraph("Compressor Gain", &fHbargraph0,
+                                            FAUSTFLOAT(-50.0f), FAUSTFLOAT(10.0f));
+        ui_interface->closeBox();
+        ui_interface->declare(0, "1", "");
+        ui_interface->openHorizontalBox("0x00");
+        ui_interface->declare(0, "3", "");
+        ui_interface->openHorizontalBox("Compression Control");
+        ui_interface->declare(&fHslider1, "0", "");
+        ui_interface->declare(&fHslider1, "style", "knob");
+        ui_interface->declare(
+            &fHslider1, "tooltip",
+            "A compression Ratio of N means that for each N dB increase in input         "
+            "signal level above Threshold, the output level goes up 1 dB");
+        ui_interface->addHorizontalSlider("Ratio", &fHslider1, FAUSTFLOAT(2.0f),
+                                          FAUSTFLOAT(1.0f), FAUSTFLOAT(20.0f),
+                                          FAUSTFLOAT(0.100000001f));
+        ui_interface->declare(&fHslider4, "1", "");
+        ui_interface->declare(&fHslider4, "style", "knob");
+        ui_interface->declare(&fHslider4, "tooltip",
+                              "When the signal level exceeds the Threshold (in dB), its "
+                              "level         is compressed according to the Ratio");
+        ui_interface->declare(&fHslider4, "unit", "dB");
+        ui_interface->addHorizontalSlider("Threshold", &fHslider4, FAUSTFLOAT(-24.0f),
+                                          FAUSTFLOAT(-100.0f), FAUSTFLOAT(10.0f),
+                                          FAUSTFLOAT(0.100000001f));
+        ui_interface->closeBox();
+        ui_interface->declare(0, "4", "");
+        ui_interface->openHorizontalBox("Compression Response");
+        ui_interface->declare(&fHslider3, "1", "");
+        ui_interface->declare(&fHslider3, "scale", "log");
+        ui_interface->declare(&fHslider3, "style", "knob");
+        ui_interface->declare(
+            &fHslider3, "tooltip",
+            "Time constant in ms (1/e smoothing time) for the compression gain         "
+            "to approach (exponentially) a new lower target level (the compression       "
+            "  `kicking in')");
+        ui_interface->declare(&fHslider3, "unit", "ms");
+        ui_interface->addHorizontalSlider("Attack", &fHslider3, FAUSTFLOAT(15.0f),
+                                          FAUSTFLOAT(1.0f), FAUSTFLOAT(1000.0f),
+                                          FAUSTFLOAT(0.100000001f));
+        ui_interface->declare(&fHslider2, "2", "");
+        ui_interface->declare(&fHslider2, "scale", "log");
+        ui_interface->declare(&fHslider2, "style", "knob");
+        ui_interface->declare(
+            &fHslider2, "tooltip",
+            "Time constant in ms (1/e smoothing time) for the compression gain         "
+            "to approach (exponentially) a new higher target level (the compression      "
+            "   'releasing')");
+        ui_interface->declare(&fHslider2, "unit", "ms");
+        ui_interface->addHorizontalSlider("Release", &fHslider2, FAUSTFLOAT(40.0f),
+                                          FAUSTFLOAT(1.0f), FAUSTFLOAT(1000.0f),
+                                          FAUSTFLOAT(0.100000001f));
+        ui_interface->closeBox();
+        ui_interface->closeBox();
+        ui_interface->declare(&fHslider0, "5", "");
+        ui_interface->declare(
+            &fHslider0, "tooltip",
+            "The compressed-signal output level is increased by this amount         (in "
+            "dB) to make up for the level lost due to compression");
+        ui_interface->declare(&fHslider0, "unit", "dB");
+        ui_interface->addHorizontalSlider("MakeUpGain", &fHslider0, FAUSTFLOAT(2.0f),
+                                          FAUSTFLOAT(-96.0f), FAUSTFLOAT(96.0f),
+                                          FAUSTFLOAT(0.100000001f));
+        ui_interface->closeBox();
+    }
+
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs)
+    {
+        FAUSTFLOAT* input0  = inputs[0];
+        FAUSTFLOAT* output0 = outputs[0];
+        int iSlow0          = int(float(fCheckbox0));
+        float fSlow1        = std::pow(10.0f, 0.0500000007f * float(fHslider0));
+        float fSlow2  = 1.0f / std::max<float>(1.1920929e-07f, float(fHslider1)) + -1.0f;
+        float fSlow3  = std::max<float>(fConst0, 0.00100000005f * float(fHslider2));
+        int iSlow4    = std::fabs(fSlow3) < 1.1920929e-07f;
+        float fThen2  = std::exp(0.0f - fConst0 / ((iSlow4) ? 1.0f : fSlow3));
+        float fSlow5  = ((iSlow4) ? 0.0f : fThen2);
+        float fSlow6  = std::max<float>(fConst0, 0.00100000005f * float(fHslider3));
+        int iSlow7    = std::fabs(fSlow6) < 1.1920929e-07f;
+        float fThen4  = std::exp(0.0f - fConst0 / ((iSlow7) ? 1.0f : fSlow6));
+        float fSlow8  = ((iSlow7) ? 0.0f : fThen4);
+        float fSlow9  = float(fHslider4);
+        float fSlow10 = 0.5f * fSlow6;
+        int iSlow11   = std::fabs(fSlow10) < 1.1920929e-07f;
+        float fThen7  = std::exp(0.0f - fConst0 / ((iSlow11) ? 1.0f : fSlow10));
+        float fSlow12 = ((iSlow11) ? 0.0f : fThen7);
+        float fSlow13 = 1.0f - fSlow12;
+        for (int i0 = 0; i0 < count; i0 = i0 + 1) {
+            float fTemp0 = float(input0[i0]);
+            float fTemp1 = ((iSlow0) ? 0.0f : fTemp0);
+            float fTemp2 = std::fabs(fTemp1);
+            float fTemp3 = ((fTemp2 > fRec3[1]) ? fSlow8 : fSlow5);
+            fRec3[0]     = fTemp2 * (1.0f - fTemp3) + fRec3[1] * fTemp3;
+            fRec2[0] =
+                fSlow2
+                    * std::max<float>(
+                        20.0f * std::log10(std::max<float>(1.17549435e-38f, fRec3[0]))
+                            - fSlow9,
+                        0.0f)
+                    * fSlow13
+                + fSlow12 * fRec2[1];
+            float fTemp4 = fTemp1 * std::pow(10.0f, 0.0500000007f * fRec2[0]);
+            float fTemp5 = std::fabs(std::fabs(fTemp4));
+            float fTemp6 = ((fTemp5 > fRec1[1]) ? fSlow8 : fSlow5);
+            fRec1[0]     = fTemp5 * (1.0f - fTemp6) + fRec1[1] * fTemp6;
+            fRec0[0] =
+                fSlow2
+                    * std::max<float>(
+                        20.0f * std::log10(std::max<float>(1.17549435e-38f, fRec1[0]))
+                            - fSlow9,
+                        0.0f)
+                    * fSlow13
+                + fSlow12 * fRec0[1];
+            fHbargraph0 = FAUSTFLOAT(
+                20.0f
+                * std::log10(std::max<float>(1.17549435e-38f,
+                                             std::pow(10.0f, 0.0500000007f * fRec0[0]))));
+            float fThen9 = fSlow1 * fTemp4;
+            output0[i0]  = FAUSTFLOAT(((iSlow0) ? fTemp0 : fThen9));
+            fRec3[1]     = fRec3[0];
+            fRec2[1]     = fRec2[0];
+            fRec1[1]     = fRec1[0];
+            fRec0[1]     = fRec0[0];
+        }
+    }
+};
+
+#endif
diff --git a/src/dblsqd/feed.cpp b/src/dblsqd/feed.cpp
new file mode 100644 (file)
index 0000000..6a71fc2
--- /dev/null
@@ -0,0 +1,333 @@
+#include "feed.h"
+
+namespace dblsqd
+{
+
+/*!
+  \class Feed
+ * \brief The Feed class provides methods for accessing DBLSQD Feeds and downloading
+ Releases.
+ *
+ * A Feed is a representation of an Application’s Releases.
+ * This class can retrieve Feeds via HTTP(S) and offers convenience methods for
+ *
+ * \section3 Loading Feeds
+ *
+ * Before a Feed can be loaded with load(), it needs to be initialized with setUrl().
+ *
+ * \section3 Downloading Updates
+ * This class also allows downloading updates through the downloadRelease() method.
+ */
+
+/*!
+ * \brief Constructs a new Feed object.
+ *
+ * \sa setUrl()
+ */
+Feed::Feed(QString baseUrl, QString channel, QString os, QString arch, QString type)
+    : feedReply(NULL)
+    , downloadReply(NULL)
+    , downloadFile(NULL)
+    , redirects(0)
+    , _ready(false)
+{
+    if (!baseUrl.isEmpty()) {
+        this->setUrl(baseUrl, channel, os, arch, type);
+    }
+}
+
+/*!
+ * \brief Sets the Feed URL.
+ *
+ * This method can be used to manually set the Feed URL.
+ */
+void Feed::setUrl(QUrl url)
+{
+    this->url = url;
+}
+
+/*!
+ * \brief Sets the Feed URL by specifying its components.
+ *
+ * The only required component is baseUrl which must be the base URL for an Application
+ * provided by the DBSLQD CLI Tool. It should include the full schema and does not require
+ * a trailing "/".
+ */
+void Feed::setUrl(QString baseUrl, QString channel, QString os, QString arch,
+                  QString type)
+{
+    QStringList urlParts;
+    urlParts << baseUrl;
+    urlParts << channel;
+
+    if (!os.isEmpty()) {
+        urlParts << os;
+    } else {
+        QString autoOs = QSysInfo::productType().toLower();
+        if (autoOs == "windows") {
+            autoOs = "win";
+        } else if (autoOs == "osx" || autoOs == "macos") {
+            autoOs = "mac";
+        } else {
+            autoOs = QSysInfo::kernelType();
+        }
+        urlParts << autoOs;
+    }
+
+    if (!arch.isEmpty()) {
+        urlParts << arch;
+    } else {
+        QString autoArch = QSysInfo::buildCpuArchitecture();
+        if (autoArch == "i386" || autoArch == "i586" || autoArch == "i586") {
+            autoArch = "x86";
+        }
+        urlParts << autoArch;
+    }
+
+    if (!type.isEmpty()) {
+        urlParts << "?t=" + type;
+    }
+
+    this->url = QUrl(urlParts.join("/"));
+}
+
+/*!
+ * \brief Returns the Feed URL.
+ */
+QUrl Feed::getUrl()
+{
+    return QUrl(url);
+}
+
+/*!
+ * \brief Returns a list of all Releases in the Feed.
+ *
+ * The list is sorted in descending order by version number/release date.
+ * If called before ready() was emitted, an empty list is returned.
+ * \sa getReleases()
+ */
+QList<Release> Feed::getReleases()
+{
+    return releases;
+}
+
+/*!
+ * \brief Returns a list of all Releases in the Feed that are newer than the given
+ * Release.
+ *
+ * The list is sorted in descending order by version number/release date.
+ * If called before ready() was emitted, an empty list is returned.
+ * \sa getReleases()
+ */
+QList<Release> Feed::getUpdates(Release currentRelease)
+{
+    QList<Release> updates;
+    for (int i = 0; i < releases.size(); i++) {
+        if (currentRelease < releases.at(i))
+            updates << releases.at(i);
+    }
+    return updates;
+}
+
+/*!
+ * \brief Returns the pointer to a QTemporaryFile for a downloaded file.
+ *
+ * If called before downloadFinished() was emitted, this might return a NULL
+ * pointer.
+ */
+QTemporaryFile* Feed::getDownloadFile()
+{
+    return downloadFile;
+}
+
+/*!
+ * \brief Returns true if Feed information has been retrieved successfully.
+ *
+ * A ready Feed might not contain any release information.
+ * If downloading the Feed failed, false is returned.
+ */
+bool Feed::isReady()
+{
+    return _ready;
+}
+
+/*
+ * Async API functions
+ */
+/*!
+ * \brief Retrieves and parses data from the Feed.
+ *
+ * A Feed URL must have been set before with setUrl(). Emits ready() or loadError() on
+ * completion.
+ */
+void Feed::load()
+{
+    if (feedReply != NULL && !feedReply->isFinished()) {
+        return;
+    }
+
+    QNetworkRequest request(getUrl());
+    feedReply = nam.get(request);
+    connect(feedReply, SIGNAL(finished()), this, SLOT(handleFeedFinished()));
+}
+
+/*!
+ * \brief Starts the download of a given Release.
+ * \sa downloadFinished() downloadError() downloadProgress()
+ */
+void Feed::downloadRelease(Release release)
+{
+    redirects = 0;
+    makeDownloadRequest(release.getDownloadUrl());
+    this->release = release;
+}
+
+/*
+ * Private methods
+ */
+void Feed::makeDownloadRequest(QUrl url)
+{
+    if (downloadReply != NULL && !downloadReply->isFinished()) {
+        disconnect(downloadReply);
+        downloadReply->abort();
+        downloadReply->deleteLater();
+    }
+    if (downloadFile != NULL) {
+        disconnect(downloadFile);
+        downloadFile->close();
+        downloadFile->deleteLater();
+        downloadFile = NULL;
+    }
+
+    QNetworkRequest request(url);
+    downloadReply = nam.get(request);
+    connect(downloadReply, SIGNAL(downloadProgress(qint64, qint64)), this,
+            SLOT(handleDownloadProgress(qint64, qint64)));
+    connect(downloadReply, SIGNAL(readyRead()), this, SLOT(handleDownloadReadyRead()));
+    connect(downloadReply, SIGNAL(finished()), this, SLOT(handleDownloadFinished()));
+}
+
+/*
+ * Signals
+ */
+/*! \fn void Feed::ready()
+ * This signal is emitted when a Feed has been successfully downloaded and parsed.
+ * \sa loadError() load()
+ */
+
+/*! \fn void Feed::loadError(QString message)
+ * This signal is emitted when a Feed could not be downloaded.
+ * When loadError() is emitted, ready() is not emitted.
+ * \sa ready() load()
+ */
+
+/*! \fn void Feed::downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+ * This signal is emitted during the download of a Release through downloadRelease().
+ * \sa downloadRelease()
+ */
+
+/*! \fn void Feed::downloadFinished()
+ * This signal is emitted when the download of a Release was successful.
+ * A QTemporaryFile* of the downloaded file can then be retrieved with getDownloadFile().
+ * \sa downloadRelease()
+ */
+
+/*! \fn void Feed::downloadError()
+ * This signal is emitted when there was an error downloading or verifying a Release.
+ * When downloadError() is emitted, downloadFinished() is not emitted.
+ * \sa downloadFinished() downloadRelease()
+ */
+
+/*
+ * Private Slots
+ */
+void Feed::handleFeedFinished()
+{
+    if (feedReply->error() != QNetworkReply::NoError) {
+        emit loadError(feedReply->errorString());
+        return;
+    }
+
+    releases.clear();
+    QByteArray json         = feedReply->readAll();
+    QJsonDocument doc       = QJsonDocument::fromJson(json);
+    QJsonArray releasesInfo = doc.object().value("releases").toArray();
+    for (int i = 0; i < releasesInfo.size(); i++) {
+        releases << Release(releasesInfo.at(i).toObject());
+    }
+    std::sort(releases.begin(), releases.end());
+    std::reverse(releases.begin(), releases.end());
+
+    _ready = true;
+    emit ready();
+}
+
+void Feed::handleDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
+{
+    emit downloadProgress(bytesReceived, bytesTotal);
+}
+
+void Feed::handleDownloadReadyRead()
+{
+    if (downloadFile == NULL) {
+        QString fileName = downloadReply->url().fileName();
+        // Workaround for dblsqd to extract filename via query params from a
+        // Github-formatted redirect URL
+        QUrl url     = downloadReply->url();
+        QString host = url.host();
+        if (host.contains("github", Qt::CaseInsensitive) && url.hasQuery()) {
+            QString query = url.query();
+            QRegularExpression rx("filename%3D(.*?)(&|$)");
+            QRegularExpressionMatch match = rx.match(query);
+            if (match.hasMatch()) {
+                fileName = match.captured(1);
+            }
+        }
+        // End workaround
+        int extensionPos =
+            fileName.indexOf(QRegularExpression("(?:\\.tar)?\\.[a-zA-Z0-9]+$"));
+        if (extensionPos > -1) {
+            fileName.insert(extensionPos, "-XXXXXX");
+        }
+        downloadFile = new QTemporaryFile(QDir::tempPath() + "/" + fileName);
+        downloadFile->open();
+    }
+    downloadFile->write(downloadReply->readAll());
+}
+
+void Feed::handleDownloadFinished()
+{
+    if (downloadReply->error() != QNetworkReply::NoError) {
+        emit downloadError(downloadReply->errorString());
+        return;
+    } else if (!downloadReply->attribute(QNetworkRequest::RedirectionTargetAttribute)
+                    .isNull()) {
+        if (redirects >= 8) {
+            emit downloadError(tr("Too many redirects."));
+            return;
+        }
+        QUrl redirectionTarget =
+            downloadReply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
+        QUrl redirectedUrl = downloadReply->url().resolved(redirectionTarget);
+        redirects++;
+        makeDownloadRequest(redirectedUrl);
+        return;
+    } else if (downloadFile == NULL) {
+        emit downloadError(tr("No data received from server"));
+        return;
+    }
+
+    downloadFile->flush();
+    downloadFile->seek(0);
+    QCryptographicHash fileHash(QCryptographicHash::Sha256);
+    fileHash.addData(downloadFile->readAll());
+    QString hashResult = fileHash.result().toHex();
+    if (hashResult.toLower() != release.getDownloadSHA256().toLower()) {
+        emit downloadError(tr("Could not verify download integrity."));
+        return;
+    }
+
+    emit downloadFinished();
+}
+
+}  // namespace dblsqd
diff --git a/src/dblsqd/feed.h b/src/dblsqd/feed.h
new file mode 100644 (file)
index 0000000..fc030ac
--- /dev/null
@@ -0,0 +1,67 @@
+#ifndef DBLSQD_FEED_H
+#define DBLSQD_FEED_H
+
+#include <QtCore>
+#include <QtNetwork>
+
+#include "release.h"
+
+namespace dblsqd
+{
+
+class Feed : public QObject
+{
+    Q_OBJECT
+
+   public:
+    Feed(QString baseUrl = "", QString channel = "release", QString os = QString(),
+         QString arch = QString(), QString type = QString());
+
+    void setUrl(QUrl url);
+    void setUrl(QString baseUrl, QString channel = "release", QString os = QString(),
+                QString arch = QString(), QString type = QString());
+    QUrl getUrl();
+
+    // Async API
+    void load();
+    void downloadRelease(Release release);
+
+    // Sync API
+    QList<Release> getUpdates(
+        Release currentRelease = Release(QCoreApplication::applicationVersion()));
+    QList<Release> getReleases();
+    QTemporaryFile* getDownloadFile();
+    bool isReady();
+
+   signals:
+    void ready();
+    void loadError(QString message);
+    void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
+    void downloadFinished();
+    void downloadError(QString message);
+
+   private:
+    QUrl url;
+
+    QList<Release> releases;
+
+    void makeDownloadRequest(QUrl url);
+
+    QNetworkAccessManager nam;
+    QNetworkReply* feedReply;
+    Release release;
+    QNetworkReply* downloadReply;
+    QTemporaryFile* downloadFile;
+    uint redirects;
+    bool _ready;
+
+   private slots:
+    void handleFeedFinished();
+    void handleDownloadProgress(qint64, qint64);
+    void handleDownloadReadyRead();
+    void handleDownloadFinished();
+};
+
+}  // namespace dblsqd
+
+#endif  // DBLSQD_FEED_H
diff --git a/src/dblsqd/release.cpp b/src/dblsqd/release.cpp
new file mode 100644 (file)
index 0000000..e1c728a
--- /dev/null
@@ -0,0 +1,143 @@
+#include "release.h"
+
+namespace dblsqd
+{
+
+/*!
+ * \class Release
+ * \brief This class is used to represent information about a single Release
+ * from a Feed.
+ */
+
+/*!
+ * \brief Constructs a new Release from q QJsonObject.
+ */
+Release::Release(QJsonObject releaseInfo)
+{
+    this->version   = releaseInfo.value("version").toString();
+    this->changelog = releaseInfo.value("changelog").toString();
+
+    QJsonObject downloadInfo = releaseInfo.value("download").toObject();
+    this->date =
+        QDateTime::fromString(downloadInfo.value("date").toString(), Qt::ISODate);
+    this->downloadUrl    = QUrl(downloadInfo.value("url").toString());
+    this->downloadSize   = downloadInfo.value("size").toDouble();
+    this->downloadSHA1   = downloadInfo.value("sha1").toString();
+    this->downloadSHA256 = downloadInfo.value("sha256").toString();
+    this->downloadDSA    = downloadInfo.value("dsa").toString();
+}
+
+/*!
+ * \brief Constructs a new Release from a version string and a date.
+ *
+ * This method is useful when constructing a "virtual" Release for comparing
+ * it with Releases retrieved from a Feed.
+ */
+Release::Release(QString version, QDateTime date)
+    : version(version)
+    , date(date)
+    , changelog("")
+    , downloadUrl("")
+    , downloadSize(0)
+    , downloadSHA1("")
+    , downloadSHA256("")
+    , downloadDSA("")
+{
+}
+
+/*!
+ * \brief Compares two Releases.
+ *
+ * If the Release version is compatible with SemVer, the version determines
+ * Release order. If versions are identical or not compatible with SemVer,
+ * Release date is used for determining order instead.
+ */
+bool operator<(const Release& one, const Release& other)
+{
+    SemVer v1(one.version);
+    SemVer v2(other.version);
+    if (v1.isValid() && v2.isValid()) {
+        return (v1 < v2);
+    } else {
+        return (one.date < other.date);
+    }
+}
+
+bool operator==(const Release& one, const Release& other)
+{
+    return one.version == other.version;
+}
+
+bool operator<=(const Release& one, const Release& other)
+{
+    return one == other || one < other;
+}
+
+/*
+ * Getters
+ */
+/*!
+ * \brief Returns the Release version.
+ */
+QString Release::getVersion() const
+{
+    return this->version;
+}
+
+/*!
+ * \brief Returns the Release changelog.
+ */
+QString Release::getChangelog() const
+{
+    return this->changelog;
+}
+
+/*!
+ * \brief Returns the Release date.
+ */
+QDateTime Release::getDate() const
+{
+    return this->date;
+}
+
+/*!
+ * \brief Returns the Release download URL.
+ */
+QUrl Release::getDownloadUrl() const
+{
+    return this->downloadUrl;
+}
+
+/*!
+ * \brief Returns the SHA1 hash of the Release download.
+ */
+QString Release::getDownloadSHA1() const
+{
+    return this->downloadSHA1;
+}
+
+/*!
+ * \brief Returns the SHA256 hash of the Release download.
+ */
+QString Release::getDownloadSHA256() const
+{
+    return this->downloadSHA256;
+}
+
+/*!
+ * \brief Returns the DSA signature of the Release download.
+ */
+QString Release::getDownloadDSA() const
+{
+    return this->downloadDSA;
+}
+
+/*!
+ * \brief Returns the size of the Release download in bytes.
+ */
+qint64 Release::getDownloadSize() const
+{
+    return (qint64)this->downloadSize;
+}
+
+}  // namespace dblsqd
diff --git a/src/dblsqd/release.h b/src/dblsqd/release.h
new file mode 100644 (file)
index 0000000..9c769bf
--- /dev/null
@@ -0,0 +1,44 @@
+#ifndef DBLSQD_RELEASE_H
+#define DBLSQD_RELEASE_H
+
+#include <QUrl>
+#include <QtCore>
+
+#include "semver.h"
+
+namespace dblsqd
+{
+
+class Release
+{
+   public:
+    Release(QJsonObject releaseInfo);
+    Release(QString version = QString(), QDateTime date = QDateTime());
+
+    friend bool operator<(const Release& one, const Release& other);
+    friend bool operator==(const Release& one, const Release& other);
+    friend bool operator<=(const Release& one, const Release& other);
+
+    QString getVersion() const;
+    QString getChangelog() const;
+    QDateTime getDate() const;
+    QUrl getDownloadUrl() const;
+    QString getDownloadSHA1() const;
+    QString getDownloadSHA256() const;
+    QString getDownloadDSA() const;
+    qint64 getDownloadSize() const;
+
+   private:
+    QString version;
+    QDateTime date;
+    QString changelog;
+    QUrl downloadUrl;
+    long downloadSize;
+    QString downloadSHA1;
+    QString downloadSHA256;
+    QString downloadDSA;
+};
+
+}  // namespace dblsqd
+
+#endif  // DBLSQD_RELEASE_H
diff --git a/src/dblsqd/semver.cpp b/src/dblsqd/semver.cpp
new file mode 100644 (file)
index 0000000..a15e2f5
--- /dev/null
@@ -0,0 +1,88 @@
+#include "semver.h"
+
+namespace dblsqd
+{
+
+/*!
+ * \class SemVer
+ * \brief SemVer encapsulates a version according to
+ * Semantic Versioning 2.0.
+ */
+
+/*!
+ * \brief Constructs a new SemVer object from a string.
+ */
+SemVer::SemVer(QString version) : original(version), valid(false)
+{
+    QRegularExpression rx(getRegExp());
+    QRegularExpressionMatch match = rx.match(version);
+    if (match.hasMatch()) {
+        this->major      = match.captured(1).toInt();
+        this->minor      = match.captured(2).toInt();
+        this->patch      = match.captured(3).toInt();
+        this->prerelease = match.captured(4);
+        this->build      = match.captured(5);
+        this->valid      = true;
+    } else {
+        this->major      = 0;
+        this->minor      = 0;
+        this->patch      = 0;
+        this->prerelease = "";
+        this->build      = "";
+        this->valid      = false;
+    }
+}
+
+/*!
+ * \brief Returns true if this version is valid according to the SemVer
+ * specification. Otherwise returns false.
+ */
+bool SemVer::isValid() const
+{
+    return this->valid;
+}
+
+/*!
+ * \brief Compares two SemVer objects.
+ *
+ * Returns true if the left-hand SemVer object represents a lower version
+ * according to the SemVer 2.0 specification.
+ * Otherweise returns false.
+ * Returns false if one of the SemVer objects does not represent a valid
+ * SemVer.
+ * \sa isValid()
+ */
+bool SemVer::operator<(const SemVer& other)
+{
+    if (!this->isValid() || !other.isValid()) {
+        return false;
+    }
+
+    if (this->major != other.major) {
+        return this->major < other.major;
+    } else if (this->minor != other.minor) {
+        return this->minor < other.minor;
+    } else if (this->patch != other.patch) {
+        return this->patch < other.patch;
+    } else if (this->prerelease != other.prerelease) {
+        if (this->prerelease == "") {
+            return false;
+        } else if (other.prerelease == "") {
+            return true;
+        }
+        return (QString::localeAwareCompare(this->prerelease, other.prerelease) < 0);
+    } else {
+        return (QString::localeAwareCompare(this->build, other.build) < 0);
+    }
+}
+
+QString SemVer::getRegExp()
+{
+    QString v = "(0|[1-9]\\d*)";
+    QString p =
+        "(?:-((?:0|[1-9A-Za-z][0-9A-Za-z]*)(?:\\.(?:0|[1-9A-Za-z][0-9A-Za-z]*))*))?";
+    QString b = "(?:\\+((?:[0-9A-Za-z]*)(?:\\.(?:[0-9A-Za-z][0-9A-Za-z]*))*))?";
+    return "^" + v + "." + v + "." + v + p + b + "$";
+}
+
+}  // namespace dblsqd
diff --git a/src/dblsqd/semver.h b/src/dblsqd/semver.h
new file mode 100644 (file)
index 0000000..eb60a4e
--- /dev/null
@@ -0,0 +1,34 @@
+#ifndef DBLSQD_SEMVER_H
+#define DBLSQD_SEMVER_H
+
+#include <QObject>
+#include <QRegularExpression>
+
+namespace dblsqd
+{
+
+class SemVer
+{
+   public:
+    SemVer(QString version);
+
+    bool operator<(const SemVer& other);
+
+    bool isValid() const;
+    QString toString();
+
+   private:
+    QString original;
+    int major;
+    int minor;
+    int patch;
+    QString prerelease;
+    QString build;
+    bool valid;
+
+    static QString getRegExp();
+};
+
+}  // namespace dblsqd
+
+#endif  // DBLSQD_SEMVER_H
diff --git a/src/dblsqd/update_dialog.cpp b/src/dblsqd/update_dialog.cpp
new file mode 100644 (file)
index 0000000..bb914ef
--- /dev/null
@@ -0,0 +1,694 @@
+#include "update_dialog.h"
+
+#include "ui_update_dialog.h"
+
+namespace dblsqd
+{
+
+/*!
+ * \class UpdateDialog
+ * \brief A dialog class for displaying and downloading update information.
+ *
+ * UpdateDialog is a drop-in class for adding a fully-functional auto-update
+ * component to an existing application.
+ *
+ * The most simple integration is
+ * possible with just three lines of code:
+ * \code
+ * dblsqd::Feed* feed = new dblsqd::Feed();
+ * feed->setUrl("https://feeds.dblsqd.com/:app_token");
+ * dblsqd::UpdateDialog* updateDialog = new dblsqd::UpdateDialog(feed);
+ * \endcode
+ *
+ * The update dialog can also display an application icon which can be set with
+ * setIcon().
+ */
+
+/*!
+ * \enum UpdateDialog::Type
+ * \brief This flag determines the if and when the UpdateDialog is displayed
+ * automatically.
+ *
+ * *OnUpdateAvailable*: Automatically display the dialog as soon as the Feed
+ * has been downloaded and parsed and if there is a newer version than the
+ * current version returned by QCoreApplication::applicationVersion().
+ *
+ * *OnLastWindowClosed*: If there is a newer version available than the current
+ * version returned by QCoreApplication::applicationVersion(), the update
+ * dialog is displayed when QGuiApplication emits the lastWindowClosed() event.
+ * Note that when this flag is used,
+ * QGuiApplication::setQuitOnLastWindowClosed(false) will be called.
+ *
+ * *Manual*: The dialog is only displayed when explicitly requested via show()
+ * or exec().
+ * Note that update information might not be available instantly after
+ * constructing an UpdateDialog.
+ *
+ * *ManualChangelog*: The dialog is only displayed when explicitly requested via
+ * show() or exec().
+ * Instead of the full update interface, only the changelog will be shown.
+ */
+
+/*!
+ * \brief Constructs a new UpdateDialog.
+ *
+ * A Feed object needs to be constructed first and passed to this constructor.
+ * Feed::load() does not need to be called on the Feed object.
+ *
+ * The given UpdateDialog::Type flag determines when/if the dialog is shown
+ * automatically.
+ *
+ * UpdateDialog uses QSettings to save information such as when a release was
+ * skipped by the users. If you want to use a specially initialized QSettings
+ * object, you may also pass it to this constructor.
+ *
+ */
+UpdateDialog::UpdateDialog(Feed* feed, int type, QWidget* parent, QSettings* settings)
+    : QDialog(parent)
+    , ui(new Ui::UpdateDialog)
+    , feed(feed)
+    , type(type)
+    , settings(settings)
+    , accepted(false)
+    , isDownloadFinished(false)
+    , acceptedInstallButton(NULL)
+{
+    ui->setupUi(this);
+
+    QPalette palette        = this->palette();
+    QString textColor       = palette.color(QPalette::Text).name();
+    QString backgroundColor = palette.color(QPalette::Base).name();
+    QString labelChangelogStyle =
+        QString("color: %1; background: %2").arg(textColor, backgroundColor);
+    ui->labelChangelog->setStyleSheet(labelChangelogStyle);
+
+    ui->buttonCancel->addAction(ui->actionCancel);
+    ui->buttonCancel->addAction(ui->actionSkip);
+    ui->buttonCancel->setDefaultAction(ui->actionCancel);
+
+    _openExternalLinks = true;
+    connect(ui->labelChangelog, SIGNAL(linkActivated(QString)), this,
+            SLOT(onLinkActivated(QString)));
+
+    switch (type) {
+    case OnUpdateAvailable: {
+        connect(this, SIGNAL(ready()), this, SLOT(showIfUpdatesAvailable()));
+        break;
+    }
+    case OnLastWindowClosed: {
+        QGuiApplication* app = (QGuiApplication*)QApplication::instance();
+        app->setQuitOnLastWindowClosed(false);
+        connect(app, SIGNAL(lastWindowClosed()), this,
+                SLOT(showIfUpdatesAvailableOrQuit()));
+        break;
+    }
+    case Manual: {
+        // don’t do anything
+    }
+    }
+
+    if (feed->isReady()) {
+        handleFeedReady();
+    } else {
+        setupLoadingUi();
+        feed->load();
+        connect(feed, SIGNAL(ready()), this, SLOT(handleFeedReady()));
+    }
+}
+
+UpdateDialog::~UpdateDialog()
+{
+    delete ui;
+}
+
+/*
+ * Setters
+ */
+/*!
+ * \brief Sets the icon displayed in the update window.
+ */
+void UpdateDialog::setIcon(QPixmap pixmap)
+{
+    ui->labelIcon->setPixmap(QPixmap(pixmap));
+    ui->labelIcon->setHidden(false);
+}
+
+void UpdateDialog::setIcon(QString fileName)
+{
+    ui->labelIcon->setPixmap(QPixmap(fileName));
+    ui->labelIcon->setHidden(false);
+}
+
+/*!
+ * \brief Sets the minimum version to be displayed in the changelog.
+ * Defaults to QApplication::applicationVersion() if not set.
+ * \param version
+ */
+void UpdateDialog::setMinVersion(QString version)
+{
+    _minVersion = version;
+    setupChangelogUi();
+}
+
+/*!
+ * \brief Sets the maximum version to be displayed in the changelog
+ * \param version
+ */
+void UpdateDialog::setMaxVersion(QString version)
+{
+    _maxVersion = version;
+    setupChangelogUi();
+}
+
+/*!
+ * \brief Convenience method for setting minimum and maximum version to be displayed in
+ * the changelog. maximumVersion is set to QApplication::applicationVersion()
+ *
+ * \param previousVersion
+ */
+void UpdateDialog::setPreviousVersion(QString previousVersion)
+{
+    _previousVersion = previousVersion;
+    _minVersion      = previousVersion;
+    _maxVersion      = QApplication::applicationVersion();
+    setupChangelogUi();
+}
+
+/*!
+ * \brief Adds a custom button for handling update installation.
+ * \param button
+ *
+ * When the custom button is clicked after an update has been downloaded or when
+ * downloading an update that was started by clicking the button has finished,
+ * installButtonClicked(QAbstractButton* button, QString filePath) is emitted.
+ */
+void UpdateDialog::addInstallButton(QAbstractButton* button)
+{
+    installButtons.append(button);
+    ui->buttonContainer->layout()->addWidget(button);
+    if (isVisible() && ui->buttonCancel->isVisible()) {
+        setupUpdateUi();
+    }
+}
+
+/*!
+ * \propget UpdateExternalLinks
+ *
+ * Determines if links in the changelog should be opened automatically by
+ * QDesktopServices::openUrl() when a user clicks on them.
+ * If set to false, the linkActivated() signal is emitted instead.
+ *
+ * The default value is true.
+
+ */
+bool UpdateDialog::openExternalLinks()
+{
+    return _openExternalLinks;
+}
+
+/*!
+ * \propset UpdateDialog::openExternalLinks
+ */
+void UpdateDialog::setOpenExternalLinks(bool open)
+{
+    _openExternalLinks = open;
+}
+
+/*
+ * Public Slots
+ */
+/*!
+ * \brief Default handler for the install button.
+ *
+ * Closes the dialog if no other action (such as
+ * downloading or installing a Release) is required first.
+ */
+void UpdateDialog::onButtonInstall()
+{
+    accepted = true;
+    if (isDownloadFinished) {
+        startUpdate();
+    } else if (!latestRelease.getVersion().isEmpty()) {
+        startDownload();
+    } else {
+        done(QDialog::Accepted);
+    }
+}
+
+void UpdateDialog::onButtonCustomInstall()
+{
+    accepted = true;
+    if (isDownloadFinished) {
+        emit installButtonClicked((QAbstractButton*)sender(), updateFilePath);
+    } else if (!latestRelease.getVersion().isEmpty()) {
+        acceptedInstallButton = (QAbstractButton*)sender();
+        startDownload();
+    } else {
+        done(QDialog::Accepted);
+    }
+}
+
+/*!
+ * \brief Skips the latest retrieved Release.
+ *
+ * If a release has been skipped, UpdateDialog will not be displayed
+ * automatically when using Type::OnUpdateAvailable or
+ * Type::OnLastWindowClosed.
+ */
+void UpdateDialog::skip()
+{
+    if (!updateFilePath.isEmpty()) {
+        QFile::remove(updateFilePath);
+    }
+    setSettingsValue("skipRelease", latestRelease.getVersion(), settings);
+    done(QDialog::Rejected);
+}
+
+/*!
+ * \brief Shows the dialog if there are available updates.
+ */
+void UpdateDialog::showIfUpdatesAvailable()
+{
+    QString latestVersion = latestRelease.getVersion();
+    bool skipRelease =
+        (settingsValue("skipRelease", "", settings).toString() == latestVersion);
+    if (!latestVersion.isEmpty() && !skipRelease) {
+        show();
+    }
+}
+
+/*!
+ * \brief Shows the dialog if there are updates available or quits the application.
+ */
+void UpdateDialog::showIfUpdatesAvailableOrQuit()
+{
+    if (type == OnLastWindowClosed) {
+        QGuiApplication* app = (QGuiApplication*)QApplication::instance();
+        app->setQuitOnLastWindowClosed(true);
+        disconnect(app, SIGNAL(lastWindowClosed()), this,
+                   SLOT(showIfUpdatesAvailableOrQuit()));
+    }
+    QString latestVersion = latestRelease.getVersion();
+    bool skipRelease =
+        (settingsValue("skipRelease", "", settings).toString() == latestVersion);
+    if (!latestVersion.isEmpty() && !skipRelease) {
+        show();
+    } else {
+        QCoreApplication::quit();
+    }
+}
+
+/*
+ * Static settings helpers
+ */
+QVariant UpdateDialog::settingsValue(QString key, QVariant defaultValue,
+                                     QSettings* settings)
+{
+    return settings->value("DBLSQD/" + key, defaultValue);
+}
+
+void UpdateDialog::setSettingsValue(QString key, QVariant value, QSettings* settings)
+{
+    settings->setValue("DBLSQD/" + key, value);
+}
+
+void UpdateDialog::removeSetting(QString key, QSettings* settings)
+{
+    settings->remove("DBLSQD/" + key);
+}
+
+void UpdateDialog::setDefaultSettingsValue(QString key, QVariant value,
+                                           QSettings* settings)
+{
+    if (settings->contains("DBLSQD/" + key))
+        return;
+    setSettingsValue(key, value, settings);
+}
+
+/*!
+ * \brief Enables or disables automatic downloads.
+ */
+void UpdateDialog::enableAutoDownload(bool enabled, QSettings* settings)
+{
+    setSettingsValue("autoDownload", enabled, settings);
+}
+
+/*!
+ * \brief Returns true if automatic downloads are enabled.
+ *
+ * If defaultValue is provided, it is stored if no other value has previously been set.
+ */
+bool UpdateDialog::autoDownloadEnabled(QVariant defaultValue, QSettings* settings)
+{
+    if (defaultValue.isValid()) {
+        setDefaultSettingsValue("autoDownload", defaultValue, settings);
+    } else {
+        defaultValue = false;
+    }
+    return settingsValue("autoDownload", defaultValue, settings).toBool();
+}
+
+/*!
+ * \overload
+ */
+bool UpdateDialog::autoDownloadEnabled(QSettings* settings)
+{
+    return settingsValue("autoDownload", false, settings).toBool();
+}
+
+/*
+ * Helpers
+ */
+
+void UpdateDialog::adjustDialogSize()
+{
+    adjustSize();
+
+/*HACK: Qt seems to incorrectly calculate window geometry on Windows.
+        This code avoids warning messages logged by the application
+        in that case.*/
+#if defined(Q_OS_WIN) || defined(Q_WS_WIN)
+    QSize dialogSize = size();
+    resize(dialogSize.width(), dialogSize.height() + 3);
+#endif
+}
+
+void UpdateDialog::resetUi()
+{
+    QList<QWidget*> hiddenWidgets;
+    for (int i = 0; i < installButtons.size(); i++) {
+        hiddenWidgets << installButtons.at(i);
+    }
+    hiddenWidgets << ui->headerContainer << ui->labelIcon << ui->headerContainerLoading
+                  << ui->headerContainerNoUpdates << ui->headerContainerChangelog
+                  << ui->scrollAreaChangelog << ui->progressBar << ui->checkAutoDownload
+                  << ui->buttonCancel << ui->buttonCancelLoading << ui->buttonConfirm
+                  << ui->buttonInstall;
+    for (int i = 0; i < hiddenWidgets.size(); i++) {
+        hiddenWidgets.at(i)->hide();
+        hiddenWidgets.at(i)->disconnect();
+    }
+    ui->progressBar->reset();
+    adjustDialogSize();
+}
+
+void UpdateDialog::setupLoadingUi()
+{
+    resetUi();
+    ui->headerContainerLoading->show();
+    ui->progressBar->show();
+    ui->progressBar->setMaximum(0);
+    ui->progressBar->setMinimum(0);
+    ui->buttonCancelLoading->show();
+    ui->buttonCancelLoading->setFocus();
+    connect(ui->buttonCancelLoading, SIGNAL(clicked(bool)), this, SLOT(reject()));
+    adjustDialogSize();
+}
+
+void UpdateDialog::setupUpdateUi()
+{
+    resetUi();
+
+    QList<QWidget*> showWidgets;
+    showWidgets << ui->headerContainer << ui->scrollAreaChangelog << ui->checkAutoDownload
+                << ui->buttonCancel << ui->buttonInstall;
+    for (int i = 0; i < showWidgets.size(); i++) {
+        showWidgets.at(i)->show();
+    }
+
+    QList<QLabel*> labels;
+    labels << ui->labelHeadline << ui->labelInfo;
+    for (int i = 0; i < labels.size(); i++) {
+        QString text = labels.at(i)->text();
+        replaceAppVars(text);
+        labels.at(i)->setText(text);
+    }
+    ui->labelChangelog->setText(generateChangelogDocument());
+
+    ui->checkAutoDownload->setChecked(autoDownloadEnabled(settings));
+
+    // Adapt buttons if release has been downloaded already
+    if (isDownloadFinished) {
+        ui->progressBar->show();
+        ui->progressBar->setMaximum(1);
+        ui->progressBar->setValue(1);
+    }
+
+    connect(feed, SIGNAL(downloadFinished()), this, SLOT(handleDownloadFinished()));
+    connect(feed, SIGNAL(downloadError(QString)), this,
+            SLOT(handleDownloadError(QString)));
+    connect(feed, SIGNAL(downloadProgress(qint64, qint64)), this,
+            SLOT(updateProgressBar(qint64, qint64)));
+
+    connect(ui->buttonConfirm, SIGNAL(clicked()), this, SLOT(accept()));
+    connect(ui->actionCancel, SIGNAL(triggered()), this, SLOT(reject()));
+    connect(ui->actionSkip, SIGNAL(triggered()), this, SLOT(skip()));
+    connect(ui->checkAutoDownload, SIGNAL(toggled(bool)), this,
+            SLOT(autoDownloadCheckboxToggled(bool)));
+
+    // Install buttons
+    if (installButtons.isEmpty()) {
+        ui->buttonInstall->setFocus();
+        connect(ui->buttonInstall, SIGNAL(clicked()), this, SLOT(onButtonInstall()));
+    } else {
+        ui->buttonInstall->hide();
+        for (int i = 0; i < installButtons.size(); i++) {
+            installButtons.at(i)->show();
+            connect(installButtons.at(i), SIGNAL(clicked(bool)), this,
+                    SLOT(onButtonCustomInstall()));
+        }
+        installButtons.last()->setFocus();
+    }
+
+    adjustDialogSize();
+}
+
+void UpdateDialog::setupChangelogUi()
+{
+    resetUi();
+
+    QList<QWidget*> showWidgets;
+    showWidgets << ui->headerContainerChangelog << ui->buttonConfirm
+                << ui->scrollAreaChangelog;
+    for (int i = 0; i < showWidgets.size(); i++) {
+        showWidgets.at(i)->show();
+    }
+    QList<QLabel*> labels;
+    labels << ui->labelHeadlineChangelog << ui->labelInfoChangelog;
+    for (int i = 0; i < labels.size(); i++) {
+        QString text = labels.at(i)->text();
+        replaceAppVars(text);
+        labels.at(i)->setText(text);
+    }
+    ui->labelChangelog->setText(generateChangelogDocument());
+    connect(ui->buttonConfirm, SIGNAL(clicked(bool)), this, SLOT(accept()));
+    ui->buttonConfirm->setFocus();
+    adjustDialogSize();
+}
+
+void UpdateDialog::setupNoUpdatesUi()
+{
+    resetUi();
+    QList<QWidget*> showWidgets;
+    showWidgets << ui->headerContainerNoUpdates << ui->buttonConfirm;
+    for (int i = 0; i < showWidgets.size(); i++) {
+        showWidgets.at(i)->show();
+    }
+    ui->buttonConfirm->setFocus();
+
+    QString text = ui->labelHeadlineNoUpdates->text();
+    replaceAppVars(text);
+    ui->labelHeadlineNoUpdates->setText(text);
+
+    connect(ui->buttonConfirm, SIGNAL(clicked(bool)), this, SLOT(accept()));
+    adjustDialogSize();
+}
+
+void UpdateDialog::disableButtons(bool disable)
+{
+    QList<QWidget*> buttons;
+    for (int i = 0; i < installButtons.size(); i++) {
+        buttons << installButtons.at(i);
+    }
+    buttons << ui->buttonCancel << ui->buttonCancelLoading << ui->buttonConfirm
+            << ui->buttonConfirm << ui->buttonInstall << ui->checkAutoDownload;
+    for (int i = 0; i < buttons.size(); i++) {
+        buttons.at(i)->setDisabled(disable);
+    }
+}
+
+void UpdateDialog::replaceAppVars(QString& string)
+{
+    string.replace("%APPNAME%", QCoreApplication::applicationName());
+    string.replace("%CURRENT_VERSION%", QCoreApplication::applicationVersion());
+    string.replace("%UPDATE_VERSION%", latestRelease.getVersion());
+}
+
+QString UpdateDialog::generateChangelogDocument()
+{
+    QString changelog;
+    QList<Release> changelogReleases;
+    if (_minVersion.isEmpty() && _maxVersion.isEmpty()) {
+        changelogReleases = updates;
+    } else {
+        Release minRelease(_minVersion.isEmpty() ? QApplication::applicationVersion()
+                                                 : _minVersion);
+        Release maxRelease(_maxVersion);
+        for (int i = 0; i < releases.size(); i++) {
+            if (minRelease < releases.at(i)
+                && (_maxVersion.isEmpty() || releases.at(i) <= maxRelease)) {
+                changelogReleases << releases.at(i);
+            }
+        }
+    }
+    for (int i = 0; i < changelogReleases.size(); i++) {
+        QString h2Style = "font-size: medium;";
+        if (i > 0) {
+            h2Style.append("margin-top: 1em;");
+        }
+        changelog.append("<h2 style=\"" + h2Style + "\">"
+                         + changelogReleases.at(i).getVersion() + "</h2>");
+        changelog.append("<p>" + changelogReleases.at(i).getChangelog() + "</p>");
+    }
+    return changelog;
+}
+
+void UpdateDialog::startDownload()
+{
+    feed->downloadRelease(latestRelease);
+    disableButtons(true);
+}
+
+void UpdateDialog::startUpdate()
+{
+    if (QDesktopServices::openUrl(QUrl::fromLocalFile(updateFilePath))) {
+        done(QDialog::Accepted);
+        QApplication::quit();
+    } else {
+        handleDownloadError(tr("Could not open downloaded file %1").arg(updateFilePath));
+    }
+}
+
+/*
+ * Private Slots
+ */
+
+void UpdateDialog::autoDownloadCheckboxToggled(bool enabled)
+{
+    enableAutoDownload(enabled, settings);
+}
+
+void UpdateDialog::handleFeedReady()
+{
+    // Retrieve update information
+    Release currentRelease(QApplication::applicationVersion());
+    updates  = feed->getUpdates(currentRelease);
+    releases = feed->getReleases();
+    if (!updates.isEmpty()) {
+        latestRelease = updates.first();
+    }
+
+    if (type == ManualChangelog) {
+        setupChangelogUi();
+        emit ready();
+        return;
+    }
+
+    // Check if an update has been downloaded previously
+    updateFilePath = settingsValue("updateFilePath", "", settings).toString();
+    if (!updateFilePath.isEmpty() && QFile::exists(updateFilePath)) {
+        QString updateFileVersion =
+            settingsValue("updateFileVersion", "", settings).toString();
+        if (updateFileVersion != latestRelease.getVersion()
+            || updateFileVersion == QApplication::applicationVersion()) {
+            QFile::remove(updateFilePath);
+            removeSetting("updateFilePath");
+            removeSetting("updateFileVersion");
+            updateFilePath = "";
+        } else {
+            isDownloadFinished = true;
+        }
+    }
+
+    // Check if there are any updates
+    if (updates.isEmpty()) {
+        setupNoUpdatesUi();
+        return;
+    }
+
+    // Automatic downloads
+    QString latestVersion = latestRelease.getVersion();
+    bool skipRelease =
+        (settingsValue("skipRelease", "", settings).toString() == latestVersion);
+    bool autoDownload = autoDownloadEnabled(settings) && (!skipRelease);
+    if (autoDownload && !isDownloadFinished) {
+        startDownload();
+    }
+
+    // Setup UI
+    setupUpdateUi();
+    emit ready();
+}
+
+void UpdateDialog::handleDownloadFinished()
+{
+    QTemporaryFile* file = feed->getDownloadFile();
+    isDownloadFinished   = true;
+    updateFilePath       = file->fileName();
+    file->setAutoRemove(false);
+    file->close();
+    file->deleteLater();
+    setSettingsValue("updateFilePath", updateFilePath, settings);
+    setSettingsValue("updateFileVersion", latestRelease.getVersion(), settings);
+
+    if (accepted) {
+        if (acceptedInstallButton == NULL) {
+            startUpdate();
+        } else {
+            emit installButtonClicked(acceptedInstallButton, updateFilePath);
+        }
+
+    } else {
+        disableButtons(false);
+    }
+}
+
+void UpdateDialog::handleDownloadError(QString message)
+{
+    QMessageBox* messageBox = new QMessageBox(this);
+    messageBox->setIcon(QMessageBox::Warning);
+    messageBox->setText("There was an error while downloading the update.");
+    messageBox->setInformativeText(message);
+    messageBox->show();
+    done(QDialog::Rejected);
+}
+
+void UpdateDialog::updateProgressBar(qint64 bytesReceived, qint64 bytesTotal)
+{
+    ui->progressBar->show();
+    ui->progressBar->setMaximum(bytesTotal / 1024);
+    ui->progressBar->setValue(bytesReceived / 1024);
+}
+
+void UpdateDialog::onLinkActivated(QString link)
+{
+    if (_openExternalLinks) {
+        QDesktopServices::openUrl(link);
+    } else {
+        emit linkActivated(link);
+    }
+}
+
+/*
+ * Signals
+ */
+/*! \fn void Feed::ready()
+ * This signal is emitted when a updates are available and the UpdateDialog is
+ * ready to be shown with show() or exec().
+ */
+
+/*! \fn void Feed::installButtonClicked(QAbstractButton* button, QString filePath)
+ * This signal is emitted when a custom install button was clicked.
+ */
+
+}  // namespace dblsqd
diff --git a/src/dblsqd/update_dialog.h b/src/dblsqd/update_dialog.h
new file mode 100644 (file)
index 0000000..ce49ea8
--- /dev/null
@@ -0,0 +1,106 @@
+#ifndef DBLSQD_UPDATE_DIALOG_H
+#define DBLSQD_UPDATE_DIALOG_H
+
+#include <QDesktopServices>
+#include <QFile>
+#include <QMessageBox>
+#include <QSettings>
+#include <QTemporaryFile>
+
+#include "feed.h"
+#include "ui_update_dialog.h"
+
+namespace dblsqd
+{
+
+class UpdateDialog : public QDialog
+{
+    Q_OBJECT
+
+   public:
+    enum Type { OnUpdateAvailable, OnLastWindowClosed, Manual, ManualChangelog };
+    explicit UpdateDialog(Feed* feed, int = OnUpdateAvailable, QWidget* parent = 0,
+                          QSettings* settings = new QSettings());
+    ~UpdateDialog();
+
+    void setIcon(QString fileName);
+    void setIcon(QPixmap pixmap);
+    void addInstallButton(QAbstractButton* button);
+
+    void setMinVersion(QString version);
+    void setMaxVersion(QString version);
+    void setPreviousVersion(QString version);
+
+    static bool autoDownloadEnabled(QVariant defaultValue,
+                                    QSettings* settings = new QSettings);
+    static bool autoDownloadEnabled(QSettings* settings = new QSettings());
+    static void enableAutoDownload(bool enabled, QSettings* settings = new QSettings);
+
+    void setOpenExternalLinks(bool open);
+    bool openExternalLinks();
+
+   signals:
+    void ready();
+    void installButtonClicked(QAbstractButton* button, QString filePath);
+    void linkActivated(QString link);
+
+   public slots:
+    void onButtonInstall();
+    void onButtonCustomInstall();
+    void skip();
+    void showIfUpdatesAvailable();
+    void showIfUpdatesAvailableOrQuit();
+
+   private:
+    Ui::UpdateDialog* ui;
+    Feed* feed;
+    int type;
+
+    QSettings* settings;
+    void replaceAppVars(QString& string);
+    QString generateChangelogDocument();
+
+    void disableButtons(bool disable = true);
+    void resetUi();
+    void setupLoadingUi();
+    void setupUpdateUi();
+    void setupChangelogUi();
+    void setupNoUpdatesUi();
+    void adjustDialogSize();
+
+    void startDownload();
+    virtual void startUpdate();
+
+    bool accepted;
+    bool isDownloadFinished;
+    QString updateFilePath;
+    QList<Release> releases;
+    QList<Release> updates;
+    Release latestRelease;
+    QList<QAbstractButton*> installButtons;
+    QAbstractButton* acceptedInstallButton;
+    bool _openExternalLinks;
+    QString _minVersion;
+    QString _maxVersion;
+    QString _previousVersion;
+
+    static void setSettingsValue(QString key, QVariant value,
+                                 QSettings* settings = new QSettings());
+    static QVariant settingsValue(QString key, QVariant defaultValue = QVariant(),
+                                  QSettings* settings = new QSettings());
+    static void removeSetting(QString key, QSettings* settings = new QSettings());
+    static void setDefaultSettingsValue(QString key, QVariant value,
+                                        QSettings* settings = new QSettings());
+
+   private slots:
+    void handleFeedReady();
+    void handleDownloadFinished();
+    void handleDownloadError(QString);
+    void updateProgressBar(qint64, qint64);
+    void autoDownloadCheckboxToggled(bool enabled = true);
+    void onLinkActivated(QString link);
+};
+
+}  // namespace dblsqd
+
+#endif  // DBLSQD_UPDATE_DIALOG_H
diff --git a/src/dblsqd/update_dialog.ui b/src/dblsqd/update_dialog.ui
new file mode 100644 (file)
index 0000000..8bb40e4
--- /dev/null
@@ -0,0 +1,402 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>UpdateDialog</class>
+ <widget class="QDialog" name="UpdateDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>600</width>
+    <height>645</height>
+   </rect>
+  </property>
+  <property name="minimumSize">
+   <size>
+    <width>600</width>
+    <height>0</height>
+   </size>
+  </property>
+  <property name="modal">
+   <bool>true</bool>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <item>
+    <widget class="QWidget" name="headerContainerLoading" native="true">
+     <layout class="QGridLayout" name="gridLayout_4">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>6</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item row="0" column="0">
+       <widget class="QLabel" name="labelHeadlineLoading">
+        <property name="font">
+         <font>
+          <weight>75</weight>
+          <bold>true</bold>
+         </font>
+        </property>
+        <property name="text">
+         <string>Loading update information …</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="headerContainer" native="true">
+     <layout class="QGridLayout" name="gridLayout">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>6</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>18</number>
+      </property>
+      <property name="horizontalSpacing">
+       <number>24</number>
+      </property>
+      <property name="verticalSpacing">
+       <number>6</number>
+      </property>
+      <item row="0" column="2">
+       <widget class="QLabel" name="labelHeadline">
+        <property name="font">
+         <font>
+          <weight>75</weight>
+          <bold>true</bold>
+         </font>
+        </property>
+        <property name="text">
+         <string>A new version of %APPNAME% is available!</string>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="0" rowspan="3">
+       <widget class="QLabel" name="labelIcon">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="2" rowspan="2">
+       <widget class="QLabel" name="labelInfo">
+        <property name="sizePolicy">
+         <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+          <horstretch>0</horstretch>
+          <verstretch>0</verstretch>
+         </sizepolicy>
+        </property>
+        <property name="text">
+         <string>%APPNAME% %UPDATE_VERSION% is available (you have %CURRENT_VERSION%).
+Would you like to update now?</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="headerContainerChangelog" native="true">
+     <layout class="QGridLayout" name="gridLayout_3">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>6</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>18</number>
+      </property>
+      <item row="0" column="0">
+       <widget class="QLabel" name="labelHeadlineChangelog">
+        <property name="font">
+         <font>
+          <weight>75</weight>
+          <bold>true</bold>
+         </font>
+        </property>
+        <property name="text">
+         <string>Changelog for %APPNAME%</string>
+        </property>
+       </widget>
+      </item>
+      <item row="1" column="0">
+       <widget class="QLabel" name="labelInfoChangelog">
+        <property name="text">
+         <string>You are using version %CURRENT_VERSION%.</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="headerContainerNoUpdates" native="true">
+     <layout class="QGridLayout" name="gridLayout_2">
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>6</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>18</number>
+      </property>
+      <property name="horizontalSpacing">
+       <number>24</number>
+      </property>
+      <item row="1" column="0">
+       <widget class="QLabel" name="labelInfoNoUpdates">
+        <property name="text">
+         <string>There are currently no updates available.</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item row="0" column="0">
+       <widget class="QLabel" name="labelHeadlineNoUpdates">
+        <property name="font">
+         <font>
+          <weight>75</weight>
+          <bold>true</bold>
+         </font>
+        </property>
+        <property name="text">
+         <string>You are using %APPNAME% %CURRENT_VERSION%.</string>
+        </property>
+        <property name="wordWrap">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item>
+    <widget class="QScrollArea" name="scrollAreaChangelog">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <property name="minimumSize">
+      <size>
+       <width>0</width>
+       <height>150</height>
+      </size>
+     </property>
+     <property name="widgetResizable">
+      <bool>true</bool>
+     </property>
+     <widget class="QWidget" name="scrollAreaWidgetContents">
+      <property name="geometry">
+       <rect>
+        <x>0</x>
+        <y>0</y>
+        <width>580</width>
+        <height>235</height>
+       </rect>
+      </property>
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="styleSheet">
+       <string notr="true">background:white;</string>
+      </property>
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <property name="leftMargin">
+        <number>0</number>
+       </property>
+       <property name="topMargin">
+        <number>0</number>
+       </property>
+       <property name="rightMargin">
+        <number>0</number>
+       </property>
+       <property name="bottomMargin">
+        <number>0</number>
+       </property>
+       <item>
+        <widget class="QLabel" name="labelChangelog">
+         <property name="sizePolicy">
+          <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
+           <horstretch>0</horstretch>
+           <verstretch>0</verstretch>
+          </sizepolicy>
+         </property>
+         <property name="textFormat">
+          <enum>Qt::RichText</enum>
+         </property>
+         <property name="alignment">
+          <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+         </property>
+         <property name="wordWrap">
+          <bool>true</bool>
+         </property>
+         <property name="margin">
+          <number>5</number>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+    </widget>
+   </item>
+   <item>
+    <spacer name="verticalSpacer_2">
+     <property name="orientation">
+      <enum>Qt::Vertical</enum>
+     </property>
+     <property name="sizeType">
+      <enum>QSizePolicy::Fixed</enum>
+     </property>
+     <property name="sizeHint" stdset="0">
+      <size>
+       <width>20</width>
+       <height>20</height>
+      </size>
+     </property>
+    </spacer>
+   </item>
+   <item>
+    <widget class="QProgressBar" name="progressBar">
+     <property name="value">
+      <number>24</number>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QCheckBox" name="checkAutoDownload">
+     <property name="text">
+      <string>Automatically download future updates</string>
+     </property>
+    </widget>
+   </item>
+   <item>
+    <widget class="QWidget" name="buttonContainer" native="true">
+     <property name="sizePolicy">
+      <sizepolicy hsizetype="Preferred" vsizetype="Minimum">
+       <horstretch>0</horstretch>
+       <verstretch>0</verstretch>
+      </sizepolicy>
+     </property>
+     <layout class="QHBoxLayout" name="horizontalLayout">
+      <property name="spacing">
+       <number>12</number>
+      </property>
+      <property name="leftMargin">
+       <number>0</number>
+      </property>
+      <property name="topMargin">
+       <number>0</number>
+      </property>
+      <property name="rightMargin">
+       <number>0</number>
+      </property>
+      <property name="bottomMargin">
+       <number>0</number>
+      </property>
+      <item>
+       <widget class="QToolButton" name="buttonCancel">
+        <property name="contextMenuPolicy">
+         <enum>Qt::ActionsContextMenu</enum>
+        </property>
+        <property name="popupMode">
+         <enum>QToolButton::MenuButtonPopup</enum>
+        </property>
+        <property name="toolButtonStyle">
+         <enum>Qt::ToolButtonFollowStyle</enum>
+        </property>
+        <property name="autoRaise">
+         <bool>true</bool>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="buttonCancelLoading">
+        <property name="text">
+         <string>Cancel</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <spacer name="horizontalSpacer">
+        <property name="orientation">
+         <enum>Qt::Horizontal</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>40</width>
+          <height>20</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item>
+       <widget class="QPushButton" name="buttonInstall">
+        <property name="text">
+         <string>Install update now</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="buttonConfirm">
+        <property name="text">
+         <string>OK</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </widget>
+   </item>
+  </layout>
+  <action name="actionCancel">
+   <property name="text">
+    <string>Remind me later</string>
+   </property>
+  </action>
+  <action name="actionSkip">
+   <property name="text">
+    <string>Skip this version</string>
+   </property>
+  </action>
+ </widget>
+ <layoutdefault spacing="6" margin="11"/>
+ <tabstops>
+  <tabstop>checkAutoDownload</tabstop>
+ </tabstops>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/freeverbdsp.h b/src/freeverbdsp.h
new file mode 100644 (file)
index 0000000..2b7c661
--- /dev/null
@@ -0,0 +1,2563 @@
+/* ------------------------------------------------------------
+author: "Romain Michon"
+license: "LGPL"
+name: "freeverb"
+version: "0.0"
+Code generated with Faust 2.41.1 (https://faust.grame.fr)
+Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn freeverbdsp -es 1 -mcd 16
+-single -ftz 0
+------------------------------------------------------------ */
+
+#ifndef __freeverbdsp_H__
+#define __freeverbdsp_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 <string>
+#include <vector>
+
+/************************************************************************
+ ************************************************************************
+    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.41.1"
+
+// 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<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> 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 <xmmintrin.h>
+#endif
+
+class FAUST_API ScopedNoDenormals
+{
+   private:
+    intptr_t fpsr;
+
+    void setFpStatusRegister(intptr_t fpsr_aux) noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("msr fpcr, %0" : : "ri"(fpsr_aux));
+#elif defined(__SSE__)
+        _mm_setcsr(static_cast<uint32_t>(fpsr_aux));
+#endif
+    }
+
+    void getFpStatusRegister() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("mrs %0, fpcr" : "=r"(fpsr));
+#elif defined(__SSE__)
+        fpsr          = static_cast<intptr_t>(_mm_getcsr());
+#endif
+    }
+
+   public:
+    ScopedNoDenormals() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        intptr_t mask = (1 << 24 /* FZ */);
+#else
+#if defined(__SSE__)
+#if defined(__SSE2__)
+        intptr_t mask = 0x8040;
+#else
+        intptr_t mask = 0x8000;
+#endif
+#else
+        intptr_t mask = 0x0000;
+#endif
+#endif
+        getFpStatusRegister();
+        setFpStatusRegister(fpsr | mask);
+    }
+
+    ~ScopedNoDenormals() noexcept { setFpStatusRegister(fpsr); }
+};
+
+#define AVOIDDENORMALS ScopedNoDenormals();
+
+#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 <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** 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<typename REAL>
+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<FAUSTFLOAT> {
+    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 <algorithm>
+#include <map>
+#include <regex>
+#include <set>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class FAUST_API PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+    std::vector<std::string> fFullPaths;
+    std::map<std::string, std::string> 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<std::string>
+            uniquePaths;  // all full paths transformed but made unique with a prefix
+        std::map<std::string, std::string>
+            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<std::string, int> 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<std::string> collisionSet;
+            std::map<std::string, std::string> 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<char>& 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 <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// 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<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? 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<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(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<double>(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<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(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<double>(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<UpdatableValueConverter*> 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<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> 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<std::string, std::string> 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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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<ZoneControl*>* 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<ZoneControl*>* 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<ZoneControl*>* 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
+    //-------------------------------------------------------------------------------
+    int getParamsCount() { return int(fItems.size()); }
+
+    int getParamIndex(const char* path_aux)
+    {
+        std::string path = std::string(path_aux);
+        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;
+    }
+
+    const char* getParamLabel(int p) { return fItems[uint(p)].fLabel.c_str(); }
+    const char* getParamShortname(int p) { return fItems[uint(p)].fShortname.c_str(); }
+    const char* getParamAddress(int p) { return fItems[uint(p)].fPath.c_str(); }
+
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> metadata = fMetaData[uint(p)];
+        for (const auto& it : metadata) {
+            res[it.first.c_str()] = it.second.c_str();
+        }
+        return res;
+    }
+
+    const char* getMetadata(int p, const char* key)
+    {
+        return (fMetaData[uint(p)].find(key) != fMetaData[uint(p)].end())
+                   ? fMetaData[uint(p)][key].c_str()
+                   : "";
+    }
+    FAUSTFLOAT getParamMin(int p) { return fItems[uint(p)].fMin; }
+    FAUSTFLOAT getParamMax(int p) { return fItems[uint(p)].fMax; }
+    FAUSTFLOAT getParamStep(int p) { return fItems[uint(p)].fStep; }
+    FAUSTFLOAT getParamInit(int p) { return fItems[uint(p)].fInit; }
+
+    FAUSTFLOAT* getParamZone(int p) { return fItems[uint(p)].fZone; }
+
+    FAUSTFLOAT getParamValue(int p) { return *fItems[uint(p)].fZone; }
+    FAUSTFLOAT getParamValue(const char* path)
+    {
+        int index = getParamIndex(path);
+        if (index >= 0) {
+            return getParamValue(index);
+        } else {
+            fprintf(stderr, "getParamValue : '%s' not found\n",
+                    (path == nullptr ? "NULL" : path));
+            return FAUSTFLOAT(0);
+        }
+    }
+
+    void setParamValue(int p, FAUSTFLOAT v) { *fItems[uint(p)].fZone = v; }
+    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
+     * @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
+     * @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
+     * @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
+     * @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; }
+
+    // getScreenColor() : -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/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+#include <math.h>
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifndef FAUSTCLASS
+#define FAUSTCLASS freeverbdsp
+#endif
+
+#ifdef __APPLE__
+#define exp10f __exp10f
+#define exp10  __exp10
+#endif
+
+#if defined(_WIN32)
+#define RESTRICT __restrict
+#else
+#define RESTRICT __restrict__
+#endif
+
+class freeverbdsp : public dsp
+{
+   private:
+    int fSampleRate;
+    float fConst1;
+    FAUSTFLOAT fVslider0;
+    float fConst2;
+    FAUSTFLOAT fVslider1;
+    float fRec9[2];
+    FAUSTFLOAT fVslider2;
+    int IOTA0;
+    float fVec0[8192];
+    int iConst3;
+    float fRec8[2];
+    float fRec11[2];
+    float fVec1[8192];
+    int iConst4;
+    float fRec10[2];
+    float fRec13[2];
+    float fVec2[8192];
+    int iConst5;
+    float fRec12[2];
+    float fRec15[2];
+    float fVec3[8192];
+    int iConst6;
+    float fRec14[2];
+    float fRec17[2];
+    float fVec4[8192];
+    int iConst7;
+    float fRec16[2];
+    float fRec19[2];
+    float fVec5[8192];
+    int iConst8;
+    float fRec18[2];
+    float fRec21[2];
+    float fVec6[8192];
+    int iConst9;
+    float fRec20[2];
+    float fRec23[2];
+    float fVec7[8192];
+    int iConst10;
+    float fRec22[2];
+    float fVec8[2048];
+    int iConst11;
+    int iConst12;
+    float fRec6[2];
+    float fVec9[2048];
+    int iConst13;
+    int iConst14;
+    float fRec4[2];
+    float fVec10[2048];
+    int iConst15;
+    int iConst16;
+    float fRec2[2];
+    float fVec11[1024];
+    int iConst17;
+    int iConst18;
+    float fRec0[2];
+    float fRec33[2];
+    float fVec12[8192];
+    float fConst19;
+    FAUSTFLOAT fVslider3;
+    float fRec32[2];
+    float fRec35[2];
+    float fVec13[8192];
+    float fRec34[2];
+    float fRec37[2];
+    float fVec14[8192];
+    float fRec36[2];
+    float fRec39[2];
+    float fVec15[8192];
+    float fRec38[2];
+    float fRec41[2];
+    float fVec16[8192];
+    float fRec40[2];
+    float fRec43[2];
+    float fVec17[8192];
+    float fRec42[2];
+    float fRec45[2];
+    float fVec18[8192];
+    float fRec44[2];
+    float fRec47[2];
+    float fVec19[8192];
+    float fRec46[2];
+    float fVec20[2048];
+    float fRec30[2];
+    float fVec21[2048];
+    float fRec28[2];
+    float fVec22[2048];
+    float fRec26[2];
+    float fVec23[2048];
+    float fRec24[2];
+
+   public:
+    void metadata(Meta* m)
+    {
+        m->declare("author", "Romain Michon");
+        m->declare("compile_options",
+                   "-a faust2header.cpp -lang cpp -i -inpl -cn freeverbdsp -es 1 -mcd 16 "
+                   "-single -ftz 0");
+        m->declare("delays.lib/name", "Faust Delay Library");
+        m->declare("delays.lib/version", "0.1");
+        m->declare("description",
+                   "Freeverb implementation in Faust, from the Faust Library's "
+                   "dm.freeverb_demo in demos.lib");
+        m->declare("filename", "freeverbdsp.dsp");
+        m->declare("filters.lib/allpass_comb:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/allpass_comb:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/allpass_comb:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/lowpass0_highpass1", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/name", "Faust Filters Library");
+        m->declare("filters.lib/version", "0.3");
+        m->declare("license", "LGPL");
+        m->declare("maths.lib/author", "GRAME");
+        m->declare("maths.lib/copyright", "GRAME");
+        m->declare("maths.lib/license", "LGPL with exception");
+        m->declare("maths.lib/name", "Faust Math Library");
+        m->declare("maths.lib/version", "2.5");
+        m->declare("name", "freeverb");
+        m->declare("platform.lib/name", "Generic Platform Library");
+        m->declare("platform.lib/version", "0.2");
+        m->declare("reverbs.lib/mono_freeverb:author", "Romain Michon");
+        m->declare("reverbs.lib/name", "Faust Reverb Library");
+        m->declare("reverbs.lib/stereo_freeverb:author", "Romain Michon");
+        m->declare("reverbs.lib/version", "0.2");
+        m->declare("version", "0.0");
+    }
+
+    virtual int getNumInputs() { return 2; }
+    virtual int getNumOutputs() { return 2; }
+
+    static void classInit(int /*sample_rate*/) {}
+
+    virtual void instanceConstants(int sample_rate)
+    {
+        fSampleRate = sample_rate;
+        float fConst0 =
+            std::min<float>(192000.0f, std::max<float>(1.0f, float(fSampleRate)));
+        fConst1  = 12348.0f / fConst0;
+        fConst2  = 17640.0f / fConst0;
+        iConst3  = int(0.0253061224f * fConst0);
+        iConst4  = int(0.0269387756f * fConst0);
+        iConst5  = int(0.0289569162f * fConst0);
+        iConst6  = int(0.0307482984f * fConst0);
+        iConst7  = int(0.0322448984f * fConst0);
+        iConst8  = int(0.033809524f * fConst0);
+        iConst9  = int(0.0353061222f * fConst0);
+        iConst10 = int(0.0366666652f * fConst0);
+        iConst11 = int(0.0126077095f * fConst0);
+        iConst12 = std::min<int>(1024, std::max<int>(0, iConst11 + -1));
+        iConst13 = int(0.00999999978f * fConst0);
+        iConst14 = std::min<int>(1024, std::max<int>(0, iConst13 + -1));
+        iConst15 = int(0.00773242628f * fConst0);
+        iConst16 = std::min<int>(1024, std::max<int>(0, iConst15 + -1));
+        iConst17 = int(0.00510204071f * fConst0);
+        iConst18 = std::min<int>(1024, std::max<int>(0, iConst17 + -1));
+        fConst19 = 0.00104308384f * fConst0;
+    }
+
+    virtual void instanceResetUserInterface()
+    {
+        fVslider0 = FAUSTFLOAT(0.10000000000000001f);
+        fVslider1 = FAUSTFLOAT(0.5f);
+        fVslider2 = FAUSTFLOAT(0.10000000000000001f);
+        fVslider3 = FAUSTFLOAT(0.5f);
+    }
+
+    virtual void instanceClear()
+    {
+        for (int l0 = 0; l0 < 2; l0 = l0 + 1) {
+            fRec9[l0] = 0.0f;
+        }
+        IOTA0 = 0;
+        for (int l1 = 0; l1 < 8192; l1 = l1 + 1) {
+            fVec0[l1] = 0.0f;
+        }
+        for (int l2 = 0; l2 < 2; l2 = l2 + 1) {
+            fRec8[l2] = 0.0f;
+        }
+        for (int l3 = 0; l3 < 2; l3 = l3 + 1) {
+            fRec11[l3] = 0.0f;
+        }
+        for (int l4 = 0; l4 < 8192; l4 = l4 + 1) {
+            fVec1[l4] = 0.0f;
+        }
+        for (int l5 = 0; l5 < 2; l5 = l5 + 1) {
+            fRec10[l5] = 0.0f;
+        }
+        for (int l6 = 0; l6 < 2; l6 = l6 + 1) {
+            fRec13[l6] = 0.0f;
+        }
+        for (int l7 = 0; l7 < 8192; l7 = l7 + 1) {
+            fVec2[l7] = 0.0f;
+        }
+        for (int l8 = 0; l8 < 2; l8 = l8 + 1) {
+            fRec12[l8] = 0.0f;
+        }
+        for (int l9 = 0; l9 < 2; l9 = l9 + 1) {
+            fRec15[l9] = 0.0f;
+        }
+        for (int l10 = 0; l10 < 8192; l10 = l10 + 1) {
+            fVec3[l10] = 0.0f;
+        }
+        for (int l11 = 0; l11 < 2; l11 = l11 + 1) {
+            fRec14[l11] = 0.0f;
+        }
+        for (int l12 = 0; l12 < 2; l12 = l12 + 1) {
+            fRec17[l12] = 0.0f;
+        }
+        for (int l13 = 0; l13 < 8192; l13 = l13 + 1) {
+            fVec4[l13] = 0.0f;
+        }
+        for (int l14 = 0; l14 < 2; l14 = l14 + 1) {
+            fRec16[l14] = 0.0f;
+        }
+        for (int l15 = 0; l15 < 2; l15 = l15 + 1) {
+            fRec19[l15] = 0.0f;
+        }
+        for (int l16 = 0; l16 < 8192; l16 = l16 + 1) {
+            fVec5[l16] = 0.0f;
+        }
+        for (int l17 = 0; l17 < 2; l17 = l17 + 1) {
+            fRec18[l17] = 0.0f;
+        }
+        for (int l18 = 0; l18 < 2; l18 = l18 + 1) {
+            fRec21[l18] = 0.0f;
+        }
+        for (int l19 = 0; l19 < 8192; l19 = l19 + 1) {
+            fVec6[l19] = 0.0f;
+        }
+        for (int l20 = 0; l20 < 2; l20 = l20 + 1) {
+            fRec20[l20] = 0.0f;
+        }
+        for (int l21 = 0; l21 < 2; l21 = l21 + 1) {
+            fRec23[l21] = 0.0f;
+        }
+        for (int l22 = 0; l22 < 8192; l22 = l22 + 1) {
+            fVec7[l22] = 0.0f;
+        }
+        for (int l23 = 0; l23 < 2; l23 = l23 + 1) {
+            fRec22[l23] = 0.0f;
+        }
+        for (int l24 = 0; l24 < 2048; l24 = l24 + 1) {
+            fVec8[l24] = 0.0f;
+        }
+        for (int l25 = 0; l25 < 2; l25 = l25 + 1) {
+            fRec6[l25] = 0.0f;
+        }
+        for (int l26 = 0; l26 < 2048; l26 = l26 + 1) {
+            fVec9[l26] = 0.0f;
+        }
+        for (int l27 = 0; l27 < 2; l27 = l27 + 1) {
+            fRec4[l27] = 0.0f;
+        }
+        for (int l28 = 0; l28 < 2048; l28 = l28 + 1) {
+            fVec10[l28] = 0.0f;
+        }
+        for (int l29 = 0; l29 < 2; l29 = l29 + 1) {
+            fRec2[l29] = 0.0f;
+        }
+        for (int l30 = 0; l30 < 1024; l30 = l30 + 1) {
+            fVec11[l30] = 0.0f;
+        }
+        for (int l31 = 0; l31 < 2; l31 = l31 + 1) {
+            fRec0[l31] = 0.0f;
+        }
+        for (int l32 = 0; l32 < 2; l32 = l32 + 1) {
+            fRec33[l32] = 0.0f;
+        }
+        for (int l33 = 0; l33 < 8192; l33 = l33 + 1) {
+            fVec12[l33] = 0.0f;
+        }
+        for (int l34 = 0; l34 < 2; l34 = l34 + 1) {
+            fRec32[l34] = 0.0f;
+        }
+        for (int l35 = 0; l35 < 2; l35 = l35 + 1) {
+            fRec35[l35] = 0.0f;
+        }
+        for (int l36 = 0; l36 < 8192; l36 = l36 + 1) {
+            fVec13[l36] = 0.0f;
+        }
+        for (int l37 = 0; l37 < 2; l37 = l37 + 1) {
+            fRec34[l37] = 0.0f;
+        }
+        for (int l38 = 0; l38 < 2; l38 = l38 + 1) {
+            fRec37[l38] = 0.0f;
+        }
+        for (int l39 = 0; l39 < 8192; l39 = l39 + 1) {
+            fVec14[l39] = 0.0f;
+        }
+        for (int l40 = 0; l40 < 2; l40 = l40 + 1) {
+            fRec36[l40] = 0.0f;
+        }
+        for (int l41 = 0; l41 < 2; l41 = l41 + 1) {
+            fRec39[l41] = 0.0f;
+        }
+        for (int l42 = 0; l42 < 8192; l42 = l42 + 1) {
+            fVec15[l42] = 0.0f;
+        }
+        for (int l43 = 0; l43 < 2; l43 = l43 + 1) {
+            fRec38[l43] = 0.0f;
+        }
+        for (int l44 = 0; l44 < 2; l44 = l44 + 1) {
+            fRec41[l44] = 0.0f;
+        }
+        for (int l45 = 0; l45 < 8192; l45 = l45 + 1) {
+            fVec16[l45] = 0.0f;
+        }
+        for (int l46 = 0; l46 < 2; l46 = l46 + 1) {
+            fRec40[l46] = 0.0f;
+        }
+        for (int l47 = 0; l47 < 2; l47 = l47 + 1) {
+            fRec43[l47] = 0.0f;
+        }
+        for (int l48 = 0; l48 < 8192; l48 = l48 + 1) {
+            fVec17[l48] = 0.0f;
+        }
+        for (int l49 = 0; l49 < 2; l49 = l49 + 1) {
+            fRec42[l49] = 0.0f;
+        }
+        for (int l50 = 0; l50 < 2; l50 = l50 + 1) {
+            fRec45[l50] = 0.0f;
+        }
+        for (int l51 = 0; l51 < 8192; l51 = l51 + 1) {
+            fVec18[l51] = 0.0f;
+        }
+        for (int l52 = 0; l52 < 2; l52 = l52 + 1) {
+            fRec44[l52] = 0.0f;
+        }
+        for (int l53 = 0; l53 < 2; l53 = l53 + 1) {
+            fRec47[l53] = 0.0f;
+        }
+        for (int l54 = 0; l54 < 8192; l54 = l54 + 1) {
+            fVec19[l54] = 0.0f;
+        }
+        for (int l55 = 0; l55 < 2; l55 = l55 + 1) {
+            fRec46[l55] = 0.0f;
+        }
+        for (int l56 = 0; l56 < 2048; l56 = l56 + 1) {
+            fVec20[l56] = 0.0f;
+        }
+        for (int l57 = 0; l57 < 2; l57 = l57 + 1) {
+            fRec30[l57] = 0.0f;
+        }
+        for (int l58 = 0; l58 < 2048; l58 = l58 + 1) {
+            fVec21[l58] = 0.0f;
+        }
+        for (int l59 = 0; l59 < 2; l59 = l59 + 1) {
+            fRec28[l59] = 0.0f;
+        }
+        for (int l60 = 0; l60 < 2048; l60 = l60 + 1) {
+            fVec22[l60] = 0.0f;
+        }
+        for (int l61 = 0; l61 < 2; l61 = l61 + 1) {
+            fRec26[l61] = 0.0f;
+        }
+        for (int l62 = 0; l62 < 2048; l62 = l62 + 1) {
+            fVec23[l62] = 0.0f;
+        }
+        for (int l63 = 0; l63 < 2; l63 = l63 + 1) {
+            fRec24[l63] = 0.0f;
+        }
+    }
+
+    virtual void init(int sample_rate)
+    {
+        classInit(sample_rate);
+        instanceInit(sample_rate);
+    }
+    virtual void instanceInit(int sample_rate)
+    {
+        instanceConstants(sample_rate);
+        instanceResetUserInterface();
+        instanceClear();
+    }
+
+    virtual freeverbdsp* clone() { return new freeverbdsp(); }
+
+    virtual int getSampleRate() { return fSampleRate; }
+
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        ui_interface->openHorizontalBox("Freeverb");
+        ui_interface->declare(0, "0", "");
+        ui_interface->openVerticalBox("0x00");
+        ui_interface->declare(&fVslider1, "0", "");
+        ui_interface->declare(&fVslider1, "style", "knob");
+        ui_interface->declare(&fVslider1, "tooltip",
+                              "Somehow control the   density of the reverb.");
+        ui_interface->addVerticalSlider("Damp", &fVslider1, FAUSTFLOAT(0.5f),
+                                        FAUSTFLOAT(0.0f), FAUSTFLOAT(1.0f),
+                                        FAUSTFLOAT(0.0250000004f));
+        ui_interface->declare(&fVslider0, "1", "");
+        ui_interface->declare(&fVslider0, "style", "knob");
+        ui_interface->declare(
+            &fVslider0, "tooltip",
+            "The room size   between 0 and 1 with 1 for the largest room.");
+        ui_interface->addVerticalSlider("RoomSize", &fVslider0, FAUSTFLOAT(0.100000001f),
+                                        FAUSTFLOAT(0.0f), FAUSTFLOAT(1.0f),
+                                        FAUSTFLOAT(0.0250000004f));
+        ui_interface->declare(&fVslider3, "2", "");
+        ui_interface->declare(&fVslider3, "style", "knob");
+        ui_interface->declare(
+            &fVslider3, "tooltip",
+            "Spatial   spread between 0 and 1 with 1 for maximum spread.");
+        ui_interface->addVerticalSlider("Stereo Spread", &fVslider3, FAUSTFLOAT(0.5f),
+                                        FAUSTFLOAT(0.0f), FAUSTFLOAT(1.0f),
+                                        FAUSTFLOAT(0.00999999978f));
+        ui_interface->closeBox();
+        ui_interface->declare(&fVslider2, "1", "");
+        ui_interface->declare(&fVslider2, "tooltip",
+                              "The amount of reverb applied to the signal   between 0 "
+                              "and 1 with 1 for the maximum amount of reverb.");
+        ui_interface->addVerticalSlider("Wet", &fVslider2, FAUSTFLOAT(0.100000001f),
+                                        FAUSTFLOAT(0.0f), FAUSTFLOAT(1.0f),
+                                        FAUSTFLOAT(0.0250000004f));
+        ui_interface->closeBox();
+    }
+
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs)
+    {
+        FAUSTFLOAT* input0  = inputs[0];
+        FAUSTFLOAT* input1  = inputs[1];
+        FAUSTFLOAT* output0 = outputs[0];
+        FAUSTFLOAT* output1 = outputs[1];
+        float fSlow0        = fConst1 * float(fVslider0) + 0.699999988f;
+        float fSlow1        = fConst2 * float(fVslider1);
+        float fSlow2        = 1.0f - fSlow1;
+        float fSlow3        = float(fVslider2);
+        float fSlow4        = 0.100000001f * fSlow3;
+        float fSlow5        = 1.0f - fSlow3;
+        int iSlow6          = int(fConst19 * float(fVslider3));
+        int iSlow7          = iConst3 + iSlow6;
+        int iSlow8          = iConst4 + iSlow6;
+        int iSlow9          = iConst5 + iSlow6;
+        int iSlow10         = iConst6 + iSlow6;
+        int iSlow11         = iConst7 + iSlow6;
+        int iSlow12         = iConst8 + iSlow6;
+        int iSlow13         = iConst9 + iSlow6;
+        int iSlow14         = iConst10 + iSlow6;
+        int iSlow15         = iSlow6 + -1;
+        int iSlow16         = std::min<int>(1024, std::max<int>(0, iConst11 + iSlow15));
+        int iSlow17         = std::min<int>(1024, std::max<int>(0, iConst13 + iSlow15));
+        int iSlow18         = std::min<int>(1024, std::max<int>(0, iConst15 + iSlow15));
+        int iSlow19         = std::min<int>(1024, std::max<int>(0, iConst17 + iSlow15));
+        for (int i0 = 0; i0 < count; i0 = i0 + 1) {
+            float fTemp0        = float(input0[i0]);
+            float fTemp1        = float(input1[i0]);
+            fRec9[0]            = fSlow1 * fRec9[1] + fSlow2 * fRec8[1];
+            float fTemp2        = fSlow4 * (fTemp0 + fTemp1);
+            fVec0[IOTA0 & 8191] = fSlow0 * fRec9[0] + fTemp2;
+            fRec8[0]            = fVec0[(IOTA0 - iConst3) & 8191];
+            fRec11[0]           = fSlow1 * fRec11[1] + fSlow2 * fRec10[1];
+            fVec1[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec11[0];
+            fRec10[0]           = fVec1[(IOTA0 - iConst4) & 8191];
+            fRec13[0]           = fSlow1 * fRec13[1] + fSlow2 * fRec12[1];
+            fVec2[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec13[0];
+            fRec12[0]           = fVec2[(IOTA0 - iConst5) & 8191];
+            fRec15[0]           = fSlow1 * fRec15[1] + fSlow2 * fRec14[1];
+            fVec3[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec15[0];
+            fRec14[0]           = fVec3[(IOTA0 - iConst6) & 8191];
+            fRec17[0]           = fSlow1 * fRec17[1] + fSlow2 * fRec16[1];
+            fVec4[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec17[0];
+            fRec16[0]           = fVec4[(IOTA0 - iConst7) & 8191];
+            fRec19[0]           = fSlow1 * fRec19[1] + fSlow2 * fRec18[1];
+            fVec5[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec19[0];
+            fRec18[0]           = fVec5[(IOTA0 - iConst8) & 8191];
+            fRec21[0]           = fSlow1 * fRec21[1] + fSlow2 * fRec20[1];
+            fVec6[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec21[0];
+            fRec20[0]           = fVec6[(IOTA0 - iConst9) & 8191];
+            fRec23[0]           = fSlow1 * fRec23[1] + fSlow2 * fRec22[1];
+            fVec7[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec23[0];
+            fRec22[0]           = fVec7[(IOTA0 - iConst10) & 8191];
+            float fTemp3        = fRec8[0] + fRec10[0] + fRec12[0] + fRec14[0] + fRec16[0]
+                           + fRec18[0] + fRec20[0] + fRec22[0] + 0.5f * fRec6[1];
+            fVec8[IOTA0 & 2047]  = fTemp3;
+            fRec6[0]             = fVec8[(IOTA0 - iConst12) & 2047];
+            float fRec7          = 0.0f - 0.5f * fTemp3;
+            float fTemp4         = fRec6[1] + fRec7 + 0.5f * fRec4[1];
+            fVec9[IOTA0 & 2047]  = fTemp4;
+            fRec4[0]             = fVec9[(IOTA0 - iConst14) & 2047];
+            float fRec5          = 0.0f - 0.5f * fTemp4;
+            float fTemp5         = fRec4[1] + fRec5 + 0.5f * fRec2[1];
+            fVec10[IOTA0 & 2047] = fTemp5;
+            fRec2[0]             = fVec10[(IOTA0 - iConst16) & 2047];
+            float fRec3          = 0.0f - 0.5f * fTemp5;
+            float fTemp6         = fRec2[1] + fRec3 + 0.5f * fRec0[1];
+            fVec11[IOTA0 & 1023] = fTemp6;
+            fRec0[0]             = fVec11[(IOTA0 - iConst18) & 1023];
+            float fRec1          = 0.0f - 0.5f * fTemp6;
+            output0[i0]          = FAUSTFLOAT(fRec1 + fRec0[1] + fSlow5 * fTemp0);
+            fRec33[0]            = fSlow1 * fRec33[1] + fSlow2 * fRec32[1];
+            fVec12[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec33[0];
+            fRec32[0]            = fVec12[(IOTA0 - iSlow7) & 8191];
+            fRec35[0]            = fSlow1 * fRec35[1] + fSlow2 * fRec34[1];
+            fVec13[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec35[0];
+            fRec34[0]            = fVec13[(IOTA0 - iSlow8) & 8191];
+            fRec37[0]            = fSlow1 * fRec37[1] + fSlow2 * fRec36[1];
+            fVec14[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec37[0];
+            fRec36[0]            = fVec14[(IOTA0 - iSlow9) & 8191];
+            fRec39[0]            = fSlow1 * fRec39[1] + fSlow2 * fRec38[1];
+            fVec15[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec39[0];
+            fRec38[0]            = fVec15[(IOTA0 - iSlow10) & 8191];
+            fRec41[0]            = fSlow1 * fRec41[1] + fSlow2 * fRec40[1];
+            fVec16[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec41[0];
+            fRec40[0]            = fVec16[(IOTA0 - iSlow11) & 8191];
+            fRec43[0]            = fSlow1 * fRec43[1] + fSlow2 * fRec42[1];
+            fVec17[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec43[0];
+            fRec42[0]            = fVec17[(IOTA0 - iSlow12) & 8191];
+            fRec45[0]            = fSlow1 * fRec45[1] + fSlow2 * fRec44[1];
+            fVec18[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec45[0];
+            fRec44[0]            = fVec18[(IOTA0 - iSlow13) & 8191];
+            fRec47[0]            = fSlow1 * fRec47[1] + fSlow2 * fRec46[1];
+            fVec19[IOTA0 & 8191] = fTemp2 + fSlow0 * fRec47[0];
+            fRec46[0]            = fVec19[(IOTA0 - iSlow14) & 8191];
+            float fTemp7 = fRec32[0] + fRec34[0] + fRec36[0] + fRec38[0] + fRec40[0]
+                           + fRec42[0] + fRec44[0] + fRec46[0] + 0.5f * fRec30[1];
+            fVec20[IOTA0 & 2047] = fTemp7;
+            fRec30[0]            = fVec20[(IOTA0 - iSlow16) & 2047];
+            float fRec31         = 0.0f - 0.5f * fTemp7;
+            float fTemp8         = fRec30[1] + fRec31 + 0.5f * fRec28[1];
+            fVec21[IOTA0 & 2047] = fTemp8;
+            fRec28[0]            = fVec21[(IOTA0 - iSlow17) & 2047];
+            float fRec29         = 0.0f - 0.5f * fTemp8;
+            float fTemp9         = fRec28[1] + fRec29 + 0.5f * fRec26[1];
+            fVec22[IOTA0 & 2047] = fTemp9;
+            fRec26[0]            = fVec22[(IOTA0 - iSlow18) & 2047];
+            float fRec27         = 0.0f - 0.5f * fTemp9;
+            float fTemp10        = fRec26[1] + fRec27 + 0.5f * fRec24[1];
+            fVec23[IOTA0 & 2047] = fTemp10;
+            fRec24[0]            = fVec23[(IOTA0 - iSlow19) & 2047];
+            float fRec25         = 0.0f - 0.5f * fTemp10;
+            output1[i0]          = FAUSTFLOAT(fRec25 + fRec24[1] + fSlow5 * fTemp1);
+            fRec9[1]             = fRec9[0];
+            IOTA0                = IOTA0 + 1;
+            fRec8[1]             = fRec8[0];
+            fRec11[1]            = fRec11[0];
+            fRec10[1]            = fRec10[0];
+            fRec13[1]            = fRec13[0];
+            fRec12[1]            = fRec12[0];
+            fRec15[1]            = fRec15[0];
+            fRec14[1]            = fRec14[0];
+            fRec17[1]            = fRec17[0];
+            fRec16[1]            = fRec16[0];
+            fRec19[1]            = fRec19[0];
+            fRec18[1]            = fRec18[0];
+            fRec21[1]            = fRec21[0];
+            fRec20[1]            = fRec20[0];
+            fRec23[1]            = fRec23[0];
+            fRec22[1]            = fRec22[0];
+            fRec6[1]             = fRec6[0];
+            fRec4[1]             = fRec4[0];
+            fRec2[1]             = fRec2[0];
+            fRec0[1]             = fRec0[0];
+            fRec33[1]            = fRec33[0];
+            fRec32[1]            = fRec32[0];
+            fRec35[1]            = fRec35[0];
+            fRec34[1]            = fRec34[0];
+            fRec37[1]            = fRec37[0];
+            fRec36[1]            = fRec36[0];
+            fRec39[1]            = fRec39[0];
+            fRec38[1]            = fRec38[0];
+            fRec41[1]            = fRec41[0];
+            fRec40[1]            = fRec40[0];
+            fRec43[1]            = fRec43[0];
+            fRec42[1]            = fRec42[0];
+            fRec45[1]            = fRec45[0];
+            fRec44[1]            = fRec44[0];
+            fRec47[1]            = fRec47[0];
+            fRec46[1]            = fRec46[0];
+            fRec30[1]            = fRec30[0];
+            fRec28[1]            = fRec28[0];
+            fRec26[1]            = fRec26[0];
+            fRec24[1]            = fRec24[0];
+        }
+    }
+};
+
+#endif
diff --git a/src/freeverbmonodsp.h b/src/freeverbmonodsp.h
new file mode 100644 (file)
index 0000000..e7cea89
--- /dev/null
@@ -0,0 +1,2558 @@
+/* ------------------------------------------------------------
+name: "freeverbmonodsp"
+Code generated with Faust 2.41.1 (https://faust.grame.fr)
+Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn freeverbmonodsp -es 1 -mcd
+16 -single -ftz 0
+------------------------------------------------------------ */
+
+#ifndef __freeverbmonodsp_H__
+#define __freeverbmonodsp_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 <string>
+#include <vector>
+
+/************************************************************************
+ ************************************************************************
+    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.41.1"
+
+// 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<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> 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 <xmmintrin.h>
+#endif
+
+class FAUST_API ScopedNoDenormals
+{
+   private:
+    intptr_t fpsr;
+
+    void setFpStatusRegister(intptr_t fpsr_aux) noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("msr fpcr, %0" : : "ri"(fpsr_aux));
+#elif defined(__SSE__)
+        _mm_setcsr(static_cast<uint32_t>(fpsr_aux));
+#endif
+    }
+
+    void getFpStatusRegister() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("mrs %0, fpcr" : "=r"(fpsr));
+#elif defined(__SSE__)
+        fpsr          = static_cast<intptr_t>(_mm_getcsr());
+#endif
+    }
+
+   public:
+    ScopedNoDenormals() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        intptr_t mask = (1 << 24 /* FZ */);
+#else
+#if defined(__SSE__)
+#if defined(__SSE2__)
+        intptr_t mask = 0x8040;
+#else
+        intptr_t mask = 0x8000;
+#endif
+#else
+        intptr_t mask = 0x0000;
+#endif
+#endif
+        getFpStatusRegister();
+        setFpStatusRegister(fpsr | mask);
+    }
+
+    ~ScopedNoDenormals() noexcept { setFpStatusRegister(fpsr); }
+};
+
+#define AVOIDDENORMALS ScopedNoDenormals();
+
+#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 <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** 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<typename REAL>
+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<FAUSTFLOAT> {
+    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 <algorithm>
+#include <map>
+#include <regex>
+#include <set>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class FAUST_API PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+    std::vector<std::string> fFullPaths;
+    std::map<std::string, std::string> 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<std::string>
+            uniquePaths;  // all full paths transformed but made unique with a prefix
+        std::map<std::string, std::string>
+            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<std::string, int> 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<std::string> collisionSet;
+            std::map<std::string, std::string> 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<char>& 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 <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// 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<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? 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<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(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<double>(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<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(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<double>(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<UpdatableValueConverter*> 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<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> 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<std::string, std::string> 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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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<ZoneControl*>* 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<ZoneControl*>* 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<ZoneControl*>* 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
+    //-------------------------------------------------------------------------------
+    int getParamsCount() { return int(fItems.size()); }
+
+    int getParamIndex(const char* path_aux)
+    {
+        std::string path = std::string(path_aux);
+        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;
+    }
+
+    const char* getParamLabel(int p) { return fItems[uint(p)].fLabel.c_str(); }
+    const char* getParamShortname(int p) { return fItems[uint(p)].fShortname.c_str(); }
+    const char* getParamAddress(int p) { return fItems[uint(p)].fPath.c_str(); }
+
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> metadata = fMetaData[uint(p)];
+        for (const auto& it : metadata) {
+            res[it.first.c_str()] = it.second.c_str();
+        }
+        return res;
+    }
+
+    const char* getMetadata(int p, const char* key)
+    {
+        return (fMetaData[uint(p)].find(key) != fMetaData[uint(p)].end())
+                   ? fMetaData[uint(p)][key].c_str()
+                   : "";
+    }
+    FAUSTFLOAT getParamMin(int p) { return fItems[uint(p)].fMin; }
+    FAUSTFLOAT getParamMax(int p) { return fItems[uint(p)].fMax; }
+    FAUSTFLOAT getParamStep(int p) { return fItems[uint(p)].fStep; }
+    FAUSTFLOAT getParamInit(int p) { return fItems[uint(p)].fInit; }
+
+    FAUSTFLOAT* getParamZone(int p) { return fItems[uint(p)].fZone; }
+
+    FAUSTFLOAT getParamValue(int p) { return *fItems[uint(p)].fZone; }
+    FAUSTFLOAT getParamValue(const char* path)
+    {
+        int index = getParamIndex(path);
+        if (index >= 0) {
+            return getParamValue(index);
+        } else {
+            fprintf(stderr, "getParamValue : '%s' not found\n",
+                    (path == nullptr ? "NULL" : path));
+            return FAUSTFLOAT(0);
+        }
+    }
+
+    void setParamValue(int p, FAUSTFLOAT v) { *fItems[uint(p)].fZone = v; }
+    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
+     * @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
+     * @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
+     * @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
+     * @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; }
+
+    // getScreenColor() : -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/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+#include <math.h>
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifndef FAUSTCLASS
+#define FAUSTCLASS freeverbmonodsp
+#endif
+
+#ifdef __APPLE__
+#define exp10f __exp10f
+#define exp10  __exp10
+#endif
+
+#if defined(_WIN32)
+#define RESTRICT __restrict
+#else
+#define RESTRICT __restrict__
+#endif
+
+class freeverbmonodsp : public dsp
+{
+   private:
+    FAUSTFLOAT fVslider0;
+    int fSampleRate;
+    float fConst1;
+    FAUSTFLOAT fVslider1;
+    float fConst2;
+    FAUSTFLOAT fVslider2;
+    float fRec9[2];
+    int IOTA0;
+    float fVec0[8192];
+    int iConst3;
+    float fConst4;
+    FAUSTFLOAT fVslider3;
+    float fRec8[2];
+    float fRec11[2];
+    float fVec1[8192];
+    int iConst5;
+    float fRec10[2];
+    float fRec13[2];
+    float fVec2[8192];
+    int iConst6;
+    float fRec12[2];
+    float fRec15[2];
+    float fVec3[8192];
+    int iConst7;
+    float fRec14[2];
+    float fRec17[2];
+    float fVec4[8192];
+    int iConst8;
+    float fRec16[2];
+    float fRec19[2];
+    float fVec5[8192];
+    int iConst9;
+    float fRec18[2];
+    float fRec21[2];
+    float fVec6[8192];
+    int iConst10;
+    float fRec20[2];
+    float fRec23[2];
+    float fVec7[8192];
+    int iConst11;
+    float fRec22[2];
+    float fVec8[2048];
+    int iConst12;
+    float fRec6[2];
+    float fVec9[2048];
+    int iConst13;
+    float fRec4[2];
+    float fVec10[2048];
+    int iConst14;
+    float fRec2[2];
+    float fVec11[2048];
+    int iConst15;
+    float fRec0[2];
+    float fRec33[2];
+    float fVec12[8192];
+    float fRec32[2];
+    float fRec35[2];
+    float fVec13[8192];
+    float fRec34[2];
+    float fRec37[2];
+    float fVec14[8192];
+    float fRec36[2];
+    float fRec39[2];
+    float fVec15[8192];
+    float fRec38[2];
+    float fRec41[2];
+    float fVec16[8192];
+    float fRec40[2];
+    float fRec43[2];
+    float fVec17[8192];
+    float fRec42[2];
+    float fRec45[2];
+    float fVec18[8192];
+    float fRec44[2];
+    float fRec47[2];
+    float fVec19[8192];
+    float fRec46[2];
+    float fVec20[2048];
+    int iConst16;
+    float fRec30[2];
+    float fVec21[2048];
+    int iConst17;
+    float fRec28[2];
+    float fVec22[2048];
+    int iConst18;
+    float fRec26[2];
+    float fVec23[1024];
+    int iConst19;
+    float fRec24[2];
+
+   public:
+    void metadata(Meta* m)
+    {
+        m->declare("compile_options",
+                   "-a faust2header.cpp -lang cpp -i -inpl -cn freeverbmonodsp -es 1 "
+                   "-mcd 16 -single -ftz 0");
+        m->declare("delays.lib/name", "Faust Delay Library");
+        m->declare("delays.lib/version", "0.1");
+        m->declare("filename", "freeverbmonodsp.dsp");
+        m->declare("filters.lib/allpass_comb:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/allpass_comb:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/allpass_comb:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/lowpass0_highpass1", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/name", "Faust Filters Library");
+        m->declare("filters.lib/version", "0.3");
+        m->declare("freeverbdsp.dsp/author", "Romain Michon");
+        m->declare("freeverbdsp.dsp/description",
+                   "Freeverb implementation in Faust, from the Faust Library's "
+                   "dm.freeverb_demo in demos.lib");
+        m->declare("freeverbdsp.dsp/license", "LGPL");
+        m->declare("freeverbdsp.dsp/name", "freeverb");
+        m->declare("freeverbdsp.dsp/version", "0.0");
+        m->declare("maths.lib/author", "GRAME");
+        m->declare("maths.lib/copyright", "GRAME");
+        m->declare("maths.lib/license", "LGPL with exception");
+        m->declare("maths.lib/name", "Faust Math Library");
+        m->declare("maths.lib/version", "2.5");
+        m->declare("name", "freeverbmonodsp");
+        m->declare("platform.lib/name", "Generic Platform Library");
+        m->declare("platform.lib/version", "0.2");
+        m->declare("reverbs.lib/mono_freeverb:author", "Romain Michon");
+        m->declare("reverbs.lib/name", "Faust Reverb Library");
+        m->declare("reverbs.lib/stereo_freeverb:author", "Romain Michon");
+        m->declare("reverbs.lib/version", "0.2");
+    }
+
+    virtual int getNumInputs() { return 1; }
+    virtual int getNumOutputs() { return 1; }
+
+    static void classInit(int /*sample_rate*/) {}
+
+    virtual void instanceConstants(int sample_rate)
+    {
+        fSampleRate = sample_rate;
+        float fConst0 =
+            std::min<float>(192000.0f, std::max<float>(1.0f, float(fSampleRate)));
+        fConst1  = 12348.0f / fConst0;
+        fConst2  = 17640.0f / fConst0;
+        iConst3  = int(0.0253061224f * fConst0);
+        fConst4  = 0.00104308384f * fConst0;
+        iConst5  = int(0.0269387756f * fConst0);
+        iConst6  = int(0.0289569162f * fConst0);
+        iConst7  = int(0.0307482984f * fConst0);
+        iConst8  = int(0.0322448984f * fConst0);
+        iConst9  = int(0.033809524f * fConst0);
+        iConst10 = int(0.0353061222f * fConst0);
+        iConst11 = int(0.0366666652f * fConst0);
+        iConst12 = int(0.0126077095f * fConst0);
+        iConst13 = int(0.00999999978f * fConst0);
+        iConst14 = int(0.00773242628f * fConst0);
+        iConst15 = int(0.00510204071f * fConst0);
+        iConst16 = std::min<int>(1024, std::max<int>(0, iConst12 + -1));
+        iConst17 = std::min<int>(1024, std::max<int>(0, iConst13 + -1));
+        iConst18 = std::min<int>(1024, std::max<int>(0, iConst14 + -1));
+        iConst19 = std::min<int>(1024, std::max<int>(0, iConst15 + -1));
+    }
+
+    virtual void instanceResetUserInterface()
+    {
+        fVslider0 = FAUSTFLOAT(0.10000000000000001f);
+        fVslider1 = FAUSTFLOAT(0.10000000000000001f);
+        fVslider2 = FAUSTFLOAT(0.5f);
+        fVslider3 = FAUSTFLOAT(0.5f);
+    }
+
+    virtual void instanceClear()
+    {
+        for (int l0 = 0; l0 < 2; l0 = l0 + 1) {
+            fRec9[l0] = 0.0f;
+        }
+        IOTA0 = 0;
+        for (int l1 = 0; l1 < 8192; l1 = l1 + 1) {
+            fVec0[l1] = 0.0f;
+        }
+        for (int l2 = 0; l2 < 2; l2 = l2 + 1) {
+            fRec8[l2] = 0.0f;
+        }
+        for (int l3 = 0; l3 < 2; l3 = l3 + 1) {
+            fRec11[l3] = 0.0f;
+        }
+        for (int l4 = 0; l4 < 8192; l4 = l4 + 1) {
+            fVec1[l4] = 0.0f;
+        }
+        for (int l5 = 0; l5 < 2; l5 = l5 + 1) {
+            fRec10[l5] = 0.0f;
+        }
+        for (int l6 = 0; l6 < 2; l6 = l6 + 1) {
+            fRec13[l6] = 0.0f;
+        }
+        for (int l7 = 0; l7 < 8192; l7 = l7 + 1) {
+            fVec2[l7] = 0.0f;
+        }
+        for (int l8 = 0; l8 < 2; l8 = l8 + 1) {
+            fRec12[l8] = 0.0f;
+        }
+        for (int l9 = 0; l9 < 2; l9 = l9 + 1) {
+            fRec15[l9] = 0.0f;
+        }
+        for (int l10 = 0; l10 < 8192; l10 = l10 + 1) {
+            fVec3[l10] = 0.0f;
+        }
+        for (int l11 = 0; l11 < 2; l11 = l11 + 1) {
+            fRec14[l11] = 0.0f;
+        }
+        for (int l12 = 0; l12 < 2; l12 = l12 + 1) {
+            fRec17[l12] = 0.0f;
+        }
+        for (int l13 = 0; l13 < 8192; l13 = l13 + 1) {
+            fVec4[l13] = 0.0f;
+        }
+        for (int l14 = 0; l14 < 2; l14 = l14 + 1) {
+            fRec16[l14] = 0.0f;
+        }
+        for (int l15 = 0; l15 < 2; l15 = l15 + 1) {
+            fRec19[l15] = 0.0f;
+        }
+        for (int l16 = 0; l16 < 8192; l16 = l16 + 1) {
+            fVec5[l16] = 0.0f;
+        }
+        for (int l17 = 0; l17 < 2; l17 = l17 + 1) {
+            fRec18[l17] = 0.0f;
+        }
+        for (int l18 = 0; l18 < 2; l18 = l18 + 1) {
+            fRec21[l18] = 0.0f;
+        }
+        for (int l19 = 0; l19 < 8192; l19 = l19 + 1) {
+            fVec6[l19] = 0.0f;
+        }
+        for (int l20 = 0; l20 < 2; l20 = l20 + 1) {
+            fRec20[l20] = 0.0f;
+        }
+        for (int l21 = 0; l21 < 2; l21 = l21 + 1) {
+            fRec23[l21] = 0.0f;
+        }
+        for (int l22 = 0; l22 < 8192; l22 = l22 + 1) {
+            fVec7[l22] = 0.0f;
+        }
+        for (int l23 = 0; l23 < 2; l23 = l23 + 1) {
+            fRec22[l23] = 0.0f;
+        }
+        for (int l24 = 0; l24 < 2048; l24 = l24 + 1) {
+            fVec8[l24] = 0.0f;
+        }
+        for (int l25 = 0; l25 < 2; l25 = l25 + 1) {
+            fRec6[l25] = 0.0f;
+        }
+        for (int l26 = 0; l26 < 2048; l26 = l26 + 1) {
+            fVec9[l26] = 0.0f;
+        }
+        for (int l27 = 0; l27 < 2; l27 = l27 + 1) {
+            fRec4[l27] = 0.0f;
+        }
+        for (int l28 = 0; l28 < 2048; l28 = l28 + 1) {
+            fVec10[l28] = 0.0f;
+        }
+        for (int l29 = 0; l29 < 2; l29 = l29 + 1) {
+            fRec2[l29] = 0.0f;
+        }
+        for (int l30 = 0; l30 < 2048; l30 = l30 + 1) {
+            fVec11[l30] = 0.0f;
+        }
+        for (int l31 = 0; l31 < 2; l31 = l31 + 1) {
+            fRec0[l31] = 0.0f;
+        }
+        for (int l32 = 0; l32 < 2; l32 = l32 + 1) {
+            fRec33[l32] = 0.0f;
+        }
+        for (int l33 = 0; l33 < 8192; l33 = l33 + 1) {
+            fVec12[l33] = 0.0f;
+        }
+        for (int l34 = 0; l34 < 2; l34 = l34 + 1) {
+            fRec32[l34] = 0.0f;
+        }
+        for (int l35 = 0; l35 < 2; l35 = l35 + 1) {
+            fRec35[l35] = 0.0f;
+        }
+        for (int l36 = 0; l36 < 8192; l36 = l36 + 1) {
+            fVec13[l36] = 0.0f;
+        }
+        for (int l37 = 0; l37 < 2; l37 = l37 + 1) {
+            fRec34[l37] = 0.0f;
+        }
+        for (int l38 = 0; l38 < 2; l38 = l38 + 1) {
+            fRec37[l38] = 0.0f;
+        }
+        for (int l39 = 0; l39 < 8192; l39 = l39 + 1) {
+            fVec14[l39] = 0.0f;
+        }
+        for (int l40 = 0; l40 < 2; l40 = l40 + 1) {
+            fRec36[l40] = 0.0f;
+        }
+        for (int l41 = 0; l41 < 2; l41 = l41 + 1) {
+            fRec39[l41] = 0.0f;
+        }
+        for (int l42 = 0; l42 < 8192; l42 = l42 + 1) {
+            fVec15[l42] = 0.0f;
+        }
+        for (int l43 = 0; l43 < 2; l43 = l43 + 1) {
+            fRec38[l43] = 0.0f;
+        }
+        for (int l44 = 0; l44 < 2; l44 = l44 + 1) {
+            fRec41[l44] = 0.0f;
+        }
+        for (int l45 = 0; l45 < 8192; l45 = l45 + 1) {
+            fVec16[l45] = 0.0f;
+        }
+        for (int l46 = 0; l46 < 2; l46 = l46 + 1) {
+            fRec40[l46] = 0.0f;
+        }
+        for (int l47 = 0; l47 < 2; l47 = l47 + 1) {
+            fRec43[l47] = 0.0f;
+        }
+        for (int l48 = 0; l48 < 8192; l48 = l48 + 1) {
+            fVec17[l48] = 0.0f;
+        }
+        for (int l49 = 0; l49 < 2; l49 = l49 + 1) {
+            fRec42[l49] = 0.0f;
+        }
+        for (int l50 = 0; l50 < 2; l50 = l50 + 1) {
+            fRec45[l50] = 0.0f;
+        }
+        for (int l51 = 0; l51 < 8192; l51 = l51 + 1) {
+            fVec18[l51] = 0.0f;
+        }
+        for (int l52 = 0; l52 < 2; l52 = l52 + 1) {
+            fRec44[l52] = 0.0f;
+        }
+        for (int l53 = 0; l53 < 2; l53 = l53 + 1) {
+            fRec47[l53] = 0.0f;
+        }
+        for (int l54 = 0; l54 < 8192; l54 = l54 + 1) {
+            fVec19[l54] = 0.0f;
+        }
+        for (int l55 = 0; l55 < 2; l55 = l55 + 1) {
+            fRec46[l55] = 0.0f;
+        }
+        for (int l56 = 0; l56 < 2048; l56 = l56 + 1) {
+            fVec20[l56] = 0.0f;
+        }
+        for (int l57 = 0; l57 < 2; l57 = l57 + 1) {
+            fRec30[l57] = 0.0f;
+        }
+        for (int l58 = 0; l58 < 2048; l58 = l58 + 1) {
+            fVec21[l58] = 0.0f;
+        }
+        for (int l59 = 0; l59 < 2; l59 = l59 + 1) {
+            fRec28[l59] = 0.0f;
+        }
+        for (int l60 = 0; l60 < 2048; l60 = l60 + 1) {
+            fVec22[l60] = 0.0f;
+        }
+        for (int l61 = 0; l61 < 2; l61 = l61 + 1) {
+            fRec26[l61] = 0.0f;
+        }
+        for (int l62 = 0; l62 < 1024; l62 = l62 + 1) {
+            fVec23[l62] = 0.0f;
+        }
+        for (int l63 = 0; l63 < 2; l63 = l63 + 1) {
+            fRec24[l63] = 0.0f;
+        }
+    }
+
+    virtual void init(int sample_rate)
+    {
+        classInit(sample_rate);
+        instanceInit(sample_rate);
+    }
+    virtual void instanceInit(int sample_rate)
+    {
+        instanceConstants(sample_rate);
+        instanceResetUserInterface();
+        instanceClear();
+    }
+
+    virtual freeverbmonodsp* clone() { return new freeverbmonodsp(); }
+
+    virtual int getSampleRate() { return fSampleRate; }
+
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        ui_interface->openHorizontalBox("Freeverb");
+        ui_interface->declare(0, "0", "");
+        ui_interface->openVerticalBox("0x00");
+        ui_interface->declare(&fVslider2, "0", "");
+        ui_interface->declare(&fVslider2, "style", "knob");
+        ui_interface->declare(&fVslider2, "tooltip",
+                              "Somehow control the   density of the reverb.");
+        ui_interface->addVerticalSlider("Damp", &fVslider2, FAUSTFLOAT(0.5f),
+                                        FAUSTFLOAT(0.0f), FAUSTFLOAT(1.0f),
+                                        FAUSTFLOAT(0.0250000004f));
+        ui_interface->declare(&fVslider1, "1", "");
+        ui_interface->declare(&fVslider1, "style", "knob");
+        ui_interface->declare(
+            &fVslider1, "tooltip",
+            "The room size   between 0 and 1 with 1 for the largest room.");
+        ui_interface->addVerticalSlider("RoomSize", &fVslider1, FAUSTFLOAT(0.100000001f),
+                                        FAUSTFLOAT(0.0f), FAUSTFLOAT(1.0f),
+                                        FAUSTFLOAT(0.0250000004f));
+        ui_interface->declare(&fVslider3, "2", "");
+        ui_interface->declare(&fVslider3, "style", "knob");
+        ui_interface->declare(
+            &fVslider3, "tooltip",
+            "Spatial   spread between 0 and 1 with 1 for maximum spread.");
+        ui_interface->addVerticalSlider("Stereo Spread", &fVslider3, FAUSTFLOAT(0.5f),
+                                        FAUSTFLOAT(0.0f), FAUSTFLOAT(1.0f),
+                                        FAUSTFLOAT(0.00999999978f));
+        ui_interface->closeBox();
+        ui_interface->declare(&fVslider0, "1", "");
+        ui_interface->declare(&fVslider0, "tooltip",
+                              "The amount of reverb applied to the signal   between 0 "
+                              "and 1 with 1 for the maximum amount of reverb.");
+        ui_interface->addVerticalSlider("Wet", &fVslider0, FAUSTFLOAT(0.100000001f),
+                                        FAUSTFLOAT(0.0f), FAUSTFLOAT(1.0f),
+                                        FAUSTFLOAT(0.0250000004f));
+        ui_interface->closeBox();
+    }
+
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs)
+    {
+        FAUSTFLOAT* input0  = inputs[0];
+        FAUSTFLOAT* output0 = outputs[0];
+        float fSlow0        = float(fVslider0);
+        float fSlow1        = 0.200000003f * fSlow0;
+        float fSlow2        = fConst1 * float(fVslider1) + 0.699999988f;
+        float fSlow3        = fConst2 * float(fVslider2);
+        float fSlow4        = 1.0f - fSlow3;
+        int iSlow5          = int(fConst4 * float(fVslider3));
+        int iSlow6          = iConst3 + iSlow5;
+        int iSlow7          = iConst5 + iSlow5;
+        int iSlow8          = iConst6 + iSlow5;
+        int iSlow9          = iConst7 + iSlow5;
+        int iSlow10         = iConst8 + iSlow5;
+        int iSlow11         = iConst9 + iSlow5;
+        int iSlow12         = iConst10 + iSlow5;
+        int iSlow13         = iConst11 + iSlow5;
+        int iSlow14         = iSlow5 + -1;
+        int iSlow15         = std::min<int>(1024, std::max<int>(0, iConst12 + iSlow14));
+        int iSlow16         = std::min<int>(1024, std::max<int>(0, iConst13 + iSlow14));
+        int iSlow17         = std::min<int>(1024, std::max<int>(0, iConst14 + iSlow14));
+        int iSlow18         = std::min<int>(1024, std::max<int>(0, iConst15 + iSlow14));
+        float fSlow19       = 2.0f * (1.0f - fSlow0);
+        for (int i0 = 0; i0 < count; i0 = i0 + 1) {
+            float fTemp0        = float(input0[i0]);
+            float fTemp1        = fSlow1 * fTemp0;
+            fRec9[0]            = fSlow3 * fRec9[1] + fSlow4 * fRec8[1];
+            fVec0[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec9[0];
+            fRec8[0]            = fVec0[(IOTA0 - iSlow6) & 8191];
+            fRec11[0]           = fSlow3 * fRec11[1] + fSlow4 * fRec10[1];
+            fVec1[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec11[0];
+            fRec10[0]           = fVec1[(IOTA0 - iSlow7) & 8191];
+            fRec13[0]           = fSlow3 * fRec13[1] + fSlow4 * fRec12[1];
+            fVec2[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec13[0];
+            fRec12[0]           = fVec2[(IOTA0 - iSlow8) & 8191];
+            fRec15[0]           = fSlow3 * fRec15[1] + fSlow4 * fRec14[1];
+            fVec3[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec15[0];
+            fRec14[0]           = fVec3[(IOTA0 - iSlow9) & 8191];
+            fRec17[0]           = fSlow3 * fRec17[1] + fSlow4 * fRec16[1];
+            fVec4[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec17[0];
+            fRec16[0]           = fVec4[(IOTA0 - iSlow10) & 8191];
+            fRec19[0]           = fSlow3 * fRec19[1] + fSlow4 * fRec18[1];
+            fVec5[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec19[0];
+            fRec18[0]           = fVec5[(IOTA0 - iSlow11) & 8191];
+            fRec21[0]           = fSlow3 * fRec21[1] + fSlow4 * fRec20[1];
+            fVec6[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec21[0];
+            fRec20[0]           = fVec6[(IOTA0 - iSlow12) & 8191];
+            fRec23[0]           = fSlow3 * fRec23[1] + fSlow4 * fRec22[1];
+            fVec7[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec23[0];
+            fRec22[0]           = fVec7[(IOTA0 - iSlow13) & 8191];
+            float fTemp2        = fRec8[0] + fRec10[0] + fRec12[0] + fRec14[0] + fRec16[0]
+                           + fRec18[0] + fRec20[0] + fRec22[0] + 0.5f * fRec6[1];
+            fVec8[IOTA0 & 2047]  = fTemp2;
+            fRec6[0]             = fVec8[(IOTA0 - iSlow15) & 2047];
+            float fRec7          = 0.0f - 0.5f * fTemp2;
+            float fTemp3         = fRec6[1] + fRec7 + 0.5f * fRec4[1];
+            fVec9[IOTA0 & 2047]  = fTemp3;
+            fRec4[0]             = fVec9[(IOTA0 - iSlow16) & 2047];
+            float fRec5          = 0.0f - 0.5f * fTemp3;
+            float fTemp4         = fRec4[1] + fRec5 + 0.5f * fRec2[1];
+            fVec10[IOTA0 & 2047] = fTemp4;
+            fRec2[0]             = fVec10[(IOTA0 - iSlow17) & 2047];
+            float fRec3          = 0.0f - 0.5f * fTemp4;
+            float fTemp5         = fRec2[1] + fRec3 + 0.5f * fRec0[1];
+            fVec11[IOTA0 & 2047] = fTemp5;
+            fRec0[0]             = fVec11[(IOTA0 - iSlow18) & 2047];
+            float fRec1          = 0.0f - 0.5f * fTemp5;
+            fRec33[0]            = fSlow3 * fRec33[1] + fSlow4 * fRec32[1];
+            fVec12[IOTA0 & 8191] = fSlow2 * fRec33[0] + fTemp1;
+            fRec32[0]            = fVec12[(IOTA0 - iConst3) & 8191];
+            fRec35[0]            = fSlow3 * fRec35[1] + fSlow4 * fRec34[1];
+            fVec13[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec35[0];
+            fRec34[0]            = fVec13[(IOTA0 - iConst5) & 8191];
+            fRec37[0]            = fSlow3 * fRec37[1] + fSlow4 * fRec36[1];
+            fVec14[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec37[0];
+            fRec36[0]            = fVec14[(IOTA0 - iConst6) & 8191];
+            fRec39[0]            = fSlow3 * fRec39[1] + fSlow4 * fRec38[1];
+            fVec15[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec39[0];
+            fRec38[0]            = fVec15[(IOTA0 - iConst7) & 8191];
+            fRec41[0]            = fSlow3 * fRec41[1] + fSlow4 * fRec40[1];
+            fVec16[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec41[0];
+            fRec40[0]            = fVec16[(IOTA0 - iConst8) & 8191];
+            fRec43[0]            = fSlow3 * fRec43[1] + fSlow4 * fRec42[1];
+            fVec17[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec43[0];
+            fRec42[0]            = fVec17[(IOTA0 - iConst9) & 8191];
+            fRec45[0]            = fSlow3 * fRec45[1] + fSlow4 * fRec44[1];
+            fVec18[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec45[0];
+            fRec44[0]            = fVec18[(IOTA0 - iConst10) & 8191];
+            fRec47[0]            = fSlow3 * fRec47[1] + fSlow4 * fRec46[1];
+            fVec19[IOTA0 & 8191] = fTemp1 + fSlow2 * fRec47[0];
+            fRec46[0]            = fVec19[(IOTA0 - iConst11) & 8191];
+            float fTemp6 = fRec32[0] + fRec34[0] + fRec36[0] + fRec38[0] + fRec40[0]
+                           + fRec42[0] + fRec44[0] + fRec46[0] + 0.5f * fRec30[1];
+            fVec20[IOTA0 & 2047] = fTemp6;
+            fRec30[0]            = fVec20[(IOTA0 - iConst16) & 2047];
+            float fRec31         = 0.0f - 0.5f * fTemp6;
+            float fTemp7         = fRec30[1] + fRec31 + 0.5f * fRec28[1];
+            fVec21[IOTA0 & 2047] = fTemp7;
+            fRec28[0]            = fVec21[(IOTA0 - iConst17) & 2047];
+            float fRec29         = 0.0f - 0.5f * fTemp7;
+            float fTemp8         = fRec28[1] + fRec29 + 0.5f * fRec26[1];
+            fVec22[IOTA0 & 2047] = fTemp8;
+            fRec26[0]            = fVec22[(IOTA0 - iConst18) & 2047];
+            float fRec27         = 0.0f - 0.5f * fTemp8;
+            float fTemp9         = fRec26[1] + fRec27 + 0.5f * fRec24[1];
+            fVec23[IOTA0 & 1023] = fTemp9;
+            fRec24[0]            = fVec23[(IOTA0 - iConst19) & 1023];
+            float fRec25         = 0.0f - 0.5f * fTemp9;
+            output0[i0] =
+                FAUSTFLOAT(fRec0[1] + fRec24[1] + fRec25 + fRec1 + fSlow19 * fTemp0);
+            fRec9[1]  = fRec9[0];
+            IOTA0     = IOTA0 + 1;
+            fRec8[1]  = fRec8[0];
+            fRec11[1] = fRec11[0];
+            fRec10[1] = fRec10[0];
+            fRec13[1] = fRec13[0];
+            fRec12[1] = fRec12[0];
+            fRec15[1] = fRec15[0];
+            fRec14[1] = fRec14[0];
+            fRec17[1] = fRec17[0];
+            fRec16[1] = fRec16[0];
+            fRec19[1] = fRec19[0];
+            fRec18[1] = fRec18[0];
+            fRec21[1] = fRec21[0];
+            fRec20[1] = fRec20[0];
+            fRec23[1] = fRec23[0];
+            fRec22[1] = fRec22[0];
+            fRec6[1]  = fRec6[0];
+            fRec4[1]  = fRec4[0];
+            fRec2[1]  = fRec2[0];
+            fRec0[1]  = fRec0[0];
+            fRec33[1] = fRec33[0];
+            fRec32[1] = fRec32[0];
+            fRec35[1] = fRec35[0];
+            fRec34[1] = fRec34[0];
+            fRec37[1] = fRec37[0];
+            fRec36[1] = fRec36[0];
+            fRec39[1] = fRec39[0];
+            fRec38[1] = fRec38[0];
+            fRec41[1] = fRec41[0];
+            fRec40[1] = fRec40[0];
+            fRec43[1] = fRec43[0];
+            fRec42[1] = fRec42[0];
+            fRec45[1] = fRec45[0];
+            fRec44[1] = fRec44[0];
+            fRec47[1] = fRec47[0];
+            fRec46[1] = fRec46[0];
+            fRec30[1] = fRec30[0];
+            fRec28[1] = fRec28[0];
+            fRec26[1] = fRec26[0];
+            fRec24[1] = fRec24[0];
+        }
+    }
+};
+
+#endif
diff --git a/src/gui/AppIcon.qml b/src/gui/AppIcon.qml
new file mode 100644 (file)
index 0000000..32eb8aa
--- /dev/null
@@ -0,0 +1,29 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    id: appIcon
+
+    property alias icon: btn.icon
+    property string color: ""
+    property string defaultColor: virtualstudio.darkMode ? "#CCCCCC" : "#333333"
+    signal clicked()
+
+    Button {
+        id: btn
+        anchors.fill: parent
+        anchors.centerIn: parent
+        topInset: 0
+        leftInset: 0
+        rightInset: 0
+        bottomInset: 0
+        padding: 0
+
+        background: Rectangle { color: "transparent" }
+        icon.color: color ? color : defaultColor
+        icon.width: parent.width
+        icon.height: parent.height
+        display: AbstractButton.IconOnly
+        onClicked: appIcon.clicked()
+    }
+}
diff --git a/src/gui/AudioInterfaceMode.h b/src/gui/AudioInterfaceMode.h
new file mode 100644 (file)
index 0000000..3416b87
--- /dev/null
@@ -0,0 +1,88 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file AudioInterfaceMode.h
+ * \author Matt Horton
+ * \date December 2022
+ */
+
+#ifndef AUDIOINTERFACEMODE_H
+#define AUDIOINTERFACEMODE_H
+
+enum class AudioInterfaceMode {
+    JACK,     ///< Jack Mode
+    RTAUDIO,  ///< RtAudio Mode
+    ALL,
+    NONE
+};
+
+#ifdef RT_AUDIO
+#ifndef NO_JACK
+constexpr AudioInterfaceMode mode = AudioInterfaceMode::ALL;
+#else
+constexpr AudioInterfaceMode mode = AudioInterfaceMode::RTAUDIO;
+#endif
+#else
+#ifndef NO_JACK
+constexpr AudioInterfaceMode mode = AudioInterfaceMode::JACK;
+#else
+constexpr AudioInterfaceMode mode = AudioInterfaceMode::NONE;
+#endif
+#endif
+
+template<AudioInterfaceMode backend>
+constexpr auto isBackendAvailable()
+{
+    if constexpr (backend == AudioInterfaceMode::RTAUDIO) {
+        if (mode == AudioInterfaceMode::RTAUDIO || mode == AudioInterfaceMode::ALL) {
+            return true;
+        } else {
+            return false;
+        }
+    } else if constexpr (backend == AudioInterfaceMode::JACK) {
+        if (mode == AudioInterfaceMode::JACK || mode == AudioInterfaceMode::ALL) {
+            return true;
+        } else {
+            return false;
+        }
+    } else if constexpr (backend == AudioInterfaceMode::ALL) {
+        if (mode == AudioInterfaceMode::ALL) {
+            return true;
+        } else {
+            return false;
+        }
+    } else {
+        return false;
+    }
+}
+
+#endif  // AUDIOINTERFACEMODE_H
diff --git a/src/gui/AudioSettings.qml b/src/gui/AudioSettings.qml
new file mode 100644 (file)
index 0000000..0b56065
--- /dev/null
@@ -0,0 +1,803 @@
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+    width: parent.width
+    height: parent.height
+    color: backgroundColour
+
+    property bool connected: false
+    property bool showMeters: true
+    property bool showTestAudio: true
+
+    property int fontBig: 20
+    property int fontMedium: 13
+    property int fontSmall: 11
+    property int fontExtraSmall: 8
+
+    property int leftMargin: 48
+    property int rightMargin: 16
+    property int bottomToolTipMargin: 8
+    property int rightToolTipMargin: 4
+    property int buttonWidth: 103
+    property int buttonHeight: 25
+
+    property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+    property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+    property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
+    property string toolTipTextColour: textColour
+
+    property string errorFlagColour: "#DB0A0A"
+    property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+
+    function getCurrentInputDeviceIndex () {
+        if (audio.inputDevice === "") {
+            return audio.inputComboModel.findIndex(elem => elem.type === "element");
+        }
+
+        let idx = audio.inputComboModel.findIndex(elem => elem.type === "element" && elem.text === audio.inputDevice);
+        if (idx < 0) {
+            idx = audio.inputComboModel.findIndex(elem => elem.type === "element");
+        }
+        return idx;
+    }
+
+    function getCurrentOutputDeviceIndex() {
+        if (audio.outputDevice === "") {
+            return audio.outputComboModel.findIndex(elem => elem.type === "element");
+        }
+
+        let idx = audio.outputComboModel.findIndex(elem => elem.type === "element" && elem.text === audio.outputDevice);
+        if (idx < 0) {
+            idx = audio.outputComboModel.findIndex(elem => elem.type === "element");
+        }
+        return idx;
+    }
+
+    function getCurrentInputChannelsIndex() {
+        let idx = audio.inputChannelsComboModel.findIndex(elem => elem.baseChannel === audio.baseInputChannel
+            && elem.numChannels === audio.numInputChannels);
+        if (idx < 0) {
+            idx = 0;
+        }
+        return idx;
+    }
+
+    function getCurrentOutputChannelsIndex() {
+        let idx = audio.outputChannelsComboModel.findIndex(elem => elem.baseChannel === audio.baseOutputChannel
+            && elem.numChannels === audio.numOutputChannels);
+        if (idx < 0) {
+            idx = 0;
+        }
+        return idx;
+    }
+
+    function getCurrentMixModeIndex() {
+        let idx = audio.inputMixModeComboModel.findIndex(elem => elem.value === audio.inputMixMode);
+        if (idx < 0) {
+            idx = 0;
+        }
+        return idx;
+    }
+
+    Loader {
+        anchors.fill: parent
+        sourceComponent: audio.audioBackend == "JACK" ? usingJACK : ((!audio.deviceModelsInitialized || audio.scanningDevices) ? scanningDevices : usingRtAudio);
+    }
+
+    Component {
+        id: usingRtAudio
+
+        Item {
+            anchors.top: parent.top
+            anchors.topMargin: 24 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            anchors.left: parent.left
+            anchors.leftMargin: 24 * virtualstudio.uiScale
+            anchors.right: parent.right
+
+            Rectangle {
+                id: leftSpacer
+                x: 0; y: 0
+                width: 144 * virtualstudio.uiScale
+                height: 0
+                color: "transparent"
+            }
+
+            Text {
+                id: outputLabel
+                x: 0; y: 0
+                text: "Output Device"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                bottomPadding: 10 * virtualstudio.uiScale
+                color: textColour
+            }
+
+            InfoTooltip {
+                id: outputHelpIcon
+                anchors.left: outputLabel.right
+                anchors.bottom: outputLabel.top
+                anchors.bottomMargin: -8 * virtualstudio.uiScale
+                size: 16 * virtualstudio.uiScale
+                content: qsTr("How you'll hear the studio audio")
+            }
+
+            AppIcon {
+                id: headphonesIcon
+                anchors.left: outputLabel.left
+                anchors.top: outputLabel.bottom
+                width: 28 * virtualstudio.uiScale
+                height: 28 * virtualstudio.uiScale
+                icon.source: "headphones.svg"
+            }
+
+            ComboBox {
+                id: outputCombo
+                anchors.left: leftSpacer.right
+                anchors.verticalCenter: outputLabel.verticalCenter
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                width: parent.width - leftSpacer.width - rightMargin * virtualstudio.uiScale
+                model: audio.outputComboModel
+                currentIndex: getCurrentOutputDeviceIndex()
+                delegate: ItemDelegate {
+                    required property var modelData
+                    required property int index
+
+                    leftPadding: 0
+
+                    width: parent.width
+                    contentItem: Text {
+                        leftPadding: modelData.type === "element" && outputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12
+                        text: modelData.text
+                        font.bold: modelData.type === "header"
+                    }
+                    highlighted: outputCombo.highlightedIndex === index
+                    MouseArea {
+                        anchors.fill: parent
+                        onClicked: {
+                            if (modelData.type == "element") {
+                                outputCombo.currentIndex = index
+                                outputCombo.popup.close()
+                                audio.outputDevice = modelData.text
+                                if (modelData.category.startsWith("Low-Latency")) {
+                                    let inputComboIdx = inputCombo.model.findIndex(it => it.category.startsWith("Low-Latency") && it.text === modelData.text);
+                                    if (inputComboIdx !== null && inputComboIdx !== undefined) {
+                                        inputCombo.currentIndex = inputComboIdx;
+                                        audio.inputDevice = modelData.text
+                                    }
+                                }
+                                if (connected) {
+                                    virtualstudio.triggerReconnect(false);
+                                } else {
+                                    audio.validateDevices()
+                                    audio.restartAudio()
+                                }
+                            }
+                        }
+                    }
+                }
+                contentItem: Text {
+                    leftPadding: 12
+                    font: outputCombo.font
+                    horizontalAlignment: Text.AlignHLeft
+                    verticalAlignment: Text.AlignVCenter
+                    elide: Text.ElideRight
+                    text: outputCombo.model[outputCombo.currentIndex]!=undefined && outputCombo.model[outputCombo.currentIndex].text ? outputCombo.model[outputCombo.currentIndex].text : ""
+                }
+            }
+
+            Meter {
+                id: outputDeviceMeters
+                anchors.left: outputCombo.left
+                anchors.right: outputCombo.right
+                anchors.top: outputCombo.bottom
+                anchors.topMargin: 16 * virtualstudio.uiScale
+                height: 24 * virtualstudio.uiScale
+                model: showMeters ? audio.outputMeterLevels : [0, 0]
+                clipped: audio.outputClipped
+                visible: showMeters
+                enabled: audio.audioReady && !Boolean(audio.devicesError)
+            }
+
+            VolumeSlider {
+                id: outputSlider
+                anchors.left: outputCombo.left
+                anchors.right: parent.right
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                anchors.top: outputDeviceMeters.bottom
+                anchors.topMargin: 16 * virtualstudio.uiScale
+                height: 30 * virtualstudio.uiScale
+                labelText: "Studio"
+                tooltipText: "How loudly you hear other participants"
+                showLabel: false
+                sliderEnabled: true
+                visible: showMeters
+            }
+
+            Text {
+                id: outputChannelsLabel
+                anchors.left: outputCombo.left
+                anchors.right: outputCombo.horizontalCenter
+                anchors.top: showMeters ? outputSlider.bottom : outputCombo.bottom
+                anchors.topMargin: 12 * virtualstudio.uiScale
+                textFormat: Text.RichText
+                text: "Output Channel(s)"
+                font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: textColour
+            }
+
+            ComboBox {
+                id: outputChannelsCombo
+                anchors.left: outputCombo.left
+                anchors.right: outputCombo.horizontalCenter
+                anchors.rightMargin: 8 * virtualstudio.uiScale
+                anchors.top: outputChannelsLabel.bottom
+                anchors.topMargin: 4 * virtualstudio.uiScale
+                model: audio.outputChannelsComboModel
+                enabled: audio.outputChannelsComboModel.length > 1
+                currentIndex: getCurrentOutputChannelsIndex()
+                delegate: ItemDelegate {
+                    required property var modelData
+                    required property int index
+                    width: parent.width
+                    contentItem: Text {
+                        text: modelData.label
+                    }
+                    highlighted: outputChannelsCombo.highlightedIndex === index
+                    MouseArea {
+                        anchors.fill: parent
+                        onClicked: {
+                            outputChannelsCombo.currentIndex = index
+                            outputChannelsCombo.popup.close()
+                            audio.baseOutputChannel = modelData.baseChannel
+                            audio.numOutputChannels = modelData.numChannels
+                            if (connected) {
+                                virtualstudio.triggerReconnect(false);
+                            } else {
+                                audio.validateDevices()
+                                audio.restartAudio()
+                            }
+                        }
+                    }
+                }
+                contentItem: Text {
+                    leftPadding: 12
+                    font: inputCombo.font
+                    horizontalAlignment: Text.AlignHLeft
+                    verticalAlignment: Text.AlignVCenter
+                    elide: Text.ElideRight
+                    text: outputChannelsCombo.model[outputChannelsCombo.currentIndex].label || ""
+                }
+            }
+
+            Button {
+                id: testOutputAudioButton
+                visible: showTestAudio
+                background: Rectangle {
+                    radius: 6 * virtualstudio.uiScale
+                    color: testOutputAudioButton.down ? buttonPressedColour : (testOutputAudioButton.hovered ? buttonHoverColour : buttonColour)
+                    border.width: 1
+                    border.color: testOutputAudioButton.down || testOutputAudioButton.hovered ? buttonPressedStroke : (testOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke)
+                }
+                onClicked: { audio.playOutputAudio() }
+                anchors.right: parent.right
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                anchors.verticalCenter: outputChannelsCombo.verticalCenter
+                width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                Text {
+                    text: "Play Test Tone"
+                    font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                    anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                    color: textColour
+                }
+            }
+
+            Rectangle {
+                id: divider1
+                anchors.top: showTestAudio ? testOutputAudioButton.bottom : outputChannelsCombo.bottom
+                anchors.topMargin: 24 * virtualstudio.uiScale
+                width: parent.width - x - (16 * virtualstudio.uiScale); height: 2 * virtualstudio.uiScale
+                color: "#E0E0E0"
+            }
+
+            Text {
+                id: inputLabel
+                anchors.left: outputLabel.left
+                anchors.top: divider1.bottom
+                anchors.topMargin: 24 * virtualstudio.uiScale
+                text: "Input Device"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                bottomPadding: 10 * virtualstudio.uiScale
+                color: textColour
+            }
+
+            InfoTooltip {
+                id: inputHelpIcon
+                anchors.left: inputLabel.right
+                anchors.bottom: inputLabel.top
+                anchors.bottomMargin: -8 * virtualstudio.uiScale
+                size: 16 * virtualstudio.uiScale
+                content: qsTr("Audio sent to the studio (microphone, instrument, mixer, etc.)")
+            }
+
+            AppIcon {
+                id: microphoneIcon
+                anchors.left: inputLabel.left
+                anchors.top: inputLabel.bottom
+                width: 32 * virtualstudio.uiScale
+                height: 32 * virtualstudio.uiScale
+                icon.source: "mic.svg"
+            }
+
+            ComboBox {
+                id: inputCombo
+                model: audio.inputComboModel
+                currentIndex: getCurrentInputDeviceIndex()
+                anchors.left: outputCombo.left
+                anchors.right: outputCombo.right
+                anchors.verticalCenter: inputLabel.verticalCenter
+                delegate: ItemDelegate {
+                    required property var modelData
+                    required property int index
+
+                    leftPadding: 0
+
+                    width: parent.width
+                    contentItem: Text {
+                        leftPadding: modelData.type === "element" && inputCombo.model.filter(it => it.type === "header").length > 0 ? 24 : 12
+                        text: modelData.text
+                        font.bold: modelData.type === "header"
+                    }
+                    highlighted: inputCombo.highlightedIndex === index
+                    MouseArea {
+                        anchors.fill: parent
+                        onClicked: {
+                            if (modelData.type == "element") {
+                                inputCombo.currentIndex = index
+                                inputCombo.popup.close()
+                                audio.inputDevice = modelData.text
+                                if (modelData.category.startsWith("Low-Latency")) {
+                                    let outputComboIdx = outputCombo.model.findIndex(it => it.category.startsWith("Low-Latency") && it.text === modelData.text);
+                                    if (outputComboIdx !== null && outputComboIdx !== undefined) {
+                                        outputCombo.currentIndex = outputComboIdx;
+                                        audio.outputDevice = modelData.text
+                                    }
+                                }
+                                if (connected) {
+                                    virtualstudio.triggerReconnect(false);
+                                } else {
+                                    audio.validateDevices()
+                                    audio.restartAudio()
+                                }
+                            }
+                        }
+                    }
+                }
+                contentItem: Text {
+                    leftPadding: 12
+                    font: inputCombo.font
+                    horizontalAlignment: Text.AlignHLeft
+                    verticalAlignment: Text.AlignVCenter
+                    elide: Text.ElideRight
+                    text: inputCombo.model[inputCombo.currentIndex] != undefined && inputCombo.model[inputCombo.currentIndex].text ? inputCombo.model[inputCombo.currentIndex].text : ""
+                }
+            }
+
+            Meter {
+                id: inputDeviceMeters
+                anchors.left: inputCombo.left
+                anchors.right: inputCombo.right
+                anchors.top: inputCombo.bottom
+                anchors.topMargin: 16 * virtualstudio.uiScale
+                height: 24 * virtualstudio.uiScale
+                model: showMeters ? audio.inputMeterLevels : [0, 0]
+                clipped: audio.inputClipped
+                visible: showMeters
+                enabled: audio.audioReady && !Boolean(audio.devicesError)
+            }
+
+            VolumeSlider {
+                id: inputSlider
+                anchors.left: inputCombo.left
+                anchors.right: parent.right
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                anchors.top: inputDeviceMeters.bottom
+                anchors.topMargin: 16 * virtualstudio.uiScale
+                height: 30 * virtualstudio.uiScale
+                labelText: "Send"
+                tooltipText: "How loudly other participants hear you"
+                showLabel: false
+                sliderEnabled: true
+                visible: showMeters
+            }
+
+            Button {
+                id: hiddenInputButton
+                anchors.right: parent.right
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                anchors.verticalCenter: inputSlider.verticalCenter
+                width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                visible: false
+            }
+
+            Text {
+                id: inputChannelsLabel
+                anchors.left: inputCombo.left
+                anchors.right: inputCombo.horizontalCenter
+                anchors.top: showMeters ? inputSlider.bottom : inputCombo.bottom
+                anchors.topMargin: 12 * virtualstudio.uiScale
+                textFormat: Text.RichText
+                text: "Input Channel(s)"
+                font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: textColour
+            }
+
+            ComboBox {
+                id: inputChannelsCombo
+                anchors.left: inputCombo.left
+                anchors.right: inputCombo.horizontalCenter
+                anchors.rightMargin: 8 * virtualstudio.uiScale
+                anchors.top: inputChannelsLabel.bottom
+                anchors.topMargin: 4 * virtualstudio.uiScale
+                model: audio.inputChannelsComboModel
+                enabled: audio.inputChannelsComboModel.length > 1
+                currentIndex: getCurrentInputChannelsIndex()
+                delegate: ItemDelegate {
+                    required property var modelData
+                    required property int index
+                    width: parent.width
+                    contentItem: Text {
+                        text: modelData.label
+                    }
+                    highlighted: inputChannelsCombo.highlightedIndex === index
+                    MouseArea {
+                        anchors.fill: parent
+                        onClicked: {
+                            inputChannelsCombo.currentIndex = index
+                            inputChannelsCombo.popup.close()
+                            audio.baseInputChannel = modelData.baseChannel
+                            audio.numInputChannels = modelData.numChannels
+                            if (connected) {
+                                virtualstudio.triggerReconnect(false);
+                            } else {
+                                audio.validateDevices()
+                                audio.restartAudio()
+                            }
+                        }
+                    }
+                }
+                contentItem: Text {
+                    leftPadding: 12
+                    font: inputCombo.font
+                    horizontalAlignment: Text.AlignHLeft
+                    verticalAlignment: Text.AlignVCenter
+                    elide: Text.ElideRight
+                    text: inputChannelsCombo.model[inputChannelsCombo.currentIndex].label || ""
+                }
+            }
+
+            Text {
+                id: inputMixModeLabel
+                anchors.left: inputCombo.horizontalCenter
+                anchors.right: inputCombo.right
+                anchors.rightMargin: 8 * virtualstudio.uiScale
+                anchors.top: showMeters ? inputSlider.bottom : inputCombo.bottom
+                anchors.topMargin: 12 * virtualstudio.uiScale
+                textFormat: Text.RichText
+                text: "Mono / Stereo"
+                font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: textColour
+            }
+
+            ComboBox {
+                id: inputMixModeCombo
+                anchors.left: inputCombo.horizontalCenter
+                anchors.right: inputCombo.right
+                anchors.rightMargin: 8 * virtualstudio.uiScale
+                anchors.top: inputMixModeLabel.bottom
+                anchors.topMargin: 4 * virtualstudio.uiScale
+                model: audio.inputMixModeComboModel
+                enabled: audio.inputMixModeComboModel.length > 1
+                currentIndex: getCurrentMixModeIndex()
+                delegate: ItemDelegate {
+                    required property var modelData
+                    required property int index
+                    width: parent.width
+                    contentItem: Text {
+                        text: modelData.label
+                    }
+                    highlighted: inputMixModeCombo.highlightedIndex === index
+                    MouseArea {
+                        anchors.fill: parent
+                        onClicked: {
+                            inputMixModeCombo.currentIndex = index
+                            inputMixModeCombo.popup.close()
+                            audio.inputMixMode = audio.inputMixModeComboModel[index].value
+                            if (connected) {
+                                virtualstudio.triggerReconnect(false);
+                            } else {
+                                audio.validateDevices()
+                                audio.restartAudio()
+                            }
+                        }
+                    }
+                }
+                contentItem: Text {
+                    leftPadding: 12
+                    font: inputCombo.font
+                    horizontalAlignment: Text.AlignHLeft
+                    verticalAlignment: Text.AlignVCenter
+                    elide: Text.ElideRight
+                    text: inputMixModeCombo.model[inputMixModeCombo.currentIndex].label || ""
+                }
+            }
+
+            Text {
+                id: inputChannelHelpMessage
+                anchors.left: inputChannelsCombo.left
+                anchors.leftMargin: 2 * virtualstudio.uiScale
+                anchors.right: inputChannelsCombo.right
+                anchors.top: inputChannelsCombo.bottom
+                anchors.topMargin: 8 * virtualstudio.uiScale
+                textFormat: Text.RichText
+                wrapMode: Text.WordWrap
+                text: audio.inputChannelsComboModel.length > 1 ? "Choose up to 2 channels" : "Only 1 channel available"
+                font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: textColour
+            }
+
+            Text {
+                id: inputMixModeHelpMessage
+                anchors.left: inputMixModeCombo.left
+                anchors.leftMargin: 2 * virtualstudio.uiScale
+                anchors.right: inputMixModeCombo.right
+                anchors.top: inputMixModeCombo.bottom
+                anchors.topMargin: 8 * virtualstudio.uiScale
+                textFormat: Text.RichText
+                wrapMode: Text.WordWrap
+                text: (() => {
+                    if (audio.inputMixMode === 2) {
+                        return "Treat the channels as Left and Right signals, coming through each speaker separately.";
+                    } else if (audio.inputMixMode === 3) {
+                        return "Combine the channels into one central channel coming through both speakers.";
+                    } else if (audio.inputMixMode === 1) {
+                        return "Send a single channel of audio";
+                    } else {
+                        return "";
+                    }
+                })()
+                font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: textColour
+            }
+
+            Connections {
+                target: audio
+                // anything that sets currentIndex to the value of a function needs
+                // to be manually updated whenever there is a change to any vars it uses
+                function onInputDeviceChanged() {
+                    inputCombo.currentIndex = getCurrentInputDeviceIndex();
+                }
+                function onOutputDeviceChanged() {
+                    outputCombo.currentIndex = getCurrentOutputDeviceIndex();
+                }
+                function onNumInputChannelsChanged() {
+                    inputChannelsCombo.currentIndex = getCurrentInputChannelsIndex();
+                }
+                function onBaseInputChannelChanged() {
+                    inputChannelsCombo.currentIndex = getCurrentInputChannelsIndex();
+                }
+                function onNumOutputChannelsChanged() {
+                    outputChannelsCombo.currentIndex = getCurrentOutputChannelsIndex();
+                }
+                function onBaseOutputChannelChanged() {
+                    outputChannelsCombo.currentIndex = getCurrentOutputChannelsIndex();
+                }
+                function onInputMixModeChanged() {
+                    inputMixModeCombo.currentIndex = getCurrentMixModeIndex();
+                }
+            }
+        }
+    }
+
+    Component {
+        id: usingJACK
+
+        Item {
+            anchors.top: parent.top
+            anchors.topMargin: 24 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            anchors.left: parent.left
+            anchors.leftMargin: leftMargin * virtualstudio.uiScale
+            anchors.right: parent.right
+
+            Text {
+                id: jackLabel
+                x: 0; y: 0
+                width: parent.width - rightMargin * virtualstudio.uiScale
+                text: "Using JACK for audio input and output. Use QjackCtl to adjust your sample rate, buffer, and device settings."
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                wrapMode: Text.WordWrap
+                color: textColour
+            }
+
+            Text {
+                id: jackOutputLabel
+                anchors.left: jackLabel.left
+                anchors.top: jackLabel.bottom
+                anchors.topMargin: 48 * virtualstudio.uiScale
+                width: 144 * virtualstudio.uiScale
+                text: "Output Volume"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                wrapMode: Text.WordWrap
+                bottomPadding: 10 * virtualstudio.uiScale
+                color: textColour
+            }
+
+            AppIcon {
+                id: jackHeadphonesIcon
+                anchors.left: jackOutputLabel.left
+                anchors.top: jackOutputLabel.bottom
+                width: 28 * virtualstudio.uiScale
+                height: 28 * virtualstudio.uiScale
+                icon.source: "headphones.svg"
+            }
+
+            Meter {
+                id: jackOutputMeters
+                anchors.left: jackOutputLabel.right
+                anchors.right: parent.right
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                anchors.verticalCenter: jackOutputLabel.verticalCenter
+                height: 24 * virtualstudio.uiScale
+                model: showMeters ? audio.outputMeterLevels : [0, 0]
+                clipped: audio.outputClipped
+                enabled: audio.audioReady && !Boolean(audio.devicesError)
+            }
+
+            Button {
+                id: jackTestOutputAudioButton
+                background: Rectangle {
+                    radius: 6 * virtualstudio.uiScale
+                    color: jackTestOutputAudioButton.down ? buttonPressedColour : (jackTestOutputAudioButton.hovered ? buttonHoverColour : buttonColour)
+                    border.width: 1
+                    border.color: jackTestOutputAudioButton.down ? buttonPressedStroke : (jackTestOutputAudioButton.hovered ? buttonHoverStroke : buttonStroke)
+                }
+                onClicked: { audio.playOutputAudio() }
+                anchors.right: parent.right
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                anchors.verticalCenter: jackOutputVolumeSlider.verticalCenter
+                width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                Text {
+                    text: "Play Test Tone"
+                    font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                    anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                    color: textColour
+                }
+            }
+
+            VolumeSlider {
+                id: jackOutputVolumeSlider
+                anchors.left: jackOutputMeters.left
+                anchors.right: jackTestOutputAudioButton.left
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                anchors.top: jackOutputMeters.bottom
+                anchors.topMargin: 16 * virtualstudio.uiScale
+                height: 30 * virtualstudio.uiScale
+                labelText: "Studio"
+                tooltipText: "How loudly you hear other participants"
+                showLabel: false
+                sliderEnabled: true
+            }
+
+            Text {
+                id: jackInputLabel
+                anchors.left: jackLabel.left
+                anchors.top: jackOutputVolumeSlider.bottom
+                anchors.topMargin: 48 * virtualstudio.uiScale
+                width: 144 * virtualstudio.uiScale
+                text: "Input Volume"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                wrapMode: Text.WordWrap
+                bottomPadding: 10 * virtualstudio.uiScale
+                color: textColour
+            }
+
+            AppIcon {
+                id: jackMicrophoneIcon
+                anchors.left: jackInputLabel.left
+                anchors.top: jackInputLabel.bottom
+                width: 32 * virtualstudio.uiScale
+                height: 32 * virtualstudio.uiScale
+                icon.source: "mic.svg"
+            }
+
+            Meter {
+                id: jackInputMeters
+                anchors.left: jackInputLabel.right
+                anchors.right: parent.right
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                anchors.verticalCenter: jackInputLabel.verticalCenter
+                height: 24 * virtualstudio.uiScale
+                model: showMeters ? audio.inputMeterLevels : [0, 0]
+                clipped: audio.inputClipped
+                enabled: audio.audioReady && !Boolean(audio.devicesError)
+            }
+
+            VolumeSlider {
+                id: jackInputVolumeSlider
+                anchors.left: jackInputMeters.left
+                anchors.right: parent.right
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                anchors.top: jackInputMeters.bottom
+                anchors.topMargin: 16 * virtualstudio.uiScale
+                height: 30 * virtualstudio.uiScale
+                labelText: "Send"
+                tooltipText: "How loudly other participants hear you"
+                showLabel: false
+                sliderEnabled: true
+            }
+
+            Button {
+                id: jackHiddenInputButton
+                anchors.right: parent.right
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                anchors.verticalCenter: jackInputVolumeSlider.verticalCenter
+                width: 144 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                visible: false
+            }
+        }
+    }
+
+    Component {
+        id: noBackend
+
+        Item {
+            anchors.top: parent.top
+            anchors.topMargin: 24 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            anchors.left: parent.left
+            anchors.leftMargin: leftMargin * virtualstudio.uiScale
+            anchors.right: parent.right
+
+            Text {
+                id: noBackendLabel
+                x: 0; y: 0
+                width: parent.width - (16 * virtualstudio.uiScale)
+                text: "JackTrip has been compiled without an audio backend. Please rebuild with the rtaudio flag or without the nojack flag."
+                font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+                wrapMode: Text.WordWrap
+                color: textColour
+            }
+        }
+    }
+
+    Component {
+        id: scanningDevices
+
+        Item {
+            anchors.top: parent.top
+            anchors.topMargin: 24 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            anchors.left: parent.left
+            anchors.leftMargin: leftMargin * virtualstudio.uiScale
+            anchors.right: parent.right
+
+            Text {
+                id: scanningDevicesLabel
+                x: 0; y: 0
+                width: parent.width - (16 * virtualstudio.uiScale)
+                text: "Scanning audio devices..."
+                font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+                wrapMode: Text.WordWrap
+                color: textColour
+            }
+        }
+    }
+}
diff --git a/src/gui/Browse.qml b/src/gui/Browse.qml
new file mode 100644 (file)
index 0000000..d632152
--- /dev/null
@@ -0,0 +1,328 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    Rectangle {
+        width: parent.width; height: parent.height
+        color: backgroundColour
+    }
+
+    property bool refreshing: false
+
+    property int buttonHeight: 25
+    property int buttonWidth: 103
+    property int extraSettingsButtonWidth: 16
+    property int emptyListMessageWidth: 450
+    property int createMessageTopMargin: 16
+    property int createButtonTopMargin: 24
+    property int fontBig: 28
+    property int fontMedium: 11
+
+    property int scrollY: 0
+
+    property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+    property string createButtonStroke: virtualstudio.darkMode ? "#AB0F0F" : "#0F0D0D"
+
+    function refresh() {
+        scrollY = studioListView.contentY;
+        var currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY);
+        if (currentIndex == -1) {
+            currentIndex = studioListView.indexAt(16 * virtualstudio.uiScale, studioListView.contentY + (16 * virtualstudio.uiScale));
+        }
+        virtualstudio.refreshStudios(currentIndex, true)
+    }
+
+    Rectangle {
+        z: 1
+        width: parent.width; height: parent.height
+        color: "#40000000"
+        visible: refreshing
+        MouseArea {
+            anchors.fill: parent
+            propagateComposedEvents: false
+            hoverEnabled: true
+            preventStealing: true
+        }
+    }
+
+    Component {
+        id: footer
+        Rectangle {
+            height: 16 * virtualstudio.uiScale
+            x: 16 * virtualstudio.uiScale
+            width: parent.width - (2 * x)
+            color: backgroundColour
+        }
+    }
+
+    ListView {
+        id: studioListView
+        x:0; y: 0; width: parent.width - (2 * x); height: parent.height - 36 * virtualstudio.uiScale
+        spacing: 16 * virtualstudio.uiScale
+        header: footer
+        footer: footer
+        model: virtualstudio.serverModel
+        clip: true
+        boundsBehavior: Flickable.StopAtBounds
+        delegate: Studio {
+            x: 16 * virtualstudio.uiScale
+            width: studioListView.width - (2 * x)
+            serverLocation: virtualstudio.regions[modelData.location] ? "in " + virtualstudio.regions[modelData.location].label : ""
+            flagImage: modelData.bannerURL ? modelData.bannerURL : modelData.flag
+            studioName: modelData.name
+            publicStudio: modelData.isPublic
+            admin: modelData.isAdmin
+            available: modelData.canConnect
+            connected: false
+            studioId: modelData.id ? modelData.id : ""
+            streamId: modelData.streamId ? modelData.streamId : ""
+            inviteKeyString: modelData.inviteKey ? modelData.inviteKey : ""
+            sampleRate: modelData.sampleRate
+        }
+
+        section { property: "modelData.type"; criteria: ViewSection.FullString; delegate: SectionHeading {} }
+
+        // Show sectionHeading if there are no Studios in list
+        SectionHeading {
+            id: emptyListSectionHeading
+            listIsEmpty: true
+            visible: parent.count == 0 && !virtualstudio.showCreateStudio
+        }
+
+        Text {
+            id: emptyListMessage
+            visible: parent.count == 0 && !virtualstudio.showCreateStudio
+            text: "No studios found that match your filter criteria."
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: emptyListMessageWidth
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: emptyListSectionHeading.horizontalCenter
+            anchors.verticalCenter: parent.verticalCenter
+        }
+
+        Button {
+            id: resetFiltersButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: resetFiltersButton.down ? buttonPressedColour : (resetFiltersButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: resetFiltersButton.down ? buttonPressedStroke : (resetFiltersButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            visible: parent.count == 0 && !virtualstudio.showCreateStudio
+            onClicked: {
+                virtualstudio.showSelfHosted = false;
+                virtualstudio.showInactive = true;
+                refreshing = true;
+                refresh();
+            }
+            anchors.top: emptyListMessage.bottom
+            anchors.topMargin: createButtonTopMargin
+            anchors.horizontalCenter: emptyListMessage.horizontalCenter
+            width: 120 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
+            Text {
+                text: "Reset Filters"
+                font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+
+        Rectangle {
+            id: newUserEmptyList
+            anchors.fill: parent
+            color: "transparent"
+            visible: parent.count == 0 && virtualstudio.showCreateStudio
+
+            Rectangle {
+                color: "transparent"
+                width: emptyListMessageWidth
+                height: createButton.height + createStudioMessage.height + welcomeMessage.height + createButtonTopMargin + createMessageTopMargin
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+
+                Text {
+                    id: welcomeMessage
+                    text: "Welcome"
+                    font { family: "Poppins"; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale; weight: Font.Bold }
+                    color: textColour
+                    width: emptyListMessageWidth
+                    wrapMode: Text.Wrap
+                    horizontalAlignment: Text.AlignHCenter
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.top: parent.top
+                }
+
+                Text {
+                    id: createStudioMessage
+                    text: "Looks like you're not a member of any studios!\nHave the studio owner send you an invite link, or create your own studio to invite others."
+                    font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                    width: emptyListMessageWidth
+                    wrapMode: Text.Wrap
+                    horizontalAlignment: Text.AlignHCenter
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.top: welcomeMessage.bottom
+                    anchors.topMargin: createMessageTopMargin
+                }
+
+                Button {
+                    id: createButton
+                    background: Rectangle {
+                        radius: 6 * virtualstudio.uiScale
+                        color: createButton.down ? "#E7E8E8" : "#F2F3F3"
+                        border.width: 1
+                        border.color: createButton.down ? "#B0B5B5" : createButtonStroke
+                        layer.enabled: createButton.hovered && !createButton.down
+                    }
+                    onClicked: { virtualstudio.createStudio(); }
+                    anchors.top: createStudioMessage.bottom
+                    anchors.topMargin: createButtonTopMargin
+                    anchors.horizontalCenter: createStudioMessage.horizontalCenter
+                    width: 210 * virtualstudio.uiScale; height: 45 * virtualstudio.uiScale
+                    Text {
+                        text: "Create a Studio"
+                        font.family: "Poppins"
+                        font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+                        font.weight: Font.Bold
+                        color: "#DB0A0A"
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        anchors.verticalCenter: parent.verticalCenter
+                    }
+                }
+            }
+        }
+
+        // Disable momentum scroll
+        MouseArea {
+            z: -1
+            anchors.fill: parent
+            onWheel: function (wheel) {
+                // trackpad
+                studioListView.contentY -= wheel.pixelDelta.y;
+                // mouse wheel
+                studioListView.contentY -= wheel.angleDelta.y;
+                studioListView.returnToBounds();
+            }
+        }
+
+        Component.onCompleted: {
+            // Customize scroll properties on different platforms
+            if (Qt.platform.os == "linux" || Qt.platform.os == "osx" ||
+                Qt.platform.os == "unix" || Qt.platform.os == "windows") {
+                var scrollBar = Qt.createQmlObject('import QtQuick.Controls; ScrollBar{}',
+                                                   studioListView,
+                                                   "dynamicSnippet1");
+                scrollBar.policy = ScrollBar.AlwaysOn;
+                ScrollBar.vertical = scrollBar;
+            }
+        }
+    }
+
+    Rectangle {
+        x: 0; y: parent.height - 36 * virtualstudio.uiScale; width: parent.width; height: 36 * virtualstudio.uiScale
+        border.color: "#33979797"
+        color: backgroundColour
+
+        Button {
+            id: refreshButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: { refreshing = true; refresh() }
+            anchors.verticalCenter: parent.verticalCenter
+            x: 16 * virtualstudio.uiScale
+            width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+            Text {
+                text: "Refresh List"
+                font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors {horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+
+        Button {
+            id: aboutButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: aboutButton.down ? buttonPressedColour : (aboutButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: aboutButton.down ? buttonPressedStroke : (aboutButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: { virtualstudio.showAbout() }
+            anchors.verticalCenter: parent.verticalCenter
+            x: parent.width - ((230 + extraSettingsButtonWidth) * virtualstudio.uiScale)
+            width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+            Text {
+                text: "About"
+                font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+
+        Button {
+            id: settingsButton
+            text: "Settings"
+            palette.buttonText: textColour
+            icon {
+                source: "cog.svg";
+                color: textColour;
+            }
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: settingsButton.down ? buttonPressedColour : (settingsButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: settingsButton.down ? buttonPressedStroke : (settingsButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: { virtualstudio.windowState = "settings"; audio.startAudio(); }
+            display: AbstractButton.TextBesideIcon
+            font {
+                family: "Poppins";
+                pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale;
+            }
+            leftPadding: 0
+            rightPadding: 4
+            spacing: 0
+            anchors.verticalCenter: parent.verticalCenter
+            x: parent.width - ((119 + extraSettingsButtonWidth) * virtualstudio.uiScale)
+            width: (buttonWidth + extraSettingsButtonWidth) * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+        }
+    }
+
+    FeedbackSurvey {
+    }
+
+    Connections {
+        target: virtualstudio
+        // Need to do this to avoid layout issues with our section header.
+        function onNewScale() {
+            studioListView.positionViewAtEnd();
+            studioListView.positionViewAtBeginning();
+            scrollY = studioListView.contentY;
+        }
+        function onRefreshFinished(index) {
+            refreshing = false;
+            if (index == -1) {
+                studioListView.contentY = scrollY
+            } else {
+                studioListView.positionViewAtIndex(index, ListView.Beginning);
+            }
+        }
+        function onPeriodicRefresh() { refresh() }
+    }
+}
diff --git a/src/gui/ChangeDevices.qml b/src/gui/ChangeDevices.qml
new file mode 100644 (file)
index 0000000..7937ec2
--- /dev/null
@@ -0,0 +1,115 @@
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+    width: parent.width; height: parent.height
+    color: backgroundColour
+    clip: true
+
+    property int fontBig: 28
+    property int fontMedium: 12
+    property int fontSmall: 10
+    property int fontTiny: 8
+
+    property int rightMargin: 16
+    property int bottomToolTipMargin: 8
+    property int rightToolTipMargin: 4
+
+    property string saveButtonText: "#DB0A0A"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0"
+    property real muteButtonLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0
+    property real muteButtonMutedLightnessValue: 0.24
+    property real muteButtonMutedSaturationValue: 0.73
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+    property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1"
+    property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
+    property string toolTipTextColour: textColour
+
+    property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+    property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+    property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+    property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+
+    property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+
+    MouseArea {
+        anchors.fill: parent
+        propagateComposedEvents: false
+    }
+
+    Rectangle {
+        id: audioSettingsView
+        width: parent.width;
+        height: parent.height;
+        color: backgroundColour
+        radius: 6 * virtualstudio.uiScale
+
+        DeviceRefreshButton {
+            id: refreshButton
+            anchors.top: parent.top;
+            anchors.topMargin: 16 * virtualstudio.uiScale;
+            anchors.right: parent.right;
+            anchors.rightMargin: 16 * virtualstudio.uiScale;
+            enabled: !audio.scanningDevices
+            onDeviceRefresh: function () {
+                virtualstudio.triggerReconnect(true);
+            }
+        }
+
+        Text {
+            text: "Restarting Audio"
+            anchors.verticalCenter: refreshButton.verticalCenter
+            anchors.right: refreshButton.left;
+            anchors.rightMargin: 16 * virtualstudio.uiScale;
+            font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            visible: audio.scanningDevices
+        }
+
+        AudioSettings {
+            id: audioSettings
+            showMeters: false
+            showTestAudio: false
+            connected: true
+            height: 300 * virtualstudio.uiScale
+            anchors.top: refreshButton.bottom;
+            anchors.topMargin: 16 * virtualstudio.uiScale;
+        }
+    }
+
+    Button {
+        id: backButton
+        background: Rectangle {
+            radius: 6 * virtualstudio.uiScale
+            color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour)
+        }
+        onClicked: {
+            virtualstudio.saveSettings();
+            virtualstudio.windowState = "connected";
+        }
+        anchors.bottom: parent.bottom
+        anchors.bottomMargin: 16 * virtualstudio.uiScale;
+        anchors.left: parent.left
+        anchors.leftMargin: 16 * virtualstudio.uiScale;
+        width: 150 * virtualstudio.uiScale; height: 36 * virtualstudio.uiScale
+
+        Text {
+            text: "Back"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
+            anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+            color: textColour
+        }
+    }
+
+    DeviceWarning {
+        id: deviceWarning
+        anchors.left: backButton.right
+        anchors.leftMargin: 24 * virtualstudio.uiScale
+        anchors.bottom: parent.bottom
+        anchors.bottomMargin: 16 * virtualstudio.uiScale;
+        visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+    }
+}
diff --git a/src/gui/Connected.qml b/src/gui/Connected.qml
new file mode 100644 (file)
index 0000000..00d6798
--- /dev/null
@@ -0,0 +1,85 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import org.jacktrip.jacktrip 1.0
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    property bool connecting: false
+
+    property int leftHeaderMargin: 16
+    property int fontBig: 28
+    property int fontMedium: 12
+    property int fontSmall: 10
+    property int fontTiny: 8
+
+    property int bodyMargin: 60
+    property int rightMargin: 16
+    property int bottomToolTipMargin: 8
+    property int rightToolTipMargin: 4
+
+    property string studioStatus: (virtualstudio.currentStudio.id === "" ? "" : virtualstudio.currentStudio.status)
+    property bool showReadyScreen: studioStatus === "Ready"
+    property bool showStartingScreen: studioStatus === "Starting"
+    property bool showStoppingScreen: (virtualstudio.currentStudio.id === "" ? false : (virtualstudio.currentStudio.isAdmin && !virtualstudio.currentStudio.enabled && virtualstudio.currentStudio.cloudId !== ""))
+    property bool showWaitingScreen: !showStoppingScreen && !showStartingScreen && !showReadyScreen
+
+    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string strokeColor: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+
+    property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+    property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string browserButtonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+    property string browserButtonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+    property string browserButtonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+    property string saveButtonBackgroundColour: "#F2F3F3"
+    property string saveButtonPressedColour: "#E7E8E8"
+    property string saveButtonStroke: "#EAEBEB"
+    property string saveButtonPressedStroke: "#B0B5B5"
+    property string saveButtonText: "#DB0A0A"
+
+    property string muteButtonMutedColor: "#FCB6B6"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string meterColor: virtualstudio.darkMode ? "gray" : "#E0E0E0"
+    property real muteButtonLightnessValue: virtualstudio.darkMode ? 1.0 : 0.0
+    property real muteButtonMutedLightnessValue: 0.24
+    property real muteButtonMutedSaturationValue: 0.73
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+    property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1"
+    property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
+    property string toolTipTextColour: textColour
+    property string warningTextColour: "#DB0A0A"
+    property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+
+    property string meterGreen: "#61C554"
+    property string meterYellow: "#F5BF4F"
+    property string meterRed: "#F21B1B"
+
+    property bool isUsingRtAudio: audio.audioBackend == "RtAudio"
+
+    Loader {
+        id: studioWebLoader
+        anchors.top: parent.top
+        anchors.right: parent.right
+        anchors.left: parent.left
+        anchors.bottom: deviceControlsGroup.top
+
+        property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
+        property string studioId: virtualstudio.currentStudio.id
+
+        source: accessToken && studioId ? "Web.qml" : "WebNull.qml"
+    }
+
+    DeviceControlsGroup {
+        id: deviceControlsGroup
+        anchors.bottom: footer.top
+    }
+
+    Footer {
+        id: footer
+        anchors.bottom: parent.bottom
+    }
+}
diff --git a/src/gui/CreateStudio.qml b/src/gui/CreateStudio.qml
new file mode 100644 (file)
index 0000000..4228f51
--- /dev/null
@@ -0,0 +1,124 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import QtWebEngine
+import org.jacktrip.jacktrip 1.0
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    property int fontMedium: 12
+    property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+    property string browserButtonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+
+    Loader {
+        id: webLoader
+        anchors.top: parent.top
+        anchors.right: parent.right
+        anchors.left: parent.left
+        anchors.bottom: footer.top
+        property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
+        sourceComponent: virtualstudio.windowState === "create_studio" && accessToken ? createStudioWeb : createStudioNull
+    }
+
+    Component {
+        id: createStudioNull
+        Rectangle {
+            anchors.fill: parent
+            color: backgroundColour
+        }
+    }
+
+    Component {
+        id: createStudioWeb
+        WebEngineView {
+            id: webEngineView
+            anchors.fill: parent
+            settings.javascriptCanAccessClipboard: true
+            settings.javascriptCanPaste: true
+            settings.screenCaptureEnabled: true
+            profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}`
+            url: `https://${virtualstudio.apiHost}/qt/create?accessToken=${accessToken}`
+
+            onContextMenuRequested: function(request) {
+                // this disables the default context menu: https://doc.qt.io/qt-6.2/qml-qtwebengine-contextmenurequest.html#accepted-prop
+                request.accepted = true;
+            }
+
+            onNewWindowRequested: function(request) {
+                Qt.openUrlExternally(request.requestedUrl);
+            }
+
+            onFeaturePermissionRequested: function(securityOrigin, feature) {
+                webEngineView.grantFeaturePermission(securityOrigin, feature, true);
+            }
+
+            onRenderProcessTerminated: function(terminationStatus, exitCode) {
+                var status = "";
+                switch (terminationStatus) {
+                case WebEngineView.NormalTerminationStatus:
+                    status = "(normal exit)";
+                    break;
+                case WebEngineView.AbnormalTerminationStatus:
+                    status = "(abnormal exit)";
+                    break;
+                case WebEngineView.CrashedTerminationStatus:
+                    status = "(crashed)";
+                    break;
+                case WebEngineView.KilledTerminationStatus:
+                    status = "(killed)";
+                    break;
+                }
+                console.log("Render process exited with code " + exitCode + " " + status);
+            }
+        }
+    }
+
+    Rectangle {
+        id: footer
+        anchors.bottom: parent.bottom
+        width: parent.width
+        height: 48
+        color: backgroundColour
+
+        RowLayout {
+            id: layout
+            anchors.fill: parent
+
+            Item {
+                Layout.fillHeight: true
+                Layout.fillWidth: true
+
+                Button {
+                    id: backButton
+                    anchors.centerIn: parent
+                    width: 180 * virtualstudio.uiScale
+                    height: 36 * virtualstudio.uiScale
+                    background: Rectangle {
+                        radius: 8 * virtualstudio.uiScale
+                        color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour)
+                    }
+                    onClicked: virtualstudio.windowState = "browse"
+
+                    Text {
+                        text: "Back to Studios"
+                        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
+                        anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                        color: textColour
+                    }
+                }
+            }
+        }
+
+        Rectangle {
+            id: backgroundBorder
+            width: parent.width
+            height: 1
+            y: parent.height - footer.height
+            color: buttonStroke
+        }
+    }
+}
diff --git a/src/gui/DeviceControls.qml b/src/gui/DeviceControls.qml
new file mode 100644 (file)
index 0000000..38e1745
--- /dev/null
@@ -0,0 +1,197 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+
+Item {
+    width: parent.width
+    height: parent.height
+    clip: true
+
+    required property bool isInput
+    property string muteButtonMutedColor: "#FCB6B6"
+    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#D5D9D9"
+
+    Component {
+        id: controlIndicator
+
+        Button {
+            id: iconButton
+            width: 24 * virtualstudio.uiScale
+            height: 24 * virtualstudio.uiScale
+
+            anchors.left: parent.left
+            anchors.leftMargin: 8 * virtualstudio.uiScale
+
+            background: Rectangle {
+                color: isInput ? (audio.inputMuted ? muteButtonMutedColor : buttonColour) : "transparent"
+                width: 24 * virtualstudio.uiScale
+                radius: 4 * virtualstudio.uiScale
+            }
+
+            onClicked: isInput ? audio.inputMuted = !audio.inputMuted : console.log()
+
+            AppIcon {
+                id: iconImage
+                anchors.centerIn: parent
+                width: 24 * virtualstudio.uiScale
+                height: 24 * virtualstudio.uiScale
+                icon.source: isInput ? (audio.inputMuted ? "micoff.svg" : "mic.svg") : "headphones.svg"
+                color: isInput ? (audio.inputMuted ? "red" : ( virtualstudio.darkMode ? "#CCCCCC" : "#333333" )) : (virtualstudio.darkMode ? "#CCCCCC" : "#333333")
+                onClicked: isInput ? audio.inputMuted = !audio.inputMuted : console.log()
+            }
+
+            ToolTip {
+                visible: isInput && iconButton.hovered
+                x: iconButton.x + iconButton.width
+                y: iconButton.y + iconButton.height
+
+                contentItem: Text {
+                    text: audio.inputMuted ? qsTr("Click to unmute yourself") : qsTr("Click to mute yourself")
+                    font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                }
+
+                background: Rectangle {
+                    color: toolTipBackgroundColour
+                    radius: 4
+                    layer.enabled: true
+                    layer.effect: Glow {
+                        color: "#66000000"
+                        transparentBorder: true
+                    }
+                }
+            }
+        }
+    }
+
+    Component {
+        id: inputControls
+
+        ColumnLayout {
+            anchors.fill: parent
+            spacing: 2 * virtualstudio.uiScale
+
+            VolumeSlider {
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                labelText: "Send"
+                tooltipText: "How loudly other participants hear you"
+                sliderEnabled: !audio.inputMuted
+            }
+
+            DeviceWarning {
+                id: deviceWarning
+                visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+            }
+        }
+    }
+
+    Component {
+        id: outputControls
+
+        ColumnLayout {
+            anchors.fill: parent
+            spacing: 4 * virtualstudio.uiScale
+
+            VolumeSlider {
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                labelText: "Studio"
+                tooltipText: "How loudly you hear other participants"
+                sliderEnabled: true
+            }
+
+            VolumeSlider {
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                labelText: "Monitor"
+                tooltipText: "How loudly you hear yourself"
+                sliderEnabled: true
+            }
+        }
+    }
+
+    ColumnLayout {
+        anchors.fill: parent
+        spacing: 5 * virtualstudio.uiScale
+
+        Item {
+            Layout.topMargin: 5 * virtualstudio.uiScale
+            Layout.preferredHeight: 30 * virtualstudio.uiScale
+            Layout.fillWidth: true
+
+            RowLayout {
+                anchors.fill: parent
+                spacing: 8 * virtualstudio.uiScale
+
+                Item {
+                    Layout.fillHeight: true
+                    Layout.preferredWidth: 100 * virtualstudio.uiScale
+
+                    Loader {
+                        id: typeIconIndicator
+                        anchors.left: parent.left
+                        sourceComponent: controlIndicator
+                    }
+
+                    Text {
+                        id: label
+                        anchors.left: parent.left
+                        anchors.leftMargin: 36 * virtualstudio.uiScale
+
+                        text: isInput ? "Input" : "Output"
+                        font { family: "Poppins"; weight: Font.Bold; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                        color: textColour
+                    }
+
+                    InfoTooltip {
+                        content: isInput ? qsTr("Audio sent to the studio (microphone, instrument, mixer, etc.)") : qsTr("How you'll hear the studio audio")
+                        size: 16
+                        anchors.left: label.right
+                        anchors.leftMargin: 2 * virtualstudio.uiScale
+                        anchors.verticalCenter: label.verticalCenter
+                    }
+                }
+
+                Item {
+                    Layout.fillHeight: true
+                    Layout.fillWidth: true
+                    Layout.preferredWidth: 200 * virtualstudio.uiScale
+
+                    Meter {
+                        anchors.fill: parent
+                        anchors.rightMargin: 8 * virtualstudio.uiScale
+                        model: isInput ? audio.inputMeterLevels : audio.outputMeterLevels
+                        clipped: isInput ? audio.inputClipped : audio.outputClipped
+                        enabled: true
+                    }
+                }
+            }
+        }
+
+        Item {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+            Layout.bottomMargin: 5 * virtualstudio.uiScale
+
+            RowLayout {
+                anchors.fill: parent
+                spacing: 8 * virtualstudio.uiScale
+
+                Item {
+                    Layout.fillHeight: true
+                    Layout.fillWidth: true
+                    Layout.leftMargin: 8 * virtualstudio.uiScale
+                    Layout.rightMargin: 8 * virtualstudio.uiScale
+
+                    Loader {
+                        anchors.fill: parent
+                        anchors.top: parent.top
+                        sourceComponent: isInput ? inputControls : outputControls
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/src/gui/DeviceControlsGroup.qml b/src/gui/DeviceControlsGroup.qml
new file mode 100644 (file)
index 0000000..43ca5fd
--- /dev/null
@@ -0,0 +1,383 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Rectangle {
+    property string disabledButtonText: "#D3D4D4"
+    property string saveButtonText: "#DB0A0A"
+    property int fullHeight: 88 * virtualstudio.uiScale
+    property int minimumHeight: 48 * virtualstudio.uiScale
+
+    property bool isUsingRtAudio: audio.audioBackend == "RtAudio"
+    property bool isReady: virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready"
+    property bool showDeviceControls: getShowDeviceControls()
+
+    id: deviceControlsGroup
+    width: parent.width
+    height: isReady ? (showDeviceControls ? fullHeight : (feedbackDetectedModal.visible ? minimumHeight : 0)) : minimumHeight;
+    color: backgroundColour
+
+    function getShowDeviceControls () {
+        // self-managed servers do not support minified controls so keep it full size
+        return (!virtualstudio.currentStudio.isManaged && virtualstudio.currentStudio.sessionId === "") || (!virtualstudio.collapseDeviceControls && isReady);
+    }
+
+    MouseArea {
+        anchors.fill: parent
+        propagateComposedEvents: false
+    }
+
+    RowLayout {
+        id: layout
+        anchors.fill: parent
+        spacing: 2
+        visible: !feedbackDetectedModal.visible
+
+        Item {
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            visible: !isReady
+
+            Button {
+                id: backButton
+                anchors.centerIn: parent
+                width: 180 * virtualstudio.uiScale
+                height: 36 * virtualstudio.uiScale
+                background: Rectangle {
+                    radius: 8 * virtualstudio.uiScale
+                    color: backButton.down ? browserButtonPressedColour : (backButton.hovered ? browserButtonHoverColour : browserButtonColour)
+                }
+                onClicked: virtualstudio.disconnect()
+
+                Text {
+                    text: "Back to Studios"
+                    font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
+                    anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                    color: textColour
+                }
+            }
+        }
+
+        Item {
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            visible: showDeviceControls
+
+            DeviceControls {
+                isInput: true
+            }
+        }
+
+        Item {
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            visible: showDeviceControls
+
+            DeviceControls {
+                isInput: false
+            }
+        }
+
+        Item {
+            Layout.fillHeight: true
+            Layout.preferredWidth: 48 * virtualstudio.uiScale
+            visible: showDeviceControls
+
+            ColumnLayout {
+                anchors.fill: parent
+                spacing: 2
+
+                Item {
+                    Layout.preferredHeight: 24 * virtualstudio.uiScale
+                    Layout.preferredWidth: 24 * virtualstudio.uiScale
+                    Layout.topMargin: 2 * virtualstudio.uiScale
+                    Layout.rightMargin: 2 * virtualstudio.uiScale
+                    Layout.alignment: Qt.AlignRight | Qt.AlignTop
+
+                    Button {
+                        id: closeDeviceControlsButton
+                        visible: virtualstudio.currentStudio.isManaged || virtualstudio.currentStudio.sessionId !== ""
+                        width: 24 * virtualstudio.uiScale
+                        height: 24 * virtualstudio.uiScale
+                        background: Rectangle {
+                            color: backgroundColour
+                        }
+                        anchors.top: parent.top
+                        anchors.right: parent.right
+                        onClicked: {
+                            virtualstudio.collapseDeviceControls = true;
+                        }
+
+                        AppIcon {
+                            id: closeDeviceControlsIcon
+                            anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+                            width: 24 * virtualstudio.uiScale
+                            height: 24 * virtualstudio.uiScale
+                            color: closeDeviceControlsButton.hovered ? textColour : browserButtonHoverColour
+                            icon.source: "close.svg"
+                            onClicked: {
+                                virtualstudio.collapseDeviceControls = true;
+                            }
+                        }
+                    }
+                }
+
+                Item {
+                    visible: isUsingRtAudio
+                    Layout.preferredWidth: 40 * virtualstudio.uiScale
+                    Layout.preferredHeight: 64 * virtualstudio.uiScale
+                    Layout.bottomMargin: 5 * virtualstudio.uiScale
+                    Layout.topMargin: 2 * virtualstudio.uiScale
+                    Layout.rightMargin: 2 * virtualstudio.uiScale
+                    Layout.alignment: Qt.AlignHCenter | Qt.AlignTop
+
+                    Button {
+                        id: changeDevicesButton
+                        width: 36 * virtualstudio.uiScale
+                        height: 36 * virtualstudio.uiScale
+                        anchors.top: parent.top
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        background: Rectangle {
+                            radius: 8 * virtualstudio.uiScale
+                            color: changeDevicesButton.down ? browserButtonPressedColour : (changeDevicesButton.hovered ? browserButtonHoverColour : browserButtonColour)
+                        }
+                        onClicked: {
+                            virtualstudio.windowState = "change_devices"
+                            if (!audio.deviceModelsInitialized) {
+                                audio.refreshDevices();
+                            }
+                        }
+
+                        AppIcon {
+                            id: changeDevicesIcon
+                            anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+                            width: 20 * virtualstudio.uiScale
+                            height: 20 * virtualstudio.uiScale
+                            icon.source: "cog.svg"
+                            onClicked: {
+                                virtualstudio.windowState = "change_devices"
+                                if (!audio.deviceModelsInitialized) {
+                                    audio.refreshDevices();
+                                }
+                            }
+                        }
+                    }
+
+                    Text {
+                        anchors.top: changeDevicesButton.bottom
+                        text: "Devices"
+                        font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale}
+                        anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                        color: textColour
+                    }
+                }
+            }
+        }
+    }
+
+    Rectangle {
+        id: backgroundBorder
+        width: parent.width
+        height: 1
+        anchors.top: layout.top
+        color: strokeColor
+    }
+
+    Popup {
+        id: feedbackDetectedModal
+        padding: 1
+        width: parent.width
+        height: parent.height
+        anchors.centerIn: parent
+        dim: false
+        modal: false
+        focus: true
+        closePolicy: Popup.NoAutoClose
+
+        background: Rectangle {
+            anchors.fill: parent
+            color: "transparent"
+            border.width: 1
+            border.color: buttonStroke
+            clip: true
+        }
+
+        contentItem: Rectangle {
+            width: parent.width
+            height: 232 * virtualstudio.uiScale
+            color: backgroundColour
+
+            Item {
+                id: feedbackDetectedContent
+                anchors.top: parent.top
+                anchors.bottom: parent.bottom
+                anchors.left: parent.left
+                anchors.leftMargin: 16 * virtualstudio.uiScale
+                anchors.right: parent.right
+
+                AppIcon {
+                    id: feedbackWarningIcon
+                    anchors.left: parent.left
+                    anchors.top: parent.top
+                    anchors.topMargin: 10 * virtualstudio.uiScale
+                    width: 32 * virtualstudio.uiScale
+                    height: 32 * virtualstudio.uiScale
+                    icon.source: "warning.svg"
+                    color: "#F21B1B"
+                    visible: showDeviceControls
+                }
+
+                AppIcon {
+                    id: feedbackWarningIconMinified
+                    anchors.left: parent.left
+                    anchors.verticalCenter: parent.verticalCenter
+                    height: 24 * virtualstudio.uiScale
+                    width: 24 * virtualstudio.uiScale
+                    icon.source: "warning.svg"
+                    color: "#F21B1B"
+                    visible: !showDeviceControls
+                }
+
+                Text {
+                    id: feedbackDetectedHeader
+                    anchors.top: parent.top
+                    anchors.topMargin: 10 * virtualstudio.uiScale
+                    anchors.left: feedbackWarningIcon.right
+                    anchors.leftMargin: 16 * virtualstudio.uiScale
+                    width: parent.width
+                    text: "Audio feedback detected!"
+                    font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
+                    color: textColour
+                    elide: Text.ElideRight
+                    wrapMode: Text.WordWrap
+                    visible: showDeviceControls
+                }
+
+                Text {
+                    id: feedbackDetectedText
+                    anchors.top: feedbackDetectedHeader.bottom
+                    anchors.topMargin: 4 * virtualstudio.uiScale
+                    anchors.left: feedbackWarningIcon.right
+                    anchors.leftMargin: 16 * virtualstudio.uiScale
+                    width: parent.width
+                    text: "JackTrip detected a feedback loop. Your monitor and input volume have automatically been disabled."
+                    font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                    elide: Text.ElideRight
+                    wrapMode: Text.WordWrap
+                    visible: showDeviceControls
+                }
+
+                Text {
+                    id: feedbackDetectedTextMinified
+                    anchors.verticalCenter: parent.verticalCenter
+                    anchors.left: feedbackWarningIcon.right
+                    anchors.leftMargin: 16 * virtualstudio.uiScale
+                    width: parent.width
+                    text: "JackTrip detected a feedback loop. Your monitor and input volume have automatically been disabled."
+                    font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                    elide: Text.ElideRight
+                    wrapMode: Text.WordWrap
+                    visible: !showDeviceControls
+                }
+
+                Text {
+                    id: feedbackDetectedText2
+                    anchors.top: feedbackDetectedText.bottom
+                    anchors.topMargin: 2 * virtualstudio.uiScale
+                    anchors.left: feedbackWarningIcon.right
+                    anchors.leftMargin: 16 * virtualstudio.uiScale
+                    width: parent.width
+                    text: "You can disable this behavior under <b>Settings</b> > <b>Advanced</b>"
+                    textFormat: Text.RichText
+                    font {family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                    color: textColour
+                    elide: Text.ElideRight
+                    wrapMode: Text.WordWrap
+                    visible: showDeviceControls
+                }
+
+                Button {
+                    id: closeFeedbackDetectedModalButton
+                    anchors.right: parent.right
+                    anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                    anchors.verticalCenter: parent.verticalCenter
+                    width: 128 * virtualstudio.uiScale;
+                    height: 30 * virtualstudio.uiScale
+                    onClicked: feedbackDetectedModal.close()
+
+                    background: Rectangle {
+                        radius: 6 * virtualstudio.uiScale
+                        color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour)
+                        border.width: 1
+                        border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke)
+                    }
+
+                    Text {
+                        text: "Ok"
+                        font.family: "Poppins"
+                        font.pixelSize: showDeviceControls ? fontSmall * virtualstudio.fontScale * virtualstudio.uiScale : fontTiny * virtualstudio.fontScale * virtualstudio.uiScale
+                        font.weight: Font.Bold
+                        color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        anchors.verticalCenter: parent.verticalCenter
+                    }
+                    visible: showDeviceControls
+                }
+
+                Button {
+                    id: closeFeedbackDetectedModalButtonMinified
+                    anchors.right: parent.right
+                    anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                    anchors.verticalCenter: parent.verticalCenter
+                    width: 80 * virtualstudio.uiScale
+                    height: 24 * virtualstudio.uiScale
+                    onClicked: feedbackDetectedModal.close()
+
+                    background: Rectangle {
+                        radius: 6 * virtualstudio.uiScale
+                        color: closeFeedbackDetectedModalButton.down ? browserButtonPressedColour : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverColour : browserButtonColour)
+                        border.width: 1
+                        border.color: closeFeedbackDetectedModalButton.down ? browserButtonPressedStroke : (closeFeedbackDetectedModalButton.hovered ? browserButtonHoverStroke : browserButtonStroke)
+                    }
+
+                    Text {
+                        text: "Ok"
+                        font.family: "Poppins"
+                        font.pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale
+                        font.weight: Font.Bold
+                        color: !Boolean(audio.devicesError) && audio.backendAvailable ? saveButtonText : disabledButtonText
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        anchors.verticalCenter: parent.verticalCenter
+                    }
+                    visible: !showDeviceControls
+                }
+            }
+        }
+    }
+
+    Connections {
+        target: virtualstudio
+
+        function onFeedbackDetected() {
+            if (virtualstudio.windowState === "connected") {
+                feedbackDetectedModal.visible = true;
+            }
+        }
+
+        function onCollapseDeviceControlsChanged(collapseDeviceControls) {
+            showDeviceControls = getShowDeviceControls()
+        }
+
+        function onCurrentStudioChanged(currentStudio) {
+            isReady = virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready"
+            showDeviceControls = getShowDeviceControls()
+        }
+
+        function onConnectionStateChanged(connectionState) {
+            isReady = virtualstudio.currentStudio.id !== "" && virtualstudio.currentStudio.status == "Ready"
+            showDeviceControls = getShowDeviceControls()
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/gui/DeviceRefreshButton.qml b/src/gui/DeviceRefreshButton.qml
new file mode 100644 (file)
index 0000000..1c3ed65
--- /dev/null
@@ -0,0 +1,38 @@
+import QtQuick
+import QtQuick.Controls
+
+Button {
+    id: refreshButton
+    text: "Refresh Devices"
+
+    property int fontExtraSmall: 8
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+    property var onDeviceRefresh: function () { audio.refreshDevices(); };
+
+    width: 144 * virtualstudio.uiScale;
+    height: 30 * virtualstudio.uiScale
+    palette.buttonText: textColour
+    display: AbstractButton.TextBesideIcon
+
+    background: Rectangle {
+        radius: 6 * virtualstudio.uiScale
+        color: refreshButton.down ? buttonPressedColour : (refreshButton.hovered ? buttonHoverColour : buttonColour)
+        border.width: 1
+        border.color: refreshButton.down ? buttonPressedStroke : (refreshButton.hovered ? buttonHoverStroke : buttonStroke)
+    }
+    icon {
+        source: "refresh.svg";
+        color: textColour;
+    }
+    onClicked: { onDeviceRefresh(); }
+    font {
+        family: "Poppins"
+        pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale
+    }
+}
diff --git a/src/gui/DeviceWarning.qml b/src/gui/DeviceWarning.qml
new file mode 100644 (file)
index 0000000..4b8302a
--- /dev/null
@@ -0,0 +1,58 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    height: 28 * virtualstudio.uiScale
+    property string devicesWarningColour: "#F21B1B"
+
+    AppIcon {
+        id: devicesWarningIcon
+        anchors.left: parent.left
+        anchors.verticalCenter: parent.verticalCenter
+        width: parent.height
+        height: parent.height
+        icon.source: "warning.svg"
+        color: devicesWarningColour
+        visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+    }
+    
+    Text {
+        id: warningOrErrorText
+        text: Boolean(audio.devicesError) ? "Audio Configuration Error" : "Audio Configuration Warning"
+        anchors.left: devicesWarningIcon.right
+        anchors.leftMargin: 4 * virtualstudio.uiScale
+        anchors.verticalCenter: devicesWarningIcon.verticalCenter
+        visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+        font { family: "Poppins"; pixelSize: 9 * virtualstudio.fontScale * virtualstudio.uiScale }
+        color: devicesWarningColour
+    }
+
+    InfoTooltip {
+        id: devicesWarningTooltip
+        anchors.left: warningOrErrorText.right
+        anchors.leftMargin: 2 * virtualstudio.uiScale
+        anchors.top: devicesWarningIcon.top
+        content: qsTr(audio.devicesError || audio.devicesWarning)
+        iconColor: devicesWarningColour
+        size: 16 * virtualstudio.uiScale
+        visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+    }
+
+    MouseArea {
+        id: devicesWarningToolTipArea
+        anchors.top: devicesWarningIcon.top
+        anchors.bottom: devicesWarningIcon.bottom
+        anchors.left: devicesWarningIcon.left
+        anchors.right: devicesWarningTooltip.right
+        hoverEnabled: true
+        onEntered: devicesWarningTooltip.showToolTip = true
+        onExited: devicesWarningTooltip.showToolTip = false
+        onClicked: {
+            if (Boolean(audio.devicesError) && audio.devicesErrorHelpUrl !== "") {
+                virtualstudio.openLink(audio.devicesErrorHelpUrl);
+            } else if (Boolean(audio.devicesWarning) && audio.devicesWarningHelpUrl !== "") {
+                virtualstudio.openLink(audio.devicesWarningHelpUrl);
+            }
+        }
+    }
+}
diff --git a/src/gui/Failed.qml b/src/gui/Failed.qml
new file mode 100644 (file)
index 0000000..956406d
--- /dev/null
@@ -0,0 +1,79 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    property int leftMargin: 16
+    property int fontBig: 28
+    property int fontMedium: 18
+    property int fontSmall: 11
+
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5"
+    property string buttonStroke: virtualstudio.darkMode ? "#9C9C9C" : "#A4A7A7"
+    property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A"
+    property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A"
+    property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A"
+
+    AppIcon {
+        id: ohnoImage
+        y: 60
+        anchors.horizontalCenter: parent.horizontalCenter
+        width: 180
+        height: 180
+        icon.source: "sentiment_very_dissatisfied.svg"
+    }
+
+    Text {
+        id: ohnoHeader
+        text: "Oh no!"
+        font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        color: textColour
+        anchors.horizontalCenter: parent.horizontalCenter
+        anchors.top: ohnoImage.bottom
+        anchors.topMargin: 16 * virtualstudio.uiScale
+    }
+
+    Text {
+        id: ohnoMessage
+        text: virtualstudio.failedMessage || "Unable to process request - please try again later."
+        font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+        color: textColour
+        width: 400
+        wrapMode: Text.Wrap
+        horizontalAlignment: Text.AlignHCenter
+        anchors.horizontalCenter: parent.horizontalCenter
+        anchors.top: ohnoHeader.bottom
+        anchors.topMargin: 32 * virtualstudio.uiScale
+    }
+
+    Button {
+        id: backButton
+        background: Rectangle {
+            radius: 6 * virtualstudio.uiScale
+            color: backButton.down ? buttonPressedColour : (backButton.hovered ? buttonHoverColour : buttonColour)
+            border.width: backButton.down ? 1 : 0
+            border.color: buttonStroke
+            layer.enabled: !backButton.down
+        }
+        onClicked: { virtualstudio.windowState = "browse" }
+        width: 256 * virtualstudio.uiScale
+        height: 42 * virtualstudio.uiScale
+        anchors.horizontalCenter: parent.horizontalCenter
+        anchors.top: ohnoMessage.bottom
+        anchors.topMargin: 60 * virtualstudio.uiScale
+        Text {
+            text: "Back"
+            font.family: "Poppins"
+            font.pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.verticalCenter: parent.verticalCenter
+            color: backButton.down ? buttonTextPressed : (backButton.hovered ? buttonTextHover : buttonTextColour)
+        }
+        visible: true
+    }
+}
diff --git a/src/gui/FeedbackSurvey.qml b/src/gui/FeedbackSurvey.qml
new file mode 100644 (file)
index 0000000..6c1528d
--- /dev/null
@@ -0,0 +1,421 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    id: userFeedbackSurvey
+
+    anchors.centerIn: parent
+    width: 480 * virtualstudio.uiScale
+    height: 232 * virtualstudio.uiScale
+
+    property int leftHeaderMargin: 16
+    property int fontBig: 28
+    property int fontMedium: 12
+    property int fontSmall: 10
+    property int fontTiny: 8
+    property int bodyMargin: 60
+    property int rightMargin: 16
+    property int bottomToolTipMargin: 8
+    property int rightToolTipMargin: 4
+
+    property string buttonColour: "#F2F3F3"
+    property string buttonHoverColour: "#E7E8E8"
+    property string buttonPressedColour: "#E7E8E8"
+    property string buttonStroke: "#EAEBEB"
+    property string buttonHoverStroke: "#B0B5B5"
+    property string buttonPressedStroke: "#B0B5B5"
+
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string textAreaTextColour: virtualstudio.darkMode ? "#A6A6A6" : "#757575"
+    property string textAreaColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+
+    property string serverId: ""
+
+    property int rating: 0
+    property int hover: star1MouseArea.containsMouse ? 1 : star2MouseArea.containsMouse ? 2 : star3MouseArea.containsMouse ? 3 : star4MouseArea.containsMouse ? 4 : star5MouseArea.containsMouse ? 5 : 0
+    property int currentView: (hover > 0 ? hover : rating)
+    property bool submitted: false;
+
+    property string message: ""
+
+    Popup {
+        id: userFeedbackModal
+        padding: 1
+        width: parent.width
+        height: 300 * virtualstudio.uiScale
+        anchors.centerIn: parent
+        modal: true
+        focus: true
+        closePolicy: Popup.NoAutoClose
+
+        background: Rectangle {
+            anchors.fill: parent
+            color: "transparent"
+            radius: 6 * virtualstudio.uiScale
+            border.width: 1
+            border.color: buttonStroke
+            clip: true
+        }
+
+        contentItem: Rectangle {
+            width: parent.width
+            height: parent.height
+            color: backgroundColour
+            radius: 6 * virtualstudio.uiScale
+
+            Item {
+                id: userFeedbackSurveyContent
+                anchors.top: parent.top
+                anchors.topMargin: 24 * virtualstudio.uiScale
+                anchors.bottom: parent.bottom
+                anchors.left: parent.left
+                anchors.right: parent.right
+                visible: !submitted
+
+                Text {
+                    id: userFeedbackSurveyHeader
+                    anchors.top: parent.top
+                    anchors.topMargin: 16 * virtualstudio.uiScale
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    width: parent.width
+                    text: "How did your session go?"
+                    font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
+                    horizontalAlignment: Text.AlignHCenter
+                    color: textColour
+                    elide: Text.ElideRight
+                    wrapMode: Text.WordWrap
+                }
+
+                Text {
+                    id: ratingItemInstructions
+                    anchors.top: userFeedbackSurveyHeader.bottom
+                    anchors.topMargin: 12 * virtualstudio.uiScale
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    width: parent.width
+                    text: "Rate your session on a scale of 1 to 5"
+                    font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                    horizontalAlignment: Text.AlignHCenter
+                    color: textColour
+                    elide: Text.ElideRight
+                    wrapMode: Text.WordWrap
+                }
+
+                Item {
+                    id: ratingItem
+                    anchors.top: ratingItemInstructions.bottom
+                    anchors.topMargin: 4 * virtualstudio.uiScale
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    height: 40 * virtualstudio.uiScale
+                    width: 200 * virtualstudio.uiScale
+
+                    Item {
+                        id: star1Container
+                        anchors.verticalCenter: parent.verticalCenter
+                        anchors.right: star2Container.left
+                        width: parent.width / 5
+                        height: parent.height
+
+                        AppIcon {
+                            id: star1Icon
+                            anchors.centerIn: parent
+                            width: currentView >= 1 ? parent.width : 20 * virtualstudio.uiScale
+                            height: currentView >= 1 ? parent.height : 20 * virtualstudio.uiScale
+                            icon.source: "star.svg"
+                            color: currentView >= 1 ? "#faaf00" : "#606060"
+                        }
+
+                        MouseArea {
+                            id: star1MouseArea
+                            anchors.fill: parent
+                            hoverEnabled: true
+                            onClicked: () => {
+                                if (rating === 1) {
+                                    rating = 0;
+                                } else {
+                                    rating = 1;
+                                }
+                            }
+                        }
+                    }
+
+                    Item {
+                        id: star2Container
+                        anchors.verticalCenter: parent.verticalCenter
+                        anchors.right: star3Container.left
+                        width: parent.width / 5
+                        height: parent.height
+
+                        AppIcon {
+                            id: star2Icon
+                            anchors.centerIn: parent
+                            width: currentView >= 2 ? parent.width : 20 * virtualstudio.uiScale
+                            height: currentView >= 2 ? parent.height : 20 * virtualstudio.uiScale
+                            icon.source: "star.svg"
+                            color: currentView >= 2 ? "#faaf00" : "#606060"
+                        }
+
+                        MouseArea {
+                            id: star2MouseArea
+                            anchors.fill: parent
+                            hoverEnabled: true
+                            onClicked: () => {
+                                if (rating === 2) {
+                                    rating = 0;
+                                } else {
+                                    rating = 2;
+                                }
+                            }
+                        }
+                    }
+
+                    Item {
+                        id: star3Container
+                        anchors.verticalCenter: parent.verticalCenter
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        width: parent.width / 5
+                        height: parent.height
+
+                        AppIcon {
+                            id: star3Icon
+                            anchors.centerIn: parent
+                            width: currentView >= 3 ? parent.width : 20 * virtualstudio.uiScale
+                            height: currentView >= 3 ? parent.height : 20 * virtualstudio.uiScale
+                            icon.source: "star.svg"
+                            color: currentView >= 3 ? "#faaf00" : "#606060"
+                        }
+
+                        MouseArea {
+                            id: star3MouseArea
+                            anchors.fill: parent
+                            hoverEnabled: true
+                            onClicked: () => {
+                                if (rating === 3) {
+                                    rating = 0;
+                                } else {
+                                    rating = 3;
+                                }
+                            }
+                        }
+                    }
+
+                    Item {
+                        id: star4Container
+                        anchors.verticalCenter: parent.verticalCenter
+                        anchors.left: star3Container.right
+                        width: parent.width / 5
+                        height: parent.height
+
+                        AppIcon {
+                            id: star4Icon
+                            anchors.centerIn: parent
+                            width: currentView >= 4 ? parent.width : 20 * virtualstudio.uiScale
+                            height: currentView >= 4 ? parent.height : 20 * virtualstudio.uiScale
+                            icon.source: "star.svg"
+                            color: currentView >= 4 ? "#faaf00" : "#606060"
+                        }
+
+                        MouseArea {
+                            id: star4MouseArea
+                            anchors.fill: parent
+                            hoverEnabled: true
+                            onClicked: () => {
+                                if (rating === 4) {
+                                    rating = 0;
+                                } else {
+                                    rating = 4;
+                                }
+                            }
+                        }
+                    }
+
+                    Item {
+                        id: star5Container
+                        anchors.verticalCenter: parent.verticalCenter
+                        anchors.left: star4Container.right
+                        width: parent.width / 5
+                        height: parent.height
+
+                        AppIcon {
+                            id: star5Icon
+                            anchors.centerIn: parent
+                            width: currentView >= 5 ? parent.width : 20 * virtualstudio.uiScale
+                            height: currentView >= 5 ? parent.height : 20 * virtualstudio.uiScale
+                            icon.source: "star.svg"
+                            color: currentView >= 5 ? "#faaf00" : "#606060"
+                        }
+
+                        MouseArea {
+                            id: star5MouseArea
+                            anchors.fill: parent
+                            hoverEnabled: true
+                            onClicked: () => {
+                                if (rating === 5) {
+                                    rating = 0;
+                                } else {
+                                    rating = 5;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                ScrollView {
+                    id: messageBoxScrollArea
+                    anchors.left: parent.left
+                    anchors.leftMargin: 32 * virtualstudio.uiScale
+                    anchors.right: parent.right
+                    anchors.rightMargin: 32 * virtualstudio.uiScale
+                    anchors.top: ratingItem.bottom
+                    anchors.topMargin: 12 * virtualstudio.uiScale
+                    height: 64 * virtualstudio.uiScale
+
+                    TextArea {
+                        id: messageBox
+                        placeholderText: qsTr("(Optional) Let us know how we can improve your experience.")
+                        placeholderTextColor: textAreaTextColour
+                        color: textColour
+                        background: Rectangle {
+                        color: textAreaColour
+                        radius: 6 * virtualstudio.uiScale
+                        border.width: 1
+                        border.color: buttonStroke
+                        }
+                    }
+                }
+
+                Item {
+                    id: buttonsArea
+                    height: 32 * virtualstudio.uiScale
+                    width: 324 * virtualstudio.uiScale
+                    anchors.horizontalCenter: messageBoxScrollArea.horizontalCenter
+                    anchors.top: messageBoxScrollArea.bottom
+                    anchors.topMargin: 24 * virtualstudio.uiScale
+
+                    Button {
+                        id: userFeedbackButton
+                        anchors.right: buttonsArea.right
+                        anchors.horizontalCenter: buttonsArea.horizontalCenter
+                        anchors.verticalCenter: parent.buttonsArea
+                        width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                        onClicked: () => {
+                            if (rating === 0 && messageBox.text === "") {
+                                userFeedbackModal.close();
+                                serverId = "";
+                                messageBox.clear();
+                                return;
+                            }
+                            virtualstudio.collectFeedbackSurvey(serverId, rating, messageBox.text);
+                            submitted = true;
+                            rating = 0;
+                            serverId = "";
+                            messageBox.clear();
+                            userFeedbackModal.height = 150 * virtualstudio.uiScale
+                            submittedFeedbackTimer.start();
+                        }
+
+                        background: Rectangle {
+                            radius: 6 * virtualstudio.uiScale
+                            color: userFeedbackButton.down ? buttonPressedColour : (userFeedbackButton.hovered ? buttonHoverColour : buttonColour)
+                            border.width: 1
+                            border.color: userFeedbackButton.down ? buttonPressedStroke : (userFeedbackButton.hovered ? buttonHoverStroke : buttonStroke)
+                        }
+
+                        Text {
+                            text: (rating === 0 && messageBox.text === "") ? "Dismiss" : "Submit"
+                            font.family: "Poppins"
+                            font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
+                            font.weight: Font.Bold
+                            color: "#DB0A0A"
+                            anchors.horizontalCenter: parent.horizontalCenter
+                            anchors.verticalCenter: parent.verticalCenter
+                        }
+                    }
+
+                    Timer {
+                        id: submittedFeedbackTimer
+                        interval: 5000; running: false; repeat: false
+                        onTriggered: () => {
+                            submitted = false;
+                            userFeedbackModal.height = 300 * virtualstudio.uiScale
+                            userFeedbackModal.close();
+                        }
+                    }
+                }
+            }
+
+            Item {
+                id: submittedFeedbackContent
+                anchors.top: parent.top
+                anchors.bottom: parent.bottom
+                anchors.left: parent.left
+                anchors.right: parent.right
+                visible: submitted
+
+                Text {
+                    id: submittedFeedbackHeader
+                    anchors.top: submittedFeedbackContent.top
+                    anchors.topMargin: 24 * virtualstudio.uiScale
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    width: parent.width
+                    text: "Thank you!"
+                    font {family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale; bold: true }
+                    horizontalAlignment: Text.AlignHCenter
+                    color: textColour
+                    elide: Text.ElideRight
+                    wrapMode: Text.WordWrap
+                }
+
+                Text {
+                    id: submittedFeedbackText
+                    anchors.top: submittedFeedbackHeader.bottom
+                    anchors.topMargin: 16 * virtualstudio.uiScale
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    width: parent.width
+                    text: "Your feedback has been recorded."
+                    font {family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                    horizontalAlignment: Text.AlignHCenter
+                    color: textColour
+                    elide: Text.ElideRight
+                    wrapMode: Text.WordWrap
+                }
+
+                Button {
+                    id: closeButtonFeedback
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.top: submittedFeedbackText.bottom
+                    anchors.topMargin: 16 * virtualstudio.uiScale
+                    width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                    onClicked: () => {
+                        submitted = false;
+                        userFeedbackModal.height = 300 * virtualstudio.uiScale
+                        userFeedbackModal.close();
+                    }
+
+                    background: Rectangle {
+                        radius: 6 * virtualstudio.uiScale
+                        color: closeButtonFeedback.down ? buttonPressedColour : (closeButtonFeedback.hovered ? buttonHoverColour : buttonColour)
+                        border.width: 1
+                        border.color: closeButtonFeedback.down ? buttonPressedStroke : (closeButtonFeedback.hovered ? buttonHoverStroke : buttonStroke)
+                    }
+
+                    Text {
+                        text: "Close"
+                        font.family: "Poppins"
+                        font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
+                        anchors.horizontalCenter: parent.horizontalCenter
+                        anchors.verticalCenter: parent.verticalCenter
+                    }
+                }
+            }
+        }
+    }
+
+    Connections {
+      target: virtualstudio
+
+      function onOpenFeedbackSurveyModal(serverId) {
+        userFeedbackSurvey.serverId = serverId;
+        userFeedbackModal.open();
+      }
+    }
+}
diff --git a/src/gui/FirstLaunch.qml b/src/gui/FirstLaunch.qml
new file mode 100644 (file)
index 0000000..70d8f1c
--- /dev/null
@@ -0,0 +1,128 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1"
+    property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5"
+    property string buttonStroke: virtualstudio.darkMode ? "#636060" : "#DEDFDF"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
+
+    Image {
+        id: jtlogo
+        source: "logo.svg"
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 35 * virtualstudio.uiScale
+        width: 50 * virtualstudio.uiScale; height: 92 * virtualstudio.uiScale
+        sourceSize: Qt.size(jtlogo.width,jtlogo.height)
+        fillMode: Image.PreserveAspectFit
+        smooth: true
+    }
+
+    Text {
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 168 * virtualstudio.uiScale
+        text: "Sign in with a Virtual Studio account?"
+        font.family: "Poppins"
+        font.pixelSize: 17 * virtualstudio.fontScale * virtualstudio.uiScale
+        color: textColour
+    }
+
+    Text {
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 219 * virtualstudio.uiScale
+        text: "You'll be able to change your mind later"
+        font.family: "Poppins"
+        font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+        color: textColour
+    }
+
+    Button {
+        id: vsButton
+        background: Rectangle {
+            radius: 10 * virtualstudio.uiScale
+            color: vsButton.down ? buttonPressedColour : (vsButton.hovered ? buttonHoverColour : buttonColour)
+            border.width: 1
+            border.color: vsButton.down ? buttonPressedStroke : (vsButton.hovered ? buttonHoverStroke : buttonStroke)
+            layer.enabled: vsButton.hovered && !vsButton.down
+        }
+        onClicked: { virtualstudio.showFirstRun = false; virtualstudio.windowState = "login"; virtualstudio.toVirtualStudio(); }
+        x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale
+        width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale
+        Text {
+            text: "Yes"
+            font.family: "Poppins"
+            font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+            font.weight: Font.Bold
+            color: "#DB0A0A"
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.verticalCenter: parent.verticalCenter
+        }
+    }
+    Text {
+        text: "• Seamless Audio &amp; Video<br>• Recording &amp; Livestreaming<br>• No Servers Required"
+        textFormat: Text.StyledText
+        font.family: "Poppins"
+        font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+        x: parent.width / 2 - (265 * virtualstudio.uiScale);
+        y: 355 * virtualstudio.uiScale;
+        width: 230 * virtualstudio.uiScale
+        padding: 0
+        wrapMode: Text.WordWrap
+        horizontalAlignment: Text.AlignHCenter
+        color: textColour
+    }
+    Image {
+        source: "JTVS.png"
+        x: parent.width / 2 - (265 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale
+        width: 234 * virtualstudio.uiScale; height: 195 * virtualstudio.uiScale;
+    }
+
+    Button {
+        id: standardButton
+        background: Rectangle {
+            radius: 10 * virtualstudio.uiScale
+            color: standardButton.down ? buttonPressedColour : (standardButton.hovered ? buttonHoverColour : buttonColour)
+            border.width: 1
+            border.color: standardButton.down ? buttonPressedStroke : (standardButton.hovered ? buttonHoverStroke : buttonStroke)
+            layer.enabled: standardButton.hovered && !standardButton.down
+        }
+        onClicked: { virtualstudio.toStandard(); }
+        x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 290 * virtualstudio.uiScale
+        width: 234 * virtualstudio.uiScale; height: 49 * virtualstudio.uiScale
+        Text {
+            text: "No"
+            font.family: "Poppins"
+            font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+            font.weight: Font.Bold
+            color: "#DB0A0A"
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.verticalCenter: parent.verticalCenter
+        }
+    }
+    Image {
+        source: "JTOriginal.png"
+        x: parent.width / 2 + (32 * virtualstudio.uiScale); y: 420 * virtualstudio.uiScale
+        width: 234 * virtualstudio.uiScale; height: 337.37 * virtualstudio.uiScale;
+    }
+    Text {
+        text: virtualstudio.psiBuild ? "• Connect via IP address<br>• Run a local hub server<br>• The Standard JackTrip experience" :
+              "• Connect via IP address<br>• Run a local hub server<br>• The Classic JackTrip experience"
+        textFormat: Text.StyledText
+        font.family: "Poppins"
+        font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+        x: parent.width / 2 + (32 * virtualstudio.uiScale);
+        y: 355 * virtualstudio.uiScale;
+        width: 230 * virtualstudio.uiScale
+        padding: 0
+        wrapMode: Text.WordWrap
+        horizontalAlignment: Text.AlignHCenter
+        color: textColour
+    }
+}
diff --git a/src/gui/Footer.qml b/src/gui/Footer.qml
new file mode 100644 (file)
index 0000000..18f8292
--- /dev/null
@@ -0,0 +1,180 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Rectangle {
+    id: footer
+    width: parent.width
+    height: 24
+    anchors.bottom: parent.bottom
+    color: backgroundColour
+    clip: true
+
+    property string statsOrange: "#b26a00"
+    property string connectionStateColor: getConnectionStateColor()
+    property variant networkStatsText: getNetworkStatsText()
+
+    function getConnectionStateColor() {
+        if (virtualstudio.connectionState == "Connected") {
+            return meterGreen
+        }
+        if (virtualstudio.connectionState.includes("Disconnected") || virtualstudio.connectionState.includes("Error")) {
+            return meterRed
+        }
+        if (studioStatus === "Starting" || virtualstudio.connectionState == "Connecting..." || virtualstudio.connectionState == "Reconnecting...") {
+            return meterYellow
+        }
+        return "grey"
+    }
+
+    function getNetworkStatsText() {
+        let minRtt = virtualstudio.networkStats.minRtt;
+        let maxRtt = virtualstudio.networkStats.maxRtt;
+        let avgRtt = virtualstudio.networkStats.avgRtt;
+
+        let texts = ["Unstable", "Please plug into Ethernet & turn off WIFI.", meterRed];
+        if (virtualstudio.networkOutage) {
+            return texts;
+        }
+
+        texts = ["Measuring...", "", "grey"];
+        if (!minRtt || !maxRtt) {
+            return texts;
+        }
+
+        texts[1] = "<b>" + minRtt + " ms - " + maxRtt + " ms</b>, avg " + avgRtt + " ms";
+        let quality = "Poor";
+        let color = meterRed;
+        if (avgRtt < 10 && maxRtt < 15) {
+            quality = "Excellent";
+            color = meterGreen;
+        } else if (avgRtt < 20 && maxRtt < 30) {
+            quality = "Good";
+            color = meterYellow;
+        } else if (avgRtt < 30 && maxRtt < 40) {
+            quality = "Fair";
+            color = statsOrange;
+        }
+
+        texts[0] = quality
+        texts[2] = color;
+        return texts;
+    }
+
+    MouseArea {
+        anchors.fill: parent
+        propagateComposedEvents: false
+    }
+
+    RowLayout {
+        id: layout
+        anchors.fill: parent
+        spacing: 4
+
+        Rectangle {
+            color: backgroundColour
+            Layout.minimumWidth: 256
+            Layout.preferredWidth: 512
+            Layout.maximumWidth: 640
+            Layout.fillHeight: true
+            Layout.fillWidth: true
+            visible: studioStatus === "Ready"
+
+            AppIcon {
+                id: connectionQualityIcon
+                anchors.left: parent.left
+                anchors.leftMargin: 8 * virtualstudio.uiScale
+                anchors.verticalCenter: parent.verticalCenter
+                width: 20 * virtualstudio.uiScale
+                height: 20 * virtualstudio.uiScale
+                icon.source: "speed.svg"
+            }
+
+            Text {
+                id: connectionQualityText
+                anchors.left: connectionQualityIcon.right
+                anchors.leftMargin: 4 * virtualstudio.uiScale
+                anchors.verticalCenter: parent.verticalCenter
+                text: "Connection:"
+                font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: textColour
+            }
+
+            Text {
+                id: connectionQualityName
+                anchors.left: connectionQualityText.right
+                anchors.leftMargin: 2 * virtualstudio.uiScale
+                anchors.verticalCenter: parent.verticalCenter
+                text: networkStatsText[0]
+                font { family: "Poppins"; weight: Font.Bold; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: networkStatsText[2]
+            }
+
+            Text {
+                id: connectionQualityTime
+                anchors.left: connectionQualityName.right
+                anchors.leftMargin: 8 * virtualstudio.uiScale
+                anchors.verticalCenter: parent.verticalCenter
+                text: networkStatsText[1]
+                font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: textColour
+            }
+        }
+
+        Item {
+            Layout.fillWidth: true
+            Layout.fillHeight: true
+        }
+
+        Rectangle {
+            color: backgroundColour
+            Layout.minimumWidth: 96
+            Layout.preferredWidth: 128
+            Layout.maximumWidth: 160
+            Layout.fillHeight: true
+
+            Rectangle {
+                id: connectionStatusDot
+                anchors.right: connectionStatusText.left
+                anchors.rightMargin: 4 * virtualstudio.uiScale
+                anchors.verticalCenter: parent.verticalCenter
+                width: 12
+                height: connectionStatusDot.width
+                radius: connectionStatusDot.height / 2
+                color: connectionStateColor
+            }
+
+            Text {
+                id: connectionStatusText
+                anchors.right: parent.right
+                anchors.rightMargin: 8 * virtualstudio.uiScale
+                anchors.verticalCenter: parent.verticalCenter
+                text: studioStatus === "Starting" ? "Starting..." : virtualstudio.connectionState
+                font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: textColour
+            }
+        }
+    }
+
+    Rectangle {
+        id: backgroundBorder
+        width: parent.width
+        height: 1
+        y: parent.height - footer.height
+        color: buttonStroke
+    }
+
+    Connections {
+        target: virtualstudio
+
+        function onConnectionStateChanged() {
+            connectionStatusDot.color = getConnectionStateColor()
+        }
+        function onNetworkStatsChanged() {
+            networkStatsText = getNetworkStatsText();
+        }
+        function onUpdatedNetworkOutage() {
+            networkStatsText = getNetworkStatsText();
+        }
+    }
+}
diff --git a/src/gui/InfoTooltip.qml b/src/gui/InfoTooltip.qml
new file mode 100644 (file)
index 0000000..5d5c95e
--- /dev/null
@@ -0,0 +1,63 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+import Qt5Compat.GraphicalEffects
+
+Item {
+    required property string content
+    required property int size
+
+    width: size * virtualstudio.uiScale
+    height: size * virtualstudio.uiScale
+
+    property string iconSource: "help.svg"
+    property string iconColor: ""
+    property string backgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
+    property bool showToolTip: false
+
+    Item {
+        anchors.fill: parent
+
+        AppIcon {
+            id: tooltipIcon
+            anchors.centerIn: parent
+            width: parent.width
+            height: parent.height
+            icon.source: iconSource
+            color: iconColor
+        }
+
+        MouseArea {
+            id: mouseArea
+            anchors.fill: tooltipIcon
+            hoverEnabled: true
+            onEntered: showToolTip = true
+            onExited: showToolTip = false
+        }
+
+        ToolTip {
+            visible: showToolTip
+            x: tooltipIcon.x + tooltipIcon.width
+            y: tooltipIcon.y + tooltipIcon.height
+
+            contentItem: Text {
+                text: content
+                font { family: "Poppins"; pixelSize: fontTiny * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: textColour
+                elide: Text.ElideRight
+                wrapMode: Text.WordWrap
+            }
+
+            background: Rectangle {
+                color: backgroundColour
+                radius: 4
+                layer.enabled: true
+                layer.effect: Glow {
+                    radius: 8
+                    color: "#66000000"
+                    transparentBorder: true
+                }
+            }
+        }
+    }
+}
diff --git a/src/gui/JTOriginal.png b/src/gui/JTOriginal.png
new file mode 100644 (file)
index 0000000..a6c92cd
Binary files /dev/null and b/src/gui/JTOriginal.png differ
diff --git a/src/gui/JTVS.png b/src/gui/JTVS.png
new file mode 100644 (file)
index 0000000..05573f2
Binary files /dev/null and b/src/gui/JTVS.png differ
diff --git a/src/gui/LearnMoreButton.qml b/src/gui/LearnMoreButton.qml
new file mode 100644 (file)
index 0000000..aa26177
--- /dev/null
@@ -0,0 +1,30 @@
+import QtQuick
+import QtQuick.Controls
+
+Button {
+    property string url
+    property string buttonText: "Learn more"
+
+    width: 150 * virtualstudio.uiScale;
+    height: 30 * virtualstudio.uiScale
+
+    onClicked: {
+        virtualstudio.openLink(url);
+    }
+
+    background: Rectangle {
+        radius: 6 * virtualstudio.uiScale
+        color: parent.down ? buttonPressedColour : (parent.hovered ? buttonHoverColour : buttonColour)
+        border.width: 1
+        border.color: parent.down ? buttonPressedStroke : (parent.hovered ? buttonHoverStroke : buttonStroke)
+    }
+
+    Text {
+        text: buttonText
+        font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+        color: textColour
+        horizontalAlignment: Text.AlignHCenter
+        anchors.horizontalCenter: parent.horizontalCenter
+        anchors.verticalCenter: parent.verticalCenter
+    }
+}
diff --git a/src/gui/Login.qml b/src/gui/Login.qml
new file mode 100644 (file)
index 0000000..a11e1d5
--- /dev/null
@@ -0,0 +1,351 @@
+import QtQuick
+import QtQuick.Controls
+import VS 1.0
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    state: auth.authenticationStage
+    states: [
+        State {
+            name: "unauthenticated"
+        },
+        State {
+            name: "refreshing"
+        },
+        State {
+            name: "polling"
+        },
+        State {
+            name: "success"
+        },
+        State {
+            name: "failed"
+        }
+    ]
+
+    Rectangle {
+        width: parent.width; height: parent.height
+        color: backgroundColour
+    }
+
+    property bool codeCopied: false
+    property int numFailures: 0;
+
+    property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#FAFBFB" : "#F0F1F1"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#E9E9E9" : "#E4E5E5"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#FAFBFB" : "#E4E5E5"
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#6F6C6C" : "#B0B5B5"
+    property string buttonTextColour: virtualstudio.darkMode ? "#272525" : "#DB0A0A"
+    property string buttonTextHover: virtualstudio.darkMode ? "#242222" : "#D00A0A"
+    property string buttonTextPressed: virtualstudio.darkMode ? "#323030" : "#D00A0A"
+    property string shadowColour: virtualstudio.darkMode ? "40000000" : "#80A1A1A1"
+    property string linkTextColour: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+    property string toolTipTextColour: codeCopied ? "#FAFBFB" : textColour
+    property string toolTipBackgroundColour: codeCopied ? "#57B147" : (virtualstudio.darkMode ? "#323232" : "#F3F3F3")
+    property string tooltipStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+    property string disabledButtonText: "#D3D4D4"
+    property string errorTextColour: "#DB0A0A"
+
+    property bool showCodeFlow: (loginScreen.state === "unauthenticated" && !auth.attemptingRefreshToken) || (loginScreen.state === "polling" || loginScreen.state === "failed" || (loginScreen.state === "success" && auth.authenticationMethod === "code flow"))
+    property bool showLoading: (loginScreen.state === "unauthenticated" ** auth.attemptingRefreshToken) || loginScreen.state === "refreshing" || (loginScreen.state === "success" && auth.authenticationMethod === "refresh token")
+
+    Clipboard {
+        id: clipboard
+    }
+
+    Item {
+        id: loginScreenHeader
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: showCodeFlow ? 48 * virtualstudio.uiScale : 144 * virtualstudio.uiScale
+
+        Image {
+            id: loginLogo
+            source: "logo.svg"
+            x: parent.width / 2 - (150 * virtualstudio.uiScale);
+            width: 42 * virtualstudio.uiScale; height: 76 * virtualstudio.uiScale
+            sourceSize: Qt.size(loginLogo.width,loginLogo.height)
+            fillMode: Image.PreserveAspectFit
+            smooth: true
+        }
+
+        Image {
+            source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png"
+            anchors.bottom: loginLogo.bottom
+            x: parent.width / 2 - (88 * virtualstudio.uiScale)
+            width: 238 * virtualstudio.uiScale; height: 56 * virtualstudio.uiScale
+        }
+
+        Text {
+            text: "Virtual Studio"
+            font.family: "Poppins"
+            font.pixelSize: 24 * virtualstudio.fontScale * virtualstudio.uiScale
+            anchors.horizontalCenter: parent.horizontalCenter
+            y: 80 * virtualstudio.uiScale
+            color: textColour
+        }
+    }
+
+    Item {
+        id: codeFlow
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 68 * virtualstudio.uiScale
+        height: parent.height - codeFlow.y
+        visible: showCodeFlow
+        width: parent.width
+
+        Text {
+            id: deviceVerificationExplanation
+            text: `Please sign in and confirm the following code using your web browser. Return here when you are done.`
+            font.family: "Poppins"
+            font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+            anchors.horizontalCenter: parent.horizontalCenter
+            y: 128 * virtualstudio.uiScale
+            width: 500 * virtualstudio.uiScale;
+            visible: true
+            color: textColour
+            wrapMode: Text.WordWrap
+            horizontalAlignment: Text.AlignHCenter
+            textFormat: Text.RichText
+            onLinkActivated: link => {
+                if (!Boolean(auth.verificationCode)) {
+                    return;
+                }
+                virtualstudio.openLink(link)
+            }
+        }
+
+        AppIcon {
+            id: successIcon
+            y: 224 * virtualstudio.uiScale
+            anchors.horizontalCenter: parent.horizontalCenter
+            width: 96 * virtualstudio.uiScale
+            height: 96 * virtualstudio.uiScale
+            icon.source: "check.svg"
+            color: "green"
+            visible: loginScreen.state === "success"
+        }
+
+        Text {
+            id: deviceVerificationCode
+            text: auth.verificationCode || ((numFailures >= 5) ? "Error" : "Loading...");
+            font.family: "Poppins"
+            font.pixelSize: 20 * virtualstudio.fontScale * virtualstudio.uiScale
+            font.letterSpacing: Boolean(auth.verificationCode) ? 8 : 1
+            anchors.horizontalCenter: parent.horizontalCenter
+            y: 196 * virtualstudio.uiScale
+            width: 360 * virtualstudio.uiScale;
+            visible: !auth.isAuthenticated
+            color: Boolean(auth.verificationCode) ? textColour : disabledButtonText
+            wrapMode: Text.WordWrap
+            horizontalAlignment: Text.AlignHCenter
+
+            Timer {
+                id: copiedResetTimer
+                interval: 2000; running: false; repeat: false
+                onTriggered: codeCopied = false;
+            }
+
+            MouseArea {
+                id: deviceVerificationCodeMouseArea
+                anchors.fill: parent
+                cursorShape: Qt.PointingHandCursor
+                enabled: Boolean(auth.verificationCode)
+                hoverEnabled: true
+                onClicked: () => {
+                    codeCopied = true;
+                    clipboard.setText(auth.verificationCode);
+                    copiedResetTimer.restart()
+                }
+            }
+
+            ToolTip {
+                parent: deviceVerificationCode
+                visible: loginScreen.state === "polling" && deviceVerificationCodeMouseArea.containsMouse
+                delay: 100
+                contentItem: Rectangle {
+                    color: toolTipBackgroundColour
+                    radius: 3
+                    anchors.fill: parent
+                    layer.enabled: true
+                    border.width: 1
+                    border.color: tooltipStroke
+
+                    Text {
+                        anchors.centerIn: parent
+                        font { family: "Poppins"; pixelSize: 8 * virtualstudio.fontScale * virtualstudio.uiScale}
+                        text: codeCopied ? qsTr("📋 Copied code to clipboard") : qsTr("📋 Copy code to Clipboard")
+                        color: toolTipTextColour
+                    }
+                }
+                background: Rectangle {
+                    color: "transparent"
+                }
+            }
+        }
+
+        Button {
+            id: loginButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: loginButton.down ? buttonPressedColour : (loginButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: loginButton.down ? buttonPressedStroke : (loginButton.hovered ? buttonHoverStroke : buttonStroke)
+                layer.enabled: !loginButton.down
+            }
+            onClicked: {
+                if (auth.verificationCode && auth.verificationUrl) {
+                    virtualstudio.openLink(auth.verificationUrl);
+                }
+            }
+            anchors.horizontalCenter: parent.horizontalCenter
+            y: 260 * virtualstudio.uiScale
+            width: 263 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale
+            Text {
+                text: "Sign In"
+                font.family: "Poppins"
+                font.pixelSize: 18 * virtualstudio.fontScale * virtualstudio.uiScale
+                font.weight: Font.Bold
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+                color: loginButton.down ? buttonTextPressed : (loginButton.hovered ? buttonTextHover : buttonTextColour)
+            }
+            visible: !auth.isAuthenticated
+        }
+
+        Text {
+            id: authFailedText
+            text: "There was an error trying to sign in. Please try again."
+            font.family: "Poppins"
+            font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.bottom: loginScreenFooter.top
+            anchors.bottomMargin: 16 * virtualstudio.uiScale
+            visible: (loginScreen.state === "failed" || numFailures > 0) && loginScreen.state !== "success"
+            color: errorTextColour
+        }
+
+        Item {
+            id: loginScreenFooter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.bottom: parent.bottom
+            anchors.bottomMargin: 24 * virtualstudio.uiScale
+            width: parent.width
+            height: 48 * virtualstudio.uiScale
+
+            property bool showBackButton: !virtualstudio.vsFtux
+            property bool showClassicModeButton: virtualstudio.vsFtux
+
+            Item {
+                id: backButton
+                visible: parent.showBackButton
+                anchors.verticalCenter: parent.verticalCenter
+                x: (parent.x + parent.width / 2) - backButton.width - 8 * virtualstudio.uiScale
+                width: 144 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
+                Text {
+                    text: "Back"
+                    font.family: "Poppins"
+                    font.underline: true
+                    font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.verticalCenter: parent.verticalCenter
+                    color: textColour
+                }
+                MouseArea {
+                    anchors.fill: parent
+                    onClicked: () => { if (!auth.isAuthenticated) { virtualstudio.windowState = "start"; } }
+                    cursorShape: Qt.PointingHandCursor
+                }
+            }
+
+            Item {
+                id: classicModeButton
+                visible: parent.showClassicModeButton
+                anchors.verticalCenter: parent.verticalCenter
+                x: (parent.x + parent.width / 2) - classicModeButton.width - 8 * virtualstudio.uiScale
+                width: 160 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
+                Text {
+                    text: "Use Classic Mode"
+                    font.underline: true
+                    font.family: "Poppins"
+                    font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.verticalCenter: parent.verticalCenter
+                    color: textColour
+                }
+                MouseArea {
+                    anchors.fill: parent
+                    onClicked: () => { virtualstudio.windowState = "login"; virtualstudio.toStandard(); }
+                    cursorShape: Qt.PointingHandCursor
+                }
+            }
+
+            Item {
+                id: resetCodeButton
+                visible: true
+                x: (parent.showBackButton || parent.showClassicModeButton) ? (parent.x + parent.width / 2) + 8 * virtualstudio.uiScale : (parent.x + parent.width / 2) - resetCodeButton.width / 2
+                anchors.verticalCenter: parent.verticalCenter
+                width: 144 * virtualstudio.uiScale; height: 32 * virtualstudio.uiScale
+                Text {
+                    text: "Reset Code"
+                    font.family: "Poppins"
+                    font.underline: true
+                    font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.verticalCenter: parent.verticalCenter
+                    color: textColour
+                }
+                MouseArea {
+                    anchors.fill: parent
+                    onClicked: () => {
+                        if (auth.verificationCode && auth.verificationUrl) {
+                            auth.resetCode();
+                        }
+                    }
+                    cursorShape: Qt.PointingHandCursor
+                }
+            }
+        }
+    }
+
+    Item {
+        id: refreshToken
+        anchors.horizontalCenter: parent.horizontalCenter
+        y: 108 * virtualstudio.uiScale
+        visible: showLoading
+
+        Text {
+            id: loadingViaRefreshToken
+            text: "Logging In...";
+            font.family: "Poppins"
+            font.pixelSize: 20 * virtualstudio.fontScale * virtualstudio.uiScale
+            anchors.horizontalCenter: parent.horizontalCenter
+            y: 208 * virtualstudio.uiScale
+            width: 360 * virtualstudio.uiScale;
+            color: textColour
+            wrapMode: Text.WordWrap
+            horizontalAlignment: Text.AlignHCenter
+        }
+    }
+
+    Connections {
+        target: auth
+        function onUpdatedAuthenticationStage (stage) {
+            loginScreen.state = stage;
+            if (stage === "failed") {
+                numFailures = numFailures + 1;
+                if (numFailures < 5 && !virtualstudio.hasRefreshToken) {
+                    virtualstudio.login();
+                }
+            }
+            if (stage === "success") {
+                numFailures = 0;
+            }
+        }
+    }
+}
diff --git a/src/gui/Meter.qml b/src/gui/Meter.qml
new file mode 100644 (file)
index 0000000..292cef9
--- /dev/null
@@ -0,0 +1,53 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    required property var model
+    property int bins: Math.max(15, width/20)
+    property int innerMargin: 2 * virtualstudio.uiScale
+    property int boxRadius: 3 * virtualstudio.uiScale
+    property int boxThickness: 12
+    required property bool clipped
+    property bool enabled: true
+    property string meterColor: enabled ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4") : (virtualstudio.darkMode ? "#7b7979" : "#EAECEC")
+    property string meterRed: "#F21B1B"
+
+    Item {
+        id: meters
+        width: parent.width - boxThickness - innerMargin
+        height: parent.height
+
+        MeterBars {
+            id: leftchannel
+            x: 0
+            y: 0
+            width: parent.width
+            height: boxThickness
+            level: parent.parent.model[0]
+            enabled: parent.parent.enabled
+        }
+
+        MeterBars {
+            id: rightchannel
+            x: 0;
+            anchors.top: leftchannel.bottom
+            anchors.topMargin: innerMargin
+            width: parent.width
+            height: boxThickness
+            level: parent.parent.model[1]
+            enabled: parent.parent.enabled
+        }
+    }
+
+    Rectangle {
+        id: clipIndicator
+        y: 0
+        anchors.left: meters.right
+        anchors.leftMargin: innerMargin
+
+        width: boxThickness
+        height: leftchannel.height + rightchannel.height + innerMargin
+        radius: boxRadius
+        color: clipped ? meterRed : meterColor
+    }
+}
\ No newline at end of file
diff --git a/src/gui/MeterBars.qml b/src/gui/MeterBars.qml
new file mode 100644 (file)
index 0000000..cbdbf0f
--- /dev/null
@@ -0,0 +1,49 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Item {
+    required property var level
+    required property var enabled
+    property int boxHeight: height
+    property int boxWidth: (width / bins) - innerMargin
+    property string meterColor: enabled ? (virtualstudio.darkMode ? "#5B5858" : "#D3D4D4") : (virtualstudio.darkMode ? "#7b7979" : "#EAECEC")
+    property string meterGreen: "#61C554"
+    property string meterYellow: "#F5BF4F"
+    property string meterRed: "#F21B1B"
+
+    function getBoxColor (idx) {
+        // Case where the meter should not be filled
+        if (!enabled || level <= (idx / bins)) {
+            return meterColor;
+        }
+        // Case where the meter should be filled
+        let fillColor = meterGreen;
+        if (idx > 0.5*bins && idx <= 0.8*bins) {
+            fillColor = meterYellow;
+        } else if (idx > 0.8*bins) {
+            fillColor = meterRed;
+        }
+        return fillColor;
+    }
+
+    RowLayout {
+        anchors.fill: parent
+        spacing: innerMargin
+
+        Repeater {
+            model: bins
+            Rectangle {
+                Layout.fillHeight: true
+                Layout.fillWidth: true
+                x: (boxWidth) * index + innerMargin * index;
+                y: 0
+                z: 1
+                width: boxWidth
+                height: boxHeight
+                color: getBoxColor(index)
+                radius: boxRadius
+            }
+        }
+    }
+}
diff --git a/src/gui/NoNap.h b/src/gui/NoNap.h
new file mode 100644 (file)
index 0000000..0fe3eda
--- /dev/null
@@ -0,0 +1,45 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2020 Aaron Wyatt.
+
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#ifndef __NONAP_H__
+#define __NONAP_H__
+
+#include <objc/objc.h>
+
+class NoNap
+{
+   public:
+    NoNap();
+    ~NoNap();
+
+    void disableNap();
+    void enableNap();
+
+   private:
+    id m_activity;
+    bool m_preventNap;
+};
+
+#endif  // __NONAP_H__
diff --git a/src/gui/NoNap.mm b/src/gui/NoNap.mm
new file mode 100644 (file)
index 0000000..cb2edb2
--- /dev/null
@@ -0,0 +1,58 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2020 Aaron Wyatt.
+  
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#include "NoNap.h"
+#include <Foundation/Foundation.h>
+
+NoNap::NoNap() :
+    m_preventNap(false)
+{}
+
+void NoNap::disableNap()
+{
+    if (m_preventNap) {
+        return;
+    }
+    m_preventNap = true;
+    m_activity = [[NSProcessInfo processInfo] beginActivityWithOptions:NSActivityLatencyCritical | NSActivityUserInitiated reason:@"Disable App Nap"];
+    [m_activity retain];
+}
+
+void NoNap::enableNap()
+{
+    if (!m_preventNap) {
+        return;
+    }
+    m_preventNap = false;
+    [[NSProcessInfo processInfo] endActivity:m_activity];
+    [m_activity release];
+}
+
+NoNap::~NoNap()
+{
+    if (m_preventNap) {
+        enableNap();
+    }
+}
diff --git a/src/gui/Permissions.qml b/src/gui/Permissions.qml
new file mode 100644 (file)
index 0000000..da538c6
--- /dev/null
@@ -0,0 +1,202 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    property int fontBig: 20
+    property int fontMedium: 13
+    property int fontSmall: 11
+    property int fontExtraSmall: 8
+
+    property string saveButtonBackgroundColour: "#F2F3F3"
+    property string saveButtonStroke: "#EAEBEB"
+    property string saveButtonText: "#DB0A0A"
+
+    Item {
+        id: requestMicPermissionsItem
+        width: parent.width; height: parent.height
+        visible: permissions.micPermission == "unknown"
+
+        AppIcon {
+            id: microphonePrompt
+            y: 60
+            anchors.horizontalCenter: parent.horizontalCenter
+            width: 260
+            height: 250
+            icon.source: "Prompt.svg"
+        }
+
+        Image {
+            id: micLogo
+            source: "logo.svg"
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: microphonePrompt.top
+            anchors.topMargin: 18 * virtualstudio.uiScale
+            width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale
+            sourceSize: Qt.size(micLogo.width,micLogo.height)
+            fillMode: Image.PreserveAspectFit
+            smooth: true
+        }
+
+        Button {
+            id: showPromptButton
+            width: 112 * virtualstudio.uiScale
+            height: 30 * virtualstudio.uiScale
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: showPromptButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                border.width: 2
+                border.color: showPromptButton.down || showPromptButton.hovered ? saveButtonPressedStroke : saveButtonStroke
+                layer.enabled: showPromptButton.hovered && !showPromptButton.down
+            }
+            onClicked: {
+                permissions.getMicPermission();
+            }
+            anchors.right: microphonePrompt.right
+            anchors.rightMargin: 13.5 * virtualstudio.uiScale
+            anchors.bottomMargin: 17 * virtualstudio.uiScale
+            anchors.bottom: microphonePrompt.bottom
+            Text {
+                text: "OK"
+                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                font.weight: Font.Bold
+                color: saveButtonText
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+            }
+        }
+
+        Text {
+            id: micPermissionsHeader
+            text: "JackTrip needs your sounds!"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: microphonePrompt.bottom
+            anchors.topMargin: 48 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: micPermissionsSubheader1
+            text: "JackTrip requires permission to use your microphone."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 400
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: micPermissionsHeader.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: micPermissionsSubheader2
+            text: "Click ‘OK’ to give JackTrip access to your microphone, instrument, or other audio device."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 400
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: micPermissionsSubheader1.bottom
+            anchors.topMargin: 24 * virtualstudio.uiScale
+        }
+    }
+
+    Item {
+        id: noMicItem
+        width: parent.width; height: parent.height
+        visible: permissions.micPermission == "denied"
+
+        AppIcon {
+            id: noMic
+            y: 60
+            anchors.horizontalCenter: parent.horizontalCenter
+            width: 109.27
+            height: 170
+            icon.source: "micoff.svg"
+        }
+
+        Button {
+            id: openSettingsButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: openSettingsButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                border.width: 1
+                border.color: openSettingsButton.down || openSettingsButton.hovered ? saveButtonPressedStroke : saveButtonStroke
+                layer.enabled: openSettingsButton.hovered && !openSettingsButton.down
+            }
+            onClicked: {
+                permissions.openSystemPrivacy();
+            }
+            anchors.right: parent.right
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            anchors.bottomMargin: 16 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            width: 200 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            Text {
+                text: "Open Privacy Settings"
+                font.family: "Poppins"
+                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                font.weight: Font.Bold
+                color: saveButtonText
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+            }
+        }
+
+        Text {
+            id: noMicHeader
+            text: "JackTrip can't hear you!"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: noMic.bottom
+            anchors.topMargin: 48 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: noMicSubheader1
+            text: "JackTrip requires permission to use your microphone."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 400
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: noMicHeader.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: noMicSubheader2
+            text: "Click 'Open Privacy Settings' to give JackTrip permission to access your microphone, instrument, or other audio device."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 400
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: noMicSubheader1.bottom
+            anchors.topMargin: 24 * virtualstudio.uiScale
+        }
+    }
+
+    Connections {
+        target: permissions
+
+        function onMicPermissionUpdated() {
+            if (permissions.micPermission === "granted") {
+                if (virtualstudio.studioToJoin === "") {
+                    virtualstudio.windowState = "browse";
+                } else {
+                    virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
+                    virtualstudio.joinStudio();
+                }
+            }
+        }
+    }
+
+}
diff --git a/src/gui/Poppins-Bold.ttf b/src/gui/Poppins-Bold.ttf
new file mode 100644 (file)
index 0000000..00559ee
Binary files /dev/null and b/src/gui/Poppins-Bold.ttf differ
diff --git a/src/gui/Poppins-Regular.ttf b/src/gui/Poppins-Regular.ttf
new file mode 100644 (file)
index 0000000..9f0c71b
Binary files /dev/null and b/src/gui/Poppins-Regular.ttf differ
diff --git a/src/gui/Prompt.svg b/src/gui/Prompt.svg
new file mode 100644 (file)
index 0000000..110d116
--- /dev/null
@@ -0,0 +1,9 @@
+<svg width="260" height="250" viewBox="0 0 260 250" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect x="1" y="1" width="258" height="248" rx="9" stroke="#0F0D0D" stroke-width="2"/>
+<rect x="18" y="205" width="108" height="26" rx="5" stroke="#0F0D0D" stroke-width="2"/>
+<path d="M42.1189 222H44.9021C47.4861 222 48.9158 220.4 48.9158 217.775V217.764C48.9158 215.145 47.4802 213.545 44.9021 213.545H42.1189V222ZM42.9216 221.273V214.271H44.8552C46.9177 214.271 48.0955 215.59 48.0955 217.77V217.781C48.0955 219.961 46.9236 221.273 44.8552 221.273H42.9216ZM53.151 222.105C54.9147 222.105 56.0221 220.857 56.0221 218.848V218.836C56.0221 216.826 54.9147 215.584 53.151 215.584C51.3873 215.584 50.2857 216.826 50.2857 218.836V218.848C50.2857 220.857 51.3873 222.105 53.151 222.105ZM53.151 221.414C51.8502 221.414 51.0768 220.441 51.0768 218.848V218.836C51.0768 217.242 51.8502 216.275 53.151 216.275C54.4518 216.275 55.2252 217.242 55.2252 218.836V218.848C55.2252 220.441 54.4518 221.414 53.151 221.414ZM57.6323 222H58.4057V218.25C58.4057 217.061 59.1616 216.275 60.2749 216.275C61.3706 216.275 61.9155 216.891 61.9155 218.092V222H62.6889V217.898C62.6889 216.428 61.8862 215.584 60.4799 215.584C59.4956 215.584 58.8159 216.035 58.4819 216.797H58.4057V215.689H57.6323V222ZM64.598 216.662H65.1898L66.0335 213.545H65.1312L64.598 216.662ZM70.0871 222.041C70.2746 222.041 70.4503 222.023 70.6261 221.994V221.326C70.4679 221.344 70.3507 221.35 70.175 221.35C69.4601 221.35 69.1847 221.027 69.1847 220.254V216.34H70.6261V215.689H69.1847V214.049H68.382V215.689H67.3683V216.34H68.382V220.43C68.382 221.578 68.88 222.041 70.0871 222.041ZM74.8251 222H75.6688L76.6063 219.41H80.1629L81.1004 222H81.9442L78.7801 213.545H77.9833L74.8251 222ZM78.3465 214.594H78.4168L79.911 218.719H76.8524L78.3465 214.594ZM83.3845 222H84.1579V213.176H83.3845V222ZM86.2076 222H86.981V213.176H86.2076V222ZM91.5209 222.105C93.2846 222.105 94.392 220.857 94.392 218.848V218.836C94.392 216.826 93.2846 215.584 91.5209 215.584C89.7572 215.584 88.6557 216.826 88.6557 218.836V218.848C88.6557 220.857 89.7572 222.105 91.5209 222.105ZM91.5209 221.414C90.2201 221.414 89.4467 220.441 89.4467 218.848V218.836C89.4467 217.242 90.2201 216.275 91.5209 216.275C92.8217 216.275 93.5951 217.242 93.5951 218.836V218.848C93.5951 220.441 92.8217 221.414 91.5209 221.414ZM96.969 222H97.7776L99.3303 216.762H99.4065L100.959 222H101.774L103.573 215.689H102.799L101.387 221.045H101.317L99.758 215.689H98.9905L97.4319 221.045H97.3616L95.9612 215.689H95.1819L96.969 222Z" fill="#272525"/>
+<path d="M141.5 205H239.5C242.261 205 244.5 207.239 244.5 210V226C244.5 228.761 242.261 231 239.5 231H141.5C138.739 231 136.5 228.761 136.5 226V210C136.5 207.239 138.739 205 141.5 205Z" fill="white" stroke="#0F0D0D" stroke-width="2"/>
+<path d="M185.77 213.328C183.286 213.328 181.698 215.027 181.698 217.77C181.698 220.512 183.262 222.217 185.77 222.217C188.272 222.217 189.831 220.512 189.831 217.77C189.831 215.033 188.266 213.328 185.77 213.328ZM185.77 214.881C187.147 214.881 188.026 216 188.026 217.77C188.026 219.533 187.147 220.664 185.77 220.664C184.381 220.664 183.508 219.533 183.508 217.77C183.508 216 184.399 214.881 185.77 214.881ZM193.228 222V219.451L194.066 218.461L196.521 222H198.642L195.343 217.254L198.425 213.545H196.456L193.333 217.312H193.228V213.545H191.458V222H193.228Z" fill="#272525"/>
+<path d="M20.1581 104.84L18.9647 108.814H20.7801L21.5164 104.84H20.1581ZM23.0716 104.84L21.9735 108.814H23.7889L24.43 104.84H23.0716ZM24.6637 111.309C24.6637 113.105 25.8951 114.235 27.774 114.235C29.7164 114.235 30.8844 113.13 30.8844 111.156V104.84H28.9674V111.144C28.9674 112.045 28.5357 112.521 27.7486 112.521C27.0123 112.521 26.5299 112.045 26.5172 111.309H24.6637ZM35.3011 112.718C34.66 112.718 34.2093 112.4 34.2093 111.88C34.2093 111.378 34.5965 111.093 35.39 111.036L36.8055 110.947V111.461C36.8055 112.172 36.1581 112.718 35.3011 112.718ZM34.6917 114.108C35.5995 114.108 36.3612 113.727 36.723 113.086H36.8373V114H38.6273V109.22C38.6273 107.722 37.5799 106.846 35.7264 106.846C33.9681 106.846 32.7811 107.659 32.6605 108.954H34.3617C34.514 108.535 34.9583 108.306 35.6249 108.306C36.3866 108.306 36.8055 108.636 36.8055 109.22V109.792L35.1107 109.893C33.3714 109.995 32.4002 110.731 32.4002 112C32.4002 113.283 33.346 114.108 34.6917 114.108ZM46.586 109.468C46.4337 107.875 45.3229 106.846 43.4693 106.846C41.2794 106.846 40.0479 108.16 40.0479 110.483C40.0479 112.832 41.2857 114.152 43.4693 114.152C45.2911 114.152 46.4337 113.143 46.586 111.575H44.8595C44.7198 112.267 44.2247 112.635 43.4693 112.635C42.4791 112.635 41.9142 111.88 41.9142 110.483C41.9142 109.106 42.4728 108.363 43.4693 108.363C44.2564 108.363 44.7325 108.801 44.8595 109.468H46.586ZM50.0062 109.734H49.8919V104.326H48.0448V114H49.8919V111.677L50.3998 111.156L52.4437 114H54.6336L51.7835 110.001L54.4496 106.999H52.3485L50.0062 109.734ZM59.7169 114V106.478H62.4654V104.84H55.0514V106.478H57.7999V114H59.7169ZM63.1878 114H65.035V110.122C65.035 109.138 65.7269 108.541 66.7362 108.541C67.0345 108.541 67.4662 108.598 67.6122 108.655V106.973C67.4535 106.916 67.1424 106.884 66.8885 106.884C65.9999 106.884 65.2762 107.417 65.0921 108.116H64.9779V106.999H63.1878V114ZM68.8107 114H70.6578V106.999H68.8107V114ZM69.7374 106.021C70.3976 106.021 70.8292 105.634 70.8292 105.088C70.8292 104.536 70.3976 104.155 69.7374 104.155C69.0709 104.155 68.6456 104.536 68.6456 105.088C68.6456 105.634 69.0709 106.021 69.7374 106.021ZM76.5218 106.884C75.557 106.884 74.7635 107.36 74.3827 108.147H74.2684V106.999H72.4784V116.323H74.3255V112.927H74.4398C74.7826 113.67 75.5506 114.108 76.5536 114.108C78.3055 114.108 79.3783 112.756 79.3783 110.496C79.3783 108.23 78.2928 106.884 76.5218 106.884ZM75.8934 112.565C74.9159 112.565 74.3065 111.785 74.3065 110.502C74.3065 109.22 74.9159 108.433 75.8998 108.433C76.8837 108.433 77.4803 109.214 77.4803 110.496C77.4803 111.791 76.89 112.565 75.8934 112.565ZM82.2145 108.814L83.3063 104.84H81.4972L80.8497 108.814H82.2145ZM85.128 108.814L86.3151 104.84H84.506L83.7633 108.814H85.128ZM100.95 106.999H99.1158L98.0748 112.02H97.9606L96.7291 106.999H94.9645L93.7394 112.02H93.6251L92.5841 106.999H90.7115L92.5778 114H94.5201L95.7579 109.169H95.8722L97.1227 114H99.0904L100.95 106.999ZM105.031 114.152C107.182 114.152 108.477 112.788 108.477 110.496C108.477 108.224 107.163 106.846 105.031 106.846C102.898 106.846 101.584 108.23 101.584 110.496C101.584 112.788 102.879 114.152 105.031 114.152ZM105.031 112.642C104.04 112.642 103.482 111.861 103.482 110.496C103.482 109.15 104.047 108.357 105.031 108.357C106.008 108.357 106.579 109.15 106.579 110.496C106.579 111.854 106.015 112.642 105.031 112.642ZM116.271 106.999H114.424V111.036C114.424 111.969 113.929 112.546 113.008 112.546C112.158 112.546 111.72 112.051 111.72 111.086V106.999H109.873V111.562C109.873 113.188 110.812 114.152 112.323 114.152C113.383 114.152 114.037 113.689 114.367 112.876H114.481V114H116.271V106.999ZM118.136 114H119.983V104.326H118.136V114ZM124.362 114.108C125.333 114.108 126.127 113.657 126.501 112.902H126.615V114H128.405V104.326H126.558V108.116H126.45C126.089 107.341 125.308 106.884 124.362 106.884C122.604 106.884 121.512 108.262 121.512 110.49C121.512 112.724 122.597 114.108 124.362 114.108ZM124.99 108.433C125.974 108.433 126.577 109.227 126.577 110.502C126.577 111.785 125.981 112.565 124.99 112.565C124 112.565 123.41 111.791 123.41 110.496C123.41 109.214 124.006 108.433 124.99 108.433ZM133.633 114H135.481V104.326H133.633V114ZM137.365 114H139.212V106.999H137.365V114ZM138.291 106.021C138.952 106.021 139.383 105.634 139.383 105.088C139.383 104.536 138.952 104.155 138.291 104.155C137.625 104.155 137.2 104.536 137.2 105.088C137.2 105.634 137.625 106.021 138.291 106.021ZM143.057 109.734H142.943V104.326H141.096V114H142.943V111.677L143.451 111.156L145.495 114H147.685L144.835 110.001L147.501 106.999H145.4L143.057 109.734ZM151.41 108.262C152.273 108.262 152.831 108.839 152.87 109.766H149.886C149.95 108.858 150.553 108.262 151.41 108.262ZM152.908 112.007C152.711 112.47 152.209 112.73 151.479 112.73C150.515 112.73 149.905 112.083 149.88 111.042V110.947H154.685V110.382C154.685 108.16 153.466 106.846 151.403 106.846C149.321 106.846 148.039 108.255 148.039 110.534C148.039 112.807 149.296 114.152 151.429 114.152C153.142 114.152 154.349 113.327 154.628 112.007H152.908ZM159.976 105.335V107.068H158.885V108.522H159.976V112.108C159.976 113.473 160.649 114.025 162.35 114.025C162.706 114.025 163.049 113.987 163.277 113.943V112.534C163.1 112.553 162.973 112.559 162.731 112.559C162.103 112.559 161.824 112.28 161.824 111.67V108.522H163.277V107.068H161.824V105.335H159.976ZM167.821 114.152C169.973 114.152 171.268 112.788 171.268 110.496C171.268 108.224 169.954 106.846 167.821 106.846C165.688 106.846 164.374 108.23 164.374 110.496C164.374 112.788 165.669 114.152 167.821 114.152ZM167.821 112.642C166.831 112.642 166.272 111.861 166.272 110.496C166.272 109.15 166.837 108.357 167.821 108.357C168.798 108.357 169.37 109.15 169.37 110.496C169.37 111.854 168.805 112.642 167.821 112.642ZM178.552 112.718C177.911 112.718 177.461 112.4 177.461 111.88C177.461 111.378 177.848 111.093 178.641 111.036L180.057 110.947V111.461C180.057 112.172 179.409 112.718 178.552 112.718ZM177.943 114.108C178.851 114.108 179.612 113.727 179.974 113.086H180.089V114H181.879V109.22C181.879 107.722 180.831 106.846 178.978 106.846C177.219 106.846 176.032 107.659 175.912 108.954H177.613C177.765 108.535 178.21 108.306 178.876 108.306C179.638 108.306 180.057 108.636 180.057 109.22V109.792L178.362 109.893C176.623 109.995 175.652 110.731 175.652 112C175.652 113.283 176.597 114.108 177.943 114.108ZM189.837 109.468C189.685 107.875 188.574 106.846 186.721 106.846C184.531 106.846 183.299 108.16 183.299 110.483C183.299 112.832 184.537 114.152 186.721 114.152C188.542 114.152 189.685 113.143 189.837 111.575H188.111C187.971 112.267 187.476 112.635 186.721 112.635C185.73 112.635 185.165 111.88 185.165 110.483C185.165 109.106 185.724 108.363 186.721 108.363C187.508 108.363 187.984 108.801 188.111 109.468H189.837ZM197.428 109.468C197.276 107.875 196.165 106.846 194.311 106.846C192.121 106.846 190.89 108.16 190.89 110.483C190.89 112.832 192.128 114.152 194.311 114.152C196.133 114.152 197.276 113.143 197.428 111.575H195.701C195.562 112.267 195.067 112.635 194.311 112.635C193.321 112.635 192.756 111.88 192.756 110.483C192.756 109.106 193.315 108.363 194.311 108.363C195.098 108.363 195.574 108.801 195.701 109.468H197.428ZM201.851 108.262C202.714 108.262 203.273 108.839 203.311 109.766H200.328C200.391 108.858 200.994 108.262 201.851 108.262ZM203.349 112.007C203.152 112.47 202.651 112.73 201.921 112.73C200.956 112.73 200.347 112.083 200.321 111.042V110.947H205.126V110.382C205.126 108.16 203.908 106.846 201.845 106.846C199.763 106.846 198.48 108.255 198.48 110.534C198.48 112.807 199.737 114.152 201.87 114.152C203.584 114.152 204.79 113.327 205.069 112.007H203.349ZM206.433 109.055C206.433 110.134 207.093 110.782 208.451 111.08L209.721 111.366C210.337 111.499 210.603 111.708 210.603 112.051C210.603 112.502 210.108 112.8 209.391 112.8C208.654 112.8 208.204 112.527 208.064 112.051H206.261C206.388 113.397 207.506 114.152 209.353 114.152C211.187 114.152 212.438 113.238 212.438 111.842C212.438 110.794 211.828 110.223 210.47 109.925L209.156 109.639C208.508 109.493 208.21 109.284 208.21 108.941C208.21 108.497 208.699 108.198 209.359 108.198C210.045 108.198 210.47 108.478 210.565 108.928H212.273C212.171 107.589 211.124 106.846 209.346 106.846C207.601 106.846 206.433 107.729 206.433 109.055ZM213.617 109.055C213.617 110.134 214.277 110.782 215.636 111.08L216.905 111.366C217.521 111.499 217.788 111.708 217.788 112.051C217.788 112.502 217.292 112.8 216.575 112.8C215.839 112.8 215.388 112.527 215.248 112.051H213.446C213.573 113.397 214.69 114.152 216.537 114.152C218.371 114.152 219.622 113.238 219.622 111.842C219.622 110.794 219.013 110.223 217.654 109.925L216.34 109.639C215.693 109.493 215.394 109.284 215.394 108.941C215.394 108.497 215.883 108.198 216.543 108.198C217.229 108.198 217.654 108.478 217.749 108.928H219.457C219.355 107.589 218.308 106.846 216.531 106.846C214.785 106.846 213.617 107.729 213.617 109.055ZM224.913 105.335V107.068H223.822V108.522H224.913V112.108C224.913 113.473 225.586 114.025 227.288 114.025C227.643 114.025 227.986 113.987 228.214 113.943V112.534C228.037 112.553 227.91 112.559 227.668 112.559C227.04 112.559 226.761 112.28 226.761 111.67V108.522H228.214V107.068H226.761V105.335H224.913ZM229.768 114H231.615V109.982C231.615 109.074 232.149 108.458 233.075 108.458C233.913 108.458 234.364 108.96 234.364 109.931V114H236.211V109.493C236.211 107.811 235.316 106.865 233.812 106.865C232.783 106.865 232.022 107.348 231.698 108.147H231.584V104.326H229.768V114ZM240.983 108.262C241.847 108.262 242.405 108.839 242.443 109.766H239.46C239.523 108.858 240.126 108.262 240.983 108.262ZM242.481 112.007C242.285 112.47 241.783 112.73 241.053 112.73C240.088 112.73 239.479 112.083 239.454 111.042V110.947H244.259V110.382C244.259 108.16 243.04 106.846 240.977 106.846C238.895 106.846 237.613 108.255 237.613 110.534C237.613 112.807 238.87 114.152 241.002 114.152C242.716 114.152 243.922 113.327 244.202 112.007H242.481ZM91.5539 130H93.4011V125.785C93.4011 125.004 93.9153 124.439 94.6389 124.439C95.3625 124.439 95.7942 124.865 95.7942 125.607V130H97.5715V125.715C97.5715 124.973 98.0539 124.439 98.803 124.439C99.5837 124.439 99.9709 124.852 99.9709 125.684V130H101.818V125.195C101.818 123.754 100.948 122.846 99.552 122.846C98.5744 122.846 97.7683 123.36 97.4446 124.141H97.3303C97.051 123.329 96.3782 122.846 95.3943 122.846C94.4739 122.846 93.7439 123.341 93.4582 124.141H93.344V122.999H91.5539V130ZM103.575 130H105.422V122.999H103.575V130ZM104.502 122.021C105.162 122.021 105.594 121.634 105.594 121.088C105.594 120.536 105.162 120.155 104.502 120.155C103.835 120.155 103.41 120.536 103.41 121.088C103.41 121.634 103.835 122.021 104.502 122.021ZM113.438 125.468C113.286 123.875 112.175 122.846 110.322 122.846C108.132 122.846 106.9 124.16 106.9 126.483C106.9 128.832 108.138 130.152 110.322 130.152C112.143 130.152 113.286 129.143 113.438 127.575H111.712C111.572 128.267 111.077 128.635 110.322 128.635C109.331 128.635 108.766 127.88 108.766 126.483C108.766 125.106 109.325 124.363 110.322 124.363C111.109 124.363 111.585 124.801 111.712 125.468H113.438ZM114.833 130H116.681V126.122C116.681 125.138 117.373 124.541 118.382 124.541C118.68 124.541 119.112 124.598 119.258 124.655V122.973C119.099 122.916 118.788 122.884 118.534 122.884C117.646 122.884 116.922 123.417 116.738 124.116H116.624V122.999H114.833V130ZM123.383 130.152C125.534 130.152 126.829 128.788 126.829 126.496C126.829 124.224 125.515 122.846 123.383 122.846C121.25 122.846 119.936 124.23 119.936 126.496C119.936 128.788 121.231 130.152 123.383 130.152ZM123.383 128.642C122.392 128.642 121.834 127.861 121.834 126.496C121.834 125.15 122.399 124.357 123.383 124.357C124.36 124.357 124.931 125.15 124.931 126.496C124.931 127.854 124.366 128.642 123.383 128.642ZM132.332 122.884C131.367 122.884 130.573 123.36 130.192 124.147H130.078V122.999H128.288V132.323H130.135V128.927H130.25C130.592 129.67 131.36 130.108 132.363 130.108C134.115 130.108 135.188 128.756 135.188 126.496C135.188 124.23 134.103 122.884 132.332 122.884ZM131.703 128.565C130.726 128.565 130.116 127.785 130.116 126.502C130.116 125.22 130.726 124.433 131.709 124.433C132.693 124.433 133.29 125.214 133.29 126.496C133.29 127.791 132.7 128.565 131.703 128.565ZM136.704 130H138.551V125.982C138.551 125.074 139.084 124.458 140.011 124.458C140.849 124.458 141.3 124.96 141.3 125.931V130H143.147V125.493C143.147 123.811 142.252 122.865 140.747 122.865C139.719 122.865 138.957 123.348 138.634 124.147H138.519V120.326H136.704V130ZM147.995 130.152C150.147 130.152 151.442 128.788 151.442 126.496C151.442 124.224 150.128 122.846 147.995 122.846C145.862 122.846 144.548 124.23 144.548 126.496C144.548 128.788 145.843 130.152 147.995 130.152ZM147.995 128.642C147.005 128.642 146.446 127.861 146.446 126.496C146.446 125.15 147.011 124.357 147.995 124.357C148.973 124.357 149.544 125.15 149.544 126.496C149.544 127.854 148.979 128.642 147.995 128.642ZM152.901 130H154.748V125.963C154.748 125.042 155.275 124.439 156.132 124.439C157.008 124.439 157.42 124.947 157.42 125.912V130H159.267V125.474C159.267 123.798 158.429 122.846 156.874 122.846C155.84 122.846 155.129 123.335 154.805 124.122H154.691V122.999H152.901V130ZM164.04 124.262C164.903 124.262 165.461 124.839 165.5 125.766H162.516C162.58 124.858 163.183 124.262 164.04 124.262ZM165.538 128.007C165.341 128.47 164.839 128.73 164.109 128.73C163.145 128.73 162.535 128.083 162.51 127.042V126.947H167.315V126.382C167.315 124.16 166.096 122.846 164.033 122.846C161.951 122.846 160.669 124.255 160.669 126.534C160.669 128.807 161.926 130.152 164.059 130.152C165.772 130.152 166.979 129.327 167.258 128.007H165.538ZM170.113 130.171C170.799 130.171 171.237 129.708 171.237 129.067C171.237 128.426 170.799 127.969 170.113 127.969C169.434 127.969 168.99 128.426 168.99 129.067C168.99 129.708 169.434 130.171 170.113 130.171Z" fill="#272525"/>
+<path d="M23.7968 160V152.494H26.5214V151.545H20.0175V152.494H22.7421V160H23.7968ZM28.032 160H29.0398V156.262C29.0398 155.195 29.6609 154.48 30.7917 154.48C31.7468 154.48 32.2507 155.037 32.2507 156.156V160H33.2585V155.91C33.2585 154.428 32.4148 153.572 31.0788 153.572C30.112 153.572 29.4499 153.982 29.1335 154.68H29.0398V151.176H28.032V160ZM35.0738 160H36.0816V153.684H35.0738V160ZM35.5777 152.4C35.9644 152.4 36.2808 152.084 36.2808 151.697C36.2808 151.311 35.9644 150.994 35.5777 150.994C35.191 150.994 34.8746 151.311 34.8746 151.697C34.8746 152.084 35.191 152.4 35.5777 152.4ZM37.809 155.412C37.809 156.326 38.3481 156.836 39.5317 157.123L40.6157 157.387C41.2895 157.551 41.6176 157.844 41.6176 158.277C41.6176 158.857 41.0082 159.262 40.1586 159.262C39.35 159.262 38.8461 158.922 38.6762 158.389H37.6391C37.7504 159.438 38.7172 160.111 40.1235 160.111C41.559 160.111 42.6547 159.332 42.6547 158.201C42.6547 157.293 42.0805 156.777 40.8911 156.49L39.9184 156.256C39.1743 156.074 38.8227 155.805 38.8227 155.371C38.8227 154.809 39.4086 154.428 40.1586 154.428C40.9203 154.428 41.4125 154.762 41.5473 155.266H42.5434C42.4086 154.229 41.4887 153.572 40.1645 153.572C38.8227 153.572 37.809 154.363 37.809 155.412ZM49.4728 159.227C48.7404 159.227 48.1954 158.852 48.1954 158.207C48.1954 157.574 48.6173 157.24 49.5783 157.176L51.2775 157.064V157.645C51.2775 158.547 50.5099 159.227 49.4728 159.227ZM49.2853 160.111C50.129 160.111 50.8204 159.742 51.2306 159.068H51.3243V160H52.2853V155.676C52.2853 154.363 51.424 153.572 49.8829 153.572C48.5353 153.572 47.5392 154.24 47.4044 155.254H48.424C48.5646 154.756 49.0919 154.469 49.8478 154.469C50.7911 154.469 51.2775 154.896 51.2775 155.676V156.25L49.4552 156.361C47.9845 156.449 47.1525 157.1 47.1525 158.23C47.1525 159.385 48.0607 160.111 49.2853 160.111ZM57.1767 153.572C56.3154 153.572 55.5596 154.012 55.1553 154.738H55.0615V153.684H54.1006V162.109H55.1084V159.051H55.2021C55.5478 159.719 56.2744 160.111 57.1767 160.111C58.7822 160.111 59.831 158.816 59.831 156.842C59.831 154.855 58.7881 153.572 57.1767 153.572ZM56.9365 159.203C55.7998 159.203 55.0791 158.289 55.0791 156.842C55.0791 155.389 55.7998 154.48 56.9424 154.48C58.0967 154.48 58.7881 155.365 58.7881 156.842C58.7881 158.318 58.0967 159.203 56.9365 159.203ZM64.4412 153.572C63.5799 153.572 62.8241 154.012 62.4198 154.738H62.326V153.684H61.3651V162.109H62.3729V159.051H62.4666C62.8123 159.719 63.5389 160.111 64.4412 160.111C66.0467 160.111 67.0955 158.816 67.0955 156.842C67.0955 154.855 66.0526 153.572 64.4412 153.572ZM64.201 159.203C63.0643 159.203 62.3436 158.289 62.3436 156.842C62.3436 155.389 63.0643 154.48 64.2069 154.48C65.3612 154.48 66.0526 155.365 66.0526 156.842C66.0526 158.318 65.3612 159.203 64.201 159.203ZM71.9683 160H72.9761V156.086C72.9761 155.195 73.6734 154.551 74.6343 154.551C74.8335 154.551 75.1968 154.586 75.2788 154.609V153.602C75.1499 153.584 74.939 153.572 74.7749 153.572C73.937 153.572 73.2105 154.006 73.023 154.621H72.9292V153.684H71.9683V160ZM78.8343 154.463C79.8363 154.463 80.5043 155.201 80.5277 156.32H77.0472C77.1234 155.201 77.8265 154.463 78.8343 154.463ZM80.4984 158.365C80.2347 158.922 79.684 159.221 78.8695 159.221C77.7972 159.221 77.1 158.43 77.0472 157.182V157.135H81.5883V156.748C81.5883 154.785 80.5511 153.572 78.8461 153.572C77.1117 153.572 75.9984 154.861 75.9984 156.848C75.9984 158.846 77.0941 160.111 78.8461 160.111C80.2289 160.111 81.2015 159.449 81.5062 158.365H80.4984ZM85.4485 153.572C83.8488 153.572 82.8059 154.861 82.8059 156.842C82.8059 158.834 83.8371 160.111 85.4485 160.111C86.3391 160.111 87.0188 159.742 87.4231 159.051H87.5168V162.109H88.5363V153.684H87.5637V154.738H87.4699C87.0949 154.029 86.3098 153.572 85.4485 153.572ZM85.677 159.203C84.5285 159.203 83.8488 158.324 83.8488 156.842C83.8488 155.365 84.5344 154.48 85.6828 154.48C86.8254 154.48 87.5461 155.395 87.5461 156.842C87.5461 158.295 86.8313 159.203 85.677 159.203ZM95.4962 153.684H94.4883V157.422C94.4883 158.529 93.879 159.191 92.7657 159.191C91.7579 159.191 91.336 158.664 91.336 157.527V153.684H90.3282V157.773C90.3282 159.268 91.0665 160.111 92.4844 160.111C93.4512 160.111 94.1251 159.713 94.4415 159.01H94.5352V160H95.4962V153.684ZM97.37 160H98.3778V153.684H97.37V160ZM97.8739 152.4C98.2607 152.4 98.5771 152.084 98.5771 151.697C98.5771 151.311 98.2607 150.994 97.8739 150.994C97.4872 150.994 97.1708 151.311 97.1708 151.697C97.1708 152.084 97.4872 152.4 97.8739 152.4ZM100.252 160H101.26V156.086C101.26 155.195 101.957 154.551 102.918 154.551C103.117 154.551 103.48 154.586 103.562 154.609V153.602C103.433 153.584 103.222 153.572 103.058 153.572C102.22 153.572 101.494 154.006 101.306 154.621H101.213V153.684H100.252V160ZM107.118 154.463C108.12 154.463 108.788 155.201 108.811 156.32H105.331C105.407 155.201 106.11 154.463 107.118 154.463ZM108.782 158.365C108.518 158.922 107.967 159.221 107.153 159.221C106.081 159.221 105.383 158.43 105.331 157.182V157.135H109.872V156.748C109.872 154.785 108.835 153.572 107.13 153.572C105.395 153.572 104.282 154.861 104.282 156.848C104.282 158.846 105.378 160.111 107.13 160.111C108.512 160.111 109.485 159.449 109.79 158.365H108.782ZM111.259 155.412C111.259 156.326 111.798 156.836 112.982 157.123L114.066 157.387C114.74 157.551 115.068 157.844 115.068 158.277C115.068 158.857 114.458 159.262 113.609 159.262C112.8 159.262 112.296 158.922 112.126 158.389H111.089C111.201 159.438 112.167 160.111 113.574 160.111C115.009 160.111 116.105 159.332 116.105 158.201C116.105 157.293 115.531 156.777 114.341 156.49L113.369 156.256C112.624 156.074 112.273 155.805 112.273 155.371C112.273 154.809 112.859 154.428 113.609 154.428C114.371 154.428 114.863 154.762 114.998 155.266H115.994C115.859 154.229 114.939 153.572 113.615 153.572C112.273 153.572 111.259 154.363 111.259 155.412ZM120.978 160H121.986V156.086C121.986 155.195 122.624 154.48 123.45 154.48C124.247 154.48 124.769 154.961 124.769 155.711V160H125.777V155.939C125.777 155.137 126.362 154.48 127.241 154.48C128.132 154.48 128.571 154.938 128.571 155.869V160H129.579V155.635C129.579 154.311 128.859 153.572 127.569 153.572C126.696 153.572 125.976 154.012 125.636 154.68H125.542C125.249 154.023 124.652 153.572 123.796 153.572C122.952 153.572 122.319 153.977 122.032 154.68H121.939V153.684H120.978V160ZM131.395 160H132.402V153.684H131.395V160ZM131.898 152.4C132.285 152.4 132.602 152.084 132.602 151.697C132.602 151.311 132.285 150.994 131.898 150.994C131.512 150.994 131.195 151.311 131.195 151.697C131.195 152.084 131.512 152.4 131.898 152.4ZM139.503 155.617C139.327 154.492 138.39 153.572 136.854 153.572C135.085 153.572 133.96 154.85 133.96 156.818C133.96 158.828 135.091 160.111 136.86 160.111C138.378 160.111 139.321 159.256 139.503 158.096H138.483C138.296 158.811 137.704 159.203 136.854 159.203C135.729 159.203 135.003 158.277 135.003 156.818C135.003 155.389 135.718 154.48 136.854 154.48C137.763 154.48 138.319 154.99 138.483 155.617H139.503ZM140.943 160H141.951V156.086C141.951 155.195 142.648 154.551 143.609 154.551C143.808 154.551 144.172 154.586 144.254 154.609V153.602C144.125 153.584 143.914 153.572 143.75 153.572C142.912 153.572 142.185 154.006 141.998 154.621H141.904V153.684H140.943V160ZM147.885 160.111C149.684 160.111 150.797 158.869 150.797 156.842C150.797 154.809 149.684 153.572 147.885 153.572C146.086 153.572 144.973 154.809 144.973 156.842C144.973 158.869 146.086 160.111 147.885 160.111ZM147.885 159.203C146.69 159.203 146.016 158.336 146.016 156.842C146.016 155.342 146.69 154.48 147.885 154.48C149.081 154.48 149.754 155.342 149.754 156.842C149.754 158.336 149.081 159.203 147.885 159.203ZM155.408 153.572C154.546 153.572 153.79 154.012 153.386 154.738H153.292V153.684H152.331V162.109H153.339V159.051H153.433C153.779 159.719 154.505 160.111 155.408 160.111C157.013 160.111 158.062 158.816 158.062 156.842C158.062 154.855 157.019 153.572 155.408 153.572ZM155.167 159.203C154.031 159.203 153.31 158.289 153.31 156.842C153.31 155.389 154.031 154.48 155.173 154.48C156.328 154.48 157.019 155.365 157.019 156.842C157.019 158.318 156.328 159.203 155.167 159.203ZM159.655 160H160.662V156.262C160.662 155.195 161.283 154.48 162.414 154.48C163.369 154.48 163.873 155.037 163.873 156.156V160H164.881V155.91C164.881 154.428 164.037 153.572 162.701 153.572C161.735 153.572 161.073 153.982 160.756 154.68H160.662V151.176H159.655V160ZM169.269 160.111C171.067 160.111 172.181 158.869 172.181 156.842C172.181 154.809 171.067 153.572 169.269 153.572C167.47 153.572 166.357 154.809 166.357 156.842C166.357 158.869 167.47 160.111 169.269 160.111ZM169.269 159.203C168.073 159.203 167.4 158.336 167.4 156.842C167.4 155.342 168.073 154.48 169.269 154.48C170.464 154.48 171.138 155.342 171.138 156.842C171.138 158.336 170.464 159.203 169.269 159.203ZM173.715 160H174.723V156.262C174.723 155.154 175.373 154.48 176.381 154.48C177.389 154.48 177.869 155.02 177.869 156.156V160H178.877V155.91C178.877 154.41 178.086 153.572 176.668 153.572C175.701 153.572 175.086 153.982 174.769 154.68H174.676V153.684H173.715V160ZM183.194 154.463C184.196 154.463 184.864 155.201 184.887 156.32H181.407C181.483 155.201 182.186 154.463 183.194 154.463ZM184.858 158.365C184.595 158.922 184.044 159.221 183.229 159.221C182.157 159.221 181.46 158.43 181.407 157.182V157.135H185.948V156.748C185.948 154.785 184.911 153.572 183.206 153.572C181.471 153.572 180.358 154.861 180.358 156.848C180.358 158.846 181.454 160.111 183.206 160.111C184.589 160.111 185.561 159.449 185.866 158.365H184.858ZM192.766 159.227C192.034 159.227 191.489 158.852 191.489 158.207C191.489 157.574 191.911 157.24 192.872 157.176L194.571 157.064V157.645C194.571 158.547 193.803 159.227 192.766 159.227ZM192.579 160.111C193.422 160.111 194.114 159.742 194.524 159.068H194.618V160H195.579V155.676C195.579 154.363 194.717 153.572 193.176 153.572C191.829 153.572 190.833 154.24 190.698 155.254H191.717C191.858 154.756 192.385 154.469 193.141 154.469C194.084 154.469 194.571 154.896 194.571 155.676V156.25L192.749 156.361C191.278 156.449 190.446 157.1 190.446 158.23C190.446 159.385 191.354 160.111 192.579 160.111ZM202.62 155.617C202.445 154.492 201.507 153.572 199.972 153.572C198.202 153.572 197.077 154.85 197.077 156.818C197.077 158.828 198.208 160.111 199.978 160.111C201.495 160.111 202.439 159.256 202.62 158.096H201.601C201.413 158.811 200.822 159.203 199.972 159.203C198.847 159.203 198.12 158.277 198.12 156.818C198.12 155.389 198.835 154.48 199.972 154.48C200.88 154.48 201.437 154.99 201.601 155.617H202.62ZM209.287 155.617C209.111 154.492 208.174 153.572 206.639 153.572C204.869 153.572 203.744 154.85 203.744 156.818C203.744 158.828 204.875 160.111 206.645 160.111C208.162 160.111 209.106 159.256 209.287 158.096H208.268C208.08 158.811 207.488 159.203 206.639 159.203C205.514 159.203 204.787 158.277 204.787 156.818C204.787 155.389 205.502 154.48 206.639 154.48C207.547 154.48 208.104 154.99 208.268 155.617H209.287ZM213.247 154.463C214.249 154.463 214.917 155.201 214.94 156.32H211.46C211.536 155.201 212.239 154.463 213.247 154.463ZM214.911 158.365C214.647 158.922 214.097 159.221 213.282 159.221C212.21 159.221 211.513 158.43 211.46 157.182V157.135H216.001V156.748C216.001 154.785 214.964 153.572 213.259 153.572C211.524 153.572 210.411 154.861 210.411 156.848C210.411 158.846 211.507 160.111 213.259 160.111C214.642 160.111 215.614 159.449 215.919 158.365H214.911ZM217.389 155.412C217.389 156.326 217.928 156.836 219.111 157.123L220.195 157.387C220.869 157.551 221.197 157.844 221.197 158.277C221.197 158.857 220.588 159.262 219.738 159.262C218.93 159.262 218.426 158.922 218.256 158.389H217.219C217.33 159.438 218.297 160.111 219.703 160.111C221.139 160.111 222.234 159.332 222.234 158.201C222.234 157.293 221.66 156.777 220.471 156.49L219.498 156.256C218.754 156.074 218.402 155.805 218.402 155.371C218.402 154.809 218.988 154.428 219.738 154.428C220.5 154.428 220.992 154.762 221.127 155.266H222.123C221.988 154.229 221.068 153.572 219.744 153.572C218.402 153.572 217.389 154.363 217.389 155.412ZM223.505 155.412C223.505 156.326 224.044 156.836 225.227 157.123L226.311 157.387C226.985 157.551 227.313 157.844 227.313 158.277C227.313 158.857 226.704 159.262 225.854 159.262C225.046 159.262 224.542 158.922 224.372 158.389H223.335C223.446 159.438 224.413 160.111 225.819 160.111C227.255 160.111 228.35 159.332 228.35 158.201C228.35 157.293 227.776 156.777 226.587 156.49L225.614 156.256C224.87 156.074 224.518 155.805 224.518 155.371C224.518 154.809 225.104 154.428 225.854 154.428C226.616 154.428 227.108 154.762 227.243 155.266H228.239C228.104 154.229 227.184 153.572 225.86 153.572C224.518 153.572 223.505 154.363 223.505 155.412ZM233.645 152.049V153.684H232.625V154.527H233.645V158.359C233.645 159.566 234.166 160.047 235.467 160.047C235.666 160.047 235.86 160.023 236.059 159.988V159.139C235.872 159.156 235.772 159.162 235.59 159.162C234.934 159.162 234.653 158.846 234.653 158.102V154.527H236.059V153.684H234.653V152.049H233.645ZM240.036 160.111C241.835 160.111 242.949 158.869 242.949 156.842C242.949 154.809 241.835 153.572 240.036 153.572C238.238 153.572 237.124 154.809 237.124 156.842C237.124 158.869 238.238 160.111 240.036 160.111ZM240.036 159.203C238.841 159.203 238.167 158.336 238.167 156.842C238.167 155.342 238.841 154.48 240.036 154.48C241.232 154.48 241.906 155.342 241.906 156.842C241.906 158.336 241.232 159.203 240.036 159.203ZM27.91 173.227C27.1776 173.227 26.6327 172.852 26.6327 172.207C26.6327 171.574 27.0546 171.24 28.0155 171.176L29.7147 171.064V171.645C29.7147 172.547 28.9472 173.227 27.91 173.227ZM27.7225 174.111C28.5663 174.111 29.2577 173.742 29.6679 173.068H29.7616V174H30.7225V169.676C30.7225 168.363 29.8612 167.572 28.3202 167.572C26.9725 167.572 25.9765 168.24 25.8417 169.254H26.8612C27.0018 168.756 27.5292 168.469 28.285 168.469C29.2284 168.469 29.7147 168.896 29.7147 169.676V170.25L27.8925 170.361C26.4218 170.449 25.5897 171.1 25.5897 172.23C25.5897 173.385 26.4979 174.111 27.7225 174.111ZM32.5964 174H33.6042V165.176H32.5964V174ZM35.5719 174H36.5797V165.176H35.5719V174ZM41.0844 174.111C42.8832 174.111 43.9965 172.869 43.9965 170.842C43.9965 168.809 42.8832 167.572 41.0844 167.572C39.2856 167.572 38.1723 168.809 38.1723 170.842C38.1723 172.869 39.2856 174.111 41.0844 174.111ZM41.0844 173.203C39.8891 173.203 39.2153 172.336 39.2153 170.842C39.2153 169.342 39.8891 168.48 41.0844 168.48C42.2797 168.48 42.9535 169.342 42.9535 170.842C42.9535 172.336 42.2797 173.203 41.0844 173.203ZM53.2415 167.684H52.2278L50.9856 172.734H50.8919L49.4798 167.684H48.513L47.1009 172.734H47.0071L45.7649 167.684H44.7454L46.5149 174H47.5345L48.9407 169.113H49.0345L50.4466 174H51.472L53.2415 167.684ZM58.3017 166.049V167.684H57.2822V168.527H58.3017V172.359C58.3017 173.566 58.8232 174.047 60.124 174.047C60.3232 174.047 60.5166 174.023 60.7158 173.988V173.139C60.5283 173.156 60.4287 173.162 60.247 173.162C59.5908 173.162 59.3095 172.846 59.3095 172.102V168.527H60.7158V167.684H59.3095V166.049H58.3017ZM62.2498 174H63.2576V170.262C63.2576 169.195 63.8787 168.48 65.0096 168.48C65.9647 168.48 66.4686 169.037 66.4686 170.156V174H67.4764V169.91C67.4764 168.428 66.6326 167.572 65.2967 167.572C64.3299 167.572 63.6678 167.982 63.3514 168.68H63.2576V165.176H62.2498V174ZM71.7878 168.463C72.7897 168.463 73.4577 169.201 73.4811 170.32H70.0007C70.0768 169.201 70.78 168.463 71.7878 168.463ZM73.4518 172.365C73.1882 172.922 72.6374 173.221 71.8229 173.221C70.7507 173.221 70.0534 172.43 70.0007 171.182V171.135H74.5417V170.748C74.5417 168.785 73.5046 167.572 71.7995 167.572C70.0651 167.572 68.9518 168.861 68.9518 170.848C68.9518 172.846 70.0475 174.111 71.7995 174.111C73.1823 174.111 74.155 173.449 74.4596 172.365H73.4518ZM80.4457 167.684H79.4379V174.328C79.4379 175.025 79.2035 175.307 78.4886 175.307H78.3363V176.168H78.5121C79.8363 176.168 80.4457 175.611 80.4457 174.311V167.684ZM79.9418 166.4C80.3285 166.4 80.6449 166.084 80.6449 165.697C80.6449 165.311 80.3285 164.994 79.9418 164.994C79.555 164.994 79.2386 165.311 79.2386 165.697C79.2386 166.084 79.555 166.4 79.9418 166.4ZM84.2649 173.227C83.5324 173.227 82.9875 172.852 82.9875 172.207C82.9875 171.574 83.4094 171.24 84.3703 171.176L86.0695 171.064V171.645C86.0695 172.547 85.302 173.227 84.2649 173.227ZM84.0774 174.111C84.9211 174.111 85.6125 173.742 86.0227 173.068H86.1164V174H87.0774V169.676C87.0774 168.363 86.216 167.572 84.675 167.572C83.3274 167.572 82.3313 168.24 82.1965 169.254H83.216C83.3567 168.756 83.884 168.469 84.6399 168.469C85.5832 168.469 86.0695 168.896 86.0695 169.676V170.25L84.2473 170.361C82.7766 170.449 81.9445 171.1 81.9445 172.23C81.9445 173.385 82.8528 174.111 84.0774 174.111ZM94.1192 169.617C93.9434 168.492 93.0059 167.572 91.4708 167.572C89.7012 167.572 88.5762 168.85 88.5762 170.818C88.5762 172.828 89.7071 174.111 91.4766 174.111C92.9942 174.111 93.9376 173.256 94.1192 172.096H93.0997C92.9122 172.811 92.3204 173.203 91.4708 173.203C90.3458 173.203 89.6192 172.277 89.6192 170.818C89.6192 169.389 90.334 168.48 91.4708 168.48C92.379 168.48 92.9356 168.99 93.0997 169.617H94.1192ZM96.7196 170.443H96.6259V165.176H95.6181V174H96.6259V171.604L97.2294 171.041L99.5966 174H100.88L97.9794 170.391L100.698 167.684H99.4618L96.7196 170.443ZM105.219 169.412C105.219 170.326 105.758 170.836 106.942 171.123L108.026 171.387C108.7 171.551 109.028 171.844 109.028 172.277C109.028 172.857 108.419 173.262 107.569 173.262C106.76 173.262 106.256 172.922 106.087 172.389H105.049C105.161 173.438 106.128 174.111 107.534 174.111C108.969 174.111 110.065 173.332 110.065 172.201C110.065 171.293 109.491 170.777 108.301 170.49L107.329 170.256C106.585 170.074 106.233 169.805 106.233 169.371C106.233 168.809 106.819 168.428 107.569 168.428C108.331 168.428 108.823 168.762 108.958 169.266H109.954C109.819 168.229 108.899 167.572 107.575 167.572C106.233 167.572 105.219 168.363 105.219 169.412ZM114.119 168.463C115.121 168.463 115.789 169.201 115.812 170.32H112.332C112.408 169.201 113.111 168.463 114.119 168.463ZM115.783 172.365C115.519 172.922 114.968 173.221 114.154 173.221C113.082 173.221 112.384 172.43 112.332 171.182V171.135H116.873V170.748C116.873 168.785 115.835 167.572 114.13 167.572C112.396 167.572 111.283 168.861 111.283 170.848C111.283 172.846 112.378 174.111 114.13 174.111C115.513 174.111 116.486 173.449 116.79 172.365H115.783ZM118.407 174H119.414V170.086C119.414 169.195 120.112 168.551 121.073 168.551C121.272 168.551 121.635 168.586 121.717 168.609V167.602C121.588 167.584 121.377 167.572 121.213 167.572C120.375 167.572 119.649 168.006 119.461 168.621H119.367V167.684H118.407V174ZM128.085 167.684H127.007L125.278 172.887H125.185L123.456 167.684H122.378L124.716 174H125.747L128.085 167.684ZM131.67 168.463C132.672 168.463 133.34 169.201 133.363 170.32H129.883C129.959 169.201 130.662 168.463 131.67 168.463ZM133.334 172.365C133.07 172.922 132.52 173.221 131.705 173.221C130.633 173.221 129.936 172.43 129.883 171.182V171.135H134.424V170.748C134.424 168.785 133.387 167.572 131.682 167.572C129.947 167.572 128.834 168.861 128.834 170.848C128.834 172.846 129.93 174.111 131.682 174.111C133.064 174.111 134.037 173.449 134.342 172.365H133.334ZM135.958 174H136.966V170.086C136.966 169.195 137.663 168.551 138.624 168.551C138.823 168.551 139.186 168.586 139.268 168.609V167.602C139.14 167.584 138.929 167.572 138.765 167.572C137.927 167.572 137.2 168.006 137.013 168.621H136.919V167.684H135.958V174ZM144.241 166.049V167.684H143.221V168.527H144.241V172.359C144.241 173.566 144.762 174.047 146.063 174.047C146.262 174.047 146.456 174.023 146.655 173.988V173.139C146.467 173.156 146.368 173.162 146.186 173.162C145.53 173.162 145.249 172.846 145.249 172.102V168.527H146.655V167.684H145.249V166.049H144.241ZM150.632 174.111C152.431 174.111 153.544 172.869 153.544 170.842C153.544 168.809 152.431 167.572 150.632 167.572C148.833 167.572 147.72 168.809 147.72 170.842C147.72 172.869 148.833 174.111 150.632 174.111ZM150.632 173.203C149.437 173.203 148.763 172.336 148.763 170.842C148.763 169.342 149.437 168.48 150.632 168.48C151.828 168.48 152.501 169.342 152.501 170.842C152.501 172.336 151.828 173.203 150.632 173.203ZM163.644 169.617C163.468 168.492 162.53 167.572 160.995 167.572C159.226 167.572 158.101 168.85 158.101 170.818C158.101 172.828 159.232 174.111 161.001 174.111C162.519 174.111 163.462 173.256 163.644 172.096H162.624C162.437 172.811 161.845 173.203 160.995 173.203C159.87 173.203 159.144 172.277 159.144 170.818C159.144 169.389 159.858 168.48 160.995 168.48C161.903 168.48 162.46 168.99 162.624 169.617H163.644ZM167.029 173.227C166.297 173.227 165.752 172.852 165.752 172.207C165.752 171.574 166.174 171.24 167.135 171.176L168.834 171.064V171.645C168.834 172.547 168.066 173.227 167.029 173.227ZM166.842 174.111C167.685 174.111 168.377 173.742 168.787 173.068H168.881V174H169.842V169.676C169.842 168.363 168.98 167.572 167.439 167.572C166.092 167.572 165.096 168.24 164.961 169.254H165.98C166.121 168.756 166.648 168.469 167.404 168.469C168.348 168.469 168.834 168.896 168.834 169.676V170.25L167.012 170.361C165.541 170.449 164.709 171.1 164.709 172.23C164.709 173.385 165.617 174.111 166.842 174.111ZM174.733 167.572C173.872 167.572 173.116 168.012 172.712 168.738H172.618V167.684H171.657V176.109H172.665V173.051H172.759C173.104 173.719 173.831 174.111 174.733 174.111C176.339 174.111 177.387 172.816 177.387 170.842C177.387 168.855 176.345 167.572 174.733 167.572ZM174.493 173.203C173.356 173.203 172.636 172.289 172.636 170.842C172.636 169.389 173.356 168.48 174.499 168.48C175.653 168.48 176.345 169.365 176.345 170.842C176.345 172.318 175.653 173.203 174.493 173.203ZM179.343 166.049V167.684H178.324V168.527H179.343V172.359C179.343 173.566 179.865 174.047 181.166 174.047C181.365 174.047 181.558 174.023 181.757 173.988V173.139C181.57 173.156 181.47 173.162 181.289 173.162C180.632 173.162 180.351 172.846 180.351 172.102V168.527H181.757V167.684H180.351V166.049H179.343ZM188.342 167.684H187.334V171.422C187.334 172.529 186.725 173.191 185.612 173.191C184.604 173.191 184.182 172.664 184.182 171.527V167.684H183.174V171.773C183.174 173.268 183.913 174.111 185.331 174.111C186.297 174.111 186.971 173.713 187.288 173.01H187.381V174H188.342V167.684ZM190.193 174H191.201V170.086C191.201 169.195 191.898 168.551 192.859 168.551C193.058 168.551 193.421 168.586 193.503 168.609V167.602C193.374 167.584 193.163 167.572 192.999 167.572C192.161 167.572 191.435 168.006 191.247 168.621H191.154V167.684H190.193V174ZM197.059 168.463C198.061 168.463 198.729 169.201 198.752 170.32H195.272C195.348 169.201 196.051 168.463 197.059 168.463ZM198.723 172.365C198.459 172.922 197.908 173.221 197.094 173.221C196.022 173.221 195.324 172.43 195.272 171.182V171.135H199.813V170.748C199.813 168.785 198.776 167.572 197.07 167.572C195.336 167.572 194.223 168.861 194.223 170.848C194.223 172.846 195.319 174.111 197.07 174.111C198.453 174.111 199.426 173.449 199.731 172.365H198.723ZM206.631 173.227C205.898 173.227 205.353 172.852 205.353 172.207C205.353 171.574 205.775 171.24 206.736 171.176L208.435 171.064V171.645C208.435 172.547 207.668 173.227 206.631 173.227ZM206.443 174.111C207.287 174.111 207.978 173.742 208.389 173.068H208.482V174H209.443V169.676C209.443 168.363 208.582 167.572 207.041 167.572C205.693 167.572 204.697 168.24 204.562 169.254H205.582C205.723 168.756 206.25 168.469 207.006 168.469C207.949 168.469 208.435 168.896 208.435 169.676V170.25L206.613 170.361C205.142 170.449 204.31 171.1 204.31 172.23C204.31 173.385 205.219 174.111 206.443 174.111ZM216.368 167.684H215.36V171.422C215.36 172.529 214.751 173.191 213.637 173.191C212.63 173.191 212.208 172.664 212.208 171.527V167.684H211.2V171.773C211.2 173.268 211.938 174.111 213.356 174.111C214.323 174.111 214.997 173.713 215.313 173.01H215.407V174H216.368V167.684ZM220.556 174.111C221.429 174.111 222.179 173.695 222.578 172.992H222.671V174H223.632V165.176H222.625V168.68H222.537C222.179 167.988 221.435 167.572 220.556 167.572C218.951 167.572 217.902 168.861 217.902 170.842C217.902 172.828 218.939 174.111 220.556 174.111ZM220.791 168.48C221.933 168.48 222.648 169.395 222.648 170.842C222.648 172.301 221.939 173.203 220.791 173.203C219.636 173.203 218.945 172.318 218.945 170.842C218.945 169.371 219.642 168.48 220.791 168.48ZM225.565 174H226.573V167.684H225.565V174ZM226.069 166.4C226.455 166.4 226.772 166.084 226.772 165.697C226.772 165.311 226.455 164.994 226.069 164.994C225.682 164.994 225.366 165.311 225.366 165.697C225.366 166.084 225.682 166.4 226.069 166.4ZM231.042 174.111C232.841 174.111 233.954 172.869 233.954 170.842C233.954 168.809 232.841 167.572 231.042 167.572C229.243 167.572 228.13 168.809 228.13 170.842C228.13 172.869 229.243 174.111 231.042 174.111ZM231.042 173.203C229.847 173.203 229.173 172.336 229.173 170.842C229.173 169.342 229.847 168.48 231.042 168.48C232.238 168.48 232.911 169.342 232.911 170.842C232.911 172.336 232.238 173.203 231.042 173.203ZM236.203 174.059C236.625 174.059 236.965 173.713 236.965 173.297C236.965 172.875 236.625 172.535 236.203 172.535C235.787 172.535 235.442 172.875 235.442 173.297C235.442 173.713 235.787 174.059 236.203 174.059Z" fill="#272525"/>
+</svg>
diff --git a/src/gui/Recommendations.qml b/src/gui/Recommendations.qml
new file mode 100644 (file)
index 0000000..6d4e949
--- /dev/null
@@ -0,0 +1,564 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    property int fontBig: 20
+    property int fontMedium: 13
+    property int fontSmall: 11
+    property int fontExtraSmall: 8
+
+    property int buttonWidth: 103
+    property int buttonHeight: 25
+
+    property int leftMargin: 48
+    property int rightMargin: 16
+
+    property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+    property string saveButtonBackgroundColour: "#F2F3F3"
+    property string saveButtonPressedColour: "#E7E8E8"
+    property string saveButtonStroke: "#EAEBEB"
+    property string saveButtonPressedStroke: "#B0B5B5"
+    property string recommendationText: "#DB0A0A"
+    property string saveButtonText: "#DB0A0A"
+    property string checkboxStroke: "#0062cc"
+    property string checkboxPressedStroke: "#007AFF"
+    property string disabledButtonText: "#D3D4D4"
+    property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+
+    property bool currShowRecommendations: virtualstudio.showWarnings
+    property string recommendationScreen: virtualstudio.showWarnings ? "ethernet" : ( permissions.micPermission == "unknown" ? "microphone" : "acknowledged")
+    property bool onWindows: Qt.platform.os === "windows"
+
+    Rectangle {
+        id: recommendationsHeader
+        x: -1
+        y: 0
+
+        width: parent.width + 2
+        height: 64
+
+        color: backgroundColour
+        border.color: "#33979797"
+
+        Image {
+            source: virtualstudio.darkMode ? "jacktrip white.png" : "jacktrip.png"
+            anchors.left: parent.left
+            anchors.leftMargin: 32 * virtualstudio.uiScale
+            anchors.verticalCenter: parent.verticalCenter
+            width: 119 * virtualstudio.uiScale; height: 28 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: gettingStartedText1
+            visible: recommendationScreen === "ethernet"
+            text: "Getting Started with JackTrip (1/5)"
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.right: parent.right
+            anchors.rightMargin: 32 * virtualstudio.uiScale
+            anchors.verticalCenter: parent.verticalCenter
+        }
+
+        Text {
+            id: gettingStartedText2
+            visible: recommendationScreen === "fiber"
+            text: "Getting Started with JackTrip (2/5)"
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.right: parent.right
+            anchors.rightMargin: 32 * virtualstudio.uiScale
+            anchors.verticalCenter: parent.verticalCenter
+        }
+
+        Text {
+            id: gettingStartedText3
+            visible: recommendationScreen === "audiointerface"
+            text: "Getting Started with JackTrip (3/5)"
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.right: parent.right
+            anchors.rightMargin: 32 * virtualstudio.uiScale
+            anchors.verticalCenter: parent.verticalCenter
+        }
+
+        Text {
+            id: gettingStartedText4
+            visible: recommendationScreen === "headphones"
+            text: "Getting Started with JackTrip (4/5)"
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.right: parent.right
+            anchors.rightMargin: 32 * virtualstudio.uiScale
+            anchors.verticalCenter: parent.verticalCenter
+        }
+
+        Text {
+            id: gettingStartedText5
+            visible: recommendationScreen === "acknowledged"
+            text: "Getting Started with JackTrip (5/5)"
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.right: parent.right
+            anchors.rightMargin: 32 * virtualstudio.uiScale
+            anchors.verticalCenter: parent.verticalCenter
+        }
+    }
+
+    Item {
+        id: ethernetRecommendationItem
+        width: parent.width; height: parent.height
+        visible: recommendationScreen == "ethernet"
+
+        AppIcon {
+            id: ethernetRecommendationLogo
+            y: 90
+            anchors.horizontalCenter: parent.horizontalCenter
+            width: 179
+            height: 128
+            icon.source: "ethernet.svg"
+        }
+
+        Text {
+            id: ethernetRecommendationHeader1
+            text: "Wired Ethernet Recommended"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: ethernetRecommendationLogo.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: ethernetRecommendationSubheader1
+            text: "JackTrip works best when you connect your computer directly to your Internet router via a wired ethernet cable."
+                + "<br/><br/>"
+                + "WiFi works OK for some people, but generates significantly more latency and audio glitches."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 600
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: ethernetRecommendationHeader1.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        LearnMoreButton {
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: ethernetRecommendationSubheader1.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+            url: "https://support.jacktrip.com/wired-internet-versus-wi-fi"
+        }
+
+        Button {
+            id: okButtonEthernet
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: okButtonEthernet.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                border.width: 1
+                border.color: okButtonEthernet.down || okButtonEthernet.hovered ? saveButtonPressedStroke : saveButtonStroke
+                layer.enabled: okButtonEthernet.hovered && !okButtonEthernet.down
+            }
+            onClicked: { recommendationScreen = "fiber" }
+            anchors.right: parent.right
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            anchors.bottomMargin: 16 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            Text {
+                text: "Continue"
+                font.family: "Poppins"
+                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                font.weight: Font.Bold
+                color: saveButtonText
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+            }
+        }
+    }
+
+    Item {
+        id: fiberRecommendationItem
+        width: parent.width; height: parent.height
+        visible: recommendationScreen == "fiber"
+
+        AppIcon {
+            id: fiberRecommendationLogo
+            y: 90
+            anchors.horizontalCenter: parent.horizontalCenter
+            width: 179
+            height: 128
+            icon.source: "networkCheck.svg"
+        }
+
+        Text {
+            id: fiberRecommendationHeader
+            text: "Fiber Internet Recommended"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: fiberRecommendationLogo.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: fiberRecommendationSubheader
+            text: "A Fiber Internet connection from your Internet Service Provider (ISP) will give you the best experience while using JackTrip."
+                + "<br/><br/>"
+                + "It's OK to use JackTrip with Cable and DSL, but these types of Internet connections introduce significantly more latency."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 600
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: fiberRecommendationHeader.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        LearnMoreButton {
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: fiberRecommendationSubheader.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+            url: "https://support.jacktrip.com/how-to-optimize-latency-when-using-jacktrip"
+        }
+
+        Button {
+            id: okButtonFiber
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: okButtonFiber.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                border.width: 1
+                border.color: okButtonFiber.down || okButtonFiber.hovered ? saveButtonPressedStroke : saveButtonStroke
+                layer.enabled: okButtonFiber.hovered && !okButtonFiber.down
+            }
+            onClicked: { recommendationScreen = "audiointerface" }
+            anchors.right: parent.right
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            anchors.bottomMargin: 16 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            Text {
+                text: "Continue"
+                font.family: "Poppins"
+                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                font.weight: Font.Bold
+                color: saveButtonText
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+            }
+        }
+    }
+
+    Item {
+        id: headphoneRecommendationItem
+        width: parent.width; height: parent.height
+        visible: recommendationScreen == "headphones"
+
+        AppIcon {
+            id: headphoneWarningLogo
+            y: 90
+            anchors.horizontalCenter: parent.horizontalCenter
+            width: 118
+            height: 128
+            icon.source: "headphones.svg"
+        }
+
+        Text {
+            id: headphoneRecommendationHeader1
+            text: "Wired Headphones Required"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: headphoneWarningLogo.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: headphoneRecommendationSubheader1
+            text: "JackTrip requires the use of wired headphones."
+                + "<br/><br/>"
+                + "Using speakers will generate echos and loud feedback loops."
+                + "<br/><br/>"
+                + "Wireless and bluetooth headphones introduce higher latency."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 600
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: headphoneRecommendationHeader1.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Button {
+            id: okButtonHeadphones
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: okButtonHeadphones.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                border.width: 1
+                border.color: okButtonHeadphones.down || okButtonHeadphones.hovered ? saveButtonPressedStroke : saveButtonStroke
+                layer.enabled: okButtonHeadphones.hovered && !okButtonHeadphones.down
+            }
+            onClicked: {
+                recommendationScreen = "acknowledged";
+            }
+            anchors.right: parent.right
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            anchors.bottomMargin: 16 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            Text {
+                text: "Continue"
+                font.family: "Poppins"
+                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                font.weight: Font.Bold
+                color: saveButtonText
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+            }
+        }
+    }
+
+    Item {
+        id: audioInterfaceRecommendationItem
+        width: parent.width; height: parent.height
+        visible: recommendationScreen == "audiointerface"
+
+        AppIcon {
+            id: audioInterfaceRecommendationLogo
+            y: 90
+            anchors.horizontalCenter: parent.horizontalCenter
+            width: 118
+            height: 128
+            icon.source: "externalMic.svg"
+        }
+
+        Text {
+            id: audioInterfaceRecommendationHeaderNonWindows
+            visible: !onWindows
+            text: "External Audio Device Recommended"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: audioInterfaceRecommendationLogo.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: audioInterfaceRecommendationSubheaderNonWindows
+            visible: !onWindows
+            text: "Your audio device controls the quality of sound, and can also have a big impact on latency."
+                + "<br/><br/>"
+                + "It's OK to use the audio device that is built into your computer, but external USB and "
+                + "Thunderbolt audio interfaces will usually produce better quality and lower latency."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 600
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: audioInterfaceRecommendationHeaderNonWindows.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: audioInterfaceRecommendationHeaderWindows
+            visible: onWindows
+            text: "External Audio Device Recommended"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: audioInterfaceRecommendationLogo.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: audioInterfaceRecommendationSubheaderWindows
+            visible: onWindows
+            text: "Your audio device controls the quality of sound, and can also have a big impact on latency."
+                + "<br/><br/>"
+                + "ASIO drivers are required for low latency on Windows. "
+                + "<br/><br/>"
+                + "It's OK to use the audio device that is built into your computer, but external USB and "
+                + "Thunderbolt devices will produce better quality and much lower latency."
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 600
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: audioInterfaceRecommendationHeaderWindows.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        LearnMoreButton {
+            width: 250 * virtualstudio.uiScale;
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: onWindows ? audioInterfaceRecommendationSubheaderWindows.bottom : audioInterfaceRecommendationSubheaderNonWindows.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+            buttonText: "See recommended devices"
+            url: "https://support.jacktrip.com/recommended-audio-interfaces"
+        }
+
+        Button {
+            id: okButtonAudioInterface
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: okButtonAudioInterface.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                border.width: 1
+                border.color: okButtonAudioInterface.down || okButtonAudioInterface.hovered ? saveButtonPressedStroke : saveButtonStroke
+                layer.enabled: okButtonAudioInterface.hovered && !okButtonAudioInterface.down
+            }
+            onClicked: {
+                recommendationScreen = "headphones";
+            }
+            anchors.right: parent.right
+            anchors.rightMargin: 16 * virtualstudio.uiScale
+            anchors.bottomMargin: 16 * virtualstudio.uiScale
+            anchors.bottom: parent.bottom
+            width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            Text {
+                text: "Continue"
+                font.family: "Poppins"
+                font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                font.weight: Font.Bold
+                color: saveButtonText
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+            }
+        }
+    }
+
+    Item {
+        id: acknowledgedRecommendationItem
+        width: parent.width; height: parent.height
+        visible: recommendationScreen == "acknowledged"
+
+        Text {
+            id: acknowledgedHeader
+            text: "Remind Me Again Next Time?"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: parent.top
+            anchors.topMargin: 176 * virtualstudio.uiScale
+        }
+
+        Text {
+            id: acknowledgedSubheader
+            text: "Would you like to review the getting started recommendations again the next time you start JackTrip?"
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 600
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: acknowledgedHeader.bottom
+            anchors.topMargin: 32 * virtualstudio.uiScale
+        }
+
+        Item {
+            id: acknowledgedButtonsContainer
+            width: 320 * virtualstudio.uiScale
+
+            anchors.top: acknowledgedSubheader.bottom
+            anchors.topMargin: 64 * virtualstudio.uiScale
+            anchors.horizontalCenter: parent.horizontalCenter
+
+            Button {
+                id: acknowledgedYesButton
+                anchors.left: parent.left
+                anchors.verticalCenter: parent.verticalCenter
+                background: Rectangle {
+                    radius: 6 * virtualstudio.uiScale
+                    color: acknowledgedYesButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                    border.width: 1
+                    border.color: acknowledgedYesButton.down || acknowledgedYesButton.hovered ? saveButtonPressedStroke : saveButtonStroke
+                    layer.enabled: acknowledgedYesButton.hovered && !acknowledgedYesButton.down
+                }
+                onClicked: {
+                    virtualstudio.showWarnings = true;
+                    virtualstudio.saveSettings();
+                    if (permissions.micPermission !== "granted") {
+                        virtualstudio.windowState = "permissions";
+                    } else if (virtualstudio.studioToJoin === "") {
+                        virtualstudio.windowState = "browse";
+                    } else {
+                        virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
+                        virtualstudio.joinStudio();
+                    }
+                }
+                width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                Text {
+                    text: "Yes"
+                    font.family: "Poppins"
+                    font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                    font.weight: Font.Bold
+                    color: saveButtonText
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.verticalCenter: parent.verticalCenter
+                }
+            }
+
+            Button {
+                id: acknowledgedNoButton
+                anchors.right: parent.right
+                anchors.verticalCenter: parent.verticalCenter
+                background: Rectangle {
+                    radius: 6 * virtualstudio.uiScale
+                    color: acknowledgedNoButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                    border.width: 1
+                    border.color: acknowledgedNoButton.down || acknowledgedNoButton.hovered ? saveButtonPressedStroke : saveButtonStroke
+                    layer.enabled: acknowledgedNoButton.hovered && !acknowledgedNoButton.down
+                }
+                onClicked: {
+                    virtualstudio.showWarnings = false;
+                    virtualstudio.saveSettings();
+                    if (permissions.micPermission !== "granted") {
+                        virtualstudio.windowState = "permissions";
+                    } else if (virtualstudio.studioToJoin === "") {
+                        virtualstudio.windowState = "browse";
+                    } else {
+                        virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
+                        virtualstudio.joinStudio();
+                    }
+                }
+                width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                Text {
+                    text: "No"
+                    font.family: "Poppins"
+                    font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+                    font.weight: Font.Bold
+                    color: saveButtonText
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.verticalCenter: parent.verticalCenter
+                }
+            }
+        }
+
+
+        Text {
+            id: acknowledgedSettingsInfo
+            text: "You can change this setting at any time under <b>Settings > Advanced</b>"
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            width: 600
+            wrapMode: Text.Wrap
+            horizontalAlignment: Text.AlignHCenter
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: acknowledgedButtonsContainer.bottom
+            anchors.topMargin: 64 * virtualstudio.uiScale
+        }
+    }
+}
diff --git a/src/gui/SectionHeading.qml b/src/gui/SectionHeading.qml
new file mode 100644 (file)
index 0000000..84568df
--- /dev/null
@@ -0,0 +1,163 @@
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+  property string filterStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+
+  property bool listIsEmpty: false
+  // required property string section: section (for 5.15)
+  color: "transparent"
+  height: 72 * virtualstudio.uiScale
+  x: 16 * virtualstudio.uiScale
+  y: listIsEmpty ? 16 * virtualstudio.uiScale : 0
+  width: listIsEmpty ? parent.width - (2 * x) : ListView.view.width - (2 * x)
+  Text {
+      id: sectionText
+      //anchors.bottom: parent.bottom
+      y: 12 * virtualstudio.uiScale
+      // text: parent.section (for 5.15)
+      width: parent.width - 332 * virtualstudio.uiScale
+      fontSizeMode: Text.HorizontalFit
+      text: listIsEmpty ? "No Studios" : section
+      font { family: "Poppins"; pixelSize: 28 * virtualstudio.fontScale * virtualstudio.uiScale; weight: Font.Bold }
+      color: textColour
+      verticalAlignment: Text.AlignBottom
+  }
+  Button {
+      id: createButton
+      background: Rectangle {
+          radius: 6 * virtualstudio.uiScale
+          color: createButton.down ? "#E7E8E8" : "#F2F3F3"
+          border.width: 1
+          border.color: createButton.down ? "#B0B5B5" : "#EAEBEB"
+          layer.enabled: createButton.hovered && !createButton.down
+      }
+      onClicked: { virtualstudio.createStudio(); }
+      anchors.right: filterButton.left
+      anchors.rightMargin: 16
+      anchors.verticalCenter: sectionText.verticalCenter
+      width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+      Text {
+          text: "Create a Studio"
+          font.family: "Poppins"
+          font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+          font.weight: Font.Bold
+          color: "#DB0A0A"
+          anchors.horizontalCenter: parent.horizontalCenter
+          anchors.verticalCenter: parent.verticalCenter
+      }
+      visible: listIsEmpty ? true : (section == virtualstudio.logoSection ? true : false)
+  }
+  Button {
+      id: filterButton
+      background: Rectangle {
+          radius: 6 * virtualstudio.uiScale
+          color: filterButton.down ? "#E7E8E8" : "#F2F3F3"
+          border.width: 1
+          border.color: filterButton.down ? "#B0B5B5" : "#EAEBEB"
+          layer.enabled: filterButton.hovered && !filterButton.down
+      }
+      onClicked: { filterMenu.open(); }
+      anchors.right: parent.right
+      anchors.verticalCenter: sectionText.verticalCenter
+      width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+      Text {
+          text: "Filter Studios"
+          font.family: "Poppins"
+          font.pixelSize: 11 * virtualstudio.fontScale * virtualstudio.uiScale
+          anchors.horizontalCenter: parent.horizontalCenter
+          anchors.verticalCenter: parent.verticalCenter
+      }
+      visible: listIsEmpty ? true : (section == virtualstudio.logoSection ? true : false)
+
+      Popup {
+          id: filterMenu
+          y: Math.round(parent.height + 8)
+          rightMargin: 16 * virtualstudio.uiScale
+          width: 210 * virtualstudio.uiScale; height: 64 * virtualstudio.uiScale
+          modal: false
+          focus: false
+          closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+          background: Rectangle {
+              radius: 6 * virtualstudio.uiScale
+              color: "#F6F8F8"
+              border.width: 1
+              border.color: filterStroke
+              layer.enabled: true
+          }
+          contentItem: Column {
+              anchors.fill: parent
+              CheckBox {
+                  id: inactiveCheckbox
+                  text: qsTr("Show my inactive Studios")
+                  checkState: virtualstudio.showInactive ? Qt.Checked : Qt.Unchecked
+                  onClicked: { virtualstudio.showInactive = inactiveCheckbox.checkState == Qt.Checked;
+                      refreshing = true;
+                      refresh();
+                  }
+                  indicator: Rectangle {
+                      implicitWidth: 16 * virtualstudio.uiScale
+                      implicitHeight: 16 * virtualstudio.uiScale
+                      x: inactiveCheckbox.leftPadding
+                      y: parent.height / 2 - height / 2
+                      radius: 3 * virtualstudio.uiScale
+                      border.color: inactiveCheckbox.down ? "#007AFF" : "#0062cc"
+
+                      Rectangle {
+                          width: 10 * virtualstudio.uiScale
+                          height: 10 * virtualstudio.uiScale
+                          x: 3 * virtualstudio.uiScale
+                          y: 3 * virtualstudio.uiScale
+                          radius: 2 * virtualstudio.uiScale
+                          color: inactiveCheckbox.down ? "#007AFF" : "#0062cc"
+                          visible: inactiveCheckbox.checked
+                      }
+                  }
+                  contentItem: Text {
+                      text: inactiveCheckbox.text
+                      font.family: "Poppins"
+                      font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                      anchors.horizontalCenter: parent.horizontalCenter
+                      anchors.verticalCenter: parent.verticalCenter
+                      leftPadding: inactiveCheckbox.indicator.width + inactiveCheckbox.spacing
+                  }
+              }
+              CheckBox {
+                  id: selfHostedCheckbox
+                  text: qsTr("Show self-hosted Studios")
+                  checkState: virtualstudio.showSelfHosted ? Qt.Checked : Qt.Unchecked
+                  onClicked: { virtualstudio.showSelfHosted = selfHostedCheckbox.checkState == Qt.Checked;
+                      refreshing = true;
+                      refresh();
+                  }
+                  indicator: Rectangle {
+                      implicitWidth: 16 * virtualstudio.uiScale
+                      implicitHeight: 16 * virtualstudio.uiScale
+                      x: selfHostedCheckbox.leftPadding
+                      y: parent.height / 2 - height / 2
+                      radius: 3 * virtualstudio.uiScale
+                      border.color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC"
+
+                      Rectangle {
+                          width: 10 * virtualstudio.uiScale
+                          height: 10 * virtualstudio.uiScale
+                          x: 3 * virtualstudio.uiScale
+                          y: 3 * virtualstudio.uiScale
+                          radius: 2 * virtualstudio.uiScale
+                          color: selfHostedCheckbox.down ? "#007AFF" : "#0062CC"
+                          visible: selfHostedCheckbox.checked
+                      }
+                  }
+                  contentItem: Text {
+                      text: selfHostedCheckbox.text
+                      font.family: "Poppins"
+                      font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                      anchors.horizontalCenter: parent.horizontalCenter
+                      anchors.verticalCenter: parent.verticalCenter
+                      leftPadding: selfHostedCheckbox.indicator.width + selfHostedCheckbox.spacing
+                  }
+              }
+          }
+      }
+  }
+}
\ No newline at end of file
diff --git a/src/gui/Settings.qml b/src/gui/Settings.qml
new file mode 100644 (file)
index 0000000..b970e91
--- /dev/null
@@ -0,0 +1,781 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    Rectangle {
+        width: parent.width; height: parent.height
+        color: backgroundColour
+    }
+
+    property int fontBig: 20
+    property int fontMedium: 13
+    property int fontSmall: 11
+    property int fontExtraSmall: 8
+
+    property int leftMargin: 48
+    property int rightMargin: 16
+    property int buttonWidth: 103
+    property int buttonHeight: 25
+
+    property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
+    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#40979797"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+    property string sliderColour: virtualstudio.darkMode ? "#BABCBC" :  "#EAECEC"
+    property string sliderPressedColour: virtualstudio.darkMode ? "#ACAFAF" : "#DEE0E0"
+    property string sliderTrackColour: virtualstudio.darkMode ? "#5B5858" : "light gray"
+    property string sliderActiveTrackColour: virtualstudio.darkMode ? "light gray" : "black"
+    property string warningTextColour: "#DB0A0A"
+    property string checkboxStroke: "#0062cc"
+    property string checkboxPressedStroke: "#007AFF"
+    property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
+
+    property string errorFlagColour: "#DB0A0A"
+    property string disabledButtonTextColour: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+
+    property string settingsGroupView: "Audio"
+
+    function getCurrentBufferSizeIndex () {
+        let bufferSize = audio.bufferSize;
+        let idx = audio.bufferSizeComboModel.findIndex(elem => parseInt(elem) === bufferSize);
+        if (idx < 0) {
+            idx = 0;
+        }
+        return idx;
+    }
+
+    function getCurrentAudioBackendIndex () {
+        let idx = audio.audioBackendComboModel.findIndex(elem => elem === audio.audioBackend);
+        if (idx < 0) {
+            idx = 0;
+        }
+        return idx;
+    }
+
+    Rectangle {
+        id: audioSettingsView
+        width: 0.8 * parent.width
+        height: parent.height - header.height
+        x: 0.2 * window.width
+        y: header.height
+        visible: settingsGroupView == "Audio"
+
+        AudioSettings {
+            id: audioSettings
+        }
+    }
+
+    ToolBar {
+        id: header
+        width: parent.width
+        height: 64 * virtualstudio.uiScale
+
+        background: Rectangle {
+            border.color: "#33979797"
+            color: backgroundColour
+            width: parent.width
+        }
+
+        contentItem: Item {
+            id: headerContent
+            width: header.width
+            height: header.height
+
+            Label {
+                id: pageTitle
+                text: "Settings"
+                height: headerContent.height;
+                anchors.left: headerContent.left;
+                anchors.leftMargin: 32 * virtualstudio.uiScale
+                elide: Label.ElideRight
+                horizontalAlignment: Text.AlignHCenter
+                verticalAlignment: Text.AlignVCenter
+                font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: textColour
+            }
+
+            DeviceRefreshButton {
+                id: refreshButton
+                anchors.verticalCenter: pageTitle.verticalCenter;
+                anchors.right: headerContent.right;
+                anchors.rightMargin: 16 * virtualstudio.uiScale;
+                visible: audio.audioBackend == "RtAudio" && settingsGroupView == "Audio"
+                enabled: audio.audioReady && !audio.scanningDevices
+            }
+
+            Text {
+                text: "Restarting Audio"
+                anchors.verticalCenter: pageTitle.verticalCenter;
+                anchors.right: refreshButton.left;
+                anchors.rightMargin: 16 * virtualstudio.uiScale;
+                font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                color: textColour
+                visible: !audio.audioReady
+            }
+        }
+    }
+
+    Drawer {
+        id: drawer
+        width: 0.2 * parent.width
+        height: parent.height - header.height
+        y: header.height-1
+        modal: false
+        interactive: false
+        visible: virtualstudio.windowState == "settings"
+
+        background: Rectangle {
+            border.color: "#33979797"
+            color: backgroundColour
+        }
+
+        ButtonGroup {
+            buttons: viewControls.children
+            onClicked: function(button) {
+                settingsGroupView = button.text
+            }
+        }
+
+        Column {
+            id: viewControls
+            width: parent.width
+            spacing: 24 * virtualstudio.uiScale
+            anchors.centerIn: parent
+            Button {
+                id: audioBtn
+                text: "Audio"
+                width: parent.width
+                contentItem: Item {
+                    implicitWidth: audioButtonText.implicitWidth
+                    implicitHeight: audioButtonText.implicitHeight
+
+                    Label {
+                        id: audioButtonText
+                        text: audioBtn.text
+                        width: Boolean(audio.devicesError) ? parent.width - 16 * virtualstudio.uiScale : parent.width
+                        font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                        horizontalAlignment: Text.AlignHCenter
+                        verticalAlignment: Text.AlignVCenter
+                        color: textColour
+                    }
+
+                    Rectangle {
+                        id: audioDevicesErrorFlag
+                        anchors.left: audioButtonText.right
+                        anchors.verticalCenter: audioButtonText.verticalCenter
+                        anchors.rightMargin: 16 * virtualstudio.uiScale
+                        width: 8 * virtualstudio.uiScale
+                        height: 8 * virtualstudio.uiScale
+                        color: errorFlagColour
+                        radius: 4 * virtualstudio.uiScale
+                        visible: Boolean(audio.devicesError)
+                    }
+                }
+                background: Rectangle {
+                    width: parent.width
+                    color: audioBtn.down ? buttonPressedColour : (audioBtn.hovered || settingsGroupView == "Audio" ? buttonHoverColour : backgroundColour)
+                }
+            }
+            Button {
+                id: appearanceBtn
+                text: "Appearance"
+                width: parent.width
+                contentItem: Item {
+                    implicitWidth: appearanceButtonText.implicitWidth
+                    implicitHeight: appearanceButtonText.implicitHeight
+
+                    Label {
+                        id: appearanceButtonText
+                        text: appearanceBtn.text
+                        width: parent.width
+                        font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                        horizontalAlignment: Text.AlignHCenter
+                        verticalAlignment: Text.AlignVCenter
+                        color: textColour
+                    }
+                }
+
+
+                background: Rectangle {
+                    width: parent.width
+                    color: appearanceBtn.down ? buttonPressedColour : (appearanceBtn.hovered || settingsGroupView == "Appearance" ? buttonHoverColour : backgroundColour)
+                }
+            }
+            Button {
+                id: advancedBtn
+                text: "Advanced"
+                width: parent.width
+                contentItem: Item {
+                    implicitWidth: advancedButtonText.implicitWidth
+                    implicitHeight: advancedButtonText.implicitHeight
+
+                    Label {
+                        id: advancedButtonText
+                        text: advancedBtn.text
+                        width: parent.width
+                        font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                        horizontalAlignment: Text.AlignHCenter
+                        verticalAlignment: Text.AlignVCenter
+                        color: textColour
+                    }
+                }
+                background: Rectangle {
+                    width: parent.width
+                    color: advancedBtn.down ? buttonPressedColour : (advancedBtn.hovered || settingsGroupView == "Advanced" ? buttonHoverColour : backgroundColour)
+                }
+            }
+            Button {
+                id: profileBtn
+                text: "Profile"
+                width: parent.width
+                contentItem: Item {
+
+                    implicitWidth: profileButtonText.implicitWidth
+                    implicitHeight: profileButtonText.implicitHeight
+
+                    Label {
+                        id: profileButtonText
+                        text: profileBtn.text
+                        width: parent.width
+                        font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                        horizontalAlignment: Text.AlignHCenter
+                        verticalAlignment: Text.AlignVCenter
+                        color: textColour
+                    }
+                }
+                background: Rectangle {
+                    width: parent.width
+                    color: profileBtn.down ? buttonPressedColour : (profileBtn.hovered || settingsGroupView == "Profile" ? buttonHoverColour : backgroundColour)
+                }
+            }
+        }
+
+        Column {
+            id: appVersion
+            width: parent.width
+            spacing: 24 * virtualstudio.uiScale
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.bottom: parent.bottom
+
+            Text {
+                text: "Version " + virtualstudio.versionString
+                font { family: "Poppins"; pixelSize: 9 * virtualstudio.fontScale * virtualstudio.uiScale}
+                color: textColour
+                opacity: 0.8
+                width: parent.width
+                horizontalAlignment: Text.AlignHCenter
+                verticalAlignment: Text.AlignVCenter
+                bottomPadding: 5 * virtualstudio.uiScale
+            }
+        }
+    }
+
+    Rectangle {
+        id: appearanceSettingsView
+        width: 0.8 * parent.width
+        height: parent.height - header.height
+        x: 0.2 * window.width
+        y: header.height
+        color: backgroundColour
+        visible: settingsGroupView == "Appearance"
+
+        Slider {
+            id: scaleSlider
+            x: 220 * virtualstudio.uiScale;
+            y: 100 * virtualstudio.uiScale
+            width: backendCombo.width
+            from: 1; to: 1.25; value: virtualstudio.uiScale
+            onMoved: { virtualstudio.uiScale = value }
+
+            background: Rectangle {
+                x: scaleSlider.leftPadding
+                y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2
+                implicitWidth: parent.width
+                implicitHeight: 6
+                width: scaleSlider.availableWidth
+                height: implicitHeight
+                radius: 4
+                color: sliderTrackColour
+
+                Rectangle {
+                    width: scaleSlider.visualPosition * parent.width
+                    height: parent.height
+                    color: sliderActiveTrackColour
+                    radius: 4
+                }
+            }
+
+            handle: Rectangle {
+                x: scaleSlider.leftPadding + scaleSlider.visualPosition * (scaleSlider.availableWidth - width)
+                y: scaleSlider.topPadding + scaleSlider.availableHeight / 2 - height / 2
+                implicitWidth: 26 * virtualstudio.uiScale
+                implicitHeight: 26 * virtualstudio.uiScale
+                radius: 13 * virtualstudio.uiScale
+                color: scaleSlider.pressed ? sliderPressedColour : sliderColour
+                border.color: buttonStroke
+            }
+        }
+
+        Text {
+            anchors.verticalCenter: scaleSlider.verticalCenter
+            x: leftMargin * virtualstudio.uiScale
+            text: "Scale Interface"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        Button {
+            id: darkButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: darkButton.down ? buttonPressedColour : (darkButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: darkButton.down ? buttonPressedStroke : (darkButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: { virtualstudio.darkMode = !virtualstudio.darkMode; }
+            x: parent.width - (232 * virtualstudio.uiScale); y: modeButton.y + (56 * virtualstudio.uiScale)
+            width: 216 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            Text {
+                text: virtualstudio.darkMode ? "Switch to Light Mode" : "Switch to Dark Mode"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+
+        Text {
+            anchors.verticalCenter: darkButton.verticalCenter
+            x: leftMargin * virtualstudio.uiScale
+            text: "Color Theme"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+    }
+
+    Rectangle {
+        id: advancedSettingsView
+        width: 0.8 * parent.width
+        height: parent.height - header.height
+        x: 0.2 * window.width
+        y: header.height
+        color: backgroundColour
+        visible: settingsGroupView == "Advanced"
+
+        Button {
+            id: modeButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: modeButton.down ? buttonPressedColour : (modeButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: modeButton.down ? buttonPressedStroke : (modeButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: {
+                // essentially the same here as clicking the cancel button
+                audio.stopAudio();
+                virtualstudio.windowState = "browse";
+                virtualstudio.loadSettings();
+                audio.validateDevices();
+
+                // switch mode
+                virtualstudio.toStandard();
+            }
+            x: 220 * virtualstudio.uiScale;
+            y: 100 * virtualstudio.uiScale
+            width: 216 * virtualstudio.uiScale;
+            height: 30 * virtualstudio.uiScale
+            Text {
+                text: virtualstudio.psiBuild ? "Switch to Standard Mode" : "Switch to Classic Mode"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+
+        Text {
+            anchors.verticalCenter: modeButton.verticalCenter
+            x: leftMargin * virtualstudio.uiScale
+            text: "Display Mode"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        ComboBox {
+            id: updateChannelCombo
+            x: 220 * virtualstudio.uiScale; y: modeButton.y + (48 * virtualstudio.uiScale)
+            width: parent.width - x - (16 * virtualstudio.uiScale); height: 36 * virtualstudio.uiScale
+            model: virtualstudio.updateChannelComboModel
+            currentIndex: virtualstudio.updateChannel == "stable" ? 0 : 1
+            onActivated: { virtualstudio.updateChannel = currentIndex == 0 ? "stable": "edge" }
+            font.family: "Poppins"
+            enabled: !virtualstudio.noUpdater
+        }
+
+        Text {
+            anchors.verticalCenter: updateChannelCombo.verticalCenter
+            x: leftMargin * virtualstudio.uiScale
+            text: "Update Channel"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        ComboBox {
+            id: backendCombo
+            model: audio.audioBackendComboModel
+            enabled: audio.audioBackendComboModel.length > 1
+            currentIndex: getCurrentAudioBackendIndex()
+            onActivated: {
+                audio.audioBackend = currentText
+                audio.restartAudio();
+            }
+            x: 220 * virtualstudio.uiScale; y: updateChannelCombo.y + (48 * virtualstudio.uiScale)
+            width: updateChannelCombo.width; height: updateChannelCombo.height
+        }
+
+        Text {
+            id: backendLabel
+            anchors.verticalCenter: backendCombo.verticalCenter
+            x: leftMargin * virtualstudio.uiScale
+            text: "Audio Backend"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        ComboBox {
+            id: bufferCombo
+            x: 220 * virtualstudio.uiScale; y: backendCombo.y + (48 * virtualstudio.uiScale)
+            width: backendCombo.width; height: updateChannelCombo.height
+            model: audio.bufferSizeComboModel
+            currentIndex: getCurrentBufferSizeIndex()
+            onActivated: {
+                audio.bufferSize = parseInt(currentText, 10);
+                audio.restartAudio();
+            }
+            font.family: "Poppins"
+        }
+
+        Text {
+            anchors.verticalCenter: bufferCombo.verticalCenter
+            x: 48 * virtualstudio.uiScale
+            text: "Buffer Size"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        ComboBox {
+            id: bufferStrategyCombo
+            x: updateChannelCombo.x; y: bufferCombo.y + (48 * virtualstudio.uiScale)
+            width: updateChannelCombo.width; height: updateChannelCombo.height
+            model: audio.bufferStrategyComboModel
+            currentIndex: audio.bufferStrategy
+            onActivated: { audio.bufferStrategy = currentIndex }
+            font.family: "Poppins"
+        }
+
+        Text {
+            anchors.verticalCenter: bufferStrategyCombo.verticalCenter
+            x: 48 * virtualstudio.uiScale
+            text: "Buffer Strategy"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        ComboBox {
+            id: feedbackDetectionCombo
+            x: updateChannelCombo.x; y: bufferStrategyCombo.y + (48 * virtualstudio.uiScale)
+            width: updateChannelCombo.width; height: updateChannelCombo.height
+            model: audio.feedbackDetectionComboModel
+            currentIndex: audio.feedbackDetectionEnabled ? 0 : 1
+            onActivated: {
+                if (currentIndex === 1) {
+                    audio.feedbackDetectionEnabled = false;
+                } else {
+                    audio.feedbackDetectionEnabled = true;
+                }
+            }
+            font.family: "Poppins"
+        }
+
+        Text {
+            anchors.verticalCenter: feedbackDetectionCombo.verticalCenter
+            x: 48 * virtualstudio.uiScale
+            text: "Feedback Detection"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        CheckBox {
+            id: showStartupSetup
+            checked: virtualstudio.showDeviceSetup
+            text: qsTr("Show device setup screen before connecting to a studio")
+            x: updateChannelCombo.x; y: feedbackDetectionCombo.y + (48 * virtualstudio.uiScale)
+            onClicked: { virtualstudio.showDeviceSetup = showStartupSetup.checkState == Qt.Checked; }
+            indicator: Rectangle {
+                implicitWidth: 16 * virtualstudio.uiScale
+                implicitHeight: 16 * virtualstudio.uiScale
+                x: showStartupSetup.leftPadding
+                y: parent.height / 2 - height / 2
+                radius: 3 * virtualstudio.uiScale
+                border.color: showStartupSetup.down || showStartupSetup.hovered ? checkboxPressedStroke : checkboxStroke
+
+                Rectangle {
+                    width: 10 * virtualstudio.uiScale
+                    height: 10 * virtualstudio.uiScale
+                    x: 3 * virtualstudio.uiScale
+                    y: 3 * virtualstudio.uiScale
+                    radius: 2 * virtualstudio.uiScale
+                    color: showStartupSetup.down || showStartupSetup.hovered ? checkboxPressedStroke : checkboxStroke
+                    visible: showStartupSetup.checked
+                }
+            }
+            contentItem: Text {
+                text: showStartupSetup.text
+                font.family: "Poppins"
+                font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+                leftPadding: showStartupSetup.indicator.width + showStartupSetup.spacing
+                color: textColour
+            }
+        }
+
+        Text {
+            anchors.verticalCenter: showStartupSetup.verticalCenter
+            x: 48 * virtualstudio.uiScale
+            text: "Device Setup"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        CheckBox {
+            id: showStartupWarnings
+            checked: virtualstudio.showWarnings
+            text: qsTr("Show recommendations on startup again next time")
+            x: updateChannelCombo.x; y: showStartupSetup.y + (48 * virtualstudio.uiScale)
+            onClicked: { virtualstudio.showWarnings = showStartupWarnings.checkState == Qt.Checked; }
+            indicator: Rectangle {
+                implicitWidth: 16 * virtualstudio.uiScale
+                implicitHeight: 16 * virtualstudio.uiScale
+                x: showStartupWarnings.leftPadding
+                y: parent.height / 2 - height / 2
+                radius: 3 * virtualstudio.uiScale
+                border.color: showStartupWarnings.down || showStartupWarnings.hovered ? checkboxPressedStroke : checkboxStroke
+
+                Rectangle {
+                    width: 10 * virtualstudio.uiScale
+                    height: 10 * virtualstudio.uiScale
+                    x: 3 * virtualstudio.uiScale
+                    y: 3 * virtualstudio.uiScale
+                    radius: 2 * virtualstudio.uiScale
+                    color: showStartupWarnings.down || showStartupWarnings.hovered ? checkboxPressedStroke : checkboxStroke
+                    visible: showStartupWarnings.checked
+                }
+            }
+            contentItem: Text {
+                text: showStartupWarnings.text
+                font.family: "Poppins"
+                font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+                leftPadding: showStartupWarnings.indicator.width + showStartupWarnings.spacing
+                color: textColour
+            }
+        }
+
+        Text {
+            anchors.verticalCenter: showStartupWarnings.verticalCenter
+            x: 48 * virtualstudio.uiScale
+            text: "Recommendations"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+    }
+
+    Rectangle {
+        id: profileSettingsView
+        width: 0.8 * parent.width
+        height: parent.height - header.height
+        x: 0.2 * window.width
+        y: header.height
+        color: backgroundColour
+        visible: settingsGroupView == "Profile"
+
+        Image {
+            id: profilePicture
+            width: 96; height: 96
+            y: 60 * virtualstudio.uiScale
+            source: virtualstudio.userMetadata.picture ? virtualstudio.userMetadata.picture : ""
+            anchors.horizontalCenter: parent.horizontalCenter
+            fillMode: Image.PreserveAspectCrop
+        }
+
+        Text {
+            id: displayName
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: profilePicture.bottom
+            text: virtualstudio.userMetadata.user_metadata ? ( virtualstudio.userMetadata.user_metadata.display_name ? virtualstudio.userMetadata.user_metadata.display_name : virtualstudio.userMetadata.nickname ) : virtualstudio.userMetadata.name || ""
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        Text {
+            id: email
+            anchors.horizontalCenter: parent.horizontalCenter
+            anchors.top: displayName.bottom
+            text: virtualstudio.userMetadata.email ? virtualstudio.userMetadata.email : ""
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        Button {
+            id: editButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: editButton.down ? buttonPressedColour : (editButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: editButton.down ? buttonPressedStroke : (editButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: { virtualstudio.editProfile(); }
+            anchors.horizontalCenter: parent.horizontalCenter
+            y: email.y + (56 * virtualstudio.uiScale)
+            width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            Text {
+                text: "Edit Profile"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+
+        Button {
+            id: logoutButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: logoutButton.down ? buttonPressedColour : (logoutButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: logoutButton.down ? buttonPressedStroke : (logoutButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: { virtualstudio.showFirstRun = false; virtualstudio.logout(); }
+            anchors.horizontalCenter: parent.horizontalCenter
+            y: editButton.y + (48 * virtualstudio.uiScale)
+            width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            Text {
+                text: "Log Out"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+
+        Button {
+            id: testModeButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: testModeButton.down ? buttonPressedColour : (testModeButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: testModeButton.down ? buttonPressedStroke : (testModeButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: {
+                virtualstudio.testMode = !virtualstudio.testMode;
+
+                // behave like "Cancel" and switch back to browse mode
+                audio.stopAudio();
+                virtualstudio.windowState = "browse";
+                virtualstudio.loadSettings();
+                audio.validateDevices();
+            }
+            anchors.horizontalCenter: parent.horizontalCenter
+            y: logoutButton.y + (48 * virtualstudio.uiScale)
+            width: 260 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+            visible: virtualstudio.userMetadata.email ? ( virtualstudio.userMetadata.email.endsWith("@jacktrip.org") ? true : false ) : false
+            Text {
+                text: virtualstudio.testMode ? "Switch to Prod Mode" : "Switch to Test Mode"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+    }
+
+    Rectangle {
+        x: -1; y: parent.height - (36 * virtualstudio.uiScale)
+        width: parent.width; height: (36 * virtualstudio.uiScale)
+        border.color: "#33979797"
+        color: backgroundColour
+
+        Button {
+            id: cancelButton
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: cancelButton.down ? buttonPressedColour : (cancelButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: cancelButton.down ? buttonPressedStroke : (cancelButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: {
+                audio.stopAudio();
+                virtualstudio.windowState = "browse";
+                virtualstudio.loadSettings();
+                audio.validateDevices();
+            }
+            anchors.verticalCenter: parent.verticalCenter
+            x: parent.width - (230 * virtualstudio.uiScale)
+            width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+            Text {
+                text: "Cancel"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: textColour
+            }
+        }
+
+        Button {
+            id: saveButton
+            enabled: !Boolean(audio.devicesError)
+            background: Rectangle {
+                radius: 6 * virtualstudio.uiScale
+                color: saveButton.down ? buttonPressedColour : (saveButton.hovered ? buttonHoverColour : buttonColour)
+                border.width: 1
+                border.color: saveButton.down ? buttonPressedStroke : (saveButton.hovered ? buttonHoverStroke : buttonStroke)
+            }
+            onClicked: {
+                audio.stopAudio();
+                virtualstudio.windowState = "browse";
+                virtualstudio.saveSettings();
+            }
+            anchors.verticalCenter: parent.verticalCenter
+            x: parent.width - (119 * virtualstudio.uiScale)
+            width: buttonWidth * virtualstudio.uiScale; height: buttonHeight * virtualstudio.uiScale
+            Text {
+                text: "Save"
+                font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+                anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter }
+                color: Boolean(audio.devicesError) ? disabledButtonTextColour : textColour
+            }
+        }
+
+        DeviceWarning {
+            id: deviceWarning
+            x: (0.2 * window.width) + 16 * virtualstudio.uiScale
+            anchors.verticalCenter: parent.verticalCenter
+            visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+        }
+    }
+
+    Connections {
+        target: audio
+        // anything that sets currentIndex to the value of a function needs
+        // to be manually updated whenever there is a change to any vars it uses
+        function onBufferSizeChanged() {
+            bufferCombo.currentIndex = getCurrentBufferSizeIndex();
+        }
+        function onAudioBackendChanged() {
+            backendCombo.currentIndex = getCurrentAudioBackendIndex();
+        }
+    }
+}
diff --git a/src/gui/Setup.qml b/src/gui/Setup.qml
new file mode 100644 (file)
index 0000000..969325d
--- /dev/null
@@ -0,0 +1,201 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    property int fontBig: 20
+    property int fontMedium: 13
+    property int fontSmall: 11
+    property int fontExtraSmall: 8
+
+    property int leftMargin: 48
+    property int rightMargin: 16
+
+    property string strokeColor: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string buttonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
+    property string buttonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"  
+    property string buttonPressedColour: virtualstudio.darkMode ? "#524F4F" : "#DEE0E0"
+    property string buttonStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+    property string buttonHoverStroke: virtualstudio.darkMode ? "#7B7777" : "#BABCBC"  
+    property string buttonPressedStroke: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+    property string saveButtonBackgroundColour: "#F2F3F3"
+    property string saveButtonPressedColour: "#E7E8E8" 
+    property string saveButtonStroke: "#EAEBEB"
+    property string saveButtonPressedStroke: "#B0B5B5"
+    property string saveButtonText: "#DB0A0A"
+    property string checkboxStroke: "#0062cc"
+    property string checkboxPressedStroke: "#007AFF"
+    property string disabledButtonText: virtualstudio.darkMode ? "#827D7D" : "#BABCBC"
+
+    Item {
+        id: setupItem
+        width: parent.width; height: parent.height
+
+        property bool isUsingRtAudio: audio.audioBackend == "RtAudio"
+
+        Text {
+            id: pageTitle
+            x: 16 * virtualstudio.uiScale;
+            y: 16 * virtualstudio.uiScale
+            text: "Choose your audio devices"
+            font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        DeviceRefreshButton {
+            id: refreshButton
+            anchors.right: parent.right
+            anchors.rightMargin: rightMargin * virtualstudio.uiScale
+            anchors.verticalCenter: pageTitle.verticalCenter
+            visible: parent.isUsingRtAudio
+            enabled: audio.audioReady && !audio.scanningDevices
+        }
+
+        Text {
+            text: "Restarting Audio"
+            anchors.verticalCenter: pageTitle.verticalCenter;
+            anchors.right: refreshButton.left;
+            anchors.rightMargin: 16 * virtualstudio.uiScale;
+            font { family: "Poppins"; pixelSize: fontExtraSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            visible: !audio.audioReady
+        }
+
+        AudioSettings {
+            id: audioSettings
+            width: parent.width
+            anchors.top: pageTitle.bottom
+            anchors.topMargin: 16 * virtualstudio.uiScale
+        }
+
+        Rectangle {
+            id: headerBorder
+            width: parent.width
+            height: 1
+            anchors.top: audioSettings.top
+            color: strokeColor
+        }
+
+        Rectangle {
+            id: footerBorder
+            width: parent.width
+            height: 1
+            anchors.top: audioSettings.bottom
+            color: strokeColor
+        }
+
+        Rectangle {
+            property int footerHeight: (30 + (rightMargin * 2)) * virtualstudio.uiScale;
+            x: -1; y: parent.height - footerHeight;
+            width: parent.width; height: footerHeight;
+            border.color: "#33979797"
+            color: backgroundColour
+
+            Button {
+                id: backButton
+                background: Rectangle {
+                    radius: 6 * virtualstudio.uiScale
+                    color: backButton.down ? buttonPressedColour : buttonColour
+                    border.width: 1
+                    border.color: backButton.down || backButton.hovered ? buttonPressedStroke : buttonStroke
+                }
+                onClicked: { virtualstudio.windowState = "browse"; virtualstudio.studioToJoin = ""; audio.stopAudio(); }
+                anchors.left: parent.left
+                anchors.leftMargin: 16 * virtualstudio.uiScale
+                anchors.bottomMargin: rightMargin * virtualstudio.uiScale
+                anchors.bottom: parent.bottom
+                width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                Text {
+                    text: "Back"
+                    font.family: "Poppins"
+                    font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
+                    color: textColour
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.verticalCenter: parent.verticalCenter
+                }
+            }
+
+            DeviceWarning {
+                id: deviceWarning
+                anchors.left: backButton.right
+                anchors.leftMargin: 16 * virtualstudio.uiScale
+                anchors.verticalCenter: backButton.verticalCenter
+                visible: Boolean(audio.devicesError) || Boolean(audio.devicesWarning)
+            }
+
+            Button {
+                id: saveButton
+                background: Rectangle {
+                    radius: 6 * virtualstudio.uiScale
+                    color: saveButton.down ? saveButtonPressedColour : saveButtonBackgroundColour
+                    border.width: 1
+                    border.color: saveButton.down || saveButton.hovered ? saveButtonPressedStroke : saveButtonStroke
+                }
+                enabled: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady
+                onClicked: {
+                    audio.stopAudio(true);
+                    virtualstudio.studioToJoin = virtualstudio.currentStudio.id;
+                    virtualstudio.windowState = "connected";
+                    virtualstudio.saveSettings();
+                    virtualstudio.joinStudio();
+                }
+                anchors.right: parent.right
+                anchors.rightMargin: rightMargin * virtualstudio.uiScale
+                anchors.bottomMargin: rightMargin * virtualstudio.uiScale
+                anchors.bottom: parent.bottom
+                width: 150 * virtualstudio.uiScale; height: 30 * virtualstudio.uiScale
+                Text {
+                    text: "Connect to Studio"
+                    font.family: "Poppins"
+                    font.pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale
+                    font.weight: Font.Bold
+                    color: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady ? saveButtonText : disabledButtonText
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.verticalCenter: parent.verticalCenter
+                }
+            }
+
+            CheckBox {
+                id: showAgainCheckbox
+                checked: virtualstudio.showDeviceSetup
+                visible: audio.backendAvailable
+                text: qsTr("Ask again next time")
+                anchors.right: saveButton.left
+                anchors.rightMargin: 16 * virtualstudio.uiScale
+                anchors.verticalCenter: saveButton.verticalCenter
+                onClicked: { virtualstudio.showDeviceSetup = showAgainCheckbox.checkState == Qt.Checked }
+                indicator: Rectangle {
+                    implicitWidth: 16 * virtualstudio.uiScale
+                    implicitHeight: 16 * virtualstudio.uiScale
+                    x: showAgainCheckbox.leftPadding
+                    y: parent.height / 2 - height / 2
+                    radius: 3 * virtualstudio.uiScale
+                    border.color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke
+
+                    Rectangle {
+                        width: 10 * virtualstudio.uiScale
+                        height: 10 * virtualstudio.uiScale
+                        x: 3 * virtualstudio.uiScale
+                        y: 3 * virtualstudio.uiScale
+                        radius: 2 * virtualstudio.uiScale
+                        color: showAgainCheckbox.down || showAgainCheckbox.hovered ? checkboxPressedStroke : checkboxStroke
+                        visible: showAgainCheckbox.checked
+                    }
+                }
+
+                contentItem: Text {
+                    text: showAgainCheckbox.text
+                    font.family: "Poppins"
+                    font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.verticalCenter: parent.verticalCenter
+                    leftPadding: showAgainCheckbox.indicator.width + showAgainCheckbox.spacing
+                    color: textColour
+                }
+            }
+        }
+    }
+}
diff --git a/src/gui/Studio.qml b/src/gui/Studio.qml
new file mode 100644 (file)
index 0000000..726ac52
--- /dev/null
@@ -0,0 +1,402 @@
+import QtQuick
+import QtQuick.Controls
+import Qt5Compat.GraphicalEffects
+import VS 1.0
+
+Rectangle {
+    width: 664; height: 83 * virtualstudio.uiScale
+    radius: 6 * virtualstudio.uiScale
+    color: backgroundColour
+
+    property string serverLocation: "Germany - Berlin"
+    property string flagImage: "flags/DE.svg"
+    property string hostname: "app.jacktrip.com"
+    property string studioName: "Test Studio"
+    property string studioId: ""
+    property string streamId: ""
+    property string inviteKeyString: ""
+    property int sampleRate: 48000
+    property bool publicStudio: false
+    property bool admin: false
+    property bool available: true
+    property bool connected: false
+    property bool inviteCopied: false
+
+    property int leftMargin: 81
+    property int topMargin: 13
+    property int bottomToolTipMargin: 8
+    property int rightToolTipMargin: 4
+
+    property real fontBig: 18
+    property real fontMedium: 11
+    property real fontSmall: 8
+
+    property string backgroundColour: virtualstudio.darkMode ? "#494646" : "#F4F6F6"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+    property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1"
+    property string inviteToolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
+    property string inviteToolTipTextColour: textColour
+    property string inviteCopiedBackgroundColour: "#57B147"
+    property string inviteCopiedTextColour: "#FAFBFB"
+    property string tooltipStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+
+    property string baseButtonColour: virtualstudio.darkMode ? "#F0F1F1" : "#EAEBEB"
+    property string baseButtonHoverColour: virtualstudio.darkMode ? "#CCCDCD" : "#D3D3D3"
+    property string baseButtonPressedColour: virtualstudio.darkMode ? "#E4E5E5" : "#EAEBEB"
+    property string baseButtonStroke: virtualstudio.darkMode ? "#8B8D8D" : "#949494"
+
+    property string joinAvailableColour: virtualstudio.darkMode ? "#E2EBE0" : "#C4F4BE"
+    property string joinAvailableHoverColour: virtualstudio.darkMode ? "#BAC7B8" : "#B0DCAB"
+    property string joinAvailablePressedColour: virtualstudio.darkMode ? "#D8E2D6" : "#BAE8B5"
+    property string joinAvailableStroke: virtualstudio.darkMode ? "#748F70" : "#5DB752"
+
+    property string joinUnavailableColour: baseButtonColour
+    property string joinUnavailableHoverColour: baseButtonHoverColour
+    property string joinUnavailablePressedColour: baseButtonPressedColour
+    property string joinUnavailableStroke: baseButtonStroke
+
+    property string startColour: virtualstudio.darkMode ? "#E2EBE0" : "#C4F4BE"
+    property string startHoverColour: virtualstudio.darkMode ? "#BAC7B8" : "#B0DCAB"
+    property string startPressedColour: virtualstudio.darkMode ? "#D8E2D6" : "#BAE8B5"
+    property string startStroke: virtualstudio.darkMode ? "#748F70" : "#5DB752"
+
+    property string manageColour: baseButtonColour
+    property string manageHoverColour: baseButtonHoverColour
+    property string managePressedColour: baseButtonPressedColour
+    property string manageStroke: baseButtonStroke
+
+    property string leaveColour: virtualstudio.darkMode ? "#FCB6B6" : "#FCB6B6"
+    property string leaveHoverColour: virtualstudio.darkMode ? "#D49696" : "#E3A4A4"
+    property string leavePressedColour: virtualstudio.darkMode ? "#F2AEAE" : "#EFADAD"
+    property string leaveStroke: virtualstudio.darkMode ? "#A65959" : "#C95E5E"
+
+    property string studioStroke: virtualstudio.darkMode ? "#80827D7D" : "#34979797"
+
+    border.width: 1
+    border.color: studioStroke
+
+    Clipboard {
+        id: clipboard
+    }
+
+    Rectangle {
+        id: shadow
+        anchors.fill: parent
+        color: "transparent"
+        radius: 6
+    }
+
+    Rectangle {
+        width: 12 * virtualstudio.uiScale; height: parent.height
+        radius: width / 2
+        color: available ? "#0C1424" : "#B3B3B3"
+    }
+
+    Image {
+        id: wedge
+        source: available ? "wedge.svg" : "wedge_inactive.svg"
+        x: 6; y: 0; width: 52 * virtualstudio.uiScale; height: 83 * virtualstudio.uiScale
+        sourceSize: Qt.size(wedge.width,wedge.height)
+        fillMode: Image.PreserveAspectFit
+        smooth: true
+    }
+
+    Image {
+        id: studioLogo
+        source: "logo.svg"
+        x: 8; y: 11; width: 32 * virtualstudio.uiScale; height: 59 * virtualstudio.uiScale
+        sourceSize: Qt.size(studioLogo.width,studioLogo.height)
+        fillMode: Image.PreserveAspectFit
+        smooth: true
+    }
+
+    Rectangle {
+        x: 33 * virtualstudio.uiScale; y: 8 * virtualstudio.uiScale
+        width: 32 * virtualstudio.uiScale; height: width
+        radius: width / 2
+        color: available ? "#0C1424" : "#B3B3B3"
+    }
+
+    Image {
+        id: flag
+        source: flagImage
+        x: 30 * virtualstudio.uiScale; y: 9 * virtualstudio.uiScale
+        width: 40 * virtualstudio.uiScale; height: width / 4 * 3
+        fillMode: Image.PreserveAspectCrop
+        layer.enabled: true
+        layer.effect: OpacityMask {
+            maskSource: mask
+        }
+
+        AppIcon {
+            id: defaultFlag
+            anchors.fill: parent
+            width: 32 * virtualstudio.uiScale
+            height: 32 * virtualstudio.uiScale
+            icon.source: "language.svg"
+            color: "white"
+            visible: flag.status != Image.Ready
+        }
+    }
+
+    Rectangle {
+        id: mask
+        x: 0 ; y: 0 ; width: flag.width; height: flag.height
+        visible: false
+        color: "#00000000"
+        Rectangle {
+            x: 7 * virtualstudio.uiScale; y: 3 * virtualstudio.uiScale
+            width:24 * virtualstudio.uiScale; height: width
+            radius: width / 2
+        }
+    }
+
+    Text {
+        x: leftMargin * virtualstudio.uiScale; y: 11 * virtualstudio.uiScale;
+        width: (admin || connected) ? parent.width - (310 * virtualstudio.uiScale) : parent.width - (233 * virtualstudio.uiScale)
+        text: studioName
+        fontSizeMode: Text.HorizontalFit
+        font { family: "Poppins"; weight: Font.Bold; pixelSize: fontBig * virtualstudio.fontScale * virtualstudio.uiScale }
+        elide: Text.ElideRight
+        verticalAlignment: Text.AlignVCenter
+        color: textColour
+    }
+
+    Rectangle {
+        id: publicRect
+        x: leftMargin * virtualstudio.uiScale; y: 52 * virtualstudio.uiScale
+        width: 14 * virtualstudio.uiScale; height: width
+        radius: 2 * virtualstudio.uiScale
+        color: publicStudio ? "#0095FF" : "#FF9800"
+        Image {
+            id: pubPriv
+            source: publicStudio ? "public.svg" : "private.svg"
+            x: 1 * virtualstudio.uiScale; y: x; width: 12 * virtualstudio.uiScale; height: width
+            sourceSize: Qt.size(pubPriv.width,pubPriv.height)
+            fillMode: Image.PreserveAspectFit
+            smooth: true
+        }
+    }
+
+    Text {
+        anchors.verticalCenter: publicRect.verticalCenter
+        x: (leftMargin + 22) * virtualstudio.uiScale
+        width: (admin || connected) ? parent.width - (255 * virtualstudio.uiScale) : parent.width - (178 * virtualstudio.uiScale)
+        text: publicStudio ? "Public hub studio " + serverLocation : "Private hub studio " + serverLocation
+        font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+        elide: Text.ElideRight
+        color: textColour
+    }
+
+    Button {
+        id: joinButton
+        x: (admin || connected) ? parent.width - (219 * virtualstudio.uiScale) : parent.width - (142 * virtualstudio.uiScale)
+        y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width
+        background: Rectangle {
+            radius: width / 2
+            color: available ? (joinButton.down ? joinAvailablePressedColour : (joinButton.hovered ? joinAvailableHoverColour : joinAvailableColour))
+                : (joinButton.down ? joinUnavailablePressedColour : (joinButton.hovered ? joinUnavailableHoverColour : joinUnavailableColour))
+            border.width: joinButton.down ? 1 : 0
+            border.color: available ? joinAvailableStroke : joinUnavailableStroke
+        }
+        visible: !connected
+        onClicked: {
+            virtualstudio.studioToJoin = studioId;
+            virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
+            virtualstudio.joinStudio();
+        }
+        Image {
+            id: join
+            width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale
+            anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+            source: "join.svg"
+            sourceSize: Qt.size(join.width,join.height)
+            fillMode: Image.PreserveAspectFit
+            smooth: true
+        }
+    }
+
+    Button {
+        id: leaveButton
+        x: (admin || connected) ? parent.width - (219 * virtualstudio.uiScale) : parent.width - (142 * virtualstudio.uiScale)
+        y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width
+        background: Rectangle {
+            radius: width / 2
+            color: leaveButton.down ? leavePressedColour : (leaveButton.hovered ? leaveHoverColour : leaveColour)
+            border.width: leaveButton.down ? 1 : 0
+            border.color: leaveStroke
+        }
+        visible: connected
+        onClicked: {
+            virtualstudio.disconnect();
+        }
+        Image {
+            id: leave
+            width: 22 * virtualstudio.uiScale; height: 20 * virtualstudio.uiScale
+            anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+            source: "leave.svg"
+            sourceSize: Qt.size(leave.width,leave.height)
+            fillMode: Image.PreserveAspectFit
+            smooth: true
+        }
+    }
+
+    Text {
+        anchors.horizontalCenter: joinButton.horizontalCenter
+        y: 56 * virtualstudio.uiScale
+        text: connected ? "Leave" : "Join"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale}
+        visible: true
+        color: textColour
+    }
+
+    Button {
+        id: inviteButton
+        x: (admin || connected) ? parent.width - (142 * virtualstudio.uiScale) : parent.width - (65 * virtualstudio.uiScale)
+        y: topMargin * virtualstudio.uiScale; width: 40 * virtualstudio.uiScale; height: width
+        background: Rectangle {
+            radius: width / 2
+            color: inviteButton.down ? managePressedColour : (inviteButton.hovered ? manageHoverColour : manageColour)
+            border.width:  inviteButton.down ? 1 : 0
+            border.color: manageStroke
+        }
+        Timer {
+            id: copiedResetTimer
+            interval: 3000; running: false; repeat: false
+            onTriggered: inviteCopied = false;
+        }
+        onClicked: {
+            inviteCopied = true;
+            if (virtualstudio.testMode) {
+                hostname = "test.jacktrip.com";
+            }
+            if (!inviteKeyString) {
+                clipboard.setText(qsTr("https://" + hostname + "/studios/" + studioId + "?invited=true"));
+            } else {
+                clipboard.setText(qsTr("https://" + hostname + "/studios/" + studioId + "?invited=" + inviteKeyString));
+            }
+            copiedResetTimer.restart()
+        }
+        visible: true
+        Image {
+            id: shareImg
+            width: 24 * virtualstudio.uiScale; height: width
+            anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+            source: "share.svg"
+            sourceSize: Qt.size(shareImg.width,shareImg.height)
+            fillMode: Image.PreserveAspectFit
+            smooth: true
+        }
+        ToolTip {
+            parent: inviteButton
+            visible: !inviteCopied && inviteButton.hovered
+            bottomPadding: bottomToolTipMargin * virtualstudio.uiScale
+            rightPadding: rightToolTipMargin * virtualstudio.uiScale
+            delay: 100
+            contentItem: Rectangle {
+                color: inviteToolTipBackgroundColour
+                radius: 3
+                anchors.fill: parent
+                anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale
+                anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale
+                layer.enabled: true
+                border.width: 1
+                border.color: tooltipStroke
+
+                Text {
+                    anchors.centerIn: parent
+                    font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale}
+                    text: qsTr("Copy invite link for Studio")
+                    color: inviteToolTipTextColour
+                }
+            }
+            background: Rectangle {
+                color: "transparent"
+            }
+        }
+        ToolTip {
+            parent: inviteButton
+            visible: inviteCopied
+            bottomPadding: bottomToolTipMargin * virtualstudio.uiScale
+            rightPadding: rightToolTipMargin * virtualstudio.uiScale
+            delay: 100
+            contentItem: Rectangle {
+                color: inviteCopiedBackgroundColour
+                radius: 3
+                anchors.fill: parent
+                anchors.bottomMargin: bottomToolTipMargin * virtualstudio.uiScale
+                anchors.rightMargin: rightToolTipMargin * virtualstudio.uiScale
+                layer.enabled: true
+                border.width: 1
+                border.color: tooltipStroke
+
+                Text {
+                    anchors.centerIn: parent
+                    font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale}
+                    text: qsTr("📋 Copied invitation link to Clipboard")
+                    color: inviteCopiedTextColour
+                }
+            }
+            background: Rectangle {
+                color: "transparent"
+            }
+        }
+    }
+
+    Text {
+        anchors.horizontalCenter: inviteButton.horizontalCenter
+        y: 56 * virtualstudio.uiScale
+        text: "Invite"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        visible: true
+        color: textColour
+    }
+
+    Button {
+        id: manageButton
+        x: parent.width - (65 * virtualstudio.uiScale); y: topMargin * virtualstudio.uiScale
+        width: 40 * virtualstudio.uiScale; height: width
+        background: Rectangle {
+            radius: width / 2
+            color: manageButton.down ? managePressedColour : (manageButton.hovered ? manageHoverColour : manageColour)
+            border.width:  manageButton.down ? 1 : 0
+            border.color: manageStroke
+        }
+        onClicked: {
+            var url = "";
+            if (streamId === "") {
+                if (virtualstudio.testMode) {
+                    url = "https://test.jacktrip.com/studios/" + studioId;
+                } else {
+                    url = "https://app.jacktrip.com/studios/" + studioId;
+                }
+            } else {
+                if (virtualstudio.testMode) {
+                    url = "https://next-test.jacktrip.com/@" + streamId + "/dashboard";
+                } else {
+                    url = "https://www.jacktrip.com/@" + streamId + "/dashboard";
+                }
+            }
+            virtualstudio.openLink(qsTr(url));
+        }
+        visible: admin || connected
+        Image {
+            id: manageImg
+            width: 20 * virtualstudio.uiScale; height: width
+            anchors { verticalCenter: parent.verticalCenter; horizontalCenter: parent.horizontalCenter }
+            source: "manage.svg"
+            sourceSize: Qt.size(manageImg.width,manageImg.height)
+            fillMode: Image.PreserveAspectFit
+            smooth: true
+        }
+    }
+
+    Text {
+        anchors.horizontalCenter: manageButton.horizontalCenter
+        y: 56 * virtualstudio.uiScale
+        text: "Manage"
+        font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+        visible: admin || connected
+        color: textColour
+    }
+}
diff --git a/src/gui/VolumeSlider.qml b/src/gui/VolumeSlider.qml
new file mode 100644 (file)
index 0000000..1f2a467
--- /dev/null
@@ -0,0 +1,122 @@
+import QtQuick
+import QtQuick.Controls
+import QtQuick.Layouts
+
+Item {
+    width: parent.width
+    height: parent.height
+
+    required property string labelText
+    required property string tooltipText
+    required property bool sliderEnabled
+    property bool showLabel: true
+    property int fontSize: 8
+
+    property string iconColor: virtualstudio.darkMode ? "#ACAFAF" : "gray"
+    property string sliderPressedColour: virtualstudio.darkMode ? "#BABCBC" :  "#EAECEC"
+
+    Item {
+        anchors.fill: parent
+        anchors.verticalCenter: parent.verticalCenter
+
+        Text {
+            id: label
+            anchors.left: parent.left
+            anchors.verticalCenter: parent.verticalCenter
+            width: 40 * virtualstudio.uiScale
+            horizontalAlignment: Text.AlignRight
+            text: labelText
+            font {family: "Poppins"; weight: Font.Medium; pixelSize: fontSize * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            visible: showLabel
+        }
+
+        InfoTooltip {
+            id: tooltip
+            content: tooltipText
+            size: 16
+            anchors.left: label.right
+            anchors.leftMargin: 2 * virtualstudio.uiScale
+            anchors.verticalCenter: label.verticalCenter
+            visible: showLabel
+        }
+
+        AppIcon {
+            id: quieterIcon
+            anchors.left: showLabel ? tooltip.right : parent.left
+            anchors.leftMargin: showLabel ? 8 * virtualstudio.uiScale : 0
+            anchors.verticalCenter: label.verticalCenter
+            width: 16
+            height: 16
+            icon.source: "quiet.svg"
+            color: iconColor
+        }
+
+        AppIcon {
+            id: louderIcon
+            anchors.right: parent.right
+            anchors.verticalCenter: label.verticalCenter
+            width: 18
+            height: 18
+            icon.source: "loud.svg"
+            color: iconColor
+        }
+
+        Slider {
+            id: slider
+            value: labelText == "Monitor" ? audio.monitorVolume : (labelText == "Studio" ? audio.outputVolume : audio.inputVolume )
+            onMoved: {
+                if (labelText == "Monitor") {
+                    audio.monitorVolume = value;
+                } else if (labelText == "Studio") {
+                    audio.outputVolume = value;
+                } else {
+                    audio.inputVolume = value;
+                }
+            }
+            enabled: sliderEnabled
+            from: 0.0
+            to: 1.0
+            stepSize: 0.01
+            padding: 0
+            anchors.left: quieterIcon.right
+            anchors.leftMargin: 4 * virtualstudio.uiScale
+            anchors.right: louderIcon.left
+            anchors.rightMargin: 4 * virtualstudio.uiScale
+            anchors.verticalCenter: label.verticalCenter
+
+            background: Rectangle {
+                x: slider.leftPadding
+                y: slider.topPadding + slider.availableHeight / 2 - height / 2
+                implicitWidth: parent.width
+                implicitHeight: 8 * virtualstudio.uiScale
+                width: slider.availableWidth
+                height: implicitHeight
+                radius: 4
+                color: "gray"
+
+                Rectangle {
+                    width: slider.visualPosition * parent.width
+                    height: parent.height
+                    radius: 4
+                    gradient: Gradient {
+                        orientation: Gradient.Horizontal
+                        GradientStop { position: 0.0; color: sliderEnabled ? "#67C6F3" : "light gray" }
+                        GradientStop { position: 1.0; color: sliderEnabled ? "#00897b" : "light gray" }
+                    }
+                }
+            }
+
+            handle: Rectangle {
+                x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
+                y: slider.topPadding + slider.availableHeight / 2 - height / 2
+                implicitWidth: 18 * virtualstudio.uiScale
+                implicitHeight: 18 * virtualstudio.uiScale
+                radius: implicitWidth / 2
+                color: slider.pressed ? sliderPressedColour : "white"
+                border.width: 3
+                border.color: sliderEnabled ? "#00897b" : "light gray"
+            }
+        }
+    }
+}
diff --git a/src/gui/Web.qml b/src/gui/Web.qml
new file mode 100644 (file)
index 0000000..13927f8
--- /dev/null
@@ -0,0 +1,10 @@
+import QtQuick
+import QtQuick.Controls
+
+Loader {
+    anchors.fill: parent
+    source: "WebEngine.qml"
+
+    // TODO: Add support for QtWebView
+    // source: useWebEngine ? "WebEngine.qml" : "WebView.qml"
+}
diff --git a/src/gui/WebEngine.qml b/src/gui/WebEngine.qml
new file mode 100644 (file)
index 0000000..a45b459
--- /dev/null
@@ -0,0 +1,121 @@
+import QtQuick
+import QtQuick.Controls
+import QtWebEngine
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    function contentScriptFactory (port) {
+        return `
+            // add script tag for qwebchannel
+            document.head.addEventListener("initqwebchannel", () => {
+
+                var script = document.createElement("script");
+                script.onload = function () {
+                    var url = "ws://localhost:${port}";
+                    var socket = new WebSocket(url);
+                    
+                    socket.onclose = function() {
+                        console.error("[QT] web channel closed");
+                    };
+                    socket.onerror = function(event) {
+                        console.error("[QT] web channel error: " + event.type);
+                    };
+                    socket.onopen = function() {
+                        new QWebChannel(socket, function(channel) {
+                            console.log("[QT] Socket opened");
+
+                            // make core object accessible globally
+                            window.virtualstudio = channel.objects.virtualstudio;
+                            window.auth = channel.objects.auth;
+                            window.clipboard = channel.objects.clipboard;
+
+                            const event = new CustomEvent("qwebchannelinitialized");
+                            document.head.dispatchEvent(event);
+                            console.log("[QT] Dispatched qwebchannelinitialized event");
+                            console.log("[QT] Connected to WebChannel, ready to send/receive messages!");
+                        });
+                    }
+                }
+                script.setAttribute("src", "qrc:///qtwebchannel/qwebchannel.js");
+                script.setAttribute("type", "text/javascript");
+                document.head.appendChild(script);
+                console.log("[QT] Added qwebchannel initialization script to DOM.");
+            });
+            console.log("[QT] Added initqwebchannel event listener");
+        `
+    }
+
+    Rectangle {
+        id: web
+        anchors.fill: parent
+        color: backgroundColour
+
+        property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
+        property string studioId: virtualstudio.currentStudio.id
+
+        WebEngineView {
+            id: webEngineView
+            anchors.fill: parent
+            settings.javascriptCanAccessClipboard: true
+            settings.javascriptCanPaste: true
+            settings.screenCaptureEnabled: true
+            profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}`
+            url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}`
+
+            // useful for debugging
+            // onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID) {
+            //     console.log(level, message, lineNumber, sourceID);
+            // }
+
+            // useful for debugging
+            // onLoadingChanged: function(loadRequest) {
+            //     console.log("onLoadingChanged", loadRequest.errorCode, loadRequest.errorDomain, loadRequest.errorString, loadRequest.status, loadRequest.url);
+            // }
+
+            onContextMenuRequested: function(request) {
+                // this disables the default context menu: https://doc.qt.io/qt-6.2/qml-qtwebengine-contextmenurequest.html#accepted-prop
+                request.accepted = true;
+            }
+
+            onNewWindowRequested: function(request) {
+                Qt.openUrlExternally(request.requestedUrl);
+            }
+
+            onFeaturePermissionRequested: function(securityOrigin, feature) {
+                webEngineView.grantFeaturePermission(securityOrigin, feature, true);
+            }
+
+            onRenderProcessTerminated: function(terminationStatus, exitCode) {
+                var status = "";
+                switch (terminationStatus) {
+                case WebEngineView.NormalTerminationStatus:
+                    status = "(normal exit)";
+                    break;
+                case WebEngineView.AbnormalTerminationStatus:
+                    status = "(abnormal exit)";
+                    break;
+                case WebEngineView.CrashedTerminationStatus:
+                    status = "(crashed)";
+                    break;
+                case WebEngineView.KilledTerminationStatus:
+                    status = "(killed)";
+                    break;
+                }
+                console.log("Render process exited with code " + exitCode + " " + status);
+            }
+
+            onNavigationRequested: function(request) {
+                webEngineView.userScripts.collection = [
+                    {
+                        name: "script",
+                        sourceCode: contentScriptFactory(virtualstudio.webChannelPort),
+                        injectionPoint: WebEngineScript.DocumentReady,
+                        worldId: WebEngineScript.MainWorld
+                    }
+                ]
+            }
+        }
+    }
+}
diff --git a/src/gui/WebNull.qml b/src/gui/WebNull.qml
new file mode 100644 (file)
index 0000000..6f1c6de
--- /dev/null
@@ -0,0 +1,12 @@
+import QtQuick
+import QtQuick.Controls
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    Item {
+        id: webNull
+        anchors.fill: parent
+    }
+}
diff --git a/src/gui/WebSocketTransport.cpp b/src/gui/WebSocketTransport.cpp
new file mode 100644 (file)
index 0000000..d9c9688
--- /dev/null
@@ -0,0 +1,95 @@
+/****************************************************************************
+**
+** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com,
+*author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL21$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** As a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include "WebSocketTransport.h"
+
+#include <QDebug>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QtWebSockets/QWebSocket>
+
+/*!
+    \brief QWebChannelAbstractSocket implementation that uses a QWebSocket internally.
+    The transport delegates all messages received over the QWebSocket over its
+    textMessageReceived signal. Analogously, all calls to sendTextMessage will
+    be send over the QWebSocket to the remote client.
+*/
+
+QT_BEGIN_NAMESPACE
+
+/*!
+    Construct the transport object and wrap the given socket.
+    The socket is also set as the parent of the transport object.
+*/
+WebSocketTransport::WebSocketTransport(QWebSocket* socket)
+    : QWebChannelAbstractTransport(socket), m_socket(socket)
+{
+    connect(socket, &QWebSocket::textMessageReceived, this,
+            &WebSocketTransport::textMessageReceived);
+}
+
+/*!
+    Destroys the WebSocketTransport.
+*/
+WebSocketTransport::~WebSocketTransport() {}
+
+/*!
+    Serialize the JSON message and send it as a text message via the WebSocket to the
+   client.
+*/
+void WebSocketTransport::sendMessage(const QJsonObject& message)
+{
+    QJsonDocument doc(message);
+    m_socket->sendTextMessage(QString::fromUtf8(doc.toJson(QJsonDocument::Compact)));
+}
+
+/*!
+    Deserialize the stringified JSON messageData and emit messageReceived.
+*/
+void WebSocketTransport::textMessageReceived(const QString& messageData)
+{
+    QJsonParseError error;
+    QJsonDocument message = QJsonDocument::fromJson(messageData.toUtf8(), &error);
+    if (error.error) {
+        qWarning() << "Failed to parse text message as JSON object:" << messageData
+                   << "Error is:" << error.errorString();
+        return;
+    } else if (!message.isObject()) {
+        qWarning() << "Received JSON message that is not an object: " << messageData;
+        return;
+    }
+    emit messageReceived(message.object(), this);
+}
+
+QT_END_NAMESPACE
\ No newline at end of file
diff --git a/src/gui/WebSocketTransport.h b/src/gui/WebSocketTransport.h
new file mode 100644 (file)
index 0000000..4f93b23
--- /dev/null
@@ -0,0 +1,61 @@
+/****************************************************************************
+**
+** Copyright (C) 2014 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com,
+*author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt.io/licensing/
+**
+** This file is part of the QtWebChannel module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL21$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see http://www.qt.io/terms-conditions. For further
+** information use the contact form at http://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 2.1 or version 3 as published by the Free
+** Software Foundation and appearing in the file LICENSE.LGPLv21 and
+** LICENSE.LGPLv3 included in the packaging of this file. Please review the
+** following information to ensure the GNU Lesser General Public License
+** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
+** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** As a special exception, The Qt Company gives you certain additional
+** rights. These rights are described in The Qt Company LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef WEBSOCKETTRANSPORT_H
+#define WEBSOCKETTRANSPORT_H
+
+#include <QtWebChannel/QWebChannelAbstractTransport>
+
+QT_BEGIN_NAMESPACE
+
+class QWebSocket;
+class WebSocketTransport : public QWebChannelAbstractTransport
+{
+    Q_OBJECT
+   public:
+    explicit WebSocketTransport(QWebSocket* socket);
+    virtual ~WebSocketTransport();
+
+    void sendMessage(const QJsonObject& message) Q_DECL_OVERRIDE;
+
+   private Q_SLOTS:
+    void textMessageReceived(const QString& message);
+
+   private:
+    QWebSocket* m_socket;
+};
+
+QT_END_NAMESPACE
+
+#endif  // WEBSOCKETTRANSPORT_H
\ No newline at end of file
diff --git a/src/gui/WebView.qml b/src/gui/WebView.qml
new file mode 100644 (file)
index 0000000..871a1d9
--- /dev/null
@@ -0,0 +1,23 @@
+import QtQuick
+import QtQuick.Controls
+import QtWebView
+
+Item {
+    width: parent.width; height: parent.height
+    clip: true
+
+    Item {
+        id: web
+        anchors.fill: parent
+
+        property string accessToken: auth.isAuthenticated && Boolean(auth.accessToken) ? auth.accessToken : ""
+        property string studioId: virtualstudio.currentStudio.id
+
+        WebView {
+            id: webEngineView
+            anchors.fill: parent
+            httpUserAgent: `JackTrip/${virtualstudio.versionString}`
+            url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}`
+        }
+    }
+}
diff --git a/src/gui/about.cpp b/src/gui/about.cpp
new file mode 100644 (file)
index 0000000..5c25979
--- /dev/null
@@ -0,0 +1,104 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2020 Aaron Wyatt.
+
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#include "about.h"
+
+#include "../jacktrip_globals.h"
+#include "ui_about.h"
+
+#ifdef BUILD_TYPE
+const QString About::s_buildType = QStringLiteral(BUILD_TYPE);
+#else
+const QString About::s_buildType = QLatin1String("");
+#endif
+#ifdef BUILD_ID
+const QString About::s_buildID = QStringLiteral(BUILD_ID);
+#elif defined(JACKTRIP_BUILD_INFO)
+#define STR(s)       #s
+#define TO_STRING(s) STR(s)
+const QString About::s_buildID   = QLatin1String(TO_STRING(JACKTRIP_BUILD_INFO));
+#else
+const QString About::s_buildID = QLatin1String("");
+#endif
+
+About::About(QWidget* parent) : QDialog(parent), m_ui(new Ui::About)
+{
+    m_ui->setupUi(this);
+    connect(m_ui->closeButton, &QPushButton::clicked, this, [=]() {
+        this->done(0);
+    });
+
+    // Replace %VERSION% and %QTVERSION%
+    m_ui->aboutLabel->setText(
+        m_ui->aboutLabel->text().replace(QLatin1String("%VERSION%"), gVersion));
+    m_ui->aboutLabel->setText(
+        m_ui->aboutLabel->text().replace(QLatin1String("%QTVERSION%"), qVersion()));
+
+    // Replace %LICENSE%
+    QString licenseText;
+#if defined(_WIN32) && defined(RT_AUDIO)
+    licenseText = QLatin1String(
+        "This build of JackTrip includes support for ASIO. ASIO is a trademark and "
+        "software of Steinberg Media Technologies GmbH.</p><p></p><p>");
+#endif
+#ifdef QT_OPENSOURCE
+    licenseText += QLatin1String("This build of JackTrip is subject to LGPL license. ");
+#endif
+    m_ui->aboutLabel->setText(
+        m_ui->aboutLabel->text().replace(QLatin1String("%LICENSE%"), licenseText));
+
+    // Replace %BUILD%
+    QString buildString;
+    if (!s_buildType.isEmpty() || !s_buildID.isEmpty()) {
+        buildString = QStringLiteral("<br/>(");
+        if (!s_buildType.isEmpty()) {
+            buildString.append(s_buildType);
+            if (!s_buildID.isEmpty()) {
+                buildString.append(QStringLiteral(", build %1").arg(s_buildID));
+            }
+        } else {
+            buildString.append(QStringLiteral("Build %1").arg(s_buildID));
+        }
+        buildString.append(")");
+    }
+    m_ui->aboutLabel->setText(
+        m_ui->aboutLabel->text().replace(QLatin1String("%BUILD%"), buildString));
+
+#ifdef __APPLE__
+    m_ui->aboutImage->setPixmap(QPixmap(":/qjacktrip/about@2x.png"));
+#endif
+
+    aboutText.setHtml(m_ui->aboutLabel->text());
+    aboutText.setDefaultFont(m_ui->aboutLabel->font());
+}
+
+void About::resizeEvent(QResizeEvent* event)
+{
+    QDialog::resizeEvent(event);
+    aboutText.setTextWidth(m_ui->textLayout->geometry().width());
+    m_ui->aboutLabel->setMinimumHeight(aboutText.size().height());
+}
+
+About::~About() = default;
diff --git a/src/gui/about.h b/src/gui/about.h
new file mode 100644 (file)
index 0000000..28766b7
--- /dev/null
@@ -0,0 +1,56 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2020 Aaron Wyatt.
+
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#ifndef ABOUT_H
+#define ABOUT_H
+
+#include <QDialog>
+#include <QScopedPointer>
+#include <QTextDocument>
+
+namespace Ui
+{
+class About;
+}
+
+class About : public QDialog
+{
+    Q_OBJECT
+
+   public:
+    explicit About(QWidget* parent = nullptr);
+    ~About() override;
+
+    void resizeEvent(QResizeEvent* event) override;
+
+    static const QString s_buildType;
+    static const QString s_buildID;
+
+   private:
+    QScopedPointer<Ui::About> m_ui;
+    QTextDocument aboutText;
+};
+
+#endif  // ABOUT_H
diff --git a/src/gui/about.png b/src/gui/about.png
new file mode 100644 (file)
index 0000000..0f877b9
Binary files /dev/null and b/src/gui/about.png differ
diff --git a/src/gui/about.ui b/src/gui/about.ui
new file mode 100644 (file)
index 0000000..e644f2e
--- /dev/null
@@ -0,0 +1,154 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>About</class>
+ <widget class="QDialog" name="About">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>524</width>
+    <height>310</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>About</string>
+  </property>
+  <layout class="QHBoxLayout" name="horizontalLayout">
+   <item>
+    <layout class="QVBoxLayout" name="imageLayout">
+     <item>
+      <widget class="QLabel" name="aboutImage">
+       <property name="sizePolicy">
+        <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+         <horstretch>0</horstretch>
+         <verstretch>0</verstretch>
+        </sizepolicy>
+       </property>
+       <property name="autoFillBackground">
+        <bool>false</bool>
+       </property>
+       <property name="styleSheet">
+        <string notr="true">background-color: rgb(255,255,255);
+border: 4px solid black; </string>
+       </property>
+       <property name="frameShape">
+        <enum>QFrame::Box</enum>
+       </property>
+       <property name="lineWidth">
+        <number>3</number>
+       </property>
+       <property name="text">
+        <string/>
+       </property>
+       <property name="pixmap">
+        <pixmap resource="qjacktrip.qrc">:/qjacktrip/about.png</pixmap>
+       </property>
+       <property name="scaledContents">
+        <bool>true</bool>
+       </property>
+       <property name="margin">
+        <number>0</number>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="imageVerticalSpacer">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>20</width>
+         <height>40</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+    </layout>
+   </item>
+   <item>
+    <layout class="QVBoxLayout" name="textLayout">
+     <property name="sizeConstraint">
+      <enum>QLayout::SetMinimumSize</enum>
+     </property>
+     <item>
+      <widget class="QLabel" name="titleLabel">
+       <property name="styleSheet">
+        <string notr="true">font: 24pt;</string>
+       </property>
+       <property name="text">
+        <string>JackTrip</string>
+       </property>
+       <property name="margin">
+        <number>3</number>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QLabel" name="aboutLabel">
+       <property name="minimumSize">
+        <size>
+         <width>366</width>
+         <height>191</height>
+        </size>
+       </property>
+       <property name="text">
+        <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;A system for high quality audio network performance over the internet.&lt;/p&gt;&lt;p&gt;&lt;b&gt;Version %VERSION%&lt;/b&gt;%BUILD%&lt;br/&gt;Using Qt %QTVERSION%&lt;/p&gt;&lt;p&gt;Copyright © 2008-2020 Juan-Pablo Caceres, Chris Chafe, et al. SoundWIRE group at CCRMA, Stanford University.&lt;/p&gt;&lt;p&gt;Classic mode graphical user interface component originally released as QJackTrip, Copyright © 2020 Aaron Wyatt.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;Virtual Studio interface and integration Copyright © 2022-2023 JackTrip Labs, Inc.&lt;/p&gt;&lt;p&gt;&lt;/p&gt;&lt;p&gt;%LICENSE%JackTrip source code is released under MIT and GPL licenses. See LICENSE.md file for more information.&lt;/p&gt;&lt;p&gt;This is free software and is provided &amp;quot;as is&amp;quot;, without warranty of any kind.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+       </property>
+       <property name="alignment">
+        <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+       </property>
+       <property name="wordWrap">
+        <bool>true</bool>
+       </property>
+       <property name="margin">
+        <number>3</number>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <spacer name="textVerticalSpacer">
+       <property name="orientation">
+        <enum>Qt::Vertical</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>20</width>
+         <height>40</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <layout class="QHBoxLayout" name="buttonLayout">
+       <item>
+        <spacer name="buttonHorizontalSpacer">
+         <property name="orientation">
+          <enum>Qt::Horizontal</enum>
+         </property>
+         <property name="sizeHint" stdset="0">
+          <size>
+           <width>40</width>
+           <height>20</height>
+          </size>
+         </property>
+        </spacer>
+       </item>
+       <item>
+        <widget class="QPushButton" name="closeButton">
+         <property name="text">
+          <string>Close</string>
+         </property>
+        </widget>
+       </item>
+      </layout>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources>
+  <include location="qjacktrip.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/src/gui/about@2x.png b/src/gui/about@2x.png
new file mode 100644 (file)
index 0000000..f51508b
Binary files /dev/null and b/src/gui/about@2x.png differ
diff --git a/src/gui/alt/Jacktrip.ai b/src/gui/alt/Jacktrip.ai
new file mode 100644 (file)
index 0000000..fc14ad6
--- /dev/null
@@ -0,0 +1,1014 @@
+%PDF-1.6\r%âãÏÓ\r
+1 0 obj\r<</Metadata 2 0 R/OCProperties<</D<</ON[21 0 R]/Order 22 0 R/RBGroups[]>>/OCGs[21 0 R]>>/Pages 3 0 R/Type/Catalog>>\rendobj\r2 0 obj\r<</Length 38021/Subtype/XML/Type/Metadata>>stream\r
+<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
+<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 6.0-c002 79.164460, 2020/05/12-16:04:17        ">
+   <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+      <rdf:Description rdf:about=""
+            xmlns:dc="http://purl.org/dc/elements/1.1/"
+            xmlns:xmp="http://ns.adobe.com/xap/1.0/"
+            xmlns:xmpGImg="http://ns.adobe.com/xap/1.0/g/img/"
+            xmlns:xmpMM="http://ns.adobe.com/xap/1.0/mm/"
+            xmlns:stRef="http://ns.adobe.com/xap/1.0/sType/ResourceRef#"
+            xmlns:stEvt="http://ns.adobe.com/xap/1.0/sType/ResourceEvent#"
+            xmlns:illustrator="http://ns.adobe.com/illustrator/1.0/"
+            xmlns:xmpTPg="http://ns.adobe.com/xap/1.0/t/pg/"
+            xmlns:stDim="http://ns.adobe.com/xap/1.0/sType/Dimensions#"
+            xmlns:xmpG="http://ns.adobe.com/xap/1.0/g/"
+            xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
+         <dc:format>application/pdf</dc:format>
+         <dc:title>
+            <rdf:Alt>
+               <rdf:li xml:lang="x-default">Jacktrip</rdf:li>
+            </rdf:Alt>
+         </dc:title>
+         <xmp:CreatorTool>Adobe Illustrator 24.2 (Macintosh)</xmp:CreatorTool>
+         <xmp:CreateDate>2020-07-25T23:44:56+10:00</xmp:CreateDate>
+         <xmp:ModifyDate>2020-07-25T23:44:56+10:00</xmp:ModifyDate>
+         <xmp:MetadataDate>2020-07-25T23:44:56+10:00</xmp:MetadataDate>
+         <xmp:Thumbnails>
+            <rdf:Alt>
+               <rdf:li rdf:parseType="Resource">
+                  <xmpGImg:width>136</xmpGImg:width>
+                  <xmpGImg:height>256</xmpGImg:height>
+                  <xmpGImg:format>JPEG</xmpGImg:format>
+                  <xmpGImg:image>/9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA&#xA;AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK&#xA;DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f&#xA;Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgBAACIAwER&#xA;AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA&#xA;AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB&#xA;UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE&#xA;1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ&#xA;qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy&#xA;obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp&#xA;0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo&#xA;+DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7&#xA;FXYq7FXYq7FXYq7FXylrx8yfnZ+ZPmLSbnVrjS/Inli4Nl9Rtm4PPIrPHzYGqsztEzcnB4rQAbk4&#xA;qgNU03X/AMgdX0nzB5d1e7v/ACdd3S2uraNdsGHxAvUBAiciisVcICpFDUNTFX12rKyhlIZWFVYb&#xA;gg9xireKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV8q+Svrmkf8AOR3nfQ/Lrrd+XbiZ7vVnf/dM&#xA;x/eFYyD9pLid4qdwP8nFUP8A85Atf6j518meXNYK2nkm/vYGnvVBLGYy+lKHNfh9OKT4f9Ynemyr&#xA;6yAAFBsB0GKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KviGy8h+Q/Nn5p/mV/izWH0n6lrl19S&#xA;4XNvbep6t5depX6wknLj6a/Z6V3xVLPzK/Lf8t/K0GjXflbXH1W7uNQjhuIXurW4CxEFi3GCNGG4&#xA;AqdsVfeOKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvhyD8n/+Vi/mn+ZH+5b9F/ovXLr/AI9/&#xA;rHqfWLy5/wCLYePH0fetcVSv8x/yLHkC30jUTrZ1IXl/Ha+kLb6sVqC/IOJpf5fDFX3virsVdirs&#xA;VdirsVdirsVdirsVdirsVdirsVdir4Pu/wApda/MH80/zE/Rl5bWn6M1y89b6z6nxfWLy448eCv0&#xA;9I1riqUedvyS1/yGmlahqOoWtxHeXsdsgtfU5qxq/L94ij9nFX6D4q7FXYq7FXYq7FXYq7FXYq7F&#xA;XYq7FXYq7FXYq+Y/yY/8mn+b/wD23G/6jL7FVH/nJ7/jgeW/+2zF/wAm3xV9RYq7FXYq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXYq+Y/wAmP/Jp/m//ANtxv+oy+xVR/wCcnv8AjgeW/wDtsxf8m3xV9RYq&#xA;7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXxR/zkn5l85Qfndf6TpGtX1nFKtjHBbQXU0MQeWCMfZR&#xA;goqzb7YQLNIJp59H5G/MWO4nuY7tkuLpzJdTLdMHlckktIwNWNWJqfHL/wArNh4oW3PkT8w7oILq&#xA;5M4jbmglumcKw/aHImhx/KzXxQyr8ofMnnyH86vLujatr2oXCi+RLm3kvJ5YmBQtRlZyrDfKZRMT&#xA;RZg2+8Mil2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV8Pf8AORP/AK0k3/GXS/8Ak3Fk8X1D3sZciyPN&#xA;u4jsVYV5B/8AWldF/wC2jF/yZzV5/rLlY/pfeeUs3Yq7FXYq7FXYq7FXYq7FXYq7FXYq7FXw9/zk&#xA;T/60k3/GXS/+TcWTxfUPexlyLI827iOxVhXkH/1pXRf+2jF/yZzV5/rLlY/pfeeUs3Yq7FXYq7FX&#xA;Yq7FXYq7FXYq7FXYq7FXw9/zkT/60k3/ABl0v/k3Fk8X1D3sZciyPNu4jsVYV5B/9aV0X/toxf8A&#xA;JnNXn+suVj+l955SzdirsVdirsVdirsVdirsVdirsVdirsVfD3/ORP8A60k3/GXS/wDk3Fk8X1D3&#xA;sZciyPNu4jsVYV5B/wDWldF/7aMX/JnNXn+suVj+l955SzdirsVdirsVdirsVdirsVdirsVdirsV&#xA;fD3/ADkT/wCtJN/xl0v/AJNxZPF9Q97GXIsjzbuI7FWFeQf/AFpXRf8Atoxf8mc1ef6y5WP6X3nl&#xA;LN2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV8Pf85E/+tJN/xl0v/k3Fk8X1D3sZciyPNu4jsVYV5B/9&#xA;aV0X/toxf8mc1ef6y5WP6X3nlLN2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV8Tf85WafeaH+d1tr00R&#xA;e0vYbO7t2AorG1pFJHXpyBiBPswyUTRBQRYR1hrWlX9slzaXUcsTioIYVHswO4Psc20ZgiwXEIIV&#xA;LnU9OtYWnuLqKKJBVnZ1A/XhMgOaACxf8j7e481f85C6bf2EbfVbaeS+mkoaJBbxFVZvDm3Ffm2a&#xA;nLLikS5cRQfd+QZOxV2KuxV2KuxV2KuxV2KuxV2KuxV2KsV/Mf8ALTyv+YOgNo+vwkqp52l5FRbi&#xA;3k6c4nIbr0YEEHv2xV+fn5meTU8meetX8sR3RvY9NlVEuWT0y6vGsgqoLUID064qt/LjyafOnnbS&#xA;vLAu/qP6TkaM3ZT1fTCRtITw5Jy2T+YYq+9vyq/J/wAp/lvpMlpo6NPe3NDf6nPxM8xXovwgBI1/&#xA;ZQfTU74qznFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq/Pb/AJyR/wDJ2+af+M8P/UNFirv+cbv/&#xA;ACdvlb/jPN/1DS4q/QnFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FX57f85I/+Tt80/wDGeH/q&#xA;GixV3/ON3/k7fK3/ABnm/wCoaXFX6E4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq/Pb/nJH/y&#xA;dvmn/jPD/wBQ0WKu/wCcbv8Aydvlb/jPN/1DS4q/QnFXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq&#xA;7FX57f8AOSP/AJO3zT/xnh/6hosVd/zjd/5O3yt/xnm/6hpcVfoTirsVdirsVdirsVdirsVdirsV&#xA;dirsVdirsVdir89v+ckf/J2+af8AjPD/ANQ0WKu/5xu/8nb5W/4zzf8AUNLir9CcVdirsVdirsVd&#xA;irsVdirsVdirsVdirsVdirsVfnt/zkj/AOTt80/8Z4f+oaLFXf8AON3/AJO3yt/xnm/6hpcVfoTi&#xA;rsVdirsVdirsVdirsVdirsVdirsVdirsVdir89v+ckf/ACdvmn/jPD/1DRYq7/nG7/ydvlb/AIzz&#xA;f9Q0uKv0JxV8q/8AQ8//AH5P/c0/7M8Vd/0PP/35P/c0/wCzPFXf9Dz/APfk/wDc0/7M8VTDy9/z&#xA;ml+l9f03ST5O9AajdQWnr/pLnw9eRY+fH6qtePKtKjFX01irsVdirsVdirsVdirsVdirsVfnt/zk&#xA;j/5O3zT/AMZ4f+oaLFXf843f+Tt8rf8AGeb/AKhpcVfoTir5U17/AJwgZLGWTQfM/q3qgmK3vbfh&#xA;G5/lMsbsU+fA4q+YdW0u/wBJ1S80vUIjBf2E0ltdwkglJYmKOtQSDRh2xVHeT/Kes+bvMlj5d0WN&#xA;ZdS1BykIduCAKpd3duyoilj7DbFX1L5O/wCcMrfR9V0rV9Q80PPd2FxDdvbW9qEjLwusgQSPIzFa&#xA;rSvEfIYq+lsVdirsVdirsVdirsVdirsVdir89f8AnI8k/nb5pqCP9Ii2P/MNF4Yq7/nHAkfnb5Wo&#xA;Cf8ASJdh/wAw0vjir9CsVdir88v+citObT/zp80wlQvqXS3IoAARcwpNXbx9Tf3xV3/OOuqxaX+d&#xA;XlW5kICyXT2m/TldwSWy/wDDSjFX6G4q7FXYq7FXYq7FXYq7FXYq7FXYq/N785dWTVvzW813yNzj&#xA;bU7iOJxuGSBzChG52KoMVZT/AM4q6c95+duiSBeUdlHd3MvsBayRqfoeRcVffGKuxV8Yf85o+WXs&#xA;fP8ApmvolLfWbERu/jcWjcXr/wA8pIsVeB6ZqF1pupWmo2jcLqymjuIH8JImDqfvGKv038r+YLLz&#xA;H5c0zXbI1tdTtormIVrQSKGKn3U7H3xVNMVdirsVdirsVdirsVdirsVY7+YnmyDyj5H1rzHKVB06&#xA;1kkgVujTkcIE/wBnKyrir80JZZJZHllYvJIxZ3Y1JYmpJPvir6X/AOcJPLLy695h8zSJ+6tLaPT4&#xA;HPQvcOJZOPuqwrX/AFsVfXWKuxV5F/zlD5Dk81/lbdzWkXqanoL/AKStVUfE0calbhB33iYtQdSo&#xA;xV8EYq+sP+cN/wA0Yntrn8vdSmpNGXvNCLnZkb4ri3WvdW/eqPd/DFX1JirsVdirsVdirsVdirsV&#xA;dir5F/5zF/NKK/1C28g6XNzg09xda06H4TcFf3UFR/vtWLMPEjuuKvmXFX6G/wDOPnkJ/JX5X6Xp&#xA;9zGYtTvQdQ1NCKMs9wAQjDxjiVEPuMVej4q7FXEBgQRUHYg9CMVfn7/zkN+VEv5f+eJhaQlfLmrM&#xA;9zpEgB4ICayW1d94S1B/klTirzfS9U1DStSttT06d7W/s5FmtriM0ZJENVYfTir78/I387dI/MnQ&#xA;gsrJa+Z7JB+lNOGwPb14ASSYmP0qdj2JVenYq7FXYq7FXYq7FXYq8f8A+cgPz30/8vtHk0zS5Y7j&#xA;zhex0tbfZxaow/3omHan7Cn7R/ya4q+ELq6ubu6mu7qVp7m4dpZ5pCWd5HJZmZjuSxNScVex/wDO&#xA;MP5SyedPOiazqEPLy5oEiT3JYfDPcj4oYB47jm/+SKH7QxV92Yq7FXYq7FWLfmV+Xeh+f/KtzoGr&#xA;LxEn7yzu1AMlvcKDwlSvhWjDuKjFX57+fvIPmPyN5jn0HXoPSuIvihmWpiniJIWWJiByVqfMHY0I&#xA;xVKtF1vVtD1S31bSLuSx1G0bnb3MLcXVun3EGhB2I2OKvrn8pv8AnLvQdWjg0rz2F0nVNkXVkB+p&#xA;zHoDIBUwMe/VO9V6Yq+h7K9s761iu7KeO6tJl5Q3ELrJG6nurqSpHyxVWxV2KuxVC6nqumaVYy3+&#xA;qXcNjZQjlNc3EixRqP8AKdyAMVfNv5t/85gafbRTaT+Xg+t3ZBR9enQiGPtW3icVkYfzOOPswxV8&#xA;oajqN/qV9Pf6hcSXd7cuZLi5mYvI7tuWZmqScVZL+WX5Z+Y/zC8yRaNo0dI1o9/fOD6NtDWhdz4n&#xA;9lerH6SFX6EeR/Jeh+S/LFl5d0WL07OzWhkahklkbd5ZGAFXc7n7hsAMVT7FXYq7FXYq7FWI/mX+&#xA;V/lX8w9COla7AfUjq1jfxUFxbSH9qNjXY0+JTsfnQhV8N/mp+SHnX8ur1/0jbm80Vm42utW6kwOC&#xA;fhEnUwuf5W/2JYb4q89xVP8Ayt5985+VJzN5c1m60wsQzxwyEROR/PEaxv8A7JTir1bRv+cx/wA2&#xA;bGNY76LTdWA+1LcW7RSHbxt5IU/4TFU8f/nN3zkYyI/LunLJQUZnnYVrvsGX9eKsd1v/AJzA/N7U&#xA;UZLNrDSFNQr2ltzcD53LTivvxxV5T5k85ea/M9yLnzBq11qkq19P6zK0ipXqI0J4oPZQMVSbFXqH&#xA;5R/kB5y/MO5juUjbTPLgb9/rE6niwHVbdDQyt8vhHc9sVfcHkH8vvK/kXQY9F8vWvoQD4p53o008&#xA;lKGSZ6Dkx+4dAAMVZJirsVdirsVdirsVdiqldWtrd20lrdwpcW0ylJoJVDo6nYqysCCD4HFXhP5h&#xA;f84geRdfkkvfLU7+W79/iMCL61kx6/3JKtHX/IbiP5cVeB+aP+cWPzi0JneHTI9ZtVr+/wBNlWU0&#xA;7fuX9OY/QhxV5zqflDzZpTFNU0W/sGXqLm2mhp1/nVfA4qlSI8jhEUu7GiqoqSfAAYqneleRPO2r&#xA;sF0vy/qN8W6G3tJpB1puVUgDbqcVek+Vf+cTfzd1tke9s4NCtWoTLfyrzp7Qw+q9fZwuKvfvy+/5&#xA;xK/Lvy28V5rZfzLqUe4+tKI7RT7WwLcv+ejMPbFXt0cccUaxxqEjQBURQAqqBQAAdAMVXYq7FXYq&#xA;7FX/2Q==</xmpGImg:image>
+               </rdf:li>
+            </rdf:Alt>
+         </xmp:Thumbnails>
+         <xmpMM:OriginalDocumentID>uuid:9E3E5C9A8C81DB118734DB58FDDE4BA7</xmpMM:OriginalDocumentID>
+         <xmpMM:DocumentID>xmp.did:49477e0f-276a-4cd3-8c1b-cab8815bed51</xmpMM:DocumentID>
+         <xmpMM:InstanceID>uuid:2e5c36ab-b08f-3840-94fd-90b8522b24ab</xmpMM:InstanceID>
+         <xmpMM:RenditionClass>proof:pdf</xmpMM:RenditionClass>
+         <xmpMM:DerivedFrom rdf:parseType="Resource">
+            <stRef:instanceID>uuid:dab6724e-c618-4184-9b2e-7d44879e5e5f</stRef:instanceID>
+            <stRef:documentID>xmp.did:008add62-65b7-3547-8416-6472cd533b2c</stRef:documentID>
+            <stRef:originalDocumentID>uuid:9E3E5C9A8C81DB118734DB58FDDE4BA7</stRef:originalDocumentID>
+            <stRef:renditionClass>proof:pdf</stRef:renditionClass>
+         </xmpMM:DerivedFrom>
+         <xmpMM:History>
+            <rdf:Seq>
+               <rdf:li rdf:parseType="Resource">
+                  <stEvt:action>saved</stEvt:action>
+                  <stEvt:instanceID>xmp.iid:49477e0f-276a-4cd3-8c1b-cab8815bed51</stEvt:instanceID>
+                  <stEvt:when>2020-07-25T23:40:45+10:00</stEvt:when>
+                  <stEvt:softwareAgent>Adobe Illustrator 24.2 (Macintosh)</stEvt:softwareAgent>
+                  <stEvt:changed>/</stEvt:changed>
+               </rdf:li>
+            </rdf:Seq>
+         </xmpMM:History>
+         <illustrator:StartupProfile>Basic RGB</illustrator:StartupProfile>
+         <illustrator:Type>Document</illustrator:Type>
+         <illustrator:CreatorSubTool>AIRobin</illustrator:CreatorSubTool>
+         <xmpTPg:NPages>1</xmpTPg:NPages>
+         <xmpTPg:HasVisibleTransparency>False</xmpTPg:HasVisibleTransparency>
+         <xmpTPg:HasVisibleOverprint>False</xmpTPg:HasVisibleOverprint>
+         <xmpTPg:MaxPageSize rdf:parseType="Resource">
+            <stDim:w>1024.000000</stDim:w>
+            <stDim:h>1024.000000</stDim:h>
+            <stDim:unit>Pixels</stDim:unit>
+         </xmpTPg:MaxPageSize>
+         <xmpTPg:PlateNames>
+            <rdf:Seq>
+               <rdf:li>Cyan</rdf:li>
+               <rdf:li>Magenta</rdf:li>
+               <rdf:li>Yellow</rdf:li>
+               <rdf:li>Black</rdf:li>
+            </rdf:Seq>
+         </xmpTPg:PlateNames>
+         <xmpTPg:SwatchGroups>
+            <rdf:Seq>
+               <rdf:li rdf:parseType="Resource">
+                  <xmpG:groupName>Default Swatch Group</xmpG:groupName>
+                  <xmpG:groupType>0</xmpG:groupType>
+                  <xmpG:Colorants>
+                     <rdf:Seq>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>White</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>255</xmpG:red>
+                           <xmpG:green>255</xmpG:green>
+                           <xmpG:blue>255</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>Black</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>0</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Red</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>255</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>0</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Yellow</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>255</xmpG:red>
+                           <xmpG:green>255</xmpG:green>
+                           <xmpG:blue>0</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Green</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>255</xmpG:green>
+                           <xmpG:blue>0</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Cyan</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>255</xmpG:green>
+                           <xmpG:blue>255</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Blue</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>255</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>RGB Magenta</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>255</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>255</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=193 G=39 B=45</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>193</xmpG:red>
+                           <xmpG:green>39</xmpG:green>
+                           <xmpG:blue>45</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=237 G=28 B=36</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>237</xmpG:red>
+                           <xmpG:green>28</xmpG:green>
+                           <xmpG:blue>36</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=241 G=90 B=36</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>241</xmpG:red>
+                           <xmpG:green>90</xmpG:green>
+                           <xmpG:blue>36</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=247 G=147 B=30</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>247</xmpG:red>
+                           <xmpG:green>147</xmpG:green>
+                           <xmpG:blue>30</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=251 G=176 B=59</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>251</xmpG:red>
+                           <xmpG:green>176</xmpG:green>
+                           <xmpG:blue>59</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=252 G=238 B=33</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>252</xmpG:red>
+                           <xmpG:green>238</xmpG:green>
+                           <xmpG:blue>33</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=217 G=224 B=33</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>217</xmpG:red>
+                           <xmpG:green>224</xmpG:green>
+                           <xmpG:blue>33</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=140 G=198 B=63</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>140</xmpG:red>
+                           <xmpG:green>198</xmpG:green>
+                           <xmpG:blue>63</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=57 G=181 B=74</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>57</xmpG:red>
+                           <xmpG:green>181</xmpG:green>
+                           <xmpG:blue>74</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=0 G=146 B=69</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>146</xmpG:green>
+                           <xmpG:blue>69</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=0 G=104 B=55</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>104</xmpG:green>
+                           <xmpG:blue>55</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=34 G=181 B=115</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>34</xmpG:red>
+                           <xmpG:green>181</xmpG:green>
+                           <xmpG:blue>115</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=0 G=169 B=157</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>169</xmpG:green>
+                           <xmpG:blue>157</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=41 G=171 B=226</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>41</xmpG:red>
+                           <xmpG:green>171</xmpG:green>
+                           <xmpG:blue>226</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=0 G=113 B=188</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>113</xmpG:green>
+                           <xmpG:blue>188</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=46 G=49 B=146</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>46</xmpG:red>
+                           <xmpG:green>49</xmpG:green>
+                           <xmpG:blue>146</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=27 G=20 B=100</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>27</xmpG:red>
+                           <xmpG:green>20</xmpG:green>
+                           <xmpG:blue>100</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=102 G=45 B=145</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>102</xmpG:red>
+                           <xmpG:green>45</xmpG:green>
+                           <xmpG:blue>145</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=147 G=39 B=143</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>147</xmpG:red>
+                           <xmpG:green>39</xmpG:green>
+                           <xmpG:blue>143</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=158 G=0 B=93</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>158</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>93</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=212 G=20 B=90</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>212</xmpG:red>
+                           <xmpG:green>20</xmpG:green>
+                           <xmpG:blue>90</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=237 G=30 B=121</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>237</xmpG:red>
+                           <xmpG:green>30</xmpG:green>
+                           <xmpG:blue>121</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=199 G=178 B=153</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>199</xmpG:red>
+                           <xmpG:green>178</xmpG:green>
+                           <xmpG:blue>153</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=153 G=134 B=117</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>153</xmpG:red>
+                           <xmpG:green>134</xmpG:green>
+                           <xmpG:blue>117</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=115 G=99 B=87</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>115</xmpG:red>
+                           <xmpG:green>99</xmpG:green>
+                           <xmpG:blue>87</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=83 G=71 B=65</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>83</xmpG:red>
+                           <xmpG:green>71</xmpG:green>
+                           <xmpG:blue>65</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=198 G=156 B=109</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>198</xmpG:red>
+                           <xmpG:green>156</xmpG:green>
+                           <xmpG:blue>109</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=166 G=124 B=82</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>166</xmpG:red>
+                           <xmpG:green>124</xmpG:green>
+                           <xmpG:blue>82</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=140 G=98 B=57</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>140</xmpG:red>
+                           <xmpG:green>98</xmpG:green>
+                           <xmpG:blue>57</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=117 G=76 B=36</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>117</xmpG:red>
+                           <xmpG:green>76</xmpG:green>
+                           <xmpG:blue>36</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=96 G=56 B=19</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>96</xmpG:red>
+                           <xmpG:green>56</xmpG:green>
+                           <xmpG:blue>19</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=66 G=33 B=11</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>66</xmpG:red>
+                           <xmpG:green>33</xmpG:green>
+                           <xmpG:blue>11</xmpG:blue>
+                        </rdf:li>
+                     </rdf:Seq>
+                  </xmpG:Colorants>
+               </rdf:li>
+               <rdf:li rdf:parseType="Resource">
+                  <xmpG:groupName>Cold</xmpG:groupName>
+                  <xmpG:groupType>1</xmpG:groupType>
+                  <xmpG:Colorants>
+                     <rdf:Seq>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>C=56 M=0 Y=20 K=0</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>101</xmpG:red>
+                           <xmpG:green>200</xmpG:green>
+                           <xmpG:blue>208</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>C=51 M=43 Y=0 K=0</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>131</xmpG:red>
+                           <xmpG:green>139</xmpG:green>
+                           <xmpG:blue>197</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>C=26 M=41 Y=0 K=0</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>186</xmpG:red>
+                           <xmpG:green>155</xmpG:green>
+                           <xmpG:blue>201</xmpG:blue>
+                        </rdf:li>
+                     </rdf:Seq>
+                  </xmpG:Colorants>
+               </rdf:li>
+               <rdf:li rdf:parseType="Resource">
+                  <xmpG:groupName>Grays</xmpG:groupName>
+                  <xmpG:groupType>1</xmpG:groupType>
+                  <xmpG:Colorants>
+                     <rdf:Seq>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=0 G=0 B=0</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>0</xmpG:red>
+                           <xmpG:green>0</xmpG:green>
+                           <xmpG:blue>0</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=26 G=26 B=26</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>26</xmpG:red>
+                           <xmpG:green>26</xmpG:green>
+                           <xmpG:blue>26</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=51 G=51 B=51</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>51</xmpG:red>
+                           <xmpG:green>51</xmpG:green>
+                           <xmpG:blue>51</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=77 G=77 B=77</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>77</xmpG:red>
+                           <xmpG:green>77</xmpG:green>
+                           <xmpG:blue>77</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=102 G=102 B=102</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>102</xmpG:red>
+                           <xmpG:green>102</xmpG:green>
+                           <xmpG:blue>102</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=128 G=128 B=128</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>128</xmpG:red>
+                           <xmpG:green>128</xmpG:green>
+                           <xmpG:blue>128</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=153 G=153 B=153</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>153</xmpG:red>
+                           <xmpG:green>153</xmpG:green>
+                           <xmpG:blue>153</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=179 G=179 B=179</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>179</xmpG:red>
+                           <xmpG:green>179</xmpG:green>
+                           <xmpG:blue>179</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=204 G=204 B=204</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>204</xmpG:red>
+                           <xmpG:green>204</xmpG:green>
+                           <xmpG:blue>204</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=230 G=230 B=230</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>230</xmpG:red>
+                           <xmpG:green>230</xmpG:green>
+                           <xmpG:blue>230</xmpG:blue>
+                        </rdf:li>
+                        <rdf:li rdf:parseType="Resource">
+                           <xmpG:swatchName>R=242 G=242 B=242</xmpG:swatchName>
+                           <xmpG:mode>RGB</xmpG:mode>
+                           <xmpG:type>PROCESS</xmpG:type>
+                           <xmpG:red>242</xmpG:red>
+                           <xmpG:green>242</xmpG:green>
+                           <xmpG:blue>242</xmpG:blue>
+                        </rdf:li>
+                     </rdf:Seq>
+                  </xmpG:Colorants>
+               </rdf:li>
+            </rdf:Seq>
+         </xmpTPg:SwatchGroups>
+         <pdf:Producer>Adobe PDF library 15.00</pdf:Producer>
+      </rdf:Description>
+   </rdf:RDF>
+</x:xmpmeta>
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                                                                                                    
+                           
+<?xpacket end="w"?>\rendstream\rendobj\r3 0 obj\r<</Count 1/Kids[5 0 R]/Type/Pages>>\rendobj\r5 0 obj\r<</ArtBox[260.012 83.8406 713.5 947.255]/BleedBox[0.0 0.0 1024.0 1024.0]/Contents 23 0 R/CropBox[0.0 0.0 1024.0 1024.0]/LastModified(D:20200725234456+10'00')/MediaBox[0.0 0.0 1024.0 1024.0]/Parent 3 0 R/PieceInfo<</Illustrator 7 0 R>>/Resources<</ColorSpace<</CS0 24 0 R>>/ExtGState<</GS0 25 0 R>>/Properties<</MC0 21 0 R>>>>/Thumb 26 0 R/TrimBox[0.0 0.0 1024.0 1024.0]/Type/Page>>\rendobj\r23 0 obj\r<</Filter/FlateDecode/Length 572>>stream\r
+H\89ÌTË\8eÛ0\f¼ë+ø\ 3a(R¢¨kÒ¢§m\11äÐ\ f\bú8l\17ØæÐßïÈv¼NÚÞ\8b\0\8aI\9bÃÇ\8c¸ÿt¤ýÓQèðîHi\7f<\v]®$\1cÚîNº^^Òþ\ 3^\7f»&\8fÌÖ3E\ 4w    ÚYfU£ÜY\8aÑÏ/éë\fu<\93L?:\1f?¦¢ô\8bÒ+åÉ\95ɽ\92\ 6\12þHÃ1N­ÊjJÏé\9c\15\9e,Zæc7\9d\0ÿL/\e\14ËÊVZ\ 3Tf¯(c\83·ë\8d\9bQCa¡@hÎ\91\95nÿ«ã2ç[AK4î\8e¼\95et¸BÎx²"LÈ\99\1e\81g<a\e\1f\8eÓ\813\8dq3\ 1u®ÔJù­:Ka\8b\8a1\f#*¦ÜgC\ 5pÑ       M\171Ø£{¼TÔµµ/©\82\93ÞÞ<ÏÉ\91lëiý\ro<ß\12]\12,AÀwðxJÙ\1fhû¿\8b\96¥ô\99\80¿ê8ËèÈéU\88 \b5\1fÂ:L¾°2ùf±->¿÷m¦\ 1źA+\90   K\85Æ×\99ì\9c\ebw\19\9a\984}¸Ó\18p¹B0\14\1d\16\7fDþ+°Vô\82\17]\94÷\8aG`Ñ ÃÌb0­A\94\98\16\9aÁæ\88I7\87vîm|ÎV\9d¬ãnÛ\bÒÚoÖ%iæ\0Èb\8f\v\ f\ 2\96ÈÅZ\81o6ôo¸âh¦+j2à©pFªq:¾\1c\8d-\eb\10£õ^jêÂ\92\15Á`<*HX;\1dj\8bV&²\a\8a>,\17\13ã\9c- 2T\93\8dV34c\83\99`\95\82Í\81yöA0·\9bñ¸\11L¡?Ülmà»·»-\93\v\vè\9aÆ    Z\1d%\8fû±<Ï`sw\86ä&È\83Õ\11¨\ f\94\rÕdcGÚI]ï\9f°\8bOé·\0\ 3\0¼%\16}\rendstream\rendobj\r26 0 obj\r<</BitsPerComponent 8/ColorSpace 27 0 R/Filter[/ASCII85Decode/FlateDecode]/Height 106/Length 325/Width 106>>stream\r
+8;Z]b5n\c'%#*ZsM8NlC%==^R8%K72"G.)t9]nQ^%[548!jiY1;/VUTKn49iTS--H
+:D61!;o5\DU2I]V:<Z%U4eYr\o[Zs%pKfJ>Un69cBV(lgd[ZmSZdHu6Bn3#,lHrg3
+#5(qW)EM(MXs_ImMkkW5OYfjN;Le:;<smU_:`h5aMf9q5OsEV^,Y=0u.&E8$qAjXI
+eLD<cGAro`F-<F,e!$\%cq]5)+^BL6PJXbBaP>'pp*td(FB$EU&31tPed@*(WDI8`
+ni_M!9C%2gd.4%pr\9@8lMBA"D:Vq.SLs+6kaQ^Wo8-bp.#N&;49bf4%(e_~>\rendstream\rendobj\r27 0 obj\r[/Indexed/DeviceRGB 255 28 0 R]\rendobj\r28 0 obj\r<</Filter[/ASCII85Decode/FlateDecode]/Length 428>>stream\r
+8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0
+b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup`
+E1r!/,*0[*9.aFIR2&b-C#s<Xl5FH@[<=!#6V)uDBXnIr.F>oRZ7Dl%MLY\.?d>Mn
+6%Q2oYfNRF$$+ON<+]RUJmC0I<jlL.oXisZ;SYU[/7#<&37rclQKqeJe#,UF7Rgb1
+VNWFKf>nDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j<etJICj7e7nPMb=O6S7UOH<
+PO7r\I.Hu&e0d&E<.')fERr/l+*W,)q^D*ai5<uuLX.7g/>$XKrcYp0n+Xl_nU*O(
+l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~>\rendstream\rendobj\r21 0 obj\r<</Intent 29 0 R/Name(Layer 1)/Type/OCG/Usage 30 0 R>>\rendobj\r29 0 obj\r[/View/Design]\rendobj\r30 0 obj\r<</CreatorInfo<</Creator(Adobe Illustrator 24.2)/Subtype/Artwork>>>>\rendobj\r25 0 obj\r<</AIS false/BM/Normal/CA 1.0/OP false/OPM 1/SA true/SMask/None/Type/ExtGState/ca 1.0/op false>>\rendobj\r24 0 obj\r[/ICCBased 31 0 R]\rendobj\r31 0 obj\r<</Filter/FlateDecode/Length 2574/N 3>>stream\r
+H\89\9c\96yTSw\16Ç\7f\9e\90\95°Ãc\r[\80°\ 6\905la\91\1d\ 4Q\bI\b\ 1\12BHØ\ 5AD\ 5\14ED\84ª\952ÖmtFOE\9d.®c­\ eÖ}êÒ\ 3õ0êè8´\16×\8e\9d\178G\9dNg¦Óï\1fï÷9÷wïïÝß½÷\9dó\0 '¥ªµÕ0\v\0\8dÖ ÏJ\8cÅ\16\15\14b¤    \0\ 3
\ 2\11\02y­.-;!\aà\92ÆK°ZÜ    ü\8b\9e^\a\90i½"LÊÀ0ðÿ\89-×é\r\0@\198\a(\94µr\9c;q®ª7èLö\19\9c\95&\86Q\13ëñ\ 4q¶4±j\9e½ç|æ9ÚÄ
+\8dV\81³)g\9dB£0ñi\9c\19\958#©8wÕ©\95õ8_Å٥ʨQãüÜ\14«QÊj\ 1@é&»A)/ÇÙ\ fgº>'K\82ó\ 2\0ÈtÕ;\ú\ e\e\94\r\ 6Ó¥$ÕºF½ZUnÀÜå\1e\98(4T\8c%)ë«\94\ 6\830C&¯\94é\15\98¤Z£\93i\e\ 1\98¿ó\9c8¦Úbx\91\83E¡ÁÁB\7f\1fÑ;\85ú¯\9b¿P¦ÞÎÓ\93̹\9e\vom?çW=
+\80x\16¯Íú·¶Ò-\0\8c¯\ 4Àòæ[\9bËû\00ñ¾\1d¾øÎ}ø¦y)7\18ta¾¾õõõ>j¥ÜÇTÐ7ú\9f\ e¿@ï¼ÏÇtÜ\9bò`qÊ2\99±Ê\80\99ê&¯®ª6ê±Z\9dL®Ä\84?\1dâ_\1døóyxg)Ë\94\16\8fÈçL­UáíÖ*Ô\ 6\16SkÿS\13\7feØO4?׸¸c¯\ 1¯Ø\a°.ò\0ò·\v\0åÒ\0\rß\81Þô-\95\92\a2ð5ßáÞüÜÏ     ú÷Sá>Ó£V­\9a\8b\93då`r£¾n~ÏôY\ 2\ 2 \ 2\ 1+`\ f\9c\81;\10\ 2\7f\10\ 2ÂA4\88\aÉ \1dä\80\ 2°\14ÈA9Ð\0\a\1dt\81\1e°\1el\ 2Ã`;\18\ 3»Á~p\10\8c\83\8fÁ  ðGp\1e|   ®\81[`\12L\83\87`\ 6<\ 5¯ \b"A\f\88\vYA\ e\90\ 5ùCb(\12\8a\87R¡,¨\0*\81T\90\162B-Ð
\aê\87\86¡\1dÐnè÷ÐQè\ 4t\ eº\ 4}\ 5MA\ f ï \970\ 2Óa\1el\a»Á¾°\18\8e\81\1cx ¬\82kà&¸\13^\a\ fÁ£ð>ø0|\ 2>\ f_\83\87ð,\ 2\10\1aÂG\1c\11!"F$H:R\88\94!z¤\15éF\ 6\91Qd?r\f9\8b\A&\91\v\94\88rQ\f\15¢áh\12\9a\8bÊÑ\1a´\15íE\87Ñ]èaô4z\ 5\9dBgÐ×\ 4\ 6Á\96àE\b#H     \8b\b*B=¡\8b0HØIø\88p\86p\8d0MxJ$\12ùD\ 11\84\98D, V\10\9b\89½Ä­Ä\ 3ÄãÄKÄ»ÄY\12\89dEò"E\90ÒI2\92\81ÔEÚBÚGú\8ct\994MzN¦\91\1dÈþä\ 4r!YKî \ f\92÷\90?%_&ß#¿¢°(®\940J:EAi¤ôQÆ(Ç(\17\94WT6U@\8d æP+¨íÔ!ê~ê\19êmê\13\1a\8dæD\v¥eÒÔ´å´!Úïh\9fÓ¦h/è\1cº']B/¢\eéëè\1fÒ\8fÓ¿¢?a0\18n\8chF!ÃÀXÇØÍ8Åø\9añÜ\8ckæc&5S\98µ\99\8d\98\1d6»lö\98Iaº2c\98K\99MÌAæ!æEæ#\16\85åÆ\92°d¬VÖ\bë(ë\ 6k\96Íe\8bØél\r»\97½\87}\8e}\9fCâ¸qâ9
+N'ç\ 3Î)Î].ÂuæJ¸rî
\18÷\fw\9aGä       xR^\ 5¯\87÷[Þ\ 4\9cc\1eh\9egÞ`>bþ\89ù$\1fá»ñ¥ü*~\1fÿ ÿ:ÿ¥\85\9dE\8c\85Òb\8dÅ~\8bË\16Ï,m,£-\95\96Ý\96\a,¯Y¾´Â¬â­*­6X\8d[ݱF­=­3­ë­·Y\9f±~dó      ·\91ÛtÛ\1c´¹i\vÛzÚfÙ6Û~`{ÁvÖÎÞ.ÑNg·Åî\94Ý#{¾}´}\85ý\80ý§ö\ f\1c¸\ e\91\ ej\87\ 1\87Ï\1cþ\8a\99c1X\156\84\9dÆf\1cm\1d\93\1c\8d\8e;\1c'\1c_9     \9cr\9d:\9c\ e8Ýq¦:\8b\9dË\9c\a\9cO:ϸ8¸¤¹´¸ìu¹éJq\15»\96»nv=ëúÌMà\96ï¶ÊmÜí¾ÀR \154      ö
+n»3Ü£ÜkÜGݯz\10\1e\95\1e[=¾ô\84=\83<Ë=G</zÁ^Á^j¯­^\97¼     Þ¡ÞZïQï\eBº0FX'Ü+\9còáû¤útø\8cû<öuñ-ôÝà{Ö÷µ_\90_\95ß\98ß-\11G\94\10\1d\13}çïé/÷\1fñ¿\1aÀ\bH\bh\v8\12ðm W 2p[à\9f\83¸AiA«\82N\ 6ý#8$X\1f¼?øA\88KHIÈ{!7Ä<q\86¸Wüy(!46´-ôãÐ\17aÁa\86°\83a\7f\ f\17\86W\86ï   ¿¿@°@¹`lÁÝ\b§\b\8e\88ÉH,²$òýÈÉ(Ç(YÔhÔ7ÑÎÑ\8aè\9dÑ÷b<b*böÅ<\8eõ\8bÕÇ~\14ûL\12&Y&9\1e\87Ä%ÆuÇMÄsâsã\87ã¿NpJP%ìM\98I\fJlN<\9eDHJIÚ\90tCj'\95KwKg\92C\92\97%\9fN¡§d§\f§|\93ê\99ªO=\96\ 6§%§mL»½Ðu¡váx:H\97¦oL¿\93!È¨ÉøC&13#s$ó/Y¢¬\96¬³ÙÜìâì=ÙOsbsúrnåºç\1asOæ1ó\8aòvç=Ë\8fËïÏ\9f\ä»hÙ¢ó\ 5Ö\ 5ê\82#\85¤Â¼Â\9d\85³\8bã\17oZ<]\14TÔUt}\89`IÃ\92sK­\97V-ý¤\98Y,+>TB(É/ÙSò\83,]6*\9b-\95\96¾W:#\97È7Ë\1f\15\ 3\8a\aÊ\be¿ò^YDY\7fÙ}U\84j£êAyTù`ù#µD=¬þ¶"©b{ųÊôÊ\ f+\7f¬Ê¯: !kJ4Gµ\1cm¥ötµ}uCõ%\9d\97®K7Y\13V³©fF\9f¢ßY\vÕ.©=bàá?S\17\8cîÆ\95Æ©ºÈº\91ºçõyõ\87\1aØ\rÚ\86\v\8d\9e\8dk\1aï5%4ý¦\19m\967\9flqlio\99Z\16³lG+ÔZÚz²Í¹­³mzyâò]íÔöÊö?uøuôw|¿"\7fűN»Îå\9dwW&®ÜÛe֥ﺱ*|ÕöÕèjõê\895\ 1k¶¬yÝ­èþ¢Ç¯g°ç\87^yï\17kEk\87Öþ¸®lÝD_pß¶õÄõÚõ×7DmØÕÏîoê¿»1mãá\ 1l {àûMÅ\9bÎ\r\ 6\ enßLÝlÜ<9\94úO\0¤\ 1\98¸\99$\99\90\99ü\9ah\9aÕ\9bB\9b¯\9c\1c\9c\89\9c÷\9dd\9dÒ\9e@\9e®\9f\1d\9f\8b\9fú i Ø¡G¡¶¢&¢\96£\ 6£v£æ¤V¤Ç¥8¥©¦\1a¦\8b¦ý§n§à¨R¨Ä©7©©ª\1cª\8f«\ 2«u«é¬\¬Ð­D­¸®-®¡¯\16¯\8b°\0°u°ê±`±Ö²K²Â³8³®´%´\9cµ\13µ\8a\ 1¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼\9b½\15½\8f¾
\84¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûÛ\80Ü\ 5Ü\8aÝ\10Ý\96Þ\1cÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäüå\84æ\ræ\96ç\1fç©è2è¼éFéÐê[êåëpëûì\86í\11í\9cî(î´ï@ïÌðXðåñrñÿò\8có\19ó§ô4ôÂõPõÞömöû÷\8aø\19ø¨ù8ùÇúWúçûwü\aü\98ý)ýºþKþÜÿmÿÿ\ 2\f\0÷\84óû\rendstream\rendobj\r7 0 obj\r<</LastModified(D:20200725234456+10'00')/Private 16 0 R>>\rendobj\r16 0 obj\r<</AIMetaData 17 0 R/AIPrivateData1 18 0 R/AIPrivateData2 19 0 R/ContainerVersion 12/CreatorVersion 24/NumBlock 2/RoundtripStreamType 2/RoundtripVersion 24>>\rendobj\r17 0 obj\r<</Length 1128>>stream\r
+%!PS-Adobe-3.0 \r%%Creator: Adobe Illustrator(R) 24.0\r%%AI8_CreatorVersion: 24.2.1\r%%For: (Aaron Wyatt) ()\r%%Title: (Jacktrip.ai)\r%%CreationDate: 25/7/20 23:44\r%%Canvassize: 16383\r%%BoundingBox: 260 -941 714 -76\r%%HiResBoundingBox: 260.012435076958 -940.159420289856 713.5 -76.7445203867428\r%%DocumentProcessColors: Cyan Magenta Yellow Black\r%AI5_FileFormat 14.0\r%AI12_BuildNumber: 496\r%AI3_ColorUsage: Color\r%AI7_ImageSettings: 0\r%%RGBProcessColor: 0 0 0 ([Registration])\r%AI3_Cropmarks: 0 -1024 1024 0\r%AI3_TemplateBox: 512.5 -512.5 512.5 -512.5\r%AI3_TileBox: 232.5 -892 791.5 -109\r%AI3_DocumentPreview: None\r%AI5_ArtSize: 14400 14400\r%AI5_RulerUnits: 6\r%AI24_LargeCanvasScale: 1\r%AI9_ColorModel: 1\r%AI5_ArtFlags: 0 0 0 1 0 0 1 0 0\r%AI5_TargetResolution: 800\r%AI5_NumLayers: 1\r%AI9_OpenToView: -379.499237432242 87.568545097045 0.654974427262942 1428 807 18 0 0 6 43 0 0 0 1 1 0 1 1 0 1\r%AI5_OpenViewLayers: 7\r%%PageOrigin:112 -812\r%AI7_GridSettings: 72 8 72 8 1 0 0.800000011920929 0.800000011920929 0.800000011920929 0.899999976158142 0.899999976158142 0.899999976158142\r%AI9_Flatten: 1\r%AI12_CMSettings: 00.MS\r%%EndComments\r\rendstream\rendobj\r18 0 obj\r<</Length 65536>>stream\r
+%AI24_ZStandard_Data(µ/ý\0XÌM\ 5\8eUÆÒ\f'ÐØ°b\ 3\14\13«|<\8b­l\9b¶©k"â\8aM\8aìM;Ë\f\86\rÎ\8cR\ f¸àª\1e\r\10Ñ\vÍ\vRD\1a3\82î-u\92Ï("\1a\13UO\17s¯3\A!U\86zâY¼\9eNs|É~b\99Ï\9c\84åpEejN/ë\86u£*8"\952â+Æj%u+òÃ#\8egY¯!\8d!\8aãePE+6ëuS£dRD\1e\919}:Ã\14CßG7\91\19\9e\19Ga#åÑ+g\83\8aVÃ:¥lÓÑ\89Âá\9a³\8f\94Ã7É\91\8e.%óÎKÓ`ÅuÊG¿ÖÃU6\ fVØÜøèfi¸aè\ 2\b        !¸\0\aDMôýQ\9dX\99\9cµjØDõ|Ç1\8d\9a(\92äúô<Ä\14vSCrªì»hXïÝÅÆKtÆlÝD7w\1eT\82R6cJ\fysF\1dsÓ:s\Óºå>nÈÒ\14]x&Å\92\84½ÐÌø\rIµuâgÜ\14%V}\88u!w\94(N¦]¤\8cÈÌ£&îN\8dÆM\9cèbô«¹ñ\9eÌMô\92ã(cç\ 6\9d¸E¬¬¦?ÎÄðþ\12£°[\8d\96°×w¾ýGYÌÈ\8dH»*þªWgô\9eÙ¨º\8d² mÎÈ,SrFV,Û)Ã,X33zÝðÆj\10\89+\8e+\8a\92>¬Ä\95x\8a\8d\9a\94\8c\9a\18úÔ\1es\14µ\8c2qr\12Ò§ccNAι~þLeÔå]Ý\9dÝpL  ßØÍ/ù\83äJß|Ú\84EÞ*Å#\9e\85s+¯æ\13\8f)afuæ\93jFI\85I;hâuñüá9H*Zw\98       #9Oé´7nÂJnB?ß\86Oa²\8e\9b8_æ>\8f\8a.\8e2a\92¾\88£hXó\85èã¬Djþ\91ÈÔ\8bæß\95ãRçú~ã%|ZÍ]d\88\89âO̯ÙãÂá\10\13C^ÌãRA¿\1a\8cS3\91\94ë|¿:FZ%\14¡[ï\86\9d\82$6|\82\96qRa#3Î\84GÆÈgõ\86Ù\1f\95Oì»ò\9d~C66G3ùtß aMi\93_b\1fÄ\8a:OKhò\8d\97\9bz\  êÉÈ\87Æ\95`\8d\rù½é(V\94|\ 6\9d¨\97\89}¡Í`+\8aj£~æ\92R\14iH'zòá\v;;\91N?\r\9e¢ÆÜ±\98Ы\86\83NºêhD\94«jm\1d\96\85Å8\9d:\92GäÞG]\18\89YÍ.Rß·c\87HÍa\17C³ïwÈ!RQ÷\8d_èU\99áÛøE¥ìÒYw\9c¯¤ä\86>G7z\14¿\14EìæêJ¯\18\8d"ÏæCäÒ\85¤ãÎääCS"׫!\95¨N\86¨ô9\1e½dTºe\8cT¿l\9cH\95HgDzhj­\96o\86\9d\7f¬J·Ï\f;\8aû\87\85bL\92¡\17ù\ 6\1d\85í¡íu7£"4Ýl5xv£oP(ªè%ú±~!rì6õ\15\89\89>U\1ae\14coÅ5[\15ý*4h¥O\194G\ 6\15ÅXõ¬$+Ý»£×\94\8d²ÆìLK/£¢Ón\17\19$\85ËcEíÜi¯åD\97+\ f\11ˬHÌ\9es\95ìéN&6tv<\91JNj§#Ý\88ø\1a,
+\96\19k7\ f\87\90Â*¥£×Îᢨ¯T»\94m\9cPôS9º´8ªÆFÆ>\1c\e%\14e\96\eÝfjTQ\94ülìGdc\84V\14µ\9fl¿2\r\11\8a\12ûèUÒp\15\8d_ÌLU\8d{\11\85ÑÇ/H¼a]h7JwÒö\96M-\13\89Y±N\8aµfb\9b\9cý¬5Ve\91\8bívѨPÜQ\19g\90x,K\99¶[=£b¬\13ÕÜtØÅ¹\83ô\11\1a©ËÐGÑÝ£\17\14b\1d½í\8d)
\9d\8cÔiä0¢ Ý1'\86çÖÉH\9döFª\9fÍÑ\86x\17+\1a\89üª\91×Ì\98\13¾;¶B¥º\9f\1a6w\961'N&¥:QmØPL\11ÇMÔÝÈÔ-\9cQ¦ð\85\83'j´\9e^m\a\8b\17@H`\10V5êD9®Ñ¯Ôñ¢øéq&h4:IQЮ\83L¼\Ôºí8μ\0\aÄÔ\84zT\16â \13UDR\1c4\14ä\92A&æHE2h\86üa\13îI#\83\86\17@HÀ@\ 3\8e\13\9aLÊ,'wâéçªkç¹\1dëÞ\e"\81\ 6A\eÀ\ 5\10\12\10p\ 1\84\ 4\ 2\ 2\11l\90A  >\80ÁÂ\ 5\174@!\0B\ 3\ e8X0\80à\ 5\10\12\ e XÐ\ 1\ 6\ep\90Á\a\1fÈ á\83\ e\14\fp\ 1\84\84\r\1e .\80\90`\ 2q\ 1\84\ 4
+h\0\ 4B:À\ 1\ 1\ 1!\14*\10\14$\10"\1cÀ\ 2\rx  |\ 1\84\ 4\14\\0!!\82\82
+4  ä\ 2\b w\96²_^ÇYAý\88|\10QÐ*S;}j\(\88EÃ,\12\ 4\10!\ 5\19\0â\0¬!Éiä\17ý¨®ºÅj\88\8bhõyª2k·%Ü8ÐA\1c\1e\1a\1c\1a\1a\1a\18\1a\16\1a\14\1aÞp\rÕ@\r       n\98\ 66H\83\ 3C\ 3\ 3\ 3Ã\ 2\83\ 2Ã\19\8c¡\18ÌP\ 6apXhXXXø\82-ÐÂ,páA¡AaAá
+¥`\85*\88\82Ã\19Ê\84S¸">\9c\r
+\r      
+¿zph`XHXPø\15yaa\81\v       \ 2 @\84\ f2\18A\ 6ÈúÆû:ÙLJ\92\9a\eM8DØQ\95\9fk]L\ 5ü5D\98Ë8|#×z\89Ô£\92\98Ó×-Ë]\8a\86tÍæ"ä\f\11c*±\1a\13Y^\r\ f\7fH\90\82Ã\83,$\9c\9cN\ 5.\90\81\r\ 3¥téú*t¡\fm¨C\1f*óà\ 4\85Ûl&\93mqh\98\85Yx9\\90\85\ 4\a\86;\8aÐ\897N¾ák\7f\97Í\9a\f\11C¯!\17gn¥*W\9d]ìåÁá¡á\81áaáAáá\ f÷`\ fõ@\ f~\98\87>ÈÃ\83\83\83C\83\ 3\83Ã\82\83\82Ã\1dÎÁ\1cÊ\81\1c\14D¡á\81ô\820\88\83<\88\90,\87C³h<VUm\86\b\eR\9d\91w\8e4¡Ô¥%#\91ÛkH8\85\85\87\ 4k\18\992ás     \r\r\ 2 ê\10qê¥$\13¢9\1a"ZtGcW\15\86\bÚÕÝ\98ÜÓ©!bJfìZÎø556      7\1f\85\ 61]\8fù|\ 67\90¨È\14\1a\12¤luu\86j(\87JPH¨30$4ÈÔ\1f\14\1e\16\1e\18\1e\1a\1e\1cä\ eÇp       ?;ºa°\9b\19=TC-PM±\1e¨\81¦Á\fVð¹L\ e\93ÐðP\87«Õ\81\r      m`0\12\87'ÌÂT\eLzð\ 3y\9cP3\81\81\f\82$<0|\81á\16îî\12ìªÁ\18nÇP\v¥\90@\ fä°0äà\86:Aa>7f:$,ü*A\16\1e\14\1e\14\1c\14\1c\14\1a\18\16\14\8a\14<lxBÃ\19¾p\ 5>\9cÃ)Ü\9dQ\13\1a\8c¡\84á,å@\r´@
+ttT    m i\18\86Y\98[æa!\81\95\ 6\1c\aa\90\ 5Q0\9b\1f"~ºl­m\88\82$\8féñBÇ\9e\8e\8a\1a\19\1c\1a¤\81n k8\83L)$$ A(\15aèB\15Ìt \ 3\17¨À£I:HB\ 2\12\ 4 \14"\10 \88`\ 2\f6Øà\ 2%DãB®³±\1cIFD|þ\\86B?«!B\12<X ;\88F\86\b¢\96ù¶éb"\14\19½:×Ii\9ax\88¨:O*þ\87.W¤QG5\e"\84Чù±âq\88\98;\93MK"*{ãñßC\84\rÇgR§Ü6D\\87V£¹r%C\ 4\85\15©jÆÛÅ\1a"®Ää\86\8c"d\87$p`êð\863\1cݼ$4üÎç\as0\akX0\ 5³²2ò\98C9\94\839\98Ã9Üá\ e
+\ e
+\ e\v\ e\v\ e\f\ e\r\ e\r\ e\ e\ e\ e\ e\ f\ e      \ f\ eò \ f|àC\1fú0\ fóà\a\ 3=ÔC=Ø\83=ÜÃýAáAa\81¡ÁáAäJ¡\16ª¡\1eJ¬æÛ\82\83ñxst        µàÓ]á
+gxÃ\1dîpQ¨Má
+\f\r
+Gc\91H\1c\96u\10\87\ 4\ 5\93¢P\ 1        B\19\19b0Ã0\94\81\fÂð°à°Ð°À°°° °ð\85\85\ 5/ÌB\17dáAÁA¡A\81AaAAAá
+§`
+¥@
+V\18\85*PA\14\1eîð\84\ 4$\90ÜoH\82\ 2\12\1c\9a\9bÏÂ0\8cÃ<\f]ÛäòYÁ\v\83\85JC¦¢£Ó\ 21\90\ 3=\10«5EUu%\95\a>840,(\9cÇ\84\ 4H\82\ 2\12\1eJ\8cV\e³%\90\95\99\9dÝ\14\18¬Á\1cìÁäx½9\1f]\9dÝÝOá\16\8eá\1aÎá\1eNÎ÷s\9f^·ßÿ\853¼á      \ e\7f¸(P¡
+£`\ 5R(\ 5S8\85+((,(0(4(8(<(È\ 2\17º0\v\85\85\82ÂÂÂ\ 2ÃBÃ\82\ 3\19Ê0\ f\v\ f\v \fd CB\19:\ff0\ 31\10C1\941\1cÃ1\9cá\f
+\f
+\f\v\f\v\f\f\r\f\r\f\ e\f\ e\f\ f\f\ f\fÒÀ\ 66´¡\rÓ0\r   n \ 6\86\ 6»\867¼A¡A¡a¡a¡\81¡\81¡¡Á¡Á¡á¡á¡A\1c\84ã0\ e\ 39\90\ 3É\ay\90\ 4\ai\90\ 6a\10\86\ 5Q\10\85ËÉ\89\89\15y\12JIH@\ 2\13\14\90Ðð\85+ü·×\13èó^þp\ eÇp\v§p?»:º¹\1eOÎÁ\1a\14ìvfFf\e£\89=\94C5ÔB)Ô˪jªÅJx \ 6\ 6R Ó\91\11\91i\88$ô`\a3xÁ
+¾Ïæ²=¦é\87q\18\86a\18\85¹±\99:4!\ 1  
+H¨\ 4\1a\ fyÜép82\1c\99\87>4Á¡\rm(C\19ºÐ\ 5\85÷zÖj\95JuÝv:mËJ¥              °\10\91 \80\ f<@A\83\0\81\ 6\1c,\0\ 4\e84,$x° \84ô`á\ 2\b \15\0\82\10¡\10ª\10B\88è`á\ 2\b      .û\90\84#ýÒô\8eÄnH\ 2ÉÖº"öÓW\95å7rUå\90\ 4I J[êÈ\18\8b\86$p \ 1\ 4\v¢BD\82\0XP\82
+\88\b(Ð`!\0â\0\1fl\10!\ 3\10`ÐÁ\ 26X\10/@\ 1\81Ã\ 5\14!\ 5\19\ 3\10l\10A\83\ 51\82
+\b\ 1D8\ 1\ 6\80\0Á\ 6(àà\ 4\80\a\19  \ 2¢\83\b Ø \83\10\8cà\ 3\f\81\ f"l\80\81\ 6\13ø`\83\f0x@\a\18l\80\81\ 6\13\b\80\10À     8РB
+6p0A\a\1c\84 BC
+2øà\ 3\11NàA\a\11&\10\0!\0\11l\90Á\a0èÀ\ 2"\0B\0\1f\ 2       < \ 1\ 1\82\b-È \ 3\ e>ø@\ 4\f\83\11\9c\0\83\ eD\90\81\ 6\1a\10¡\ 3\f4xø\80 "|à\ 1\v*\10A\83\ fdðA\a\18\ 4\1e\ 1\a\1e0\ 1\v\81\a2xÀ\b\ 1\ 6\80è\80\83\ f2\0\81\83ø \ 3\ 5\ 1\9c \ 3\f\86\ 6\1cp`\81\ f:P\b\80\10\80\10\80\ 6\1cp`\ 11\ 2\ fPÐÁ\82\18Á\a\18L`\ 3   lÐA\84\ f>À`\ 2\ 1Hà \ 4\e\ 4(à \83\ 5l\90\81\bP Á\ 4\b$\88`\83\f>Ø \83\ e\ 1\a\11¸à\ 4(ø B\ 5\81\ 6\1ct0\81\r2\10A\84\ 5\b\10\12<ø`\83\b&0\ 1\a\f ØÀ\ 1\ 2\84\84
+@`\88@\80á\82r\83\b\1f|\80\81\ 6\ f5\8fXö÷Ò\93rãD\r\ 4\19\80ò
+ J\1e`\83\fD\90A\a\12\88 \ 2\f&\0\ 1B\82© B\ 6\18ؠ   $À±¨£09q5EýV­¶ÝTC\ 4YìB¦+\1av¢nÇ6ý=nS\0\fp`\81\r"\9c@\ 2\1et\10\81\ 5\18\ 4(\88À}\80\ 2\r8ð \ 2\ 5>\88 \82\ e\01Ðàá\83\ e\14\r"\80\80\ 3\b¼\ 1
+\10\12(\ e\10A\ 3\f>P\81\b\0\83\a\8a\ 3xðA\ 6\e\83\b\1a \80\ 2"v\ 5>è@!Â\ 6\1cD@\1c@\ 5\112HA\84\v\v hð&ú\9bj\90\81\ 5
+ h\94\14á\ 3\f48\81     "Xà\82\11®°\0\ 1<(A\a\18\98à\ 4\1c\80\ elÀA\ 6\19\88 \ 2\ 58\10\0\11\aXX\0\ 5 @H\10q\0\ fD°A\ 6\1f|\10¡\ 2\11D\80\81  \1ap\815@\ 1B\828@\ 4\11`àÁ\ 5.\98\ 6(@H¨[\1c \83\b\1e0\ 1
+"\0\ 2\r2@ÁÂ\ 5e\ 3\14(>ÀÀ\ 4\1d\10A\83\f\1e\ 6(@H(Á\a0Ð`A¨\80\ 3\r4\88°\ 1\81Á\a\eD\10\ 1\ 6\10\10\e\ 2\ 6q\0\ f:à@\ 3\ e\18<ð Â\a8è`\ 2\1fl\10Á\ 4\0\a\r\10 $\mu#%ÛFY\ 1\ 4\8d¡\89¢ûUê\13\87ø\ 5\10G/\13\1a\972<\88@\81\f\18\0\ 1BBY\ 1\84i\82~\ 1\84\ 1VÀÁ\82\b\0\ 4\e \80\83\ 5\b\10\12æ¦(\ e\10¡\ 5\19\ 1\ 6\0\ 3\ 6\b\10\12ì\ 3L\85Ô\0f\0ª8@\84\ f0hØ\80\83\f\ 4\90\ 1\ 6ã\ 4\19tðA        >\80\81\ 3\ 2 \ e\10A\83\ f\0\ 4\b        \ 2\b\80\10À\a\1f\82\f\88\r\0\ 3\85\0\88\ 3tÀÁ\ 3\ 4\b     \1c\1d®8À\ 6\1cdàA\a\110pÁ5@\ 1\ 2\84\ 4\19®\10\0\ 5\ 2Ð\ 1\a\1f`ÐÁ\a\1e @\ 3\r\1a Ä\ 1:àà\ 3\f"tÀ\0\ 1B\82\901\ 5\88à\ 1\ e@ P\80
+L\0\0\10\ 26Ì3\9e\67JÆJ(¦öQ\95Ë<#öz=Ì'ç=´¹9s\19\91Èk;©¡Í'dÍ\8e6\ fk"¶ÙD¬UËf\93+{ûÜ0©ÑRñ\ 6\1c\ 1\ 6\1fp@\ 1\13 à\ 3\1fPXX\b\80\10Á\ 6\19tðA\ 5\f \ 40'\9ac
+\10\0a47=\11\12$¤É°i\99m¾öJʾ-£%lâ\8dß\12\8fVG\9d\91ÏhL$>\8d§­q5UªñMHÆ´WÓö^ã;Ú>\19\91Í\9eDQÑ­$üÖ\9d?Ìu² 9jé­û\8d\12\91v©«\96\}øÄ\83Ä\86åùþð   »Ä3I}g\92\99\92gF´Ï\96\9e±þ·t\11´ÿLgOÿEb\1f¸ªò}7*2ÊQ\15ÍrTEfrT6³»\92#*J\89Û7sYñ=¯Ht«2\91àÜ\8e\ 6\91hoäè&á(\eYF\8e\88Êñ]Ï;ú\14Yâs\9f%¨Å\12SV×\921$*V;ú\9dÇèʧ\97kL:²ík¥n\1a\ f#\8e\94¥\8a,u3¡Q4%Eìç5Ï$\1cEet$SR35£Ì\e\87Ç \16v-\r\8d9
+#ߨ\11URc\1ar4f¯s\91 ÕØ×LE5\8f¸\fQ\91QLÕè\11U$\15Õ>8f\14w\11*iè\96Êð\95K,½\16\8f\1f)\97×¹èd$Ã'\1e\ f\9fI<ôÚ\1e¾Îuhøî£\9d¡Û\Ã÷Ñ
+\93;C'\97Í+<Ýì\86\8f¾Î\87\8fâC6ª¢èQGQ*òÐ\91\86üZ%D§
\12     Ñ9XÁ\9bë-î0\11£±k\réelHOX©ø\8dYQ\84\1f
+׬£\95+\v+ìħ_FIePåLR«°LÏRA\12dä#È·\93¹\89X\8e°\9a\91}¾¢Ä¯ÇΫ--Rí×­\86´¥¶¶´?í\8eW¼h´¥­U\ro¨ã#ÈÆsì \8bù]ÄA\9c\99øVûùì³ð±&¶M<C&\9eNM<\172±§¥\1aBÂbWE\164Ñn¤$Hì¦Ê\8a\9f¡¸(5\8a\ 4\87H/!9)ÑD|\9az\88\90\8b¦ÈÜ\88)â1\92í\1dϸÀ\0\18hJ\96\11\8c\9c\95¬êÊb{Ì\8a\89ãÁÖÆÔ\15c"Í\92ÌômLÅ\19\15ívp\ 4M5ò\b³#\12\ e\8e¨»"\1a\1dÁ\12±\921ä¸\8a©Þ\94;\8c4bE¾ibI#\9bmvu\e\8c\86Ya»°læ\83*,õÜïKCÄJö"\92ÙXî>\7fÚ\fó¢uG碪ùÝ/ò\8c;i>/ISá\19Q)#é~\9d\8eh\8c0\191ó#éV\1e³1¢\8e¨ã\ 4Q\95b\1f\13òÏäRúQ\98\89\ 6q\90°ÍÆ$'"Ì+2\r\8aÞP\8a.F\93E\91F36,LÌÄè×\12ûxÑf1\19b\94AÂH>,nuVe\91Q\98\18
\84Ê¢Å+ÛÝ\r\90\8cN=\8e«\19aºäd\94µ*F/Iª)Fbs>²\ e±Úô¢²*MD\9aR\15\12©áut\84±}Ñ\10ã\\91\aÙ¹J^˱JF1Ã\1aµ(¹«è¶Ô ÉI}\1c\854ó­7\96L¯±$L7\8aê¯"¾!³A*{Ül\9cÅÑåNn:ç\1eg&>Ó«eã\8c¢¨ÎvL\1fâ\r9
+*\17ÙC\97º\ e\9f\9c¸Ä³Æ-ÈeSºéÙÓ³\8e[Ôâ)ÝqÜw¼\85U>¥»\13£\e¦\ fñ\8d\e1\95\1asv¬\11C5#É9hó\aIxnÐIjH\9asè7ï ;\12Õ*\9bäæÎ"z\9dÕ ön3c64E5!Á«\9b¢\1a\8b^+\8d\9a\10éQ#aÒrÑË\9eQ\13\eBâD.º¼fÔ(%|\e\87Þ3\91¬%çZó-7b\Î}\97k\88ܰÅ˵ù¤Cbn\88Êä\aʪþ\96\8d*Aò*\7f¨Æ§*²s­ê`\15\15ËhT\89O\1d\95I\12æ\93"\9fNf\90È8\8baKdb/\80\90`\82©±²°.\99fGÏ\99vW\9a\19\e½²\93\89\fÑê\9b1ÅD+é\99\8b\1c\8d©!Ú\87¯Q±gN®°»¢yò¼\95W¬´SÕî\92÷\eå\90\8fLÌgRÍf?\9aÓ\98ðèúZÏ&{%´Õ\b]åb\19\9b¹\ÇæR\13¾f~d\8dÐ\95\89µsÜ!5Ó*É!´\9a±/­ÝNdZTlí­Í|\8cf´*èø1Tá#ú\8c\fsO¦n^ïG]q%ÆWkìNT¶Ó\1d\13\13Õ°<f1MsJÕ¯Ælâ\e\9a\97g\83x\92\9f\9c±\88ìÿCcCd±¡ùÑÜ ªÉ\ 6QÅõf85¥
+\92ÔhÚ\9cî\8d³\8dØíj\88ÖA\17&\95Ñ|Æ:,.¾*ùb¨<-\ e_\;fd¼\91á\v¢ù1\17\1d\1aÞ\98\8bK\95ç¶k\1cUnÌËsTQ\91\9a¾æ®\18\12$·Ê°\89ãÇDGä\9f¢"þ3=\9e\r\91Ýæ÷ª*¹1÷\8f\rÑÐüFF#\1eoI\1as1Ò\9bæô¬2xâĤãÛ¼áö.\17ñ\7f\93\8d1aZ^¿ZQõÏ×F?*¿¾\9b6öÚ vLÆÔ\1aUN\*?;\1ftQÄÖxÚ\9f}×Ñ\eé\86I)O>®\19½0\87|esU\e\8d^\98ñ¬|qÕ\10Ö\8eê"\92þ²òOh½è\89­,¬¸Ü\98\bù¶«\92Y¶÷ÎC\84ã¶#27Jh~,]\90\84B\95\999Óò)©\9f\8c\8a\8d»°YÕ>x\17d\9b\8f#ã§TLy>»óW©\98±\98íb\1e·|ldØ\1a^M(î\93ºG±bxD[¥J[yb\87½Jþ"UÝK&\93q)\92\87\94ÞX\91\ eþgï¸PqZ.\7fZ\17É\86\86o\8d¸ÈG$âýw£X÷ø)̽Ö^§\95Cf³Ðl%e\e\1a\15+¥]ê\1fS:«2\13£\13]       \8bµ\15é\86ú\9aØåu\83x\1e3"&F¤ ª£3Z\89½Èè|KQ\15­Ìt¾\8dÕ\18¹Q\89}\848\8ac¤bn\9f\18ñb>ÍØ~òs\9cÒJ¯r\14\19Æ
\91rªêP\98Ýfü\93áp+c¯Á!½:w584ú\88¬\88\7f:«ºÍ<8B¼\9aA\19\9bY£eZ=\1a2\15\9c\r\19\92úÔÏx+ø2­ng\192õgÎqí¸ÄV/E­£J3v\85¯\93®£5\8do¾³¨CJAU¢zP+8\14«_u3¬\15dWDs\8eúbõÄoM\87§¢×¡ø6\9eþ&'<Ö¶hò.gHÌe\15G\11¡2\19\8aÏé\9d­®Bñ=B×ÌV63KZÅ7z\86M\8bT\11\9fB\ f\9b
+\92±]M)È|~^{\r1)|\8bY\8d\9b^çt\9b¾i68\15õ!bÉ~^v\1aâ
+²Ê^/¹\1a'\ 5Ç\8aN\83L"+K©¤ýIé\8aF\86äæV¬uäæWï wê\84\84#rs1ÍÞ<cGcåPÙÜV7²Û¼¥\1aä\9a%«O\15Ù\90Kå|ÜyÝz3X
+\9fÙX²ùT\85Ϫu\96\ eSÅý£HQó££!UÊ\98C\8a³\8d©ú\14\97\9f|Ã:\88¥è\f\8d5Ltf¼Xª\95£E\1a\16S\1e\19\aÉ·¢l"Eã\14\1c\8bÕLÆ\87ÍNÕõ\9bò \8dÂEr\18\ 5Ñ\1d·\8c4.\96\87yL\1a\1aFi\9dï\86\95êè\9aY_2\9c\1fd\8a·<¦BmMýn\95§d^Ë\97\9d\15Û`Ey\99ª¹Á\8a2eªnúøbÖª³÷\98á°
++£]wé\86\98ú©WåFo.£\16,ªr\8b\1e4Âv\1c»\83VA¯\11Õ{ë¨aU\fyÌêÕÚ°òEl~v\83#|¡Ï\ 6[\1e×È\1f÷<%c=\89]»Í¥þcÑç¸\94ñÓ\8eWѱf\9e©]X!v_+CF\1c\99t\84\86\8dèk\8e\86CFtêæêTs\90Ì \11gUG#ß*_ù\9aâ\8c«\8dYæµ3Ñ\f3\9d|\9ca\16f\87zRl\99&råG-î\F;]Ù 9Äâu©ØÍý°3²!¾\v\rZ\14\87¢î\19å9G+ìßT4ÅV\8d\9c\98\f¥ÄQ¨ìzucH¢LO\r\97gAÖËUõJGV÷pªÚa\167"¥GH7\99ÇÕèË\9d]\91ÕïlƵNVW©\96Õ[â¸R\Ñ\86Ytè\7f}ÆlÜ=Nâ<&±{¤Fñe\1a.a\9d\12\8f\96b^\ f!Q£\e\9a\1d}u»R̶k}\1cmq\1fÿu*û8\9a\98\87-*R\7f]\86\1fG\93²¿\8e\14\94q\99\88ØÉµqä\f[\94Ñ\88Ø;¬q$\vû\biæ-ì>ó¸\90ÄåæÚG\94\11«ö\11ñ8á\9dÔÅNÒ\87Ù£¡«\9b\10\r\97¢Nn:u\e\1e9ªÓ\8d\99×(\89\8f-®)¦îã$jÛS\17S\94\18~被Æ\17ªÛ\18OZ§®NhèF\8e¤ºÈɸ!\87\11\8fDró\947\19´1Ê        \17\85n(F:\83JÜZ$\8dg\86Îú\98Ô\10ª\a-LÎoý^5Ê$?i½b2\84(\8c\84ÝȱÎ\9f\83D¶Æ\91¸\9a³á\12~)23\%¬â\1a½\ 4\rõG·9\8d\96\90ò£ûW£·kÕgèÎ\8eê7®ÄÜÌÑG\8b¢tVÖ;ª]\8d+a²¹£\ fýÑ#QöÜéB¿\re\94Å©æ§\9b\9cÆ\91GLïtDÂ\19\1e\88oFòGn\13JëèF¹1#Ã#X®£ó/+IÉÕÑÝ¢1%æð\b\8eNeîÏñ\11çù¦qDA-\1aµ\98£\e©»¼Q,\8d«Ð\10[>)\13Ϥ \11«öañU¯\97/\96¯\9f\8e/\96Dözu\e\1fQñÜ£JQí\84C¥Ø\8d\r\877¥Ý¤Í\94Ø\9búu2u\14Û¤j\e\85åÇìÛD¿-b\9fÞPï1\92ð~5ÿvÇlä\95íÂWxç\9ag\8c\8e.IG£Ú      ç<×*³\12\13ÏKl.²¸Ãî\96ïh'IÒPùäÿöÿÿ_â±ù\84lU?³\9dÔÐÂ
+9d¦9Y¾\18Ç\88׺\9c¿ðÊ\92pÆÎFd:FÂûÍNÎê3\85ö߯×ëÿ}\96s\88\rÖ\91Ù\87Q\19\19\19ñ\86\86\11\r#SëU®*\eFV'\e&[\r\93\11Ñ0\91y\98L\96A4TCªê\ 6\91Ù\19éîséÃl¶\9bhÔ7d3j¬º\8d)\99"r\8d)ZlL\9dÒ\1a¯âÕ ã\rÍ0;+3Ñ2;2;{¯\1e½ÔÎfÛF\93Lèä\17]ëó¡Mh¬\7fcÙ°¿>~\99\9b\11\96h¯ÒDZ´\92}\11\8d4\1a"[ÚÍQzò\89\11Y¹£\93QŪúªªôÉ«*o"q\85\96\89\91\15\1a\e×XfF¦\r\89¦M\91iäï\84S\ fÑnn&ýùGÂ\9f,32t\8ffºÉ\9cZ»¡ólLáçäôÓÑ\89)Õù~¿:\1e©&âc\1e\ e\16\1dÍ\akP\9dÍ\89jTÞ`\rVëÊ8Zm\95\8f£U2¬jg£ªÚ\1abb41Zý2h\83fZ¬ê&³\93ë\9d\1c-Dz²³cÝÃ\8eî\ 2\b     \1f ÑG\99Ý\87«ÊtÇá:\12\19®\91\91á²×\aÕ\98µeEGýðm?v\vmLlõ#ÛÝYÅVÿTȪ\ f\9déµ\12Ê]\vm\15\8ḇ%\93\1ekãå¤^Å:ë&ÊyÊæËè®\e\9eUÕáÑÚ+*Ou¡ë5\855D!i¡XX&ò¤\84u\12s\98h^X&z¨\9dzÍ×\rm\84\86jε%³ÕZ¦\99ä+k\999c
+[gì\91jn\86#-!\9a\95±\89\88ôÊ"-!\16Ñu¢¡WÉPäÌ®ú\92Ccõét¼äÅ\86uÇÎ-uWSÒ÷o"*ó(²d-:Õ*5r\84H\16ë\91!ãGÜ^±$Ë®\14ùÅLÖ\18\15É\94\90î*V<y\ f¹È¦íz¡ÉƯ©³\11»\91rÂÚKj;\91$óù|>\9fKËõ˪6w½ÞÜÍÍ\98ãÿñ\18     -­;½N\1ft:½l3uLh#Iä\8c\87¯ÚDääGEa\8dzFÓ\91§\9c~±$ï?ÒhjbDZk±\9fõ¯\93W-é÷*««\90\8cîüm6®ü\8dÌÇê­\15uh\9fÓÌÙ©Cu3\93\9aLUÃ\1aÒP\9dÅz¨lÈ¡RÙ\90ì®áÛ2×\90?×}öqCsêB\9e\8d\88¦q\9a\v $|\80w6\86|6ÌF£\1eô\87733\9dÑaç:nª\1eâ\1a6\9câèP©Ö\93Auu\88\83:¤Ñù\94\87§&\1f3CòÑñ\87õæf\n*6DÌ\10©{þ\10Ê\9c\8c¹ñö:¤D6r£d\8c\fieH\ 1F\86HuHyÈø-§ãfPÙ\9fÞQÆÚÇ\8dC*µ\f©ÌÇ_\1e\ fé8søÃÒ\1ab65DHC\8cn\86tÈ(\8e\91\92!¬Q\17@Hø\0;\8a3ª4hfFMÕÌ\8d÷h\eïyã¥ßxë\91!#Óºê¨É\99q46hË[þ\83|ßÔa\12\9aa2\99\86©Æ\1a&\93Ô0               \11\11\11\11Yj\ 6QM;\88\86\83¨Cè,$çÒQ}\94èæ£ä\e¥Q*QK£tÒq\9bm\1cäûÝ2Ì£T\1fDDd%¢\ f¢¢êq\14u.t£::6v\15iÐ%£!6\15\rñÔ\10_Õn\88i\8fÅÉeÄâ|ØXGÃtÄwÆ\11ÑÑQ\89Æiê<\dä\1a\89íÆÚ\87ýþúÉ£®\7f\9dG\16¹M\84¦ÐnEÊ&ÿý·»\8c\95ðW-\91\8a¯r³Úo¨.dR\9bMé¸ßX\9d»\8d:^ÜÉ¿Áó<\7fº±a;»\89Ý\ 4\95ÝngBÃlìªh\10±\1e&\9a\8e¡\179tÄë\r"£w\1c\99£\88Ô\13ǹ\1dã\19cs\8cÇW\1fõ\98\9eó \9a3\eg;ã\10ÿ´\9bÍx\89$w¼Æ\8e×e\1dïk´\ f\9b\9b»Q\92"ÕùÆÇõGuØ\eÕ3Ú¨N½F\15\ 5\19Ð\80\10@\0ÄCÀÁ\ 5,8\ 1\a\f\17l\90\ 1\ 6\10è@á\ 2\10\0\83\b'\88@\81\v<h\80 \8b±YI\91L)Ë&Y\8d<KèwC.V;±ã«D\8e\16«\95<\1a«\8aU\17E\16+:±\91\7fA<ªÑ\8f76F§*­=¡½Ä<LVêµ\vÍæFh®\14\99\8c#4ÛQ\8cDB\9b¾(e+ªÕªco±\8aF!zLs¬Õlb\95dVy]\8d]é<'"«kÏtVd×\9bé®ì[IJª2ÓË$4sI\95\95õÌwD¶c\89ÌêX£Ç\13#ÚIrVé\92\ féÿÔ\99ÝYµìî\¼;\9f¤¾UU¤?wîVv­ê\99ÕmÞ§\18º\97\15}¾¡\9b\15ÍÊÞ\'dßøg.éü\r¥\1fû\9d(±ÈMqyä¦t\8aä\14Ú\11ÉMqÕÈM¬r&é­\8aÂ\9b1úiçÏxEÒYGRKº\95ÔßNwåM´n%½\9f\9fÝB\9a\9f 1\9f\9a\9aK*\11\99ôhÌvRC§\ fGÚ*3kî\ 4Í\9d\86æz¢Ì\8aæ8'èeÆaW'=IΩÖ\94n24\1cT;\9a\f\1e\r¹\8f%c¢²3++ÕqLX{\93N§8«\93\15\92Þ\9dF÷å3x\82æî\90î\9ft\15ºÿë\7fÛIê'}^Â:¬(Cª¹ )W\11çâ¡3\13t\8bOT=\95{º^õó÷Tt    ²Ì\9c¤H(§\12\9fÚ\\8ads¢ªÆR£È\1d\86 ÄÏ2ö8áIÕ.*§\18\eÕNÆMµ\93\e\ 5ejhCÞ½Þ©\19'Õäg\83Ãq\154"QM\7f¡zM\1c7\1e«ù8\85\19ûb}¥\19¶µ\8b]è:5X»Tk\17NK¡Ø\89WAU\e\1dÑúõ\8aJ\8b_ìÇ$Vd#)Þà\90ØOî\1c))VJ¬(Vä\98\88\9b\¿s\89ñ®¦\94³\90yõ²Ë¨~Ô\88×\9cb36âFBGu#4\866ä"µCûüìå\88©*­d¤\91×Ù©©\1a\8c¤Êc4½Tª~$F§\1a*\8f©|U*»,ÓÏ\8d «²Kl6Ss5\84¬F¢Ê¤:¢9R¡{«Ì¡·\8a\ 4\8dæ­2I\90\8bE\12\12ý\ 2\b        \f4ÓºoÄÝK\96ù<\95"NÝ\94f°FNÈv"3>ÂçñÄÛËxª\8f8¢ñT¾;\9enDÝL3ÂJÚh\98X=[§\19¦FP¥üÌtD\rï×\11\1e³{Ñ\10³P¬#l\84=®¢dîNç\11*!¢ÙÑ]¿}xÊÆè\89é\84højúM}È\89P>äf\94cúÝÜ\90\8d     kÈ\rOñFMFëbàïD<
+\9f&¼z\8bÂ\88¢Ú\1a\18ÖÆèÆèÊv#5\88¢´\8dnUwVS£\14)\13GgF\1dD\9c\9aY%
+\1aR\0ñ\9fQël\92!5$4\99k­ëLÄÝcS\97MR\17\89\98\9e|ê(ª$5ewlTÔØÌ\14\994HX\8dTëÌ»u¾\a)¦Êæ}\b¡ ã\90ê\8e'Q\93\9f9\9a\1aAr=¢aÑ\91°)\9eê÷\11­\99S\rÅ\9d ¸¬\8aC£ðÑ\98\90KÌq
\13\97\8cäV¿\9f\9bë£J\13Q\99\8fNÒÝjF\9c\11E\8a\ 4Y¥(q#ÍÕ[ºèL«Qã«·c\14#²á3\1aY\8a¡\1dù\88F\ f\8b\91Xµîuòâº2\8dUJdt;\13_6»\ f\15K§OOl\92M\91ZE+éV\z¥\95\15Õ:B\16=D\15\9a\11\1aËS\9c\12\9a´X\16iNMÚ­Xô\9cg;\94\8d\8dÐ\86\8e4féuFµd\9a\9dÖ\87\95\13û\rÉtnÊr\bËhÚÉ]\9fjÝÚ\8dêsZݸê\8dóêsV:íÄDsî\97ªñ\13Æù\91fF]\90\88\8bR©1\v=33Èí\8dݱ\12\1eÍP\7fQæò\ 5éÒÌP\854sþ\9d\9a\8a\15Í\8c\96Pj¦Õ¾JGsÎ\bÙ \89k\8cYÚul\171²ñÅ#U\9còØ\99¡\9aªó\90Í|f.­\1aSU\ f}Rã\8e\9e\8bï!U^øÎFó³\99aÿCN\7fN\96\8eÆÏSs²\98¦ãÚ>ó\9bÝ\r]\9a¨\93¹±ÇLNX\8bè8æ\84Ñ©c÷}î\8boNþçf\\91ÏÍøÓ\9a\eÏYgnì\8aÒ\88§N3ÕQ\13\1c¢i\88¬D\89~6n\9f:xÂF~\1a!\19Rj\86ìÄ\99)5v§¹Xï4wÂ$\9dfdz\9a!:£\9e¸!ÖÌ9\9d)+12\9e\99ºý\1e£·¶ùIéÿ¼%2õ\95d̳¬lÐ\84\8dIõ©T\822\1f°ë\84\1aÝÄ'ö\88\9c¥Njxñë¤.3¶\9e IÄ~M5ØVgÈEM(c»qønZ\1ar¢7Ñ2Xé\9d\1em\8fü\97\18\91Ñ\18\9d\8c\96(\93Ð\ZK\90C\93Ê\98\12.Kó+\9dq¹Q\13&.ËsÂg¥ÆÐ\86ö°\8b\9a±1ÿ¤vñO\8dynÆãĪ\1cóÝ¢qR» \9eÕì¢.êÙ8¢Ñ\97Iv§c\8e\17\9f\91R±waSe2ý®vï«Øº\9aô׫\9dD¬,Uö\7fV³9\96Ï*²\97«Ý:eF§2\85ÆNý\r\v\19\ 5bê<æXS¹V\1fE¯;{Mæ*ßTwÿ­ÊùòÒ\8a~5$D\8aÝýµ\92\91¿BÆúRb­\86^~YÓÓò\86ffLÒL½ÎdÖæ\16\97¶'²\ f\ e\87\1eZUæsj©'&íL¥.ùû\11·\8aRFGqM+®)¹âK+þÝ\97\9aÒP\8ad(\94³ºÚ32\9bô\8arz(71\8aM¡ó¾ÔضÊ\99³\17±Ç\1aû#\85Æ\8fÇãPN3\85Æü\84&\ eѤ\93Î#GOªÍÐÔ¤\9ebTs\8b\94}M}»ÔÎes|éxF²\91Nò­J~\94sj:\8e±ÎÎ\95\9a\9e#ºúèbGvD·áQõd\87\8e>»16dõ\12#ÉêZ=³Õµì³\9eÍÊZ\17"ÕKxRåa\8d·ÌÿÕÕÕU\8ee¿ß²sÑü\18ïÌh謵\8b&\9d\99ÝÔ«ìÈ\8fêJ\99>\8fÔfµõLxURë»\9cfV´2«²²*\9aTËL\12ñ¾\95\91\15\8c\8a­²º;\99\fÿbmX\91¤Ü\19ê\14\992&ÇÍa§Ó:SÜrl¢Ô\10EZe?UÏ/v\96\88«¦=%\16s|¶¾X'!\89Ä£7cTRo\15yT\95\1a«\92²[Õ\8e<³ÖQ¯¨×£Þ8G-Ï\15ÕÆ5W­¹º\8cñ\8a*|G+f$éê\9e®Ö*Ç6³\8b5\92ÔH;\1aK\8aë\84ÿÇúª½N²\8d\14\ e±2U$\1c^©¯³Lc\87\8b\95'Fd¯Î-    ýêì\95{GV×\e»]ý\11rTëb}-gÊ(7jê£e*Ò\1aÛúÊ\9a\99\19·Böæj¬ìÙãimNIùv\fegöT!\9bì³$û\96Ò8k\9f­Ê\9d<ìm69Kõ\9fS7ßf\1f$\95\13\87Y\9b\9d\92É\84ã³ô§»Ïç\19éRuz¨¦\f\8dê·Sù¦ÊìüÈ?\99ê\85.t¡\93X¾Üw\9e\94\85ÌjB¿ÓHkvÍ#µj'§]y£'\8bO\9f?\91ãØ\98\90æ¬+\8bé%ùà±CTÆÛr$ÎI\87\1c©Ó*T'UêlY±Vèc\16\11ï\9b©\92\f3\12íE\9b#C\91cíb#]\91\18ïªÄXVóè%Æ*ãÝ\19I¬xcNºÚX\8fTÆ£\92\8b¦\ö_t,¡\93f\96ÎÉ0©ÑH\9d\8d±¨e.óÏýYhvë-RÒ¹H\ eÉç\9d¦Ûê\85l,Iê0V\8e|zÊ<Ê\882£X¿4\19Ö\ e\8b|;\12_\89jª-³\8c\"eÓ\8dm\9fí\8djrgö\18]q\8e4zÑs­cËä\83ZÿÔ\8fn\9bß¾b+\14\93º§Î£ÌÍ\89Oº    OèrÊÐ\91EÆ£"_õ\9b%º]·ÍÌ\19Õ\b  Å\86®æJ\13j;¨8¬¢ª*\1agh\1c+z\19Å2\942¢Ñr4Å"\11âÔÛkÝwd\99¢\99¢û\11\87<ľUæu<e7\9e\9fø^Icój>{¥hÊ&õÈ\9ezM±±Ë¥:\93«\86Uù\12²ù6ròp(Vb,9ó\10«!C±\97_õ)\12\9f\8bw\9a\97>§)F\9aþÔ¨)VÛý\½}×P®wå¶sÝH²éÞ6C7=\9bD\8aÅÒ\88\ f\91Ìl\14©\1f½÷_Þ\12©+/D6e\8fÈÑ9w£\vÇU$ÓhE®\97Ù\1a©ËØ\19³lר\88î\91º\18\1fÒTµÚ\9bß­¬>V\93ÌnnªÞ³b2\9dW¬®NG\19îQ8ä\96\8b,¯idÇ\90ò²b·ý\86\91ÒÖ¥tí¶Ú\10\12\91Ö¡!\85æb\9e\12¡ÔDx\95éÐpk\9b\1a\935Ä)y4Ô9)d\r±\8c4]4gfø\8eצèhCd3Ç×áÍ<Ý\88Å\9f\1e£ßi!\8e\91Dñ0¢\94½e§\97\89Dr;\9d®½Ó\89Ç{\7ff\99\98ös\8aÌ÷\86x\862\8cxgvqæÈÅú\98\11\8b\8c\88îÒ¥xÿÄn¼X%d&\88Ä\19j\9dN×íusaQÌH³î\12Ç&«ÑØQÆ\11\1e\9dÎF×0¢ydÈdóÙÐh+Ô±Û±¿í¸%wÚ!Ö>§7¤í\95\8aevÄæ\8beJ~µ¦Än?<}NÈÚ.Ù¾ùn\9cµ9Û/©"Év$\9aB6V\8eZeëò9ó\8cêãÒªI>ä²\96ØÈЧ´^\89F\9eZgÛË7Hj¢9\9f<å)Í#åÈ\8d\91²LG$u$u\1eéMî\i[ÞU\8d9rk®Tse\11çb<ªYSº\98    \9d·:¿C\1c#½Ï\1eéGº\86æ(VSº×Ðé«M\9dµ¡9Ò¦c\8e3ÇóQJ?\92Ðýó|\8eÑ é\90\9d¥ÿµ.ß\884wB²Ðë%=\9f¤¼×ÈJÌ7lÇ:\1eó\87\1d2ºù®Vä¢\96jlÒò§þ(\87ä\96k:VW\9c\1aÇ\16iö­}Ù?v®ªÆ\97\9b\8c\93\1d\91+\8db©H¶X\8dï[hü²\9eÉLÝ\fjGÆÔÞ¦Eï\r\92lm\\85\9eé\87Înú;ºöóÈÇ\95\9c|\\8a\fºU¾"~^®\96\9f\9eLM\9d\16\9fÎ7h:BÓ\9cöôÛÚÚ\92Ƶ'\1cöF\88EFçÜçæöÂ_Íß\9f\99÷¹\90K\16úÒ\96Ç´SgÇm[ºÏïj\9fW\99ì\1fã\8c\9bÞÔ4\9d\e·VW+bgµº\9d\8e\8dÛzlWûêVýp}³3i'jÕÇxCÊaÕ¯5F\eÖûüVUë[ª\8b\15\ e\8cÃZ½\9a*}/³Ø\95]\15\95kF!oªÌxFVõ»£Á!¯7c\16ëC34ÕpN,wý(\93¨F\86\99½«!=D&K®eæpj®ñao=s\98\8fÍÆ\87s<W-W\1f»\1a\99²Mù$Òúü3\84\86$uò´£\1f½\8e~\16\1a\91h\86W±T\11\94,sDºq8^¬\9fè6öÄ.B¹««°n¢°ä0±BNÉ\89·bõX$\ f\1eiªh:£ÅÓÏX&:\19m\93\1cmÓØc\e2äIä\88~\1c:\9b+\19$ÚW)#Ö5/\12Ý\8cC¢«³\18ñÎ:Û\9dñèrCã\8cE-]Þ\19g\94±Ñ«,Q\91k\8cyUSïycÌ\11\9b©©1&\99±÷iÝÄ7Τ\b\16©\1fQl<75æËª\8b\99\f\ f]Äú\98!«fx¹\92¯ªFs×\r\8bdDæçÄWWS\ e«=«×¼,?\99»ª\9bðĨW³«:\8b3t3ÖSDz\9fÍR\93E¿ó\96Î/wt¦)×\8cênu¢\9aNÂëå\94\96\8e\1f©$µç\98\a\9dSVÆf\88\92ó\8dçÌb®°Î¦77=\r3ò.&\1e\89\8e'6úÑ\14\15\95¦ÊSÅ)úI?j¤kÑ\18¥­ÒmîF\1dmXãÇ\1d\87Xrez  iÜx\16É7\16M\8fGÒç$Ùo¤\*½GÛ)B%³#ô£\11\12¢3\9a\8c5\19\13\19tÖêÌÅû3\9d£ai/ssIy\17©ûØII«cý\ÊÌ\97;\e%=#_t3.5\9b\94SW\16vNÊ?\96\86ή\97øãÕãØ©9\1fkF6;\95±¹¡\92Q¬Û\9b\16Û"ª#\99\1a\12\1dYÚwH˳:»ÞØÒÞÍöÙ³_v3\8e-¶¼\94\88ïfH\91        Ýi\Úº\öS¾MùVÔQ\1aÎ¥\84èò=%kþÝî®w¾ÙÒЧFLÚk½\9b9ædI¯ó>³¹uì2¤³\1dÒG(Sú\19mÈyb¡Óy\97n\96\8c\19§\9f¡_3¿1&\8fÚ£ö¨iÔuM\9bâ\99nHY\rÒÝ8\9fùq®\8cû.\ eS\8d\95\14\r*±!ö;\1abFb3ªÆÙæ.\80\90Àp2'\7féÏç\9f˯·SÍ7ï\89Ç|n\11\9fð|wwòü\88S\ f:º»\91MYçÈf\90Ù\9d'6ö;\19Wb\17@HøÀÙ\8d;\9eç\aÏ\ fÞL?ÖÞ~\1c\91\8b¨ôQ"³G©DçQ¢ZÇyHP8D8¤N:dL<D\ 6¹2(v£(\88Æcz5É\8cy\906c\86Çö\98ãÔ¨Ú\15Íq2NÆÑɸq$\ef~\ 6\19q\90q\10uPÉî¨ÖU­8ªÚñðLÍøØØÐLÆÍM]fÜüãò\i\90i2î1Ä14\9f\8e¡¡¡sÈ4\1dåagÐãaUè\83EÇb\r\93\90ùÃtk\8d*«ÕlÔj5W\91\87\90Ñ?Cʵ\87\94\94PJêþñ£l÷6>#\1e3õuÌÌÌÌÌ\9c\98lÐÜFÍçÈC|SÓº\1a/o4¬%R£:ª¤\7fT«ÕjuTÇ\94Z\8eÏ\98Î8«Ö¸\13\1a\8c£uÜUI\8d²ñ\19Câº\r\9b«\8d²òd_ä/·Ü\88\9cú;\91ýª½Þ¯ZÒÜUý\88½\93¡\98ysä(f5$±\92açò«¶ÂN±úz\19Oäõ*©${;E®¢¢þäìjªîÄD!^j\98XiûË\14\9d·¸¼²\94ie/)¢ÊKHHL\8e3&D,«\JeºêåYÏê.OH+\19»Üªw(fEu\ eYå¹>¼-§\8dìª\8cÌ\8c\8a\86tV&õ\e\9a\86\1eö\9c\19\13»°ÌÇ\96è³^óèèì»\9dÔtnff\8aÌ3_rÑÍ]ÊDâ\ 5®4¨ÔÏg&Ã"Ç)¥\10Í\8c\ 4\ 1\0\0\93\11\0\08$\ e\ 6\ 3B\ 1±\80\a\14\0\bÀ\96@`\16\84\10K´¥2Î\0\0\0\0\0\0\0\0\0\0\0k\13±\8d\a{\8e%\1eäx\9dd#<$âN¬þ\1d\80>jïP\v\9e\18\83\ e\1e\93\ 5Ï w\98\16\ 2àÉèÛ\ e\1e}$í°\9a\v©A²\83\8c$\ 2vxùÅ.pò;9F«\83Ä\14Û²\ 1£:\ 5¹>\8a`$íþE0Ã\95\7fQ\1fuÐñx%)qwÛt\90\91Ò3Ýj\9f\84(\8eÔáú\9b\88í\84ÛÕ!òïÄ"Jß\89-¤·dÕN²rÄwi;1\86âwb\aã\11\ 5|'v\83t&ú\9d\90+u\88o;9\81G\0¼ðB\92©v²Å\89®I\a5Ú ¯¢C±É\ 5:¨FôÎÁÖ;ivsȤñ\84]e\ e½\90â\85'\98p9Ì$>åàÜ?J\ e\e)\ 2r°û\15Ça\94Üüà\89\9b\17\87\94x\9a8Xø\9d4ípH\10\v\85\83kí¤\93\ 5\874c\15p0ÑNú\8aß\10Ó
+\ e¾ß\e\9d\9ccÞàñ\88¶:a×ÝP\94êÄ\8eº\ 1õì$I4\10\r\f¸á\1cÞ\89ÜÛ\86\8dØÉD®\rvÌJ\eD@\ e\ 3v"ÑdÃ:`\8a\r\8b§\13¥i Øà\ f\9fNÞKu^\83{Q(\ e×°¹pÎ\1a>>öj\98O\8cªA\81Ó\89\K\rKa\ 65\1c}NÄã4\8co9\19ÚK\83£Â;\ fcÒpþ¹£aWÉ¥hPYN\ 4]hX¯\9c\f\8b@\83;IÞ\95o9!\9bÏ\90¨\9cü\81g\88×\93s\86\ 2\9e\92Iª7ÃÉsâÅž\8eQDåd\1e3\83à9i¹¿\8bN'RÃ\f+y\9dÌå2¸0\84eÐÇ\9d¤\982\14\14\9eØ\ 6(OV\9eð¢®è\ 1*îà\89\96'<\16êÉà\9f\92xùÄ\9cá>Ñe\8fª\93Áü~Ò=\ 4@\91¤\90@á6e(1\9dR\ 6Í%(äY\10(ãÕFP^\14mPÌL\19ÔH(       H\19º°Plè5\14\ 3­\fº}(I¥\f¥\10
+{ã8¢\ 4\87²Z0¢|\8bñP,áüJI÷;\r\11\8b(ê]\19&)¢¶à\84\87\1aHm\86²Ì¹to\19t ÎËP\96¼L×\95A'Ü­\fÊ\1dJ_uêP\94FÊðÅáË8\9d:\94A[\19¼Û¡ôͼ\88\94¡Æ\ eQ=\19:\14\ 6ú\1a\8f¦ã\10¢hÜÊ00D9\a\15ú\81\Ð¥ö\88\88b\91¤\fPü`Ê·¥¢%âÊ`ðÏ
+\91[\ 6·Lý2¨9Q2\18\81fËpV\7f$L\14\16®\f]C\14ëR\ 6\942Q:GúÚAB\80\19\97\7fz&J{\92\f=\91\150Xâñ½\94N18ÿ¸1,7y\19\83Ç_.\86Ý*Ê\r\86]\14·S\f\81Ì(Ì\94\18J\13\1eÄ @o\14>\1c\86¬<\8aé/\f\90\ 5)QL\18ê\91Å\ e\ 6`\8b\94\98\14\f\8d\94\947\16\18\93\12_\80¡\8eÑü\ 5\94@A¤¯\94gÑr®F\10\97ÒÏ{!-Êê\ 5\11Á\94®å\85\bÉ\14n\v/TMJ)¼\0I\9b²Cî\82F\9c"\12vax\9d2dºà2=¥¿s!¥\9fÂ\90r¡oP1Y\\80XC%\p¡iQù\83·\10×G%/\89*\8aR\91r¶ =¦ÒÍk!\959\15®ª\85Æ\85*vÆD\88k¨K\15\8c\87\16\ 6S\95£ë,\98ܪ\b\15³°h¬2\96Ê\82g­Ò\91\90\85\1cÅX\bc!\90¯\12\97±\10_Xy{X\88ϱ\12Û`!\10\97\95§½â\82Vbí\15\97Z\99Ó®`\15[i'¹B\12n\85Åm\85\12·\15£¤\15\10ûV\82öÊ
+\9d\19æ\9«°\82Áº"\8dü®Ðå*´\17½òÈÉAâ+k§
+óú\95S:T\ 3\96\ 1\ 6\8b\878\85üÿ \8c
+ô+ÐåW\8a\f*¨4±\b\1eó­½XN\0\1eöÆ2\1fï\7f,\eµd °G\161\88¿\ fe¹n¥ }\96%«I¡QÌbN¤\80n\18¥ø» *\19k\16¦úJ®\8d\9d%F\8aÂvùY¦\81(¸;´t»PH\ 5¤\85\96eß9à\962-S\ 5
+U²ñ'\80\8fZ\ 2Î'ôbµ<©'dik\89\ eOH\ 6\83\9d\10\vÉ\96èÏ     í¥-ß\8c\13RÙ¶%\a?í"·\88¦M\10m·ô©MH½·°^MhÝ¿ÅzÑ\ 4È\83K\90Ì\84¾?u!\13Ð\82±\81\1cTÓ¶üù%¤­\\92xÐ\\92Ò%äz.¦×\12\90\8c.Aa        ©?]þ`%Ä\90u       >gÔò\18»\ 4\1d%D\11l\97\ fÇ\a¡»T®I\80óÌ\92ÐDßÅ|$\ 1£ð\12G#!ïxy\1d\91\90Ìå%\1a!!\11ôòõ\8f\10\92z      l\8f\90Göò¤\1d!Aw/5+¤>ñe\936B§æ\8bu4\ 2\ 4ëK\142Bôûòè\17!\1eôK\90-Bkþòª\15!\19ÿ%ª\14¡B\ 1æ\ 3'B\8c\ 3&ä$Bø\ 2óL\11\10Lì@\84\80
\19\ f\f&E\12\ f\r¡©\b\f\ 1W\13&BYV
++Ì^\15\vc\90      \ 1\ f\r\132\84\90x\1cæÉd\84\1eæö¶F\1eÄ\88\9e $Æ\88yó@ȨÄÄ©@Èyb^1 $Ã)&#¡øÒ¢Ì?(ºÄ¼¿\1f¤\9b0&îü »\8cyà}\10¬\1a\13V}PÇ\eópêÃ\9ccJ\b\1f@\ 4t\95{\0y\8c8ìA .æÇéAÆ\85L\98\9e\a\9d(2\1f+\ fb;2!\18\ fBM2wÇ;Ñ5Kò\92\91­w`È\14w   '\93íì ºPÆÍÊ_ÐÅ¢½\ 5¾ñ,¨ê \96\92é ¤+ã;\\9dT\0V\969aÎ\81\96[F\18æ`ü.³·\1d0ó{r\90\ e\88\99x\ 1ã"3lã?dÍL\85\85\ 3\98ÑL\b\81\83\10kæ)ߠ˵\998\91\iý_}3\10Ï\rú±\ 57\0ÚãL\1c¢J\99<\87\81³ANv\86'\9c\ 1é\89gL\\10h`_\13Ï\14\935\0\84J5(S\9fùþ4\88ôÏdE»Ö\ 3Í~K\83lc\v£\8d\ 6\10\9f\b\rZ04f{\ 6ð\85hbÉ\19\ 4T4Oi\ 6±ïÖÙ\v1£±
+3\0ñ\vˠʣ1üd\0O\91&\88\91AÑIóÌc\90åJ\136c\90ïKó;Å \10\ 21p\8eÓt[\18\84¤§áÈ`\90\1dÔ\98\180\0\ 4ë\17´T©ù\e_\90â©     \88^Ð?Õ¼ú.È:VS\89Æ\96v5Sm\970bÍ\10R"®IÞÚLy/qÁÜEå\8b\v\80ª54¼\ 5ivkÞÍ\16DûibÑ\16\14á¾Z\0ì_¢\ 5ý©f\16hMk\bYÃúæÖT0,°eÙW H\18\ 5þ¬\15<#×\F¦\8f\96ùÚ5Ób\15\98\9c`\1f-õs1*À®      åSÐ(>5\ 50>b)ȯk\8cF
+°L¯\89Ó(¨ôk\1eF\14ä\19lB\11
+\12÷°y\ 1P\10\88±\89µ'\b\ 3Ù|³\13¤4e\93C\93Ùd\1f'\bél\8c´      °<´\89\8b&¨0m\1e"\13äUmB\7f       \12óÚ|
+\9fmL\11à6j$ÊÛðX\ 57\ 3p       |\8b\9bÖÄ(72\8eÎ\r\ fÝæfê\féæ\8b+A\88ë&\81ÿv\93¥&àÍä±òæ­>½1È\85{#Â\ 1©\94À¥|Ó\96Ï}£Mp¿!s\ 2àLq\12H\9eÀé\8a*'¼\82Ö\ fZíàD)fÂ\99?_8\7fr8\1cÓ¯\ 4Ê\828i\8c!\vJ\1ck\10êâ\12l\ 2r%X\95âÈÓÙâPDJ\90Ñâ¼\b.ei\8cÃgaã$\18%è©ãØ2`A\1f'\89!AA!Çë¬\8a7\ 4\1c~ª~;r\8a\1c\12 ^r\82ù\bZ@9ïs\ 4ñ\8dÊiT\8fzÉr¶Ã\b\1cÃX\ 4à}òDP$æ\18\e\11`iæ\84÷\10ä·æü¢!\bð\9b\13¨Õä&:\87a\85 Íx\8e}\b\ 1\8a>' \ 6A\7f óò@\101\86N\8c£¢\93g@Póè\18\8a\95/;éhìgô\ 3:/>¶{ÉÑÏ\v¨\90\v\e±Ù\aZ\17Áj¥\92ùXèA³Í>`ì;p\ 2ÅÀÇÒø@fO\1d\16çóh$áø\0Ýñó'~ ym°ø\80+\8dOq´»\91ø\80ó\19\11\1f\10T\ 4^ܨ\ 5\8cf\ f,Ðr\ e\b±\1fg\10VàÀj\82
+!çã\81à\ 4X\10æãÓ*°\89÷\9fÔ¡¬µ¶\91\a\8c¦ÀÚôU\9d!\ fÄ~d0\9bÓ¥20\90\a^|~¤Ðø¢\82\85<@¼-:\0D\1eà\9a\ 4Ê\9d\81-R\90\aö$\ 2á£\\85¡È\ 3V]0gÞP\ eD\1eÈ\ep\b\10FZ\912\ 3È\ 3\ 6|ã³×Ø$fY\1c\80Á\b¡\10¬´2\8f\a\88X¡3\7f>Û_fÎ>\1eðÂIB9\1eX¤À÷\14v!xØnÕg¨º\14Þ1®@\10¢;»\83j\8dM\8e\b+±ï±yüĶ&Qx{í@}ÖR\15ÀCÑ¢vÀ\1dFU\rA\8a\8a·Ú\ 1s\8fè\95\94ÑÒn¯b\1d(|LOEí@ü\14\ 6ì;`\8b'qDk)¤Sß\81­4Ü£´54E¶\9cbsµ\ 3\93¿ùEÐлÕ\ ep`å\84\94ìO%\ fë@9%±Ñ\81ÂOúrÂ\90\1f¥Is ½Êá,pï+r`¹\8c|Y\96×\1edË"\a8:¶\97"\a6'{\18U7÷R8à½68\ 2N\85ØÂ\ 1ÕQ[ª\9cªP²p\0¤\98G=\9aR\8d\84åï\96FxÅqÀ\ 5/°òu\1cÈ)\81¥ÑâÔ|¢l8\ e` Z0¥1;\ e\84ü\1cØh×Mp\1c8<T\86w\e»\14\ e0nõE\94Ô\17E\ 2ßÀù\15$F!¡Ë²\8fÜ\80\ e^\1cè¬OÝL\e@w/\94U\ 4cï9eÙ=`\ 3:ëº\8bÖ\v\ 6FáXg¥\8dºt5`«6]Lj\83\89Êi\80» çí\81\9bÊ¢\ 1øø\13òB\ 5\9cy]ð\f|µÕdL51/´\ 3µÔ<\ 3\99'\ 2\b5<\ 3\eº\1fî\8a\ 5\10\84Ú¶\v\f\11Q\9d\90\1e\11¤\fì\ 6ï\97\14\fø
+Ûé¼#Å)7~\f\83g@Sr\8c&\vhF\87o1pI/\18)a?ü8\ 5l\18pZ[¶Ü¼\97\102\14\fØ
+<Oº\9f\82~\ 1(9È\81®§:e\0þ##Ò¶\ 3\8e\89Öº@\89_Oý\94Çu\81n8¹\12C\11\17Xýï¬yô/\8e¸²\ 5pf©q\ fÙ·\93\0Z\0%ÿ߯%¦\8d\8e\ 5ÖÎ"A ¯@±H\93ÁZ\ 1\0}ºË\97¬\ 2÷^\9f\ 5o\a×G\ 5¨Í¹%é¦;'\93¤\1c\93\14\85üßH
+èP\rn]\ 2\85¥\14(
+$@\887\966\ 3\ 5P\86\1f1\o%&\eO\0\8b\183®!9¥\ 2+Li\88x\93b\9c\ 6,\9c Ü\12F®©¦`$\8a\80C\ 4ì\ 2Æøeþ¢'\93\80\8f^\8bøÂ~Ê!\ 1IX5i )}ë\b´§:Í\96ñ¹®Ä\b\88\8a\99-k \b(©ÝÍ÷KY\1f\ 2\801úúÉK\9f·\10Àã\8a`ód\97q\1e\19\ 4¨\96d\0\99¡ß\95V\199.¤^oFRú\ 1䣢£ÚQDß@ºeØØU\10\9a\8dÿ\99\84\ 6>\80Â×Ì\17\ fdðy@\7fØYFyá\99\b°\82w\80Ð:7¡\ 5êò\ e\88\0?(\9b×v7T\a\8c[âA\1e\1c` Èï\9f\9e
+\aH"ßH
+"\8eeÒÜ\80\ræ ?¹`ôP5pØ\07eؼÞCGê§\ 1ô=6­ Å'å\19Pyð©×¡Ë{e@>\\ 3\10\81¥\ f\85\9fî\18\ 3øÜÏÀÇ1êä\15YW½þÓû\83\ 1
+&Ý\7f\fÜ\83Õ\vHbæ\9b?\r\88b_°Ç\ 5x³®±ª\95láÐ\ 2æó û\0>¬2¿\ 2N±N©ç\85zZ\ 5\1c\8a\1d\ e;sS\80µ×i*\94í|(@\9f\19:ùÖ    àF\88£ÏÞÿµ­Né*;hñrè¾ùçT   (.bÈ\82\11\10\ f        ØlÈ\17Äõ\r \90\10Sâ¶þ/ #\807»¦b³x>\ 4\14Ççt\94\84\ f\ 1Í\0)L\99e\9f\82Õ `Π \9dI6Õ\9a¼~\80z\11Ehµ7\13H$z\80h9\11ht\1açu\9e\1d \ 3C6\&oç5T\7f\19\831\ e`õÓL¨ü¤Þ¤¥\r\10Å\aÊÈ;\ 2m\80        ×@z\ 4åR!\1a`\ 40>þ\86â4\f\ 5/\1d7\85í?\ 4\9b\81\ 1¸L¸X\ f\Þw\v°ÂU\83¸¢Ê\94®\0·Ôó\96,\8f\16\92\ 2xÖ7X\91Ù\ 4 Ô$Xe\14óo\19\92\0û\82 Äô}×\89\0bÜ­±ðS\1a)\b0/«me\9a¥õò\0r!\8bw\83×uÃ\ 1àË[\13ø¸âng\0[å~"*DÔ¦¡\9f\17Àjº\81uÅ>\b\1aò¾\9b\99*\0H9ÖAM\887\820\ 1èe@²I,®p\b@Â>\8eL\a`ÄÓgkÐ>à\b^\f\80ñùNþ\1eíx¬E\81\19(\0\1a=\0\80|&Õ\7f\08\ 6êC>¤ 
\91§Ë\ 2ÀrIa\9cÆ>\93\ 1\80æ2\8a¦Á;\0 è=r\f¤ô\14\80Dÿ_[\88Bûÿ£ZM\ 4³O°\93ÿk${º;\89\ f\7f®8\0¦ô\95w S¤\86÷Z¼û_²Ùæ\9bÌ®ÿ¢\9ca_ÙÌWsþ\af&äù\8fNñ®\ 6­\88©¸øO\12\ 3Î$¤"Â\9fwÞ?m`^ü\8bÃÝ\87Åì\1f¢ë\84Ü\w\85 QýO÷ÿ¨U\ezOè\7ft×0vùGÎ\88k»g\96é\88ã_\97åço\ 3´Æ=(ü\eã°,\15øþhÖvÓDX\88¶_æþ\fiEö\10
+aTûC\9bÒ\14\88Ò\85\1dìß±\9a\139¯þMñÐ\81-R×\9dþ\995>\f¥\90\ 5\16\1aý\r¸\14MRTôü\95S¥6\9b\9aÿCq]þ6öÀ^,ÿøÐ\83éÈ\85Ì\8eüÁµyùû\13áð\94Æÿo \89vGäùUÄ?4kÕ\ 3Q\11ÿ\aë.\10O{Ä?\82\8f\8f\9cÖ«I\a\7f\1d\ fþ`/¬Ü~%ò{\7fÿ\10|³9ð;V~ºxÅaµ\90\0í½Pl÷Ó{¤#M\aC\81\93û#\90Ýýð\8aÊ\11àA\16DóãÒE_\1ag        \1aç`\14¾\ e\1awãð\10¼$ûuìIÝ7Õ\17Àþgz´Ë\95£QfÙû\94È­?Õ\90cç¥\96Ö®~e¥i2Ûõ\89<1\95ú\85\80ª\8aûS·Ñô*§¿\11;'÷\9fô+\ 4é*>\18]x²Qô\87+\9c\94úáùÙA\ 2èw\88     \ f´\835ªR¡\89áù/\ 1\18\1fãE\94?VJ§\95òäö\99gf~aãr\90â[ù^|\97¿¤*@ù¬dΧòWVY­PùG\e3NåQù\19~Ln\ e\ 5\9d\fçÊ\ò\a:\8f\91D©nüò\85ü\1d<\9e\8b¦.ç\1cäø¿}5º\ 5\89±s;ùâ§\98    AØ\83µÍ:É ëMl\11`âè\ 5îðãêJøÞ\fÀ\89\8b\85\1f\88ï·\1c\147\1aüÔJÀQ\94-+\ 2?ø¡\ 5j)Æa¿ß«ÿ]\ 6nÀõ'ßç\90?P¤÷ýÜ\98ÑóGLÁûUPM\ eeŸë>ä \9e=,\90{îËÌÁ S\81¬\8cûåÊ_r½ýÑq\ 2:¦Yl\7füÐQ<\81/£mj\9cÛSû}
+IÖ\9cë\ 1ç\ f\1aÓ¶\81@X\ eJ\12Ï0û`\8eDG½BR0w\98Ù?íó3BÌÎvwUd\7f\1a1Ù+ö3­ÊMÁG{¸`_¬w\vá\eöíóúI#q&õAÓçNo\eÃõ)öÉ\99#^¤·³¾\8e\15\95(Q4\97Ø¢«\9fi7p\8e{s\9fS\17\80ª_ÌZBÔ@\96\97ú¹hõA   [6\84ú\ 6À\12\0Ò:ýÆÜ°_¦_Û¢ò3ý\r\85      T\80Ï\9d÷G¢ÒoÅ@\1aDzn\fé;\1f'\90\90º@Q \8eÑ'B{²\1dxk²>ô\85ñ\1cVùcÐw\86
+\83óN?\1fSLÛ4Ì®\ e­\9fO\96úñ'B·º¹z~ÆÄ()w\9d\9fRÇÛ¡\14¡â|\\bï7ίCé95:ÖG\8b褥w1\14\18´<°bõ²\ 6{\ f2_»´Î~ù<\82\829n\10w©±å¿­\8c¾îo¼Â°G!jåWþ\01§ÐRÓ´#å\7f\15\19\7f¦=b\rÞäç+X2!d~\95äÿ¬Ó¶h'öÂ\9a\13ùlTï¹B\19zÿ|è\1a\14\90\7f\9eÝ.¹ãw·{/\16È27~'YØC¶M#\1a\95æÒU\19ÿ´hì\11\99=M@E¶\19\9f¡\95vpS¸'f|j°\88\aL"þ#©ò.>ecƨԨøT+¶\9b_äNðCK|1«$*Âñ½Ä7\18«\ f{é p\95'+ñÉ]ºJÃ\88\92\95øòå6¬\0\85\87\12\1f#\92   ÕåÃ"¤Ä×Ìý\99ã\ 3È\85î\95ETd\98@PâÇð&HÆ\80)\80Íl\19¦¢Ä·LT7W;óÅéÞ\8b\ 5îJ%$\10âÈ\84Õ ~¡\î#z\8dø >¿$\ eJSÜ\83\1c8\88\7fûÚrK+º\99\1f}r£c±Fnø78²,0phnø\11\97ø \87\9fþ\15E{Ô  ®\ fg\91ð©\9c®ësð\aÝ$|y4­ô§hý\81>        _Å\b\vRßý\92ðÉ¿\8e)ÊÓÉ\13G-ºÙ³\93ðe\b¯´þÈàãFøÍ\91¡$#|Á{ã¿áÕÞ\ 4\18\93R-\e7)\82Ï4\vZàá\99\83À?\ 5D\b\8f\b\r"ðÕÆâ\ 5\1c\97J\ 4>¶T\10ÞD=¦é\ 4\81\7fÖò\8b;í§¨wÐ\7f\8fT\10\82ݸ®¯~\8f\91ñɵú=\89Ë+QpÐW(\92Æ\9bÒÆ÷²Í\1d[Njï5J´dĺa\9b\aîÐ{é÷&VK¬\1e3áx?\r\89Ì\94þÚã};µ\7f\ 4Ø»\97Ù\80*Úf-Ð\86\7fGËîqøíaOã\aÙýÅÛ´mè\9a\1f
+\b©{ò\12­\94\88\ 5m\82¼áÒ¢\9alÊrÿ\90ƼWMÀ1\14÷I\8f\18\1a\ 2\9b\rà::p?)ï\ 4÷\16\1dÆÐS\11\91·\17'C\93e2ôæ'¸½·s¦h\ 2ñz¶g¾ý"\14âCÕkß\ 6ã>º0£WP©öãÞ.Vàü\8fiß³ñ\11Q\rñ    ¤\84V·Jê4T'SÇþ\89.\97G\16Á`ª    c/X*GÙßI©S¢?ö\1fekwýÏPm±7Z^       Z ï\92X@÷u\ 5\9aíf¤\80ësö9!:VÍ~MS©DB_¤Ó¶»\ 4\98×\1f\82ö\83.Ç­\9a\90\8d\ 1éZêúVðýî\81]Â<*\\7fh\9aSÈW\b;\86­\1fÆé£\12\a¶~Ì\87û`QEÎüYß%KL&½5¿±^:\88õ¹W\1f\0\? Ç÷2«/5ç\18\8e\9fµUõ¢9ÅUÕs\b\8f\91QÍ´+à5T?nâ¤\81©'\81ç²ÌÝ\99GýW½X\906oÙª\15ÒP\8f\93 2éNy9Õ§ßæ¡©ÿÙ¦b:ý«\12òÿÛ)âÖô'Û6ÜCem\f\98øt!\83ç+ÛXÃÒ\1fº\81ݯWTi\8c'}\81\8aòE\80û\1aéña\86\ 2\82u\8f~¼B\8aFϧ\rË(£\91¬è;­=ËîAêDôü#Îõ\93ñN\8b\91¡Ï\83\13\b=þ5M\19\ 3Áâlâ\81¾zÔjFÜØôÏÃã\9f±ñv\ 5\18\18èóÝ\9c\8c'(<úü\9fð&v\16\ 4´\9eß,Ñe\19¯ø\8cx¾¤R
+mÌÓ½ìü (l°\8fÓÆ@çÁð9Ѩ$o¼(\87\95 4è\9b·]ûÙ\19c\95òAÛÕÚü1·µêÞ\95Æ\9açîê\8f\93\ 2¨>\8dæS£ÊË\ 1\8af\81¸ËÍ|X9\8d\7f\8e\8e1ɼ ±\15ÐàÏ\19Hb\1e^<QOfY9\8a\ 2æ\1d ¹¢\9c¾Ë«\11\82\9ednk\80¸å\95TñE÷jôY\96\9f
+\1f\13\17«\97å'Gè\ 4m³Ö\8bl®|/A×õ|\11W>\9eþaýù\11±©|eºÄÓõЬ\94/zV\9bXÔöãÓ\1e\94ß·»\93+$;\8dáä¥Å?\ 4Oe\85æKÞç\b-   s\93\ 6ËMZ_9Ñ\1eùK'âÕ       \ 1¥"\8föu\87W\8fð\eª#C\1e\bÇ×à¡=\90\97®\ 4Sqh\19È·\97\e\88\84I0   Ç3þ oÝ+\83ÇWNÊ\0gñmøø¢ÎñFÖé/H¨t9¨\ráã'J\9bY\86Ï\f\80(\8f\17\1fo\95.\9aü\8e/g6eî\8fkÞñ`[á\e98å;^¥ß$*ò!Y!\80\80¾ãqÂ\ 3\9fª\1f¬\90Ó¨\rAfï\ 2±ª*\18\93äGÐ\ eÌñééÊl6ÇÿEìN¦VÄË)kæxå½Ô\ 6¡9¾:Ï b\96©Ñ\1c?Å­\ f\8e\8a¯½\0æx\17ñ\¢<i9^\9c\89XS\ 5±\1cßëÖ\a\94ã3¹q\86h<"»v\a}9IæÆ7,²ä\a\8dØ¡ÑkË@8k\ 6ð\10±2>\ e\16\0cUê\12XC0~ \«M&\85߸øYO\1f\16]Ø+~Ó\16[ü¿À#Mñ\1d\93\14}J'\9eáR\88è@m\97s\16\83\9a"Ô"¾*ØQÖ\9a   \ 1Cd
+U\10\ f.6\8a<Q '{øÐ\8a¼\ 4Y6yY\ 4Þ2\f\85\9c3mKÃ#$x[^\98û\96½\8b\ 1\86g9ÐØÎ\124aù¼ÂÃ\\85æ9áE°\1c+õßL\8a´w[\13á\85úZ4§N|Ì\13vðà\92÷#«\1cÝ@¯\86(*\99\9bZ\86¹\vb:\88IÁ§4Ñ\1e\rÆCüz \9ac\17XSÖ\v<\92\8aʳ]\17Ê0Í\1e\0Ä\1c\82]\9d·E¶\ 1Þ\87{\9dô>\9eèûï­\ e\91ûyÂ\7fS_\90çï+\90'¯­»{¢mP¿»\8böÈz1\1aÄïmÙùá\19Õâ÷\9c=\9eåÞÜJöýA\f\1eñb) }ç 9/Gõ\84¾SQéQ$ò ÇwÜ\9aA\86\ fxj\8d\91ïÝ\92¿U»À\11yÒÔÞß×Zò\vÖ;\1cJ\r \0\88Þ?\92\90Tiç\j2ï÷4\ fðáá\95gÈ{\9fU¹SEG\98õ« \17\17Î\84Y\81\büî¨\fÁ f,\ª\15uwåºVñ{\1c=&c»\v\89n`kL\80C\8fÝ\91\9cÓFìÃÍé£uÿ$£\8e\83±,3ê>\1f¯Êùëc\8duUV\0énûµß\9f»;ã\88ék| ³NÅ6wj¹\7fzÎIáSÛ\96{\89\83AU¹µLrÇ];\97¡TÄh¹\8d;Lî\1eô\8cU\17&î QS=        \8cÖ°ÂÝòn\9cç\86r     ¸Ë\ 5ú2[U\99\19\9eضðí%é¦\84<?ñ\ 5ôn\17¿V¡\91ZL\9dÛË\98 FªÉ&\86ÅÛ\ eºÀô\ 2Xµ}Õ=¤o\ féäÎlÏÔ\97"DÚbÀ\1e¶_\13b6\80\85°³ØòÎ=,ÖÎ\9a´$Ó¦¯R\vß\9aGµK\98]¨Ü´ùi·Ê®6\8dJx\89ÎÒ\ ek-\98yX\8fz\19G;)=9¦iw5´ã\98\15eí]\96!ÛÏN\1f¤A\86a¨h¸:»Q\v\8c\90\81Í®éX´\8eùÿ\95\99\13³³ú\1f²p\92p\91e§dF7ñÆ­\ 2\11\a¸¯\8doàf£\9f¿"%\1f;4\ 3Íf\e;^Ç¿\89îΰ+µØWÌ&hüë¹ØÙ\vj\ 3Ø«¬ÄN\92x\98e0Èô¯Ãþ(ëMÐPù`\9a\14ö«©vW\9cì\15J\vv\82\17\ eÑ\ 1j\10ò\81B\ 3ìÜË%cR.ÀÎ\95õM\ 3\90Ñ\12\96_ÿ:U»yà±ã\ f_?u\86k2à¨s^·HÂw7\14o-?¾ë\9c¹¦B\95s
+ëú{Î\88F\ 6å\88r.k®cGì\99Äõò\8d è\8aÈ­aÁn=¶\|ȪvëQî)\17\b0q\1f[7ÈÜ4°´Ø­Xj=×TÑ\1f« \ 2\ 5´¾Ì\r\10\90ø#\8b~0ë ¯\9c\13¢3Ó<\88=~\9d°hæäþذ\ e6vçüV~uô×ÄÀ\8duu\96 \12GüKuuÚ¢é\81gOlnÆVgùñkeiaxÆX\1d\90¿²\8e\89ó\94V]\e¸\9dì!#ä&T=È\8b\19\19ßÒ?ª\aYv¬Ø\1d\9fO}\17\e µâ\97ûlê¶\r­³p`\81üÔR\87¶:&sPR\f\9f¤þ\1a ï\8e8碵Qw\13¥!ßl \ 4\89zÛ5¤_\a2\9f\ 6\15w        õÔu\17j\vD_¹\14\97\ 4¹U\92lMÔmrÑàÀ\87 ÐôBAubÂ^NNÇ\89\85÷\17kæÇ!äqÀ­íÊ6·à5\9aL4=,\846\\9db\87\81#Î9\8c\97
+L'¿}\ 1É\87oé½-G>¾#®t\96\0t/\¤Ç\94®\vÓ\87Cw*\f\19%ÊI·:Ñ71Ù\80m\92\8e¢#ëú\f\v\14éh\86MB\8b\88ëéí\ 2éË/ÝC=õ1\9e\8a\9b\1aÑ9\10¿~£s\ 6±;l\83\1ek\93\19Ý\8e
+\9f\13OnÍvÑU\1e\84\ 2±qÝM*º\12ÊÍó\12}Ãñ¹\85è\bîðý\buw\95C\7fáKÃèrÈB0t0ºã¯\v*4/I\93á     ÝQ\18\83¾bÙ\9aCb¿Và}·
+:EÇÄ[\fj®WüĨ!\ 3ý¼}æ\ f(\ 6ú\ 6Ô\13BzF\8d\ 1ú-\82a,\1ci\12\86ýùùA\eÈ2?Çé\1d\8d|!H¯Ï\17\90ï2aÁX|âs©ç¶\9aöÄ\95|çÔ7ä[\9cÔGÏ¡\ 2  ªD\9e\ 3\92\89>\151\ 1ú\9d7_\87øAÑ\80çbc\95?BÉÇ     ?Üy5Üna\15ÅäsìÂ`hoûu.-Ú:.a1\9d÷H\11Zð+âA\ 5¬¦\ 68>\8dq\0\8f&çT³i\88Ϭ\152|-N¥â¼ðl¨XÒE_Ó}ï\ 3çÝÛ[$ñæÙàã²\96oó;\v%0\92$
+âHTܳq\ 2´c>4·rÖ!­\97ü\84cj®1\1d\1195\ f}\9c(ÍM\vÀÞw\88\f}uAs\11:\1a\ 1¹°\13B¶ã»Ò5CØV\f\95nÕ):Q=\99óó\13\95\90zE¶c\9e:ñ,\140\80\8eyÐm¿e\8ay7\12i,ÌI_Ú\ 5æ\8f\92¢\98ý?_îÐϪIþHC\8a]ÞʤsZµ,Ô\ e.Nh¹÷,\14\9e\1eËG\80¿÷Ëþ\8emP\8a$\r\ 2.jåÌ\18*\89Úö}È*ï8\81[\ 4¨À*7U§+\ 6ü-\9d£òÏ T{fI9ÕÕ\94ÿ \16\970\85¢©\87úO\8e<R\8eF.1«(\8f"wÕÎÉ@\8b\81r\8b¢å\88\11ÈÎ34\80\âÉc\10\8e­y\1a\b\8e\93G­%\13Ù/aB\93ßk\17cfRÕÏÖ¨/9½\8aL\9aرT°M%¿I&©ä¡_hÀaq^I\9e\ 6´Eï\92°AòªBÞ¯6þÂ\91#\10§8jdøÝ\86\8b<®ÑFQ2æ3%rú\99¡\a\1d`(ä\90§uÈËäÓJj>\¸\94\14r{JK¬r\97\vr*=\9b³+°\vÈ_\17»\15\124äÇÇ.Ri\9b:@?\90\97"U\ 4âWsçñâDc0Q!ÌïxÇÉ\86"\18ùw9áêøÊÙð«_uHu|{þpS~ç\99ãV\ 2%y*'\rÇAUh=N\93µéÆ¡è=>Ò$3Ù8-Ü3kG_\ 2J\8d\ fÈ\91\84\9b\95\18Ðø\9dËý\ 1®\Æc\18\80\16\14%\1eo\ epòüVa\\81¥Tʨ¡VXøâR\10A|\ e\ fR\9c\14äEÏS¡íXh©Z|Ù\12Wâë\17û\8b\17\ 2b?\8bñ°ø+9m\9f@C¡Ì\15\93@lIú¾,+GM|¸\ 6Î\14Å9csÌ#\15\bH,¢÷ñfâÌÈ*\9a\ 1¡x \97æ\ 3Î\r7ÄQÜ[±B\9f>\1f=\\96ô¿!.R\8a¹PÊ\87q\8dW¬ÎèkÃ|¸\9a\ eíÊbÀ\16\1eNK%\1aÀ\ 1\1e ÐáÔ/ñÜ\e>ïú\8dSy5üõjsÅøy\7f}\19NÀ}±à\8c\97ba0\1cÀ\rzN;\8b¹\85¯\9d£³¢p\18µ\89ºÂ\e"L\ 2Ôg2)|ª4¶     ×Í}óÂX\12>\84¤zZfë\f l*0\8b\ e\80ðu\83íµ\1dmº¤\1d¼3èmc\18°\r\8eU)T\\80X\90\eþ@'\83\91\ 6Mh,m[p\0ºÒ¤`ÁÏO\15-Rð1\aYQ\12<'\9d帽?ÿ\f\ 4\7fÆ\19\v®øk4l*ÒAá\rüŪH.Öoà\885\80\1cþ¥\9fr\81ÏÈ\89A~Í>y\vS\9c\95Í4\9e\86Ñ\ 6\\ f"Oé°\ 4\9c­¢Ö°\13¹"àÐ\Ûâ\85\ e\ 1\10ÿ\bx*>\87\90´³IM<     pï¡ÊH¡%\ 1>\96°íe\ 5\94¡        ð\ 3ö[ÆW(\ 1\87\1e\9d(\92¢¥ê\12ð;¨\86Õ\11Ê1Ï_\ 2n4\10\1119àÑý³\v\aüR°[V´\88\99\80+\ 3q\vÏx<\99\13ð\92´1é\ 4|\1d3F    «¢\9bì\ 4<\19lr½Ð&àü;\86\86t98ýOÀwëZá\99\ 4\1c\81\92 ç\ 4Ü\87þ\16²x \9c\80Ó©³*ä   8[\v\97\9e\80Ë\vp¥G\86A¡(À\91\fÐ\ 6*\12\0?¬©G
+\80#Óc\9fþ\93\80\ 4À/c¥ÀHÌ\9e\17\0¯'\ 6rÔMøo¦¢¼syý\8fïo:ª\11\9e´ÈÏê   Koù»±Ð\8fS\ f÷ûÉAü\ 4\8f9!À5ýÎÝïG5\13å7\16\9dôë=B°à7©*îÌ8\84\9f\a¸o1\vt\a±ï^v}¯\92é\85W»Øô­\8b~i\96)ýà9gu*\r t¾\91\90O\882¹Fùn\1f'G5®\1d\9föºø>]øö\12lp¿à[C\8a\ 3\0\89,².ïÝ4f^\8bäu!\ 5÷\ eÒ \9e\99½OæÐ-Ålx¥c]o\\19Ë\89\eT\1a+\1eS-\84J\9b±ôö̦\8e|¿ôN >±,.\18·Bo¢\f\ eP>}óÖ!\83\9eê.ï&W\164Òá\84\a\e¡Ê®|Ùâx_ìÖdu\17ÂÄÛ@\94\81\17¤ð^\10ÔøÊ}"à\8d\ 1\0\82ãç8|74\10¸£ç\99\11¼[Ù\17\97CÇ×nâ\9c»\13Éi@k\99\9f\1dÖ}µ\9b¹\81\13ÀìFXKf%Ê=Âî6Ån/ã3ÌòH¹nl!\1a÷ÕM\14ͯw\9c^S7FP\1e\9fít;¿3ÞÙ\8f\18\94n\ 2ì3¼\ 2U}ØYtsöN6\e\8bó     ºS{QN9P\bç<÷x¦1©¤ë\86áܦ\7f¥\81m\82tsæö¹ È3wÅ~¨\1a\89ðr\8fb\1e®mQK\ 1Âf\1a\ 6U¹%¬\86æLî/\8c\8diR\13ç!\93Û\9d¢¨\ 3\10\19N&÷à[ZÚ\rø\ eáw\1c]% \86¦9:îD\94¬\14^XMÇ=\88[sA\86     2AÅ9Æ­Æ}\91\93õ¢Ù\9c¸}\9fð}1V?PÇøÃ\e\92Eá¶c\97\8e\1dDÝM¼_\99ÌxÁ]\r\12´Î-¸EÝîÒ o¿\ fº`òW-/û˲\ 57÷é\b\7f¯Ù>\17Ü-Ö§\\91ûÔaÁÝbýØNQµà¶\ 1|«3\89Hãé\vîI|wÕÀ\ 3²à\ 6\9f§ï\90\10ýr\1cåAIzÁ}¶\1dóbÖB\e\96ÝÀñW\83Â\1d\vJ\9e pK\e\ 37/\v\80SJJ¡p[â®ä\9fpßë\ fh\8f\84¤3 Bs\ 5T\v·6\85»á½\ 5¹¨T±Q¸\13TF\95FÌO)è )Üî\9bxt\9b\82\94%ÜMÒdù\8e\8e\8b\r·\96µèóÀ1m¸½¦¨\92'ܱ\89¼C£9ÜÙ¿\17r\11\b8E8ܲ\vçj(Ñ VY×À\86W²ã\1c\ 3°Äb\ e÷]\9eä\ 4\87[¾ã\89Ë\14'\1f\87\80=Í9Üé^¬\8d\10\87Û(\7f\14ÓéÛ­A=\ e÷I\96¸c¦\13×\127sö4¶\91I\0s»:Ö­ÖóÖó\ 1¨Äí\80ª7\WÍ%n~d½H2\15\94¸\81^\ e·Íö0æp3µÖw\89|e\8eÃ\r\1dS @\97yµá&ÜC¢\rzb$Ü¥vP»\ 3éo\9b¿\84»Þæî½áæ\93Ô»ØZm¸·!nÂϳ9    7õ\1a\f¦\84»-©kÃ\r\ f86+\98\86Û\08\1aÊ\1c ©ê\86ûÓ6^Úøßp\17müZ\8dÛ¦sÃ}ìÁܳán*½\85(Þ¾\84»ÇµI±×¤/á\86\ 6½5ñÀ\0«\84;!\f&nFk\17 ÷Nö\14¹\12/.á\86æs³b/Ø\91\95\9bÕÛ \97W\90C\8fÃí+§¦Ä=\ fc\10l%î2&Iw\8bÛM\9b
+âö°V\8a\9b\82a\84Ä=L»ap\12w/£\87þä\ e\849ÜL\9beÙÚ\16B\0Ôp\ ew\ 3\8aAâ¾¢
\84\e_H\b\8c¸N©0(\9eJEpj\0\11\ 6à\96r{?YpC¹>J\8b\10\98òª"÷6e\90\83Ûæ\0F¹Ûä2Ñt;¤Mèz\ 1T¬Ü\ 6\a»rFh\15U:¸í\12\8b\9a\15Ç ypÛ7ÖÖï¯íz\1e×       \15\ 1\9b\ f\9díýágo:Û\1d
+3óYa¨\8a\9b²½B\aÊ\10ô/ê\ 3ìb[ÂX½\81Í®`\e\90\1c\r;\10@yí\0v\93n'\82&\[iÜ        Å\97"#¸³öÑa\17½²\83¿×.\87\83<A\aç\0©jwg\80u\88ßÒÈÔæ\10ú#äm\19gÿ&ñ\84^\91z\88\v\92Ëi\1f\ʦ\9fm}@\11»\81iïO \1aC Ó6\10¢ZØ\97\b2aª\81/\9c´\81ëv4\92¸_£G;}\17\845\97§\17\13\160\1eYh\13ÚR|{7{\14h#\80\ 1\1d\8d¤nä³\87.?ÆbÈAù'\86gÓî9w\8açð\81º9;Þ6\9f^\8akrÎ\9bmàó¤g\1c0\10³U³\ 1\ 2ï \9bS\0ÏÌ.CÃ\ 6_³|~\98Ý\1aµü4\943x\90ê²SnÁ9\95\18ÛcÙØ\7f\1cv\1a\ e\18\95\r¼¬.IU\95\1d\1c¬\9c\1c\8eh\b\8e\1eÙ\10å<\12¾Ü\87!û       ¶¦\1e²ßqUU©^!¾\8fýF#¾\88\8eí6$ì\1eר#\ 1­\ 3¨dìl\98=ºè\93£m\8b\rw\89¶Ë̼¾>5µ´\14\9béÓX\8b\b\83\17@ ~Ð.\fL=\ 4£/ 6\9f¡\ 4ª\11Üæ\ 6
+\87íÍ\809\87-\95®\aùVV\81\12\86}bâÈ\98ºÀ«\7f?aw   \ 4P{\1eí'ì\vP\18 Æ\ 4¢sáÑë¬\82Íë²\84Å\fòÈÀÎi2f¼¯éºd`M¨\88\ 3\9dú\1agÈA\7f\1d\7fb\r¦iÙð\8d\ 1¹!J§ÍsfèëÆ=\b}\9e\7f\8efæH \9e<8\ 1f¾õ\ 1\ 5a«]§®\1dYóº$5~ º±¢|\11¯]\1aos6\9c5Õµî®\93F\16\1e\91\ 4ÄWvÝÒa­^¦®ÕyoàZ\12tý¯\99\15¿¨\81\97ë·éßê\1d»¢S\1d×HFû\94\v\12\9a\93\eªÈùÖ\82\17ñÆ\17±\95\97\85\1eZOê\9fÿt\14`ïµâGÑ\8cB#ÃÖ©\90\96\16OØò\ 2Öú·v6Õ\90Ø\9fÖÐØ¾ßÞ|Ð\ 12Z_Ëæ\16Oæbö¬A\8aÿò­\ 2v\97²4ë\0öob÷\eã\ 6¬¬A;\ 4\9bÔ KCd\1dYòÒ\91uáØ\90\14¨x}äV\9f\1a\ 13E¢\ 1zúX\f±\9eÛÆ\84 Ù~\ e·\aëZ\93\17þê\14\15\8bõj£s"?\8dÉøìêÓ÷\ 2\8c\99ùTh\82\808Gqµö\ 5$\b%\12³Õ°Ð3ñj"Ú³\1a*èô(0[#Ù\15Ë\82Æêüô\8bÀ\81ú»W}ó¶hevhCeµj¦ó[´6(ÚdU{+"¨wP'\f\8fª\rákàü\ 6;Jy\ 4a\9fãÇ}e\83óHuI]¦þ­2B\10Õ\83\8b\95ÜÿS÷\11X¬¨¦\9e3íÔº\ 4ÉÆmjø\92F.IÙAI¦vÂî¸\14ó@5\ 5¶Ôvhë~Ò{±\94\9aN\85}®Âé\18vHêC¯?UL¦é\8f\9a\15á\88\94\ 56j\11E\9a3R\80§\8c\16õ){\9bi\ 41zC¢^\8d${þ\86:ð`k\8c\91à      uê¡`uóp­ ¶\96\1døµ\18\88\ eP+y2K[ØÃñÓ]\90 þÕ"\8dݽ§µ\1d"\ 3;x±yz$z¸¦;ýàb\97®5ºÓ\93 \10²´%'DÕiZ\96\80§R/\83\1e\15\ f\84újü$«±³Ãéâ\90ÆÓ\91nºÃ¤n`lzp9n¬_D\ 5pºÂ\e«5kÙiÃt\15¸ü\b
+ìV\99\1ej\0\12\8a3¦ù\91\aÓØ\96=\ 1³\12¤\97V\14^_\1aÂ\8a*.͵\85ô:Kó\97 N\ f]i\85µ&½\80ëý\12\176ªJwQÛW3*\fß(SºQÒìx(}\ eûd\ fÓI´æ¤\95\1a,Èw0ip¤\1e\9d¤¯V\8c\15\1eé\86Þ\13\8d¾"½ù\1fE\0\86ôüVÚ\b\1aªºA\ 3é|\ fK\91\8b\86>z\9d ÉÌ \99\8dG\876y¯Êxó\1c\1d\9d$C\ f8\93\eí\95\97Q\19¦\14\1cªÑ\7f\9d\9b   3Ú\1c\16®O\9e×FT\12\86ÚÃu\8c¬uÑ\90qÂ?ïá\8a,`ÑÆ
+\86\87\aj\ 49f\7f\8a.\10Ì\ 6{ÓlOô)\84\ 1\0°\95èÑꫯD³¥\eÕ3f»X&î/»\88®iw­Ù\0\84\v3\17ø=\v¢ë\ 1&W\0²        Õ<ô¢]\88)*\0phàôöì\92Á3tZÑ1\95\ 4\96G/´ªé§#\89\0?\14T¡Çif>P\b^\0â\85X\95¼xyXäEè\r\1dçeèOWóåA\ fù\88\ 3\9a-\84\fz´çq\ 1\90ÀÎ%\ 5-»bs       \b\1a\1eE­£A¿³{\81\ eÉN¢\¤\ 1½2ø-5 \v\90^"¨\81\17 ÐLÉ\8aâÅ®ûgÿÜ)\b­BÙ¼ÅJ¶æ 69çO³égKc@÷\9fWf\17øÙê\9e\ 2x}¶\15­¦<Ù\0\80\b%¬ñ|Î\00M@="hUù\1c\ 3\9e.\90\99ÕCï¹u\83\1a\9fg\9c\93öÜ]\10\15\ f2\99Tõ|¹cö¤¼ëQp\17z.±áãVöíQ\ f¿Ëó­ãßW\19\7fïxÞø¤FÂó\9fh\9dÇç\92¼|gzçèÝ\83þ\0y\83"î\±\82`\ 1hg\89\17\9e\87\ 3\18¸¯s\f\1fW\1aÝ0cku®¤iy±é\8crÈëÕÊøyV\ 1\f\82\89ÎÆ\82á\1d´sNÁva\18ÑDæ|ÃÙéÌÙà\8d`ÁØ\8e\8e³Ê9Ó̰ê\94]\9cÓ\90óZ¡ÅËà\9e\90%ó+p\83\19,\93¦ëx\+Ä9ïÓ\86\9f#ÆÁ9\94-üyOÞýÍ\16ãB]¨\94e<½Ù©¤H\9b­\9b\a£=ýâæ7\11@źâ\98bu²ÍM÷\92ÇXÔÊÄ Í\9fÀ\9d­c£\9f\8dÍ\vÅCiÙvnø8½f?þ\8d)\15\13­Ùµâ\ 2ñôÞ¬æÑ©\16\12\9e\r\a½4jV\ 3UiN5Í3\92ÿK\8aG\9aáSih:Õ\18 \9a\8bÈR­ôÛ\1dû3[«O-væ\18Å\15&\ fÿ\8a\97\9b¹B\9fHl\91&¿$rdæÏFæ\1dIÖȸOË\Þ\ 2­\83IµÌ\924ñ!M\949\9c\9cä&\92¹`\ 3t\14ù1\8f©\8bñ\1a/7\f\1e\81?\10É\r¦Åü\jÞ\0\9d£\ 241Ë)\94+MÌ\1c¸¹W\rÆ\13±\98\98%nËVNæ\97Í\14M̶6¥¬\12db>\81ÙÕb\7f\ e\88L$¬±\96\82\1cf^At§\1e ?\87Y\8cÂêb\90q\98µr6     ¾\9c*BÑ8Ì\87ÿw¢/Í\84Ù\ fv\8aU\121\1f\13fÂß\ 5󬯱9avÂÈ |ÅÏ:a¾!\80
+Ö·¦    3£\81B\1cN2}Â<U^z}Ç\1c\8e©\vI¶>×¥\9cU0\8b\91a\86S\85\86\82\99æ\ 5*\15"U¬`îñ6¯H5j\8b\82¹C.¸\19JbÅ\849Du\98Çç3¥v\98ï£\1a½\19\<%;Ì\8d\1d¨LhÈLa´Ã,
+AODf\rÁ&æ¤
+æ¬\0W>\eµ\bváÌÄìg\9d\85\86\9e\ 4\8d \ 2ïß"\98\94\80Ér\98EWèå$Ä\1a ó9¾?\13\ 4\84Ù³r\9665Ô`\ e=Â|\1d»¼ôÌútúî[Q\8b0\e\13±2?æQ_\11f\8cfCh(Ãr2ÿ³17¸Ø\9b¯\1f9ÄLb/{Çê\8a\eèT(vØ ÷Ôð_N<6\99(`\1dÿ$õeáì\98eð\©\97©8\95ÍJÀðò½4ݽ$Ô\ 1N\99À.Û7fÛ\8cLü+«ù\^v\ 6\96\1dC®q\99\96\fRl¡gB\19³zË\r\8b«\a3\1fÚ²}¯\87æj\19F\8aeæã\ f ÑrL_ã\9c\ 6+Ê-\87¬Ìr=Ü'\9f\f\90ãc\19{IÆø6,?Éï\9a¥\1fÊé+\9byl\18\8d\98xf$ºr°4Ñ\95ï]\86ty%ñ¿q\96µrß\0Û\83P'9ëL\å\83>{ul\ 2i©·¹\17\1a\16ÚL[¥½]\9b\9a\17þ´ÎÞ\19tÇpº  ^4\1a\19Ñ\83\19åMË)kÕ¾\99«#&\\14%Á)ô\18Lp2`¼c°á´\81ÖNÜp\ 6fq¦U\1f¸\1f'eJ-ã(gI8¸\91 É\f/æTýÔI\91Îé(\14jëeEt^sQ\9f¦³\95Ì\98\bµ\ 4¬Nr~Ð\85\8aÖLÞuê®\a´\95\9d    .«ÅíÔ@×\7fÛÉ6Ï`;M\86\ 3¯®ß@M°vú³§7ÝÉLhæ\81¾ó\ 4Ãó9&4\r\8f§~Kn\98§Ô:aUè\99B\0\976\r\8aéÌ»´Vû\947\9eY\81\9br^\13ZËýÎ=a¤¸+0i«\8d\8fK!öß\13Äíöp\8fZt\b»ÖÏ\90ëi;ýÉÝË\r|¦üÏÅ®v\ 6¨­æY= °¼\8c>` ÎÊ\92ð\1fh7¯¥QÐ\f\9f\fª%%å1×ÛA{CÃ\84\bÍ~\1e.nB³DÎ\97\15z­Ôó¾P[\v(ACcr¨}\8dCM\ f\95®KÃ\8d7\88úêjFd[æúmÀ^²Â>{¢9¥´ø\14½íd~XTÿo5\93\8b>pÆJ\18M\14#ò¿\8c~\95\ 3ñ4Êì8\ 5áë¶C#årr£ñ®V\95)GíÉmî(D&v\8b_`j¤GÓhþÑ>\82å\91\11\ 6©Úµ©èCúÅ¥õú\8fjdÒNhD÷!\1c3ÜIº:»H\83£\9b\8c\80\89\93J2P}\83ÒS\7f\90tJ©lRã©ô\86ã\9b+\8dÅüeé5Ãõ\vw_ÊìÈ        S°\1f\9fèJ©\ 6¹Ù\ eÓ0\ f\vdJä4÷Ì\94Ý-=6\9c8Wd|ÊÕ´ìÓ;
+a\8a'Úô
+!8tpóµ\eÆi\ 5\8a¡øsjWf°ØéÚMD#\ 6\8b=:<\r'+sÛ,T\ 1ÕÇ\ 2\94¢ P\9b\96\ 6\18â9͸ÕéñG¢-ÔQ85?T\87ÃÑBQõ¹ÂXŨ·0\80\eªÒ7a\87í\ f©§\95\8a
+\93jX\1fj\95zü²J^*\88uÒu¦ö|\8358µ¬Ï²=O\95"¨B\86q:?©'æÒzÁ\4Z%
+Í]Hª·ò*\1c\9ajÔ¡hÓ¹FJ\1e÷AU/ÚZ?ªZ,è8`Uµ\ 1[KÝ\1f-Z=GXe³yçXV½¥\15Õj\15\13\96¨±. \ 6ç1îçªÜ»Xãw\15ì´ØðÕÕCý\ 6\1fÓ\19LÕ¸D\16\10
+«d\1e= Å\8a6u¬\8cí¼h²&\1dªö_Öà\ 5E40qÖi\19\16\15AëW%ác¥u@äz\93j\8dðZk\12     î³Õ@K\v\87\r\eå­ÜkkCpÍô\9c\18\f®Â\1fó\85qE\99]]S¹ª
+UÞs\ 5%EÚ\7fº®ê\1a\1d\8f\ 3°«í«è\80(RåðâàU\82\1fJTy\95¤%{z½\a\19÷Z\16\e6ùÊ£9.í+cí2\84ýÊ?I\ 6ã¿Rìª1\8b¿à"?`çÜ"Í®\8f·;"ý2\80\95Ý=À\b«§÷\96,,_zT\86\0\9eJ\11\82\ f«\9e´\1c\9c\ 6o
+=\93¨}b]\80KP,¶\9aQ\ 4>\1fTÙÁX½¿\8a\92 iìv<  \ 4äØ"õX5\ eø\1dd\ 5%'+\14ÙDÁ\89?É®Ë\1cd\92NVLYï¸>_YP\r®\19\97Å\83\b\85:\98\15£ÌêרÕ4;Ý\9fþ\84\9b\8d\95R\86rÖ?\¹êÎ"\90{6nè«\97+@k?I¶\94Ðö{oJ¢í\97\ 1\84\e-±kùA\926²-­È:äÕ\18<µwØ\f\7fZÓká9©\15%rMò¼]Eª\15Ô\vH»Õ¿ý£\aÔJ¸µ¯l­\8fºV\97\vå¸@º\8e[\11\ 5é×v\9b´\1c\88í#ï@êF¶Ìζ÷ {1m\8b¾\93\1a¯-\8bK1Ámé^Ì\8eàÖ´¼³Y\89N!#\ 1\92\9fna\82ö\19¹[3Ú\80Ä/\QªøíÞ*Ý·A¿1\ 2¸H\85¢ÕaãµH\ 4õ\84ëfó£6ÜTG(`ò,\127?}ùªÅE\81âx5\84\0|\ØHîòÝ:\ 6Y\8drׯ@\80f¹9\90Âêæ\ 5\1a\16s\9d¨ '^sC\99JÈç[\eì\80öN]ÿ\86\fùÊB×\réªLÃÏÌtå\1du\15Ù\1c\8aÕQ\1cz¬®>\ e\82{µ®Âf2_wú±«¯\18\1d´{Å\ 4DÉv»\90     \82¸\9bb\ fTÙÝçÆ#¡zW¯$Jñw\9fz2\ 4ò\fèØÁk\85\8fÈ\84gÚ=âs\94\14í,âÀGÆ\eÑ\1cÂ\9bvJ²¼GeÍËæî\7fÏ­Ó£×Sï\11\ 4õV\95|~òÐw:ÆÓ\16\8a½¹+}.ko\8dgPÒ½6\b\0\ 5úÞ\7f<è75(UÐzáð¾\ 2_ùÎñ%_ ¯Ë\97\8aL\1côm\ 4À§\ 5:<X«ªü\eÐþ\1e\99\11ÄU6ß\v\0\8bI©Ò\1dCþ²oжÀò\87ñ¡Iʾ­g2L\9a¡Â\r*úGp\9a\80ÙU\17©/È¥\ 2\83øÚÑv¨¾?ò}|¸A0/ª¾-Snu8""¡/B\81,\a+¤ýMè»sË]ß\1fôUY«!\ 2÷è\87\8cP\rÐMè|\99\7f:\8c\93_Õù\92WÜá±ê·Y\86Zb\8f`çË\85\9e\85î\1aù\96µ;_»¶©õUÊrâF
+i{À;¾õ}\93çË"¬\93§¯\1aL\ 3@VgÑ\18§¯\0PJ8ùȳ¦/º/   +;óE\9eé\vÄ\9f5ç)/\eÓwmekXÎábú¦\ 2\15\8a\88ÈXD¦ï\9eg8òx\ eK\18\97¶÷η\9aÌêV\9dïEJ\8fÁóÝ!&ö¿4m)\ fð|±¾\1957ö\80ë\1cÏ·p`¦ì\1d©(Ô´gÓÄO\14        0zç\e\848s¹;_â\18ðd=kæ£u¾o-3\a(_¯ü\13\10sƪ|ñÙÇ\88|\ 1bå[Ìá¨äù\82%¬½Å\12åKÕ/s\\9e?c\8e§½¬¥¹ÒtùÓG\10\8a÷æ\9b\1aØ8*_mså17NÔNå\ea\ 3Ùÿû\15\93o5g#´\89ß½\17¿\9e\ 5±&\ 2{->í\ 4bEñ\ 5/\8c\8bç\a\13\9f\ 4EI\bS|õ]¢\b>¢nµâk\8a`\0
+>M¾]Í\91\81ì\98rK\17\rúúOzL}i\v\9cÍ*!9õý\80\97¦ú~Îâ\93Õ\97¬\14`Ðä×<\11­\95H\ 4?¾éïØ­ÃúÖ\1arÊê\9bëàCôýz¦kæK\8fü[\90/\ 4^\116\8aðÃw\ 5»¬\í\0        \10_°r\19\18A!¾x\99Ĥ\9bøºê\1e\8c)\11ß¼\9dö's,\18ñ\95/>¬\81\8e\11_àÂ\1d¡T=\ 4C\11F|#Þ\bÞ\80\11¡\96«\rq`Û0\15Ä·\ 3KÒ©î\88¯^FEa:C|%\0cM!¾á\ 3\1d\ 1\8bøâR.\ f?ñýÛ\9c5ܦ&¾.M>r\85d\16$¾7\e\8e\ 2\9fPN|Í-\91e\88Ô#\1eºâkù×0Ì:ZE\8cjɽ4\81Q\rVU#t$b\87-\a¾\83e\9e\1c\aø\ eK\14f\80o´Ñ:¤Ýµ\87\ e>PI¾½çGyÆiMUÐg\10_.\88à)ÄWÂá¯kä\ 3ñurÒ\9c\ 41iî±zØ\83î\ 5ë%¾\\11ºÊý¤q\7f\17Xs`ò\85Û\94\99É\8atò=ÏE\8cMGM¾\93¯#\ 4Õ±ÔÌÛFô\88\16¸tk\9bÉ\1c\9e\8b\14åÛ\99Ò#E  îʯøF0\94\8aP|×nÒR\18A,Øf=Ú^ÕM*U|õ+Ó\1f²XÅ·ê¼tò­1kpÁ¢ïrzÉä+P/@\88¨R|×}+eòw÷.á¼R@Ià\v+¾M¦\16Ù\8aâ\9bËy@%\85¼°\8aï\99"ÖY\0f\17\98M\895I+¾\ 2¢~\ñÕÓ7\98Ñäû1´Ê*Å\97J4=ðä\8bÝy*¦\9d|ù½@- áq\93¹¸æ:\9c¯\93÷X³}\8cìη\8cÕ-òÎwÛr\95Æ\89\9f¬`
+\ 5ÿ\98mH\ 5Ü\8e\1diÎ×Y\17Û=ÎWÅëVÛ\82\9c/xÅi\892$Éù\1eRf|§ýA\80n\84Éw\94ó\82î/B¯¾\r,+ê<ùB\14\9aèÕKØíìÖ}Ôå\88Îù:nbÐ\85\9cj\9copïíào-¥o¡àwZ_ ù\0   ¤/#\99£Dð\0Æ#      ¥\8fíùhÙ½ÊI\8c­\ 4\1cc¡z5ï¼;Øù\86\ fC\84éÛ2
+\13z}\89$ÌÊ\86ûÒ\1d\8bÁ\82__Í«ªür\86ü£é\97\98¢\93àî÷Nµ\f%\ eÔ×\9aÝ\9a¿e¤âÅ\ 4¤¹_\aó\87\8fa¹ßÇ\10ÁÕ\8aÄöûcð\16Ô¾Iö§\99s]ÆÚ/ùyC^]´§Ú¯\1cÆÆ\87\9c½·_q¹Æ\ f\ 2C\»\95ü\91I¿\17²)\14²\ 3Ü\15~7°r\ 6¾/­¢_QÚ¬§KÑ\aÿÜU\95Êné¼½\15{OÛÚ.\19Ý߯\0\ fùEßçµe÷$Ý\9dAàâ      ùÕ\8b\7fü^åê\ 4 _\98\18\9e\97ñì\17e\82\87Å_z8+àõ÷\7f\88«|\ fâ<îúëÐÄ1ýÛÀdyÛÿf\ e\ 1<yº =Àño3<¼9è8\18°\8c1a
+\81+\bmQ«ÀÙÑÊ`ÏÀ9\ f¼¬Q\85\14Á\12;12\90\85\85QF\80tjÊW°\1fs\f\ 4\83\93\ 2­ÆÌ$\1a|½o\ 4*\ e>:ýÀzðÖ¨Ç\aa*fñlF8TWX©\841\88ÍÏío}\9e5©mnç\1c¶iéÂ\83e\18.Y̰0j82M,ãn8bS­k\18êÆÃ\95`#<?ì1\ egT\10+ó)lu\88\ 5shà­æ¿J\7fÄzRÈ\96w\8b\12\ 5Q\8e\89#\ 3\14_'®\12ÈÝPìð\11l\ 4¦Xú\8fæWÅʶ«\98W\f\9f[\ 2e­Øxõæ,Næ\9c\90.¸xÙ^¬o\vÑ\84ñUý«%\8eq\89ùÉËø\1dh_Ñ\91\82Æ?¥Æç\1d¥Q\95\8d\83Ù\8d\ 5º\bøÄ±²\ 5D\fæ\98\91p?fÝé:1Þ±-EJ\1f\1fÜyÌÓ\87à𱰹Š?ö\92"\14\93\11\1añ\80¬\9c
+ÕÁA\96÷CãBþ\13\8ek(AMJ³d\8aü#úÃÿ\92¶£æ\ e\86\13\9a\10N\8e"'\89®\9d;Sõ!_Vü§Ö Åx\1f2\aô¶XÑ\f\13\ f(\8f,É©×B~rÌ\ fyf$°þð!GL\10ØÖ`I\90Ra\ 6ϧ³\90\11#*\ 3ó!\9bD\915M\v;\19Yh÷ÓéÈ!\97«\9f\1cYqè\95¹UfÊ\91é½\8dtD¾³ãÄ\91½ö\ e:\7fàx\95Ej­;\8eì\0ócâ¢+\12G\1e8\93ºâÈ"\b\8dÂ5ìK\13\0·J´ÉgÁ¾ëðIó 9²2îÎÂ2\9b\a\18
+ÉÏ\9e\92û\8a\0\bÉ+^\1f1ÜÜ\17\92ÓÍæ\8c\vÉé\16\ fù\11E\8a¬j?¿%9\96\14º\19»\ 5,ÉC»§\18a\85xY-{
\ 6Õ²¿\ 5sR\92Inp¨é£9Jrqîyj*\85lqI®\85Q\18X\92uÚ*%Tòå\1d¾X\1fª®\98Ðö5ÙR\88\80ãt\9eI\14n>S\9c\92ϰ'ÒCJîÂËî\ eN\ 4CHÉ\8fíi\1dÁî\89þaL¡×\16»\94\fÏÄiî\\152$%\93Ï&\17^)yDî)\br*))â\ 3l?\1cFx\8b´ö»\90\btJ¾+\83\96Úkèi)Yrh#ø¤äëquù\86ýH(\95\92+-\98Ù\ 2ÉR2\8fÛàqo±ªØRrÆy¡jK¾\8a¶ü\8bÒÅÄ'\12Zg\r\7fF&?Åßj9\99,T5h\8d\9fz®´\90É÷v\b¼;Éäó\ 3ná/I&ã©D´6\99\ 1\97î7\99lr\bh\86+utd²÷\0Òvdrç~Sh\93CÚf¿\93Á\19ID\80òÛmI¢|ø\ 1\94L\99´)[ëÏhIå&
+\8cÐ*gà¥\12ºÍ\14iNj+k©¯¶¥iÕõWF\8biTdù\ 45JEË"\91\92\9f[oàûq\99\ f{MAme;\19\ 3Ò\1e6ê/\9f\99å[\eî\ fÄ\9c4uâÎ\98§ð0´È,\8b\85¼Væð*ħ\99\ 1Ý´EÏ\9cáQù0\9a·\1a\8a?ÍDGs\12kf\aùhP\v:\91ÉE\10ùÓfºàmrs{-`\87_¼»&\83\8f\97\1f\9cS\0\rë\9brΠ\ 2Ñî\9c\ 1¼ýyLç\0YÂ1²B¸3}SÇ\v\82çT@N-|\8aMÏûRÿªWª£)÷¬K/¸ò9õG»ûìü\1a ´\9f]$\r\85ý\9c#\b\bþgM\949\\11è$"hNó¹\93\ 6\1d8m\1d¡±_¼\92\85.\eìù¬¡ó\8f ÅH/~\98#\1dÑyÏ£\85¢µV\ f8gÑ\17Ã\93\7f\16­RËQbôÙù_Ð5\9aNëÅ×h\r@[¬£3SÜS\1d]ª\þ£¿(P"ÍR\81/s\92\9f\8bF\ e©\91M\1aõW9_b~\v\e)}\bÃ\vÏñ<\92Þ÷¤i®ÑS~éÜ¿¤\1f2ýx©Ð75H\12Ц·n\7f\86¤+½a§×\17\85ÿJÔÙÌ\9f\1e.\83ºå½\8f\87\8fbÔiF \f\92\86â\ fþB\10\ 2\81ÃcFGí`ÅÏdRÇVd^^jd(ÃÜK\8dm\ 1\8f\e/5û
+Ãø\ 4\9e\1cºÔ$¾ð\9dB«¥þ@5\89ÚRs\vÇâ¦å\ 1O@©^³IK «\96\9a¿öÄÍ\96Z\90×\90|®Ý8)]\10Æ\ 2Öu}¶Ô|h\\82#0ã\0?\8d\14\\ 1&«Ij:¶,Ø+hIj@DÖ¤·\17|JRÿÀ\875GÿÑ\87¤Î:sá?\ 5Z\99e\8c¤þáÅ&­É0Û\84¤\16Ï©º$u
+ö=æ<i\90ÔtY·Ç5\91æ\v\92Z\9d-n'©µ\85\16ÇÙCw!©wÆË]-µ\12 á\9d¥n¼\82â\12FG\96q¤æVJ=ò      á\8dÔ:G\94LVO$µb\8b©+âº\0\13\92\1a£ÔÂ\8c-µÚ\ 2\88(Ë7KýN\ 3±ç·fI\ 2èËR£ùÔæ\85\96Z\b\1cÁ½¥>Fî.;&%I\9d\9eF÷H­æoNç\9d){\z¤ÞÁ4=;ÆñH=ÉÕ\r@\80«_\97N"µ\97xu¿Ö'XcÔ\92z¿\ 2ZVjpxDf\a宬Ô\16\97'\93¦{[©ÝIuªÓÔôýÛ\13Lm@At\8aéå¥V\98ÓËA\86      \9eï¥~ñ3¨±\1cú±\19o'þÇK\1d\12\82\80%\13%\vd/µ\99[\83l\1a\90-\97\9f\18F\8fV
+m©a\89\ 38\bÒRó\12H&iû²\ 5\8eÔ/\ 5:X:Þ3yè\1aä|\9c\f\8aÔ\12\10?õ7\15\vÜâN'ò\81ì(¹W/p\83ÈìÎr\16©%îRmÍk £vú¡1Ë˨ÿü[\91ßÌ`Ôr\8f\1d°\7f^H\9d\11¥Ö°ËÖ©¥R;Á1\161àG­\9cJû_\92æu<ê\8fÞl´*xÔ>À að¨\832\82µ\13ÕRQw\81!8\9dgÑG¦\f\15u\96\ 4_6TÔO\80\84î>\ 1\18\15õ\12\8cú>\9a=VÔ~ÂV\9a(RQ\87Éjß\81¿®¨¯ßç\8d*\19¦\12ð\ruu>RÙ\eê\9aAy`\ 5V\9f .\9bì»&¨M«Å`\12Ôf@T4aPKP\87~Fí:Ï)Am\13\83ñá34P£B\1e\$¨\aK^\95\17æqIPC1\ 5Î÷P\1e3Pÿ¿zå¼ù\92¡®ó¸q5\19jDÜ{Â\ 4\1f\vø\fuÚy#Æ\9dì½íe¨ÉO
+4¬±\18j\ 5kuW|"á^\9b\ 3u\97üèk \8e\vq\95¡¾ÅÝ\97\8a\1aO\9b\81¶£Þö8;\93\8b\81\7f=\a-¢-L­\8f\15\13pj\ e\88\8f\8eWC\0J\10N\rÈ£¦\ 5b©;c\7f©\1dbdlòRÓ\90YZ\9fó¼h\14/uæÄÈ\96SëVë\81P]¼b\11+Õæí\897\99ªó¹\18ì¹jq\99\97©²øLmeõ/Å»r«#£\0ß»º2ÌÖ)`\1d¦÷eÊ\85k\12°\ e\e¶´Ý\vYL»º`úèý_­\8b$¨H¬ÙË\92r\ f\91¬\8f\ 1\ 1M\9aµù" c´&\95ý\ e¬u¬º v¶\eíøÚº2¬5áµp$:µuZòy2>EF¬5\18\13qÁ©\ 1mýø¥¸Ö
+\83j]¤\\12®fk´\94Þý4[ßà\816·A\85½ÙÚ\1c1\93(ûëö6[³¬Â\94IªõN¬\ 1rX¹&ªõ\19\11Ñ8B\14\ 5ÇP­u`\1f
+\1cZc\8b\ 1I\86Ö\96\99¿L« \1dк\91\18¤\1e\ fÃ\16\94\93d\19´V\9e\0«¦Ö\87ôÛ\95²¦§Ö\80cÅÖWÜ\9fZGX¥Ê;Âh¨µ(Sw\8bõ\ 1ÄÅ¡Ö(Fl\9f83äÇ¢Ö%
+x­Õ`w\91¹Ë2£[`;\aµ¾\8c;ÏZ\86N8ÖPkR\9dü  É\86Z'#@9\1aÛìm¢Ö<]Ì\86aëT\ 1\98ñ\9bäd\v[S¢:ÐÖ9\ 5¶6úà\1a\92Ë©Øú\1cÒ\8aP8þ\87lm\18)1º\19C\97\95ËïrÙºDÖ@¶å\8e\85ÊÖ\16þ!\ 52QV)n¼Û\18oW[ZLfk\ 4ܪ0\e\e6[kÇP\8f·Ö»Äh\ 6}\99îO¢:o\9d®ÚùJ\18\85ç­ëÿ¸àz\ 1BPÞ\1a!\89\98>\98
+ÞÚ¹?ÎÎ\11\8dÁºu\88¤Ã\98qm\r_êÖ¡Î\93¸Á%jl­¯;¤¯[\ f/ZÅÐì£ÐÛò»V-ݺ\15%Ìòñ\11æéX2\?L2k<Dî\8e,\99­èg­}¯{\ 4î\85å:ìfËR×¾] uwÍëzÊîZïÌc<¯IÈtÀ×D-$¬-\19\99!¿Æ0É\1a¿lÁ½èK¶\7fJ«OaÚ-û|)l\19¯©Ãæ%\1fh,#råÌ\81f|±\17í5å\e»\83   ÷~l\92/`íÒ\9f\vq\e\ae/!(5˶ö$)\16\93\87?\9d½4{¶Î\bm­CX\97ýqÄ,míO\9b\8e\99»Ö\0¬JEµrü`m%\14iε#V¸0é\1eìrÈ\9clÛMg@!0¾ÑvëͶ£sB*«Ù\8f TݾÈ|Ó\9c·{Ío+\ 3úº\ 5wÒá\9e\97¶I]ܬÊâù¸í¨yk&·Z{EnÉý4\84\e\96[Z-{hn}\ eÇ
+ÏýÛ¾\18\10Ýæ\ 1\1d\8aMw¦2\ 5\a\1dõ¥Õt|ÝüÍP´\eÙ\ e\ÜM¸\13\10õnö\vÞ(Ð\90ñ\16ë\1c<awmO\ 1uÞv\1cO¥w$³£Zï\7fµ÷WHn\8c\f£_\1eÅàβî:**P\12hu1ë1-\99ì;\91ßXwo3ì·¨\9f\98Ëêo9ÿ»#Å\94~\80Ãàfc\15¼xFà82}A\ 3×R\9dK\82ÃríB¸àüopé\9f½\ e\80puV\12\8e@
+\17Ö|k}ÏmÁô»\f\ 18kÃé\f°­ÃíçÒ¯òYÜ>\94wýûÝù\91M{]ý¶h\bQ<¤SäVq\9c0<ç\96\9c®\ 4#Â8?6`p\8cÓ\9e\88ÆÙáâ«lÚxÓ\14Ç\róÒuüö\9e\9eôy\9cù×vòã­º \9f¿\13ÂÌÍ<¿Èï¨Ú\0H\9e\99çðHɽ2yXÛ`{;y$ÊQ¶uÞ\94\13o\98\ 5Zåx\16\96/¬Ibv5«NÝ\0\1d\80øò¹lC´0oÈ\84\9a\8fùßMè`æ        2\19\81Ð\1cü3WsûI\8bW\ 1^\8d0\9bãènn ÎÕ\91æ¤d@\eC'h\9dËÔ\eÐùÎ\ 5\97²\85\ 6l*欽ãsç±}Y?׫1\b/\ 3úâ\18ô<\15ºêcZïÐ_¼Ìé\83\1e#V\ 4l\18=29úì\90\ eB\96×;\93\9eÐ*\1dD$µÏM}¦ë\87=ôpºåî0\97§\1f\1a\93¯\ 4uãÿ¶ç¢\8e
+       C©\83kûoon;u±W\a\96>\19³\9bêVx   LX\1dò\9e\9c`¹~~1JX\8f%QK/ëØÿ\aß§u ·¾7$xåºX=&r×ñÍu|´×\87\7f½\89\0ëx\84\9dJ#ö\7fýc\86Ò¨\eÙ\13øÝþ\1eá\fé5;\rð\98¨f'é<\9c®>²«S­aÖLRëªDÚ       j\7f»}_åAA\92Ót2¶ë\96\96«ÙvmL\92êvNcÚ5~»ã|à\ 1Äh¸7\1eÇÊÇ=ß>BÀÜ\7fÐ\0±Cw\1eÎÅ\17º\97\82ø\84\1f3©î#ÍîýÁ\ 10ïî[j]¼sQî\13\91!\88øYï6°\15×1²\ f%\vúþð£±ÿáz\ 3rýÝ»ñÏ\1aàiB\8d4\ 3ïNà¿\15\98*4úàm)|÷.Y#Ã\83\82ȯc\87OÑßÞ\ 3UuPÃ]\96Ú\8d\ 5Et\89OdÕ¢\82LO\88\ f8ü!;\8f7\88Ï\94N\88»Ë\aÚ\83xb%\a\8b³´\89Z\ 6ñ\898)\15¢Ø¡TÅü\0*ñòH\a¥\85n4\18\ 4ÝG#R<yd\1d\1cQ\8a\7f        30\8e÷»THj)X³µxÑ\8b\95}
+'>\8b|5\94¡\16\9f\9bÝr\1c²²x²üx
+yM;\²x¼\81¤M -·Ü,þÜ\a6@\8cÏ»\86\86w\8d÷\ 3S\13«ZaõñתþqãÑ\ f\130êÈ_¸äã)\86\ 4ÊÛ_\99\14*ß®e&\1aË7\0hÔº|[\ 3Ôv\98ÿ\18¹üÌ|º\80\92\9fêcØÞ|qô\95ö9¯\82¯SC±óð|H\1e\10<Ù¬R\90,Ð{Ô4b¥'\8f¦\92á¥0¡7ÙycT¢Ï!Ô»\9d£×-é\11\92¡´»ô>\86?\ 5Nßs¡\94&\85 ~$·9©ÔÓPz«©^\8c×Û lá\7f\94\16W¿º_¿¢¬\97\1eßËsëëF\99Ñ\vÞõÿ\ f+
\7fía&(Fûb\7f\88\0 ì¹\1e+vö\8aªCËn\800íåZª¢_{eÌ\93\ 4·\8f¬"á\97k\81ÛKe\ e\84\aÜ¿\15È z\0÷_¯±3¹\1f\12r5È}\90Ä7^í°\7f±èÞü\ 1N¡Ý'\ 4\1d¸ã=Å\ 3Sa½G@\11"1­óûó}\18Çß÷´À_k\aß\10,|A\9açê\81ÿÈTPäݱwmÉø\v¼ã\8bFäãg\90Gmò#ÚÊÇãËWÒßÊÐüIJÙ<Î\87Fôü\ 53è§\8cÑ·\8f*ý(|\0r\b\1eý§q~¨\fìÂ\88ü\82\8d\86\1f¼\9a>\ 5¶ìµ,\1f»b>âfÁÿ\ 5Ó/ûZ\1a\10pÖ¡×\7f¯u\90ë4W²\80\_ß\92\12«3\18&\ 5ɸYc¦ ãº¢Abâ\118\ eÆ)`'N\ f4Pç¢\11ÃWë\9fÚyÆl
+\9b\95\8aÐ\9dÆÉË>ÃöT¸\v\9eX½jÖùª>\82ºzF\7fz\84µ\eÁ´9>®\81i\9aè£õê\89}nF\85\84Éì\86ë3\80f\8d±àÍÚ?§¬p¨\96khY\8fg\a\184\84\9bÿæ©~ñ\9e\17/¥\Âߦò¯~´÷zü\82\83Þ\19º\9bâ\88gYáé\94õ\16@;Ý®\9c1ôí\15\ f\85û]kOýmu\89Zµ\MÎá§uÖ9¹ÓN['ÈzCoõáV\87ìq÷ù\0Ýè2RÅ.±\a\12P½¦õP:\81Ò\ 6Õ»×&BûI¯\80\1eXLÛö\1aL\1c\85\1ccE¨v\83
+Û·"9°\O©S?\f\80Ûà\8fÓ\ e'»'ë\9b\9f\19]¾xß{\12¸å«åÚ\1fkC\9e)\90\8d\ 4¹°¹E\95\19ÿ@Cö½\93L\1fºälÖ\ 6§ä
\ e\11\ 2îÊàÑwv2á\8bG\84\17\9a\86à.çB\8eìBb±a\94Óµ\88n\ 1À\ fR\84\ 1#µëFÂ{ y\1fý\ 6ø\8a\91\83ÿ\b
+9ÓXÔåITßd\rÙ\9eO\10Wà{\ e\9a;\8c\16\90EÍé\9f\1e\1e\84¾&¸ª©¢hê$¤L|$\ 33gboY6\\95ÆÀì§¢xF¤ ,+Ïq$6Â=Ö       é¸\86FëôÌÌ\\17iæÈá\f4+¬YX\ 6K\95ôؤYhO+ê¥sN®\92àÄ"\8dÕÈíØºA\8eÀà\ 3EHu¦xì\13\16\7f>å*k\8a£/Ê£\90psY¥\13\9c^ÖÜ\1d\82uÛ°¨ã\7f±u\ 5¾jÄe7\f!qîZC\0\a\99\b\85\v\9b\19 ¼ñ|DxÒØ\r­¸\ 6\8aÃ#a´xý\19Üa½w\8cÝ\1cSx\viSh¶\1f½lÿ4q\94ŲX¦k\12B+'bLÆSÍ-ù\16æª93ü2ï\ 5|®,\15µ/\9fÅß\vÐ\7f7ç©æ´c\94}\9aðIã\93\1e\16lëm\1a\8aø\eV\90\12õ\145#õ\11Äô§ÜÅJGÓmã¡\ 4ºº5·ß'ßehÔÈ\1eéØÍÅ\ 2\r\7f¢k5\99\8eB×þ­t^Ú\ 6]\fà$Ä#½{8»\7fú´k\97\ 4¥Z4JH5\8b@\9c3ÝRrʳ?âP3\8f @\11\81\ e2\87}ç¼\ 6\18r\1f{eDÓª\89çÕ\99rîÑ$+Ñ¿G\81\98 4Ük\86ÀéÓ\95âÒM \ 1DYDV\88ÜsJvÏÿ\ 5;\ 3<\13é'0\8138hæ(ð\87qh­\18Çl\ùGj\14ºy¹  ¡\99\9b\87ªf;XvfrLɪ¨\81ÏÏËç\1cÀô\1fû?[X\80Á0MC6+çd^Ä\11'\17EÃqÌ\85à®{\8aÜ"   "×ãÓàÉ\81\14cÎs\18\ 3\81T,\ 5²*XóìÐÝëÝ\19ìh§\ f\9b·@ôÞê\19!R\95Ð1t®¨^Í1×\83m\8b4k\82\82À\8aÜ\8e\16è0\8a\19umr-\bå÷FØ«\eX\95\ f\ 1\12êzÆêÁ£z\82\98á¼TRÐý\18qs\90\1d¼â\r`\8cÁ÷Î`Ãé\18¬ßwʰLxm«gç8Ôú\17ÁçØâ*\90\96]\0aw\ 6¿×]%ü{È*TÞ\7f\95\1fëL\e\16m ÜN^ç\98Ê®_êÍ\a§\09¡wÿL:yJ\1ahO­çhiT\ 3­Yj˾\8a¤$½·\e\14ÞL´¹Þ\84\10ÕÄ \7f\93ch\7f°zìY\8a\ 3R\85_\ó¡gA\bEq\92l¨6|+Y¥\82\8aº¦\81ÛÜï òi\18î\a\12¾)(;û±të\¦Ï7g¾á¹\87f=J
\92\7f¡®\ f\99\bÆ\14\82æ\82à±ÐX\8c\83\91º£\11ªBvú\1d'`×)\98\a\9bS\86jû+áQ\11§\92\94fû¹Ñó©ÿee®ç×\91#\84\96\13°æa?t\13\9e\ 5û       æÑö\ f\11<=!8|\13\0ð\15\9doÅO\18óàeS\98×\88Mÿ\0"=ã\95\890DzÆ\82¾®\bÛqò\aKÕ¢[Bmï%\956\8d4\90mâ¸\16\ 2ï\9e\9bØ(¸\1dm\7f¡\94\0\ 4³\1a\au`\r\8f.\eS\8fèâW\82Ò6]Jq \88\9aǦ0ctç\95³²(æü`\94âà´Î\9fÃd§ Z\9d\18\ 2¡÷\93¨\85\a\e[B\80«Q³ìD\96     \0\17䪲u\8f\92\9c\8f\18zwïJ\89W£n\ 5¼rç\19\86Q\er\80²×®çaî5ÝØËSøÍ    ¹\9bX)ñ\avQ1¯Ö\90\e!\7fë§Å~YXñ|DL\ 5      \945\91°ì\8d\1a8\7fC§ûü\98\81\ 2*¹¨a\a\88È\10\8b´Ì\ f\ 5\1fO9¤\99Iu\7f6\96QõSó\90¥,eüçèl?\9cúK     '\8cÆ\1f:\r\82\9e\17±u¿wjÌÔÇ\ 2èÁï\86ÕåÓÇ<Ö\eDH\r9\ 5\ 3;3ZRæ\80ôEágGPy\84$tÔà\9a!\12º\ 6f$vb\anþ|p3Ä\9d\0ÅÞ÷Úo¶2+yC\96º\90\8bÓ\8e§U6\bÃ\11¡\17Ñk\8f¤\1d\17Ç\ 3¸\ 1«Ç\96»ªà\87°ÿǤüºB\98Fô·\94\11¿¼\9a³úo\veÞdg$\86\92¥Ì{ñ\9fÀ[]\ 2\9aåp9Úºb,ݹèó.\ 3n\84\1eËMthÏ*\ e\15\v\13x0lu¼º64˪®9åô\ f\8aÀBºäÈ*EÌ+\84w'FY#U¢TT\95È!É6¿\ 5H\0\93YTvÀA\9e\1dó?¦¡½
+û4®\0ö£V\ e[õÈ\96[\1a\r\8c~#°K«|%Ä5Ã6c# Ð\81õòù\91\ 2X.\7föOä\b\ 3#        9Ic\8cè+\ 4ʼ~wÕ\88ê\e\83Ö|HÎ{×\ 5¹'ù]qQ`ÿûr?IÉ\16eÿ\14\9f¯\91\1cø\87\15<\85\7fF\10ð8P½í²ìm5\86$cü\\93M\99_\0\11ÿx\97\v\85°\85lwQ9S\9fþ¼¨\f¥\ 45ÄZÏ/@¨u\8ehÚÝÜq3\0\ 1â\ 5\ f»\8eÙ¥q\85¼!\12h,ܧô\9b"þhè2¨\14ˬ\12ê\ 2é¾0ÖjK\95<þÂ3Øk\92ð(\97\b\977\8evÊ\\9atZÖ`ö        \ 6ú¾½\82\85>Âaë\99MÞT\ 2\93°é\98r\87\9fÑ2© ¹¥0TÖÄã4\96J\8e\93\9a\1fð¾ÁÍw7ÕWXÕ3[\0
+
+öÙ^Aê\19\eý¢.b\8c2×ÝãåÈ\a%¡×aÈzÍ\91\90\1c\9e\12h\85\ 1q\19E]!áò\ 1¿Áò\ 6\ 6r\8eø\8aQàjrj·\15*u2Æ\8cA.\ea\18*]A*\ 3\18T2\ 6Å9Ôi\91"tC{\80\90\ 1\95Û\ 3w"\84ñ\96ê!¹ô\ 4¥X\10\8c!|\17fVP\18\89N\18°+¿ÏÎÎ\11z×È>A\92ê*^:ük<>³mYü< VÞ\8e§hÔ#+y\1câLáØSwªøka\82=\89\9f\ et\8e\12¾,O\14á\eD\94OÍjCÇ\898qn c|}\9dZu\1f\97Ò@Ã\15ÒA;´\13Öy\0ÖÚÀú9±\1d|\94\12eQ\96!@\ 6\99ö%î½wµÙã\9cY\19;%[²S.»éL0Æq^î\8b¡æ\8c}ÆÌ\96|F(HÏA\11q5´ó\ 4­f\11ṬÂ\83GçSÂ\18Üñ51\88ÞKµ¨ôþÜ\9e;Ü@h\83Wt\86©o±\1c0r±.;}æè\9b¬±ÔÍ\15±\fz±*\85\ft\99@Dîª%\1a]÷Ä\92Ö\98\ 6Òñ\7fÙÎìT:=L\88Gâ=L4n¯á«\92{¶yzªÿ\18\ f\94ð*%\7f\94¥óäÌðº\ f¦9ixô\97üHr\8dJË@Ï|\b£1\89ðR\1dÆA¬I·\ f ´½D&×ãË\96S  Å:­$ÊAÆÄI¨\8f\8eý¬ú°wPÀÅ\1f\ 1«è\fÚ1!wµÃ]¤eH\19H2x±4^"g¤\ fI\9bë#â=±èÍ*vD³\91_Ø<ê¶^þE        \5øSZP\96wͬ`8\7fð\130ÞÁ*$Ü\8aå²Ä5ù¸\10±G\95\9eÃräTþ\1f\91w5#Þ¨\94,M\ 2\1cÕ\8cþ\8b³\8aè³ÆþiéBÅ\96^Nò¹¾cH8±I8Æ\9cw\e\1c rD\11¡\91q\13\92­ÜÅBïsé\17aÁÒê\eXB,*ìyl\11I\84ظâc8x6æ´C²·ËêÌ\96ù\19´\13#\10ÉùB\ eä\e-M&Hb\8df%\98ê@s\89)\82µ\fô]ÜùZJ\9f¾\17Ö!Û\15Üí\ fR\9eçH\ 2\87ÔH\fº=       l\8d\80ÚM\91ÏlC1Ð\80*\823²±£ªÁ1\80AF\9b½bfÃd\øð\90¾\e¬t±ÁC£\892T¿Ë\87t`Á\7f\0\7fq\81»\9f\10\8e  Æ§\ e N²|¤\12\8d\11D0s·\91p4P_ZVÉ\17å*zF6&v\ fÚ\84dÒ¢³IÅE¥?Þ\81v\ 2Ê(Mz\84ó\85l_\f\80ê7\85Éí0\12\9b\89µ\90YÚZ\ 6#d#æy\92\b\98.\90\9c¹&ÌÊÝ.y\81TÌE=²1%ûÈXFX\0éO(uj\85o»XwQs&ï÷QüËëj\98>\83\9bpH8\11ØV©\9eº"ínëÊ\ 6';\b\91«\90Ú%\84úó\0©JÅAs\8e¢e}£«Ñ˨,\90·6QGÑ\ 5\8eÎp\86¯\1aý\8dGÑß\8e\88\1fo\1142A\16Y*´ ù\9aE>5æ¼a\8c\80çʳeèÜ\989\88Í=\80ò&y\10ò\18\92\85\e\9cÃ:\19¤K©¶F/§×\17\vV\ f½órñÜæm\88\13Gò¤±(\8b^\96¬êÎÓ:bV"\88\88\87ÔÞêIF1\1fÎ\18\89|íb\9cÁ\ 5\13´¡¹E¢B0_sVm6÷ª%Dùý\r¸\83\14-['D¯=òB\bî[\84V4½Ý\82øôP·#u\ 6àZ\a×Ðwi§Ü\ e¸\aÖ\9ed`Ä\8b\15äkw{ùÇè\8f#yà\1aæ2×\1da\vä]\ 26H\85Æ #\9a(uD\ 5\82ç\ 3\1f\ 3á\80Øa\14\83\94)\ 3c\9f\1d¸ ±YæóF\ 2%1\ 2^\ eþÃ\8eü\80\v&k\fF£¸Ü/\9aU\11ð¤±/q¤s]T¢#T4\98\92\17\12\8eS\93\13\91,\83ñ\r\9d$1\aÍ\1e\9b
+©$\1eól\8bXÂUÚ8@\v\9e\1c\9e(\13\94\14Û¬A9ÀröiØì´±&\99\r\80\ 3\19ÍÅ"Ç×õ½I\93\ e\bçj\13_KÕe&\1c\86òñuN Ò\1cFÉoIÆ\15ÁL\ 6úWÂ\bã\14ØöÔà\7fHôò\17¤ÔE"è\8bu#Cüþí²ÞÌ¢U\1a\13[a\93M\1aòy\16\86-ª¿\88~\1a®ò²í¢ë\rnG\1aó$ÂJ0²ä\9e`t\8c(âË\15fh4n\ 39­ß\ 59/\82ènz~¸«¢\02.ü\96õ¬\9d*òªÊPÂh\7f\8d\9d\19~t\ 1\11Q\14\17"CuúÇ\ 2\92»u\8b­Û\89\8dII\9aô\8f\1d£Daìs¥\8e\r\81Ùõ\80\11Ý\9deQ徺\98\ 5ÕÞC\98à°Ézx\1a%ìmt\84¸Á@×Í\0ã¦Z+C£d7@\e\Ïâ@«P}¬ûÖ¥\1eS\13\85¾<\9d\\r´\87?\13Ƕ\87\8aÆ÷"\85\ 3<{õ\ e\ve\1e¸ñµ7Y~ü­ì\19Ìë¾¹7À°   \18ýe¹Eä# ÝÜB3ÎÙ\96fpïW\9b\84@àR\9b{Eg#Ó&G\94\10S5\r\ 1\986\aj\1cwWPìØgÈ`\rB|¶"\90µþ\1d\8dæÍ¹Æ@I\88D.ÂT
+R½^1¡\-\bÉ\e\ eºv\ 3¦\93ül\9cÿ/w\fR\1eÁpEB³ \aÊ\94"I¤àA¢õÕ4Ç\9d!\99å}H\8e\91\ 2%\1a!#8\80'1±ø\9f\ 2ý:7/Æ\8fÄVìJWr8\99ÒH\r¾üÞp°¢\¹ÉVÕ\b/ö~\ 3K]AÒßðz\ f a#\87\17Ç$!\80ê\9b\9a=6â\ 1     Î\ 1#\e 2ä# \93J-ñÅ7G\80F$'\82\v\12°r\91!$T{¦òW+dÇ\11ñ;(~Ü$\93¾LÂ\1f!Ƽ\83TÉÅe­\93cR,\86æ\8a\b"us¿­4£Ê
+\13\ 6qùa6Rf¢\0\9eö\7f\92È\84\9aW<ó\8dd\89°À^
+%¼\19\18ãÙ
+\8a"ÂED\88®\ 4¯¬Âaæ\e\98øQ\16¦\9ao\1e%Wä\90\v5¿p\8e\82iBo\r\ eª\9e)\ eg­â\8b'\1d\1fË\v\81\ eb\7f\f\ 3sâ<¼gNß­]n\1e-Öµ\ 4\eµRú\v\12>7»\ 5ÄFêb\9d½\10 µYOÕ\ 1É\9c\18\85Û)ü\0(zydÙµ-ûÀ0\12£\14¡ýФB¾;÷Åôè\ 5Ôå\f÷\93Ê_õÍ%\87ä@\126ÃÊ\9fx*\eèc²\fc\a\81H\14ê,î\1d\ 3¢Ú¹\a òBªf\94ùîÊ\19å\ e\8cÊþÊ/\\ 3\88\86Ré©þ|q\1cÔ°Z\15y\1fRyü\83\90´Ðm\87¿s\12WäÆt0\82\9b\94ìcP\ f\ 6!=
+¢ZǨ7Ð\7f+§Ñë²1(p0Öô\12M\85o¹cª÷-¼\8e)\19¬}N¡\11J=û]d\7fP\ 2\9fOøAÛT\12¤ð+¿Ñ¼Ì*¨\98\98T\vJ\1a\97\84\0gé½;\f\b\ f\81.\9fÀ\ 1F{\b^ïMÏ\92ß\87\83e\fí³1iÛ-¶,¢\ 3Ñ\1fC\91@\13\e?\1aÿn\v¦æiÑY8Ø\19GjØ\11\9b@ZTX\\v/È^W\ 4î\9a\82u\8c=\v×\aŦ'G!K\8eÈÄ.jøn{\14
+c\95\14\7f¢\ 3Òh#\14\97ÙÓ¡Á\14¤Å\ f\8aR\96DèXsdÐ_-;}!:~·ö¯\98±\b\ 1e\88²7b\ 2\84¾\98K³îû=»ï.\84Ì\9d\8eë&#{¨Ñ\ÑAYõ§ñ/·\1c¥:g\99\9fÊû\93\7fa\ 5³\8f\ 4wD\1c\14./G\ 6\9e\9fª\16ýfQ\91ñûJ\r \97Lºû}O\92I,¨Ï4çÊ`Sf\86\ f\b6\92&\83Gg\9f\16ó¦vn\81ùÅeúô\90ú[\9eé_Á¨-\1d\rÄÃû\17:X¯\15p`ë)°6
+Ý@Ôì^eÍÎôw \87þ\84\8f'\r\9d8\ 5\foï_\8f|Óß\96
+n*ð{a²R\14\94ØÊ\8a¸øý¿8øÉ\8e='\ 4\89*}\f"\81qÞYQ³\92¦à\13\88¼r~ô ÷0ÍÑÉ3SÇ\92ÙÃ7
+\1e\18Á2\82\ 4í}ÇN\94ª*kì0Ýï±È 5?0I\9a\ 5
+\89É+Ç*<Ú½,\8a\13\1cT¯l,¿Æí¡vs:â\93ØÃ#zÑ¡ªU'\8c¸´{¤Ö\92\13\1e¦\85\ 1\8c\12×cä\9bêýÀÂ#&\81\80ñ÷RÍ\ 2BÔ¸ì2â«\v\1d\9c©TèÎ(ãRfK\b\ e\99}q?ÎS}\11S-(r\aÁ
\a¿£¼JM\8a?Uie>\87-:ãìéb¶\b7\ 6ÍË\8aCÏ}'\8d\aÁ¶Ì&¹VÝ\16    \81°¼í`\ 4\ 4Æ8ÞB\8bÁ\1dC\ 4}\1dT\92>\13_\ f\ 2\82\ e\9apÇl\1f\99F\90g\0\13ä\11\96zªÎ\9f\8fä\18\80[·0bZ²\1aõú\18­\89äp\98\9e\98¼E.%«×\953]%9\86êé\rI¨8\8d¾5ô).äLuÍü¦³\9c\rX\f­ù7\8fÑÆº©Ì½_ÊÆ&KoydßAµAUJðq\9b©w ¯\ fÁÌ«RN\99ÀÁð­A\88s\82°ºÃ`\8fc\89ü fõp\97y:ÿì\8f\1em
\ 4æ-JDNì$Û7\ 4@\e$\19\ 2\19ÅÃýÂ\89ÂwÁ\92TD­»5Ð\97L?Ú\ e\14\b  v\17Ìé\98\97¶¿ÝFNe+\96Ú¦À\ 59ïàV4\vss«ý¹½\0\9b¦Ïn¢\84;
\93¹-q9yZ¶lÏc\95ì\96\84\94/\ 3\94-ÌfU\85\9a\84§U;\ayp1±m\90j%hÝM\8d\86ðÑ\9dO(\8b\12\86\12\vt\0ØzWýëÿw-.\14âÚy\8b»-qR\93V {\80\ 3$säÌ\92GÄ÷BO.Q\96u<7\7f 1Y\ fü©dUv|×me\9bG\ 5ðÝ?Ü­OÓì-ôbÔV\9f(0Ñ׸i¤Ìe®xÛO\9b̪ÃìG\ 3\17\99±~\g©:\894sÚÍ«\aeV üw\8aGY5!âV\97\ e´\avaS\86~Eô\r\0\99½@\12µÆØ|%\ryÃ÷zPÁêºóéþD6O-eä~\91Ù\ 6¸\e\89^þ­zª¥4õq\e\9el\r\ e!eg\80årß8ÙÉ+\f\1au²®³}k\ 1\9d\17S\15=Ac]·+²5GÏ\86Ô¢RÃÿ2hø\11f\16é\f\Àñ·â¥tpòÕ£èå5Tϵ\18z*û\ 67tÃ\rxÕs0\83»\83LQ]É5\ 4\0\91\15ù\12\ 5I\11îÕm\ 5}\8fáê\8a\ 1Ëv\99ë)\8c³GUðÙ_Q®¥Àd&Ai
+ Õ/ùóF\89¢ª\12.ËL\86:\84¶Æ\12h\8fE·¸ÑZ¸\16\103«Vú\95\ f§ÁX¦#\95\81Ó1[¬h,\83Nh\7fITØÕŸÕ\ 2ç©\84\rÙë7ÑüÜÝNGѶ©Ïvh\rfnâªgæþ`½\92¼þ\95éþBo\80g\11Æn«      b\ 4ª÷*\94\85\97²Ó`XÖv\17\87«J\14\8d¤«þ\1a6FÁG\ 4É9Þ¿\e×H\1a¡Ì\8d8ÀD\95!Õí²úVÂ\90ÍÀ\95RLÌ^VXÍc\fã~\12Û¢\84\93\18\10l\8c4ëp+}nÙÍæ¥§TZå\bå\87¯Äl\v_4\894>¼\96ò<­\98ô\99P\90O©ÃN¹i \98\ 2c¤)É\1fhª0\88>cô|Ú¬z͵\87ºônU©$q\1eH\10cT {ß,É\ 21ôÉ-%ä8\88¡ðbÁ\9440kç"\ 6\83Y\94\17\85Ʋ·»\9aõú'j5½\8eê{\ 6q"r\84Çt0\88}\97õS    \9a X»\83µ\8a\16L\a­\86£N\vO5\93Æ\84\12\9f½/å\13l|­lOX\92Aðt¥¦ã\14>°¶$\9d´õíy\ 1 \10HgÐ\99(ãqç]://y]Y§Ð\18Y\17[\89­¢\8d\8fÕóA\9d\aÿ¢^¬v
\97â­ÃÄé\86d\95øGAæÅ¥ÔË\88¹¬5\89\9e*çÝN4\97´äKÈÜ\ 20{\99\rÆ\13ºªt¬Zâç¬Àa\ 6ºÕ®ÚÝH   XI²¢\92\89×Þ¤Q\e\vû   \85&¬ÔSQQãA\97!½¿Èol¦xðJ9\92\8d\19\1fij&\ 4CT's/a\1a\aj\93Ñ\9eR\9bh\9e\bI-¦3+
+ëà\8dqONQøÿÈrÓí\búå»ÄÀ9\9f\88­éÄî\ 4ô\93S¦ÅÍ\18x;\84x3\19±\99`Þ9W%O,AéÛ)º¹\18\98ml\16)\12ô/\9dßñ\ 2\b\10¢ñ\13µµÎ¯\18Èî\1a!\\ví´­\0V\9eï§\ 5hÕÄtW\1d·\8e:ÂR÷ñI_\rºÔ&\93º\8c\ e¹F« Ù\10F\814t8ÑÚ ªÈ\1f¬àù\9d¬\ 6Fóç@\11fÂ%\85\1f\84V4\10"\fÚdöÆ\9d0b/ª V-§r9¤ ¸[·¹6:®\97\84\88<D\16«ïÌÙ¡¾¦v\8d\11÷k\f]8\ 4ù\90îêǤRæV2O\97åLÉÍ®f\1dê\1cÎCÌlß«\95æ\81\11(ª|ª¢¥hðE«\11T·Ø_êÆ\ 4H\91d\99!\ 3\118÷ÏEÔ\8f^GÜ&öäs:Tu\ 4Ö*ë=t\1dÛ}fÉ\9e\ 4é\86j¾«E!\14rx\ 4ì7e)\988?4´/¿êª¬\80H²Ê\82IÊ÷\82\bb\ 1\80.ù¨å èKØÏº\86\13\89I\80\18\98\91\v\847\ 6Ó\80ðÌ,\ 4d¨JF\rà¯\ 6}+\1aò\90gT*bË*\97qóµ~Oë_\92¡\93\9e§\9f©­þ\15\16&æRnÂ\9a4\84ÅåüÒ¡.G\89l©Ve&\14\90ê°N\0L¬G\7f(\12­úÇfñËyÛ¥p¤Ò³I\ 2vþk\füO²AòÕ\95b\92
+\92u\8bt?1Ë"þ\9fmÊ;`¡ß/\8b°(\ fUâР      ö\v- êÕ\99\7fN\bÏô\12#\95®@âËÀ\a¼\84\ 3 F\98¬oBzåÖ«þ74¸"¶\12ô\88sy¯u\f\9d\r0\8ew\13ø\9d\84*~-ÊÒc\e\87\rN§VZ;EkÀT\8fnIÊ\16ÖÄß\85ÀA%8\17C´H?w\18 +\ 1\ e\16EHÔ\88\9c(\1c\80x\85jëòN]c½øz\ 2 ~Y©\eº%8Â\8a\9bÚéÃ}¨\95\ 2\0Khü\15\ 6í\9bOz\94øiÓ\1a<OgôÎ\8e\8bðû\86Ïg¹\82kã_@\ 4\9dh¥é=\1f\9dù\1f\8aäÖ¹A\15$P2è¹÷@ÓÅ\92*\aÊ¡\8b8\93ÑN IVå\8fS\vâ¶2!^R%KÑÙ   ÆÐæ\82oÖØKدÎ\93\92)Æ~\90\0%\r\0ƹ\82?úbZ\82g\08!¼\18©ëKô®vìbsjd®Ùdé\\aj¡}»ÉNa\1f,\ f\9e·A\9d®O¸-\84jçñ3 \8f\80q\bÜ4-\96f¥×z%gÁAJ¥|>ÙUQw\91Hª°\ 5ToUè~\ 3?fdÝÿÒ7\94\1d@rýÐo©ðË\9a\92ù|v\98´Ñ\82\15O+òÝLÊ·¥oÎ.\90nx*ä\97\19,3FÁ \ 4Ñ\b³ã\12\17à=\eæ£Lé V<µCJSÚC'ÈD3\9c^\94\rþ\9cl2e82Ñ\95n\8c\82\0±EV\10ÈÔ5Þ\b\11U\93{¾è,αvÂl\9f\9bòG'ùÀ´¨$\9d\³ø-_\99q/e'è\82slf?¡\96$\ 5\92\83\ 11ê%5cÕ\84\93Ù\1d\9d®-|ßÉÿ\85ÏêÙ§Þ«ZàÓGhÆqKî\1dN;ù²\87vE       ç\84\8d×    »Þ\1aìÞ\8b,FÖ:E\98D¤:NÜ$A\16\98JDÛRcõ\87ìôf\ e¨oï\16sz°ÀTüÛÌþC]@\8b~÷t
+Ï[Æ\85\1aXU;$²\92Ö\97\91\1a\8e\aM.­ÞåÓ.Ã\98\94­D\a\0è\ 6\17²ÿÑ#\83\b¸»\1eVJq[Sò±w\8fR6\9a4ãLx\ 3\15ûËÄþ\8bo·íô.Þ\17ëcWn\r¢¿ù¾ñPÊ\11\01\11D4\91\9aó0<Øa£59Ê¥`×M:!Q\8e5Ê{\1cÞ&\17ð ã.NÑLùóÅ÷§¤*¥FôÓ&c\b\fë]2\9b·®²G\ e³qÿ­Èe¤?^"bK¡[\ 4\9f\89Boîé\15Øh\92Ù\ 3IÝ0O\8aè8\0f3ÇÑ1oë\1cb5Ä\141°Íÿ4¾b\7fCKkcx*\a6-ü\r½\7fS·HDÒÍ\81È\97p=åv"ãò\b\ 2\f@ï}þ\e\0Ñ\a!Û»\19©ÝÛÀaµn\9c.\6Q{Í\92Ç\8aV\84)\85 \89ÑÚB\a{\92L£¦±gãÞ7©Y\85\rxJ_\ 4Øð\8bô
+\9b
+¤BÑÄ\ 2t\8e\ 4aÖWï\fݲY\ f\94tHñ)\93;\87\11t­úïy~\eO\9aÃý\0gU &ÙÂ%\ 6µì\831´F\ 5!\983ë      \86\85tô^DÎuJRí2\9c¯Î»5íÍN\9dW\89°ëÝ0õÜ\87Åá\r\92Lqe_?ó¡\10·c&?Ç\82Ç\87ÿY\8fЩü»³\93Y\8b\0v\ 5^3ò\85g)#:§4q\8d\1a5È+8\10¡fxA÷\936A\11Hüÿ1¬nH´
+#ù\92-³×¹\16Õ^b16KÐ\87\91G^Ý\80 \9a{\a\11ÓNÕ$5rß    Ìb;\16\ 59\vrÀbF\1c&\12\ 1¡k´      ­+ðz\7f\8b*|ùf\17<Îßoþq\9eé¦\rX\83ï\8aÆ©­\8f¢*#I\±Ê[\ 5õ\e5?\0î\19HBä\8c"I?\14S3ºÚ¿T\80\8e«\90\83\a\1d©â\87TóÎÚnÚ8T7Æ\ví"s\ f½\16¥RùùïÈ\8e\80à\bp\8b&ZêñU\94´v\94¥\v\86\80eXÈ\1d[½"\92T¢Áÿê\83ÑF\1aô\9f\80!E\95ì\ 5ì§ÅÔ\rÃzi\12ý2ûÓ\ e\82\azU£øSG\85\97»æ#È\7f\ 3¬`\e\0;í;5\99\95C-\0Ý\84¶Ô\ fÖÅJ9¤J8       á\8f÷?§\96\96vÒ+ØDµh\92q\9c6\ 1ZÙ}?$Óq'\16ç%\11Q\12\8cH{\92®NXÌCX\16ðáe\8d{&lb\9bø+{\93N\94=\96ú9]µ£\88§À½b"Vøê\8dWoy±ò\9dn8;\13æ§?\1f&y¬x¤þ$ý½\1e-\902Á>\97\80÷\7f´R­\16íÒcJ;ë§\f-\ 2$T\89ìþR=;\9b¥4<­¤M\9a\1fhME,¹\ 52Û2æ\8fE¥þá1|Ê\11lr\17\91&7\7fU\ ez\ f\ 2\13\8a¿\9fÄ!Ê\15\f\80I\97Òcñ!Z³ïÖd\85\13Ç'£$×\9d9\vôÿõD;³>=ò\95É\10ÿ\\9ab¸Ær\9f\91«0©|\95 ¤\ e\e\94x\ e\v¾Q\b\18\82A3üÑe\83\94ä\1f\82Ìóå\8c¨\99#Üf]\a#\8eÁ;0oåÚ@\9f\1d\95\ñ¼\8c¡\12\ 2\1d^\f\1d\rJcú\8fL`h\9a\90¥\82rà¦\rK¼wÄZa\9d\a´\92$µa\1a\8a\0)±nâª=pH¦Ø"Q\15ëûv(Ò0õ\v\92}\9cºSù\93Z<ÿ\e>
+òå#ì7¾ (\88(^ض÷\8bôªzV®\9aøu\12¼\10\86OB+Ù\12áf¾ÅðA\93Ò\88ü~Aõ\ eÈ<Çìxõ\93kýÜ¡ç1Ô_L\84þªPÊ¥u  \8cç\95&¿7'\134Ê÷9\1eE\17\94\ 5t©<©ñÏj/j?ø£þu\ e:^4   ?ØÅ\10sô¼³{;ØDÉý\ 2\ eSU{A\19NÑììÏ×ÙÍ\18Ð~\r\ 1uTõ[\1dÖqeñiR\a.¾¶\1cmt§\89Úé¡\84À9j½ç\ f\10î\89^\92\b¡S\10zWJ\89g#&\9b]ëO\ 2®=ªí¹Ô¦õüÆ\88·\8bX\b-^í\11N5¨]\81_îw¬÷ölè|\11Î÷âö\16`*Ë1ãÉ1AMæ\81ª\16\86æW>¨ÔÞmt\ 1mmzoTã55­\ 5\9d\14õC\9e\16\16\15M\82¨Ó¯Uî\97¬\92\8f²bc\92:\8cÛ\95/¥'¦*Cå\16\18>\12\17[a]öi\94zÕ¦}=\9f9äÛ<#®\rpçífÏ´(«výYî=R¦\1cÌÜX\8f\99\1a÷\9e@ë<\a\84\ 1ÐÄq=ö_
+º¡Àü7¿zä5wo\12\0µÉ\rÖ],\ 4\17Æà2× åÕî\81\9c\81\ 5ÿ\8eÑÁaiSuK\10{Ü,\ 2ppr?%\84\8a²`¾^§uØ\93r{ë\v\88¡%\18\88(Í lAçZ?õ\91e/\12z1$w\90\b1Ä4\9c Û\1eÎ\83_?TưÛoàg'    ¹m\8dÜ\94\99\88g0\89÷¢\19\91j\8dRE\16½èÐ\ 2«Ùß\85ý;K¶@ô\89pzjQ¸i\8e©o½\12áeYWã¤>Ä:ÛM\Ó\83\98+\14\13\0+x\b_ã´ ç \85Ë\91\9fXJ¼Ø³ÒÕCC¸\8f    !HÉLïö\83ôÑ\83Mpg\18êÂ_MM5\7f4\b\8aPëÖiÇMã3\rãL0\v@ÊÂΣ\95¶Ñ\92Ó\ 4Ö0¿hìO©\1f]¹µ\r\90\81åra       `¶·M\r\12ÿÏ\ 4\17s=U'À\13Á~\8e\92\83ûïXÑMÕÓá<KnÖ±R?¿¤3Ò\ f÷7°\ 25q\1fh\9d\84ý\ 1þ\e¥Ë'@1\0\15ÕE\95£\92ª\90\ fûéA\16èé\9b\82ÀIQ&\9cXz\17\96\15\82±­¬\19Z)\87\95¼«\94Å*\1fRE\8dS©ú   Òö\97¿/\90\8fúRS\1ex¾Ðü\87/¿DF\17e\8aì\ 6a}\ 5\11Þ\ 6¢V\11qsÎà HÇ\14§:íÁð\87»\ 2\82Ñ@[|\82
+\fÊüÿ%ñH¹Ñvù¶¤î\84pe\0\v¼ð\ 3î­º)\1d\ e\85J`\8d\82ëN\90ê*H
+x[6ò$U"aê)ñom\ 1\89\14ðòH\9cÅ\9d±¼\1afy¤LKþ\ eS¾u¹M1\82ðÅ\91ry¯\r \9a\ 4\,õ/ùtUí>\16»H\92\84srX¡        ôa\18̯\97\1c.\9d\1e5ñªêµè\94cXë¿\9bC        FöÜ\95ý\8aþ×ÌÇú¨yrÎc¯\15æ¨À\ 2\ 4³\91w\1cÕJ\v´ï@
+\ 6ÑØ9RóvúÏèy\9f\ 3U<\90*\1c\7f\80ëÆÿ6àä\91Ãl"`\97¿SéµÛ\92|\t    ]N¸\9f¨cy­6i¢\ 5\90VÉë\8e¶Ã×Ëżª7\ f\86_zy1×\17ª¯ãH\15ÇUÃ\89®\92\ e¿\85é-¡÷áÐíø\86È\e\86r_(\94Ô>p\17òá1Ü\0Ê\ðá\ 2]ðçöB·Ò\9e+\98ç¾;\93È×\99\85v\96D\9d5%¨Î^W7gy\8c³Î\9b\83b;ÕÜð\12\9dø§²¨=í·b6é\aON~\8a\\8ajùÏÚ­LÅ\eýPß\16{4ùë1!,¶9EEþ\17½®
+\8e¢ÿ}\14\91\95«âw=þpñìˤo\ eZ\85ÐÜÄ}7\94ÓÇ\85Ã>\16Èr^ºE-Éc{ñú\ fÞ3ï\80\90»T#Z¼\88.GüD\92ê$}-\ 5-¦L4Á X`\18­\fð\b\16,\96¾µ?å%\ 4\97\9b?\88\ 1<G;pZ\93\91\9c~\87\8cNæ6\9aë\ 6K>#\9a\9e\7fO±XßöñHÁ¼Ø¢¯B\87v#\95h{F\90´\11þ\ 5¬Õ6\92ó\ 1sÏÂ\7f\81\87­Ü\8d\94Xª¦4u5Ä\82í9v¸rÊ¥\v\8aõ!G\81b*\974¤Q\8bqÐê\8bÙR\92Ã\1eP).tþ\1fLÛ·CÂ\19®uÊ[á_\98ßO3H­?²ÝǦéøÚ7\ 2ª\19\13¼¨Ë\16\19\94¥aÄ\87\ f\11p\86©²?­\92#\95ß©j\8fªý«üæ¡î2iÈo\90\ 5ÑØr!2æ\12e\8dv\f\93\99à\92>+¼\19G\8d°¿\87+\84\15\ 5¥Eðêø¨ÑÄíjã\ 5ÍÔÿ¦\b¢^Ú-¸¤÷ý>Y²\ 3Ëh\ 3    bÊye\rþíϯڬô\98øL§\84\ 5@4ü51¡­[Iµ÷\95Ï\b2\0FTI ÏÆ7¿Tr#éöJO\94*7GÚ\8fð;'k¼\88\a\9aè\8fÆÕÈKR±þ\15]1*°þÿG<Å/2\8a\1f\7f±?\9føßûzF\1e^\9fF\15\7f6?@ô#\11\13\15\1ey¯?©¦`¼»\8f\1cM)W¡YW\ 6\96ª\94¯ÂÒª:¬Ô7ÚW©Z½M FnßÍðÒoø\ºÇ¾;        ëãÿCêö·o\7fÁ"\85\8fõRsÚèw\ fC\ 4ä\92¿ªì`é-S[ÏI\eè\86\92TÅ­°\9eÙJ\95ÐýÁÔ\1a\93g\97Ùåß\9bò.»¶,\ 4\7fG\96\18\9e\90ë\86\84ÜÑ"¡\9632£ø\9a>\18ùõ«áJ\8c¡¹C\ eÿ\a   3d¾\82ìKø¼\7fΩn¾6z\95£L\98\92+Ñ£ñgY\ 6\9d\9aR\98\ fL¤ìL´çu\rí«0íK3ý\ 3\9c\95\7fÚ\83s\8e\86S¦ï\98¶ë®\900R\98t\86×Ã\1a£k"ÙÛïq?    \98ÿ§°zM=\b©\ 6¯ï\80ûn\9aG\ 2þ!ïíüË83\99\97ãå×$YH      ¿tH\ 4ü\17?(&\10\ 2U\ fÑÊ\0Òñ2Ìá\10k~qÖ\83JwIÿþwT¬(\92x¤ãß\90\13\15á)í\ eh\85\17Íÿ÷\1e\8eÇøÆÆ9\b\10\¹òÁqÌ\8ag\rt\82\12Ö\7fò\8d*å@\b\9e
+þjMºW­·ß·\86\ 2òþþ.[²\7f\1eÇ\9aü\12ê?¿ÓÛõÏFÿçÈ;ÿ\ 1tf\ 2J8é,-ð¨f$\ 6\11ªFk¡;¥\84\11¿´]Ï\ 1â\8eq\ efF*Æ( ¤¤$é.¡, Gô0,z&ò\12°\ 2T\ 2g\ 2\87\85v9µ\1f<ZÅÁZÐ)=Í£­5[Ý_¨ô"«ñÇvY\89\86Ä\82\9aÔ|Kc÷$\97\95bÚlKI\9d\8b\8b\93\95\88z\858YTDµ\98Du{\93DNÅ!òÖ(*)N\96ÚM¥aæ\7fÏI¬DC\82Á¾Ñ»ÚÍÝѤRC\1cŲ\8d\9a^¢!±`\9f\86<«¨cÛ\86\g³ãi®eN-Õb¢3qG+¿°\97fÇÞ³ÙÚYM4$\16ènÐk{°\ 6\11÷öi\98FC÷¤Á##\1e7¿©´\93\96zfû3ó\7fèuÎêªg\rqVí{\96í\1a¼   ñvm\9bnÐG\1fæ^q÷\97\8bÎï\a1O\98\9b\83k;v;$»ßÝÒiåÍ~*\1dkÐîæ\8aG¶]Êß®×r\8fhÍZs¹[§·\95¥E7òU]%â«æ*¿ÀD5ª<£9*\9a³´Å?ó÷¤Y&Þlna¥)Õ~\aKG¿8XùÅ`Ý\99Ùà=/\8brÎFf\9fûiÓîª\8cvi¬Òyec\91<Þv^ù»¨Ðð¬ò\8b2û!NåN\9dZÎêÒ\18oSU·\86ª²Rͧ7TI6ÂSJ\9bÉZõ4»«ëºKt\17ÝX\9f_\94\7f×ø÷Úã¼íTÙ/·ÆÅ°`´ÿi(Ò\90X0º\9c¶¥\1eÕñ\85ßóÝåM\95j%!î®ýèTÜÌTó\99\9a®£¾ÓT\99g\r\buÖÈò\8b\86\ 4\ 3C\91Ò(e¥îP-æ$å\17\89\86ÄÂÕo[PÜZÎ*Òò\8a½y\88NzÅ\19\14(ªHñ\1a2óâ\90\ 25«PohtOÝ£Yª)ɺ8\94\12?\19(¿HD\147HúE"\ 2
+\14d\11\89\b\14((±@A    \ 4Ð\ 1"\82¢\80äñ`Q@BY*\15\ 6\ 4\ eÉ\ 2\v4&\f\vÄ\ 1£òp,`\15ÉÒ ,\104\88<\18ÐE\98DPqxðX\fð0\99@2¡\10 \81äÑ Ò°0LX\ eÐ\0by<L\16\1aD\1e\f  \93\85\ 6\11\96\a\84\85\ 2AEÂ@\ 2\8as¨8\1e\vh°@\1e\130\96\b\12PØê\1f¥´Úcfå¤\19¡Úús¬}:ós\ 1CäS¢)O\95\8fÊ\9f²Óå#\9d²mÓø&\9cÛFûÓ\83&2Ú7>\9b®sváøÈÆçSÒ1ÓÙjиåoîÓ£v¦ª\vo\9cÛêêUE¼- \8eú£V³uÖùü¢¦þ¯ÞN=¿¸\95uÇGÚ\1d[Ú)ÿ\9bÿöç\17þ¿\9e"µ§\1aÇnw·\e3\1d;\91ÑÜ\1dUqîé±Ê[\e+4«Á¯ÿÁ§\9d~\f\r¿\93¿é/¿c\87k\1eÅ<}~\91\83¿©,\9bükô¨ó{ö\87®\1fíì\19\8cÌ\8alhoÈÊ6e¿)Ž2\e¡\95úc¶3Ûìæfs»\1c3ßÜæfù\85\99}\fOhE\93h¸ó\9bÆpN8i´iOË/NÕÊ.E_Ëò\v\9a5¯)\96\8c\8ct_óë³s§i\9e_X­Çw·«Ç\10Çw_¢<\9dãÚ·{\8bóÕBÏ­l|ßiìó+×\87¨S\1f\1fy¼zÿÝÎniÇÎÆìÔw?{\fÓ\ 6Ï\90|9y\999V:8\96\93G\9eÓKº:\9b»\1dÛÓ»\1fÍíç\8cpì:v\9eÂ,KLý \92\1ak
+ÍsfS\98\99Ò\11\9a\95±\8cV\13á\1a))ÚÝ\98\99Íáî\1cz\ eí\87÷9ÏQÎáa\12~0¿\18L\9c\9bK?\11\11\80\86\ 4Ãâçºh·1Ë\1d³µ¬zå\9e\91\9aÑ\98͵Ô\9fëë\9f\15ý5çë¾òó¿\91ýz[×í\93È47mÕÜä¹Oïª;3Çõë\13o|½±Ó>N-T\ 3k\8c´²ÆH+ÓH\8d»\8b4§G\89\8b\98«öÚhÖë­ûÎ\117o\88Ô\90\8ej5\9dié6j\8fújól*M×îc\88ç1ÒßoÌg{úÔ©9Þ³åø\96Ôª³Ç\9d=y¯¯ûQïvìh:Fz4\BÄ¢9d¸µEû\90+\87\8c\9b¸\89\18¶M7¾ò}sn:ë\9c\u\e\19ó¦Ê\11OÙdZ·Cúh%¾¬E~º¶lkëã\9fy\191­Õ\18éø\12\10\80DCbAO÷Gæ\1có\96ÉÿmÎ÷¡úÕNùòtê\8e\85õOY1P\14Ú\93\89jå9E3ÅT\Ä\9cc¸»KÕ¼ú\åÙT&ú~c\89zOËL#K³LWÍåÙ©\8ee~q\97ù˳Õ\9cK\1d»´´\9cËËËÌÃ<˳ÌÌLô¤iÖ(ý4­ôò6/ÓH5m\93\94ÖWJ\9a\88s¥ÊPjUªFcM«¹ôôj%B\1d«âÎõ\9a;¿æÕ\94~cµc?3\9dÓÓ£âÜ\93z;GÍÓ\1cÔÕKÿtRóZ£z\1cÕ¯\9a\19ѦÑZí\10êËì¾·XUs·{\87»_8ĵҵmîmÕ»¿Ùe¸\12ÙF:v_\94\13Iï¥VvÄ¡\93ÖÉ\vZ×äæîÐ7¿ØõmQîÜÒm~ak:äµ3ZÓñ\9e\8d7¿X[»ü\82hi\9c6þ=´ùEi\95\96&\1a§ù\ 5j~a§f:§ZGsº:¦tZô,S³\ f\rYÎ5çê\85+*C\95\925¿¨-ÍÌËÌ\1d¬\1dÌDÓ\1cÌ"Ìü"ç2¿¨|êÐ3¿¨¬üP΢!M­\17kÑvQQ1\87²Ôu¸&«94*C;ÔÛG¨F\7f\b\vÑFC$Â\9d=ôî\11î¹ô\16önô2)s2ï\ e\13\vïêSwu\97_hÍ-îU\9a÷hªNu\85Xj4Þ³V\rÕ½.¿¨û­ákͶÔlKugiVcVw\85§Î¹\1dNÝÖL     m?e+[\8d­\8cÊÌSd6%ËE\1d²ííhO\9e%×M{ýRîËöz[\16ÖTÒ  O:\97_¨û\91tïô\ fý¾\87~§¡Ú¡ôe\19ñ;ç³C\9bÌf¾ÕZ×éÌ4uÇhs'¦j\1dU\8eo4vVjÖØ´\ 1å***\88\17Ê]1x0¨\88È£\ 1\f-\b\v\930(\13\ 5ÇBQ¨,\90É\ 3\82ÂäÁÃ!y(,\11\10\a
+\92e¡<P\98°@D44<\1cV +È
+.WÃåp¸ L"\vå\ 1y\80`@\91<\1a\16\87ä¡(E)(\14\88¬ ²Â\95\82¬P\8ar\ 4§ \9bÀ\80@\0Y$< ¨\ 3\9c\r\1a R\16Ç\84²\0\r\184``A¹Ñh4ªÑh4\1aÚ¨FC\17\8dÈCÂ\88\88D4\1a\fÉD\12\99P\1a\1c\a\81<\1a\1a\1ah@\18\rÆ"y@*\ f\87c¡<X\1c\90\ 3\93ʤBaT \16Ê\ 4F\ 4\bÃ\1a(\ 59xP*\8c\88\1f\18\rT$Kdi0 \ f\10\ f\ 6\ e $H\88 ±@\16    \8bDÂpX\98°\80\1c,\1cøÁ\ 42qx LÊ\11\ f\a\832\f\ 6äá¨\92 aa\rX\16\8a\ 2\90Éd¡\1c\13\a\96\a\94¥\ 1W\1e\103ú+¨
+*¨,\11\87Ê\83Âp\ 5\15¸ü@ò\90 \18*\92Ç\ 4\ 2        \12\84Z4 \ 1DäÅ\ 1\8ex ±<@L¨8D\18\0e\ 2\994\16\ f$\8f\8aÃ\83\adi0\1a(\11Ǩ@H&\v\92E2y\90°\0\85\ 1\89h,\ f\10T\ fLp8*\vDCÃÄá°\ 2\12Æ\ 3\ 2"\82Q\80¢D@\1c< ,\f\1c\15\b \12¼°\b\13\aÃA\ 2È\ 2aQ \ 1\ 3\v\fâ1q\0¨4\18\94\ 6SP4,\f\12\80\98<D&\v\96<$\8fÆÃá\81A)ÀÒ DL$X\1e&\92Ç\ 4KCA`T\1a\ fD"$®@" \16\8a\ 3JD\ 5b\81<&\80phT\1e\15    &\92\86d\89`ÀB!À\ 2Ápp4´4$\v\ 1\12!\82\ 4    \13&8\0\80¨\1c\ fÇ£\1a0\1c\1e \v\1a0°@\0
+\85\b\10\ 1a@X\1c\93ÆC\92\80\ 4\14\bL\ eH\b@\81Àò0¡²8&\12'\ 4 @@²P\1c\90\87C\83A¡,H`©D8xP*\15ÆbqL$\90\88\84\ 3\14
+\8b\0C\ 3\14$\90\18\80"\81\ 4e©@\1e\ 4(\80\ 2\82D\1e¹×J\98\94\f\11ÅB\8e\98\84ªWté\8e\9a§ãÍ:kb¥ë¸c\8aªS@¶po4Q}µg\9aÕ¤Ú·Èx\8be\8c  !I, á\0\81¨Ó§*ª­Ô\8c\18&CíÚ\19b\92\8f\f\91\1f"\ 36b\8a\ 6?¢K\84«È3Ò\9b®ö¤K5t»Ó«Âèl«\9e\91%Ñ2ø¤\96\8a\1e3(@J\eR\f)(]K\97\89¤§IT5ô;ûÝmÞF³#zL§»»Û\80ÂpH¡¸\11-MDKG\96äS¼è\1f²Ê[\r%\1eðêaV\96\92Ýhb\96\rO6u\98\ f\v\14Ua@ pH\16H$ Ñ\90X@õ âÝ\92a\12Î}\b\ fÓ4T»Þò é\9e·ô¾·\88Õø<Úí÷\86y½}3 þ\88fãôeÚN§mÅ1ÖØþÙvÙ\98l\a\9a>¨´\95i\92Z*\b\0\0\0     ã\11\b\80@( \vGdRÁDÜ~\14\80\ 4brFÂP$\8e\84¡`P¨\f\81`\84\ 1\14P\0\18\ 2\80\ 1Ä@d\ 6Dn\a\15Ü\83\9e]OÿîO
+Ãy\906\83¦D\10I¼¶\89\ 5\ 1ô«-RùE®¬ðÁ\91£4í_^\e©DO¢    =ZÄ\86&\875@kn¦dÜ:¬\1eêé%6J¯?°ZÈÕueìÌ"F\rÿ©ë\18\8bé\11±®û\14tF_\8f\19ý\ få÷jU\81x\8eÛW\f=PÆ\9fº\9b.ofvÀm^qit\84úM$<»\18\ 6ÅH³øß\fÛÆ]j\Äl\Ä2·\9fÐ\8b¬SØ{~ýé« \18\15'Q¢*t¤_Ç´;ú¡¾\81\96V\81\8e=\1cløÍ(z\88}9\±N\ fDG¿ý-SrÏ\ 1`n .m&>ÜAj\93~,\89Á\92ÊÅ\ f¢\10\95(\997B\87qQÝ õK\9b      ®55\85\ 2V,\11Íü$r.w\1a¨án\89q®» 7 \1eNdð"\10Ô\e­?L8\9e\81kû;ÖIº\19U:\b\1d³ \ 1VÏã\1f\97ëî90+\94×\95\93ë\17Üi\1dÇà}\8aH\87ïί$)5̨\9c\1cWÝÐ\88òvxÊfiQ,DTY\90»\ eºçÔ0XlÕ@\ e½Ù¹\80æ\84ó7'TN\86î´¦ûrÉ\8flp:z¦³8vòVXÔÌOc¿Ñ¤]­·´$\90\12\ f3;`Ì\0\12\e¼
+H\1a     ã-'þéÎlr\v2cvK\8f³\aS\9d©;õ¨ \96\9eÈ3\90t\f\9c¾oºì®\f&ÝÞ;\97\eXüDb§A²\86õ^²B\87\ f\9b\ðaô\ 3Cäë\13Ûë\83\9e\9b\119\7\7fh\13\rµ\89.·8è\10Ý£\14\12]\19ÿ\1f\9d
+Æ9Q­C¦\1d\ f       Þ/+^ ¥¢\f\ 4\Ö÷j)x3ÄÑì±\7fó~\9bJ§»ÙJ\eи&\93¥\9aþX\1d4J¿\18bà]Bµ¹COºIR\1føBÄøû\1eø\1aÿAA§a\89\ f\f'«ge\ 2w\ 2ÁÇæ~RE\a\9dBo\ eø¨@\8c¸±"\16¯\ 3\ 6>\94å£\92APê\97u\8a\12\83\ 2A©g}Ap\b\19¡ ÏÇf\8b[)ßL¹d\80u\ e|wªÃ
+\r>\98\8bôÈYõÇ\9a%\8bÅ\9aK²sB0\e¥\7f2\ fC/¼bf\bó°{+¶ëjAý\f»\ 4\9e\1d=ÞUD¿ÝxX«C\1c?\1f\95\90\88ö»¹\81SÖ²\bûf¬R?b\14\9dêKl#\18\aR\95åâÎf\17bB\8anÃ^ò_ÖK\ 3K\17á\88Ô\14©|\91+V¸<\90óù%¯þ\16ä¥\98ü <Vk¿'\ 2=#¸­yYÕ­\14Òëºi'7<1ÿ£;-\8b³\11Mél\88\10\8bÑ禠      ý\13\91\1e\80¼\9d¿ü=)\1eçÊ\14|³X£Y0J\e,s\bô\87\ 2·8¶\92#\8b²$y\8c\9bæè\ 6\8cã\1f#\84\ f\8eüfpâAZA\16\9dDõMt±x w%«4¯4\0|C\v\aîW\ 6¶î¢\85\92\14¹ÿSwá8\91üàD$£\1e®lf6MÞ}9_]¤m;\v\ 2MßøÂñ\r¦\98ûB\ 5wè8yC0\fzv\f\14Â1\14uæÉðWc§\828ö%¤Ü ¿\0Âh<±Ø\95¾xUbe\12Ó]Â;B+«üj¯<ð°fÎ*S\ 3P\869²¦ÿÈÎvA\9e7ÚH\1fÛ\1fØ\9c¡Â¦\v\93\86M«X\ eG\8bëc\1c\8f\95 Ð`\90Ôå4ùÆÁBq\ 3,B\ 1   ´ß\14¦\8c \8a\ 4e\15S\19\18\ 2\v\97õ½Z
+Þlüpö\16ls¢i1Q¢ooY\81¹g\84\9bGÑûIô\94Oïê\95cZ\ f\97ÖC\16-£Ü_Â\8d\11¥\ 6ñ\8c¼ß){Eü¶Ä_\ 3DUääb\96K\99\9aß`¹\970L|\7frséQtGbV¿÷2û\90¤\9f§ôîÒ\19ÓK«Ò1G<ѬlÆ{\87\88w\eð­%D0\13¨xx\13\88P\999<\81çnÞ©\94F
+"Û!è©kô(f4«\15Xú¾w{E\94QuÅY¼¨\83\9a\83bjOb)Ë\8eîiòÉ\0ÄõÌLÐÞ´¦éª.Ã\15¼Ùx\9f\96¬}×¥<Ž\ 4!è^×\bº\87Á¨ó¢GY$é¿9åbè\87çã\ 6Ëê\96I\9a\8a\9b"\8bü:\18¬í¢èd®m¾Y\18\13\13¦S\7fjA~U\12ü\82\99\ f\99ã³w\11ÖÒ9É\1c|æ¦\8fo=\80;¨sä\9eQ|\80³¨\83\87\fÍ ÿ\85È\89\v\1cô&\99\9d\82ÐeÚËz\92\ 2\ 4Ú03-ö½\13 \8b*¿)ê]¢Otcx¿`2=oË\1f \1f£\8c\ 3KA¶+½ôï°D_bÉtú\1fÀ\7f\ 46*W\a\9a¹c\v´\ eu8]HcvB¨\9e¨SSE\8fg|\9d"\97Ý\ 3\1eþF\97LHA\8e©ËA%Mî^;\8f\99Yûü\11+\7f\8e\18v½©¥] {\11Í.2}¿äb¦HDÇ5\18\9d?+T\ e\88ö®Ë!\9eä»V\10ñs\9b
+´xnéäz\9fqY²ûÉú\86&\1dHñÁ\ 4¾\esÉQrC\ 2Öw7HÚi#gæ50\r\80\14\ 6n°f7í,\13´=³LÕx ¸\9dù_þ¥ã]Â+ûѼ\9d<#EYf÷Q\0,Ñ'˦\99\1dÿv3AÊòô\7f\1afd\1e©zp×\84N\e\91ßÌT¬ÚØ#\90³26\1cÆ:¿\ 6»Ê<¹\7f\89x@T6\91§\90·\7f¿\v\1f?§Jçä{\Z\86Ó\9f®\aÅ\ap\9b8%\13Eê¤Ïâ\9e@1;°Ã/WtÂ\19ñ§³\ fî\8eѼɳ\82\94=\86î/\11¨S´ú¡ôq÷\ea¬H\ f¤\8f \86i¶³TH'Wàþ\9cûb\b\88K\19\99\ 5+Z@ù½þÁ\r\1an\13ò\0\101ëUê¿ö\13 \rÇZºÛÜ\96è\ 3ûÕd\11ù\9a\1eVXC\f\8bû¡[Ä£\87Åûº2\85o\ 6í\9e5*UhtGzU\9cóf\94^\8cc\e\e"¨@4ªìSôÐC§\12\82È$LA\1d¼    s\e\ 2Ô¬­ËQ\11¢\0çß¶\87`6&Ò
+½KÇ\96ÞRè\94K¡ *¶áÎ\12ù̾?\11&w\91¿ñ\80\r\9f\90|auì \aBA\ 4¾Ó|þ¼¤×\0Ðeq~ä4\ 5X\9f[ñÏ0±\10dG\89g\94£àì\85\b\r\81ìgÄÊgÐM¦;cÐQ;§Wþú-<ñ2á\99Q3\81\13\eãC/5\0 \8a\8a¢\10¬q£KÑÂ\ e\9bw\89\91%[\ 1|\ 1\ eêáä®øÌ¹)z\8a}0¼2{x\93î­¨KEðǰTß\17¤°Íð\7fV÷ø\90~4Gï7ôco7VP\8cºØô\91\99'\Ù\89A6Æ^~úÉQëæÀ\88\8b9i5\82pH\92µ R\16\8f\9c\97f\b%¡-0¥\rJ ýp\8e\13Ìþ\90¼$\7fÞ\9c\83Ùÿ¡Xás\ 1ñ\8d\bºæ)8\0\9eº\ 5LIÇÞ\98\9ce\ 5p^£\9c\8aQ\8cï¡\88îáÛh\94ߨÙý'Ðâ¥Ä GXèä[ªä\}f\89kù÷DLß_\8dJÔ¨«\81PúÎÖîêeÇu\94àÂK\9ct@\0Øû\8b\\9dÖ¥(\99;FÕÝ®BÑþ\81T\12áMÙ¾µÆ»à3æ¦\9a²\8f\8c¦×&W&j?©4\18,YÑ\8bZeoöÀ wÞ\11\7fFÒýmÍèÂÃÄT\1e!¸b\12üX#}6*_\18{î¡\81QOÃà\99y\86\86\96Æ\99FPµîÌ\85Ýyc -\ 6]4\9fw¢ú |\80Xä¼\7f¯\ 3mäÑ\8cC\9be&;ÐòñSÇÏ\0ÂÜ,¨óNZ³Ý¾\85\ 3TÊ7>Íâ\9b{X\bò\97\1a)"\98ëØ\eð$ýÅ\10OeqX\bÈ-\16?LlùTBÐDNw¥H\rõ\e-zwL¹Î5\vW\1e\99ë]`"k[ÿ\91©N\9e.PW6V\96\84Ù\9c*Â:©\83e\95\92õ²Jxãm\97\ 5Ä\ f\a¢&MN*áªàúKQ2Ì\ e­Yë\8f\90<z&\13A\1a\9e¹t¦ûà3gf4KuP]\1fæ\8eU¨Ýõ\1a¹lÊ\ f\89j2k^wD\ e\88 sìuø\14ê¯VU\8e\9d\99¿+\8d\1dD77LfÉAàß»\89ªDbqSεÂ\8a\rÜ\e6Û®'*zÊ\89\8e\1e»epýÁ.\9d\19æ5õâ¡7\96\89\85¬åT\86:JâåüýÀT\97´\84¾¢N¬0x\9eq\8c\93e\83`m$Õoäd\9bÈ*Æ+Ê^\8a\91vÞÓ@\83ò%\v\93ÔÆ¥c\bÛ\9c6Éãð\aXz&{Zz¨k\7fçyÂkå©è\19røaÝAûqx`pÀ\9aðL\7f\14\9cúfÞxRãþ|\1c%\aÃß\7fÊ­õ¦ë;Y\86\94Ï2Õ\13\91èu\19A\0#\ÿ\9cëBº<WÞ\9c\82\1erÑ®,ñJq.1µ8\8aæ¤ó'^Èá\ 2p\8a\18\0\1fÈ\92¤ø\1fÓîUÏ\13\ f\9b$\ f÷\11Þ®Ó®7r|ú\93êMoñ   ½AF$\92à\90è0äLâälF\7f\13\1dr\1c\84Utq\9b=#óý#ç\ 5àk\1f¨qZ¹5HÃÙêò;ëAV±åú\19³\10ã\97º÷ßj-Íí\9e\92«!81Öè¶Z\1aÄ¡XtúåÙ\83°ö\94ð0ÄÉU\1a\ 4Q\11@\1f\12\1c\9d\83¸©öçâ|òë ûqÿ/ \18vÓ}õï\8d²u¿¯:>\12N#|ɲª\ 3î\f\189á       û\12\8fþ08Ø< cÈ¢íìÖj\bÅ\98\ 3:Qw@Y¨oGu¯Í¸mº\89Ã\ 5^,4\13\8ffCXCV<?ÂEÄ\99¡$hP¦þtI¦\1a0p² Å«S|Í\92\ 4ìÛ        BÇ\90Ðe$?ó\ 3ü\8f:·Õ)`Ü\bæ4¼(0]¼U<a2,?\86\\ fð\9a\13Ä\19\9dÇ^;Ç\11\86«\rðt\eÎï}n¼{ÎC\ 6|\1e\9f\192B\98Þ}\85='\90±ÞD9%´\98ã\ 4þTì¯\\ 6^@Çð\1e\17\ 6\84ËÃS=¿ÞS\8b\14\ 3\96v¹k\f\9a#·xI\12ÇIÒ¸5    ÊA\a'Ïg!T4¤ðs>ªÊ<Ø\85\80"\ 3>\ 4º4;\1cYÖ¯\ e:'\13\95*½ð¤g2\8dÞ\97d7ÿO\0\95\13\95¥1AÐ]9¨\by\r\14²t¨;§x
+\8a9A\8f\8e\83W0\94«Û\18Â;\r7\7fºnßÍè¡\b\82#J~µÍ\10­y°<Ë:M\ 2î·9i\96K\88\rð\bBU\13À\7f-ÄÆÝªçëê\b\1a!Üiº.ÊÍär|\91];ÀC\1d\1c
+\84ð\9bû\1fRX¢û\1fë2F§\88\99A"UBeäê\14%­`«P\12\87\ 2Wrºôà<S\85\1e(`\v\9a\83\171\9aí\8e\8cϧÐG¢æÕ­\ 3{¥J\ 4Ü'\ f£¾9\11\81FÉD\80\1c\9cË\v\81ï\8b\1e\ 3\98µ}QZ\ 2))\94þ\7f\17ûÝÍ\ 4\aÙ\88§\18¦\1cå\15\93ë\ 1%\ 6®òó \81\v`\87°\83ÂS\e3A0'fsì\\9fè\90ÅË"\935\91íR\9f.Ìï¡éqÿè\87ýï!ßÀI¬ìõ\1eª\90õ7IZ6]\96+8\1d¥1\ e\\9d)\86Ø«\1fV\85Ýø´\84ÌH2j\89}\96\8fûiÔ\0)8ëó[l\84»\89È\8f¸J¼/\86
+9f,Þø\rëM\83cbLt}}º·i\1e\99\10d\84$ÿ
+ß,\ez6Ës7       'Ñ·mT\1f{\11\'\rÙÆkeÝX\ 11Á®¶á\1d\14ÌÕn4Ø\ f%\95¾´Ôn½n\1f}È@Qû\ 2°ÑT×6\9b0°Ò\8aº¢à\10ãú\0+[\9ex\1d"¥À}Á©è\13\10\988ÿêýx\f˾\19þ`R[åkÀpV\1c¢E\86\1a\ 2\ e­\fJ\ e9\eG¬ú«\rÍéH£-\1aà\ 5$ß@zG"XÔ_\86Nßq7\96!"cäìFRÏ\9e¼m*º³\b\83\8d\82\14°ÂتÝ)Q²0"õ"æÅ\1aX|/¯\92èS\17·ðrüh\JF(\14\96on\0\7fÖòh\ e\ 1\1d#>B\fI\1d5\90ÚL\80\8d½§\ 1U\ eYºÅ%Ëáx$j\9cì\94¡Û\18gcã­¦2ò¥Ê\12¨\1e\81\1eã\9b\83b#mi\9fwg\10÷\18\89P0',£«å¬:cÇ\1a\19ì4\ fñL[\9a1Z=Å\1eÏÓj_\1c9ÌL\18ζÆzm¤ó$à\1f¼\96»È\eª_¥ÕzrÀ\12' y8©\86\18\ f\ 5\bÉ5\97ôõð¸é_ÉÂ\1d»,ÝÈ®¦ökO³\123ØNà\10\1e\aH\ 1\94]\8af\fn\94Ïc·\13(g\90ã¸b!¼<Á=\92 º¨\ e'Oº¾ãß\ 6\15{ê°Êå¦5\9e\96!\8a\ 5ó-\85Ùob8nH;Þ­7À\97¿\17 \93\ 5\e \13¨·$,\ f\84\
+\v\97³\999\10\vÅÿîÿ2CU×äñ_ºî\80\82\94¤ ®à!Üì¼¶\1cÍ.L\ f\9ff\85îzíÿ\96\14¬LT<­&\ f7I\82ñ[i·vòe\12£ô\7f\1e\9b\914ãuή÷fíF®\12ÕÂ8`¸.¯ÅuY6¥@\f~²c\826è{¡\89®Á×è\7f6)n\13\92\8cÈ\9fï7C¶?ô\86Æ!\8bí×\81&1Æ\9aB[\99X\86\8b\10\12\r\84\8e?9$\87rÂtÍ=et\8bcÕiõ\12Ë{$¶mGQ³Ë\ e°\91 \ föNî`Uf\89\8eµDËkN\0\\ eÚ^i\b\15^éè\93C Lþ¹­\ f\ 6Q\98\eðø
+AË\0P\92üAd\ 6AÞ±\0ïrŷij)\86%\f`¹(wsW\ 3Çn\ 5\84\b­¼ºz\9f\85&8ó:Í@ü²\ 4²´\80;b\9f\98{­üeÿ:A\87Ïv+\1e\ e+¬"¾t\8d\8bÆ1ô\18\19y>m´=\88\9a\16@\0\15î\ eÏ8kÿI\19Äñð*\9amÙ$ñ\15\93ä¿Æð$\10ë$\8d\18ëô$\8e\ 1\98
+5\81\80LO½s¿=\11¥\9e$\0¿V»ñ\8f=Ãø\ eê\15.¼4âT\ 1\99Ûx!ò\11\13úQGþZpº÷º¥Q7\ 1µ)\18\föî8\9cÏÞ»<Hgy\1cA\98\ 6\19\ 4Ù¢I\9d\10*\ 1\87øUáÏ#Ð\90a\95Iq\862Cäe+» Hë¦Ðx\ 2©¤>c¤$o\93ª\r(3ê\b&\99\9c\133*m\ f|ô>¢^ó¹_ñÈW>"T\19Ä»\10ZGÚ\90)Z\14mB\19\1f (\v\98#3üÙî½õ$\8a1^Aî¡]#\b¨\r\93tüå=hdPp#8[\80\99\83\0¤\14 f\ e/$¶»>Ì-=
+Üf¡C¦¯Q\894F³\«õõ'2\97òñ´\88ò¾.\13\11\b¤Ü\7f ý\8bSRÊk\ZµÛR\ 5¥ÌyÇ#b\8fL\ 1\8bqt­ñ³ý \15gj<¶
+³p\18\87\ 4g$¿ÛD\86H¤Æ\92ÖV5\87W`\11n\82öx\18X³¡\84\ 2}\ eR\99ÄϪÁ\ f\90\80B&\7f\82\ e\8dD»À>\8bV\a_\12\8dþÆ\99y' ò@MÙ[Âëðih·É\15§\93Ú\16zÊ0\90ÿºú15©\ 3TáÚ{\a\8aä90\ 5ݨ\157gS.Mâ3\98Ö;\80\fÄ¡P\ 1\12Þ\ e\ 4JÙ1ø¼§G,\1c\9e:Cña\8f7Ü 1\9c:ó¬        Ô\7f\8an    Ò©ËY$öâ#ìÌ&ð¤\8dhº^¢ÜÖ¦N\9ctsöã÷    Þê\k\96\88Ö_)\9d\87\82\93u\11G\843\880K\ 2ä=\ 6\9a\15\ 5·hU#°ßùér-nÍãYÑ\9d\82aå{?êN>êÛb\8eË\12\1f«"^À\ 5\9eÄ85\ 3\ 5Æ'¹\80\ 4\ 6~¥´\8aj\90\81r_g\1c.\ 1\bÓ@KÚ®Ù\95\1cµº)¨Õ\9b1/,)5$Ù'r\1d\8e¾Ü\88 *IKÈp\84pqê\96}sÑp?M\101e#ºh5é÷pjÁ \7f'¥Æ-Bõ\1fiÕM?·$ÉÁV­µ/ab@âLµ*äu·\9a\175ídB» c\88²§;]¿¡SV_²Â\13¥ë\97e©i\99\93Q.}³ë\13Øç\80\89N-·ø~Ir3s\9c§\8c\9b\94\80«Ë\81ÉÜ$`]/\86Úzü\9f\1c\ 5̰lÑI\8fwFH$\8eE&¥\ 6\96ÌF\1dá\ 1\9aÙOü;^ôY¾a2,Ø\1a
+©æäÿi¸øéY©T¨_xα,Þ,ãÚø\ 3Cê46\95\16k°/²°LÄy£\r+!Ï~j§¦rE\8cy\8bÆõXÓ\1dÂ?ó
+)\8b\81\18×EÀ\0<\97\b\1d\ f\99F\17ND¥|ÜúÐ1\a«e\8aïÃÂ(Ês\8c)%\vêìIg©\9b¼\ 1ý ³\11u\12«v\82¿\12FÜ9;vÙ@\12u_w³-Qºv'\86_¶»- R/\84ú%Ð
+êð ÎÄ\9c¶)\9a\9c¡\9apZéܶQ\0\ 5â47¡a!SäQQ»¢\1cü)\14\12]áwþé¥0é[åMåâÁ®¬W-øÿ\8ay·4\ eþ#\12ùgãC\0æqdȲ\19ä³m©µH¿à-\ 3ÑÑ·nïVÕ0ÿ9\13±Ø\90]¶*üÐ5Ãyòó3RÉ2)a'\9dÀBð1«6\93ÌÌò'\etvhÐ\90$\89_Ës\93j"÷r°aÐ;\92éÊ\128ü)Ú\80j\81Ê\1e\89\ 5Y0$Ä\9bZ%ìFæ\19r\1a0¹ÆO\eøái Nzb­÷.H\99Z\ 5^.\8a\83\7f±\842s¤¿Î\87\ f×\ f\9d¥\99«ËòGçO\82íc6
+\16è\v\ fïçÈL4 ¶\93+i\12÷©\ 4`\8f½õ ¦\ 33A\9a1Õþ!sѧi\ 4úW\98%´Ö\16C\9a\99`µ-.\ 39\9b\ 1VYJõ\ 2f×*ï>\16\\ 5Ä\8d\93M       úQäÅqè\86ð\9a       ¤ÿ\14Þ{¡µ\94x\12:]*%\f¼ ù|q¦\1c£\81\94ñ¬Æa\ e~\ 1\14\14Y\15\r^  l6\90¢8 ¸&_®kU6>+©\86ÝÐÜ\8d¢\18\1eóJÉk\0ÍIª¾\f\97².\97¥\90!Ô?\8a\eð\aĤ¤^=-ã  Ã\ 6Õ a¹M=H       ­\86uD\8dL}®ªÈª¤ï~\12ËUÆ,£²N\vÃ\8ah\ 2¦ïíàÙäî\8d\11íÚñµò¼vò\8dø¦gpË{¿Ì@Ú\1d»a§\12\a\1dPÉ÷H \9eÚ\85]\8d.#\99®5¼ÄÛÊ_\95È© õ5\98\89ãqø<\ eë¥.\85á5aÀ©\168\87^"Ë-\81^\94¥~¡\8d¹Ãéw>À\9f\8c*x2\18\10~\85\ f\8fµ\98¶OfO÷wOI\8eÉ\9f(zD¾{r\82&Aö\ e%Óø]{+@´þÈ^\ 5ÕÃL\b\90òAÁhh/©-\89\8f\0f+ت<¥qøPNä\9e\Hñì\ 3Ðm&s0J\87à\0\ f
+\1a\ 3·b·`bê\81\87\1e\8e\84\94\a\8f¦)#xàMâüWr%q'\ 5a\ 5`Á»\8as¿'\8eChd\19®aeíï\9bîþv]aéÃ\8an¯²H¸`íÍTR\10ô=XM´{Á¶\85r×¶\1dd?,\92úê\1dfÊ9^c\88µ\9cx0lM\1d\9dø£²µ\1f\8a:\17U%\9a©\v\9e\bcúÁû´xÈB¸B6Ý%<¿\81Ùv%ûðI&\9di ß\15-\12\13\ 1{ÑÖZ#íx\89\96\81Âæ.m[eƤMf>jù\9b\8dØYðLÙï\a¿\81b9)Ò\8b¾û-\96¡\9a\8c   \14M\98\86¸x\0p°ÝrÔ\88   ÙÌ´¾ \8có¹\933Ý+¢d_"ï"=(W¨B\ 1#Ô©hD´;â \fKp\ 3Ã\86Üý@´´º¾\84Lr\0¤ç0\16,s\8bT\9c\85\b³çÆ{\8fç0\91\93VÃ8\16X¾1D\8cc\14®\86¢rC¿3\12\%¾PãD\8fxÆ+Ò¶\1fOY\87RUP    \97\95ÝÏó\94dÏx"»IX3-\17­\ eÙ\93ôÞôù=3\81`\1a\10W\ 5%áÒ $â.\91|\87\9eA3\1a0þD\89GHí\90\80\ 3h\rÄ÷i\81\1a#k±bn\ç©Ú¶\8diAÅ\ 2Úê¹f\15\9f\89ýö\0\a\ 5'jÅ\ 5ÅõÅ@sp¡7ºÃ\10Bdw\8d¹r?þ\93ïKäp&9Ù\94Ü¡\82\b^¹ÃW'\8c\8eAké\82ùþô\18¥º\V¾Zl6>ñôT,uM,\1dÍø\88êbzãÆ\ 6oR\8ab\1fU!\96\98[\12\14·\1e7e¡\8dVúõ)T\97\04ãöWÚ\85`ÅsôÙ'\9e \9fNâA\9e\89\ fÊÁ\87\16\fß{YÔ        Çbø\9aHlBÄþ%\14&§¥~Ã\NÝOør\9c1ì`\9f\95­$\84B¿IôoXI\90à©õb{¡\14÷ú÷\rpºHM®HÅ8\U\97\92)#»*\123"´Å\9c·\8a\96\93c}\ 1\85IP\98t¡ì¸¡\ 4\9a\ 4E'ÐY¶ë9×>\ 1Éì\ 4a¿JGy\9cä\1dõ»#Ú\96ä*»\9bX7Ï¿Ä\9e\ 5\14Þ7äj|/wòÛ\83[Ãê\12\92tÿ\85\94#³Î\\99\ fDÃ(=\18Î,ÙÔC-g¼éÉ®\1cÎ
+dÊ3ë|VE\11\rM\9b\10\rw¼Ö<Z\96\9e¥*\ 4Léj1Ö
+)P[?1\10k;ú\8b§¶\84B¾)ϬF31Ïpå\98\ 3ÖkÞ9[\9fb<QuÎLþO£\1föD>~ b\0±V\14~\AI\9cÛ/9\83î:Î\91í\10¾0ì!v\15/"õ#fz\7fz×å±Ú­O¥\ 3\14\ 1\1aPto+¾K\12W=WD°)\85\f7\90/£Á|wØ"=ìL\9b¾\8e!\80³\86\86¥Â'\86äÔ\85´T[6¡ÚðßKßÎ+b\eE¥-­KÞ)5ßw$b:7\8f#\8dé#O¥ý^ÍÉ\ e¯,-{ãe¢\8569\12\88Ûâ¸P(y\96É=­¨Å\80\9b/\8d\81ô\8cúÅjÁ\9c[\r\96\8bû5\19\9b\80\10:\9b\94wvqè\a\95"\8b÷ø"×e\9e>öxð½\98\92+\rLBÓ\8e"húk+\94±è\17\19¹à¹\11ZÁ#¾>\13}Ë«´k\94ö¤T»È\85ò\82>\94\91\93Ê£m\1f³\1c\10½ì\88g\1e\95\eØ-\1c\8fB3NÛ"ݾZ\v}Ìצ0 Ù=\90ÄÒ;ïæ[Ýa\90/ùdÇs±Õ\1dÜ!qñ\82ÎWògÜ#\9a\87G*zÙó³Ó¹ãL\94¾)6Y}\94*dµ\94YÄé[¼ÁØ»9û-û\1dÞ¯5AF\ 4¥\ 1\194G¯qË^XK\87Î\rÂ\80Àjì ;Ða\ 5¶1H0Ö\89áÝÍ9¤R|Ot\89\r\84\8f\vÊè\ 3\8cvÆK¼¹FïcÞ©\9b©ó\15\8d\8eàGjº`Ñf ê\89\ fn\a\9eQ\99ü\1a\16\ f¾=àj[&?%\1e\ fÃÑF,å\8aI;·®¡F°ð.åî+\83\18xEÈ\1e!´C\12\0kd^A3Ä\99Q&E\91Kþ'ýËÜù>tÎ\ e¸X©r\88y>\9cÓ+TÆê\82oK(Éx®ÄÉZ%.7âºÀ¹Y|ô6\86Üç©z\84\82(>áè'\92\80J\84oëUBõÇejG×
+3\80\82íôDûñ¯\7f\89ì¦çø\8cØÓ±\11\eióÅbce\17\1e¢:0¾\f,;(N\vdòGÂ\{\rKÄìl\r_\8d*óÿèÆvÀw,/e\8b\aéÀ¨\190\1a}\8bâYqÜ^ËÔÍ\18?´#qÅî|$\ e®u\18,ýb\19\17ÐÅd\8d\9e\85L­æg:\8dÌl\84AD©7ëc`ÜB¼g\92ʽuÐ[ã¸\ 2d\ 6·V\9b\1d$_&"\17ý\ 5';6I" \92²¥\13IJ^\93ö\87Í,sH<\eÕ\0\ eÀoÔÿ\9d\ 4\1e\v      %?¨HIôc"Æ*ªô°N-Î\89\ 1°D\8fÅ_\ 5çO,\96\95\90ÉJ&Ii·rfi(ÏÚÀGÙ¾iy\eC\eÇ\18·Æ\9b\ 1z\1ax\8cO¢©\98\f¨éDäóÏcE´{f°#U¸%}\93\1e¯\1fÀt\1c×i+xd\921\17y\ 3­ïÏà\98©\14\88 ±Ð\18\1d±À\11ì×\8d\8cJ|-A\rÏè\81­A\87È×rêYÏ\9fâË_¡\98(YÖ\13f|qÕ\9c'c\9e\18ùÄ+n¨\r²
+\96HðlÅhWm\ 2§ë8\aéÍÒ9ü4ÕÐ\13d-k¢Ü\81\0\86¬\8b\8f\ 1¤+\91ó\81\9a/\7fù`\7f\ fv­äÄ.ÿúSA"a\7f+«¹ýµH6ì¢\8a¾\9eê\8b¾­¤¶\ 6\88på®ñ\10wtXM,\8f\8at:\86<³"\14¬÷à\ 1X;l¤%~\a\81½\ 3)\16­töî×\8aÌ1\15(ÕGC8ñ}Ó\ 2|Q\15»G\19ÆWU\7f%FßV\86D\1e½-ZÜ\85\17+\14X\88\99< hí®\8eT±ö*5Þ7x;\89Æðåîb¦Ø#\ 5\15$j\15>D3À\vÛaD¥Zd\9b\8c3² 8n-ÛZ\18s{îíW\93c\9f\16²ØÊ°è\8a\ 1©Õ8z
+(?Ä\83\82\8b\ 1\bdf5±\8fE0¡\ 52D¼ã¸kcKâñ®¤uà\e\ 5\v ß>Âs °\8a\93\8fæ\e¬ÿ)Äæ¦\9cKÂGPôߥ\83Q26\85ÒÔ\8e\81Ë"\85%4³¼\8c?\82\b\12¶l3=¢\v×\r¦¶·óh\ e\a<= \198EBÇ'øgÀ>CØ\18]Ö[à+å\14\8d5{ìû-ð3\12\9b\ 5\86\14xl3väh¢1\12hlö\8f\0\ 1@¿w t°%:úF\98|l6L\9eº\8e@©2®#ñUg\82ÿ\98µîÈ·Î\bê/×ßû\7fq#yÀ\87qàñ ØÂF¥¸J\vð|Π¯\17ÝæÁ¯\98Ç/\11*ïiâ^\ 1\9e7Om\90#ÚaÔ\8eo\87â5\918\e\b\80»ITC\8b\12\9aOê!ò]\ e[y\869\bÛ.¨Ç\19Èȱ\10ì\9e\ 4ËpÞeÛâW6ÝV1\87\8d\ 2k\10\87¶Vû<ªvX\bl2§@v3v\0I\83Ñ\8aÝ\83ê3!s\1eï¼"´/\ 6\88¤=÷ý\83öh\87%àOz®ñNq1n\ 6g\8b$ 5\13ÿþ|¹#É7Â\b\eUþ¥5\9aósû\94÷»YæÇ\88ò. ã\9a2ÐD5\9f\ ey0¾¸Í#Nª\9a$½ÚÄ\83\88ý\8a\1ariÄêè\99\96é\ ef\81Xëãzkû$qì[8\8e®Uþ\9e ï\1c\80Æ}%¾ÙÎ|Îá- mWgPP9¹\17T^7N81£Föy´¹jÿÞg'\14\93øÀ\93,\99\15\1ceCÊ7'X¥7½NRÕ}\97\7f ¸áP\82!Û'aRByh(mk.Ïú\97´h©é­R"¦\96¢\15\1cîÈÏs\f\ 5'\91ÿ\ 3 ^°Uè0Ù$\98\80#\1az:\15\9c\11ç\91\80#¢¯ïX70\16\91\vw\ 3\916\12ð   \9c´znÖîÍhÓ\14ÚÈ?\bµ\82ºº\9c\86\91\8bÏ\81\ 2 Â\10\18¶.ʾ^\8dÆÄY\ 6Â\82`íñ5Ã]c\913×y¸Ú$>}#îKL¯$ÎO<·s\12\rä\0\ 5Ãü´ßT]QX&ér¢ù;\11'*\95ë\1cÌM¨Ü\O\18tÐB¸+Ü7\8ec\92ìúCýÄ%sO\94¾N\88\bÌ\90î«{vX|\ 5\9er*\82%\12Ý.\ edl\1cI¤Æ\97­U\15(QV\1eÎ\·\9cÈkÄ®då\95DM\93\9c\9a¾pä«Þ¢\85åö\19ÿZKGº¸ºq\12~<D\9fpÓÓA\8bóFì\89Q¹Ïe\ fyÉe§;Îýü\ 3\9b\98\esZ\88H.M\ 6\9cÿø#/ñîÔ}s\14\11c´ET\91\7f4zó\¦A¡xUW5Ò÷ÔÏØ"\9bñ\bYñQc"fCê>\18»Òã7Î\116,¹Sg\ 1çO°â\15ÓS7Ó\r\ 6\87èU1­æ\ 5¡\15BlB\1f\8d\ 3\95O^x\856´;áÉ¡àÐñ\bcò¶\88WPÐ\8bÖ\81UP}f\0\r\84è\82\87Ûq\ 4§\9cLoð}\ fW\1cñ\r\ 1[\0e1|\83\1a×»\ 4\80ãO/Ä\bY(¤\93Æ5úav¬ÓA0jò\9c\17æ/¤U\eÙ¿CÝ\89'\1cãzZ\v®I{\8aªÏL\15\89Ìb\96¯lÆ\9c{\ 1ÿÌØYüØø©g\9a0\ 3¿¥h\ 4¦\12;ó1~%ú"Z]Þ\84²\ e+º?³ÜÎuþI8iñ\82\9f\80z\1c\930ÄsË-TÈúØ´\15\ 6¶p\1c\r    Ü ×eº\8c¤ùn ½\ 6ï9ºÃËÊ\18æ\ 5ø)\7fM W;mBßæ\9b\ 3WL0®hE%Lè\ fï\8bdônïBÁ\9e\8bK\f\aè`\84ÎIx|ùp      NÇ\88E\8fFS¦¹ìHh\808.\97Á"o\91\17VW3\ 6Ãaά¶®ÈáôK\82õ\8c½ío\1eÒe'\15ð¾N94F.@\9c\87\8b\18'\ 1¨\87ã¬\12\82[9§Yúßså\ 5\1dÊ\91²3ÌX«\v(åjsê¿\92ߨ.ýð{Åúqî\rΣb\15¡iâÄDyK4 Õÿô`\ 2ª\0z4Òe®\97é3&\ 6\81²±å0§\ 5O\1a1ñã    ø Ìqéu\aàªbÿ
+):\ 4wÞù«_vAêØÈu6\1a\v\17´<\ 5\8eݲCÄNÇxû·¢\12Ø!ý\15ÓfËkllB\0Á\10ÖG\87Éë\rê \88Â#±¥1\16Å®Y|\15\83Ï]ðs,$ç¯D¡3\6\88å!$\9cȪ\ 2\8dïÜ×\rä\ 4âv`S\94\12ùgdûÉ\84Ö#\f®>\90¦«\87%êìÎÐ)é\93Û%\9b\14ÙzÞÓm\e\7fPݾà\8d\15x0
+"/ÿe\94¢\8c6Vu\ fßßÇFj\15Ó=\1e¢j³,Yó\94 \18\f\91\9aÒ`Ê\ 1Ú\18\1aì\b\aÅ\e_½ø0@37òùºH\\85Ó:\18\9d[h\ e°\18\1eü¾Þ¹þîý£G¨¸xÂUH¦k\9dó\1eK       f\88E)|%\94LR!ú\90fSaD=Çn\10©A\86Eê¢L«¯§Ày¡Û\12?
+ÍGYÆc\87<¹ûW\ 4í²«:µCù\1eHw¦\1fÛo\1a%H\9f\87\9cïênÏ¿cËp\$4Ñͺv@,\9d\8d\923«ê\82V\ 3R\8b\88[íJÀ¾}×Õbå*rÅ\80\9a\82\88"eg4¢½Ðd\1e¤áY+ãûÇ  Ädôrå3¢¾àÎU÷(\eæð\9dP±°8Dø¦ÊÂ\84øL   ë"Ý-w]Å\98H×X\19È\8d\8ep'}C04o\1cW|Õ,\81U\8b\88¼õR\1e~'¨Æ\\8bØ\1dS\99G[bLÍ \v\93MKpò       \91:ÀÍ    a\e\17¡\8b+u¬Rc\8d\ fF\9dµ \8e\ 4
+p\eÌßQ\96©LµÌ°_i(\aà\võx@è\97eºrs\11¬Û\bpOAl=qhß
+Àí\rêv×on\90\12K\8c½ Ü\8e\11¬4\9c0\fõoh6¾rÑ©Tm\1dô'\80CZ¦\ 5u|v\ 6\8d\12\94I\ 6øóC\1eÊ\93ÿW²ã\8d\1c\ f\12ã0\13åMÈ»À\82`˳²Lð\8aþT½Z«Err`\16yV\18}xeH¥âh¬\13)\aÃ\ eù\ e\9bË[`Zº\1a\9d\ 3\83bC\8f Ç\ 64®²,        ï\ 50,n­¯\94\19W
+)`ö\ 3þ\97\8c¼e\b\84ÆúÓ×\18\8bÖX{\97\86\1eÊY£LΨÌ\aDr©±7?ÎSnàX<\14ZwÆ\9a\82\85\*\86\ ev\ fö \ e\8eR°ª\8eY`Ò«\ f\fDO\8c«mµ\84n\fÇ\15àà\88¯ï#Ù×c\ÿ `«<¹Ó\85\87Ù´OäÌϺJ/\9fJ¶0§]g¾{ÑèÇ\9c*@Ƽa\8c¸Â%¤M¢ÖfHÇY\1cåaÜïi\ 5\94J1k°Z]3þ\86\1e¼\8f[\19;8\86ó\84v¶µ\8bÁ4WÐ\8dW\aáºÝî/ê¤/\e\ 5\8cË"d\92 ·Éϳ8$Ø\b0ÃZÙ`\v\1a1ÿÂ\1aÒ´\148ïÊûymÕcHO\ 6¿q\83pªìmÑR\ f¡\8b\96®\ e\95)4\85\r/1+h|#M6O\810\14ìu\1e}g,øzÌÜÉY\7fa]=JèÕ\7fy\98\85\ 4çú¬E\952¤ô)Çq(9\ 4ê\7f¸¾¹æÀGì
+\90;\8e:!\9f%J:\81\ 3ø\fqe\8c\ 1h×F¿\8b³æóùo@\ 3åvBl X\83)\ e⿪Ë\b\9bÄV\87¥\1eX\ 6®NCέ&K0K\fX,±Å\12\ 3
+\95û~\19\12êO\9b\96v²Ì\18\80&©qN\91V8¶KÐ\89Ów3Çäp$5\12óëÄÆ\94_¨ÒófÙpw©G˨ØÐ\9a\ f6d\8e|[ä\ 1ë:\16\rê\1f\9f\12Z,'þ¤\1d\84åñÝ\8e\19~ÍøqbKó\9bË\1c\142\1aúÛqX\ eÕrWc¡\9büG \8fJBc\9fC\9d@º"\ f 
+\rÆ¿ÉWOð\7fd¶3Ó\b¸Ñ7fJ\85\87¿\93aXY\8b£/\ 3³ã¥\ 4¯+\19v\90\ 1\ e\11ägG]×ÁE\89\9f@\e:3dªÌ-¾ä\92\8a[\18\98xG\86_[G\7fCµRÀØ2E¹ ò\90³\89¿¼´³ðßSÁ\97ØËl\1fz\89Ô©(\98Ü~\83¯vª¬P\86\8a\8eðYøI\96~
+\f\ 6,\1elÒp~d\16\ 4\ 6Í\8f´\14izËC\8eR\ 4ft;\fÎ\v\0\vâ=\17
+ΤL\85¡Ô¯,íÿæý\v#\11j@âÂ[O\16²z×ï«X\ 6\83B¸Z0çx/Døî²Ï¢ïã\1d\9e\9cA\14T@ð\ 5\a\ e\16ò>G`2ÑÆô¹"©äZÅ\8f\7f\1d|Ûy\11Õôò«â\88A\86×\9de!`g,`\12,þó\83Z\ 3\9f\ 3V\8bO\1aÎ\v®ÔÝÀxUj\a7\ew\15ÁR\9b!ÏÏÊ<(
\bZ¼Í\87\91C¨Ê\9fúÌ\1fÇ ;Y±ªüèÓÊÛß3È'|çh\840G\95\91Z|=0BßÂ\7fØ\9fR2TøòëVC]Ó§C\14VZÖÁô\17\80'\b\ 2\10§¤
+Ú4$Çóå\06\a\7fçBFH\rÆqð\86§P> ÌÈ\83i\f   \9ecʲ\84ö+$\ 6\10ôQHh¾|æ\9cF\arZ\13M4ry¥£HJ¶\ fz\87\ 6Q\f¯²i<ì\a\88¿bZë\87lUЧñìf\ 4È{q\9e\98Y\92ú\80¤º\98«{\89\9bö23pÛ¿G\9bï6Hë§å\85Õ§o0¯â\8e$S\18\b*½­¢\ eÕé"TÝN\15íg±6\9aÌ2qUò\8d\17õC >\80­©\18Ø\9d Kn¹\e\90~Wâ\91êkÇM\17\0)\97\89ü \9e¯èesq\11DiuCØ6vâ@\9c \9aÙø\96Ü?ðW\8ab\8d\ f\84mJkIvrnä\10×\8c7\89\11ªa\1c\e\9cn\85Ã;\12²«À*$.¾F¾U\96ÄG\1dü{£¼¥Çóá\82ô~Dê\11\11¯\10\83\10r\b\19ÉÏ\1dC'ú8Ù\1e\84ý¿'§\86t-r\10(²Óáò\8d\ enkú\1cq»WÇrt\ 2eÓ×´´1µT\ e£ìLëøe\14\9eó>\97\´Ï\81OtÇ\9c\88PlýØHÝ\84i\84q|îá\84½kÓк§Û\1f\13©2ù\87\ 3ª\9f\bfóÁ_ MÝ_ûNI¯\834\få­zÏíÖÆØ\8c\8a]\07¡?\eêØ_bôä~é\pN#Ð\ 25ò©ÎÿJä\8dgt Ì¸\12ñ>ÐSó õÌÖ\8b4V      Ï\96k¿) \1aÒ
+ئ1ä¿G¸þ¤«\89eøcô#}¶\15\85àE\1fµ%(f\85\81ï½Ô\eí$3+©Âhü\1a\8eÃ^ø\9b\10:\9aþ«0NA\81\ 25¥xw\10FÐÏ\163B\rÁÆ¢]ußúë,µ×\15·m¬\12,õ&Ù\8fÍ.}\81\83¡iy
+o*¿AÛd}]2ôÎxM²¨`]B@\94ÛJ÷[樬@gár\7fu\ 6+\87¸ð\87l\0\98ç\94bQ\1cÖ\14Ó\µ\98\9fn\ e´\12(3H\85Äékgf»öÉÝ?½ì\83ñ/\86i6\91\88\8f\18Zz)¼#çÃøq³\19÷\v\8a\8c\fì\r\ 6oï7å\Á
+üM
\84·\13bOÙËXbm¹mV\97­M\17¿î2\87 §X\89\18\17Рé\16G\ e)\91\18°á\86î6f\8c\96aäï!np¼n¨¿\80Ó\96ÌN@Ä\90å\8c\8dísÐ\16\80ã\\95\13waÌ.åçï\ 1BwLtQÑÙJ\99\90b+\86B\99ÿßJ\94\96\8e\93t/_\1d`\ 2Ão\11ã\fù!\6\88Ùö)=¢]ù}\rb\f\9c@¸·Dg\v\91f\7fR\95^³s\rçF\96"eF+\86\10\1d\ 4\91¸ð8t\873ÿ©\8c¹,Z,¸a;\1e\136d+\ 5v\96\96\r
+Ät\88ÖÍ\14P4!\ 4     G)\0P£ùÖ &ÔÚ\ 6áö\ 1}B ÛÝ\1c¾\1e\92Âu\96bNæ¼¶È¥\9a\r\ 6Ez_N¹»\18\12\96ÓÌ\ e\12\0*\8c0,zEU#3ây[éÍfR¡!äÉÌÂáN\17\9a'l\ 2\ 5       Þ\8dï­ÎæAÞô³âÛ\f|}ù\8a=\1e¢ý\8d\ 2¡Së\82£ûP\8c^@-%qqèqÓ[T"\9dÚ_\8fbN\\r\12s[ò\ 1dB\19\8d-#\9aø*Ô«¬ã>AóÚá\8aá\8b¢ì/ã*Êâ    á/?<\9f^Qɬ(k4\vU)Y\fð I\8b£\1c\b¡9Ù\17\1d\ 6M\80;O0\87\87{\ f\99Ð\83o\89\ 4@\9d\Î~JYAt\85¯ïv\7fÓ©\19\86\88ôJ\r\vöá\97\8f²ÝïxqoΡ#\8bÉùêß~Ð\ 3ð\8a¾\19¤/ê\14y³ÕP\12¹ãÒJÐÐmc 0ÝBàñÆ\18*\955t\17\9c\1c\11n²èÃë\10ÅSK\82p\80q\vÖãæÓ@\ 1Ö¹ä­\1d\88Æ\ e\14\85·9.ïäí³<ô\82°Ô?L\97GnsxÌÝ\8bpZ\9a\94gÿ\81r  ø\9bd«ÈÁ\1a Ê\96\r\8e\e5\ f\1e\15NO^7\a¢Û\ 3Ùø"\88jÏ´\94\11\82¤\ 3\12hêê\89~Gá=ïæë\120°Öóp½Üܧ\97S\8b\13Z\196Hcì\82þû±\8aµu^\ 6Lo\P\8a!\84K\87Hc\18\12\17µ±  ôs\96ehDi·|â\9e/
+Ñþ1\10] k1\8dos\ eâO¼'£wí\8dY¹30 Ê>²\11,\98\8dΠRë`v.\ðDÚ\1fæð}ì áu¼URqÆ|)1\88ßðº­\8b\9aÙÀ$C\19\97\r\8e\87\b\81\8fµßa\8c
+9\10¾\1fÃ\14ù̹C\ 1\9bÙ\1dQ§0îö\Ú~â\ f\92~®"ŧÎÀöÍE»e\18ê¸] '\86Érê\86\16\85Ù÷0g%M[pn\88\87x\b*@-¼\92\99\19T\aLáé\87øåiÛxà'¾E+Ã\1a/\8eâ{\82G\99\17\99@\e¥o\90\ 3\8b;\8c\f¡{ðÙ7\83´}\8f\19ÊÐ1åm\ 4#H\vWL¤wët¶÷]?3x\16`\18htq+  ßX        ã+ª·yl\ 1+2Õ\fãdOg8ñ\17¿\9a\81\97\r\87!\ 4½9©ãÉ\92Æ\90EG\ 6\ eícB¼'m\81ôÛ\84ÜR(
+6E\91À\95þ©\b\16Ú\aÕ\10\9aåN0t
+\14\7f<ή6z¥ÇT¢^øÂ%\9cÀß3|]\84\92£jt8\15u\91C.%¼B\87»Ýn \14\ e\92Õç\83Ä\18\ f¸mÁ°bQÁ\vÖ\87\rUnÀ ¾fö¿\98-\e\83\10LOmð­Zß\ 4/Ññ\88Õ\80Ø)+ÇÎAç× ú¦Ù/\vk|\ e¿9±\18ç\9b*F&ã\9axàñÜô-Î\1c\8eí\ 2ø.@êìe"í'À\r!9ªx\14Ìxø\v
+>û\91¯Éú­;WHæ|,öRW5¾\1fP:Kt\8b>\8bùÇ\85ñ\1dBá#4çÔ\8bßÎ\199ÛÄiö5[e\93\19b\0(8\17ÿP*
+äm\7f%\0`ðo\elÑ&ËÃ~K\91±Ý\92\16³& ÿ«12?w\12C\1cä×*3»Þ\1a¢éC Æ»ª®ê#0`ïäû\197\7f2T^A\ 3nÈòÕ\ 2\rÔÔ=²\ f:3Ö½»\a-å{ø0\e\11t\ 1Û¡\83Â*oò       a\84ÒúñDÞ©\9c\87,G<Y\ 6|X\96êg T,)µþÉ\ÛP\87f_|#5mÇnÆå\98¸\968©GH%Y\8a&\ 4âÿ,\80Ê}&÷¸8}7e\91K²ëD&è\lÀK5\97\85ÃÀ\94s(¸e\19#E©±\1aÕ3üTnOKð~­\9d²¿\ 41ªÇ~\92«¼]ÐÙ\84ñ\86V\85ð¼\19\9aE<õÏa\88p`&\1fjkre¾âåKÌ   ¾Ræé\e\9d\ 1\0å>ÍU\ 4Öf§iT< [ò,\ 4½\84
+(ôØÍÅ\8965Ñz¦A¸\11\8f¢\1cT|³àÆÒxJ^\18C\944MXÏ\8e¶@C\95Ü:]U³«\ 4\8f\82ù\961["O\85#ö0£\9b\a\7fÊ1\91\14#\1d,
+)\89Ïê\8bùDì\1fã®áÍH\8bã\9e_ݧ>¤«[ ñYLAw¥:¥¨\83zá}\ fþ$\ 5 8\83\85\83àÄZ\9a}\ fý¹©d\ 2\ 2»\ 3\9a\85%e×ü\18ís\95ølý~|EòÅq\84\87\90ù)g\9dG\9aM\7f¬cðºq\8d\1f+,uçKîj¦\96\94xB¬\82q=Rèf§fúd\aÆ÷S\e®\93)\8e\84«\eWÝÐÿjY [\\160«Wl\9f\0X _!ñ ôIZ\83nÝ¢8\8cI\17Âl¤\e\14BDº¶eAѯhya\82;ÿ\95Q\ 5\16\rà&É\87\ 2G\1a&"@Þ\91q\ahmäû%Óð\8faïw\ 4í(fÿûã³Ï\10P\97\ e\85»ßAðcGéâçý%\10pú¿°¾\90&ö¿"\89ñÿÊå"ä¬\ 2IAýD\10\88\15ä:\91ëÒ
\8ep\10´u"\84ö
+¹Ñ5\12\16Êt"B\8d\85Ú\9aÈä\12\13©X\16Ì,\11*iLÄç³\10\9bQ\v5£Ø\82B©[\90\9dn\8báB\9fÀ\X¹X\17ÌRß\85^KD\ e½ÐÈ\9a\17ÖKï\ 5_ß6hY"2\ 40T\ 3p÷_@-þ\85\8e'\87¼¿ ô\18\18\83A>\12y\84a |DÒ\10C=\12\91«\18*"g\f\13\92Ç\90ô\ eP\1cÃe>\b\ 3\1d\89\ 4\r3\8cû\88\Ã}f#\91\f\ e\8bÈ=\9d!vÞgpV\86\86Ú\88\88䣡K\v\ eÁ4¼\95¡\86r!2\95ª¡s\8d5\84S]\83Q-±¡¾j6P "!Ö\86y!rêÛ\90;\88Ü \e\1a\81ÈÁxCï\87\¤o\88ÿ\81\83+\1fÂd8ØÒC\18+\ e\10æ\1e\aÓË\81\8d\86¬\9f\1c*ßa\ eÁ=ç`;\87\10\13:8r\97{\96\ eL\92êpwCîu\1d*6ä\88e\87x3~·CC\réõîÐ|\86H\0\97!2\11\ fÅc\88ÜÈCi\18¢|<´,Ì\83¶/ä7\9a=ct!\vÄ\83¹\16B\80=øb!\94Ë=\18\11%s\85\ f6T\bIù`;
+a\9c>\18}B\18î\83­&\84QùÁ\19ø/÷C¼+\1a\ 2\ 6\869Bòçb"\84ª|è\15B\1a\19\88\8274\11\84~x³\82ðÌ \91ä§ç  ¹@¤O\8e\87ª\ 6¹aÌ +\e!ú0ÈvùÕ\ 5\99a!\1acAÄ·5\ 3ñPAdÔ\10Í\13DÕ­\ 41ñ9D\17\16\1f"f~ÀX\8d\13\85\93\82 cP\11Í<\906\16Ñgº\b\11\ eäö0\82;\ 3\893#Z/\10ÙjD£\ 2\eÑ\97@êÎ\11M\bD\16\1eÑr@TÊGô0 r\81Du±\9e-$X\ f\90\94\14\89n\8b\85\8c?\10Æ)\aj\94\0ÊçHT1\80\ 4E\92\18\e\0¹+K"]ÿãì\95D"þã"2\89*úÇQÔI4¾?î"\94\1f§^JtÌ\1f¯\95\97øcK\95hy?f]\89\ 6¶Î\12}öã
+.\11U?\8e\92\97È¡\1f7Y`" æ5+&¸1?\ 4L&ê\1f\ 6g¢ú´Ò\843\81e\13q\98&غ\8f\ 6¼&Ò[K¿Ðù\8c\9b\0\82õN\a'À±\8ffW_\ 6Ê£\9c°&\94\13±¬\19úÉ\9dÈ×\91¹y\82dó¡\0\9f(-\1fkç'*ÉG\8d\80¢l|ÈyAQS|H\80(\14uïÁ^¡ \13ø\88ÜCÑ\95ï!ðDQtÌáæ0\8a\1cE9÷xJx{Ì\85\14uµÇ"1f3{ô>¥\88a\8fv\\8aÔõ\bT¦ØX\8f»»)\ 2Õã\1cì\14±|vö§p*=d"**F\ f\19HEÕè!±   =¼e*Â=\8fæ£\8a\94ó\88êXi\1e×U«È\94ÈÛ\ 1\rV\8c0\ fÖc\85»ò   Za8y0³\15N\91\a+\áðx\90TW\983\1eL\93âá\ 5yE\9eüë\ 2¿ð ì+ª\83G#Á"\16x\ 4Â°Øø;N\15\8bJ¾ã\84c\11Ò;NK\16mkõ\85\18\8cåe\11°;Úq\16å¹#\f\83\16ï¸ã^iQ};\8e¦\16¥¶ã\f\\8bfí8W¶H7·\85iõnÑ\8e\0\17½Ì\8e5qQ\83ì¨t\1cvX#¹\bpÛëµ¹øø\96y\9f ý$]ää!Åú|¡u\ 1("öm\17ÿV\87²ßECªC\1aò¢5\9a£\92.\11£\17Â`«ËÛ\và¦Ãy\:ªÈ\17´¤#ÈöÅàè¸\¿(\89\8e+ú/Z¡ã\15\1aÐ1sÀ(ø\1có\10\8câ;G\8d\1a\8c\86\8e0´ô\vÃ0ç\88\8eÃ8»9®A\8c\8e\9aãZ\89\91\979Nv\8a\91\ 6s\9c\91\8b\91p9NUÆè^9®ìÆÈ£rÜpÇ\b}rÜ\ 1d´,9N$\91\11Sm~%\19\17!GU\9b\8cÞãèê¥\8cF\8e#¬VÆ4\8dã\ 6\8cú0\8eã\7f\19\15\8bã\8aÅ÷Ä\91\83fh\ fq\1cÖf0;\1c©~\86#5ΨU8*_g´\15\ e%\7f\84Ã\17=£rp´ÙÏ\88\14\1cá\ 5\8d½Àq\98C#øç\8b\86#ÀA\13i\98ù\e\9c\95\86é}\83\8ak\1a\1eó\rZ\92î\r\1fø4ê©7\82ßQãcÞ8ma\13\1c\12\16o@8«q¼\e\97|vcG¬ÑU7&_k´Ð\8dJºF\aæ\86âר3<l(\8f\e\18ìáFÌâ¾\8d\1c\99\8d\86n£*Ú¨ë6ôecÛ0¡ßØF­¯6Ð\93Z\e~0º¢6\92\91\87´ÑõãF\9b\8c\11C\ 5ÝÀm6b\9dÝ\18*Ì<P¼\ 1O²á]o\94\8d\8d\eMb#¼¿qNØ8gàh\ 1l\9cèÁ\11èÑgó\96¢EA©)\1aÃH3\84¯A/\88#$à \14G£yÊ8\84T\ 4{\ e\bv\0\7f×pÔ\9bkøkÐ\89\1cGº5j§²\9dÈÁµ\1cràeÜq\94 I\16ÌãÐ;\\86Î8J22\8e\11·Æ\rÆ\91\7faç÷Lk´3\1eék_Â\8bã\91¬ñÅ\aÖØ\fã((á\ 19\8eS\1dSY D±ÕHPB"¬ÕÈ\87ã(¬\1a/SF5¶/\8eúé (°NL55\9c0\8e\16R£}"\98\1dj\18yqð~\1a15qL\89ö:\r\93\8cM\ 3ùÄ¡Å4\1eÁ8ÈóY\96FzÐ\93\86\86\89£áÅ\88H\ 3\7fâPîh¼,qPÊhdê(\1a\81&\87F\1drBùB¡\ 1(ZÐ\98s8*¦{²'\e£ò32/q´ýÄ\85e\1c\15ÅPóÞ\19\8cv\1c]P\99åqÌ{¢õ8Þ|Ì`%Aèf8_ð±\19\1eû\86\e5\83È=\ e\97$ù]\1aG\1c`±âø\a˨D\1c\15\1a3z\80\fÇtaF²'1Ëj80ø2³á\98s\19·\848G-#;1,CiÁÊ\bX\9f2J)\8b2|ä Ä\93a\88:\93\81«áP¤\92q¥     É`²ÇÈè;\ f\19Þ\e\8e\90\ 6\19\11øý\18\b\1c\ e\8d\1eã_\9e:\ 6\85  G1\1c£RÂÑac\b\9b®g\8c5¸X\1fÞ7²UR\91ôöb´Å´\18>\18«\18H\r\87JQ\8c\17J&\ 6wÃÑþ²ù\93\19r)7Ä\18ÎíÃØuÐa\98K\1c½Ö0\1akÅ0\80&\ eM\16Æ\9b\18
+\83Ãñ\b£aù`8T5\18 ÆáÐ-\18ÿ\f\12\f\92%\8e\92\r\8cf\1c\8e\ 4\86Î!\ 1Ã\ 4\88\ 3?Hð2ß_\18r8B;6\1cåº_ô¬å\17.\ eGá¾è²Ä\91m8\1cÕô\85¬CùÂ#\96\vK\817T\10Ü\vÖ.\8e¼°\17\8dµ§\17@/\ eÍóâ\8dXyÁ\91M±OÌ]\88ú\ 5/\92\eÝ.\8eâ]\1e\ 4ü\7f\89£+^(X¦Xº»8t\9a\e\87\ex\11àÇ1ÿ·qx[£B"à#\968>\8d\17G\8d\0ÇQ\vwÔí \ad¦ä(¤N94ÞÅ\1d\97\83\8a-s\18î.bçæb¦A(_A7\0\1d\9eã¯ìBsçº0âæ¨§.ú\18J\17à6\87>èâ\19\1fç\82\91ÍÑñrÑí6GÑ\15É=\eP\T sx6\\10\1fs¸       .x\8c}\vg-G/y\8b\81\8b\v"Yp\v\19-Gµó,\a²¶\b\98f\8bkNl±xÍQÛµØè\1c½Ç¦Ð¤Z\98M\ fµÀ÷9TL\8b\13C\a\88ᣣq<o\ 6\141\1d\9eÏ\82±P\87­§ÒY¤«:Ê\9aEݬ£`Ëë\90\88Y\\8a\1dD\96EÔÙÑCY(^;jG\16;a\80çcQ\0îh\1a\8b\8eÒ\1dyÅ"VÞñD,\ e¸\f\8bE}G\rÂb\ext\ e,
+\f\8fÞ¿B±âQ«¯Ðx<:ö
+\81\93G\1f¯P\80y\94#µ+r(\1aDW\18\19z rE`\98\1eó[qnÝ\91\15\87½\1eÙZq7Ì\1e­´âþíQpVÜè=ÚdÅE\83\8fX±âÚñ\11\ 2+Nuùȼ\8aó\80>
+®âDê£Ï*îÆ>Ú«â2¦ªb¢ûè\98*vÃ\8fn¨¢2ó£Õ©\90T?º\98
+\1dïGG©\90Üù£\0©Ð\ 2ÿè0*´´\10\15öä?ÊAE\v\ 2\90\büáõ§ 5@lz
+ÊZ@,l~b\81@Lq
+¾Í¦pb
+$¡)Â\8e\91)02\10ý\97âë\ e\84\ 2\11AÊ[
+QM\90ú+Ŧ\15¤aJÑzAú\9d\14ê\19¤F9\1cDMR<\99\aa\8b\141\82\90&H¡Â"¤\94G1 \8d£è¿\12b¥cÀ\ 4
+q|Q°©\10\9f\15\ 5+\v±WÞ\854M\14ÊÞ\10žº\90\9e\1d
+ɸ\0¿$C!­ªB\91\e\vi\84P\94@\854\18\14*L\10\14α\90Ì>ó\ 3\8a
+*Ä÷\9f`ÄBÌú      B«.Äß\96Ñ\11C¢øÄ\r3$fO\1c¥AO\f·\fi"O\8c\r~'*´\8e®¾do\9dHzH'jS\86\98Í        :%9á5Â`Ò}\8aM8\9b\86\ 4²\9b\88\10\92A\0J\1a\9b \19kMt¸¤&üg\93&p¨!
+hâ¤n\b\ fg"­"\19\8a\&"Ý\90NÉDÍÂ1á\99\e\92tb¢CI\\ f\88  \98\eRñ%j³¿KXÍ!Qæ\12½(O·Dú0$@-qÒ-K,Ú
+K\fOy9®\\89\ 5\13V¯y*Q6\88\91C~ñ\94h(\87ô*%:0\1a%ÀË\17JLnÕ'1²ÓI8²l\12å!\87Ð\98DÈvÈkIüp¢$æ=#%±ÕCJ\12\8e\1f¶k\\90\84\90\10\8f\7fH[\17\89\88\ 3X"\81UsH\fÙCº       \89Eÿ\902\ 5\89"!R\v\90\10/\11),¨¥\8dÈ\1c##\89<\97#*Ò\12 ÜÚ´\1fÁØD8wDÜO¤MGÈ8å\88\95F\8a\b}77Â\14\15\11mhE\1a\ 2\ 51c\91m\1aqyÀwFhÓG\ 5¶PF$m"\8dò¤i\1cFÈ/"\8a\86\8cÌ\99\86if\ 4\89EØ,\1a     ³"úW#é+\92i#ê("¸72\9c2\8ex}"¸ç\88+\1e\82\80\1d1Y"\88\86\91\bêÔ#\1eG\ 4U>âNDPÞ\8fØ\15"¨'\90\18\ e\f\12\9f>\ 4W\85Ä\19\ fAyHì\95\89¤\98C\88û"©Ü\1038\922\rQç I]\ 2À°\92\84\0C\84LIö-Ä \97¤r\85¸Q& _!\8e¤)ÄÈMRvBì½\93t\94\10õ\80\92\b\ 4!\B\94t\1eDûHIÁA\84Ñ\94¬/G%Þ3\b\8eUâò\82 j%¦*\b"_\89\9d  \82ú±ÄËì\11\15Zâî@PÎ\96Ø2\10\14q\89+\ 5\82"A \­K²\v\88\18½äq\808H\19@ÌÛ/iù\1f\96\1fZ\a\13W\7fË\88Iu{oL\826³¢*M
+\a \14ÄíÃ\96óCò]R\15?\L\r0Áp8f3ñ\r\1fÄ\82\9b\9f\ f\831\ù0F\9a\98\8b\ f¤¨\89!øÀW­\89§÷À>£p\ f\89Óg\ f|-\13Óõ@å7ñS\ f\f\87\13\93ô@(äÄ\ 1=P`N|\9b\av\0»åÁ<q\9de\ ewv\ 2\ 2ú#+\9c,\90\0À´\a\0Ð}¿uk\18\84\81¾ÐÑaà\87\9a\8b¤Ã\88þV\92ÝäMrT\1cKÍ"\0\0\0\ 1\0\0@\9f\b:        \94
+\1dÚwÆ\1dç\9b¯Þû~k³·vóþ³¶\17ÿû3çºó\8aGý¿\95\ eµúÞ^½µVï\9dïµùwnûå×Zk·Æ÷ÚÍÿ¾ÿî\8c;ÞÝû[sÆ\95\ eÅùWK5Çx{8ã\9b¯þ»âQÞïö¿âÎ\7fç\98ó½q·¿oÍw¶¹âÑ®¯þxçj\7fí×ZooÇØrÿ«¥ÿb»«µ\17[k«ý\95\ e½:W»õÝýân1¾\1c\7f_íýþvËïÿ\9dç\8aG=®thϸ²3¾\17gl·÷Öv\8fïÎ\19ÿÛ\7fµvwÞ}µØn»uÅ£?W:4ÿ[!=ª¥;{\8c?îØw¼;æ½_­5§<÷û=¿|\7fÞ¹å·s̯Æ\97ÛÛ18\13½ïoÖ<ÿm±õÝgï¿\02\9aßë³í\e_\8fóþÞú»e Xv0þ÷úÍ1¯\r¸o¬w×6×Ï\7fï{ï\8fýå¶²@\12õ:¤\87ö¡¼¶]ç\f[}¢÷Ð>Ôj¤i\ 3\11x\9e ø\8dX²Mê\7f0ÜÅ\90¬HÆÂRÄ)ξ\16¨yÀö$é¦\0\18Å\ e-Ás&ºÞW^¿å\19{¾­¿¾sÛ½ö_oﯶ9W\92F|Üý¥ÜæL-íºÞ\8eÚ\aj©¨ÔI\81\ 1Â\ 3Xù±À\0\91\1e¨4\ 2ÍÙ\80r|õ\8f\1c\1fT\\99\1fJ\13MX2Qµµ\ 1
+Y\95æÊ,Á\13\ 5˱\ 5z\94%ûH\ 1\eÊ/9\96\1aJ\8e¥ÆblW \80ÍD\9b\88bÈa\89d`\80\rh.XQ\f9g8cÉÆÌY~£    Õ\bh\0\8f\92F/¥LBSö\19 ¥HÄgh\9a´Z\e1äL(\84\f¿ú¸\14\84\8dåYZ\85æçÁVßY\12\19\9e&ôj\ 3tBe!\85\0á¹\ 2c_Â\84B4
+\95\ 1`\94!L4óÛLóÖô¡A.µÔÒKédb©¥\13:¡ø¡A\1e\1aä\94ÍD/õÐ:\9aÂ\96îR&\15@ù©B\10(\9efXz©\ f\86fzéî»WK5µ\ f\ròРø¾L»/Ý\97ÒL³ø\ 5\10Y{é}h\90\87V)dR\82\ 1İD'"I\86g\89U\82¯ó\ 39´£â7®Êï\95y\99¯Õl4\89_\0\11­'I³½tòÐ:
+\99°d³\91DëIÒL'\14R\82¬RDKÐõɾ\10}\99³Ró\98jL±ç\9cÛ{ïµ3Rö¹\9fþO¹íÛZl\82:P§Å|×\0@57A\1d¨öîwÆ è¥8W»¹Ç?w\9boþÞ{ëýŤ\ f,1§\0Ðdï­ûn\8bµåYç|¯¾Úîëý\8b¯¿\9dwÏuõ\7fëÎñÕuw¿w\fõuå\9dwÚsö¼wyß\7f¿ßÞÞ|\7fí>û\9ewÍ9×Kóýuo\8có¥Øj|ëïô¾9_þ3ßõ~Þ)î\16ë_óö¼Öýw»½Í\e\7fË1Æ\99k1\14½þâÞiç¹ß\9f{æÝfmûÕ{W~;Í;ï8wößϵ½½âýñÆÝ_\8fy½¹^ì1õýöÌõõ]×ûÿÚñ{»\8aïÆÕ^mÅñ¥øã_/÷6güëö¼S|óöVÞÅÿwªùß\9b[\9dq¯ßß\8eíßWÛ¿±ý?[\7fsÕ¸÷\?çÝoÞsß~Û}kÎ\9dÍ\16ï®ûý|ÿNõÞ5ón÷ì5í¹»üÞÎ×{»\8b;»ñõ\9d\7fL/îþ½\9eßî^Ü«¾Ú\8a~ÜÝmµÕÇ^wÜi+|»ÏwÖuwÚ\8aWÜÑk;ÚQ~;ʹͺj\9e?¶¶n¾;º;\9a¯Ç\98wßóÜíÎk+þõÕbùþ\8bi\8aKq)nã\185WÔZ\9dõµxëî«õÜóh¿;óëoÏ\9dÞίþ\1e÷¯³ÏZ\fE7ö\9ewÜÿ÷_Wÿùõ8÷¯o¶ÝæÌ{Ö\98s̳­º~ÝQëkî(D\83>×@üg:ݼ£¹ÓÛ;õ\9dv¯¡v\865\95\16óèÅ\9eß}·æ\9bßz;ï\9dþ\9com¯Ïþç¬w·¶ZPÆ\7f\r¤eîÖ¶£\k\99Îo\8fkï(ßZFwÿâ«e2å¼×Lï×2ùæ\8f=ß¶Ú\8a;-ã+î(ïwï\9a»Ý\7fÕ\9dí·î®ëÎß\ e{-f½¼/ƶóÛQ¼;ï;\8a\9fú­eèÝ[ßú©¿6×\8bµ\f½:ß®ÃXËhÜåK7îîÝøz\¿\16Û¸Óµk.j1$j»6\8bÈF\19æA¯&ÛÙV\7f\7fÍ÷Þ_Å\14§­R\9c\16ó)\8e\85Z\0alëï4Ю½z\9a;ê)ÿ[k¼kö9o®o§;Æ\9d\ 6Âõ_\8f¿ý\9d\83\1d®1|¯ÆÑ2·ÃÙÞÍ÷θ£ûf_­¯\19ãNËôνç\9agÜ;Ý5\ 6]ýoŽêmë§\19g~Õ\9fWÝѼµî¼Wý;Ê=¯¿Ë\9dr\8a¯Ï9gû­÷Øö\9f?Ö¹ZÌûõÜ{Üï÷Ù_\ùç½ë2î[çìíÕ\\1eµ¶£\90}3]þ Ü3¼~úéåÝãª;Z?ý¸ÓV¿zÿ¯æÕÓ¬;º½ç¹z\9a9θj\7f7í¸£\98jÛ}Ý3¯¿ÓVù×ßí\8a鶸zª/½\19Ûj¯Õ¹bÌëÎ\97ö¼qÕ¸«üV\8d3æ>ãúogë¿]¯þúÛá«\91\1c³PôR\9b\ 2Å\0ÎÿÁ¤\bR\9c6\10\96©óÁdX «\1cW é\1c\13&4æÑþy¶Û請\9c\9dsûïÖ\9cîÞ/®\95¿<\95§­l¿ôïU×ýN[Y®\89^«ò\ 3KýR\v\12)¦»Z-F¿Þ¹Z\8e9®öoìýýw_½ïö\9f{þ¹öúb^-Þ\98cOïí´\fí»Òê\7f÷»{o¿¶âÛwݵó«ñÏP{»]ÿ®\7f×O­çöv\1a(Fy\17Q;\130z£*6fXá¬\ 3\8aªOv\96#)`Ã
\8eK\16\86bé§8\8dúK-§:Å\99òý\9cãÛ3îèý\9ewÊ9卑wEííDj/ÈcпÚBDó<jµ\96çÜ[ytß¾qGq\fEíç½£ÚzÞQ\8c;z«Õ\\1e½¿£\96^Måi \fµ·Ó@Ôv\9d½\1díøæîþ_?Å}óú­½ö~ìéÍZ\8cvz³¦¡¶¥eè½\9d\ 6¢½£8k\19ê?½\97n\ f¢\1ewôÒk)Î\14ãú;\8as×i \8f;º­æ¢V\13Qk¯l<Ï\99÷ÐÄQ\15\v@_\96l\93²÷\83á0dlÌ´LÙW\9aN\19(ónÇÒ^ñJ6ã\9cð\ 3\97\ 4[(26fê5\953Ñ#cc¦\13       \10p\ 3øÊ\8e½2\19¾Øæ{3&\1eG\91C     à\fW\1f9R\ 2Øê\13U¬q\14[#\8b%@ÀíöýJ\17k\1c\19tµû\14 àV»Ú«ïô\9e\v\10°o\97/¾ø\ 6\10\0\v,1¦\ 10o\82Cx\9d\bjll*0\8bF"Qáª\ fN\e0@3&\e¯qa\910$\87\ 6¯sk\ 2\97\92\82×I\1fǶ¥DÛ¶\11P\16!lÛ6    \ 4Ƕ\95\a\ 2Û6\80ÓR\92mÛD\vƶåRÀ\ 1\16i-@¶mÀ\80\ 1\ 3\ 6p\9aÄ\12{M¼nÛ`\87\86ÎÄ]á\ 1ç\ 6¥\0Ù\180\80tO\13\96ò  Á¡ü\rendstream\rendobj\r19 0 obj\r<</Length 32300>>stream\r
+ÑàׯÀH8-Kä\19\82\86ó8Z\9a\a\8a\80j\13ñ\10Q\fpÄÛp8³Â\84\19u\13/G. \13M\ 1gaMªÍ\8bä\ e|@%¢\95áÄ67\19ì_ª^ØÛ\7fÛ¶ïQa\99Øt\v\15%\98áln·m\9bÙ\91ªf\97\13\93\19f6Û¶m \ 3Û¶å\ 4\18×!-¶m##Û¶\r`ª£Ãi¦
+dÛ¶\16ÅÈc
+\16Û¶-\1eÛ¶\91YÁcV\94\95\83É@t!ðÙ#8ø¬&6\ eǶm\90\8a\88\93ÁÞ¶m{5: \81×ñ\8b\14¸mÛDWDÇ\98¶mÛJ\81mÛîG¡4ÀÛ¶m\ 3D\eÐö\9c¶8xlÛÂ\11Ù6Í\ 4\ 6\f\18@\82:\rÎkÖ ò\ 3 B\9cð\9f\ 6"\8a\ 1É£\e\9be(·-\81 \83\18¡VÉ«\94ÜÇ!ø\1eÑD\93-\96\86\1d\8cþl³m\e§\rP\905®@Íö\9f\84\99P
+\r üd¡71ÝK?\18Ê­G'¥Ñ\86D\ 5\80"\19\8a\19ó\vE\f\rO\19\86~0\94ß\ f\86b*\vé(£\7fß\ f\ 6\14Éð\14g!0ÀWú`B\ f\0Æ\17\10Da²\ f½wUOS\84ÉB+ûHò\93\952'G3zP\82\8cCòËlî
+\14ÅqE\1f\1e°ò\v\95`øÉT±ÙË\ 4\9e_   ÊÀÎ5]14\81æB\0_Ù±ú\ 6ð\95\1dP\fM\eÏÄ\95ìÓ:e\9fóS\8då
+\ 3ÆÆòµ\1aecHc\\82a+<e ë\13ÀWvdcxù\19"ÜQ\85\91\ 1\8c²1´5\ 3®!K6fdái\82\9a\0\1e°r\ 4ÀYbbcÉY\85â8
\e38\16\96¦±%û\80®oD\8e&Xù\85\ é\92.\19\96+\13Ôà*\ 3À(\83ÉfÔ\f[#\ 5Í\19\15?\99Ì\15\9a Fö¡æG_«\99¹
+E\92üè9\e\ 3øDúQYHÏ\90Òñ¤,\10\94ÑÙ\18@9Ò\81\90ÊÂÏå\92\85\ 2©l¤²O$él\f\0\ 5Z9\9a\91ì£æGA1lQÙG]²0ÀgR\19ø\ 2\05)\8b\82¯\93\82åGA\13|\99\1e\0\0Ã\93
+d8Í\8f\ 3\93\85 k\96\9fwIò+M«Q£[ è²Ê\8f%ûÎR\8a£\19áûÁL&å\a\92$F\14g\1c°\14\0\b+A\8b] ù:?2å\92\85\ 1Æ\10\0¾R\ 3\8aá\8b\81üd¥    % Ç\11\ 5]ßÌ%û@ç\8c<g"ú?\98\97    \86èÿ`.=K\90Ò{?\1146\vY`èáñ\81%\86\1cËL\8a\váîk>\v\v¶{\90\e\9a#i4\89   c~.9\9aX (zâ\8c\ 4Iâ§\1aÅÎþ`²ÏüÒS\0h\1aÑ{ï\rMZ\82äGÒÿÁ¨J\8a=Öx»è¥\94U~£ê,?úAQEÁó\13G34)+,]4\80\95\f,?Ò\94}åIÿ\aã¢çl\84­F\19\8cÒK¢ã9ÃÐ\ fæ#yNqãê\1cI3\14¿\1a\92¬\ e3¿\98y\82¦\89\ 1EÕ79W¡æA    Ê\0áù\89 iwÔ\8e¡\v\0P$Cq%~*\1a +ÁÓüÊ\9eµ\1f)\18 \ 4M°$ÇðüHæ)\e9'hªf%{o'GÓïã
+,\89"\98á÷÷Ó,C\8e\0\90%û¸dáË\f\89ÞÎýY:[ ¨û­[ ¨\ 5\82.æ-\10t\86²\85Sk¿rÉB\0\80áI\9f<\16\b\8a^érð\14°9[ È¿Vá\8b!6f@1l\8då\88\ 5ªb\e1T\85çGÆÆ\fG32Kð\ 4c_ù\8d>\18\18(ºd\18põqÎ\97+$\9d#YJáûÁ\b\0O\v\ e\b\b\17\8a\1f\rOÙÇÏ\92(Î8\1f\fÌV[õ}\84\1aPT}&0ö%\88ßj\ 4\99\9aO  \9e²ÏjÔ\9c\9fG\ 4K\ fJ`RP\16\8açl¤%Ø¢V\ 4\ 3\80 \1aÀ\19\8aßL\81ºd!ý¢Ù\98\11E0seã\80°\1c=\98\ 5\92Êq\149\1e+,;þû\1d\96d    \ 6\10\f_\fÔ4Á\92$g%Úï\0\8bñ¿óc}'\18\ 3@˼6ã¯ùîZ\80\80\0\ 2øÿ{}uÍÜ\9f-\10\14\eÏsT­BѼ\0h\99÷\ 4K'XúÎO\ 3\8adì3)\ 1nl<ÍÙ*\0\85å\17à\82°Õ(\ e\9c`©ü\ 2ÜPe¦8\ 2³ü\ 2¤LYã\91âRÜ$Åißùi>>Í\10ä\8d°Æ\1aë_-õùÿm=æ\1a[«ÿ¿úwÛ³­¸âQk«÷ûîíïö\98ó\8dû¶\97Ûû»¾»ãÿµ·^\87j\8b¿Î|ßn³¿\1eÿ~í¾ÚþËõ¿¿ÿþûæöwîÿî½o˽¿\1aû­ûïÚîÏmÎ\9bo\7f}Ö>÷«»î\ek|ñÝÕ~ëï½]ïn·ÖÝ⬽ÇÞbî·å¿cÌïÆØ^m{ηoë¯Ï¾w¿õ½·÷¿{¯x´÷J\87öþýæ\99Û\8c=ßöZ¿{þ×úݱç}ó]ñèÝ\15\8fr\8c·ÎØ÷\8a\7fÿÿc\8d÷Çÿöíýõ\9cs\8c-öÜÿÏ-ÇÝ£\96Þ«+!Êq%£1ÆûÚþ1Æ\99ãJ\ 3ͺ\12¢\97oü»¾¿sÛ³íöÞ]A \16W\1a¨­\84\95L®Ö~üq×þÿ\7f¹ÿøj®\11½\14Â\12ÇÌ\83¤\11«\91F±¦òòîY_Ï1·Ø÷}{î:wίÍ]W:\94\7f\9c±ÎvûÞ·ÍXßÜyç·ï«ë½¸Ú­sÎWã½±å\1eç\7f±öÛf{³å\1cãíqþ?{«»Õ\9ckÌ3ö·Ò¡\9cß\9f­æ¼{{­íß\7f[í·|g½íÕ½Zoy·ùëûußßz~óçûV:Ôÿ¾uµ4sþíõyóÌ«í\97û¬¹å¼âÑÛ+\1d\9a=ÎÕz\9b¯Æ_oÜóö\9ck­-Ï\7fç\8d\96w_ñèÿ\95\ eÝ·ë\½Õ\9bcËsþþgÜmÞÕÒßí¾·âQ\9do¥Có¾{[Ï­ßÚî\8d·¶7\7f\8fuµ|oÿsöÿsî1÷\1a_\8cõþ?W\9b»÷ßþ\9f;®vóî±ÆÞc|q¶y_ì·Ï\16W\9byÞ}{oûÿøzk/çÿúj)ï|ÿ\7f\7fÅ£¶Ò¡~soõÍÝçl½íùs|/ö\97S­ý½voíuÖ·ï½o¾\9e[\9fõÅùZ­½ç\98ëËñÿÕónuηâÑ­+\1d\8aíý¹Z]-Åý{ï1ιoß÷Öþ\7fmõÞº[{+\1eµ¸BzTKy¿×Ú\7fµÆ\1cç͵®t(×ûöûµÝVçûuç¸Û\8bõõý÷¼5ÿÕfïoÅ£ù^\9b³×\96s¼«åöëÎ1®xôöËoÍßzï\7fïúnÍû¾\15\8f~Î;Õ7Ûß¹öwã¾uµøú|mîöÛ\9b÷½Úß«³çØâ¯{ý\1dgÌûÞ\15\8fjk­®\96v¬wÿVk˱ÆÜï\9f±½Þê\9c·õÚfïq¶ÿs}±ïys{\7fÆôr¯qÎÙ[Ìq·\9a{Ý+\1e½þV\ 2.rl\95¤Èª§¬\ 5\9e²°ÆVXT}2pÆ"ÇJ
+\18W+Ù¬\15\8a.RÀ\95É>R@5¿ð\94}\95\14`\82²ó\13['h\9aäX\96\95\14`VR\06KVyÊÆU        ²¾\97YM2¶\82\ 5\10\92¬±\8a»ÌæC½\ eÉ|¬®XMòs\81²j\92ªj\92ä'\96`\80S¶Z\99\95U®Dre\ 6\0Åoe\ 6xUWe\95+2\14[20,ÇÊ*GÒ<Wµ\14\14\91"v\86*óS©F
+VU\8d\14/\0¬\ 2\84¥\86\ 6 ¢,P\16¶@ÍCu­/û>Å
+º¾XxkI\92¾\96$ùwI\92%I²H\11d\8d1\16YÅÓ¬âi\964\14ß\ fæA   R\9cf     Àoq\10³Ó#©ò\14Q\rJ°æ­\|\1f\86\8am\99ð\15\9cv°IFr"U\1a\91\f\91Åd\81\1a\19²ç`<k\1f\18+v²\92A\ÇFæø\ f\87ì\80\83%b±R\81\12C-bà\9d5\88^aS ~$\85        ë\18Í     Æ\12\e\1fVù¸H\8a\98\ 4¢DñY\8d8\ 5§9@   ²ð\81² \10\12@ tX\960ñu\8aMF/\ 2\99B~m£       s¥\1a%\91ë\93qve\83\16¬5C1\88
+On\91\b2"è9TFà¶\a\1a\84<\9cb¬Ò®\14X\14®\ 1Îú>\bãêá\9a¥\15§\9dÎ\ 4¿"©"¥jtb\9d\11\89@¥0ú̾ÒÈÀác8\19\>\bÕGà[\9d\ f\12\13°+ËLÌ>\1a2!y¡V©~\1f\9fÔfa¡¦\16"=\94h9l"     \ 6ÿ´D\17Mc\ 4¨¤°\10¡¢$\10DpZ£Fl\11\16R\ 5\ 1á\10r\19\88mU\81@XT\12¬\a       WFx\88\98­û\0\82lb\ e#eÀäp\ 1½(\ e­ðÛ\1c<~\88hP-Dh\1aºÀ\84¤a\15\82¹P\ 4\ f\89\bE\11áá¨\8d¥U2,8ïÀ°Ðra\188M\97<\b\ 3ŦÑ:\85,¯ÐéV$ö     F`*\17\14\fJ\ e\v\14\84\ao\81qZE\16T­Óe\12=(#SÁÅäM\12\88Åf"!"\89\92\8a\94\16\1aà¤t°\90\10#]&þDê\14ùB²£É\83\ 4á\92\96\ 2§q\8f/RX\8c\1c¯0kEl
+§Ä\99\1aÍ .\9f\11ã{á\91\ 3¶©\10\rÌ\16\ f"l%4"\ 2\8a4\10\ 5±\84\ 4ÆfHxØpLÂËÕ%        \e\81G\11Âá\82)\94ñBKè´]H\88âáô\ 2E0\8e\12\88Ó<\9cª\80\1aJÿ\0\9d¡À\vá\7f       á$áPA0\1fD\ f\ 4\vxz\1dD¼L¥\ 3     \ 3´\1c\19ä ì|\8aOfõ°ð¡\80@0\1f\v\82iàã\12#1\8feóBy`åHãÉ |ë)!\92\8aNhÅEt8M\92\0ùt$|·;\15\b\98§HÅY\80\81        \9cÿ\9a\10pL
+\ 6\a§!\16  7\98\rba\ 3\89xo\13Z\1a\93M)4Aj0/\86\bM­¢\ 3\8d\99 ê\1aÖ\f% 9uT­ÌÁy\962\16\10Ædr$¡f8M²*I\8fÑñ$¼Ò\92\b^l¦ð=
+X\81Ã{}\ e
+L\8bÁÅ\80q5°6\98ðáË\18X\84Ä\86!a1\96ÁÆ=J\ 6\8eÜ1\ 6\93\8ehbð?²¼h\ 68ße\ 4"E\97\882Ár\81eºîÒ CÒÂi=ä\90²|"\b  \96\b\95¦b)\11:\ 3\96¼\ fÈ
+\82À)UY½¾\84
+\84\15±T2\r\97®2áЪ(\80@ø¯à³z\8d
+B\8a\8cAÁf<l\ 1\83ýN\ 2Ç\ 1\ 3\8b\0\14¡1\11\98\ e\8d\86\80ÓÉh\82QD\10 à4MÅ\14\17Ø@é\\16\a\94mÁ¡P\14\1aÂ\be À\93?B\0\9cø\ 5¯\8dÂ'O\rï\8f\10@ü±\81\88ñ)\f\10B\9f@`qðÚË xÒã)ð\f¢Pç1*\96Ù#cÉê0l9u\14\82D§s\1a\87\ 5ù\9dk\88Ho4ü
+.>D\15O\95H&^\9ax\1cî\99¸\9e_¼H\84÷È@Ŷ®nµ!¢ÉÂæ2\99Î^t\86¿Ï\bL²M\81ljS \ 1Ô§@\ 4¥\17\92\86\ 3Ýè\ 5\15\9c\13ÍiVİÐÞ\ 5¢È\ 6\ 2?"\1fx}\94\17&v&O@4ϲ\92\ 6\92\1d\fP\ 5î%\90\85;5\82Â\r\12\19\ 4,nF\v\968*\18
+\ e\9fF\1eÙ\1d3Wuq\1f\17nÈ~\9fû\82\94*w\ 2\97Ó@\8f\ fwa]HfwVPY\ 6\8dëd\19£óÀr¾}±0ÆB¶\ 5\98Çê$F\15\93í\10hM$\ e\16\1c&ù¤\88&\12\9eYf²\19,?Q\91Hݤ \1f,&¢ì@¬\9e\ 5\84¯
+\14\80N\95áqB¨\9cÖÉ\97A\15U\r^C\ f\9e¤Z\a׬{\91\v1@Lt\19b\9fº±%"t\f\15   \98\ eÓQz'±p]\a\16\1d\ 3L\15\eØä\89Õ@\1eE4\f\95h ä\98\9b\81\87±+\18Høpð\80ÇH=\ 6\93\16§¤ú\10\85\843\17\9f¤D\85\18$¢f AÒ²\13\1a\89já!\90¤F\8f%\11.\b\90\1aÏÈCD\b\8b@|¢*"\ 1\8e\r\91ol(D@\81ì\89À´ç\12©ÐDð\bã¡Q#\92
+\r\17Ù^¬\80pÚÅd     !\bÛ8\ 1\1c  ,X\1dALME\aRM\89\vÄ\ 2\ 2uÈH\92\99@V ç\80,\10¼óÁ­'>@ä\ 1ë±ú  \1e\8d\92Ìô°h$¡\a\8a\80³y\9c\9e¢ò ðÏ\1f¨ßª\8f­bq<8íb\0;\1d©\ f\90\96\9aI8ÈGÈà¸X F\ eVIôqè\11Ás@\10(\108jÄI;P\13°st\ffÃÁ\0éf\8dÊ \915\1e6èjèÐ~\rUGÁ¡Q\19h\98\1a2\18*Ô@8ñN\83Ó\1e\98РñpG\94\ 6Á¢ë\ 6\19\86\ 2\væhè\8fµ5b«\16É\8d&R\14\1c\ f)X\\ 5L\94àPç¢\813¨½ÄA*\b!î¥ðêp¥\96õ8K#¡\80\93`NÎɾ\84ËA8f\12\ e\94\80jp\9cV\99\11l\í\9c¤ÀÌ\9c\80     ¼\10B-\81\a\ 6å\13\18\9d&\10\ 2\1dJe\10¨\98\ 3$\81Í                t\98ì\11p\MF`#\11,\ 2\15)\88/\10[ض\0\ 5\ 2\81\15xu$$\ 2®Ì\80C u\11-\ 4\\98\86ÉxeH2\ 6§Q `
+F¬ä\ 2\19å7ú\18\vòG0\1c\10F\ 3£«P\98\18Z\ 1bĨÀ\18@\f\8e-y\18¢\83\ 4\rÃ\81\82dÀh\11L\140Ì/æ\19\92ÜØ\fß\90/ÃÃK\1d\83A\15B\18¯\8bn0\ 4ÊÒ\82Á\17\1cç\82Ó>~D."HFlQѰ\10\87 µðà\98j\91ÚJ©ÅBÄ@,\b>Û°0\9dÎ\85\85LA$-t\88%Z\88<\10 \85ø\89ø,4\82\ 4gQ"½2\8b\8a\86Ò`Qð9,\v\b\19\85`ñp$øÅ\ 1\85b/8-\16ºyAð\85ìb\82\85½d$\8b\ 5Ôë±h0¼Æ"ä\91\8cÅìûl\v\ f\1e
+\86UÅÖ©¢q«h Ä6\8fmÀ¶ó\e_\eÈÂjm\ 2$\8aµ\95H'Õ6°M\13\e\84¨ ±õ\ 5\14ÄÆi¯JÊaÛ\f\8c¨mA1;m\v(»°éЧ´\85\18\95Âöá\8dÑ\ 6\91\\12\14÷\816̬CØ\18¤è³A\90\9cg3¨\18q6\81²±ÙR\8c\87̦\89P¼Mre\ 6\9b«rºl¢\ 3\8b\f\8dÊÆiçjE°Il\v\94Í£9\7fË!\ 3¿\95©Û7      Æ\0ß\ e
+\1a\v³¼ \9c\8c¼\81ݼ\9bÂ(f·Jè\9al\90NU7\8a\r\9d$N\ 3\9b·P\90l\95\0\8d¥\rd{\10/\8f­c)\8e\8dÓ¶¨ÒضWç6\95\ 4\vl  \9a\ 1ÆF\89h,¶Æ¶m\e×mÛ6À» ¦\81\18ø\0ëÇ\a\87±mÛ\16ñØ\ 6,¶-ò)\91­\ 2\95\0Ù\16\11\11ÈbÛ¶í<¸l[¦Ül\9bfÐl\e\85\87µØb«      cë.\84±\r\18`        \85<M\86èZ\9aEí<FåÈ\18Hºòb:-¼ñ$¯mVÂ\8c \81\12æ@a±m\ 3\ 6D|\98ä~V%&·ÌIï*n\8b^\16\rD\8fÓ\1c\1fÖGó ð;OBQa´\18\r\b\98Ù\80UmD\1eóC\10\ f±®%-\84  \8c"\81bàqZ\ e\9b~1AMµ@Ä:§\9d,W\98\91\19\f\940\94\ 3Y/O\8bdC8\80M\ 6\f\ 42Oà\806\92¾ºT\18\Ph#¡x*aæ´Bà\ôá\14pf®1\86©\805X\9c&\1a0`\80\176T\18\f°(\ 1\17\89\89õ -Ü\89òpf\ 1Cy8Ó 1U\1d\8b)\81òp×\80ò°ãò2u\1f\942\959\8d¡\ 2¢\ 2¢s:\9dN¨Î·8Z\15\ e[áh\91\rd\ 6b\13±\10`"7³\80j\940\91\8ba&\b3A;\81\99ã©CÁDn\ 6²1\91ë\98p&\13Ê\84\82\84Ç\ eD\1e\90      \8fÍÄF\1dK  \85À)P2\ 299\88\1dTüÙÁ½\1cÄ\1e\9a"\90óAìàA\11È\a±Èìà^Î
+§}pHrj\1e\971ô¡\8046\8fË\87bÀñùP\9c.3\ 2\97\9aÇE§P\1eæ´\14ê\ 4R\80\96Ó<®Æåä.§\ 5{\\17{\\ 3\19ß\13LsêN\15\aæ~\a¬8pD¢âÀ§\87á\ e«30ª8phñq\9a\ 1g\91Z|w\86U\15 ÅwG¬ÚHh 67\93\ 3\r\e\89\ 6\ 3\ 2bs7\12\9b\8d\ 49Ð0VN#;C\835æèI*\1a#²ãÇØ\18Yd\12\ 3qƱp\10\1eA4\bB\13\ 1
+C!E\90e\ 2\1eßyHØ \14\12<Èz\81 \ 6\83\9bL#ÇâØC\83\86\98ñ
+\ 6)FP ©wbV>8\8d\0ôBiSB1Ëßu\16æÄ\17
+E\1c\83+x&\8e\9d°8\vs³@\8d[\ 1\17\90hE0¢(ø\87­ÀãE\8e,m\8bD\820ñ2\99Uõ2\1c{N\fjÁÆAcjà8h\1aúÅÄÉ8Î\89Áfd1q$\f\ 4>g3c14©ï$v^"KYVø­e;¤DË"$øÒ\84Ó\1e¯pÁÀ#ñÁ\98\19E\13b(@\12\ f¼À\ 2
+\ 6±WÁØÉBÛ2\86R\r5\85òzÍ´@\b\ 4\ 2ÙTuLUÇTuj\86Óf,\94\87'5Óêj¦uª8p}PZeª\ 3B¸(\14Jòð\ 6r\99ê<(­2Õ±H\14ÊT\ 5\ 4î\16·Õ-n«[ÜV\ 5\ 4\ 3\8a\81\ 1e\92\16ßÍt\8bÛ\92,nË\92"]\86\e:'Ôv\82ØÜS\ 4±¹\99
+GËf²¡C6t,\92ÉÃÃ\9c&\81\99\12\98)\81\99ã©cYX@- \f0\91\9b\91ÀL\bÌ\8c\80\14 ­¸Á²\99L\83e3\95\ 6Ëf\1e\ 3\r\96up\9aæq\b|\19\92úÀ\1c\eu,%T  UBÕ     \8fÍp\1a\816óà`YGê\ 3-\ 2)É%½¹\85\14(\bd\ 3\ 1¢@A0GOÂ9úÂ%s
+\94\v¦@¹Ì\fx\9e\1dÜ\86ÅmÙ\83\18®ø\ 2\ eîE\12R \òAìq\80p/\a \13Ø\17È\1f\8aη@òË\8cÀ¸cFl.§\9d\9aÇ\85\ 1f\82öCÁi\8e\ fé\8cà4ÆæqÉpÚgÛ<.\9b­c\91|ÌÑ«*3\ 2+d\1ej¦e=®Î·H4lZN\8b\84Ó<¦Ek,\1a@\b÷"á4\8f@\ 5Ê[\Îí\85ò°Ç\1c=Ór\96}\82,S\r\1dY©c\91ÄF\9cLàÃ:,\92èàrZ$\1dsô$\9c6\86\ 6\87'«,\92ÎùPq`ÓÃp\87%\99,\9c 50U\1dKç\|\97Ó\14D\9c\8fk l\1e\17        çá.\fÜ\81»0p\193Ôâ»\1eéS\10È\9c/r!YU\91\8c¯    §y¤Ïc=\ e\81ï\92\ 3\r\16   §iP\9fÇ6 \13¨
+GË\8e§\8eE²iÔ\11h5\89\86\14é2\92\ 6B'd:]2§5@lº\86\81Í\80E2°É\e\89η(\904àkÒ(?\10\9b«å\8d\ 4äà;xxl8<(-Ëi\8c
+\88NGÀ¡ð"w\1c\17R\1fh#\9cÆX\946\a·± x³\bü±\17\a\1e\bU\1cØ1K}>\8cAU     '\9cÆqûÀ6F\16\89\82Bd\96òX®1á±£\85ηtº\ 6\8bD#â4àkâa²\82\89\\8fôy\1e]\83E2\86\ 6¸\8dÅ"Q\bI&ùs°¬æ¡ó-\ fN3±\1e¤e\80Â\b'\ 6¥\ 6Ëf"]FRÎ8\16\89Bì1\81\81ÀD®G\1aЯ-Bm$*\18\\16\9bIÄX\ e\19È\a}h\ 2Ó\98º\b\83\8dÁ-\vfÉi\ e
+^ý\ 1:\18\9cÓN\13\ 3\1dmr\16fD\83\16;\128F\12à4\ 6\9fcqebV6@4§\89\9d\87\98@\85Åã3        |1LF£Á*y        Þ\14(iÖ*up\e¥\84\16Ç^
+ÎÂä4\87\ 6ý \92\v\f\ 6Wpá\ 2É¢ìêĬÜ,0\91\vQU #TÐ\ 2åÚX\14\94å\fU\99X\87Ì\87&\9f\82\17ÚãÍIud\86\85»fÙ\8d*\ eÌi\8b2%v¶Eà\83re i\ 6\fI3°q`\16ÉV)^\ 3\9b\89â5°5ü\83nËÀ,\92MàÄ\90\85\85\96%·<²\10Ð$t9\8d \93\f©\93\f&¯;ÖU\bs\90\91}0É"p\80\17È\83\8a\84Ox¤O\16Ú\16      Y\0!-\8d\10©¤'\964 M­\10ÓX¼\99*\98%$°\95©\9d\914¶ÔH6(\ 4\94
\1d³%ájý½»Ðª\95ûu'}-\16\82\12.        \94~H\88\1e°\bÉÃvм<\ f\b.0\12`\98e\§
+\88\8a¾p"\e\ eðÀY\8aF\92ÉÛ¼Jÿ\9fJ\96\ 26\13U\ 1í\12·Ðj\82\952\91@öôà1\0ÙRg\80U°±ec$\8e\15\8b¤û¬ò\16\euZ\9f\8f·Ø¨c\91H6eYn\a\r\96\8d\14hP\14\10ùämIOìêÌà\8dÓd'\ 6\81\16.Ùt`Zx:\84{aè&\15É\8cÓ`Ù\86þ\0\1d\e71+9­r\166\e«p@9#°*%`9\8f\97\NK&lL\83\ 6Ë\92\88ã´\89\ f\91"\19A<\ f\8b\ 3\97±\9eª`ðÈ\87\9ddwÆi\15\94\ 6Ë®
+üBb\80E^\ 2\eqÊrÛ
+&\11\89\86\ 5§=Py\82\8a\90\v\89Hc$$ì\ 5âÀ!R`\f\1eZ¨"\1e\91ÕF\ 1~\12$*\ 3\ e6|`\10<0\f\8dSÇpIØ\1e\aY\1c\12'ûyÐ\9cö\9a$\80¦ð´§þÐ\ 1\95Î\19§AJ\1f\93Õ\ f\1aAw@=\87JåX+\82\83\ 3aÁi%\91\12*\89\83­\18Èr\9a¾xºWÀ<\91ü? kqàÀ 6¤\12¬b3ø£kÈ©\8eÃä4²7X¶t\9aÀ8Ë\90à\1d\18
+\1d\vv@Q¨Áà\9cv\10\8e\1c« IÐÜ\8fq\16æ¨\0\81c\1f\1atw\98U0\18Ü\9cTNã\90\b
+$\11\ f\97\a\84»\13³²lt\87ùX\14pÚØð0jO\8eÀ\98\99;d|Å\81\94ôÄ\9e¸\86\99\ 4eÉiçB$nN\81Ĩ\e,+É\84*ú¡r\1a\0G\94\r&ÁF\85\89\\96x\12d\87cÄ?
+\bTU}\1c\ 6]garÚ\82\ 3£c'0\98Ò\88cp\r\83\89c9-\ 1\ 2\9cÅY\84"\96cQ\r:¤/\ 2\91×.Ð+\ 2    §\1d<d(\16GEaÔpb\98\ f[\81ÃD%2<\9c6rà\12\96\8c\89\95\8a(%d%Ò¨9-dbS7¶\8eE2ó \\r"\99\88òÅI\15Ñm°ìã¡`\90Q\Ç[&D\17ý(é\89Eð"Dy2 ÊÒ\ 4q\9a¦¡Á²bg[$\12\1eÆ\ 5w<\85\80ç[4\v\8b\ 3k\1c\11\9b\99\ 1S\1eÛ\80\8d\12X\ 1\83^÷<+\8d¬ÑH\90&&\87\84FN(}:\ 6\13ó@ðåª$\97Aa¥ðµÄd\1d§qóa\13ÌÑ\93`\8aB\81Ö\r\95\89%3£        \8f\8d\1dl\8b¤[I.\9a\81\8b\ 1B/á\84       ïá%¹h8Êö`<\9b㠩بÄÍi3½\0í\ 3bid\86\ 3\12  3NjÂ\84Ä ·\ 1Et\82ö\0\7f\11âB\98\11\98kb2\81ljNS\80d¶-ë\98Èý$\13\ f\e\8dø\89È\94\1a1\88Wý½\10¼.\1a\aÍ\ f\8d"(\ 5.sb@\ 2=\86M¥c\9aø\9emN*L³-\92ËÁ\84Ç.\842\17\ 6RQ\92\ 6\9bW\10\r@¶'LP\16®`\80@a `Ài\a>\16Ã$p+m`í\rÝNf\8f\1d7\1fÖZ\1alKóUH\f\ f¬ó[\1aï4]2\81B¦(\14h\1c\92Ll>_\17É$$\99Ø\89êt\91ô\89m\91ÜûÁL´S\0hº÷\83!  k\fQ*\vA,ÙGOó\ 3é\9bÇ\96ì£\1a\ f®(\8e«ª\1a)FÐ,Á\0`YU#\ 5èúDñ»\9f¨"C\98\89¡ 9«ª\91Â\8aÕ$_ç¨\9a\99õ\94µl\0Õª\9a¤=ÃS5R\84 ë#AY(¶@VY®T­ÖÈ\ 2AQÕj\8d\15\16\v\94\99¨4R\88\94¹(E\9dR
+\91È\0\0\0\90\0c\12\00((\18\91\92\91$\84\98í\ 1\14\0\ 4vV&D:>.&\10Gc¡@\18\12\85\81\14Ia\14\83Q\10\ 3a\fc\f1¤Ìé \0\e\fû8Âô[¾\12£\ eû]Þæx&SuÙôLOé+(và ]®\82&Á¥òal1\ 5\9e\94=KmpÞh3q?\91ó\16­\85\ 6\879ÄM\99hk(Å
+êÝYÐØ\99Z\ eEÃC¸\13g\1aúf]\17\92\ 3ôÄ$ú\96-»í\9f\r\0ù.\9dP÷£¾u\19<QÇ\1aØUs¿VÑ}5x\ f\91\ 5·¿\8c¼\10º9\19\ 2\14@iÂD1\1a¥É\12\9e\1e<|FP¥}Öc+Fõ\81\89\97¢+С\14ç\r{ÈÕHO\19]vʯè\86[]º\11øG\17yLÏ\96Pùè"\8aU"o\14bÒëT\88Ð\1c]\f$\88òèÚìû\90ñ*#P]£[\1an\1d0Y¤Md9º\b¾L;\14ÝÚ\v\9b\16\16¬\8a\ 3<ݮĢúNèÂA\8a\92\eþÑM=rY\ 6\1f÷&¡\ 4ÎèYZìQÅ\11¡\128Ù9\88¯Ý5QÜ2̳Ç\92+ò-¦ø³\9cü>\8aéÃxRÌ\e½\81\91d\aÏ\r\ 4p\rçcÆ\83\960?ÖH^\91û/nX\98\87\8a;ôÖ\0Ä¥{*Â\ eS,Qò\8ef'W\9f\88\eÊ`GÅ\8b\b1.ó\1aB.L\ 6\80×E>ÓP\8f4\81þpöt\9a5\ 4\85V\7f&\1f¾PÙÄcj\10¤\1fG\ 33C\94a*¾\À÷(%å"+IÐIJgÉ6©VÑ*â$«<¤ü 1\ 1Ç(\9bkgh\89§\erË©\ 5©¦\10üß\84\16]w\b}r}1\16à-\16\v)ù=¥>\v\ fæ¼\1cÄ\86À\11Öíoy³\97\93\ 6û\88ËÁþ\89mfÞ6ß¡¼(kü¸Dp@\93ö\ 5ÕCV\11,ÖöÆ\83,̸Êkê!\94­p:2\f±M2Ý\94\9fá\83ü_YsH\ 5\8d{é4\9c¬?\9d\83G\13å\a\a)~\1a1¸ð\a\ 5ñä¢lX\98¿rØ\\81Z\15Ú\ e\9e¡\aV\85\19«\1fÙ\12\B\9cñ^\9a\ 25¿          \98TÜâ-\83\88è\0\1f\ 4öâYb}ÃÚÆÿf\97Úfº\92ç}¥;µ`ºÛ.hà»t tE\8e\9dseÅ\9b\12ßÈ\98Ç©\sLéÂx®ð2Ý[8rÍ\85ê\8f$ø`ºï\ 2UöO×^¼\19û\89eºß\9b\91®\8fÆ\ 6\97ß\99®àq+Cp\1c\e\e\98®Þ{vx!\0so9H¹òÌt\8dfmC 3ëÈ\85ÂLÈ*\9f°*t\8cúòJ¥q^ "\18ý¡Ì\0¥X\98\1f\19bST\86F§Zß]dV\ 6Ê=\9e\92\96X\rª\93\8eäÍqÔ+Ca\a-=·Ê×Xe´!Q\1a6\ 3ѧ¥ã¹:q¨\1f\ eÀ\8e[ätú\1fW\80\a\90\ 3Û\UÃÒ¿Ôìt\ 3ª\8c\92KtÆÍëÞp¿q\ 5OYeÄaÁV\98 ^Æ(¥Qr9\ 6\96\1c\ 4ìt3\97\a\19*ÊÂ\v\98:\1eëK=õ\ 4{K96\1a\8a»²v%\9f\19xáÄ\83[]¾K±¤r.5k\ 6·\93\97\1f8\v\b|2åóÙ£Ñ\9f¬_Òäã#\12ã.ò'ÀÿR\16\ 3\0[W\1e:e~\80ð\18,\9fZ\92\98-w\90¤ñ$\91ªÍÌúq\vÑ>\94þM\824\82\ÿ\86ÐÇ\94\9d\ f:\8c­Ø\8c\8cI!\91Ý\92R<\ 4\8e\18Á¬é¬ÁOÑ\9cË¥\v¯ÂpqWf8v\890û³i}\15ðer\e\88Öé\87\ 1È%¦Ñ¡\8c\19\85\fÏHp\9bré\11ðQUí\1eÓ)Â^\88\91tt,~\14\1c½¤\18\81aïÌ÷Û«éZ§À!%¸éhóÒheôl\13\10§\1a°.B¨7|7Mó\9bÏ\b\84TQ\15·æ(Ã\9bà_|_«~\90¯j4ͧ\82Ü^ì¦\9f°ã'\80Å(\eNc^t\ 1Í\8c\19\91ú\ 1@\85xÖ¿À\8aÜFÒÝä~¬EY_\95\a\84\83ãÒÒ\8bµ]Úe!\19¾T\fò_\18±\1aÅ
\19ÑS%Pøe)pÇa\85þ¦\80Lc\ 1x\r\11H\8a\18Ü\v>.\95ë\8e)\10\eò®h±Ç\ eÜ\8fA\ 1dYX¹aN\963\a\97­#Bxqâ\ fÀ#$( .E\8b\a\89ûU\bÔ#í\80\8b\13úÁ\vGÓMá¹Ë]Q?ü«jh\ f±\90þmi¦!ëQƸǠ    y%õ\9e\9cbl¿.,\9fobÒ \7fßîf\8e÷1Öt\8f.\11\9a¥¦è\80ºD\92\8d;ÿ¶2Þ)ÕqpuÏ\19¤CÓ\9c\7fº\fJÃÿûõ´®°xö>òmX\9e\90\98  I@«dá:\1eø~\8b\f(\9ccÚ±EdNÊ+uÜiù£TM5F\80\90\ 5â/¨Odº\8bÄøW¡\8b8bS\9eú·Ãó\9f0coÈO\1d\ 2úR\9e 5#\84\1da\90ß»rU·PBÞ\8d£\1d×9áäè\1atf\1cì#¡670#\92Ý&Í\9d\ f0\8d¥V\ 4ÞOI`\80E=ófÊo\õ\95ÊËL«¯Ibø\f\17ývÿì·Ã\16\83¼ÖwØ÷\8c®\10q_n 8D±µo\18\f\1fø0\90&ÙøãÈ×jP7\83\ 3äTSÌÖÿ\99Í\r¢\93\ 3\ 5\97¶6\a»y}\92\eááÑ ðÁ\98icxÄÑ\12§ÐÈ\9a[·6ÝEPÒíT³*d¯Û<\09\18ï#ÿlT\ 2»A4\1e\8d      _þg·\14\82\16\83éßZ\98\17Èp!9HG\e\eÁn\8fÈÉ\96\9fn»L\8dsÝ¢
+JAë.Üô\18\16¢Á.\92×\8cÝO\13;Í)ô\9a?\8b\;\16X ¨Ä`ÍúÙe\15È_\85!\17³íBJ\87å\82_
+J\88<'ÄC\934³©\ 3-\ 1\85K\ 2¢
+jDÅ%QÊq,l,=Q½ÛöQÉ\9b\96õ\9b>\17·\90ä\14\80Ãhc«ì\7f   Ç\80ÅÐ$ ã,\1f\\84     ÞkzáPC-\83-\ 3l³_^\7f{=Á+\ 6:î·Â}\17Æ¥Ë
+S\16(í<\94XA\11ù\99®¾]\ 5u\9e,\16\98&\15}ëÆªf,³¼Ø¹øè[x! \e\ fs\0\1c\1a\9bQÂÃ\1d´ï\92\80\97\16øÎ\b\1a\9e2/B"°µ\16\ 1Û\87Â\8bgI\ f\9a\8a\ 1{ ¢[Ä\87ù»g±èP8w6\8f\98@Xä\86\0\98\1f>|\ 4ßrÄ×-#á\ f\93Wô~2¾¼ùqH\ 6AB\18\8fOoá§>PÄ    \ 1¨·ù\9aÉ"æa6²n\1cÇw¯\16\89X&\95\87§¯e\87¯\8f¯_4=¬ÑÆìúJ/|I«Dv||Ò÷Àæ`Ë\9f}´ÝÏ­\8a«\1c\89§ã#\97+Äs-iÜÏ*Õ§Q\9c·\ 6¡V\vi\18´\95\82:ï¹sQ\83\1dg]\86^\83Mþò\0ݤð\bQ\1fÿ\9fö?Ðí\ 1\81
+p\97JqJ´\90T-ÒÒ_x\19\93\bZ¾N!\92ý\8aPzWøï\94³\13\8ej\11ó~\1cØÃ\ 6M¤b-× ðHñ\98\ f\93/\aär¡§ÕS\82ü\0ûi\1fèr\9aËPTX5\9aä\eû×|ia¯gqy\85{ëH(HZ5\16²ÿ\11\90\1að·$aÊ5ÕHaHÚ\14í«Éöð@Í\0\96l\8c\9c»×º¹\1cÛú\86 Âã»r\13Ê\9bäª&Ðèa~!Z\83Ó\16¤\ 3·õ½©æ7eFÖ\16\9cÁTG'´+\1a£6\eÜ\82Ñ\94Þ3PîKv`NCVãªT\8d\83\9e\1aéa)a\97Éeß/\92ö%ï\82¬I>Cu\15ÇpùDøÖñ\14ÇÂ
+\9d±\94\1af\93³`P\99ÜÎê\fø\86\1c·\9aM\9dgëiI.»o£bW\97­úx\19\1c\ 1\ 2xËÍ\95Æ\99x\8a+/\99+{â={ÞÀäY\97ár¼: \ 2If
+\93\88½¢ò\9eñì*÷¶Ýµß\8e¡À#Í\9f?ð!o\9e]ôaÃsq7\1dÕ3\88\87\839»}5(aÂÑíÌ8¼<{WL\80ï[\8aõóLu\93û¢\8f$\ 4|\93E[\81e\9bÑke\1döÌdîMR(,\\86¯?RAÎ\ 5\88 E½l:\ 4\16<\ 5¡\96bkØçeH[!c&J%¿\8a­\f6\ 5YÌ!)OqQ\12í\88ÛR;\8dÄ6(¶\13í\9a\81\9cý\11\19¿\96W\1fã,lÇ\9c\1f¹È\98·\eßø~\14ß\ f\ 3\8c¨!§<¤ÿÌ\15\879a(4¯\19ä [&@\ 3X^»{Jä4UyÜ¥?PÒè^ïrâ×ÅkäÏDQÞ\98Éfdå[Vû>\81\8584´×ä\92zG\8ec\84Þ®\92\88y
+vÖ¤\9e\11\ f\98ziægPFS«özò.\16¬\86ùp\96â]\8bvWQ\11äl}]~\9e\9f0hÌ\ 1ß\19\19X²4\90ñõ\82ZB­°ÙçåUD¨
+\15Þ\16Ì xnykQ\95ç\98â\8b\80ùË![\86ÕÁDD"nhí^ò\1f\90ñÜqe\b\ 1\13Õx`ÿêºÉ\96\15Ï\15bµÏ\ fÝz(\98ä   !Æ\1aÜõ1ìÂ\ 6\11\ fA
+\9d6ùÃ\97\ 6\a\12>gyÞ\15Lå·ñÃ\95­`ð¨\7f\84\8c\7f\18ª\93\80b\1dN\1cÒw1Y¨æeP\86G5\1a\84\9bÉM\ e\915\ fè/Û\8eæ\17-d¤\1fýõ¹\10þ\93ò-\ 5¿j\96úF\94Ì9³n&×Ñ\93\8d\ e
+\16\97     \8ar¶÷BH\ e®\1fõi\bßËJRcJd°\19jÚ-°\8e\88ÝÁ+¼þ\14\83Bmz5(:Ú\9f¢¶#¨q\87M\ 5\81L\8d+Èráø¤\ 6yCF¥³Ð%\80!\94Íþ~§$\81´%aÐÅé\93aWE-ÎÁý\18 \1fÅ\ 5}=?\90M\81   ¼L\1f¨3ÞN¤ÖYÑè\0\97¦Ö\896?`\85¶9u\10k+ãc \9aW\18ª\822?Ô©(6Ç\89³\8eW\7fA\82¿\15\91\88¬\8a}ÃZÌ))"rc²Ëù\11+/8\9c\e[v[Ó?\1c\1aIÌ,\1d½N4r:eZtJüA\89\1fX÷&G\80U.M\rùJ\ e\92á[\83Áo0Úâ\ 3Ebïg\97®ë¯¶ï\aD3Öí\95¯\}\9db  )[¤\95*íÛÓ\91×b±|Àà\90Á\1cú!ÆK" ±*ï%2\15\1fG\87Ù¨£\89=ÿô$Óvo«\9c\ 1\10Ô8VÒLx\96¦\91\ 1fÛÞÓ qðix]B£-ë4\8a±\8c\ 3y\90Y å$oþ\1d\19É¡#÷    YÁN\99Ñ)ô\eÊfö\892ÌQ#Ù\v\b\ e|\ 4µ\97\8aû!âÀÑ\9b\18àöA M\97\vü\ fCõ H_\83.#ó\9e¬»'$]ø9ùµå|÷®g#\ 2­5Ì\vä\19\f©ú\14^\f\10Î\839\9b\95²*n\82ý\9c\87éq0\ 5\1d\ fðËåp£Øá-Åü\86tìAù©Éºç8\91â\97Çþ¾:Þ\86Z*vÉ\ 3\8cl\w\8b70ß\11\8bÔH\89Ú{ \88\ 2!\84Ä)Ýà,ä7\88À̹\91\9b\85\a\99\9f\ e\86µÇ\1f\0\86¸\99{\81Úh|h\89ó\16ù[H<7©P¸%Ui$[C\ 1ù~^d(S\b\v\8d*\19t?ªo@ûÁ\96ï3µt@e\bÈ\1e*F÷WÖýl#_¢ÿ\9a\18\99YU×h\ 3\93&\ 3D±7É\18Å9\bnÛo}b\85¿ãkö\86\9fì\ e\9cåd¤ÿ·NfT\1a\1d>\8b\ f   ø\91ÌzÓüß²X#(\17~åô)\8f\ 6Ä\ 3§aý~ò^|~«¦PÖ\96\82     °ê×\91x×ÛÀËo¤gi\eÙr\1fµó,M\87or\98צ®Ör\ f$&I>\rø¸¬¢­pRS
+\f\9e\8añ\93 ¯Oºê±ø>b&×T\8fIuö\föh\0\98\9a\12\a\90`0ø×\8b¬       ¡ÅszÂw$VRÍ\9aàF\ fÿ\ 3\11Ã\1a6Ëì\0cæ>ô\1e\v+\(qvLh\9c\7f\86}D"Ð7#Ry!u\b\16\9c\84$\93\80\9e\a="Ø\85ׯ\9f\10H5i\92Ìõã¶¾<;Ú\8c\øt¬W¯­\91põê³þ\ 6¡ð8Éó6YòêQx¶e\v\8fxQåÈté£\18¤zíìU¼,\14h\9eÖ²Þ   <~\16Ù\9d\94\14ín³Õ\19%-´G~\9c"\87\9c\0¿V4\13ÂõÓêic9X\8e\96\ 1éL\95\ 4Ùp\9fôZN`l\8fì¨N5Ã\1f\8b\83K\16»ÔcÞ+}fûF\9f\97\ 5í\81ª1¨fè÷ýÌ2+'G\v\18\96b\É|\ 4\bµÙiKW-p:¦ëÉV   $âÛ/\8e\ e¢Á³þ\ÆÑ<:\1f\13ûS5\8bQ\9fþÓ\ 48\ 1\81î{.®ÓÄ\8a§\99^³/>ÍÃz¨Î>c\97\87÷
+þ¼]\90Éê&\87êàø5Y\86÷îsce÷Í\r]ãË×õ>£\ 4Ìl\e+"!`Hù\fä¡\1e\18\8a¾|¦\97\bê&GÏÎ\1fC\97}'üË2·×\\99Ðàlë6\15Û\1av·C¾VÆ!\1aô,Qx½ôD+N\ e¬d´\8fbçë\14¦c\8aÜ=2\0dF\17´¥\f\7f78ÚW\ 3I5Bê#ö\821W/vÌó\87Dr\81=¡u¿\15K{©+gíU\8bÐ&éG1c\83d©å)\12n\ 1p\13 \8fãÃÖ\12tN~oro÷×ë37*¶<FJÆ\aÓ-w\8a\Ûõ=\b\bðd\10Ò?\11Fu\99ª\85\rZ\ 6¢\96ÏÁÛ\1d\8aÊ×Í?ÄþÓVp\ 1l\9eb¶ÀÞMC\ 3ý¿-nhÞ<\vk\14*%\80\b\8d6\ 5\ 3ew×\\920·       Æ%\93~(Hx%â{V\8fÒF\e\1f\9btubwâ)r"\99HǼÕMÞ\86þ\98Ò\ 5ÏÅ^¹&*C3\8f\vQÅU\8a\0&\1fTÐÀìÝ2\8c"\9dà\ 27:\8að*\b|Y\1e\97mÐË\8fø\83\1c\11X°Ó\ 5-Ð\ã'(iz(3\9dº\8a\1a\8f;û¼Ëï1ÔØ.ï\ 1\e\ 6ê z¦_'\9aþ íàÙO\13Á¡æ,Èû$ßz}ý:\ 4»\8aäN\ fC\8cÃx\86\9cê\e¥\90\ 3\15]8ÑðTËi µ'zÿq;º8]\96d;.\85\8e\9eü#¨\13¤«Q\1aT\eu«c´ÛÑ?°\9d§\9a\ 41à9\87ý5\11Ç\ 3&ô°\86\96ëà«¿®n\97\98Ç\8b\97ÍxÔí\13É\1fcdí¶\1ah\vuDÖn\10Ð\90k\9fEé¥\83ZL!!G¤XU(íðýê5ö\8f\84\ 5Á\12ô\1c\8b¯cF5Ë3iÈ~5Ö\8fÈèn\867~¤\93\83õ\ 2\83\95Üü°mdÚüÃFsuu+ò\ 4\99(pã`ds\ fWàP\9a­7\8d\8dêZ<:o¯îi;÷e ½°«\12\10\89 g\92ê¿\96L~º©\ 1\96ù\93E(.\ eì\9c\92ò¬\94\16^\86\90X-¼!U!\8b)\1cF"@§YGD¦îiBøÏUµ×2-\ 6Wæ?C<¦    p\9e\80: g£erIwiÕE-\85*\b¯\15\0\vHU[àUW4ðv\89\11ùa°ÝAKaì)|#øþ\12Ì@\9bÓ\17O.
+B[y\ 5R\ 3E=n±n-Zè[0(ýÄÌ\8f\95\0ÚÏK\14D0!¦³é9ÎO¯.6\9c\12Lú÷!Ò\b\952æfúþ\ 3)Îï1AZ\ 3 \9aºÙïÛ\1e¿t?ñJÏ1'\86ÑSf;x÷ÎÑKã`«uâöüç\9c:=¾ê:\96óùCØÔ:ðýF\9eæÇ\92-\ 2Üy+ÑL÷åî\bbì~Ç\82ºÑì\7ftB[ÂZ 0Å\9b×9øä\99\ 1O¨¸JÛ\b\81\ fàSõ³[\18\ e\83(A\1c\88\8f`ÇÐ\1fá¯o\10jr=¡\1aLeï\85É\81åÇùíªDe´\bÞ0ø¿VÒ¿\8dÿÖàú\ 2\ 1\85?\v\8a¯&ÌícG?\81TÍ4*\aàú\ e\10nÀ)h²^\r\88Ôû^\15¹õC±\1c\ 3´  _!xõ\91U­&\1fC"fK\10\99¿\83¿ð°¬íQñsn\95#\16|°¼ñè;º\8f\15\13°UÀ\7f×y\9b0\92\84È\8c\8aÔàõt?Y\ 6¯\17¤\12zâ^QH+Àn\ 1Àߪ\0ùu8\ 2éFDÆ\9bHûiu\8dæ\8b\87¼V\94h=ÆR\10\ 4s\95MåCʬ\ 4Ú\vþù\ 6pkén©»A\82\ 3é¼Ö\f¥Ð\99\83\ 1\81Á<¶¶-1!ü\9f\8eZD»ã}\fÓ\1c
+g\90f\ 2\84\1fÀ¶ <ÊÅ\r­ éD\99¿J\189\91\fw\v\86Z\9a8JìîÇé%ifm\9c×ÿ?&2D±ð/ò,\98/Â4¢¡\895İ}\8c\bV\1e\84\14ð\14\vù°.\12\1aán.ÄL ]\vÂî,@MÿÐ/m²ôÑ·4\16\91      `Å6?uÅq~ç²\98³\ 2w\85Ãä\99o\86A@|¢Äð\91\1cK)=¿¨·Z¤¸´VÊï\85û\f\1eùª¶ÙmÑ´\ f\8c>FSpiÝ\19Ñ\9còRuS&Kø\95\ 1VcC)ÛD@\ 3»¨F \96\11ï\91\10ÁÅ@\ e`$¨e)³®DLK\r\8béåëÜ?\ 6Á\8a\8a
+PIc¦³´ºì(ií\ 1\80\91\9a\f,N\eçKñ\b\179ãûQþ?'þ&õTanÜÂܯ7\193]\ eÄ­dÜl\96©s½+\84údp8Â\ 6
+\8d)òR\8e\b\ f\88²x\86+\99¡ñ©ÔjÎ\81?\8c\18Ò=n\96#
\93\9b\9bZ®?x÷©L8öI!f\973íB¹\99U\8e\r\8f\85×Z\83ÊLNº\ 4ÊÜjÛ\8f§T\ f\9a\85¯¾Îií\81K8#E\1câ³\b¸bÜ\adcµ¸\18D;\15\b\9dÑ\89º\86T¾n´ðe\ 4\86bmV\94J¯Ø&ý\ f)"µ(ݰ_K\14µþ_\ f\91Óz©Ùoe\96-\ f¸éD\15Ñå3u7@X\10íÅ*Õ\bÞÔ!;ãhEÝy\bìõ\11\ 3¨\18\ 6\0\16ôÁ\98Y\95Ñ@ÛÀåÊØ?\rÈf&]\e¥o«;\15}R\v²³é\ 2bL\8f\7f4ÄYÞÑ&N\19\1a.§ÔÄ^ z\1c\11ð\95:p\1af\88\82ó·2Â\ 2R\90²&âM¸É{U\19ud\98\80\19º\ 2HêT\16ý7\90ÌR ó¤þê\8c\88q£0ß-á\8a^f\8c\7fÃ\98ÔQÞA+\88cNÂR\945âµìÖÔ·\9aȲd\86oèðtÇÑk\800!' ISM2ýs\ e\82üna\ 6_\92¯ÑRÑ\1a°Ç³¿\19§er\88t\11í\10\9cQ\81¥\84á\ 1ý\80Ì                à\96ä\8b\9e1"l\9d\8els\18\88!´ÈÃ\1a\b´9[\87ª\8eË\15åqó0Áº\9fo&vr騫p\1cÑ\en0ëÌ\14P¾HòFÇ1P \ fi\90\14Þ\15{\1eàk\92Ô\9c>ó£þ"\§ÛÚÌu\86!\13\92\1f\15\95\88\8c"\1a\8c\14°¶Çn¸5ûóK&pJ´\9dB\83ë\ 5Æ9\8f\17F\89u\bÚÂ\87Ü\95ÞOS7?·\16OÓ]\9fs\90*\ 3/\1e\91Rç(á)\97µ\13Õ.\f\e\13\8f¹|¹OqôÒr\80u'\f¹{~e\99ä[]"¬Ä\1d½ê2JöC\b¼VÉ\8e£¤øáÏ#nìvCTL%Óv\9c¯ëªDç\15Z\9b# ­»\1a·\80\1fîw\ 2±<8ZÝ\91(¾TÏqør{u÷¹µ\94\ 2Cc\9d\90\86\9fW\85,Ö2l\86Ñ\16úú=\0ÂÐ"ëÔN2Üý?gÿI#\1e\14Âåi\vp\96Vçé,ôöÃ\ fMéd\F \14\9c\9b\ 6\18<éz\ 5\17ÊU5Ux\83Û\1c=\1eê«é¶(â³B\12¨Ë¬\e\97¤Ý®ëä\19û\9ek£p6\ 1Áï0B\9cÂgÏf\9b¶QGF*<2c\85
+¿åC4S
+FEN\10\15\84õ$PK\9d¦/rxû\82-SÆ75\1a\1d\b\ f\8fjGx\89¿       Æùæ\9e\88,\vîy«\1e£\11»$îä/ \80\95\8c\9fbu%;\88  @é\ 4=
+ÁÐ\ 5*\8cÃû'©®ûõÅ\17\0ãÿüOãê J\9bð\14²f\ 5f:˧\1c+Dp\17_\9bî\8e°­ä.Lñ¦\96ñ\13\98L\vR¥Ý¸Q»è\v<lb\8b®¸Ä¿ú\v\13fF\17\93ñ\11\149N\8f
+xO\9c\17%\r\ 5o\97\f\90\8e\14\13é\a{\16í\9e\8aجÝY@½;Mn.%N\v\8cÁ\80½fðɪCcÿ\91Ò¬W\a[OV@PúfæÈÚ\13D\97\9e_\85\1e¼©a
+õ³N\89\1aÜÕ±×A\10\ 4l\10¨ Ë\9f§\11X\17óá¼j! :án\12­\ 6FôÖ\8ddË¢      ôÈ(k\89»=\85\9a\8apI\91¨ìÛ-ÃD{:\86\ 3\89áÁ0ybóTܪ\8b×-\aèèqwÕ3\92'\81w+E[ï\ 6+G;7\1c\81\ 3y=QsH\9c\894>*2¦ór\90 \ 6çu.¿\ 3Îv'Ã\e\9aU\80¿\9c¬A×þËä<%\87Û%N§¿Çg\9aa´U.@\ 1²ÉBßû0Ö$M\ 5=㮬~îõ\92§aÁhò\99\1ajGÀ\ 1¤õ©÷X¬È³B\10\9d\ 3ÑÐ>\86̤\8cTªI\ 5D\94ñ\8a AnZ\91'\11·\1e,Mxcö\80\83Ì\8a\9c\8d¿ã&TÎ-ig¶`á¾C27»\99}ºL\18St\rì,U*ÿÀ\1d\802|Ée¿#v\96Ð\9aÿhGñ;\aó\81Pu¿4X\14Ð\94\1e;~Ï#C\10\1a¶0À¼ë\A\7f\18KÃ&\ 5ÙÇ [ Ò\1cT\98\88$K²Ì7Ë|Åï |\90u\8dO\8e¯KÞã\10\98,\99`iE\1a×MCZ÷\9cT^§µþó»÷¸C\92\18Ú\13ë\87=¨CZû·gF©É3q\ 4\8cKL\8c«\13\ 2\11\99à?:ô!²²/ú\8cAüi\b0VR\96lB\9e\1a~U:"II\0\8eX¥G*õ-0½_>\90\88c\15°{©\92U\9e\18ç\14>h$\87ÑRÃG×ç<      ºÌ¼Í{Öpü¦WÙ\95\80\10\ e?\1f q\1cê¤\ 6Ë\1eIô×}Ò\8d)í¢']\ 1â\10\8e\91yÙmQ5O\99\16C\1d\bÇeWÐu\8aûg\8f¬u×ýÃ\1cûG\18Í÷Ë"\86/ÐtKòF\ 1%ª´å\8f\1a!rÃ_ÿ×\82D¦èÑ´\e\86\ 6\91ÝÌ\8f\r\1c\84û \ 4$\1f<\ 6¡\8a\89?\855Vñ\ e¯ò%Íà5ZdÆØPý\9e3\83{é*ÁTÜ#\18?b\8cÉ_M\92\15âQ\xJfZqñ\92ìi:äë\84BÍþ\85*ûVåöÐûIà7/ùºw±L\89!4Å\17µeL·GsR,äã!\94dCà_\87\10\ e´í>ªpX¢MPÔ\8c÷¹\9ej½nþB©\97RYÞ4Y\ eöÚX§ !± ) ÆÇ\95«\89R|F9<Ppóa9\16\16\ 1É\98\v>2qÅìéþÉ\13 -*ûí\83\¡Ú_¶a\84Y\e#¢%ò~}ð*hZÉscó\ f\12\91-\fð\r\9b\9eòê        !¼^AxGÜ*1\8a8\85E!¹~/# l¨\93\81
+¥h\1a\8dº\aó>"Õ\97ØÁ.\1c+\r÷(§<\84\1cµ\f\1fr\14é\86.%\1fF\16\12\vt\r\84ä\13+ºéÎÆ\1c\8eàü\9a\8fGj?½³ò"\a{\9e\8e\8b3j\93"\89\10äO'5\f\1fS>¸zR´:\f\88\ 1!T¦\ 6pFÑ\ 5ë¶q#þ\1fq\91\85#¶aøÑZK¥%1\17\8c\1cÔv\10j\v\7f
+\88\96o¬.\ 5 P\85T\aòF\r\90v\1f2\18¬Á7¸GC>fS4½\97Z70_!fÆààþ¨>Ò\92§-ü$\92\12¾\v\91F\ 3¦ír\19\19ã\r¢Ý¤Û
\97o\85øÄNE¢»\93fj(Î\84\ 2èd\ 4'WYÊ£ÇD\ 6¾Óèqu?K²+ü\0\ 5X\18\ 6ìf«ÁC®a¹:í{hÍ2M>\89:\96Ü)\97\7f
+o9\8e/\9f\99\\fÆÆ(þ«\98\ e\ f\v\ e\18׸ÐË0´´XUKþìmvÝXÀÎbâÂÒÞ¿\80\e\94\1f\f{âL`¼(\eß½jf[püòLJT\ 3Y\9a!\ 6v¶¯ì$ì\vÍ\r\10Ç\10êD{£÷ï»ÌFÀ\85ÑrÇKî\898l`\87¨,3O\rAISó\r\90³óÇ\ f\7f¹\85Éëb¤xÛ»9®\13#L¦\eµ\13¯;\ f¥L\82\12þ²m\16_¼5BÖ\v\1d\84OLl \ 3îwõ½õ\ 6C.ö\93(a\8cƪ7\163£S\92¯Vëâ^\9b-\9b\a\ 6þ{Ð+q\b\80ûÒÝ\109OBf¨Ô\a!\8e\92ÛÈ\95z\9d-\ 2©\9eFE\16åáÅG0ì¼\9b°\80e4'\94\12.\13É|\8c5\89{«­µW\eT$\18?ÒLôØÐÁ\ 3\94ÎñÖ®\8c\1f6\98\16èµ\vðå¥Dw\992ôeÃik\1e\10e\r'\13ç\90\Dj¸ïI\94Øl5¯ÛþbÝ9,z^éôáúê}ó\ 3\88£\ 5Ù¯«Gbø9¾ô\16f\8f#^s\8dúJü|.CN\ 2\8bæZ    \ 5G\92\1aê\88Öʵ¤\15\14d\eÿr°¿ÉMz>\13\86 ï?\11\ fæÂ\ fÛjϹËðµq{V¦\ 5\90\ e\83\99\1a\80\1a\e%@¬\99Q)\82\bzu×\81{VÄ\9bÌ\13¡Üá@xRÁqlÉò`\ro\1aa\17üá\eZ\9cn%'\83Õùå\a\f¦m\86,Þ±:}ä\8cØUR\9dq\0Wkq\9d\8d\96ìi\12¢\92\9aâvÇ}"l¸\8dð׿_¶µÑÊy!@\ 6¹\17q¿&!¼1Û\99#¹°Û_\8f©Ýýéñ\ 6\16ëà<\8c\v/j\92\88\9c;RÞ\8eI\8d\10µÜ³Ñ Õ&\ 3JºÇÈ\87Æÿ
+ÿJâm\1eer&
\89\8bìzA¡Èr8\f(\ f\ 5ò=a² H"änT?\11Òq|%^;\1f\a$ÙØÚlÒ\15¡\ 3ò7\ 6\85H7\ 5õ©\1a.T\12\116Ña"\18Z%\86zZ\ 5\ 4É\19\ 2»\8b\97\8b]r\b=d\84Êõ\91H\t\9e\95­'V\965¬ÐÆ\81v\1e®iåK\10\88E\bË©B\96®²\9cÎÙuf\ e¨¡pàÖ$\86Y©²"¥Ç\7f
+Ò|m(À\ 2r\9dfÈT\80/ëklxÛùî<=\1am{\19\8c{N\80\væ\ eäü\r¯E¶dè\93¨G\8a\85Ó ¿»G¸«\ eËÛÐG´ý´ël\14\150\9d°#oÑ\ 4xb\17\85\ 6ëêrWkKE\83\ 5V\9a\ 5Õ|\9eI=H\9f
\88\9eã\f\930\90SÕÈZº%O<\1fÃÞ\ 3!|,²\1e2§I3Ç\90\8bSC8\8e\12\98HáV\96uþK\92,\97\84\10Ê\14-\\1cÔ\12æ5\8f\8cÙ\ eßÍF*\15)\9bx\82ø\9dÿ\0eaXÄr\8b\9b¢\1f\95ÂØ©Ñ\ 5îÊÜí\8eOI\\aÈ\86\8f\17Màò\91u0 FS/ \ 5oݱ©\95\8cP\ f÷ÇOß2JÑ>ZÉ\89\13>n>ýQ\ 4\99z`\9dG\17£¥\15åå?ãYQð
+ï+dò=¢U\ 2F\140L(ÉððM\ eùR\15¢nþ3\8eI)~KQS%\ 2p\v#8Oë\7fÚ\eIÊ8\15¯\0Y\95ºq\9bÓä\97\91ó¤Ë\83çN\ 3jÒ¥®$4¸×\v\8aïù\f\ 4¡V\85ý\16\87\82\ 5¬­\85GßZ\ 1¡¿+h&ð¯¡c\9b33øC3Q­²»Û\87Cî!Æ\15_97~\99á;\9añW¯¡\9dÄ\80Ùcç\94\9eeåíAßï¨Ú«*\86[èª-¤]y©á\9eÄj)ÖÚ\1a1RXÂôSâ"­¥\9c®{ZDÉ\88\18I=\8d}0\1f±\8d\1d+ù/\1dIhX\ 2ù\19Ö©á¥E\82QhÆBÉe\f\1a«ð¨!\rZ$Wë\r&*ûýÌs\f~ùà\80\888ýPN\0\9aì+j ¡\9f\ f¬nbÆ\88\1f\7fq\13~\18\8c\13Ð;X»¿\94ÐF\ f ëaܽÍY\ 1¢Ì\akÉÛÒ\ 2Ú<p,/\vb¡]ÜåÐFȨ\92\8açj\90³nâÃð[L/Þ-b¾ü­\8f/\1dðsL²Ì\12#óI\83 \95\1e4P\95\8dcº°\85þ<·\8a¯ +r\8ei\13\93o÷H\1aú³Û\ e\8cÑãT\fUz¸\r4V­$x]\839ô8À \aÃÇ)dpÕ\8e/\13\94\87\95«´1\10¾\18\9f\11Çôä·rP\8aü«Ã\12jÃݲ\e5\ 6\86ýl';ØCÁó\ 5*ÃnÒy1FEîÿ\85.\8d^=\8fy\99\97\ 6\8dKÂ~ï¿n(®Ì\0ñ\81\82"?×q\ 4 ({\8aDa@gjìwwO§   \8bÁÌ]\9d\1e7\91)^\16H\a­ôÙÀ ïLzüë°\8d©÷\92_H{ýyäǦVÊ*?Å\0\13ÐÕ;Ø7CÛ^Xõ?\9cl\8b\ 2h#\1dS-Z\b\10Ð\ 5ð©»\90\11~âEr¹\ 2\9a\8a\1cÚÇç)JW©åKg­\13\86ì\95\8bb»Û-$\9c\ e\83áV\83û\91\8d\b®\ 6\1e)HZê'À³îì(=4I8¿m.\ 1(Ö c B»\r©¶z\97ü«#]\87H°Ìãv\8e©Î\9aª^ØAC%\9d\e\9a3Ï«\a\1c©ZìB¼\\§
+\17L0\f `ÐC\91³Ï'\82¢íW\99{Úørïøö\90ä´\87¸t\10ú\9aH?0\9eb=QgüüµlÖ¾\ 4\1dð\99\10
+\ fÑÏ\11¿#é6 Tä·»\84j\9a&]*ãëS\12\97\1c\87\8f\1eCÙWì`ßi¦\89\17Xz\9a\94ÿ¹û\ 6\85â%¢s\1d\ 2ôHãø]°\12¾\9b\88\99µî\15ÆÁ¸\82üçÜÑÕ\9ez\ 2\ 6m\85&-\99öÙΩøàÒØ]Àj\9ed\97¥^4*Bk'+\88|:\ e*ɼs®Í1Sg\1c\1e\asçÇßýB8\8c±â\88ø\1d\8e±ÿa.\1c\14°1\ 1ZRÀÝ9Ò¨Í2³Jr*\ìÏ\ e\1eö\arC\80Ce8zø#¨öÂÊþ`ðµwÀ\19\atu}/-$e\9aÜú\8b\a\ 6¥)\19«SÓ\91Û1XÛ\99\9aë\8b´\ 3¢*Ͻ\1fÄÓ\ 6\0Å×ðµ\85%k\1d66==ÒD\8a\13\86n¥p\8b±à\99*7\ e9\ 3\0\92²\ 1\fwPñÁ0UZ8Z¡\ 6\8eA>±'«F-\84Sð7       \v       hÄ5`] 6¼q¿¨±\18H6F\9bË\16ÜàS×\8dñ'\17Z=¬ÀZ{KØ9\ f\94\8dúÕQëCZÖ\80\8c\ 6j}Ö ú¾í(\ eø"<Ò:\88±®\ f«\1a\1c\8b/\8e\15h¶¤\0úq`\a\vÚ
+b\ 4}\8f+Oßì_è\e\1a\18r,Óè}ýýrYÕhÓ¯×o\1ð\85u/c!\89edíxu},
\83,\14\12:à[l¸§ã\98q\8dJ\14\1awòC\9fyªBø\8bQÆüu8\10\9b\87\1e½Áb\8eeL\ f_\9dÔ\84    +ÞBê\8cx¨o?ÖT\14`\1cÍÖ¹X$bzVv8\açD³\97uÄàXxÝ+ÅÜ\15\87\8eb\94P8e¼\87êr¡AÞë\87î\13\94C\88,\amå¨ß    \1dé\8ca³RZý¿\89óØ1&Røms§\ fF\87Þ¹}U\1f{>îr\9b[\86h\vºW3eë\rÕߣh\vÐæ\12¤m\f\1c\88äàã\86\84ÎB\13tåx\97kóÝ\10\9f"×˦b"/TÜ\90/\9fü¹\ 2zïf\1c~7¹ÔôÛ¤\88Á¥Ê2Ðú\r_*µë\90\9c\fñ\rKËôM\96µÂÅEZ\95\8ej,ÿâ>\ 3\94)ÑN¶óFKøµÔ²Í¿\14ïîhGôE>¼<RÝü\ fò\95è 0\92O'\e$\99\fG lÔÛ7cÔ«ôgGDEP,-\14fXÞÖýØ_\ fóG\a\89\eXPWA\¹¯g\0ò/¬C¦z\r("p\10ã
+P\b\1c\ 2\8dO=Oª[¹\97\88+^ø_öë®C7\9c\0\1cPuw5\89Ò?\97Ï\80<ÏL%÷¬
+:\88ð[Ç\ fðÓÌ.)¹   \9b:\8a:mYX=\ 4²\920±«\87æÉ]q<ð\87VÍ\ÃJÿ"\92.$w\1f{>d\82°b²î Óü\95\1dØ,Ôõmª\r\11db\ 2ù\ fê&\89t\9bSÀê\½i ¹Â\82aÚ£äÜ\1f\95)\b\9c'%ÑÆÐ¦ªÝYV4\89X\ f&i\12,´mæ6Uj\15Ãð<âÀ\8d.P"\9aÚ¢ÃqÉ(W\92É)v\1d\9bqq
+\ 1,@\8e\ 2\17³\ fé7\ 1\1cL|ëú\84j\14;2@\84\8aøB[÷C¯îÅlé¾¶EöD{W½U\112¥<\ 3ð\ 6;\1cè\ f\11\987ø¨/&<\1dò\14_^q|A        \95D\8a?ÀfÔh;\1en\ 4÷\112!\85\88\937Hõë\87Ok\84©ÏneÁä%à}0Ö¬mj\e\e J\80Ý\b½#á)D$ab\83½58À\11\84;\ 5r\12\80ùTLá'yϯ¨Ô.þ'ú¡ÿF\ 5\9b\ e\82t\86\14u\99
+\94}-\98TãÆE\91\17*\fiÙ³\88\81\84[Iò×â\a#\9eÈVN\ 6ð9¿\ 1G¡TÚÇFßw]Ð\16jæ¢Kâ]ü®Öø\ 4nt|\16ßÁWc²VI n~ÙoJäÔ\ fp´6»\86¼\ eclL@z~I\18|­'\r£B\94ë ü7ôä\ ez\11\9dô?©´oEu~6\9c~¾(\1d°?wÏyY\8ey\9d\98â\ 1\ 3\v\17}þNëº]\81º¢sÞ\ 1MC\9dî×òmaØÛ³\8eÔÍ\18M\18GHÈ
+               \94R\14ÃiÚgg\94\0\14æ&4P\8cïWã\9f±¢ÌÌ\13\1eñ\17\ 6\18°w&iU2È\9cÌÂñ\93d\86ì!Û90;akW½î¬\8a¯\8a·»h\8dJÎIò?Ö_^èÞ\9aÇæy¹\93g¯2J{\85\91~>¼8ü¼o\98hN\7fÁ0\9c#ïÖ^%Áo\9d\1f%2p÷\ 20Ù`ïñUE\12\89X:\83\91aù¥\[Õ\93bÛ¬uj2F´\ 2Jeywk\83\82\82)\99I6UX¨\9a#D!¶NxÞ+2K.\90\17³ÅQ\1a¯t¿°WÔKÎ\9aýUí©\82ÀÂþC#\17\90~¹ !W²#x\96\8c\b/ZØzXȦ\91\8bñ\ 1Þæ\ 5´\7fJV½\13®P\81 \14®¿\9czûúz«Ñ\vfëm8\94\ f\19Nk\82áÖIÏav\nB\81ËF\fdWTQ§\96\ 6\97\ 6\13\81\18_É ÅG÷\0Ã>¼\9f\9d\9b$Óó³«:ö¹'×W ³9&\13¦oº(lE÷Ë\92é\85w\93\ 1áÆà¤ÄÊ.ó\11\81\r\9fÏ\95½_èk²\9a^.¨ï>`ÈÃB?çN/É\ 2\88RîþÒ\8bPU¢\13%¿ñG¦\97W\9bû7*(h\85Ç)L\90\ 4\12\vp\ e\82\81SgÎ\ 4\96%Ô      \12®Þó²Ów\19+æCÛ\9b\8d\97@ïã\82\9c\89\8b\1aV&±Ö²êØ=6nÏÀ\8e\18Ü.QÆ\ 3ÎãÞ\ 4*\88\8aÚÇ%÷¯
+Ûß\80¶¢\9b"\8a#9c;\147\80Û\14¡¥×÷Ý[Ó.N*`¡\98ó\18t2Çú\\82ç+\815\86\båüÐ\9eY\13^ h|Pv{\1f\12\98\99­.ýR ü\b;0\ 1\9dóHC~¥Ì\82 #0CS£"gts¡\1csc£)26Á\91\8d\9c}:£     B\1cà@\89`ØK\10\ 1¡C\9byÔ'\fÌÕ¥\8b4\ 2>]õ=Óz\1eiBºQ\8cG$=¸,Ç\89\ 5áv\1f\92fª¦\18Ý\913tpNÐ7À\1dQå8~/H\97nýºÜ\17eíT\8b`\14\ 4¡\ 3QlLÃ'wVÅ\95^ :l)ÐæAYí\8dÕZÁL\86ͧ$\0ì\17è´\ 3ñÂ\9dÑ\806Í£ÅA.ZÑ·!Ø?Ë\1e<Vc\12/Wÿ\bç\18\9fð~\85ME~s'¤?\ 6|\12\10\ 5¼ª\99õ\94\84VuåfT¡È²:Íl\ 1\9fØ)ã14Ìî\86ÑÇ\0Ä[á\14\búM\bâéÍ3
+`EìD\8f%~\ 4\94Ç\81\a\1f¤+¢\8bTe\10Þ·¹Ñ\aIN´\9fϯù{¨?3«w\ 6£þ?8\91ò§\9a\ 1Ïj5\0þ×ÔL\9e&<4\82\8d\83ðM\98¡\81\eêâ,_hs××\f\ 66\94\aõ\1d|^ôëu\8dèé>Ã=­­ï¤ôVÂ\9f;¡{­ñ\9c´\90\94äû`    ©öQ\1fû\ 1/¬Ù\9e\10NYû¡Î\8d0\85ãø\1cåâ\apV\99TëA\19î)ÒÎ$,³¶e{\ f\84\13\1d8îõ¿k¨ß|8)@Ðv\8c\vD\88\92ºÿ\f\16\84/ò¤§rCëÉNÜ"Ä=\8eë§\11§°\11©<ô\9fø\13\1dNÓdÌGýªõn   \9b\1f³\\94ã«`iÕ?D\88\15\1f\9b\81«qns\9f\9d\a$R_`\1d\93\8cç\17ïÚ¿ð¹êè\95\80\10°VÑy\84\ 3\13iLqá@Ì¢<«\97ËD¿"Ä]üËÈ\ e\19à!ÒÄoyWj\85®x³\ 15\83\88+\18¹xu-X.7\ 5Áy\1a\98kB:KßFô3»-,ZP,Ô\fdÙLîߺÿH\10:¢ê·¥«~ÏËså\15þÚ¸¨\96b1ù´b&o9p\17ßzÊf¥ôÆ Y¾\1e'J\bÃÚ×\94r\16\17ÿ\8b\f[å´Æ>æi]]è¥d\f»þ\9aÇ\9a\86£\0ÎóÒ|µãý#4\87A¿ø¸å\80AD¾O«÷\8cbÚ«8JR«lÍÇ¡·¸k}ñÌ\12¹m\eÖT\89¦úLX¹µyÞ>OäüøÕi\9e\16(\ 5\ 2bh0ÇKö\8cçk£ÐÇ_\ 2\8bA[\ e®\ eÇU/\96\91ÍGSäÔ°[\9dÙE:\ 3\HJdtÅÛ\8cÑ\96\11\10\83§´vë/¸\8f{òg\1fÐþ\82\85ÝÀ-C\94ßT¤¨×*MøiÞV\8dî\ 6x}\11\9bO É\e\ 6\a\ 5d^(\95\19_Ð2\a\98Pe`Þ'ø     Ù¥®m¼9(\93\a\r1¨r¿E\8bÇ\82hU¾\r':s\19´\921ûBHZÒhõ\15W\17\eÝB\15ôò\9d¹\14*y(¤\11\15yéWhæs0ät¨¦¿[¤hÂD\16Súxà8L1­\7fT\81oð¸êÑ\9b\ 6Ó?\9d×X´þYÃ\17\¢å¦&\ 5Î\12bpþiQ0f3%sy¯\8eM\95\1ewM\15¡3ÕáîÍ1\15#`d­\8bÈ\87\1c       \81\15m¯|\85Çka\16z\8fýaNb¤ôÕ\18é\87\80\88ó½\83ê¢\95óK¡\a\9bã¶oY\9d\90\ 2Á¿çüçµ-³as\ fÛ9\v½ú6\9bÁ)ë<¼\90ÌBJ|«r´oEï\98Il\11        ­?Ôì7\10C.\9aò\8cØ?\e\85^T«¹ªQÕ£·¼sâ\89\94V¦É\ 6ðªi*[OözI\f\0h|Qg\ 1\ e      \1c\ 2\9e\bíI'\80À|nûVÅ\90ï¬àuÄë\98(Oè¸\ 6o_X\89Ò\85õZ\88ÕD\85è}\1aîn\ 1¿ú=¢\85â|öæ?\15\ e6 ¤¨æBÓâ+ûF\93\9c( Á\81ÐB}\14<\81KÉñÝQ´Qù\9c\15l4cu^¤P\9c\9a>\ 6w@¦-¸5\8c\96\85¬Ñ(æ\97\19Î\ 2ZVËJ/ìu-OEç
+vJ{äF¸(\19\ 3Ý­ËÁ\12ýµ\82tFÒ\v\995\ 5P(á¥[ÿ7\ e¯\85j\12î6¥^f\88¨\90sYÙè©\ 3\92ë\v\ 4\8agsÝbÃ\18E9\96S=\91»k;{N\1c\1eà\8aJ\9aU»ÒO\93ÿ·Ì?\17¯e\17/\83S7\ 4z¯ådæÚõ¥mºâ}\9c\86ú\8cÐï    §1\ 1¤·#\8b\91
\91
+M«X\85&Ú~ѹ\vm|ÍWòÏT\15æK\14Cá±»¾A\1f:\8bÒçhe\16\9b=\8dÒR\99\1d¶ñç\8e!\8e³Ôºí
+·;µJ\þ\81íx¾\r@\89í¿1      \1a\10:aGê¯D´,{Í3ï15Ð\ eÎX¼`-ËÓ\aîóê-\b\96\94\92f¨ÂE\851ît\\97ú\8bã'\99Ý4Öu\18$ý!9\16¬¬P'\rYl,g1e-ó\ 3:\10\90\18+M\aò\IéewVÄ\95[TS\1aÀ/ÓK£è;\19\9c\88\8aX\97³ÄbHµê9\9e\1f¤\19ðå\8a\v
+î¡M\8a¿½¾Ü¥+´úòG\1f\93\9e6¡QÝ\97w\91\93ÔE\1aâù¿Ä¥\ ezª{8ü\17¼Hzáþkû#Ï\ 23°(*^\95¢\98ï\ 2R\9a\8c{q?Ì\14ó°Ñ°o½C2°\8fhîU±\13/Múö\99ê\91#2¯Ù\a\rX?ÚÃÁ¾f\ f~[¤],ßP\10¬3\r\93l¢ÝuÍv»\18\11h\8fã¿NzÇ\9bW\1fÆkAø\982kµT\9a\83hk±\ e\83\84H\90zF^Wþt3z\9a½\9b\1f\1av\15ê¸t)#\88+\eÌ;ï\93¸3½K:8    çN%\936\/jÄ\19òÑ\1cïO\ 6\94\98ÞÃ÷\98@ëJO\10EoP\ 6z\ 4'F\9a\ 2±ë¹ ï\18O\19ÖÅ\89Dr4«Ü©«\ f-1l\8c\8d>\13æ¶\ 30\1cc\8b\1dô\94²1ÓÛD\83\ 4å\a¤blϰ\9fð4uXI¥ÀÀå¨.`\16{¦º\14ì&_Í ®ªì´¾p\e\88Ø'Ó\84,\81Y\11\9cõò/§\fD\ 1ÛÔ9Ú5Ñy\95æx=-&¥ATåì\8c\97wu`ÑÎÌþ=\13Æ^>&\ 4k\aß\80½!\7f$6Ë'\7f}\80e\9aШq\eY>ú\\röt8\9b\82À\84¹eÏßþMéñ2Ý/V0Ê­\14-ÀÚíIþ3\18Y\17óH£È/GGî.ØØ6\8aF5×a\95(z \9a\89}\1c§\1e\91jfÙZAé\15Ö\1c£+¥¿\ f\ e®\bÙü2\88\94r<\11PnD¨â\92$b7wÜd\92©ô\ f\9cW\801IÉ:\92\ 44Ò¤\7fú\90\8c"½Ä±ñô5äìÞ6{\1a-Ô·\ 4Aÿh5\18øý!ëÁ\1dÅ\8d\b\96\97³\ e:E§Þ\bþJkN\1d:c#XÓÔæ\81QâVt×4eöi©fï5ئf\7fX7ÐZ3Ò\8f>¦§\98£\83låý\92\82\ 3½a\15í·\15\81\97\88?_Ç\92-`\ 1^t\9d\ 2½¨
+\18Òû\võox\82í?¥\90\ 6¤\98\92ôÌH\91,¹H\ 5ÚìÒ\\12\95Ý\92Ö¸\0\9eç:.§\r\89wFFå(Án×I\1f\1arC\12Ä.Ñ\84Ñ\97óò¼dÕ\99R\85Ï@\b\úb³h\82
+îj#­ÛE\9dÁEîehAþG\89²\9f'?\84¶kM¯\e\8b\96ä§^k8ë[@
+'\85²\16\1a±\1f\ 5O\90\9b\86\80q\1c\18\1d\1c2µ÷0à\84tÔ,S\14©\91\ 40\f\86¹µ¤­\94\ 4üÓýã&¦I>iÍÊDP\9aúÇjL!ZÕ¤4j\ 2ÕÙùg<9\19\1aîtÒ§6zz Ìb\15j\vwÌÝ¿)õµ¼ 2\83@
+  h:CÏÞú"w¥*Þx*\8f×7¸\1c-bØ%À\ 6 \ eܯúK\ 2\ 1ôq\99Ý\9f\9fá¿Û\16¡úÀ}÷\b4\19\rÙ\89·OQ\84@,à®øI\102\83\b\9f|ÝÁ[°5\ eå'@®\8c£\eWãýÃ\8f*8älM«®uóÞ«)>Í+N«Ý\8f×2\82\ e\vÂÂPèK%\82Ò\18ÚøÀb±µ×"a\ 4]\e|XcÙï\1dÄ[2\96¥°Êx\84×ò\10·Ô5\8bLO&\f\94Å2Ǭ\93ót{\877Þt#\93º\8ab5\15¢°\1eÚrå\86à2ÓÌæ©¼Çn\91 R\16H½(̧R@Ô\e¸èâº0\¦Dd²L%]|ñâõhl2Çä\ e\85R\ 1Ê5Ý<) \0`X\aÜ¿Ë3\ 2\94\ 4×uÅuG]·zÝì°'ef¦dÖ5\83¡À{j\9e\b^\8b¯\vô\ 3ã\ 3Û\ 3 \8e«pµÅ\ 3f#   $4f\19\10\81\ 1\81\92ÃÈ\b\11\83-D8Í\81\11\18\8cöfàõ\bÍ@\92UN5ú[ËLI²lNW.5/(Íp\1a\84ël\12jÀ©AÈj\87\17ëômÉ\ ew\0½\bG¦Z     N¢\b\82\11útT\ 2\18¬8ß
+\æW[Øò\82à\14\88\1fÆ \aºVC\b6&\vÎ\9d\95ô\116Ô 2 ¥\1c\13ôVÚ­õ\9dS\98bNGÄ\\91u*¯H\91q\88{eóh4ÜÜ\ 4p5±\9c*/) ¥\1dµ\10Ð\9bäZ墱ªRÒÌ¡¸àÁÀ¡¸\93\96CA«*\r9\14\18\8d\100\ 3ë'\86\8faÃÇp§\14\9dà\14±\11§\88á[I\14\8a&Ø&J`\91(EÅÚ\0\f\ 1fa\b\80!À,Í\93Ò<4O\8a\ e\90t\8e\95j'`®±Rí$¯T;Q±`v\80´à`èñ}\1a\ fïVJÁÐ\ 3ãïÓÈ|\1a\ fÑ\96\96ù4\1e\1f\18z`\9d§ñ\0=\f®#0Þaþ\81¡ap\1d\81¿\81À\99\7f`\fÍÐ@`̽è(÷¢£,\1e
+Qw0àr¨Ë½ ¨H\92«"IT\99JU\9fä90\9fäyèP\1dèáP]zH\17¹s\86\8a \87A¯R\eK¡Î)kb\96R\10`PxPÑ\13a¼BáIx(\ 5´\84\87\86\ 1gªÝú9:\ 6Ø¡óÚqNòCã\eV\95\9a¸X\fã'\10Ú\87ÓwÀÈrkæ¬\96È\9c\81*Êb­ÌbÈ\95JùÆ\18A\18\ 6\9a\13J\b\10A\90Ï\89ú\ 4Ô\13\97Ö4Õ_\9a*\1dvgK,\96LY\f§0Ï@\88ÖÂ@Û\10Öô\9c\8bO`fs |F2-èiUÆóR\8dß\8e\100>\82\97\ 6R¢ñ\e\81(ã÷\ 1\19Ƨ³ ×'0\97\13\10,øð\87Z\ 4\19D 9\13\88UEÉ1¿TÇ\82I\b\ 6Ì5\15              \ 6=Re¹\95v9\89Ùr\82ËIÌ\96@\8fDZ,H$î4:´\f¬W\19X?1|Ì\8e\8f,åT1)§Ê\84rªüD\11³·\12¾\95n%\11N\11³\f\9c"f¯Y\ 60¼YÎ&²\89Ò\10        ô\ 6h\1e\9a'³ÁÐ\ 3gn\190ôÀ\8dÐ\ 3+>\8d\968\88¡\aÎX\1dÁ\ 3ch\98\8cë\88\1dæ14Ì"ó\98DÙ@ 2ÿÀ`\f­\84¥¨xAuê\ 5=ÄCG0¢â\13¨2U¦ÊR\8b\98\1aX·±t\eKh%U¡\9c*\8foc©b K¸\92ª\8c%Ы\8d%ÔF¡\[ÂCÑ$<ë\84òxBRD\1a\8c\95½v\82w+è]ÓÁØ­$âZB\95\8a\15e8'¨d:|Ò\8dH\rª\15\85\93ÁÃ.Y\16\ 3*Ì    \ 5\8fEh ¯ö\9c¹ð\13\90\95­\8b"d:¤øf1äN¥¼® \87
+A\1cqØßXíVRX*\984Põ\ 4=8g\ eô\14\87\95j_\96\13É\93\8a\ fÊ\16W6\aÁ\80ø \f``\aÊ\bcJ&?£\108T\8eÔZÑHt&d@d1Q@\8fÇ
+b\Õ\93Xç?B2\12X=\99:V­]\8få¸P\1fÑ%p\e\93aAk\11\fª²º\9e\17O\80\88\9b\1cê\ 6äZ%Q\94\1aø²´\88\9aË\94\91\85îË\9c®DîêT\ 5ñqÄ\95j\19\87\95jo("\v@àð°R­í¤¦µ6\ 1\fÅ\84 ÔçhT,³,åhTìÈ>\ 2ù\\84Ò?¹ææòfÕyä°\91Jn¢\a~Ef«\89*â¨h30\9e\1aßR3$Hf­\1cê.&¨io\17\ 3ëE\a\1a\8f0@/äh赸¡\19\8eè\811\95\0\ 4\16\eãA©#\1d\98\17\81\1dê\8e¨\ f¢¸¶\12*\8a8EÌ\86ªFE\11\86\0S¨:\95FÅr¡3í"{`v-iT$q¬Û\80fDN\95EÉaöÒ%~§O\8düÐ\84\83ð#x\84ã§6vâ\87Q\98ä·0qÂ\8f\8c_%%\1a?\12\82Áø\81ÞÎ\822Àf\18;¨"\ 6!\ 5\94»[Õ.7³Þn\10\b\v\13ËÈ1¿L"¼\1f\e\8c"\ 1o\ eÔ´\e-\98Õ4\9diOѦb±\84§H\99\94D+Ë'£²ls\1f¥Qq1*¡¢EiT´\9d0\81K\94F\11"\e\0I\85\80\ 3      ×\9e\88p\9b\f1£`ÓAê®ÞdM¦É¥2iTÜ´\80JÆ#\8d\8a\91þ\91\10ÕBE;Ò¨92'\1fÊ%G.þ d\ 2\8d\8açæV,8U\82¤v¨»z\ 2\8d\8aÖó2U\ e\10»\81\88ì4¡R\18£ӠçÐ|Â+\11°¨á\95\80\93\81ï\92¡hb.O\84!+\13\0È#¼$\81FE2\93áE¬âa¥Ú\83À\ 1\15ñG£"\ e}\\1f\8d\8açåázZÂ\83\8aæG£â;H×K6#Úà\b×\8f\1f\8d\8a¥(k}4è¡þs4*b\v©¾\85 Ô/_Y6rZ\8bPÊàhTdà$)èýä6¾
+\7f©Ò\82j\1e\8a;BÐ\9cÆF\8b0\1a(è5\16\8b\97E\8dF\99\88J\12UÄEF£âúi\ 2 2H*f3\ 5¡Ê²\12\10\11\96C\91\10mZ\1cÈ¡¸bC\9bÖ\ 69U>ÌÚ´\8aCi\b=pyX©\964È¡.\82\ 230I\1cË´ãKU]rF2mÇÍ\\95\1a¬cÚ7Y\a\13\81Fªò\97\8bº°ìÀ\91".\ e+Õ.F)Z\11K\92«9`P\93\17\bÑ-\ f\87î"gµÕ\b\87 \14\83`6\92Q\ 5¢Þ\8aÄlå.R\18:\13"ÂIF\95|m\89\86\82?tâébÃ\10\93\89\eÈ|E\97`Ыµº\86Ø\14«æ?\a\87Md$rY0\8d£:Úb5Â:¦UqQÅ~Jy]?\95Å´\ 5\9cÓ!Vph\80øu\r.i\9c\ e)Ìëª\ 3\9dªÝ
+z"ÉY×0%°²¸\9d\15ÙÜø¤Vs`³XÊ'c±\r\12¡b%\8d\84\01\97$ä§A0\18¿\nÑÑÄ,\88\1aÅ,襹Gxm\96Eú AÊ11\ 2\ 5®_Ku\13¹áô\aÒ\89e\8f%áÌ¡kp\19`;åV\13¡R±\86\ ez\ 4\9e\82\96εvê\90»\8cÎܺè«u\ 5%vj1®\98ÅJ2\12\8b\85\80(ÂÉî\aI\89å'\1a \b¡\rØÉ\96\v\ e\ 6»ðr)\88\81;]`\84âÁ\19} ÇÙ5È\1a©ï vr{dZÚ\9a\17\14ôÊ\8c\9aöCJ;j/Ú´è\90ÔI;J\8e\ emZ±\80¤\\86ªQq\9cå`v¡jT,U,\98eUF\ fг:ÂøqÀ¡¸\fX§¶Ñ    Sï\9bÎ4\10ÐÜ\\1eAÈVEq"\0E\\86\ 4\8a\ 5E0éDh"\8cbQ\12¸  \81sè&°\89\82]v N$&ßhÕÊ&\84WÀê\9a
+´S]M\82êÚn\v\15K\93\12bR]=Ò¨¨e·rU¬°rS\ 5»\eJ \ÝqQîÅD¹#\8d\8a¥C¥ÜÑìµ\vïê\16\94¾\ 1Sx\17/\14\8fpr\bÝk@H\14È\1dÌ·&\81W\124vx  4*âÙÀÆ\14-¥/0¹:ªëí\9dlWùA\90\1f\8d\8aäßZâ\aLµ.G£âÕlõ-\87£QÑ6n)§L¥\1c\83\90cpA\90\90\8dåëCc\ 6\9c\8cÏȬjv|Ð\ 3½×l\15ôr\8aÃJ\ 5½\9d\ 5Åøt'Áå,/ÈrÌl\88;\bA\12ì\81æ3ûcÜ\87£%¾í\80Û\98\8b\80Òêz\ eoð7\a
+zÖô²ÉÍ$]¢Fdéz\98\ 1=\95\81b|kZÊ\896íZ@\92\ÅèÐ\f®M»xE©ÍD9&xPQUÛ¤J\ eÓ*øLü\ 5N¤Q±õm5\ 2z'\92(!Ek\1aS@
+CF\15ÏìÐ-\ 1¯\9c     Á\bèL\88á5ál\12(EMH0\8b\89²\9b\80\eUsßAäSWé\e´z#)®$°íÀH\7f·»mÌ\ 4È0³ \973XÇßp\ eZ_nº\ 6uR'\94\8f\0ÔׯHä³YIZ\b¢\84´ÆB×\15ô&0U\ 6\aT{]\91f\81ÝZkL\80èÒ\88ê¦\13ÛÆT\8c\ 42V\8d\80@\11]êÇ'mr¨\9bË\15\ 1K\88 Õ\89|§46YKö?Ô!¾\ 2RàzqÀ`\ 6IA#ÇDL\10¢ñAoÅé\ fä;0Dp\86\ 4Õÿf\ f\1d\f¶Zl¯Z(À\e\v\1e\1a\0[BsA\ fô@\ fô\ 4,*è\81\95\13       Ñ\87\8eAM4)kG!©Ä\a%»>ò\¼\96¤\12"$\17\ 4dzh>áE-jx\1d\90Gx\e\ 1\99l\94\88ðJ
+L\91Ð\aÒQ\1f\97\87Ë\9clW&\94¬\ eÒõ\1cá\1aÿö&º\8a¢¬Å\0\17\b\95º¬¾\89®\17ô\0½×lõ­\85T!\bµqK\1fô²,ý±L¥\8bPú3
+àIAÏtÈÆ/yJÔ\8c\1fÖìÂ@a!©\98ýF\995!N\92âðZÀ\0½Ã\96¨©!÷\17\ 4ùåÛÉmüó\13
+0G\80\ 5NÆgèúh6Bã¢\11³3\8ahxУÔ@²üF\bÁP¢T\9d      %¤DA.\7fß\ e¤s\90 £6k<D3¯>\ 1\10Cnç\f5}d7uû\95\9af\ 3ëO\ 5%ɺ\82^Å´}$\ 3¬cÚßÝY\87cгy4j\11O\95gQjà¡\83h\ 1\ 2§\ 40#Ö:\aæZÂ\89ÐÆx`VBè\81I+\13/+ù\94dyS\92pQ\10\12ðÃ¥¾¸\eRÀæX×\9e¥S\ 1A\10ô`2Ö-\80Í W\80­6]\ e¶Ê
+0\19ë\ 6Ó±
\0X9\99·Â\12\ eX+\ 1Ij¶îwg]îË÷©±Åz\7f>3æü¼»Äß/>1ïX\83{Æ·ÿî¸s\9eOËíÝûįõ\9fXküÛêÿ³ìún|w¶\99\97ù?\9f¶ìøZüü´\9cVèç¾Úfί%Ïþ\9cÿãò^¬1ïùr\9d?k~ZÝOþúí÷2\9f\18ãû\16ç·ö$\vÝö·¸ë\13ÿ\97Øb/@áÞ~ÿnû¥|\92.×¹w¾ñÖvc~9é\ 5\8b3¾Ûâ¿Ö\96\9bïÎyɵ}ýý{þ]Þs\178Ͻã\8cÿÞû\97\9f\9d÷\9c]vã½ÿ´åÞ»ð'YÈ\9a?¿9ëî¹xÇoõ[ûûsÞ\8fÏ·\96ÿk[ê¼\vXgÝ1YÀ\16?þ³ìüæû¿qçøË\7f|Þ­1Ö§µÿûæüÖfι¶\9fy?uïÛ>æeçÛ£å|ÚÞñµûä¯Ë}nÛ¯í\8fϳ\9fùËóK\97ßl?óç÷î\9cï÷\8bË,ñ4\ 5ÝOÎÉ}÷c}\96:ë¼ÿ¼¸_Ýû¦\15\19\93\ 5|ïÅ\sÌs.ïß\8dñÞ\\9f{Ûþ6ãKÒ\9d¿&ïK\16ö¹½\0\ 5û-\8d\96\9fV\9f{{\9aÞ9ßÞä÷Û\8bí-³Þ\85ÜOû]ÿw{\96ç\99ÉBþ?»íùñ.à^n®ûåÿ;¿þÝ5ÖçYÚ\13Ó
+ºÜüÞí\ 5(ì÷X¤ÿ\8bo>Ï­o\7f[ÚÂÞûÌ\7f\9fcls¿\9c\9f[kÍïî'ß\7ffÞïYv|\92\85\8d3\7fÎ˼7î]{´Âi{y~'\v=ç?ó$\9f¹\7f'ùÍ\wû\97Û|\92\ 5}\92\ 5»ïɱ\97\8b\14îB#«Á4»ë[Òy¿ÍçYî~ORÎøl4`µ\ 3\rÞ\9d[®Ë\1fð4\85Þµí\99VÈy{\ 1
+7ßVX«ÆR\e\rX\0\a\9a\80Þûí=ónñ½åãÇ\97Ä;¿ú>î\9c\97\13\97V\98®\ 6ÃýOίµ\16_Yí\80ëÇ\ f»\ 1\80 \81\15@í²°\8d\ 6h\85ÕVÛA\80\99Ë\960a­/Öî\93À:Ð\1dÔ.\8d\ 6,AVcé\0P£qÀ\8aáý\1a\8e7çûK«÷¶§-5~~\7f\9f¶ãòÏÍmaw{?óÜûÅúÚ|­Íwk\8bµí^G\ 5z\ 1\1aQÙBøªB£@¹\844\e\92Y\89\14Þ«i
\17\16^KS°\ 5«\v¶P°\85\7fÕ* è½j\15Õ¿\87\ 6 Ç\80xL»ûB\ f\fJM\v3\e·ôIN*}\91£Q\11E@¨Ïp¤\ 1\8aW¨T4\16ºl\13:\84\10\89\0\8c\0\ 1\fÃ\12H08(\14\8f\bäRòª\97ó\14\0\ 3X8&@@>88(\93Ë¢\910$
+\ 6\ 4r\18\85a\1c\ 4A\18È1å\90dH1Èù~#°~ßjm|EÁ\95`®j\b0¥?7=,¢Í`+«B\8ap\7fòÓÒ&\ 1ç\82\96Áº\88²\eI
+Û·c\96|Að\ fÜ\9aª\92*  n\16\17ý¾6\83¹\94\12ÑN\92=\99\90o\17]\94»\12ÂM\b\92\rÁ¶¼L¶ÏÀmî>lÂ34¡)Oip\80\vº®4ÐçW\7f
+@î4Lj9·ÿí\81\12m\9cE+ÓëÑÚ\ f8áºým2yþþ1¸Ô\99§¢d­\aM\vú;À¤ky\9ex\8cȲC1Pf8
+Áe\94\97\16\89Ã\8eSorÜ\ 6²\84\8eY\ 4?lºGP¡ª~xûÑ\b¾\8b\82Z<TUqºéRJ\ 5\91Þ¿ÿ\18z\ 2'LÈ#]\ e~­ü ºd6
+\89\9fï\87OÂù\ 39\15Ñ\8c\90\vp\ e\1eôg\9fçuµ\80h\13T3\88ï\1ck\90G\92N\ 47ÀAç)ÞLa\7fÊù\86T¥e«\9bä·\ 5 \9f#\ 1\ 2Ç\13à\13­»Nu\91\1f\ 6ðöYqHqá¨/\87\8c\19\18ÒÍ\89rézæS};ÖlMï2\91}XN\8c\95`1ö\f.\8bàÚH.\10ÉÙM>\9cÕ\8e®W°\9c\8dyOÛh\ 6þù*E\15\ 1ãz¡ô,Dõ?\ 5bµ+\16ÁÂÌ=ë\10ͱ\ 2\ 3üR©ûÓ\88\e\93åñ;ª\1a4
\98}¨@äÃz)=´yBK`è1ír\9f\eõ7?dô\85\83Q\vm\93©\9f¯^\8fÑyôÂkD^²\891Âê2B\95ZQC\16 \10ùrKdì\19Ã\ 2'BßíXº\99z%ÂлKÉÖ¢Ý-M7D\9a4_\9fø2¬ \99w\10 ðÈ";à\1fPIê!ñ\9f6p\13IJ\12òµe\b\88¶P£»ëf\bê5½\84ü\16ý¤\99\81\88¡c\85ЬëÎu\0u]\89nrÖ\h\9f½\vÔW\8b\87Rh\14¡Úö©<\84X"A´Þ»é¥Nñ\87¶Ôã\87ãÅïÕ\eý\9e\13O\93\90gé4\99I\1e* ×V3\0©û\v\90\94%H\12öª\80\98rmä\80\ 6\18\15¥ªb\89GíÌÐ\9bk¶:ýõ¦\ eðÉ~ÞÄÒß\16O\v¬NxÜ\ 1¤a\92ò7\7f\19r8=Ñ㩦\95­nÚ2F`e,ìp\82¨<öItõ¾D\8a«+\11R"\91÷\1cÊj\1f}\ 34¡(¦\88\e\ 2\9a\89û\14\7f7\13Î\83è´]\rvL\91é\13\1f\7f\9cyùÿÎT6\90à\89÷\17¢ºè½=¹Ci\85åM««HË\93I{Í,­\8b³\15Ò°\97\98é7oªª\81\7f©¹ô\8c%©Ã\17­Yfg\8eª>\rP@Q\ 6¯¶Kª\19ê°\9eÙ:%ô´/Q×/¸v^ËN£\88Ú3U0Ó\8a\87²À´\97ÓQ`|k\1fVrï\82\14äç:<ís\ 69w¢od7\8b·\8c¢ö\1e\ 3ÁäºcßYãíÊ\ 3#ê4¤\96\17\13¨\ 5ÝÙ\88D¸\19\99¡]ÊG|§\ 3\rÚ²áI¢s¢ù´xiê\84¯.Q9:r"n\v\96\r>S.Ê]ªØëÏC¬am¿yºÈÃÉ2z°Ö¢\87JªRWÏá\87ï÷$\84&Ó¹÷t|®û\9cÆ*p(®\1c\0\9bÐ7`¬â
+GáF\ 3»v:x©^\95\8c\9c1Ãùì¼\12#\ac¾~# ¦\80á϶1^Ej"\14÷8-\8aù\f\86 Ô\93¤C2úS\ fFúq¢Î²\11,7\10âÓá)ÅL] dp\99±\9cæÀ\rÛ6Ù­5º\13\ 1-\87¬ÆM\8f%\ 2\8bÆ\14\85%)¼\10o\1fàñÁjÜy@\1fɱqÙÙÓgæ\7fW
+\88Ë\90é\90\ fíw\ 6Ák\rÍ\14´\133°Ð\98\10b8/L\90õ\16æ\9eæy\91LL@÷Îx\92\1e\rÀ\88\99I?îÝ,\96\81\88\92ÿ\86ÑZøZO\84dOçf(¢t\8d\96CãíüѲò\94\v&j\85hRf¢\9bsæ~\89\88\93ÉþÝ1¹r\9a\va\9b\8a"(¢Ö\9ai\85\fùÃS;Ðñ\1c{\87iÔô\9b+J\99%z\95+ÔѪ\ 5\13±ù-ÈÒÍ\12l\ 4ä\ f2\7f\b!+#¾`px\r±.\ê\89`A\8dkWÙÛöºjuE\13$2³jL®
+Ït\9fè\97­\9d¤'\84|¿QL\ fÑ\8d0wo~¼º\88D\16¼Ú\11\92X"\99ñ1íf\95\e\1f.u5Ö£:N\91ð÷Î\ 3øfr´é)¥\9f7\97\89Äu¼\ 5(ï3o\1dýcá8îÛS¶å¿»/ÒáWÞ\89çdÓ\0!y°V&êL\82÷Í\ 3\f\19>$>7 \81ï<¬\13ª"{\93¯\16\a|úÕÈ,©7Ý»$;='+\84O\9e\8dÞ)½Søp\98X¯4Ø\94\ 2ç\80N¯é\9eöÅy ii<v"\8b;|²ñTV   Ö\8f{8ÊÌg±Èlh8òÄ3Ë2\91K£Hs¬ý\92ä"\8a¤v¶æ¨e\8aÜoG,Ýå\88is\85\9eµ\9f6egJBD
+BÕp&/à7j§
+G/Q£$x\8cÁCÂ.ñaW«2%«à\ 1È¡`L\81\ 4Î)´]£\r9cÌ4Ý{¥Ð\1ev\8a\1c\10=\1dm\ 6\846    \8fE\8aL\1eÓ\80Ä®=N\81î8'\93¿7¦`Ë\8eG{\1c
+\12¤ú\8egï¶ÃU>\85:\1d\10$¸²\1dï½Ê+ô\ 2s\98&¶²rÇý=\92BÄþ
+\12\14ë\8e3(\91\93\1a\nDZ¦Î]B\9e\88yôçÌ\1eì¸T@#ÿéñØN.L\9eA«ìx¤ÇÁ\915\ e½\1dÏ\12éñ©?\ 2\7fÇ#ü \95\92\99\1eÚÅtÛì¸)Q\1a23³ãOfuòYãá\8eÐã¶F*ýÝq\ 4U&ú£ÞdÇ\e=>©\9b\80Oðèñ\b~Çõ\81èÌ®ô§Ë;n  ô \11Iùì\8eû:`ºcÜmË\8eGz\1c\185\ ev\1c\8f|\1c§£ß¬\8f\82VTâ\8e»Ì0¯Çû7«ê\1d\7f\9a\96ÝãE\82A`;\9eL¿Çz¼\0={é\8e\86s\9d_\ 1w7Úñ\99\1egz¢(¢ßã:É\8e\8fôx½\14\88ì8B\8f£Êã\0tÇ÷\19ÀëñÎ\99dDÙñ\9b\96ÍZ*Cw\9cÞÿ\07¼Ö\85;n{\e#kd\fb\ frc\f    dظÇ%q\8c!?ÎGHc.Üÿ\18Ã\162bz|]Cë1Æ\1e2À{¼\8d\90\9d\92\8aX2ÖMR\8e1\8eCƾÇ{éÛÆ\18BÈ0ïñD!Lå\1d\8f¹¹K×\9cð"\ 2|ð¸;#\13\0\9dæd]#Ø{\vÉ£óÚm\ 5\ 4¡ÌX\86ê
+\1d\9fë¡h\84Ôr`£Æ#   íù\89\98ÑÞrÄ?\ 6FÜ»ý(d\8a\10kLï0`\ 5R\92ÀP\92\86(\16\81\92+ðUZÖ\96öÐÙ\17"¹n?QÆ\87@ɰæÉ·oÅ\ 1ÛúÎþ¦¡·Gíp\8e\98\1d\8e\16^:O\bE\87¡ºKÑÏcÀ\183è7¼Òk\ 4s¶+\92\86W\89}\ 5=\93ÂË\7fÁ«Çm±àÕq-\12¼RlWëxU¹\ 2\85ýõM0)\e>^ÑÍîù[Á«ÐvÕóðjË`xÛU\8d\81Wd\1d«\9b?2»ªíxµYÚmWö\1e¯@\8b±µ«Ê0#\1dÆÜ\1dvQ2Pwç#*ÑÜ»b½½¼Ò\15Abd\86\17\8a)\7f\89ÿøö\87¿åÁX$´X½`eZ¡Ü'LE£\ 2qÀìD­<î\e\83e{L¤Ùó?4Hº\8d\92\93É;©à._.SåFóº:<:\81:.¯ÔFe2&9NW @\8b¹H³6\80\82ŵ]ÿz\86\81¡\ 4"0J\98lçÐ'RH©5Ó¶\97QBÒØ\17ÎS¾:3Æå­yðX\85ðâ*/g8Ð\ 6\84uÀQöûüâ7¥ÊR\1aZ\ 5\1dÐá:íð\82!'grÆUI\9c¦a\84ìÿÿëÜߦÚÎqiæ\ 2_Õè4\16#4%ó\14\9e\1a}¡,/
+þJt¤|D\8d\9cuò$×ò^â¼ÌãåE\ e\f\7fcÕ¢©VÀjÈÀØ!2¥*Qþ¢;è\82«A²#\ 1ú~;XõåÄÈ\19\9dhÙ/I>SL¢\ 5g\b´½v4¿\80"\0Ð\f¸iº!\10©â?ku\14 @¿øc1¸k\19\92g¸Ö2x\1c\bZ\8d\18Y1\90T\10X\12]àÈøµ\ 1\16\ 4N{õ>¦iºé\932P\1e\bùpE³êPq¢­\0ª\97à"o\9cçx\17×§\86rh\rÃii j»\87U\1eyy     î\88\0Ü\ 3ôZYfª!\8c\19:3æ§_\ 3\84\15/JÃ\15\8f\87¯ãÐ2\aò%õS\8c'\ 4KPvºA\ 2\99GRé³íããÛF+½vÑiþPMD\18\ 22\18TM]\82ºaKW\89¤        9ï·õ\9b\1a3\82ÀNî\f±\8fò¶Ø-*'\ eï\8eb\95Q\12^\11þ\92\97XÁâEø\94\93\8eÚê\88ËÏIL\13\d\1e\9bvq\ 3\a\13Û'ò\16j5±\98\1aïRâqõº·\927o,ÀÆØùj\1a\83u\0bôhK\96°ieAÈ\94^\8a\855>ÿº4¥\r ü^Ò\97Ðiý\1aöMlKöì\152\99\80Ȥ;\18\83åwå¨\85]\92c`Ë#\81¡\ 6Ë\9dT$tÐ_\85A\9a~\82ú\ 5ÿ!L9­o\8d\e«\1f\89ÑO-@EÀì±í?¬\10ÅË-{·|Â\1aÏþ¾ò\14\8a\ 6ë8ßñ\9c\95\11`GB\99Î\ 2ƲÌþ#v£5*\83Íyê\9eÌf:&\f\8fOX¥\1cj\83Þo\0~o°Ê±\1a¨q6ìéâ\95$\1dÊ\1e]â³ñG´ÃGi¯8ðϯ.f@\aCôxêe<9=Ö;\9a«è\9fM{ñ\8d\eÈÜ\10*5\85ª\ 46ELzY\14õ\7f\98!k[\ 5wôÒv\a\87N}¿òj²Là\vF{Ø%¿üGÌ~\94ù\14\9f\1e^õ7ä\b\19\86èp\f¦\81\ eÏ\99± {\98.&Z¼\92â§\1c\10K\9f\1ap\ 3ÍB¤Ã{µ±=\1eµ\14»\11\17s<÷A;wE
+#Ð4    \83õ\86\v×Ê£ÉéÁ;\ f\8e_å2lV\eæg\99ÞÏâ:è\12\9d±\rË\83\17\1a8\1dvÇúÌÐu/K3.E̸£\v6Á0\10»ôCÎ\11\9cæ\89Y\13üdK\90Õï~\8aR¢R+\83ã\9cJf\0\10\18-ÿ\e\97\94
+wÆ\18>e!òÓü¢ÌÞÞa.§c±\8e\9c÷XÀ\84_¾÷%y\157\11\8d»°­â]\9f÷H6K¡\8bàùi\ e!Éê>~\ 3¢.Ïu\0\15Oe\11IæAü¶÷î\8a§lO\9aø>5\19ñ\ 4®9î\1d¯W\7f\8eÈY\9e\1aÇ \9e´+\84L\ f£¡xÚ\7f\1cAïÌ\83x¿\16¤\9d\9d0\14O\0\8eÏ\92o\8a\93u«¹#mZ\8ex\12\9dÌ7Í\9a\1fR<Å\9fo\8cG+a--\86\8e\ 2ÙW¶\9bo^\15°¹¶ÑZ    \13óM\12?+àÖrÁÖU\17ó\12m\ 2\9e\8a9í ö\87\9b{ý­\83?­àsD\86Í.}ä2OHÞÇb\7f\8f8¤Í\83Ì\90±B{µöM1Çä^})\80£BAm\v\81h=«LR@dFqSÛÄUø®°\8e|6;&\16c\1a\b\b;{/äyÈRÝwLâE\9c\8fè\88;;\8eù±øì¹L\16ìB/FÃw\1dó/ü\8aH\8eÔRîÂE:ùD\0\\8f\96\ 6Ì\13_tm7\8eÛj5¤-ôûT\18\8d\92\87DMrf\16Ì\ 6ª&ÎUG{x?ä\ 2éä\15\98\90³¥Tô\85\fa'(&O\7f\99}\14ïD\8c¥_­.ÜÚp%NË\9d\12\1az5% Ó­¾WüZ\ 3\0\ e\a`\vq\82Ñd]¬½A      8-¡\ 2£[\19ÌW\8eâ{è \97þ        ¥C)\9d\13D^/\89\1c\97ØÁ@y=ÜóÙä\91\93Zß·¢8{vÉð\ 5+X=\9bÙðè\1aE k}Ëh\8d\99M\9d7\17¸Î\8b\9f\131m\89\1a\8aÃG\19\ 64Dîw\b\13ûaH6uj\86\b\8d§Ç(\16"¶ô`\ e\84æ¨Uu\eýÅÍ\8a\90} øg~<E¼p\8d§\97ò\9dU\89Æù\1a]\95Ä\82ÿ&¶Ë§Ì\11 \0^
+Òæ6<ÛóxBPñDb\9d(Ãó\88Ëüú\87\8b-oRÙµ\88ìã\14bûx²í   \vE4\9eþ\1c\84\96¬aòö    /ÕxZÄ~ ÜÓw-ã´\87\94%O6T\8dPXÅ\98Lؼu\ 2ªØì\7fª­_\1f\1a\9a\83Ð)¸5Ä\1ckÿ[ð\98U®Ö^\ 2\9eÜÕÙ\e\92ø\ exPí\85¹nB_\10-1ÁÌü×HÆ\82Q
+FR\13ÅØ\85I\83ü`?¸\ 1
+\9b¾\vILÀñöZc\8cÌ\95\18;\1a\12wÐxÙ  k\10çCÀ\1aª>3Ê¥´4\8d\r¶~\9dc\ 2]\876E\f\95gAì½aĬÎÍp-u\ 1\92Ý\15\8c\18\ 3\87/\eÕ,ø
+,Ê\bKïùës\85U®4h\91L׸Q\97/áEcì\18§Ú+ît\93t\1dNp®ÄÛ(|Êð\9c©G)ñÿå\18G\9a
+¯mÁ\\96\9c\10\84¶LÌ7W²ÏFþºã\86\84ë\90.k¹
+\eó}n¿¨¼\81\98\86\11\12õ\b\ 5Q×A)\0e³sÑÂ\19bÕ[q"x\ 1\0\94Ì9ç\ 3³\8e\1e\14Q\8b÷\1dÿ\97\92\90q\1c\95!\ 6ÿ\9e\1e]EíS\8fJDÐI=z\f\94\ 1\18\85¦\87Ït
+o{+\9c*¯°TÝðÌ-È\9bh\108L\97@sçÞÎ\98k~ï{í\85# SH{Þé\8fÃ\91tͼ+Pà\8fLk)O®\v\99îzúN\13Øq\99.ÓBA,. @\eF·DG\92áw\7fVcSb&¹ \84\80ë¿ ÃFa?cõ|íÊ\12\17\ 1ãg
+ñ¾[ö\f\90çB£YO}4\bÖÉeyP¯e¢Rb¦ÆW\14º6m\9dú\1d\a%q~Ï\91^§Ãæ\ 3öLïß¹Þ\1fQÿ3¹;\15Û5tÔf?@ð½'ÞZvÚv\17ÙMw%áMÕ j_«\ 5\82\812\Û²ÍÉ'5è\96¨ªÚª\16W¢rõÑ\\7f\7fCìú°»¶\98Uö B\89²*b²éÍr"N»â§W®³o#9u\14ëj)"\14VX\1c¿Ô\ 5;H\1a\9a)\a6\8d ÝFL¨ý\12­\1f\81ã\b\a±\a%ïe\ 6\80RMËíJ'\86ö\9bÜ¡¶f)§~÷$æ\11½\1d\10ö)4ï\10å\87E¨­T]ÁøÍÈn\ 4Ö9\86\93\87HcX¶oÿÛ\18>Þr³Ë\87\7f\1aw\ 6¡d\a±\ 5)](oÜØ\1aÛôÓ\19òð\94\18ì´pQ¥n\9fE`\ 5\12Í.\8e\98!?g¨Jt\8e\19õ¯ØÏ\97\87\9d¬¹IïOQ\rßУç;\8be^Mm\7fþ¿LnÒ¸\11wÌÀ\15\80Ã*ýË\9b\80@ògãÀô\15\98\9b\14\r\11ʳ iüûe¶IÊ\14Û\92\ 68Òh»Ï\84¥ÅÞl\15,ý«k2\81(\19¨\7fo
\85\83\9dk*ï\10À¿\81\82¸\f÷
+óy\8f\9aéß_£\9f"öÿKÿªñ\8c\b\f\ fö\86\rE%\0sH¬¼\9b2xøWç²BÃÚ\91Õ¿AK\v?ý¢äÕÕÒæñîp-A\fÿR\19¡p¦\7fé\9e2v\85\7fINe\82d\1f¨®µ\f\11ð/^Üù\9bB£¬Ñ\94\9fÜA]\91\81ô/Óõ\9d\91\91Ü®,Wø÷\85\8a&Kê¿~ètuÂ\ 5\95ËúÁ\94\8c\8cûÛ\1c\ e1éà\8fW@EujQMa\ eA\ 6Êm%´\1c\133\9d\87B\11°Ac[¡Ë.cS©©f®Æ\91îÊÞ£eu\f\1dÔwJ\16\v\9c\94\8eë\87Vp \10Aå8\f\ f[ºbY\80h¹¡KÀ\1d×\19´\9dF~aï,Ö\9eÖ4xjæ×ò\13\98@c¤Dá    \98\85mì"U`\8f\9dB\8cÕnE]f>Û.úÂ\fl(G|Ä+é¨am~PYêÌ8U\9e\14m¡@ȤDWàæ\13\13H[Ñ\1e\8bõS\90ßkö\86\91\9aÕ@°}ú\98¥\ 2aZ¹t^_Wêf$¸tÚå¹\7f\17yÁå~»\82wk§LMu\81\12k\8a\ f\98|{^\8aòÜ\90ÚÍ1Õ\88óÔP5\ 6&\1dÜÿ\a\16²6´¡zª\10\96a7Xn\aô°êÕº4à\7f\92<ëMÈÌB\9bÚnÒ\9c餣¶\85Ú*D%\94*\ e¤Á\96\e¦Ûè\93¥\88õ`\86¤,®ÞeZBø`\9c©\82\88\fª\1aiµÀ\ 1      ¡\82\12/\87\1a»D\94\8eyßR:8,\10Ç\11wø1fß\ 6\7f\9d½\ fz{Ó\ 2RÙ9G»\9eïR¯\\K\88"\aßn\az{g\ e\83ôJà\10
+\1eX¯¤^\9fÊÿ\8cåSlR/ì^Yw\91P\84\98x\b\13\ 2ê½ÿ\96õ'\r\9aý\17J|&Ñ5\97HKo\10õ\89¹Ù5é#Þÿc\1cêµf\17\8b\e$ÐM\8d Þ8qªÝ\1cC~R/h²6©6 Z°q\ 3S@)M¤=\9c\94z5®7YW\1dÇ\11Sm¸\9cU15wAÚ\r&\98×hs\8a\86\19\ 2î¤qd}Ñ6\91\94M:¨r'Ó\8b´ejª>gÁî\9f¹\12rWR¸\96\8f\82\13ï·\18\82\9bëE>\8aÅn\19`þt¯\17=\9aþa$vY\1fZ¹ÉÂ5¸øI6Ð"Ú°}\19iB\89\8cÃ\16~]%É\eÕ)2FÉè`çâ\92$eL\91\8a&\84Söw(?ÞxÅf]\8c&ÄÔ\8e%SL¿â\950v,L¨\17\ 20¹8Àj\94\ 5õ>\82\9a%%}Uä´@\rHÇ   ½3^á?áxÌs\11w\8c\13Å,¥<JQIv\11âIP¬(ï{\ 6ìñ\ f\v±ì¸3Åæ+\82ï¥8¥­ ©\ 5\8dò6D~\ac\ e¾ë¨mV\9d¢$Û\92m/_$ùʺ¸\195X\809T\ f\1c'¹D\91Ç8AXv)\v-\8c^ªn\ 6Ĥ\16\12?j\ f¦\83\ 1\12µý\ 2Uú?        ÕNZ\9dJ\fñ\18\7f·%dG\8f\bü£§ÇIC÷mE\03|w­ÿì\ 6Ìv'³&U½è½t\8b\ 5\8cmdÜ}õ:iGD\9cæ\b¾S\90Þ\82\81\7f\870Qõ.Û\88Â\9f4\0W½`ÝÄ_Ы1íjÕ{+4ê\99\93ÎÒ\94'hl\99\1e\9cÃ\15Få¯û$´1h¯òò2\ el?ÀÀª7År}÷O¸|õj0\8cöÖ\99ãªÞ\ egW\b¼zA\8b\83$à&¤A£#VT½\90>×öª»W'¬(»¼CÚÍb\8c'¨ù¿\87Æ\bÛ_q\16\8cZuBã½ov¾\92\10³\86\81       ò\v\8f°G\ 1ì'öðH!\84Îí\96Ñs'95áÐÅ\11aGÄR¹|\81\97CÅxÁ\e\12ô0m\83\99\br)ðv¾I÷)b\16ard\877t4x\88wSIüZEÇü¼ÄÀ0\ 4\ 4H=^\8a\80ØJFø\8f¾òý\8dÊ+æ\19Þ'É3Eû¼mÒ\97ø\91\b\8b\fõþº3µnìDíúÜêi+\8a¬ÈúÊèÕ\12·\18`\8f\86Ã\97ïx\ 3#8\ f\84p\94²\85IAm°\97Ô×\88q\ 3   ¦@°\178ò;g\ 4\81+\90ú\ f#åEXî\ 4\86许|¡\85å|ÃF}\1aÍ.þ\94ÙÝ\19}Ë|| ®À Ëè9`¬\f&§ð
+{cÀ¹\8cmI\9bo?£Tåaý½\1cøÇ|/\ 1¬\8dc\b¢\9d\13\9f{áî^:â\97\1eî\87ywq\8d\95\8aûÍ¡3(v$8«Fb\bí\18ÅvúEOF\ f\ 6Ä\8f¿AÓøR'$¤ë¬³¢\84,)\94¦\94\9fGG\12\7f½~l\1aÞ]Ò\1aµ\99,{Ï|~w\8bý¤÷j\186-Þ­s\86Â/ð°ï®\ 2\7fã\ 5\7f1KÒóÇuêÝ%Ï\11ðµÝfpä\13Ç\97=^ýëÕÃCIº*¶ùü©     \8cðFrÇéÝ\8d´lóìÅ\eèݵÿÝ¡\ 3T:äÒÆ\87GÝÞÝ=³ÀoÀ´c²DÅ»ü/+ß$a\16ù¢\9ex7¨®Ú_æbzw\15D\9b\17\88uÝ|\8d\ f\ 5\19kªô4$ùé2g   N\92o\10\8e2\80Ý\8bø= o²â¨\80!Å«_´\88\83ÅÔ[<[à®Ç\94\ e\13è}&î> \7f[d\12\1c\82\13\88KEm)\0-R~kGÍ\96ÉÜò\86\8bk\19º¢5qQ9ñ\1dsÌÏ\ 5Ôý\12R\9a\99\8a&IM¬I§\19\ fÏ\83v\80 ß¨\8ag\19ú\96öÿñ\a|VÉ\8c\13-kÌ¥99ÂFp\a6âÃ\95ï\88äf\98\ eI\ 6Óê¨mª?&\8b\81\ 6®î\14\f\17Ãu)Î\8aëÝ(üwÒ Ê8ð¡\a\b\82¹*o,ì\87©\805\86Z\8fÁöÌ ´l-\96Â\¢µ\0FYL§\9c\1e½®ÑMXöY÷h³Ñ¡Û¤\19\v\b\8eó\0?ÿ<³¦\e\99KoyÂ\9dÈÒ7'ÚæÇ,s\ 6\18í\19\86S¸¬\8cÕúd\0\88\92A.KiJ\9a@À¡i\96eÎ$k³ß\98k~\8aGs\0´y¾±\9fÂU¡|û\1aëSï\99¶3xNð\94ö©r¼'¯\ 3<\9dr$\:õºÚÕÕ¬Êëh}tÂ=\89­jål&þ#n$¥\89óÎfn\99\9dXÂ{Gö»\85ÜÝéÙÄáeùyõÞ/RÚ\95h¢VtXy\15¶ÜÆã´üé \81Ý¥Þ9¨s\85\9e%j~\14ø\87\92ûØõ\rĶb\búé±\ 2\90îAîî«ø\14\19¿¼\94
+\7fLA5NRh\86È\174    ñzÛ²$ \14q&þS\ f\19¼\ 1fÇÐJ\ f®\r  \ 3\0\ 2®\ 4\rendstream\rendobj\r22 0 obj\r[21 0 R]\rendobj\r32 0 obj\r<</CreationDate(D:20200725234456+10'00')/Creator(Adobe Illustrator 24.2 \(Macintosh\))/ModDate(D:20200725234456+10'00')/Producer(Adobe PDF library 15.00)/Title(Jacktrip)>>\rendobj\rxref\r
+0 33\r
+0000000004 65535 f\r
+0000000016 00000 n\r
+0000000147 00000 n\r
+0000038245 00000 n\r
+0000000000 00000 f\r
+0000038296 00000 n\r
+0000000000 00000 f\r
+0000043346 00000 n\r
+0000000000 00000 f\r
+0000000000 00000 f\r
+0000000000 00000 f\r
+0000000000 00000 f\r
+0000000000 00000 f\r
+0000000000 00000 f\r
+0000000000 00000 f\r
+0000000000 00000 f\r
+0000043419 00000 n\r
+0000043593 00000 n\r
+0000044772 00000 n\r
+0000110360 00000 n\r
+0000000000 00000 f\r
+0000040363 00000 n\r
+0000142712 00000 n\r
+0000038693 00000 n\r
+0000040663 00000 n\r
+0000040550 00000 n\r
+0000039334 00000 n\r
+0000039802 00000 n\r
+0000039850 00000 n\r
+0000040434 00000 n\r
+0000040465 00000 n\r
+0000040698 00000 n\r
+0000142737 00000 n\r
+trailer\r<</Size 33/Root 1 0 R/Info 32 0 R/ID[<0736C03EA6374B77AEB4921C0FCAF2A6><B3D402B8E9994E6585EF6BDAA695FC42>]>>\rstartxref\r142925\r%%EOF\r
\ No newline at end of file
diff --git a/src/gui/alt/about.png b/src/gui/alt/about.png
new file mode 100644 (file)
index 0000000..99d8c77
Binary files /dev/null and b/src/gui/alt/about.png differ
diff --git a/src/gui/alt/about@2x.png b/src/gui/alt/about@2x.png
new file mode 100644 (file)
index 0000000..0b2f82c
Binary files /dev/null and b/src/gui/alt/about@2x.png differ
diff --git a/src/gui/alt/icon.png b/src/gui/alt/icon.png
new file mode 100644 (file)
index 0000000..d6a2d90
Binary files /dev/null and b/src/gui/alt/icon.png differ
diff --git a/src/gui/check.svg b/src/gui/check.svg
new file mode 100644 (file)
index 0000000..333cf07
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="240" viewBox="0 96 960 960" width="240"><path d="M421 676.692 320.077 574q-7.154-5.385-16.615-5.769-9.462-.385-15.847 6-7.154 7.154-7.154 16.615 0 9.462 7.154 15.616l109.923 110.154q9.049 11 23.371 11t24.553-11l227.153-226.385q5.616-6.385 6-15.846.385-9.462-6-16.847-7.384-6.153-16.961-6.038-9.577.115-15.731 6.269L421 676.692ZM480.134 952q-78.082 0-146.274-29.859-68.193-29.86-119.141-80.762-50.947-50.902-80.833-119.033Q104 654.215 104 576.134q0-77.569 29.918-146.371 29.919-68.803 80.922-119.917 51.003-51.114 119.032-80.48Q401.901 200 479.866 200q77.559 0 146.353 29.339 68.794 29.34 119.922 80.422 51.127 51.082 80.493 119.841Q856 498.361 856 575.95q0 78.358-29.339 146.21-29.34 67.853-80.408 118.902-51.069 51.048-119.81 80.993Q557.702 952 480.134 952ZM480 908.231q137.897 0 235.064-97.282Q812.231 713.666 812.231 576q0-137.897-97.167-235.064T480 243.769q-137.666 0-234.949 97.167Q147.769 438.103 147.769 576q0 137.666 97.282 234.949Q342.334 908.231 480 908.231ZM480 576Z"/></svg>
\ No newline at end of file
diff --git a/src/gui/close.svg b/src/gui/close.svg
new file mode 100644 (file)
index 0000000..e96a4e7
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
\ No newline at end of file
diff --git a/src/gui/cog.svg b/src/gui/cog.svg
new file mode 100644 (file)
index 0000000..ba5645a
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 13.5C9.07177 13.5 8.18153 13.1313 7.52515 12.4749C6.86877 11.8185 6.50002 10.9283 6.50002 10C6.50002 9.07174 6.86877 8.1815 7.52515 7.52513C8.18153 6.86875 9.07177 6.5 10 6.5C10.9283 6.5 11.8185 6.86875 12.4749 7.52513C13.1313 8.1815 13.5 9.07174 13.5 10C13.5 10.9283 13.1313 11.8185 12.4749 12.4749C11.8185 13.1313 10.9283 13.5 10 13.5V13.5ZM17.43 10.97C17.47 10.65 17.5 10.33 17.5 10C17.5 9.67 17.47 9.34 17.43 9L19.54 7.37C19.73 7.22 19.78 6.95 19.66 6.73L17.66 3.27C17.54 3.05 17.27 2.96 17.05 3.05L14.56 4.05C14.04 3.66 13.5 3.32 12.87 3.07L12.5 0.42C12.46 0.18 12.25 0 12 0H8.00002C7.75002 0 7.54002 0.18 7.50002 0.42L7.13002 3.07C6.50002 3.32 5.96002 3.66 5.44002 4.05L2.95002 3.05C2.73002 2.96 2.46002 3.05 2.34002 3.27L0.340022 6.73C0.210022 6.95 0.270023 7.22 0.460023 7.37L2.57002 9C2.53002 9.34 2.50002 9.67 2.50002 10C2.50002 10.33 2.53002 10.65 2.57002 10.97L0.460023 12.63C0.270023 12.78 0.210022 13.05 0.340022 13.27L2.34002 16.73C2.46002 16.95 2.73002 17.03 2.95002 16.95L5.44002 15.94C5.96002 16.34 6.50002 16.68 7.13002 16.93L7.50002 19.58C7.54002 19.82 7.75002 20 8.00002 20H12C12.25 20 12.46 19.82 12.5 19.58L12.87 16.93C13.5 16.67 14.04 16.34 14.56 15.94L17.05 16.95C17.27 17.03 17.54 16.95 17.66 16.73L19.66 13.27C19.78 13.05 19.73 12.78 19.54 12.63L17.43 10.97Z" fill="#494646"/>
+</svg>
diff --git a/src/gui/ethernet.svg b/src/gui/ethernet.svg
new file mode 100644 (file)
index 0000000..d35645b
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="179" height="128" viewBox="0 0 179 128" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <image id="b" x="0" y="0" width="179" height="128" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALMAAACACAYAAABA+ljwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABLPSURBVHgB7V0HsCxFFT0GjIiICeNrMGMAMcf/AMtQRkRFUeSpKIgKmFBA2fkqiGJE+CgGnmJAxZzje5SlRZkFFDHNA8UESFCy8O3j7NTv7Z2e7p7pnrA7p+rU/2+mZ6Z79uzd2923bwMDBvhjNOaAAb3GwZIbxzwcAwb0FK/HJiF3TtDXQXs4QHJXyftLXh/zh39L/kjydZK/R/dBIb/VcO4IyUMxp/g4pr/h88y90W3wC1dW/wslb4U5BH+W+iCwJnmt5H3RTYxgF/KDMac4B/0QWNM8Bt2DzSJfLPkg7ZpFzAkE+iGsNvhHdAsHoby+F0nuoF1z2PjcXIxyCPRDWG0wRXcwQnldz5Pc0XLNrmgYszyK8ATJq1Afu0gegvnBayWTkvO0yDtJnqEcG2nXnCj5Rcw4BJqxcuchHB4JNFLnFO1jhPI6/kPy3pZrPiF5XcwBBJoRxgrCQQCN1DlFu3gNyuvHUQvdRx5pZT4teT2tDMU9kz60QDPC+BzC4UaSlwPR65yiPbwadiHfU7tmpJU5GZNu62bI3I38/MwJWqAZMR+FsEiB6HVuS8yHobxe58Jukekfb6aVOVYrk2DGINCMmEPPqP0AiF7nFM3jVbBbZF3IiVbma5I3VM7zl+yTaEHIfR/N+K7k9wqOfx9h8XZkH5oOxpdsjX7ijZJvKjn/F8mdMRk3kmAy9PPbkrtJXqkce7/kc5S/12NSzAuST5N8L3oOgbDW7GC0i1X00zK/0lKXSzBtkddrZWgwbqyV+QrKLfJtJH8zPtd7H1ogrJj3QLv4GvonZpuQc46Ua7ZHFj+SnztF8ibK+c2RdQDV69+sPZe/YGcq56+QvBd6DIGwYt4F7WIZ/RLzG+BXp5Fy7eOQCfqHkjdVjjOM+GMot8i3kzxLK7OEnkMgrJgX0S6W0R8xH4hq9Rop93iq5M2UvynkE7XyuvtwB8nfaWV218oI9BACYcV8f7SLo9APMR+KenUbFdyTs3y6kBOtjEDWgVTLvFAr8xhkoyZFz/BC10YzvoFsytQVMQXgghX4BaUvonkrxBGXt6Aeni/5bmQdwxxflnyi8rc+anEnZKNNd9Huc6Ly9yMkv45sjJrXXobwcwTRIGD+9m/A7EPAPAET44u5P+r/YvwRxV/Ap2NTpzDRzt1pfF1+D5bTLfIzJK/GZPsFegQB80ubF3AEpgkxH4Iwrs+dlXvyl/xZyt/sFCbac1n+T9p9XqKVoUW/CD0WMiEwiPmZiC/mVyCMRVaFzACiVZh9aGIB00LeRyvD8NFrUC5k3v9YdBwCg5hjizmUkIVyTwr5JK2MLugFTLtQ+2llOPN3MexCzs93WtACg5hjivn1iGORTZNDqqD3RJY+IT+3v1a3xyLrQJYJWZ9l5Cr+NtNhlEJgEHMsMb8c4S3yFrDPciZK+bxTeKBWt70wOYNYJOSRdt8T0HEIDGKOIWbb4lMX8vm6Rf6047Uj5bptMd3eS+EnZAb0qyGltM67oWMQGMQcWswvQxiLbOrsuXJUULenFLRRaGV0IX9eO69Ol78LHYLAIOYiMXMG7GXwRyghC+WeFPJXK95rpNxnAf6uxbJ2nkI+QSuzMzoCAfOLEJgPcPWzbq12hD9sa/ZcyGfrFvmkmvccKfd7ATJBuwiZLo0aiUchf1grUxZ/3TgEyl/EPphtfAGT7f2m5O3hj/1QX8hFnb2qFlnneuW+u8Au5I9ictSiyCLrIaVbomUIlL+ETvlEEZBiU1uPKCm3Rck521KnqhbZtbNXxUKrOFIrRx95c+V8kZAT7R75LKMpK2kjEBjEzHa+tKQMg2+4iPTAgnMvRRiLXLezV1XQI+08A5F0i6zHRushpaz7acr5d6MlCAxiPqHkvB6qqeY83hFhhCyUe4Z0LVwEzWyh+cQJF72qyWKKLLJuebfC9HQ52UqkncB8i3kFWS9fB/0//twWvZMXKeVs+S3KmGLaIn+qxv2qCpq/PHRpVFeqSMhHYhJFAUymZzQCgfkW86MLjgnJP6D8vai57qoIWnctKKTQPrKPoFVQyB/RyurpfXXXoojLiAj2aLfRjgmEEzPHG/ceczvl+I7K8Z2U49soxx+jHN9SOb6n9oy9Faq9592U49ugHmwfUk411sFH0CnCD79V5QFa24ss8nqtjM0ik8chUgwHpyGXxw+JKeZl5Tq1s5Qox09Qjj9VOb5iqNPF2jPUuqnpqX6qHH8+quNg+InBV9DsSDbV2bPxdEwP0a2T/I9S5p3aeRchfygvHDpbI60Xk7LshQE2CPjnjmDilNzl4Af/Gkt55qlQF6Byenkdmsea5JPH/xL5rxxTFlArFOV7kH1Bc7Czt4ryXz4K+cWIAA7+/wKT3xofy8xl8D4D4cvot2U+A9WtnI+FVttK43VKjedWYSp5a6UOd5T8FSZ9aH0GtFXXgiL8ZcEDfcTsi2X0V8z6lHZsQasLTx8e4Nmu5BdWKM/mr4T67kaYRqudPf6UnWV46CDmaQhMhkQ2IegUky7luwI9f6PlmUJ5JvNnFBm8kVKmlkWu6zPfAFm8wd0xwBWfwWRATR3Qh84FTR/aFHknkKWuzcGAnfMRD79GNpK0Nv6bv9wM8t++oGyCTNAU8irKfWQGHuWzoFOoK+bjkf1sDXADf0EehLBQO4VM1/BaQ7mtlP9zZXSCOFiTfBImhfxjFAs5R4Ksv2UTcmlnr46Y2RPfCwNcITA9hhoK/CxyC/0OTAuaEySqK7YA+0hIFawhm7JeG//NHHOrkndzuHarknMfQCbkUne0qpgPwXztwBQCnKjYAvGguhyqoJlm9tnI4ooJCnkV4ePHc9ci3xyJFpkZqrZHPdAi7wuHflWV9FyceZuLTQsDgmv0HoL4yBN4H41M0H9GFkiUI5aQ1zDtWqwijJCdx5F9LbNAtvzbBexgXIQBAs0mRaeg81RYdC8uHf8/pkVWXYs8aCqUkK0WOYePZabwv4PMD3IB17pdiAFMwt30qgj9M4ptkVXXgvmbt0M9eAuZ8BEz98C4q2NZJiNZxQD6rQ9As+BMquoGxhQyLXIuZGZDpbGrK2R29ozDb2VwdTPo7yWOZb8k+Tb4Q2C2IND8sp4QQub2DFzSdUFJmaLOHjfr2QH14NzZK4KLmDlkchLcsIYs11kVrGC2BM2ZquuhOXDYr66Q2cd5GLIVLg9EttBUxxomO3scoWG8R93E716dvargGivXKcynW+4lLNezs/IPR15quM50/ELtWeq9XI6frxy/YnxsDcVi2Qdxp4p1Hqo9fwH+G3Hy/awraAtdiXyKmfdUg4Zc0ne5kEKOEjSk4l4eFTre4X4C9RrdNe5uaOPFDdYhlJBtLsISptcP/qRCfXUuo6HkiGc5Vogvz2Vzxy2B2o3vCt9naKNAM9sTFwm5yrP/hmkh0/d+JczIp6jr1p8bYDYi5CW4V2rJ/bb4LFD7JbTNFOXJWxYQX9AHBXimbTthuhd6rDEt8qk16p3zQ2gI3P/Y9cWk8MPWaM5yxeIDYYeI2M5Ee9YCqgn5ntp9EkPZj4zbw3jkUEJuxCIT+8K9Ykvwh0DWU97YQybwa2ca+PmxOnuLlmvOwaatguswmmthuukaivM76OASdteJlCJwV6K7oF/4keRV2rERMoEcXVBeINywY6hxZI4Rn1pwLkHc/BPMVrSETNSNYAlxrfKsQUj+F9n7OLCkTIqwFrnKfYs6exyFUlesJDXr2bhFLoPryzkXAwh9K13TKIBAdUGX5VNzvW9RZ2+Dcj6moBvr7Kl4CtwrOA+bUNrApUhF7+YwQ3mBaoLmuHXZChXbfYs6exsKyqkx6kmFepqE3LhFJnxSNtWdh+87BCYTmOjct+S6FP6ioCCrCJrXPVQre2zJc0Ja6FZcC0LAvZKnY8ALYH9PpqVJAtUtdFmWfd5XnYa/HNNC3uDwnBCCXkaLWIJ7Rd+JAQSD4G3v6iDDtQJxXQ6bjxxT0I3EWpRhFe6VHVZkb4KLoF9nuFYgnoUuGrXwfY4q6EMdr2mls6dia7g3kGOsrX7rOgjmULa9t5HhWoE4FlqFj0XW6dMpbN0iE3vAvXErmF/QTxaGc7RitndnWgsoJP8F988gJ6+xCfrYCvcts9CJoQxXiHTCyC3DvWHvwXxCIBNPCrOgmX/Yx9LlYLD7tXD/DHQLbXI5jql4T19Bfxgdgs+ce+e2fW0IX8emd5DCLGgXl0OdzXsOslGHOkIrcjlCCrlM0AxC6ozbuQC/Bi1i/lC0y1OKcgt9DcrfI12O50lejTBC46/GTuPnV+nsufKFSjt3Rcf6T9wSwacxt8R8QUiejeJ3kcIs6CXEE5SJtNAnN/CczmazSuDXkHkDP7iy95Ging/dV+6PDsInrvg8zB+YfnYZ1QXtMsoxCDoQGJ/rWvkU8wsmILS9G1NKVpdOYV/ZKZeDKwgGMbvBFohVJugmfNk2yFwbrW/KniMPLB/EvAmmiEC6HDYLzc7ittp1S+iPOH35cnQIPhU/G7MPgaydZXsS2iw0r8+XgzE38lXohzB9uR4dg0/lL8XsQ+0QH2co49IppPvmEoRUhVWmvkPzTDSbfswJvo3YDLML7hylt/e4kvI2lyMGEzSbaKaI56CjeQF9G3IPzCa4UtwkkLI1eE1uqJ4ozxVoR9B85iPQUfg2xpYcsa+wxTKUuRxfRbNCziHQrKCZlb8zIxdF8G3QqzGb4F6GNmGUdQpjC/oNhucKxBc0778zegDfhn0MswuBLIVCWftNFvrJiCuoMkEza30a6ZlcIhdzl6ygYGIQn8b9BbMNAbswdEEz+q1qPLIvRyX1Pj3gc1IU52vuNH4P/4beEbMBUxpeuhw2C527HFz93GQ+ZjK2y8EFGJ32jU2g2+Db2E7N+lSEQPZF3q3kvE3QDNiPYZG59ZgtHjqGoPk+Hoke4yj4N3oF/Uf+JWZwvEnQLp3C0EzGz+bMYZOC5i/NzdFz7An/F34JevozNIaewIWC3t1QVsBuoUMLOQeXVMUWNP3sRcwImJK2youPsZF4ExAwRwqWuRwuwqhD08SMi4UeGa4VKO8U0je+CWYI3D7tn/B/+T9BP3EizG1iQNBzDdfR5fAd+fFhKnk3w7NDW2j+vRNmFFUH/BfRP5gyd+Y0+dDc5+4ixBMzyWFPUVztYILmBvE3wwyDSbKrvPwV9BP80G2CVqftmZfiAsQVck7656b4l7qdwnWYAzDIpurLX0Q/YbPQdDko6PuhOSHnTDGdTzmHi4U2ZU6aG5yCai9+Ff2FzUKTMSZEEthXxf8d2aaiRWAGpKqdwrkAG1/1w9kD/UWddlcVco7EUpa5lu9tqDffeZmgy+KwZx4cN7Z9203kyoc+J4cZoXkh50gs1zC9g0AxigS9hhkeqfDBt1H9gzoZ/YaLy1GHv4V5oimBXdD3NVyrdgqPQb8ns4JiEfU+sL3RbzAJYExB/xLmSYq3Wa5dQ9YZLQLDT3sRb9w0fJLC6GRnaXv0Exx+44aPGyNzFdUtNEdVtsUAZ/hsn1bEFB1d7FiC+yDbA2RjQ6SFNgW8J5ZrXRKMD1BA/66uoLdGP/AAxFm6b7vnzyRvYahTYrm2c/kquownov6HyQTmAt3GXoiToCVBtpTJtvLj5zCPAiUF5TndPYxUVMB3Uf9DTdFdQbvunlRFyDlcBH2a5FaGOiZKufdhGKmoDIEwHaJU8iHoDuircufQ2ELO4Spok1AZN7MOA2oj5NjrCO3j0Ygbl3y84bkU9BmWa38teRsMiIpTEe7D5gcq0Dxo9d6OeCJW+cGSOpxmuZb9jL50nHsJgfDDVkdI3h7xcSNk2/3+HQhafxs/YKjPrSV/UXIdF8cegAFRwWD10B/4GrIhpjsjPGgFKYqYK0NsNC2DoqCLfOgU87mTVys4EnE+dCY7/7jk41Efi8iixZqYADka9jjnowz11AXNdztTa/H6gNgZLzkd/gVkrsEisoTdRfvMXR+Z+/MoZL19Bjk1OYN3+LgeDP4521L2SBSDgv7SuA0DWgCHtUJ2CF14JTKXJFVYd0fTOnyr9k4oaJuFPhwDOgkK+ldoT0xt0jSN7GKhy3I8D2gRnIJlqoE+CDAUE5TDxUIPU9EdBUcM6gTz94UMfH8J3MBt04oE/Tt0ONv8gE2gP9gXYfqSKzweCj+oLge/CBzR6E1+4wHAG9EvkbpwVfK2qAYK+qcYYip6C2b8SdEvwRaRs3AjDJh7MJnMBvRLvCoH33bAFB6HfllpjlsnKJ6gGTDg/+DOVD4bzTdNuhSfQf/WLA5oCQJZHEPT+37Y+C3Jh2HAgAq4HbJIthTtCZgZ/zkTtwMGDAgEJixZRvx8xySj8lYk98Owhm5AZDwWWVoprlQOJWAu8+cGPEsYBDw36FrvncJ7MLKkLAz/3G58LOfmyOp8GTKrzhm2v0r+SfJMZNF1TM17LgbMHf4H4Vg2S7fqhVUAAAAASUVORK5CYII="/>
+</svg>
diff --git a/src/gui/expand_less.svg b/src/gui/expand_less.svg
new file mode 100644 (file)
index 0000000..08039b6
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 8.29504L6 14.295L7.41 15.705L12 11.125L16.59 15.705L18 14.295L12 8.29504Z" fill="black"/>
+</svg>
diff --git a/src/gui/expand_more.svg b/src/gui/expand_more.svg
new file mode 100644 (file)
index 0000000..71b70fd
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16.59 8.29504L12 12.875L7.41 8.29504L6 9.70504L12 15.705L18 9.70504L16.59 8.29504Z" fill="black"/>
+</svg>
diff --git a/src/gui/externalMic.svg b/src/gui/externalMic.svg
new file mode 100644 (file)
index 0000000..ad8e7dd
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M184-692q-14-15-19-34t-5-39q0-47.917 33.25-81.458Q226.5-880 274-880t80.75 33.542Q388-812.917 388-765q0 20-5 39t-19 34H184ZM396-80q-63.938 0-109.469-45Q241-170 241-235h-20q-6 0-10.125-3.889T206-249l-36-369q-2-13.5 7.25-23.25T200-651h148q13.5 0 22.75 9.75T378-618l-36 369q-.75 6.222-4.875 10.111Q333-235 327-235h-26q0 39 27.867 67 27.868 28 67 28Q435-140 462.5-167.906 490-195.812 490-235v-490q0-65 45-110t110-45q65 0 110 45t45 110v615q0 12.75-8.675 21.375Q782.649-80 769.825-80 757-80 748.5-88.625T740-110v-615q0-39.188-27.867-67.094-27.867-27.906-67-27.906Q606-820 578-792.094 550-764.188 550-725v490q0 65-45.237 110Q459.525-80 396-80ZM261-295h26l28-296h-82l28 296Zm26-296h-54 82-28Z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/AE.svg b/src/gui/flags/AE.svg
new file mode 100644 (file)
index 0000000..59ddafd
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><path fill="#009e49" d="M0 0h513v114H0z"/><path d="M0 228h513v114H0z"/><path fill="#ce1126" d="M0 0h171v342H0z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/AU.svg b/src/gui/flags/AU.svg
new file mode 100644 (file)
index 0000000..f91b013
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#10338c" d="M0 0h513v342H0z"/><g fill="#FFF"><path d="M222.2 170.7c.3-.3.5-.6.8-.9-.2.3-.5.6-.8.9zM188 212.6l11 22.9 24.7-5.7-11 22.8 19.9 15.8-24.8 5.6.1 25.4-19.9-15.9-19.8 15.9.1-25.4-24.8-5.6 19.9-15.8-11.1-22.8 24.8 5.7zM385.9 241.1l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6v12.2l-9.4-7.6-9.5 7.6.1-12.2-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zM337.3 125.1l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.9-2.7 9.5-7.5-5.3-10.9L332 136zM385.9 58.9l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.7v12.1l-9.4-7.6-9.5 7.6.1-12.1-11.8-2.7 9.5-7.5-5.3-10.9 11.8 2.7zM428.4 108.6l5.2 10.9 11.8-2.7-5.3 10.9 9.5 7.5-11.8 2.6V150l-9.4-7.6-9.5 7.6v-12.2l-11.8-2.6 9.5-7.5-5.3-10.9 11.8 2.7zM398 166.5l4.1 12.7h13.3l-10.8 7.8 4.2 12.7-10.8-7.9-10.8 7.9 4.1-12.7-10.7-7.8h13.3z"/><path d="M254.8 0v30.6l-45.1 25.1h45.1V115h-59.1l59.1 32.8v22.9h-26.7l-73.5-40.9v40.9H99v-48.6l-87.4 48.6H-1.2v-30.6L44 115H-1.2V55.7h59.1L-1.2 22.8V0h26.7L99 40.8V0h55.6v48.6L242.1 0z"/></g><path fill="#D80027" d="M142.8 0h-32v69.3h-112v32h112v69.4h32v-69.4h112v-32h-112z"/><path fill="#0052B4" d="m154.6 115 100.2 55.7v-15.8L183 115z"/><path fill="#FFF" d="m154.6 115 100.2 55.7v-15.8L183 115z"/><g fill="#D80027"><path d="m154.6 115 100.2 55.7v-15.8L183 115zM70.7 115l-71.9 39.9v15.8L99 115z"/></g><path fill="#0052B4" d="M99 55.7-1.2 0v15.7l71.9 40z"/><path fill="#FFF" d="M99 55.7-1.2 0v15.7l71.9 40z"/><g fill="#D80027"><path d="M99 55.7-1.2 0v15.7l71.9 40zM183 55.7l71.8-40V0L154.6 55.7z"/></g></svg>
\ No newline at end of file
diff --git a/src/gui/flags/BE.svg b/src/gui/flags/BE.svg
new file mode 100644 (file)
index 0000000..cc1b013
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#fdda25" d="M0 0h513v342H0z"/><path d="M0 0h171v342H0z"/><path fill="#ef3340" d="M342 0h171v342H342z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/BR.svg b/src/gui/flags/BR.svg
new file mode 100644 (file)
index 0000000..f4dbb02
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#009b3a" d="M0 0h513v342H0z"/><path fill="#fedf00" d="m256.5 19.3 204.9 151.4L256.5 322 50.6 170.7z"/><circle fill="#FFF" cx="256.5" cy="171" r="80.4"/><path fill="#002776" d="M215.9 165.7c-13.9 0-27.4 2.1-40.1 6 .6 43.9 36.3 79.3 80.3 79.3 27.2 0 51.3-13.6 65.8-34.3-24.9-31-63.2-51-106-51zM334.9 186c.9-5 1.5-10.1 1.5-15.4 0-44.4-36-80.4-80.4-80.4-33.1 0-61.5 20.1-73.9 48.6 10.9-2.2 22.1-3.4 33.6-3.4 46.8.1 89 19.5 119.2 50.6z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/CA.svg b/src/gui/flags/CA.svg
new file mode 100644 (file)
index 0000000..457d316
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><g fill="red"><path d="M0 0h142v342H0zM371 0h142v342H371zM306.5 206l50.4-25.2-25.2-12.6V143l-50.4 25.2 25.2-50.4h-25.2L256.1 80l-25.2 37.8h-25.2l25.2 50.4-50.4-25.2v25.2l-25.2 12.6 50.4 25.2-12.6 25.2h50.4V269h25.2v-37.8h50.4z"/></g></svg>
\ No newline at end of file
diff --git a/src/gui/flags/CH.svg b/src/gui/flags/CH.svg
new file mode 100644 (file)
index 0000000..498b7d1
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 513 342"><path fill="red" d="M0 85.337h513v342H0z"/><path fill="#FFF" d="M356.174 222.609h-66.783v-66.783h-66.782v66.783h-66.783v66.782h66.783v66.783h66.782v-66.783h66.783z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/DE.svg b/src/gui/flags/DE.svg
new file mode 100644 (file)
index 0000000..df0775b
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.331h512v341.337H0z"/><path d="M0 85.331h512v113.775H0z"/><path fill="#FFDA44" d="M0 312.882h512v113.775H0z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/FR.svg b/src/gui/flags/FR.svg
new file mode 100644 (file)
index 0000000..9f02836
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.331h512v341.337H0z"/><path fill="#0052B4" d="M0 85.331h170.663v341.337H0z"/><path fill="#D80027" d="M341.337 85.331H512v341.337H341.337z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/GB.svg b/src/gui/flags/GB.svg
new file mode 100644 (file)
index 0000000..4ada58a
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.333h512V426.67H0z"/><path fill="#D80027" d="M288 85.33h-64v138.666H0v64h224v138.666h64V287.996h224v-64H288z"/><g fill="#0052B4"><path d="M393.785 315.358 512 381.034v-65.676zM311.652 315.358 512 426.662v-31.474l-143.693-79.83zM458.634 426.662l-146.982-81.664v81.664z"/></g><path fill="#FFF" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><path fill="#D80027" d="M311.652 315.358 512 426.662v-31.474l-143.693-79.83z"/><g fill="#0052B4"><path d="M90.341 315.356 0 365.546v-50.19zM200.348 329.51v97.151H25.491z"/></g><path fill="#D80027" d="M143.693 315.358 0 395.188v31.474l200.348-111.304z"/><g fill="#0052B4"><path d="M118.215 196.634 0 130.958v65.676zM200.348 196.634 0 85.33v31.474l143.693 79.83zM53.366 85.33l146.982 81.664V85.33z"/></g><path fill="#FFF" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><path fill="#D80027" d="M200.348 196.634 0 85.33v31.474l143.693 79.83z"/><g fill="#0052B4"><path d="M421.659 196.636 512 146.446v50.19zM311.652 182.482V85.331h174.857z"/></g><path fill="#D80027" d="M368.307 196.634 512 116.804V85.33L311.652 196.634z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/HK.svg b/src/gui/flags/HK.svg
new file mode 100644 (file)
index 0000000..284a722
--- /dev/null
@@ -0,0 +1 @@
+<svg viewBox="0 0.5 21 14" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FFF" d="M0 0h21v15H0z"/><path fill="#ee1c25" d="M0 0h21v15H0z"/><path d="M12 7.19c-.798-.5-1 .409-1 0 0-.828.895-1.5 2-1.5s2 .672 2 1.5c-.949 0-1.044.5-1.5.5-.56 0-.702 0-1.5-.5zM13.25 7a.25.25 0 1 0 0-.5.25.25 0 0 0 0 .5zm-1.81 1.962c.228-.913-.698-.824-.31-.95.788-.257 1.703.387 2.045 1.438.341 1.05-.021 2.11-.809 2.366-.293-.903-.798-.838-.939-1.272-.173-.533-.217-.668.012-1.582zm.566 1.13a.25.25 0 1 0 .476-.154.25.25 0 0 0-.476.154zM9.58 8.977c.94-.065.57-.919.81-.588.486.67.157 1.74-.737 2.389-.894.65-2.013.632-2.5-.038.768-.558.55-1.018.92-1.286.453-.33.568-.413 1.507-.477zm-.899.888a.25.25 0 1 0 .294.405.25.25 0 0 0-.294-.405zm.312-2.652c.351.874 1.049.258.809.588-.487.67-1.606.687-2.5.038-.894-.65-1.223-1.719-.736-2.39.767.559 1.138.21 1.507.478.453.33.568.413.92 1.286zm-1.124-.58a.25.25 0 1 0-.293.404.25.25 0 0 0 .293-.404zm2.619-.524c-.722.605.08 1.078-.309.951-.788-.256-1.15-1.315-.809-2.365.342-1.05 1.257-1.695 2.045-1.439-.293.903.153 1.147.012 1.581-.173.533-.217.668-.939 1.272zm.205-1.247a.25.25 0 1 0-.475-.155.25.25 0 0 0 .475.155z" fill="#FFF"/></g></svg>
\ No newline at end of file
diff --git a/src/gui/flags/ID.svg b/src/gui/flags/ID.svg
new file mode 100644 (file)
index 0000000..45d3745
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.333h512v341.333H0z"/><path fill="#E00" d="M0 85.333h512V256H0z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/IT.svg b/src/gui/flags/IT.svg
new file mode 100644 (file)
index 0000000..17b1314
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M341.334 85.33H0v341.332h512V85.33z"/><path fill="#6DA544" d="M0 85.333h170.663V426.67H0z"/><path fill="#D80027" d="M341.337 85.333H512V426.67H341.337z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/JP.svg b/src/gui/flags/JP.svg
new file mode 100644 (file)
index 0000000..92eb885
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.331h512v341.337H0z"/><circle fill="#D80027" cx="256" cy="255.994" r="96"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/RO.svg b/src/gui/flags/RO.svg
new file mode 100644 (file)
index 0000000..fabf12e
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFDA44" d="M0 85.331h512v341.326H0z"/><path fill="#0052B4" d="M0 85.331h170.663v341.337H0z"/><path fill="#D80027" d="M341.337 85.331H512v341.337H341.337z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/SE.svg b/src/gui/flags/SE.svg
new file mode 100644 (file)
index 0000000..7ec1787
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#0052B4" d="M0 85.333h512V426.67H0z"/><path fill="#FFDA44" d="M192 85.33h-64v138.666H0v64h128v138.666h64V287.996h320v-64H192z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/SG.svg b/src/gui/flags/SG.svg
new file mode 100644 (file)
index 0000000..c374c47
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.337h512v341.326H0z"/><path fill="#D80027" d="M0 85.337h512V256H0z"/><g fill="#FFF"><path d="M83.478 170.666c0-24.865 17.476-45.637 40.812-50.734a52.059 52.059 0 0 0-11.13-1.208c-28.688 0-51.942 23.254-51.942 51.941s23.255 51.942 51.942 51.942c3.822 0 7.543-.425 11.13-1.208-23.336-5.095-40.812-25.867-40.812-50.733zM150.261 122.435l3.684 11.337h11.921l-9.645 7.007 3.684 11.337-9.644-7.006-9.645 7.006 3.685-11.337-9.645-7.007h11.921z"/><path d="m121.344 144.696 3.683 11.337h11.921l-9.645 7.007 3.684 11.337-9.643-7.006-9.645 7.006 3.685-11.337-9.645-7.007h11.921zM179.178 144.696l3.684 11.337h11.921l-9.645 7.007 3.684 11.337-9.644-7.006-9.644 7.006 3.685-11.337-9.645-7.007h11.921zM168.047 178.087l3.684 11.337h11.921l-9.644 7.007 3.684 11.337-9.645-7.006-9.643 7.006 3.684-11.337-9.644-7.007h11.92zM132.474 178.087l3.683 11.337h11.921l-9.644 7.007 3.684 11.337-9.644-7.006-9.644 7.006 3.684-11.337-9.644-7.007h11.92z"/></g></svg>
\ No newline at end of file
diff --git a/src/gui/flags/TW.svg b/src/gui/flags/TW.svg
new file mode 100644 (file)
index 0000000..c3660f1
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#D80027" d="M0 85.337h512v341.326H0z"/><path fill="#0052B4" d="M0 85.337h256V256H0z"/><path fill="#FFF" d="M186.435 170.669 162.558 181.9l12.714 23.125-25.927-4.961-3.286 26.192L128 206.993l-18.06 19.263-3.285-26.192-25.927 4.96 12.714-23.125-23.877-11.23 23.877-11.231-12.714-23.125 25.927 4.96 3.286-26.192L128 134.344l18.06-19.263 3.285 26.192 25.928-4.96-12.715 23.125z"/><circle fill="#0052B4" cx="128" cy="170.674" r="29.006"/><path fill="#FFF" d="M128 190.06c-10.692 0-19.391-8.7-19.391-19.391 0-10.692 8.7-19.391 19.391-19.391 10.692 0 19.391 8.7 19.391 19.391 0 10.691-8.699 19.391-19.391 19.391z"/></svg>
\ No newline at end of file
diff --git a/src/gui/flags/US.svg b/src/gui/flags/US.svg
new file mode 100644 (file)
index 0000000..dc427e7
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 513 342"><path fill="#FFF" d="M0 0h513v342H0z"/><g fill="#D80027"><path d="M0 0h513v26.3H0zM0 52.6h513v26.3H0zM0 105.2h513v26.3H0zM0 157.8h513v26.3H0zM0 210.5h513v26.3H0zM0 263.1h513v26.3H0zM0 315.7h513V342H0z"/></g><path fill="#2E52B2" d="M0 0h256.5v184.1H0z"/><g fill="#FFF"><path d="m47.8 138.9-4-12.8-4.4 12.8H26.2l10.7 7.7-4 12.8 10.9-7.9 10.6 7.9-4.1-12.8 10.9-7.7zM104.1 138.9l-4.1-12.8-4.2 12.8H82.6l10.7 7.7-4 12.8 10.7-7.9 10.8 7.9-4-12.8 10.7-7.7zM160.6 138.9l-4.3-12.8-4 12.8h-13.5l11 7.7-4.2 12.8 10.7-7.9 11 7.9-4.2-12.8 10.7-7.7zM216.8 138.9l-4-12.8-4.2 12.8h-13.3l10.8 7.7-4 12.8 10.7-7.9 10.8 7.9-4.3-12.8 11-7.7zM100 75.3l-4.2 12.8H82.6L93.3 96l-4 12.6 10.7-7.8 10.8 7.8-4-12.6 10.7-7.9h-13.4zM43.8 75.3l-4.4 12.8H26.2L36.9 96l-4 12.6 10.9-7.8 10.6 7.8L50.3 96l10.9-7.9H47.8zM156.3 75.3l-4 12.8h-13.5l11 7.9-4.2 12.6 10.7-7.8 11 7.8-4.2-12.6 10.7-7.9h-13.2zM212.8 75.3l-4.2 12.8h-13.3l10.8 7.9-4 12.6 10.7-7.8 10.8 7.8-4.3-12.6 11-7.9h-13.5zM43.8 24.7l-4.4 12.6H26.2l10.7 7.9-4 12.7L43.8 50l10.6 7.9-4.1-12.7 10.9-7.9H47.8zM100 24.7l-4.2 12.6H82.6l10.7 7.9-4 12.7L100 50l10.8 7.9-4-12.7 10.7-7.9h-13.4zM156.3 24.7l-4 12.6h-13.5l11 7.9-4.2 12.7 10.7-7.9 11 7.9-4.2-12.7 10.7-7.9h-13.2zM212.8 24.7l-4.2 12.6h-13.3l10.8 7.9-4 12.7 10.7-7.9 10.8 7.9-4.3-12.7 11-7.9h-13.5z"/></g></svg>
\ No newline at end of file
diff --git a/src/gui/flags/ZA.svg b/src/gui/flags/ZA.svg
new file mode 100644 (file)
index 0000000..1b294c9
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 85.333 512 341.333"><path fill="#FFF" d="M0 85.337h512v341.326H0z"/><path d="M114.024 256.001 0 141.926v228.17z"/><path fill="#ffb915" d="M161.192 256 0 94.7v47.226l114.024 114.075L0 370.096v47.138z"/><path fill="#007847" d="M509.833 289.391c.058-.44.804-.878 2.167-1.318v-65.464H222.602L85.33 85.337H0V94.7L161.192 256 0 417.234v9.429h85.33l137.272-137.272h287.231z"/><path fill="#000c8a" d="M503.181 322.783H236.433l-103.881 103.88H512v-103.88z"/><path fill="#e1392d" d="M503.181 189.217H512V85.337H132.552l103.881 103.88z"/></svg>
\ No newline at end of file
diff --git a/src/gui/headphones.svg b/src/gui/headphones.svg
new file mode 100644 (file)
index 0000000..8041f67
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none" opacity=".1"/><path d="M12 1c-4.97 0-9 4.03-9 9v7c0 1.66 1.34 3 3 3h3v-8H5v-2c0-3.87 3.13-7 7-7s7 3.13 7 7v2h-4v8h3c1.66 0 3-1.34 3-3v-7c0-4.97-4.03-9-9-9z"/></svg>
\ No newline at end of file
diff --git a/src/gui/help.svg b/src/gui/help.svg
new file mode 100644 (file)
index 0000000..d2a8d02
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M11 18h2v-2h-2v2zm1-16C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm0-14c-2.21 0-4 1.79-4 4h2c0-1.1.9-2 2-2s2 .9 2 2c0 2-3 1.75-3 5h2c0-2.25 3-2.5 3-5 0-2.21-1.79-4-4-4z"/></svg>
\ No newline at end of file
diff --git a/src/gui/icon.png b/src/gui/icon.png
new file mode 100644 (file)
index 0000000..9344dff
Binary files /dev/null and b/src/gui/icon.png differ
diff --git a/src/gui/jacktrip white.png b/src/gui/jacktrip white.png
new file mode 100644 (file)
index 0000000..7ba69b0
Binary files /dev/null and b/src/gui/jacktrip white.png differ
diff --git a/src/gui/jacktrip.png b/src/gui/jacktrip.png
new file mode 100644 (file)
index 0000000..c4b998a
Binary files /dev/null and b/src/gui/jacktrip.png differ
diff --git a/src/gui/join.svg b/src/gui/join.svg
new file mode 100644 (file)
index 0000000..bc50d97
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="23" height="19" viewBox="0 0 23 19" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 3C9.06087 3 10.0783 3.42143 10.8284 4.17157C11.5786 4.92172 12 5.93913 12 7C12 8.06087 11.5786 9.07828 10.8284 9.82843C10.0783 10.5786 9.06087 11 8 11C6.93913 11 5.92172 10.5786 5.17157 9.82843C4.42143 9.07828 4 8.06087 4 7C4 5.93913 4.42143 4.92172 5.17157 4.17157C5.92172 3.42143 6.93913 3 8 3V3ZM8 13C10.67 13 16 14.34 16 17V19H0V17C0 14.34 5.33 13 8 13ZM15.76 3.36C17.78 5.56 17.78 8.61 15.76 10.63L14.08 8.94C14.92 7.76 14.92 6.23 14.08 5.05L15.76 3.36ZM19.07 0C23 4.05 22.97 10.11 19.07 14L17.44 12.37C20.21 9.19 20.21 4.65 17.44 1.63L19.07 0Z" fill="#000000"/>
+</svg>
diff --git a/src/gui/language.svg b/src/gui/language.svg
new file mode 100644 (file)
index 0000000..8273aed
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M11.99 2C6.47 2 2 6.48 2 12C2 17.52 6.47 22 11.99 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 11.99 2ZM18.92 8H15.97C15.65 6.75 15.19 5.55 14.59 4.44C16.43 5.07 17.96 6.35 18.92 8ZM12 4.04C12.83 5.24 13.48 6.57 13.91 8H10.09C10.52 6.57 11.17 5.24 12 4.04ZM4.26 14C4.1 13.36 4 12.69 4 12C4 11.31 4.1 10.64 4.26 10H7.64C7.56 10.66 7.5 11.32 7.5 12C7.5 12.68 7.56 13.34 7.64 14H4.26ZM5.08 16H8.03C8.35 17.25 8.81 18.45 9.41 19.56C7.57 18.93 6.04 17.66 5.08 16ZM8.03 8H5.08C6.04 6.34 7.57 5.07 9.41 4.44C8.81 5.55 8.35 6.75 8.03 8ZM12 19.96C11.17 18.76 10.52 17.43 10.09 16H13.91C13.48 17.43 12.83 18.76 12 19.96ZM14.34 14H9.66C9.57 13.34 9.5 12.68 9.5 12C9.5 11.32 9.57 10.65 9.66 10H14.34C14.43 10.65 14.5 11.32 14.5 12C14.5 12.68 14.43 13.34 14.34 14ZM14.59 19.56C15.19 18.45 15.65 17.25 15.97 16H18.92C17.96 17.65 16.43 18.93 14.59 19.56ZM16.36 14C16.44 13.34 16.5 12.68 16.5 12C16.5 11.32 16.44 10.66 16.36 10H19.74C19.9 10.64 20 11.31 20 12C20 12.69 19.9 13.36 19.74 14H16.36Z" fill="black"/>
+</svg>
diff --git a/src/gui/leave.svg b/src/gui/leave.svg
new file mode 100644 (file)
index 0000000..c44f7e7
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="23" height="20" viewBox="0 0 23 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M1 1.27L2.28 0L21 18.72L19.73 20L15.73 16C15.9 16.31 16 16.64 16 17V19H0V17C0 14.34 5.33 13 8 13C9.77 13 12.72 13.59 14.5 14.77L10.12 10.39C9.5 10.78 8.78 11 8 11C6.93913 11 5.92172 10.5786 5.17157 9.82843C4.42143 9.07828 4 8.06087 4 7C4 6.22 4.22 5.5 4.61 4.88L1 1.27ZM8 3C9.06087 3 10.0783 3.42143 10.8284 4.17157C11.5786 4.92172 12 5.93913 12 7V7.17L7.83 3H8ZM15.76 3.36C17.78 5.56 17.78 8.61 15.76 10.63L14.08 8.94C14.92 7.76 14.92 6.23 14.08 5.05L15.76 3.36ZM19.07 0C23 4.05 22.97 10.11 19.07 14L17.44 12.37C20.21 9.19 20.21 4.65 17.44 1.63L19.07 0Z" fill="#9C0707"/>
+</svg>
diff --git a/src/gui/logo.svg b/src/gui/logo.svg
new file mode 100644 (file)
index 0000000..508c81b
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="50" height="93" viewBox="0 0 50 93" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7628 73.8622C11.7925 73.9104 11.8196 73.9602 11.8441 74.0113L14.6714 78.9066C16.8218 82.5868 20.3392 85.2682 24.4576 86.3667C28.576 87.4652 32.9617 86.8917 36.6592 84.7713L36.7948 84.6967C40.4339 82.5266 43.0758 79.0148 44.1522 74.9168C45.2287 70.8188 44.6538 66.462 42.5511 62.7835L24.55 31.5952C23.4582 31.5595 22.4078 31.169 21.5579 30.4828C20.708 29.7966 20.1049 28.8521 19.84 27.7924C19.5751 26.7327 19.6627 25.6155 20.0897 24.61C20.5166 23.6046 21.2597 22.7657 22.2062 22.2204C23.1527 21.6752 24.2511 21.4533 25.3351 21.5883C26.419 21.7233 27.4295 22.208 28.2133 22.9688C28.9971 23.7296 29.5116 24.7251 29.6788 25.8046C29.8461 26.8841 29.6569 27.9886 29.1401 28.9509L47.148 60.1393C49.9521 65.0155 50.726 70.799 49.3027 76.2409C47.8795 81.6829 44.3731 86.3468 39.5407 89.2258C39.4104 89.3213 39.272 89.4052 39.1272 89.4767C34.2442 92.2178 28.4827 92.9402 23.0741 91.4895C17.6655 90.0389 13.0389 86.5302 10.183 81.7135C10.0968 81.587 10.0219 81.4531 9.95926 81.3135L7.24723 76.6284L7.16587 76.4996C6.84576 75.9775 6.33816 75.5975 5.747 75.4374C5.15584 75.2773 4.52584 75.3493 3.98601 75.6386C3.68462 75.8135 3.35172 75.9274 3.00632 75.9737C2.66092 76.02 2.30977 75.9978 1.97294 75.9084C1.63611 75.819 1.32019 75.6641 1.04321 75.4526C0.766237 75.2411 0.533626 74.9772 0.358668 74.6758C0.18371 74.3744 0.0698312 74.0415 0.0235283 73.6961C-0.0227746 73.3507 -0.000592648 72.9995 0.0888089 72.6627C0.17821 72.3259 0.333083 72.01 0.544578 71.733C0.756074 71.456 1.02005 71.2234 1.32144 71.0484C3.07661 70.0427 5.15809 69.7712 7.11251 70.2931C9.06693 70.8151 10.7359 72.0881 11.756 73.835L11.7628 73.8622ZM30.747 8.18523e-07C29.4452 -0.000742291 28.194 0.504523 27.2579 1.40909C26.3217 2.31366 25.7737 3.54669 25.7297 4.84775L15.6816 10.6515C15.6305 10.676 15.5808 10.7031 15.5325 10.7329C11.7676 12.916 9.0218 16.5028 7.89703 20.707C6.77225 24.9113 7.36024 29.3899 9.53211 33.1614L9.62703 33.3241L9.7084 33.4597L30.6249 69.6924C30.9206 70.2333 30.9936 70.8682 30.8283 71.462C30.6738 72.056 30.2913 72.5652 29.7639 72.8791C29.5615 72.992 29.3766 73.1336 29.2147 73.2994V73.2994C28.7619 73.7725 28.4992 74.3958 28.4767 75.0503C28.4542 75.7047 28.6734 76.3446 29.0927 76.8476C29.5119 77.3507 30.1017 77.6818 30.7495 77.7777C31.3973 77.8736 32.0577 77.7275 32.6047 77.3675C34.2343 76.341 35.4179 74.739 35.9204 72.8798C36.4229 71.0207 36.2074 69.0405 35.3168 67.333C35.2451 67.1605 35.1541 66.9968 35.0456 66.8448L14.2511 30.829L14.1697 30.6799L14.0748 30.5104C12.6177 27.9552 12.2276 24.9282 12.9893 22.0871C13.751 19.246 15.6029 16.8202 18.1428 15.3365L18.2784 15.2552L28.3197 9.45822C28.992 9.81663 29.7371 10.0172 30.4985 10.0448C31.2599 10.0723 32.0176 9.92613 32.714 9.61725C33.4105 9.30837 34.0275 8.84494 34.5182 8.2621C35.0088 7.67927 35.3604 6.99235 35.546 6.25343C35.7317 5.51451 35.7466 4.743 35.5897 3.99745C35.4328 3.2519 35.1081 2.55189 34.6404 1.95049C34.1726 1.3491 33.574 0.862131 32.8901 0.526529C32.2061 0.190926 31.4546 0.0154886 30.6927 0.0135535L30.747 8.18523e-07Z" fill="#F21B1B"/>
+</svg>
diff --git a/src/gui/loud.svg b/src/gui/loud.svg
new file mode 100644 (file)
index 0000000..b3aeb16
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg viewBox="0 0 13 12" width="13" height="12" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M 7.543 12 L 7.543 10.937 C 8.651 10.617 9.557 10.003 10.26 9.094 C 10.963 8.186 11.314 7.154 11.314 6 C 11.314 4.846 10.603 3.712 9.906 2.798 C 9.209 1.884 8.663 1.371 7.543 1.063 L 7.543 0 C 8.96 0.32 10.114 1.037 11.006 2.151 C 11.897 3.266 12.343 4.549 12.343 6 C 12.343 7.451 11.897 8.734 11.006 9.849 C 10.114 10.963 8.96 11.68 7.543 12 Z M 0 8.074 L 0 3.96 L 2.743 3.96 L 6.171 0.531 L 6.171 11.503 L 2.743 8.074 L 0 8.074 Z M 7.2 8.897 L 7.2 3.12 C 7.829 3.314 8.329 3.68 8.7 4.217 C 9.071 4.754 9.257 5.354 9.257 6.017 C 9.257 6.669 9.069 7.263 8.691 7.8 C 8.314 8.337 7.817 8.703 7.2 8.897 Z M 5.143 3.137 L 3.206 4.989 L 1.029 4.989 L 1.029 7.046 L 3.206 7.046 L 5.143 8.914 L 5.143 3.137 Z" fill="#353637"/>
+</svg>
\ No newline at end of file
diff --git a/src/gui/manage.svg b/src/gui/manage.svg
new file mode 100644 (file)
index 0000000..7d9ae31
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>
\ No newline at end of file
diff --git a/src/gui/messageDialog.cpp b/src/gui/messageDialog.cpp
new file mode 100644 (file)
index 0000000..9f2ac2f
--- /dev/null
@@ -0,0 +1,190 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2020 Aaron Wyatt.
+
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#include "messageDialog.h"
+
+#include <QDateTime>
+#include <QMenu>
+#include <QScrollBar>
+#include <QSettings>
+#include <iostream>
+
+#include "ui_messageDialog.h"
+
+MessageDialog::MessageDialog(QWidget* parent, const QString& windowFunction,
+                             quint32 streamCount)
+    : QDialog(parent)
+    , m_ui(new Ui::MessageDialog)
+    , m_outStreams(streamCount)
+    , m_outBufs(streamCount)
+    , m_windowFunction(windowFunction)
+    , m_addTimeStamp(true)
+    , m_timeStampFormat(QStringLiteral("hh:mm:ss: "))
+    , m_startOfLine(true)
+{
+    m_ui->setupUi(this);
+    for (quint32 i = 0; i < streamCount; i++) {
+        m_outBufs[i].reset(new textbuf);
+        m_outStreams[i].reset(new std::ostream(m_outBufs.at(i).data()));
+        connect(m_outBufs.at(i).data(), &textbuf::outputString, this,
+                &MessageDialog::receiveOutput, Qt::QueuedConnection);
+    }
+
+    m_ui->messagesTextEdit->setContextMenuPolicy(Qt::CustomContextMenu);
+    connect(m_ui->messagesTextEdit, &QPlainTextEdit::customContextMenuRequested, this,
+            &MessageDialog::provideContextMenu);
+    m_ui->messagesTextEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
+    connect(this, &QDialog::rejected, this, &MessageDialog::savePosition);
+
+    if (!m_windowFunction.isEmpty()) {
+        setWindowTitle(m_windowFunction);
+        if (m_windowFunction == QLatin1String("Stats")) {
+            m_addTimeStamp = false;
+        } else {
+            // Create an indent for wrapped lines if we're adding a timestamp.
+            // Because we're using a fixed width font we can just multiply our
+            // timeStamp length by the average character width.
+            QTextBlockFormat indent;
+            QFontMetrics metrics(m_ui->messagesTextEdit->font());
+            int marginWidth = metrics.averageCharWidth() * m_timeStampFormat.length();
+            indent.setLeftMargin(marginWidth);
+            indent.setTextIndent(-marginWidth);
+            m_ui->messagesTextEdit->textCursor().setBlockFormat(indent);
+        }
+    }
+}
+
+void MessageDialog::showEvent(QShowEvent* event)
+{
+    QDialog::showEvent(event);
+    if (!m_windowFunction.isEmpty()) {
+        QSettings settings;
+        settings.beginGroup(QStringLiteral("Window"));
+        QByteArray geometry = settings.value(m_windowFunction + "Geometry").toByteArray();
+        if (geometry.size() > 0) {
+            restoreGeometry(geometry);
+        }
+    }
+}
+
+void MessageDialog::closeEvent(QCloseEvent* event)
+{
+    QDialog::closeEvent(event);
+    savePosition();
+}
+
+QSharedPointer<std::ostream> MessageDialog::getOutputStream(int index)
+{
+    if (index >= 0 && index < m_outStreams.size()) {
+        return m_outStreams.at(index);
+    }
+    return QSharedPointer<std::ostream>();
+}
+
+bool MessageDialog::setRelayStream(std::ostream* relay, int index)
+{
+    if (index >= 0 && index < m_outBufs.size()) {
+        m_outBufs.at(index)->setOutStream(relay);
+    }
+    return false;
+}
+
+void MessageDialog::clearOutput()
+{
+    m_ui->messagesTextEdit->clear();
+}
+
+void MessageDialog::receiveOutput(const QString& output)
+{
+    if (output.isEmpty()) {
+        return;
+    }
+
+    // Automatically scroll if we're at the bottom of the text box.
+    int scrollLocation = (m_ui->messagesTextEdit->verticalScrollBar()->value());
+    bool autoScroll =
+        (scrollLocation == m_ui->messagesTextEdit->verticalScrollBar()->maximum());
+
+    // Make sure our cursor is at the end.
+    m_ui->messagesTextEdit->moveCursor(QTextCursor::End);
+
+    if (m_addTimeStamp) {
+        QString timeStamp = QDateTime::currentDateTime().toString(m_timeStampFormat);
+        if (m_startOfLine) {
+            m_ui->messagesTextEdit->insertPlainText(timeStamp);
+        }
+        if (output.indexOf(QChar('\n')) == -1) {
+            m_ui->messagesTextEdit->insertPlainText(output);
+        } else {
+            QStringList lines = output.split(QChar('\n'));
+            m_ui->messagesTextEdit->insertPlainText(
+                QStringLiteral("%1\n").arg(lines.at(0)));
+            int length = lines.length();
+            if (output.endsWith(QChar('\n'))) {
+                length--;
+            }
+            for (int i = 1; i < length; i++) {
+                m_ui->messagesTextEdit->insertPlainText(
+                    QStringLiteral("%1%2\n").arg(timeStamp, lines.at(i)));
+            }
+        }
+        m_startOfLine = output.endsWith(QChar('\n'));
+    } else {
+        m_ui->messagesTextEdit->insertPlainText(output);
+    }
+    if (autoScroll) {
+        m_ui->messagesTextEdit->verticalScrollBar()->setValue(
+            m_ui->messagesTextEdit->verticalScrollBar()->maximum());
+    } else {
+        m_ui->messagesTextEdit->verticalScrollBar()->setValue(scrollLocation);
+    }
+}
+
+void MessageDialog::provideContextMenu()
+{
+    // Add a custom context menu entry to clear the output.
+    QMenu* menu     = m_ui->messagesTextEdit->createStandardContextMenu();
+    QAction* action = menu->addAction(QIcon::fromTheme(QStringLiteral("edit-delete")),
+                                      QStringLiteral("Clear"));
+    connect(action, &QAction::triggered, this, &MessageDialog::clearOutput);
+    menu->exec(QCursor::pos());
+}
+
+void MessageDialog::savePosition()
+{
+    if (!m_windowFunction.isEmpty()) {
+        QSettings settings;
+        settings.beginGroup(QStringLiteral("Window"));
+        settings.setValue(m_windowFunction + "Geometry", saveGeometry());
+        settings.endGroup();
+    }
+}
+
+MessageDialog::~MessageDialog()
+{
+    if (isVisible()) {
+        savePosition();
+    }
+}
diff --git a/src/gui/messageDialog.h b/src/gui/messageDialog.h
new file mode 100644 (file)
index 0000000..bb0904f
--- /dev/null
@@ -0,0 +1,75 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2020 Aaron Wyatt.
+
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#ifndef MESSAGEDIALOG_H
+#define MESSAGEDIALOG_H
+
+#include <QDialog>
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <QVector>
+
+#include "textbuf.h"
+
+namespace Ui
+{
+class MessageDialog;
+}
+
+class MessageDialog : public QDialog
+{
+    Q_OBJECT
+
+   public:
+    explicit MessageDialog(QWidget* parent               = nullptr,
+                           const QString& windowFunction = QLatin1String(""),
+                           quint32 streamCount           = 1);
+    ~MessageDialog() override;
+
+    void showEvent(QShowEvent* event) override;
+    void closeEvent(QCloseEvent* event) override;
+
+    QSharedPointer<std::ostream> getOutputStream(int index = 0);
+    bool setRelayStream(std::ostream* relay, int index = 0);
+
+   public slots:
+    void clearOutput();
+
+   private slots:
+    void receiveOutput(const QString& output);
+    void provideContextMenu();
+    void savePosition();
+
+   private:
+    QScopedPointer<Ui::MessageDialog> m_ui;
+    QVector<QSharedPointer<std::ostream>> m_outStreams;
+    QVector<QSharedPointer<textbuf>> m_outBufs;
+    QString m_windowFunction;
+    bool m_addTimeStamp;
+    QString m_timeStampFormat;
+    bool m_startOfLine;
+};
+
+#endif  // MESSAGEDIALOG_H
diff --git a/src/gui/messageDialog.ui b/src/gui/messageDialog.ui
new file mode 100644 (file)
index 0000000..22b8518
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MessageDialog</class>
+ <widget class="QDialog" name="MessageDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>567</width>
+    <height>321</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Stats</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout">
+   <item row="0" column="0">
+    <widget class="QTextEdit" name="messagesTextEdit">
+     <property name="readOnly">
+      <bool>true</bool>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/gui/mic.svg b/src/gui/mic.svg
new file mode 100644 (file)
index 0000000..520b7f9
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 14c1.66 0 2.99-1.34 2.99-3L15 5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm5.3-3c0 3-2.54 5.1-5.3 5.1S6.7 14 6.7 11H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c3.28-.48 6-3.3 6-6.72h-1.7z"/></svg>
\ No newline at end of file
diff --git a/src/gui/micoff.svg b/src/gui/micoff.svg
new file mode 100644 (file)
index 0000000..8816538
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0zm0 0h24v24H0z" fill="none"/><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/></svg>
\ No newline at end of file
diff --git a/src/gui/network.svg b/src/gui/network.svg
new file mode 100644 (file)
index 0000000..4aa8827
--- /dev/null
@@ -0,0 +1,4 @@
+<svg width="624" height="750" viewBox="0 0 624 750" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M614.44 570.333C601.997 557.891 583.331 557.891 570.883 570.333L499.331 641.891V32.104C499.331 14.9947 485.331 0.99469 468.221 0.99469C451.112 0.99469 437.112 14.9947 437.112 32.104V641.877L365.555 570.32C353.112 557.877 334.445 557.877 321.997 570.32C309.555 582.763 309.555 601.429 321.997 613.877L446.44 738.32C447.997 739.877 449.549 741.429 451.107 742.987L452.664 744.544C454.221 744.544 454.221 746.101 455.773 746.101C457.331 746.101 457.331 746.101 458.883 747.659C460.44 747.659 460.44 747.659 461.992 749.216H468.216H474.44C475.997 749.216 475.997 749.216 477.549 747.659C479.107 747.659 479.107 747.659 480.659 746.101C482.216 746.101 482.216 744.544 483.768 744.544C483.768 744.544 485.325 744.544 485.325 742.987C486.883 741.429 488.435 739.877 489.992 738.32L614.435 613.877C626.888 601.435 626.888 582.768 614.44 570.325L614.44 570.333Z" fill="black"/>
+<path d="M303.333 134.773L174.224 5.664C172.667 5.664 172.667 4.10667 171.115 4.10667C169.557 4.10667 169.557 2.54934 168.005 2.54934C166.448 2.54934 166.448 2.54934 164.896 0.992004H161.787C158.667 0.997213 155.557 0.997213 150.891 0.997213H147.781C146.224 0.997213 146.224 0.997213 144.672 2.55455C143.115 2.55455 143.115 4.11188 141.563 4.11188C140.005 4.11188 140.005 5.66921 138.453 5.66921L9.34413 134.779C-3.09854 147.221 -3.09854 165.888 9.34413 178.336C21.7868 190.779 40.4535 190.779 52.9015 178.336L126 106.773V716.56C126 733.669 140 747.669 157.109 747.669C174.219 747.669 188.219 733.669 188.219 716.56L188.224 106.773L259.781 178.331C266 184.555 273.776 187.664 281.557 187.664C289.333 187.664 297.115 184.555 303.333 178.331C315.776 165.888 315.776 147.221 303.333 134.773V134.773Z" fill="black"/>
+</svg>
diff --git a/src/gui/networkCheck.svg b/src/gui/networkCheck.svg
new file mode 100644 (file)
index 0000000..3b402a6
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M444-164q-27-11-40-41t2-59q8-15 40-80.5t70-145q38-79.5 74.5-155T643-754q3-7 10.5-10t15.5-1q8 2 13 9t3 15q-9 36-29.5 119T613-451.5q-22 87.5-41 159T547-206q-12 30-43 41.5t-60 .5Zm484-393q-13 13-31.5 13T864-556q-34-29-71-54t-71-41l23-90q54 24 98.5 55t84.5 66q14 12 14 30.5T928-557Zm-896 0q-13-13-13.5-32T32-620q92-84 207-132t241-48q24 0 54.5 2.5T594-790l-42 85q-16-2-34-3.5t-38-1.5q-108 0-204.5 41.5T96-556q-14 12-32.5 12T32-557Zm727 169q-13 13-30.5 13T696-387q-11-8-19-14t-14-11l22-92q16 9 34.5 22.5T759-450q14 12 14 30t-14 32Zm-558 0q-14-14-13-32.5t13-29.5q61-53 129-81.5T483-560l-45 93q-49 6-92.5 27T264-387q-15 12-32.5 12T201-388Z"/></svg>
\ No newline at end of file
diff --git a/src/gui/private.svg b/src/gui/private.svg
new file mode 100644 (file)
index 0000000..c08eb3d
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9 4H8.5V3C8.5 1.62 7.38 0.5 6 0.5C4.62 0.5 3.5 1.62 3.5 3V4H3C2.45 4 2 4.45 2 5V10C2 10.55 2.45 11 3 11H9C9.55 11 10 10.55 10 10V5C10 4.45 9.55 4 9 4ZM4.45 3C4.45 2.145 5.145 1.45 6 1.45C6.855 1.45 7.55 2.145 7.55 3V4H4.45V3ZM8 8H6.5V9.5H5.5V8H4V7H5.5V5.5H6.5V7H8V8Z" fill="#FAFBFB"/>
+</svg>
diff --git a/src/gui/public.svg b/src/gui/public.svg
new file mode 100644 (file)
index 0000000..1407d57
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6 1C3.24 1 1 3.24 1 6C1 8.76 3.24 11 6 11C8.76 11 11 8.76 11 6C11 3.24 8.76 1 6 1ZM5.5 9.965C3.525 9.72 2 8.04 2 6C2 5.69 2.04 5.395 2.105 5.105L4.5 7.5V8C4.5 8.55 4.95 9 5.5 9V9.965ZM8.95 8.695C8.82 8.29 8.45 8 8 8H7.5V6.5C7.5 6.225 7.275 6 7 6H4V5H5C5.275 5 5.5 4.775 5.5 4.5V3.5H6.5C7.05 3.5 7.5 3.05 7.5 2.5V2.295C8.965 2.89 10 4.325 10 6C10 7.04 9.6 7.985 8.95 8.695Z" fill="#FAFBFB"/>
+</svg>
diff --git a/src/gui/qjacktrip.cpp b/src/gui/qjacktrip.cpp
new file mode 100644 (file)
index 0000000..0cfbabe
--- /dev/null
@@ -0,0 +1,2046 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2020 Aaron Wyatt.
+
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#include "qjacktrip.h"
+
+#include <QFileDialog>
+#include <QHostAddress>
+#include <QMessageBox>
+#include <QProcess>
+#include <QSettings>
+#include <QVector>
+#include <cstdlib>
+#include <ctime>
+
+#include "about.h"
+#ifndef NO_VS
+#include "virtualstudio.h"
+#endif
+#include "ui_qjacktrip.h"
+#ifdef USE_WEAK_JACK
+#include "weak_libjack.h"
+#endif
+
+#ifdef RT_AUDIO
+#include "../RtAudioInterface.h"
+#include "RtAudio.h"
+#endif
+
+#include "../Compressor.h"
+#include "../CompressorPresets.h"
+#include "../Limiter.h"
+#include "../Meter.h"
+#include "../Reverb.h"
+
+QJackTrip::QJackTrip(QSharedPointer<Settings> settings, bool suppressCommandlineWarning,
+                     QWidget* parent)
+    : QMainWindow(parent)
+    , m_ui(new Ui::QJackTrip)
+    , m_netManager(new QNetworkAccessManager(this))
+    , m_statsDialog(new MessageDialog(this, QStringLiteral("Stats")))
+    , m_debugDialog(new MessageDialog(this, QStringLiteral("Debug"), 2))
+    , m_realCout(std::cout.rdbuf())
+    , m_realCerr(std::cerr.rdbuf())
+    , m_jackTripRunning(false)
+    , m_isExiting(false)
+    , m_exitSent(false)
+    , m_suppressCommandlineWarning(suppressCommandlineWarning)
+    , m_hideWarning(false)
+{
+    m_ui->setupUi(this);
+    m_cliSettings = settings;
+
+    // Set up our debug window, and relay everything to our real cout.
+    std::cout.rdbuf(m_debugDialog->getOutputStream()->rdbuf());
+    std::cerr.rdbuf(m_debugDialog->getOutputStream(1)->rdbuf());
+    m_debugDialog->setRelayStream(&m_realCout);
+    m_debugDialog->setRelayStream(&m_realCerr, 1);
+
+    // Create all our UI connections.
+    connect(m_ui->typeComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
+            &QJackTrip::chooseRunType);
+    connect(m_ui->addressComboBox, &QComboBox::currentTextChanged, this,
+            &QJackTrip::addressChanged);
+    connect(m_ui->connectButton, &QPushButton::clicked, this, &QJackTrip::start);
+    connect(m_ui->disconnectButton, &QPushButton::clicked, this, &QJackTrip::stop);
+    connect(m_ui->exitButton, &QPushButton::clicked, this, &QJackTrip::exit);
+    connect(m_ui->certBrowse, &QPushButton::clicked, this, &QJackTrip::browseForFile);
+    connect(m_ui->keyBrowse, &QPushButton::clicked, this, &QJackTrip::browseForFile);
+    connect(m_ui->credsBrowse, &QPushButton::clicked, this, &QJackTrip::browseForFile);
+    connect(m_ui->commandLineButton, &QPushButton::clicked, this,
+            &QJackTrip::showCommandLineMessageBox);
+    connect(m_ui->useDefaultsButton, &QPushButton::clicked, this,
+            &QJackTrip::resetOptions);
+    connect(m_ui->usernameEdit, &QLineEdit::textChanged, this,
+            &QJackTrip::credentialsChanged);
+    connect(m_ui->passwordEdit, &QLineEdit::textChanged, this,
+            &QJackTrip::credentialsChanged);
+    connect(m_ui->certEdit, &QLineEdit::textChanged, this, &QJackTrip::authFilesChanged);
+    connect(m_ui->keyEdit, &QLineEdit::textChanged, this, &QJackTrip::authFilesChanged);
+    connect(m_ui->credsEdit, &QLineEdit::textChanged, this, &QJackTrip::authFilesChanged);
+    connect(m_ui->aboutButton, &QPushButton::clicked, this, [=]() {
+        About about(this);
+        about.exec();
+    });
+#ifdef NO_VS
+    m_ui->authNotVSLabel->setText(
+        QStringLiteral("(This is for JackTrip's inbuilt authentication system. To easily "
+                       "connect to a Virtual Studio server, download a Virtual Studio "
+                       "enabled version of JackTrip.)"));
+#else
+    connect(m_ui->vsModeButton, &QPushButton::clicked, this,
+            &QJackTrip::virtualStudioMode);
+#endif
+    connect(m_ui->autoPatchComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
+            this, [=]() {
+                if (m_ui->autoPatchComboBox->currentIndex() == CLIENTFOFI
+                    || m_ui->autoPatchComboBox->currentIndex() == FULLMIX) {
+                    m_ui->patchServerCheckBox->setEnabled(true);
+                } else {
+                    m_ui->patchServerCheckBox->setEnabled(false);
+                }
+            });
+    connect(m_ui->authCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        m_ui->usernameLabel->setEnabled(m_ui->authCheckBox->isChecked());
+        m_ui->usernameEdit->setEnabled(m_ui->authCheckBox->isChecked());
+        m_ui->passwordLabel->setEnabled(m_ui->authCheckBox->isChecked());
+        m_ui->passwordEdit->setEnabled(m_ui->authCheckBox->isChecked());
+        credentialsChanged();
+    });
+    connect(m_ui->requireAuthCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        m_ui->certLabel->setEnabled(m_ui->requireAuthCheckBox->isChecked());
+        m_ui->certEdit->setEnabled(m_ui->requireAuthCheckBox->isChecked());
+        m_ui->certBrowse->setEnabled(m_ui->requireAuthCheckBox->isChecked());
+        m_ui->keyLabel->setEnabled(m_ui->requireAuthCheckBox->isChecked());
+        m_ui->keyEdit->setEnabled(m_ui->requireAuthCheckBox->isChecked());
+        m_ui->keyBrowse->setEnabled(m_ui->requireAuthCheckBox->isChecked());
+        m_ui->credsLabel->setEnabled(m_ui->requireAuthCheckBox->isChecked());
+        m_ui->credsEdit->setEnabled(m_ui->requireAuthCheckBox->isChecked());
+        m_ui->credsBrowse->setEnabled(m_ui->requireAuthCheckBox->isChecked());
+        authFilesChanged();
+    });
+    connect(m_ui->ioStatsCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        m_ui->ioStatsLabel->setEnabled(m_ui->ioStatsCheckBox->isChecked());
+        m_ui->ioStatsSpinBox->setEnabled(m_ui->ioStatsCheckBox->isChecked());
+        if (!m_ui->ioStatsCheckBox->isChecked()) {
+            m_statsDialog->hide();
+        }
+    });
+    connect(m_ui->verboseCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        gVerboseFlag = m_ui->verboseCheckBox->isChecked();
+        if (!gVerboseFlag) {
+            m_debugDialog->hide();
+            m_debugDialog->clearOutput();
+        }
+    });
+    connect(m_ui->jitterCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        m_ui->broadcastCheckBox->setEnabled(m_ui->jitterCheckBox->isChecked());
+        m_ui->broadcastQueueLabel->setEnabled(m_ui->jitterCheckBox->isChecked()
+                                              && m_ui->broadcastCheckBox->isChecked());
+        m_ui->broadcastQueueSpinBox->setEnabled(m_ui->jitterCheckBox->isChecked()
+                                                && m_ui->broadcastCheckBox->isChecked());
+        m_ui->bufferStrategyLabel->setEnabled(m_ui->jitterCheckBox->isChecked());
+        m_ui->bufferStrategyComboBox->setEnabled(m_ui->jitterCheckBox->isChecked());
+        // m_ui->strategyExplanationLabel->setEnabled(m_ui->jitterCheckBox->isChecked());
+        m_ui->bufferLine->setEnabled(m_ui->jitterCheckBox->isChecked());
+        m_ui->autoQueueCheckBox->setEnabled(m_ui->jitterCheckBox->isChecked());
+        m_ui->autoQueueLabel->setEnabled(m_ui->jitterCheckBox->isChecked()
+                                         && m_ui->autoQueueCheckBox->isChecked());
+        m_ui->autoQueueSpinBox->setEnabled(m_ui->jitterCheckBox->isChecked()
+                                           && m_ui->autoQueueCheckBox->isChecked());
+        m_ui->packetsLabel->setEnabled(m_ui->jitterCheckBox->isChecked()
+                                       && m_ui->autoQueueCheckBox->isChecked());
+        m_ui->autoQueueExplanationLabel->setEnabled(
+            m_ui->jitterCheckBox->isChecked() && m_ui->autoQueueCheckBox->isChecked());
+        if (m_ui->jitterCheckBox->isChecked() && m_ui->autoQueueCheckBox->isChecked()) {
+            m_autoQueueIndicator.setText(QStringLiteral("Auto queue: enabled"));
+        } else {
+            m_autoQueueIndicator.setText(QStringLiteral("Auto queue: disabled"));
+        }
+    });
+    connect(m_ui->broadcastCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        m_ui->broadcastQueueLabel->setEnabled(m_ui->jitterCheckBox->isChecked()
+                                              && m_ui->broadcastCheckBox->isChecked());
+        m_ui->broadcastQueueSpinBox->setEnabled(m_ui->jitterCheckBox->isChecked()
+                                                && m_ui->broadcastCheckBox->isChecked());
+    });
+    connect(m_ui->autoQueueCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        m_ui->autoQueueLabel->setEnabled(m_ui->jitterCheckBox->isChecked()
+                                         && m_ui->autoQueueCheckBox->isChecked());
+        m_ui->autoQueueSpinBox->setEnabled(m_ui->jitterCheckBox->isChecked()
+                                           && m_ui->autoQueueCheckBox->isChecked());
+        m_ui->packetsLabel->setEnabled(m_ui->jitterCheckBox->isChecked()
+                                       && m_ui->autoQueueCheckBox->isChecked());
+        m_ui->autoQueueExplanationLabel->setEnabled(
+            m_ui->jitterCheckBox->isChecked() && m_ui->autoQueueCheckBox->isChecked());
+        if (m_ui->jitterCheckBox->isChecked() && m_ui->autoQueueCheckBox->isChecked()) {
+            m_autoQueueIndicator.setText(QStringLiteral("Auto queue: enabled"));
+        } else {
+            m_autoQueueIndicator.setText(QStringLiteral("Auto queue: disabled"));
+        }
+    });
+
+    connect(m_ui->inFreeverbCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        m_ui->inFreeverbLabel->setEnabled(m_ui->inFreeverbCheckBox->isChecked());
+        m_ui->inFreeverbWetnessSlider->setEnabled(m_ui->inFreeverbCheckBox->isChecked());
+    });
+    connect(m_ui->inZitarevCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        m_ui->inZitarevLabel->setEnabled(m_ui->inZitarevCheckBox->isChecked());
+        m_ui->inZitarevWetnessSlider->setEnabled(m_ui->inZitarevCheckBox->isChecked());
+    });
+
+    connect(m_ui->outFreeverbCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        m_ui->outFreeverbLabel->setEnabled(m_ui->outFreeverbCheckBox->isChecked());
+        m_ui->outFreeverbWetnessSlider->setEnabled(
+            m_ui->outFreeverbCheckBox->isChecked());
+    });
+    connect(m_ui->outZitarevCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        m_ui->outZitarevLabel->setEnabled(m_ui->outZitarevCheckBox->isChecked());
+        m_ui->outZitarevWetnessSlider->setEnabled(m_ui->outZitarevCheckBox->isChecked());
+    });
+    connect(m_ui->outLimiterCheckBox, &QCheckBox::stateChanged, this, [=]() {
+        m_ui->outLimiterLabel->setEnabled(m_ui->outLimiterCheckBox->isChecked());
+        m_ui->outClientsSpinBox->setEnabled(m_ui->outLimiterCheckBox->isChecked());
+    });
+
+    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.
+    //(loadSettings will take care of the UI in all other cases.)
+    m_ui->basePortLabel->setVisible(false);
+    m_ui->basePortSpinBox->setVisible(false);
+    m_ui->autoPatchGroupBox->setVisible(false);
+    m_ui->requireAuthGroupBox->setVisible(false);
+    m_ui->backendWarningLabel->setVisible(false);
+    m_ui->vsModeButton->setVisible(false);
+    m_ui->inputGroupBox->setVisible(false);
+    m_ui->outputGroupBox->setVisible(false);
+
+    m_inputLayout.reset(new QGridLayout(m_ui->inputGroupBox));
+    m_outputLayout.reset(new QGridLayout(m_ui->outputGroupBox));
+
+#ifdef RT_AUDIO
+    connect(m_ui->backendComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
+            this, [=](int index) {
+                if (index == 1) {
+                    m_ui->sampleRateComboBox->setEnabled(true);
+                    m_ui->sampleRateLabel->setEnabled(true);
+                    m_ui->bufferSizeComboBox->setEnabled(true);
+                    m_ui->bufferSizeLabel->setEnabled(true);
+                    m_ui->inputDeviceComboBox->setEnabled(true);
+                    m_ui->inputDeviceLabel->setEnabled(true);
+                    m_ui->outputDeviceComboBox->setEnabled(true);
+                    m_ui->outputDeviceLabel->setEnabled(true);
+                    m_ui->refreshDevicesButton->setEnabled(true);
+                    m_ui->backendWarningLabel->setVisible(true);
+                    populateDeviceMenu(m_ui->inputDeviceComboBox, true);
+                    populateDeviceMenu(m_ui->outputDeviceComboBox, false);
+                } else {
+                    m_ui->sampleRateComboBox->setEnabled(false);
+                    m_ui->sampleRateLabel->setEnabled(false);
+                    m_ui->bufferSizeComboBox->setEnabled(false);
+                    m_ui->bufferSizeLabel->setEnabled(false);
+                    m_ui->inputDeviceComboBox->setEnabled(false);
+                    m_ui->inputDeviceLabel->setEnabled(false);
+                    m_ui->outputDeviceComboBox->setEnabled(false);
+                    m_ui->outputDeviceLabel->setEnabled(false);
+                    m_ui->refreshDevicesButton->setEnabled(false);
+                    m_ui->backendWarningLabel->setVisible(false);
+                }
+            });
+    connect(m_ui->refreshDevicesButton, &QPushButton::clicked, this, [=]() {
+        populateDeviceMenu(m_ui->inputDeviceComboBox, true);
+        populateDeviceMenu(m_ui->outputDeviceComboBox, false);
+    });
+#else
+    int tabIndex = findTab(QStringLiteral("Audio Backend"));
+    if (tabIndex != -1) {
+        m_ui->optionsTabWidget->removeTab(tabIndex);
+    }
+#endif
+
+    migrateSettings();
+    m_ui->optionsTabWidget->setCurrentIndex(0);
+
+    QVector<QLabel*> labels;
+    labels << m_ui->inFreeverbLabel << m_ui->inZitarevLabel << m_ui->outFreeverbLabel;
+    std::srand(std::time(nullptr));
+    int index = std::rand() % 4;
+    if (index < 3) {
+        labels.at(index)->setToolTip(m_ui->outZitarevLabel->toolTip());
+        m_ui->outZitarevLabel->setToolTip(QLatin1String(""));
+    }
+}
+
+void QJackTrip::closeEvent(QCloseEvent* event)
+{
+    if (!m_exitSent) {
+        // Ignore the close event so that we can override the handling of it.
+        event->ignore();
+        exit();
+    }
+}
+
+void QJackTrip::resizeEvent(QResizeEvent* event)
+{
+    QMainWindow::resizeEvent(event);
+    // We need to fix the layout of our word wrapped labels.
+    // The font should be the same for all of them so we can reuse the one QFontMetrics
+    // object
+    QFontMetrics metrics(m_ui->autoQueueExplanationLabel->font());
+    // This seems like a convoluted way to get what is effectively our layout geometry,
+    // but until we look at the jitter tab, the layout geometry is unset.
+    int width = m_ui->JitterTab->contentsRect().width()
+                - m_ui->JitterTab->contentsMargins().left()
+                - m_ui->JitterTab->contentsMargins().right()
+                - m_ui->JitterTab->layout()->contentsMargins().left()
+                - m_ui->JitterTab->layout()->contentsMargins().right();
+
+    /*QRect rect = metrics.boundingRect(0, 0, width, 0, Qt::TextWordWrap,
+    m_ui->strategyExplanationLabel->text());
+    m_ui->strategyExplanationLabel->setMinimumHeight(rect.height());*/
+    QRect rect = metrics.boundingRect(0, 0, width, 0, Qt::TextWordWrap,
+                                      m_ui->autoQueueExplanationLabel->text());
+    m_ui->autoQueueExplanationLabel->setMinimumHeight(rect.height());
+
+    width = m_ui->requireAuthGroupBox->contentsRect().width()
+            - m_ui->requireAuthGroupBox->contentsMargins().left()
+            - m_ui->requireAuthGroupBox->contentsMargins().right()
+            - m_ui->requireAuthGroupBox->layout()->contentsMargins().left()
+            - m_ui->requireAuthGroupBox->contentsMargins().right();
+    rect = metrics.boundingRect(0, 0, width, 0, Qt::TextWordWrap,
+                                m_ui->authDisclaimerLabel->text());
+    m_ui->authDisclaimerLabel->setMinimumHeight(rect.height());
+
+    width = m_ui->authGroupBox->contentsRect().width()
+            - m_ui->authGroupBox->contentsMargins().left()
+            - m_ui->authGroupBox->contentsMargins().right()
+            - m_ui->authGroupBox->layout()->contentsMargins().left()
+            - m_ui->authGroupBox->contentsMargins().right();
+    rect = metrics.boundingRect(0, 0, width, 0, Qt::TextWordWrap,
+                                m_ui->authNotVSLabel->text());
+    m_ui->authNotVSLabel->setMinimumHeight(rect.height());
+
+    // The previous minimum heights should protect any further word wrapped labels,
+    // but it's worth including any additional ones here for future proofing.
+    width = m_ui->scriptingTab->contentsRect().width()
+            - m_ui->scriptingTab->contentsMargins().left()
+            - m_ui->scriptingTab->contentsMargins().right()
+            - m_ui->scriptingTab->layout()->contentsMargins().left()
+            - m_ui->scriptingTab->contentsMargins().right();
+    rect = metrics.boundingRect(0, 0, width, 0, Qt::TextWordWrap,
+                                m_ui->environmentVariableLabel->text());
+    m_ui->environmentVariableLabel->setMinimumHeight(rect.height());
+}
+
+void QJackTrip::showEvent(QShowEvent* event)
+{
+    // We need to wait to load geometry until here rather than with our other settings.
+    // If we don't, the window geometry will be improperly set on macOS whenever the
+    // VirtualStudio window is shown first.
+    QMainWindow::showEvent(event);
+    if (m_firstShow) {
+        QSettings settings;
+        loadSettings(m_cliSettings.data());
+
+        // Display a warning about any ignored command line options.
+        if (m_cliSettings->guiIgnoresArguments() && !m_suppressCommandlineWarning) {
+            QMessageBox msgBox;
+            msgBox.setText(
+                "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();
+        }
+
+        // Add an autoqueue indicator to the status bar.
+        m_ui->statusBar->addPermanentWidget(&m_autoQueueIndicator);
+        if (m_ui->jitterCheckBox->isChecked() && m_ui->autoQueueCheckBox->isChecked()) {
+            m_autoQueueIndicator.setText(QStringLiteral("Auto queue: enabled"));
+        } else {
+            m_autoQueueIndicator.setText(QStringLiteral("Auto queue: disabled"));
+        }
+
+#ifdef USE_WEAK_JACK
+        // Check if Jack is actually available
+        if (have_libjack() != 0) {
+#ifdef RT_AUDIO
+            bool usingRtAudioAlready = m_ui->backendComboBox->currentIndex() == 1;
+            m_ui->backendComboBox->setCurrentIndex(1);
+            m_ui->backendComboBox->setEnabled(false);
+            m_ui->backendLabel->setEnabled(false);
+
+            // If we're in Hub Server mode, switch us back to P2P server mode.
+            if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) {
+                m_ui->typeComboBox->setCurrentIndex(P2P_SERVER);
+            }
+            m_ui->typeComboBox->removeItem(HUB_SERVER);
+            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.");
+
+            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.
+            settings.beginGroup(QStringLiteral("Audio"));
+            if (settings.value(QStringLiteral("UsingFallback"), false).toBool()) {
+                m_ui->backendComboBox->setCurrentIndex(0);
+                settings.setValue(QStringLiteral("UsingFallback"), false);
+            }
+            settings.endGroup();
+#else   // RT_AUDIO
+            QMessageBox msgBox;
+            msgBox.setText(
+                "An installation of JACK was not found, and no other audio backends are "
+                "available. JackTrip will not be able to start. (Please install JACK to "
+                "fix this.)");
+            msgBox.setWindowTitle("JACK Not Available");
+            msgBox.exec();
+#endif  // RT_AUDIO
+        }
+#endif  // USE_WEAK_JACK
+
+        settings.beginGroup(QStringLiteral("Window"));
+        QByteArray geometry = settings.value(QStringLiteral("Geometry")).toByteArray();
+        if (geometry.size() > 0) {
+            restoreGeometry(geometry);
+        } else {
+            // Because of hidden elements in our dialog window, it's vertical size in the
+            // creator is getting rediculous. Set it to something sensible by default if
+            // this is our first load.
+            this->resize(QSize(this->size().height(), 600));
+        }
+        settings.endGroup();
+
+        // 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"))));
+        m_firstShow = false;
+    }
+}
+
+#ifndef NO_VS
+void QJackTrip::setVs(QSharedPointer<VirtualStudio> vs)
+{
+    m_vs = vs;
+    m_ui->vsModeButton->setVisible(!m_vs.isNull());
+}
+#endif
+
+void QJackTrip::processFinished()
+{
+    if (!m_jackTripRunning) {
+        // Don't execute this if our process isn't actually running.
+        return;
+    }
+    m_jackTripRunning = false;
+#ifdef __APPLE__
+    m_noNap.enableNap();
+#endif
+    m_ui->disconnectButton->setEnabled(false);
+    if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) {
+        m_udpHub.reset();
+    } 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());
+            QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
+            env.insert(QStringLiteral("JT_CLIENT_NAME"), m_assignedClientName);
+            env.insert(QStringLiteral("JT_SEND_CHANNELS"),
+                       QString::number(m_ui->channelSendSpinBox->value()));
+            env.insert(QStringLiteral("JT_RECV_CHANNELS"),
+                       QString::number(m_ui->channelRecvSpinBox->value()));
+            disconnectScript.setProcessEnvironment(env);
+            disconnectScript.startDetached();
+        }
+    }
+
+    if (m_isExiting) {
+        m_exitSent = true;
+        emit signalExit();
+    } else {
+        enableUi(true);
+        m_ui->connectButton->setEnabled(true);
+        m_ui->statusBar->showMessage(QStringLiteral("JackTrip Processes Stopped"), 2000);
+    }
+}
+
+void QJackTrip::processError(const QString& errorMessage)
+{
+    QMessageBox msgBox;
+    if (errorMessage == QLatin1String("Peer Stopped")) {
+        // Report the other end quitting as a regular occurrence rather than an error.
+        msgBox.setText(errorMessage);
+        msgBox.setWindowTitle(QStringLiteral("Disconnected"));
+    } else {
+        msgBox.setText(QStringLiteral("Error: ").append(errorMessage));
+        msgBox.setWindowTitle(QStringLiteral("Doh!"));
+    }
+    msgBox.exec();
+    processFinished();
+}
+
+void QJackTrip::receivedConnectionFromPeer()
+{
+    m_ui->statusBar->showMessage(QStringLiteral("Received Connection from Peer!"));
+    m_assignedClientName = m_jackTrip->getAssignedClientName();
+    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());
+            QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
+            env.insert(QStringLiteral("JT_CLIENT_NAME"), m_assignedClientName);
+            env.insert(QStringLiteral("JT_SEND_CHANNELS"),
+                       QString::number(m_ui->channelSendSpinBox->value()));
+            env.insert(QStringLiteral("JT_RECV_CHANNELS"),
+                       QString::number(m_ui->channelRecvSpinBox->value()));
+            connectScript.setProcessEnvironment(env);
+            connectScript.startDetached();
+        }
+    }
+}
+
+void QJackTrip::queueLengthChanged(int queueLength)
+{
+    m_autoQueueIndicator.setText(QStringLiteral("Auto queue: %1").arg(queueLength));
+}
+
+void QJackTrip::udpWaitingTooLong()
+{
+    m_ui->statusBar->showMessage(QStringLiteral("UDP waiting too long (more than 30ms)"),
+                                 1000);
+}
+
+void QJackTrip::chooseRunType(int index)
+{
+    // Update ui to reflect choice of run mode.
+    if (index == HUB_CLIENT || index == P2P_CLIENT) {
+        m_ui->addressComboBox->setEnabled(true);
+        m_ui->addressLabel->setEnabled(true);
+        if (index == HUB_CLIENT) {
+            credentialsChanged();
+        } else {
+            m_ui->connectButton->setEnabled(
+                !m_ui->addressComboBox->currentText().isEmpty());
+        }
+        m_ui->remotePortSpinBox->setVisible(true);
+        m_ui->remotePortLabel->setVisible(true);
+        m_ui->connectButton->setText(QStringLiteral("Connect"));
+        m_ui->disconnectButton->setText(QStringLiteral("Disconnect"));
+    } else {
+        m_ui->addressComboBox->setEnabled(false);
+        m_ui->addressLabel->setEnabled(false);
+        m_ui->remotePortSpinBox->setVisible(false);
+        m_ui->remotePortLabel->setVisible(false);
+        m_ui->connectButton->setText(QStringLiteral("Start"));
+        m_ui->disconnectButton->setText(QStringLiteral("Stop"));
+        m_ui->connectButton->setEnabled(true);
+    }
+
+    if (index == HUB_SERVER) {
+        m_ui->channelGroupBox->setVisible(false);
+        m_ui->timeoutCheckBox->setVisible(false);
+        m_ui->autoPatchGroupBox->setVisible(true);
+        m_ui->requireAuthGroupBox->setVisible(true);
+        advancedOptionsForHubServer(true);
+        int index = findTab(QStringLiteral("Plugins"));
+        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"));
+        if (index != -1) {
+            m_ui->optionsTabWidget->removeTab(index);
+        }
+#endif
+    } else {
+        m_ui->autoPatchGroupBox->setVisible(false);
+        m_ui->requireAuthGroupBox->setVisible(false);
+        m_ui->channelGroupBox->setVisible(true);
+        m_ui->timeoutCheckBox->setVisible(true);
+        advancedOptionsForHubServer(false);
+        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,
+                                              QStringLiteral("Audio Backend"));
+        }
+#endif
+    }
+
+    if (index == HUB_CLIENT) {
+        m_ui->remoteNameEdit->setVisible(true);
+        m_ui->remoteNameLabel->setVisible(true);
+        m_ui->authGroupBox->setVisible(true);
+    } else {
+        m_ui->remoteNameEdit->setVisible(false);
+        m_ui->remoteNameLabel->setVisible(false);
+        m_ui->authGroupBox->setVisible(false);
+    }
+}
+
+void QJackTrip::addressChanged(const QString& address)
+{
+    // Make sure we check that JackTrip isn't running.
+    //(This also gets called when we save our recent address list on connecting to a
+    // server.)
+    if (m_jackTripRunning) {
+        return;
+    }
+    if (m_ui->typeComboBox->currentIndex() == P2P_CLIENT) {
+        m_ui->connectButton->setEnabled(!address.isEmpty());
+    } else if (m_ui->typeComboBox->currentIndex() == HUB_CLIENT) {
+        credentialsChanged();
+    }
+}
+
+void QJackTrip::authFilesChanged()
+{
+    if (m_ui->typeComboBox->currentIndex() != HUB_SERVER) {
+        return;
+    }
+
+    if (m_ui->requireAuthCheckBox->isChecked()
+        && (m_ui->certEdit->text().isEmpty() || m_ui->keyEdit->text().isEmpty()
+            || m_ui->credsEdit->text().isEmpty())) {
+        m_ui->connectButton->setEnabled(false);
+    } else {
+        m_ui->connectButton->setEnabled(true);
+    }
+}
+
+void QJackTrip::credentialsChanged()
+{
+    if (m_ui->typeComboBox->currentIndex() != HUB_CLIENT) {
+        return;
+    }
+
+    if (m_ui->authCheckBox->isChecked()
+        && (m_ui->usernameEdit->text().isEmpty()
+            || m_ui->passwordEdit->text().isEmpty())) {
+        m_ui->connectButton->setEnabled(false);
+    } else {
+        m_ui->connectButton->setEnabled(!m_ui->addressComboBox->currentText().isEmpty());
+    }
+}
+
+void QJackTrip::browseForFile()
+{
+    QPushButton* sender = static_cast<QPushButton*>(QObject::sender());
+    QString fileType;
+    QLineEdit* fileEdit;
+    if (sender == m_ui->certBrowse) {
+        fileType = QStringLiteral("Certificates (*.crt *.pem)");
+        fileEdit = m_ui->certEdit;
+    } else if (sender == m_ui->keyBrowse) {
+        fileType = QStringLiteral("Keys (*.key *.pem)");
+        fileEdit = m_ui->keyEdit;
+    } else {
+        fileType = QLatin1String("");
+        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);
+    if (!fileName.isEmpty()) {
+        fileEdit->setText(fileName);
+        fileEdit->setFocus(Qt::OtherFocusReason);
+        m_lastPath = QFileInfo(fileName).canonicalPath();
+    }
+}
+
+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) {
+            m_IPv6Address = QString(reply->readAll());
+            // Make sure this isn't just a repeat of our IPv4 address.
+            if (QHostAddress(m_IPv6Address).protocol() != QAbstractSocket::IPv6Protocol) {
+                m_IPv6Address.clear();
+                reply->deleteLater();
+                return;
+            }
+        }
+    } else {
+        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 {
+            m_ui->ipLabel->setText(
+                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)));
+            }
+        }
+    }
+
+    reply->deleteLater();
+}
+
+void QJackTrip::resetOptions()
+{
+    // Reset our basic options
+    /*m_ui->channelSpinBox->setValue(2);
+    m_ui->autoPatchComboBox->setCurrentIndex(0);
+    m_ui->zeroCheckBox->setChecked(false);
+    m_ui->timeoutCheckBox->setChecked(false);*/
+
+    // Then advanced options
+    m_ui->clientNameEdit->setText(QLatin1String(""));
+    m_ui->remoteNameEdit->setText(QLatin1String(""));
+    m_ui->localPortSpinBox->setValue(gDefaultPort);
+    m_ui->remotePortSpinBox->setValue(gDefaultPort);
+    m_ui->basePortSpinBox->setValue(61002);
+    m_ui->queueLengthSpinBox->setValue(gDefaultQueueLength);
+    m_ui->redundancySpinBox->setValue(gDefaultRedundancy);
+    m_ui->resolutionComboBox->setCurrentIndex(1);
+    m_ui->connectAudioCheckBox->setChecked(true);
+    m_ui->realTimeCheckBox->setChecked(true);
+    m_ui->ioStatsCheckBox->setChecked(false);
+    m_ui->ioStatsSpinBox->setValue(1);
+    m_ui->verboseCheckBox->setChecked(false);
+
+    saveSettings();
+}
+
+void QJackTrip::start()
+{
+    m_ui->connectButton->setEnabled(false);
+    enableUi(false);
+    m_jackTripRunning = true;
+
+    if (gVerboseFlag) {
+        m_debugDialog->show();
+    }
+
+    // Start the appropriate JackTrip process.
+    try {
+        AudioInterface::audioBitResolutionT resolution;
+        if (m_ui->resolutionComboBox->currentIndex() == 0) {
+            resolution = AudioInterface::BIT8;
+        } else if (m_ui->resolutionComboBox->currentIndex() == 1) {
+            resolution = AudioInterface::BIT16;
+        } else if (m_ui->resolutionComboBox->currentIndex() == 2) {
+            resolution = AudioInterface::BIT24;
+        } else {
+            resolution = AudioInterface::BIT32;
+        }
+
+        if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) {
+            m_udpHub.reset(new UdpHubListener(m_ui->localPortSpinBox->value(),
+                                              m_ui->basePortSpinBox->value()));
+            int hubConnectionMode = hubModeFromPatchType(
+                static_cast<patchTypeT>(m_ui->autoPatchComboBox->currentIndex()));
+            if (m_ui->patchServerCheckBox->isChecked()) {
+                if (m_ui->autoPatchComboBox->currentIndex() == CLIENTFOFI) {
+                    hubConnectionMode = JackTrip::SERVFOFI;
+                } else if (m_ui->autoPatchComboBox->currentIndex() == FULLMIX) {
+                    hubConnectionMode = JackTrip::SERVFULLMIX;
+                }
+            }
+
+            m_udpHub->setHubPatch(hubConnectionMode);
+            m_udpHub->setStereoUpmix(m_ui->upmixCheckBox->isChecked());
+
+            if (m_ui->zeroCheckBox->isChecked()) {
+                // Set buffers to zero when underrun
+                m_udpHub->setUnderRunMode(JackTrip::ZEROS);
+            }
+            m_udpHub->setAudioBitResolution(resolution);
+
+            if (!m_ui->jitterCheckBox->isChecked()) {
+                m_udpHub->setBufferStrategy(-1);
+                m_udpHub->setBufferQueueLength(m_ui->queueLengthSpinBox->value());
+            } else {
+                m_udpHub->setBufferStrategy(m_ui->bufferStrategyComboBox->currentIndex()
+                                            + 1);
+                if (m_ui->broadcastCheckBox->isChecked()) {
+                    m_udpHub->setBroadcast(m_ui->broadcastQueueSpinBox->value());
+                }
+                if (m_ui->autoQueueCheckBox->isChecked()) {
+                    m_udpHub->setBufferQueueLength(-(m_ui->autoQueueSpinBox->value()));
+                    m_autoQueueIndicator.setText(QStringLiteral("Auto queue: enabled"));
+                } else {
+                    m_udpHub->setBufferQueueLength(m_ui->queueLengthSpinBox->value());
+                }
+            }
+            m_udpHub->setUseRtUdpPriority(m_ui->realTimeCheckBox->isChecked());
+
+            // Enable authentication if needed
+            if (m_ui->requireAuthCheckBox->isChecked()) {
+                m_udpHub->setRequireAuth(true);
+                m_udpHub->setCertFile(m_ui->certEdit->text());
+                m_udpHub->setKeyFile(m_ui->keyEdit->text());
+                m_udpHub->setCredsFile(m_ui->credsEdit->text());
+            }
+
+            // Open our stats window if needed
+            if (m_ui->ioStatsCheckBox->isChecked()) {
+                m_statsDialog->clearOutput();
+                m_statsDialog->show();
+                m_udpHub->setIOStatTimeout(m_ui->ioStatsSpinBox->value());
+                m_udpHub->setIOStatStream(m_statsDialog->getOutputStream());
+            }
+
+            QObject::connect(m_udpHub.data(), &UdpHubListener::signalStopped, this,
+                             &QJackTrip::processFinished, Qt::QueuedConnection);
+            QObject::connect(m_udpHub.data(), &UdpHubListener::signalError, this,
+                             &QJackTrip::processError, Qt::QueuedConnection);
+            m_ui->disconnectButton->setEnabled(true);
+            m_udpHub->start();
+            m_ui->statusBar->showMessage(QStringLiteral("Hub Server Started"));
+        } else {
+            JackTrip::jacktripModeT jackTripMode;
+            if (m_ui->typeComboBox->currentIndex() == P2P_CLIENT) {
+                jackTripMode = JackTrip::CLIENT;
+            } else if (m_ui->typeComboBox->currentIndex() == P2P_SERVER) {
+                jackTripMode = JackTrip::SERVER;
+            } else {
+                jackTripMode = JackTrip::CLIENTTOPINGSERVER;
+            }
+
+            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,
+#endif  // endwhere
+                m_ui->queueLengthSpinBox->value(), m_ui->redundancySpinBox->value(),
+                resolution));
+            m_jackTrip->setConnectDefaultAudioPorts(
+                m_ui->connectAudioCheckBox->isChecked());
+            if (m_ui->zeroCheckBox->isChecked()) {
+                // Set buffers to zero when underrun
+                m_jackTrip->setUnderRunMode(JackTrip::ZEROS);
+            }
+
+#ifdef RT_AUDIO
+            if (m_ui->backendComboBox->currentIndex() == 1) {
+                unsigned int bufferSize = m_ui->bufferSizeComboBox->currentText().toInt();
+                unsigned int sampleRate = m_ui->sampleRateComboBox->currentText().toInt();
+                m_jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO);
+                m_jackTrip->setSampleRate(sampleRate);
+                m_jackTrip->setAudioBufferSizeInSamples(bufferSize);
+                // we assume that first entry is "(default)"
+                if (m_ui->inputDeviceComboBox->currentIndex() == 0) {
+                    m_jackTrip->setInputDevice("");
+                } else {
+                    m_jackTrip->setInputDevice(
+                        m_ui->inputDeviceComboBox->currentText().toStdString());
+                }
+                if (m_ui->outputDeviceComboBox->currentIndex() == 0) {
+                    m_jackTrip->setOutputDevice("");
+                } else {
+                    m_jackTrip->setOutputDevice(
+                        m_ui->outputDeviceComboBox->currentText().toStdString());
+                }
+                AudioInterface::setPipewireLatency(bufferSize, sampleRate);
+            }
+#endif
+
+            if (m_ui->timeoutCheckBox->isChecked()) {
+                m_jackTrip->setStopOnTimeout(true);
+            }
+
+            if (m_ui->jitterCheckBox->isChecked()) {
+                m_jackTrip->setBufferStrategy(m_ui->bufferStrategyComboBox->currentIndex()
+                                              + 1);
+                if (m_ui->broadcastCheckBox->isChecked()) {
+                    m_jackTrip->setBroadcast(m_ui->broadcastQueueSpinBox->value());
+                }
+                if (m_ui->autoQueueCheckBox->isChecked()) {
+                    m_jackTrip->setBufferQueueLength(-(m_ui->autoQueueSpinBox->value()));
+                    m_autoQueueIndicator.setText(QStringLiteral("Auto queue: enabled"));
+                }
+            } else {
+                m_jackTrip->setBufferStrategy(-1);
+            }
+            m_jackTrip->setUseRtUdpPriority(m_ui->realTimeCheckBox->isChecked());
+
+            // Set peer address in client mode
+            if (jackTripMode == JackTrip::CLIENT
+                || jackTripMode == JackTrip::CLIENTTOPINGSERVER) {
+                m_jackTrip->setPeerAddress(
+                    m_ui->addressComboBox->currentText().trimmed());
+                if (jackTripMode == JackTrip::CLIENTTOPINGSERVER
+                    && !m_ui->remoteNameEdit->text().isEmpty()) {
+                    m_jackTrip->setRemoteClientName(m_ui->remoteNameEdit->text());
+                }
+            }
+
+            m_jackTrip->setBindPorts(m_ui->localPortSpinBox->value());
+            m_jackTrip->setPeerPorts(m_ui->remotePortSpinBox->value());
+            m_jackTrip->setPeerHandshakePort(m_ui->remotePortSpinBox->value());
+
+            if (!m_ui->clientNameEdit->text().isEmpty()) {
+                m_jackTrip->setClientName(m_ui->clientNameEdit->text());
+            }
+
+            // Set credentials if we're using authentication
+            if (m_ui->authCheckBox->isChecked()) {
+                m_jackTrip->setUseAuth(true);
+                m_jackTrip->setUsername(m_ui->usernameEdit->text());
+                m_jackTrip->setPassword(m_ui->passwordEdit->text());
+            }
+
+            // Open our stats window if needed
+            if (m_ui->ioStatsCheckBox->isChecked()) {
+                m_statsDialog->clearOutput();
+                m_statsDialog->show();
+                m_jackTrip->setIOStatTimeout(m_ui->ioStatsSpinBox->value());
+                m_jackTrip->setIOStatStream(m_statsDialog->getOutputStream());
+            }
+
+            // Append any plugins
+            appendPlugins(m_jackTrip.data(), m_ui->channelSendSpinBox->value(),
+                          m_ui->channelRecvSpinBox->value());
+            // Setup meters (also currently using faust plugins).
+            createMeters(m_ui->channelSendSpinBox->value(),
+                         (m_ui->channelRecvSpinBox->value()));
+
+            QObject::connect(m_jackTrip.data(), &JackTrip::signalProcessesStopped, this,
+                             &QJackTrip::processFinished, Qt::QueuedConnection);
+            QObject::connect(m_jackTrip.data(), &JackTrip::signalError, this,
+                             &QJackTrip::processError, Qt::QueuedConnection);
+            QObject::connect(
+                m_jackTrip.data(), &JackTrip::signalReceivedConnectionFromPeer, this,
+                &QJackTrip::receivedConnectionFromPeer, Qt::QueuedConnection);
+            QObject::connect(m_jackTrip.data(), &JackTrip::signalUdpWaitingTooLong, this,
+                             &QJackTrip::udpWaitingTooLong, Qt::QueuedConnection);
+            QObject::connect(m_jackTrip.data(), &JackTrip::signalQueueLengthChanged, this,
+                             &QJackTrip::queueLengthChanged, Qt::QueuedConnection);
+
+            m_ui->statusBar->showMessage(QStringLiteral("Waiting for Peer..."));
+            m_ui->disconnectButton->setEnabled(true);
+#ifdef WAIRTOHUB  // WAIR
+            m_jackTrip->startProcess(
+                0);  // for WAIR compatibility, ID in jack client name
+#else
+            m_jackTrip->startProcess();
+#endif  // endwhere
+        }
+    } catch (const std::exception& e) {
+        // Let the user know what our exception was.
+        QMessageBox msgBox;
+        msgBox.setText(QStringLiteral("Error: ").append(e.what()));
+        msgBox.setWindowTitle(QStringLiteral("Doh!"));
+        msgBox.exec();
+
+        m_jackTripRunning = false;
+        enableUi(true);
+        m_ui->connectButton->setEnabled(true);
+        m_ui->disconnectButton->setEnabled(false);
+        m_ui->statusBar->clearMessage();
+
+        return;
+    }
+
+    // Add the address to our server history.
+    QString serverAddress = m_ui->addressComboBox->currentText().trimmed();
+    int serverIndex       = m_ui->addressComboBox->findText(serverAddress);
+    if (serverIndex != -1) {
+        m_ui->addressComboBox->removeItem(serverIndex);
+    }
+    m_ui->addressComboBox->insertItem(0, serverAddress);
+    m_ui->addressComboBox->setCurrentIndex(0);
+
+#ifdef __APPLE__
+    m_noNap.disableNap();
+#endif
+}
+
+void QJackTrip::stop()
+{
+    m_ui->disconnectButton->setEnabled(false);
+    if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) {
+        m_udpHub->stop();
+    } else {
+        m_jackTrip->stop();
+    }
+}
+
+void QJackTrip::exit()
+{
+    // Only run this once.
+    if (m_isExiting) {
+        return;
+    }
+    m_isExiting = true;
+    m_ui->exitButton->setEnabled(false);
+    saveSettings();
+    if (m_jackTripRunning) {
+        stop();
+    } else {
+        m_exitSent = true;
+        emit signalExit();
+    }
+}
+
+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 < numChannels) {
+            dB = std::max(m_meterMin, valuesInDb[i]);
+        }
+
+        // Produce a normalized value from 0 to 1
+        float level = (dB - m_meterMin) / (m_meterMax - m_meterMin);
+        m_inputMeters.at(i)->setLevel(level);
+    }
+}
+
+void QJackTrip::updatedOutputMeasurements(const float* valuesInDb, int numChannels)
+{
+    for (int i = 0; i < m_outputMeters.count(); i++) {
+        // Determine decibel reading
+        qreal dB = m_meterMin;
+        if (i < numChannels) {
+            dB = std::max(m_meterMin, valuesInDb[i]);
+        }
+
+        // Produce a normalized value from 0 to 1
+        float level = (dB - m_meterMin) / (m_meterMax - m_meterMin);
+        m_outputMeters.at(i)->setLevel(level);
+    }
+}
+
+#ifndef NO_VS
+void QJackTrip::virtualStudioMode()
+{
+    this->hide();
+    m_vs->show();
+    m_vs->toVirtualStudio();
+}
+#endif
+
+int QJackTrip::findTab(const QString& tabName)
+{
+    for (int i = 0; i < m_ui->optionsTabWidget->count(); i++) {
+        if (m_ui->optionsTabWidget->tabText(i) == tabName) {
+            return i;
+        }
+    }
+    return -1;
+}
+
+void QJackTrip::enableUi(bool enabled)
+{
+    if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) {
+        m_ui->optionsTabWidget->setEnabled(enabled);
+    } else {
+        if (enabled) {
+            m_ui->inputGroupBox->setVisible(false);
+            m_ui->outputGroupBox->setVisible(false);
+            removeMeters();
+            m_ui->optionsTabWidget->setVisible(true);
+        } else {
+            m_ui->optionsTabWidget->setVisible(false);
+            m_ui->inputGroupBox->setVisible(true);
+            m_ui->outputGroupBox->setVisible(true);
+        }
+    }
+    m_ui->typeLabel->setEnabled(enabled);
+    m_ui->typeComboBox->setEnabled(enabled);
+    m_ui->addressLabel->setEnabled(
+        enabled
+        && (m_ui->typeComboBox->currentIndex() == P2P_CLIENT
+            || m_ui->typeComboBox->currentIndex() == HUB_CLIENT));
+    m_ui->addressComboBox->setEnabled(
+        enabled
+        && (m_ui->typeComboBox->currentIndex() == P2P_CLIENT
+            || m_ui->typeComboBox->currentIndex() == HUB_CLIENT));
+}
+
+void QJackTrip::advancedOptionsForHubServer(bool isHubServer)
+{
+    m_ui->clientNameLabel->setVisible(!isHubServer);
+    m_ui->clientNameEdit->setVisible(!isHubServer);
+    m_ui->redundancyLabel->setVisible(!isHubServer);
+    m_ui->redundancySpinBox->setVisible(!isHubServer);
+    m_ui->connectAudioCheckBox->setVisible(!isHubServer);
+    m_ui->basePortLabel->setVisible(isHubServer);
+    m_ui->basePortSpinBox->setVisible(isHubServer);
+    if (isHubServer) {
+        m_ui->localPortSpinBox->setToolTip(
+            "Set the local TCP port to use for the initial handshake connection. The "
+            "default is 4464.");
+    } else {
+        m_ui->localPortSpinBox->setToolTip(
+            "Set the local port to use for the connection. The default is 4464.\n(Useful "
+            "for running multiple hub clients behind the same router.)");
+    }
+}
+
+void QJackTrip::migrateSettings()
+{
+    // Function to migrate settings for users who previously had QJackTrip installed.
+    QSettings settings;
+    if (settings.value(QStringLiteral("Migrated"), false).toBool()) {
+        return;
+    }
+#ifdef __APPLE__
+    QSettings oldSettings(QStringLiteral("psi-borg.org"), QStringLiteral("QJackTrip"));
+#else
+    QSettings oldSettings(QStringLiteral("psi-borg"), QStringLiteral("QJackTrip"));
+#endif
+    QStringList keys = oldSettings.allKeys();
+    for (int i = 0; i < keys.size(); i++) {
+        settings.setValue(keys.at(i), oldSettings.value(keys.at(i), QVariant()));
+    }
+    settings.setValue(QStringLiteral("Migrated"), true);
+}
+
+void QJackTrip::loadSettings(Settings* cliSettings)
+{
+    QSettings settings;
+    bool useCommandLine = false;
+    if (cliSettings) {
+        useCommandLine = cliSettings->isModeSet();
+    }
+
+    // Migrate to separate send and receive channel numbers first if needed
+    int oldChannelSetting = settings.value(QStringLiteral("Channels"), -1).toInt();
+    if (oldChannelSetting != -1) {
+        settings.setValue(QStringLiteral("ChannelsSend"), oldChannelSetting);
+        settings.setValue(QStringLiteral("ChannelsRecv"), oldChannelSetting);
+        settings.remove(QStringLiteral("Channels"));
+    }
+
+    m_ui->verboseCheckBox->setChecked(
+        gVerboseFlag || settings.value(QStringLiteral("Debug"), 0).toBool());
+    m_lastPath = settings.value(QStringLiteral("LastPath"), QDir::homePath()).toString();
+
+    settings.beginGroup(QStringLiteral("RecentServers"));
+    for (int i = 1; i <= 5; i++) {
+        QString address =
+            settings.value(QStringLiteral("Server%1").arg(i), "").toString();
+        if (!address.isEmpty()) {
+            m_ui->addressComboBox->addItem(address);
+        }
+    }
+    settings.endGroup();
+    // Need to get this here so it isn't overwritten by the previous section.
+    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(
+        settings.value(QStringLiteral("Backend"), 0).toInt());
+    m_ui->sampleRateComboBox->setCurrentText(
+        settings.value(QStringLiteral("SampleRate"), "48000").toString());
+    m_ui->bufferSizeComboBox->setCurrentText(
+        settings.value(QStringLiteral("BufferSize"), "128").toString());
+    // update device list and set the device
+    populateDeviceMenu(m_ui->inputDeviceComboBox, true);
+    auto inDevice = settings.value(QStringLiteral("InputDevice")).toString();
+    if (!inDevice.isEmpty()) {
+        m_ui->inputDeviceComboBox->setCurrentText(inDevice);
+    }
+    populateDeviceMenu(m_ui->outputDeviceComboBox, false);
+    auto outDevice = settings.value(QStringLiteral("OutputDevice")).toString();
+    if (!outDevice.isEmpty()) {
+        m_ui->outputDeviceComboBox->setCurrentText(outDevice);
+    }
+    settings.endGroup();
+#endif
+
+    settings.beginGroup(QStringLiteral("IOStats"));
+    m_ui->ioStatsCheckBox->setChecked(
+        settings.value(QStringLiteral("Display"), false).toBool());
+    m_ui->ioStatsSpinBox->setValue(
+        settings.value(QStringLiteral("ReportingInterval"), 1).toInt());
+    settings.endGroup();
+
+    settings.beginGroup(QStringLiteral("InPlugins"));
+    m_ui->inFreeverbCheckBox->setChecked(
+        settings.value(QStringLiteral("Freeverb"), false).toBool());
+    m_ui->inFreeverbWetnessSlider->setValue(
+        settings.value(QStringLiteral("FreeverbWetness"), 0).toInt());
+    m_ui->inZitarevCheckBox->setChecked(
+        settings.value(QStringLiteral("Zitarev"), false).toBool());
+    m_ui->inZitarevWetnessSlider->setValue(
+        settings.value(QStringLiteral("ZitarevWetness"), 0).toInt());
+    m_ui->inCompressorCheckBox->setChecked(
+        settings.value(QStringLiteral("Compressor"), false).toBool());
+    m_ui->inLimiterCheckBox->setChecked(
+        settings.value(QStringLiteral("Limiter"), false).toBool());
+    settings.endGroup();
+
+    settings.beginGroup(QStringLiteral("OutPlugins"));
+    m_ui->outFreeverbCheckBox->setChecked(
+        settings.value(QStringLiteral("Freeverb"), false).toBool());
+    m_ui->outFreeverbWetnessSlider->setValue(
+        settings.value(QStringLiteral("FreeverbWetness"), 0).toInt());
+    m_ui->outZitarevCheckBox->setChecked(
+        settings.value(QStringLiteral("Zitarev"), false).toBool());
+    m_ui->outZitarevWetnessSlider->setValue(
+        settings.value(QStringLiteral("ZitarevWetness"), 0).toInt());
+    m_ui->outCompressorCheckBox->setChecked(
+        settings.value(QStringLiteral("Compressor"), false).toBool());
+    m_ui->outLimiterCheckBox->setChecked(
+        settings.value(QStringLiteral("Limiter"), false).toBool());
+    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()
+{
+    QSettings settings;
+    settings.setValue(QStringLiteral("RunMode"), m_ui->typeComboBox->currentIndex());
+    settings.setValue(QStringLiteral("LastAddress"),
+                      m_ui->addressComboBox->currentText());
+    settings.setValue(QStringLiteral("ChannelsSend"), m_ui->channelSendSpinBox->value());
+    settings.setValue(QStringLiteral("ChannelsRecv"), m_ui->channelRecvSpinBox->value());
+    settings.setValue(QStringLiteral("AutoPatchMode"),
+                      m_ui->autoPatchComboBox->currentIndex());
+    settings.setValue(QStringLiteral("PatchIncludesServer"),
+                      m_ui->patchServerCheckBox->isChecked());
+    settings.setValue(QStringLiteral("StereoUpmix"), m_ui->upmixCheckBox->isChecked());
+    settings.setValue(QStringLiteral("ZeroUnderrun"), m_ui->zeroCheckBox->isChecked());
+    settings.setValue(QStringLiteral("Timeout"), m_ui->timeoutCheckBox->isChecked());
+    settings.setValue(QStringLiteral("ClientName"), m_ui->clientNameEdit->text());
+    settings.setValue(QStringLiteral("RemoteName"), m_ui->remoteNameEdit->text());
+    settings.setValue(QStringLiteral("LocalPort"), m_ui->localPortSpinBox->value());
+    settings.setValue(QStringLiteral("RemotePort"), m_ui->remotePortSpinBox->value());
+    settings.setValue(QStringLiteral("BasePort"), m_ui->basePortSpinBox->value());
+    settings.setValue(QStringLiteral("QueueLength"), m_ui->queueLengthSpinBox->value());
+    settings.setValue(QStringLiteral("Redundancy"), m_ui->redundancySpinBox->value());
+    settings.setValue(QStringLiteral("Resolution"),
+                      m_ui->resolutionComboBox->currentIndex());
+    settings.setValue(QStringLiteral("ConnectAudio"),
+                      m_ui->connectAudioCheckBox->isChecked());
+    settings.setValue(QStringLiteral("RTNetworking"),
+                      m_ui->realTimeCheckBox->isChecked());
+    settings.setValue(QStringLiteral("Debug"), m_ui->verboseCheckBox->isChecked());
+    settings.setValue(QStringLiteral("LastPath"), m_lastPath);
+
+    settings.beginGroup(QStringLiteral("RecentServers"));
+    for (int i = 0; i < m_ui->addressComboBox->count(); i++) {
+        settings.setValue(QStringLiteral("Server%1").arg(i + 1),
+                          m_ui->addressComboBox->itemText(i));
+    }
+    settings.endGroup();
+
+#ifdef RT_AUDIO
+    settings.beginGroup(QStringLiteral("Audio"));
+    settings.setValue(QStringLiteral("Backend"), m_ui->backendComboBox->currentIndex());
+    settings.setValue(QStringLiteral("SampleRate"),
+                      m_ui->sampleRateComboBox->currentText());
+    settings.setValue(QStringLiteral("BufferSize"),
+                      m_ui->bufferSizeComboBox->currentText());
+    settings.setValue(QStringLiteral("InputDevice"),
+                      m_ui->inputDeviceComboBox->currentText());
+    settings.setValue(QStringLiteral("OutputDevice"),
+                      m_ui->outputDeviceComboBox->currentText());
+    settings.endGroup();
+#endif
+
+    settings.beginGroup(QStringLiteral("Auth"));
+    settings.setValue(QStringLiteral("Require"), m_ui->requireAuthCheckBox->isChecked());
+    settings.setValue(QStringLiteral("CertFile"), m_ui->certEdit->text());
+    settings.setValue(QStringLiteral("KeyFile"), m_ui->keyEdit->text());
+    settings.setValue(QStringLiteral("CredsFile"), m_ui->credsEdit->text());
+    settings.setValue(QStringLiteral("Use"), m_ui->authCheckBox->isChecked());
+    settings.setValue(QStringLiteral("Username"), m_ui->usernameEdit->text());
+    settings.endGroup();
+
+    settings.beginGroup(QStringLiteral("IOStats"));
+    settings.setValue(QStringLiteral("Display"), m_ui->ioStatsCheckBox->isChecked());
+    settings.setValue(QStringLiteral("ReportingInterval"), m_ui->ioStatsSpinBox->value());
+    settings.endGroup();
+
+    settings.beginGroup(QStringLiteral("JitterBuffer"));
+    settings.setValue(QStringLiteral("Enabled"), m_ui->jitterCheckBox->isChecked());
+    settings.setValue(QStringLiteral("Broadcast"), m_ui->broadcastCheckBox->isChecked());
+    settings.setValue(QStringLiteral("BroadcastLength"),
+                      m_ui->broadcastQueueSpinBox->value());
+    settings.setValue(QStringLiteral("Strategy"),
+                      m_ui->bufferStrategyComboBox->currentIndex() + 1);
+    settings.setValue(QStringLiteral("AutoQueue"), m_ui->autoQueueCheckBox->isChecked());
+    settings.setValue(QStringLiteral("TuningParameter"), m_ui->autoQueueSpinBox->value());
+    settings.endGroup();
+
+    settings.beginGroup(QStringLiteral("InPlugins"));
+    settings.setValue(QStringLiteral("Freeverb"), m_ui->inFreeverbCheckBox->isChecked());
+    settings.setValue(QStringLiteral("FreeverbWetness"),
+                      m_ui->inFreeverbWetnessSlider->value());
+    settings.setValue(QStringLiteral("Zitarev"), m_ui->inZitarevCheckBox->isChecked());
+    settings.setValue(QStringLiteral("ZitarevWetness"),
+                      m_ui->inZitarevWetnessSlider->value());
+    settings.setValue(QStringLiteral("Compressor"),
+                      m_ui->inCompressorCheckBox->isChecked());
+    settings.setValue(QStringLiteral("Limiter"), m_ui->inLimiterCheckBox->isChecked());
+    settings.endGroup();
+
+    settings.beginGroup(QStringLiteral("OutPlugins"));
+    settings.setValue(QStringLiteral("Freeverb"), m_ui->outFreeverbCheckBox->isChecked());
+    settings.setValue(QStringLiteral("FreeverbWetness"),
+                      m_ui->outFreeverbWetnessSlider->value());
+    settings.setValue(QStringLiteral("Zitarev"), m_ui->outZitarevCheckBox->isChecked());
+    settings.setValue(QStringLiteral("ZitarevWetness"),
+                      m_ui->outZitarevWetnessSlider->value());
+    settings.setValue(QStringLiteral("Compressor"),
+                      m_ui->outCompressorCheckBox->isChecked());
+    settings.setValue(QStringLiteral("Limiter"), m_ui->outLimiterCheckBox->isChecked());
+    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();
+}
+
+void QJackTrip::appendPlugins(JackTrip* jackTrip, int numSendChannels,
+                              int numRecvChannels)
+{
+    if (!jackTrip) {
+        return;
+    }
+
+    // These effects are currently deleted by the AudioInterface of jacktrip.
+    // May need to change this code if we move to smart pointers.
+    if (m_ui->outCompressorCheckBox->isChecked()) {
+        jackTrip->appendProcessPluginToNetwork(
+            new Compressor(numSendChannels, false, CompressorPresets::voice));
+    }
+    if (m_ui->inCompressorCheckBox->isChecked()) {
+        jackTrip->appendProcessPluginFromNetwork(
+            new Compressor(numRecvChannels, false, CompressorPresets::voice));
+    }
+
+    if (m_ui->outZitarevCheckBox->isChecked()) {
+        qreal wetness = m_ui->outZitarevWetnessSlider->value() / 100.0;
+        jackTrip->appendProcessPluginToNetwork(
+            new Reverb(numSendChannels, numSendChannels, 1.0 + wetness));
+    }
+    if (m_ui->inZitarevCheckBox->isChecked()) {
+        qreal wetness = m_ui->inZitarevWetnessSlider->value() / 100.0;
+        jackTrip->appendProcessPluginFromNetwork(
+            new Reverb(numRecvChannels, numRecvChannels, 1.0 + wetness));
+    }
+
+    if (m_ui->outFreeverbCheckBox->isChecked()) {
+        qreal wetness = m_ui->outFreeverbWetnessSlider->value() / 100.0;
+        jackTrip->appendProcessPluginToNetwork(
+            new Reverb(numSendChannels, numSendChannels, wetness));
+    }
+    if (m_ui->inFreeverbCheckBox->isChecked()) {
+        qreal wetness = m_ui->inFreeverbWetnessSlider->value() / 100.0;
+        jackTrip->appendProcessPluginFromNetwork(
+            new Reverb(numRecvChannels, numRecvChannels, wetness));
+    }
+
+    // Limiters go last in the plugin sequence.
+    if (m_ui->outLimiterCheckBox->isChecked()) {
+        jackTrip->appendProcessPluginToNetwork(
+            new Limiter(numSendChannels, m_ui->outClientsSpinBox->value()));
+    }
+    if (m_ui->inLimiterCheckBox->isChecked()) {
+        jackTrip->appendProcessPluginFromNetwork(new Limiter(numRecvChannels, 1));
+    }
+}
+
+void QJackTrip::createMeters(quint32 inputChannels, quint32 outputChannels)
+{
+    // These pointers are also deleted by AudioInterface.
+    Meter* inputMeter  = new Meter(inputChannels);
+    Meter* outputMeter = new Meter(outputChannels);
+    m_jackTrip->appendProcessPluginToNetwork(inputMeter);
+    m_jackTrip->appendProcessPluginFromNetwork(outputMeter);
+
+    // Create our widgets.
+    for (quint32 i = 0; i < inputChannels; i++) {
+        VuMeter* meter = new VuMeter(this);
+        m_inputMeters.append(meter);
+        QLabel* label = new QLabel(QString::number(i + 1), this);
+        m_inputLabels.append(label);
+        label->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
+        m_inputLayout->addWidget(label, i, 0, 1, 1);
+        m_inputLayout->addWidget(meter, i, 1, 1, 1);
+    }
+    // Effectively add a spacer at the bottom.
+    m_inputLayout->setRowStretch(inputChannels, 100);
+
+    for (quint32 i = 0; i < outputChannels; i++) {
+        VuMeter* meter = new VuMeter(this);
+        m_outputMeters.append(meter);
+        QLabel* label = new QLabel(QString::number(i + 1), this);
+        m_outputLabels.append(label);
+        label->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
+        m_outputLayout->addWidget(label, i, 0, 1, 1);
+        m_outputLayout->addWidget(meter, i, 1, 1, 1);
+    }
+    m_outputLayout->setRowStretch(outputChannels, 100);
+
+    QObject::connect(inputMeter, &Meter::onComputedVolumeMeasurements, this,
+                     &QJackTrip::updatedInputMeasurements);
+    QObject::connect(outputMeter, &Meter::onComputedVolumeMeasurements, this,
+                     &QJackTrip::updatedOutputMeasurements);
+}
+
+void QJackTrip::removeMeters()
+{
+    m_inputLayout->setRowStretch(m_inputMeters.count(), 0);
+    m_outputLayout->setRowStretch(m_outputMeters.count(), 0);
+    for (int i = 0; i < m_inputLabels.count(); i++) {
+        delete m_inputLabels.at(i);
+    }
+    for (int i = 0; i < m_inputMeters.count(); i++) {
+        delete m_inputMeters.at(i);
+    }
+    for (int i = 0; i < m_outputLabels.count(); i++) {
+        delete m_outputLabels.at(i);
+    }
+    for (int i = 0; i < m_outputMeters.count(); i++) {
+        delete m_outputMeters.at(i);
+    }
+    m_inputLabels.clear();
+    m_inputMeters.clear();
+    m_outputLabels.clear();
+    m_outputMeters.clear();
+}
+
+QString QJackTrip::commandLineFromCurrentOptions()
+{
+    QString commandLine = QStringLiteral("jacktrip");
+
+    if (m_ui->typeComboBox->currentIndex() == P2P_CLIENT) {
+        commandLine.append(" -c ").append(m_ui->addressComboBox->currentText());
+    } else if (m_ui->typeComboBox->currentIndex() == P2P_SERVER) {
+        commandLine.append(" -s");
+    } else if (m_ui->typeComboBox->currentIndex() == HUB_CLIENT) {
+        commandLine.append(" -C ").append(m_ui->addressComboBox->currentText());
+    } else {
+        commandLine.append(" -S");
+    }
+
+    if (m_ui->zeroCheckBox->isChecked()) {
+        commandLine.append(" -z");
+    }
+
+    if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) {
+        int hubConnectionMode = hubModeFromPatchType(
+            static_cast<patchTypeT>(m_ui->autoPatchComboBox->currentIndex()));
+        if (hubConnectionMode > 0) {
+            commandLine.append(QStringLiteral(" -p %1").arg(hubConnectionMode));
+        }
+        if (m_ui->patchServerCheckBox->isChecked()
+            && (m_ui->typeComboBox->currentIndex() == CLIENTFOFI
+                || m_ui->typeComboBox->currentIndex() == FULLMIX)) {
+            commandLine.append(" -i");
+        }
+        if (m_ui->upmixCheckBox->isChecked()) {
+            commandLine.append(" -u");
+        }
+    } else {
+        if (m_ui->channelSendSpinBox->value() != gDefaultNumInChannels
+            || m_ui->channelRecvSpinBox->value() != gDefaultNumOutChannels) {
+            if (m_ui->channelSendSpinBox->value() == m_ui->channelRecvSpinBox->value()) {
+                commandLine.append(
+                    QStringLiteral(" -n %1").arg(m_ui->channelRecvSpinBox->value()));
+            } else {
+                commandLine.append(
+                    QStringLiteral(" --receivechannels %1 --sendchannels %2")
+                        .arg(m_ui->channelRecvSpinBox->value())
+                        .arg(m_ui->channelSendSpinBox->value()));
+            }
+        }
+        if (m_ui->timeoutCheckBox->isChecked()) {
+            commandLine.append(" -t");
+        }
+    }
+
+    int bufStrategy = -1;
+    if (m_ui->jitterCheckBox->isChecked()) {
+        bufStrategy = m_ui->bufferStrategyComboBox->currentIndex() + 1;
+    }
+    if (bufStrategy != 1) {
+        commandLine.append(QStringLiteral(" --bufstrategy %1").arg(bufStrategy));
+    }
+
+    if (m_ui->jitterCheckBox->isChecked() && m_ui->autoQueueCheckBox->isChecked()) {
+        if (m_ui->autoQueueSpinBox->value() == 500) {
+            commandLine.append(" -q auto");
+        } else {
+            commandLine.append(
+                QStringLiteral(" -q auto%1").arg(m_ui->autoQueueSpinBox->value()));
+        }
+    } else if (m_ui->queueLengthSpinBox->value() != gDefaultQueueLength) {
+        commandLine.append(
+            QStringLiteral(" -q %1").arg(m_ui->queueLengthSpinBox->value()));
+    }
+
+    if (m_ui->jitterCheckBox->isChecked() && m_ui->broadcastCheckBox->isChecked()) {
+        commandLine.append(
+            QStringLiteral(" --broadcast %1").arg(m_ui->broadcastQueueSpinBox->value()));
+    }
+
+    // Port settings
+    if (m_ui->localPortSpinBox->value() != gDefaultPort) {
+        commandLine.append(QStringLiteral(" -B %1").arg(m_ui->localPortSpinBox->value()));
+    }
+    if (m_ui->typeComboBox->currentIndex() == HUB_CLIENT
+        || m_ui->typeComboBox->currentIndex() == P2P_CLIENT) {
+        if (m_ui->remotePortSpinBox->value() != gDefaultPort) {
+            commandLine.append(
+                QStringLiteral(" -P %1").arg(m_ui->remotePortSpinBox->value()));
+        }
+    }
+
+    // Auth settings
+    if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) {
+        if (m_ui->requireAuthCheckBox->isChecked()) {
+            commandLine.append(" -A");
+            if (!m_ui->certEdit->text().isEmpty()) {
+                commandLine.append(" --certfile ").append(m_ui->certEdit->text());
+            }
+            if (!m_ui->keyEdit->text().isEmpty()) {
+                commandLine.append(" --keyfile ").append(m_ui->keyEdit->text());
+            }
+            if (!m_ui->credsEdit->text().isEmpty()) {
+                commandLine.append(" --credsfile ").append(m_ui->credsEdit->text());
+            }
+        }
+    } else if (m_ui->typeComboBox->currentIndex() == HUB_CLIENT) {
+        if (m_ui->authCheckBox->isChecked()) {
+            commandLine.append(" -A");
+            if (!m_ui->usernameEdit->text().isEmpty()) {
+                commandLine.append(" --username ").append(m_ui->usernameEdit->text());
+            }
+            /*if (!m_ui->passwordEdit->text().isEmpty()) {
+                commandLine.append(" --password <password>");
+            }*/
+        }
+    }
+
+    if (m_ui->typeComboBox->currentIndex() == HUB_SERVER) {
+        int offset = m_ui->localPortSpinBox->value() - gDefaultPort;
+        if (m_ui->basePortSpinBox->value() != 61002 + offset) {
+            commandLine.append(
+                QStringLiteral(" -U %1").arg(m_ui->basePortSpinBox->value()));
+        }
+    } else {
+        if (!m_ui->clientNameEdit->text().isEmpty()) {
+            commandLine.append(
+                QStringLiteral(" -J \"%1\"").arg(m_ui->clientNameEdit->text()));
+        }
+        if (m_ui->typeComboBox->currentIndex() == HUB_CLIENT
+            && !m_ui->remoteNameEdit->text().isEmpty()) {
+            commandLine.append(
+                QStringLiteral(" -K \"%1\"").arg(m_ui->remoteNameEdit->text()));
+        }
+        if (m_ui->redundancySpinBox->value() > 1) {
+            commandLine.append(
+                QStringLiteral(" -r %1").arg(m_ui->redundancySpinBox->value()));
+        }
+        if (m_ui->resolutionComboBox->currentText() != QLatin1String("16")) {
+            commandLine.append(" -b ").append(m_ui->resolutionComboBox->currentText());
+        }
+        if (!m_ui->connectAudioCheckBox->isChecked()) {
+            commandLine.append(" -D");
+        }
+
+        if (m_ui->inLimiterCheckBox->isChecked()
+            || m_ui->outLimiterCheckBox->isChecked()) {
+            commandLine.append(" -O ");
+            if (m_ui->inLimiterCheckBox->isChecked()) {
+                commandLine.append("i");
+            }
+            if (m_ui->outLimiterCheckBox->isChecked()) {
+                commandLine.append("o");
+                if (m_ui->outClientsSpinBox->value() != 2) {
+                    commandLine.append(
+                        QStringLiteral(" -a %1").arg(m_ui->outClientsSpinBox->value()));
+                }
+            }
+        }
+
+        bool inEffects = m_ui->inFreeverbCheckBox->isChecked()
+                         || m_ui->inZitarevCheckBox->isChecked()
+                         || m_ui->inCompressorCheckBox->isChecked();
+        bool outEffects = m_ui->outFreeverbCheckBox->isChecked()
+                          || m_ui->outZitarevCheckBox->isChecked()
+                          || m_ui->outCompressorCheckBox->isChecked();
+        if (inEffects || outEffects) {
+            commandLine.append(" -f \"");
+            if (inEffects) {
+                commandLine.append("i:");
+                if (m_ui->inCompressorCheckBox->isChecked()) {
+                    commandLine.append("c");
+                }
+                if (m_ui->inFreeverbCheckBox->isChecked()) {
+                    commandLine.append(QStringLiteral("f(%1)").arg(
+                        m_ui->inFreeverbWetnessSlider->value() / 100.0));
+                }
+                if (m_ui->inZitarevCheckBox->isChecked()) {
+                    commandLine.append(QStringLiteral("f(%1)").arg(
+                        m_ui->inZitarevWetnessSlider->value() / 100.0));
+                }
+                if (outEffects) {
+                    commandLine.append(", ");
+                }
+            }
+            if (outEffects) {
+                commandLine.append("o:");
+                if (m_ui->outCompressorCheckBox->isChecked()) {
+                    commandLine.append("c");
+                }
+                if (m_ui->outFreeverbCheckBox->isChecked()) {
+                    commandLine.append(QStringLiteral("f(%1)").arg(
+                        m_ui->outFreeverbWetnessSlider->value() / 100.0));
+                }
+                if (m_ui->outZitarevCheckBox->isChecked()) {
+                    commandLine.append(QStringLiteral("f(%1)").arg(
+                        m_ui->outZitarevWetnessSlider->value() / 100.0));
+                }
+            }
+            commandLine.append("\"");
+        }
+    }
+    if (m_ui->ioStatsCheckBox->isChecked()) {
+        commandLine.append(QStringLiteral(" -I %1").arg(m_ui->ioStatsSpinBox->value()));
+    }
+
+    if (m_ui->verboseCheckBox->isChecked()) {
+        commandLine.append(" -V");
+    }
+
+    if (m_ui->realTimeCheckBox->isChecked()) {
+        commandLine.append(" --udprt");
+    }
+
+#ifdef RT_AUDIO
+    if (m_ui->typeComboBox->currentIndex() != HUB_SERVER
+        && m_ui->backendComboBox->currentIndex() == 1) {
+        commandLine.append(" --rtaudio");
+        commandLine.append(
+            QStringLiteral(" --srate %1").arg(m_ui->sampleRateComboBox->currentText()));
+        commandLine.append(
+            QStringLiteral(" --bufsize %1").arg(m_ui->bufferSizeComboBox->currentText()));
+        QString inDevice;
+        if (m_ui->inputDeviceComboBox->currentIndex() > 0) {
+            inDevice = m_ui->inputDeviceComboBox->currentText();
+        }
+        QString outDevice;
+        if (m_ui->outputDeviceComboBox->currentIndex() > 0) {
+            outDevice = m_ui->outputDeviceComboBox->currentText();
+        }
+        commandLine.append(
+            QStringLiteral(" --audiodevice \"%1\",\"%2\"").arg(inDevice, outDevice));
+    }
+#endif
+
+    return commandLine;
+}
+
+#ifdef RT_AUDIO
+void QJackTrip::populateDeviceMenu(QComboBox* menu, bool isInput)
+{
+    QString previousString = menu->currentText();
+    menu->clear();
+    menu->addItem(QStringLiteral("(default)"));
+
+    QVector<RtAudioDevice> devices;
+    RtAudioInterface::scanDevices(devices);
+    for (auto info : devices) {
+        // convert names to QString to gracefully handle invalid
+        // utf8 character sequences, such as "RØDE Microphone"
+        const QString utf8Name(QString::fromStdString(info.name));
+
+        // Don't include duplicate entries
+        if (menu->findText(utf8Name) != -1) {
+            continue;
+        }
+        if (isInput && info.inputChannels > 0) {
+            menu->addItem(utf8Name);
+        } else if (!isInput && info.outputChannels > 0) {
+            menu->addItem(utf8Name);
+        }
+    }
+
+    // set the previous value
+    menu->setCurrentText(previousString);
+}
+#endif
+
+void QJackTrip::showCommandLineMessageBox()
+{
+    QMessageBox msgBox;
+    QString messageText =
+        QStringLiteral("The equivalent command line for the current options is:\n\n%1")
+            .arg(commandLineFromCurrentOptions());
+    msgBox.setText(messageText);
+    msgBox.setWindowTitle(QStringLiteral("Command Line"));
+    msgBox.setTextInteractionFlags(Qt::TextSelectableByMouse);
+    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.)
+    std::cout.rdbuf(m_realCout.rdbuf());
+    std::cerr.rdbuf(m_realCerr.rdbuf());
+}
diff --git a/src/gui/qjacktrip.h b/src/gui/qjacktrip.h
new file mode 100644 (file)
index 0000000..8b9a33f
--- /dev/null
@@ -0,0 +1,176 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2020 Aaron Wyatt.
+
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#ifndef QJACKTRIP_H
+#define QJACKTRIP_H
+
+#include <QByteArray>
+#include <QCloseEvent>
+#include <QGridLayout>
+#include <QLabel>
+#include <QMainWindow>
+#include <QMutex>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QScopedPointer>
+#include <QString>
+#include <QTemporaryFile>
+
+#include "../JackTrip.h"
+#include "../Settings.h"
+#include "../UdpHubListener.h"
+#include "messageDialog.h"
+#include "vuMeter.h"
+
+#ifdef __APPLE__
+#include "NoNap.h"
+#endif
+
+#ifdef RT_AUDIO
+#include <QComboBox>
+#endif
+
+namespace Ui
+{
+class QJackTrip;
+}  // namespace Ui
+
+#ifndef NO_VS
+class VirtualStudio;
+#endif
+
+class QJackTrip : public QMainWindow
+{
+    Q_OBJECT
+
+   public:
+    explicit QJackTrip(QSharedPointer<Settings> settings,
+                       bool suppressCommandlineWarning = false,
+                       QWidget* parent                 = nullptr);
+    ~QJackTrip() override;
+
+    void closeEvent(QCloseEvent* event) override;
+    void resizeEvent(QResizeEvent* event) override;
+    void showEvent(QShowEvent* event) override;
+
+#ifndef NO_VS
+    enum uiModeT { UNSET, VIRTUAL_STUDIO, STANDARD };
+    void setVs(QSharedPointer<VirtualStudio> vs);
+#endif
+
+   signals:
+    void signalExit();
+
+   private slots:
+    void processFinished();
+    void processError(const QString& errorMessage);
+    void receivedConnectionFromPeer();
+    void udpWaitingTooLong();
+    void queueLengthChanged(int queueLength);
+    void chooseRunType(int index);
+    void addressChanged(const QString& address);
+    void authFilesChanged();
+    void credentialsChanged();
+    void browseForFile();
+    void receivedIP(QNetworkReply* reply);
+    void resetOptions();
+    void start();
+    void stop();
+    void exit();
+    void updatedInputMeasurements(const float* valuesInDb, int numChannels);
+    void updatedOutputMeasurements(const float* valuesInDb, int numChannels);
+#ifndef NO_VS
+    void virtualStudioMode();
+#endif
+
+   private:
+    enum runTypeT { P2P_CLIENT, P2P_SERVER, HUB_CLIENT, HUB_SERVER };
+    enum patchTypeT { SERVERTOCLIENT, CLIENTECHO, CLIENTFOFI, FULLMIX, NOAUTO };
+
+    int findTab(const QString& tabName);
+    void enableUi(bool enabled);
+    void advancedOptionsForHubServer(bool isHubServer);
+    void migrateSettings();
+    void loadSettings(Settings* cliSettings = nullptr);
+    void saveSettings();
+
+#ifdef RT_AUDIO
+    void populateDeviceMenu(QComboBox* menu, bool isInput);
+#endif
+
+    void appendPlugins(JackTrip* jackTrip, int numSendChannels, int numRecvChannels);
+    void createMeters(quint32 inputChannels, quint32 outputChannels);
+    void removeMeters();
+
+    QString commandLineFromCurrentOptions();
+    void showCommandLineMessageBox();
+
+    JackTrip::hubConnectionModeT hubModeFromPatchType(patchTypeT patchType);
+
+    QScopedPointer<Ui::QJackTrip> m_ui;
+    QScopedPointer<UdpHubListener> m_udpHub;
+    QScopedPointer<JackTrip> m_jackTrip;
+    QScopedPointer<QNetworkAccessManager> m_netManager;
+    QScopedPointer<MessageDialog> m_statsDialog;
+    QScopedPointer<MessageDialog> m_debugDialog;
+    QScopedPointer<QGridLayout> m_inputLayout;
+    QScopedPointer<QGridLayout> m_outputLayout;
+    std::ostream m_realCout;
+    std::ostream m_realCerr;
+    QString m_assignedClientName;
+    bool m_jackTripRunning;
+    bool m_isExiting;
+    bool m_exitSent;
+
+    QSharedPointer<Settings> m_cliSettings;
+    bool m_suppressCommandlineWarning;
+
+    float m_meterMax = 0.0;
+    float m_meterMin = -64.0;
+
+    QList<VuMeter*> m_inputMeters;
+    QList<QLabel*> m_inputLabels;
+    QList<VuMeter*> m_outputMeters;
+    QList<QLabel*> m_outputLabels;
+
+    QMutex m_requestMutex;
+    QString m_IPv6Address;
+    QString m_IPv4Address;
+    int m_replyCount = 0;
+    QString m_lastPath;
+
+    QLabel m_autoQueueIndicator;
+    bool m_hideWarning;
+    bool m_firstShow = true;
+
+#ifndef NO_VS
+    QSharedPointer<VirtualStudio> m_vs;
+#endif
+#ifdef __APPLE__
+    NoNap m_noNap;
+#endif
+};
+
+#endif  // QJACKTRIP_H
diff --git a/src/gui/qjacktrip.qrc b/src/gui/qjacktrip.qrc
new file mode 100644 (file)
index 0000000..7f5e0dc
--- /dev/null
@@ -0,0 +1,98 @@
+<RCC>
+  <qresource prefix="qjacktrip">
+    <file>about@2x.png</file>
+    <file>about.png</file>
+    <file>icon.png</file>
+  </qresource>
+  <qresource prefix="vs">
+    <file>vs.qml</file>
+    <file>FirstLaunch.qml</file>
+    <file>Login.qml</file>
+    <file>LearnMoreButton.qml</file>
+    <file>Recommendations.qml</file>
+    <file>Permissions.qml</file>
+    <file>ChangeDevices.qml</file>
+    <file>Studio.qml</file>
+    <file>Browse.qml</file>
+    <file>AudioSettings.qml</file>
+    <file>Settings.qml</file>
+    <file>Meter.qml</file>
+    <file>MeterBars.qml</file>
+    <file>Connected.qml</file>
+    <file>CreateStudio.qml</file>
+    <file>Failed.qml</file>
+    <file>Setup.qml</file>
+    <file>SectionHeading.qml</file>
+    <file>Footer.qml</file>
+    <file>VolumeSlider.qml</file>
+    <file>DeviceControls.qml</file>
+    <file>DeviceControlsGroup.qml</file>
+    <file>DeviceRefreshButton.qml</file>
+    <file>DeviceWarning.qml</file>
+    <file>InfoTooltip.qml</file>
+    <file>Web.qml</file>
+    <file>WebView.qml</file>
+    <file>WebEngine.qml</file>
+    <file>WebNull.qml</file>
+    <file>FeedbackSurvey.qml</file>
+    <file>AppIcon.qml</file>
+    <file>logo.svg</file>
+    <file>wedge.svg</file>
+    <file>wedge_inactive.svg</file>
+    <file>private.svg</file>
+    <file>public.svg</file>
+    <file>join.svg</file>
+    <file>leave.svg</file>
+    <file>manage.svg</file>
+    <file>speed.svg</file>
+    <file>share.svg</file>
+    <file>start.svg</file>
+    <file>star.svg</file>
+    <file>cog.svg</file>
+    <file>mic.svg</file>
+    <file>language.svg</file>
+    <file>micoff.svg</file>
+    <file>help.svg</file>
+    <file>quiet.svg</file>
+    <file>loud.svg</file>
+    <file>refresh.svg</file>
+    <file>ethernet.svg</file>
+    <file>networkCheck.svg</file>
+    <file>externalMic.svg</file>
+    <file>check.svg</file>
+    <file>warning.svg</file>
+    <file>expand_less.svg</file>
+    <file>expand_more.svg</file>
+    <file>sentiment_very_dissatisfied.svg</file>
+    <file>headphones.svg</file>
+    <file>Prompt.svg</file>
+    <file>network.svg</file>
+    <file>video.svg</file>
+    <file>close.svg</file>
+    <file>jacktrip.png</file>
+    <file>jacktrip white.png</file>
+    <file>JTOriginal.png</file>
+    <file>JTVS.png</file>
+    <file>flags/AE.svg</file>
+    <file>flags/AU.svg</file>
+    <file>flags/BE.svg</file>
+    <file>flags/BR.svg</file>
+    <file>flags/CA.svg</file>
+    <file>flags/CH.svg</file>
+    <file>flags/DE.svg</file>
+    <file>flags/FR.svg</file>
+    <file>flags/GB.svg</file>
+    <file>flags/HK.svg</file>
+    <file>flags/ID.svg</file>
+    <file>flags/IT.svg</file>
+    <file>flags/JP.svg</file>
+    <file>flags/RO.svg</file>
+    <file>flags/SE.svg</file>
+    <file>flags/SG.svg</file>
+    <file>flags/TW.svg</file>
+    <file>flags/US.svg</file>
+    <file>flags/ZA.svg</file>
+    <file>Poppins-Bold.ttf</file>
+    <file>Poppins-Regular.ttf</file>
+  </qresource>
+</RCC>
diff --git a/src/gui/qjacktrip.ui b/src/gui/qjacktrip.ui
new file mode 100644 (file)
index 0000000..b6d0dd5
--- /dev/null
@@ -0,0 +1,2006 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>QJackTrip</class>
+ <widget class="QMainWindow" name="QJackTrip">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>409</width>
+    <height>961</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>JackTrip</string>
+  </property>
+  <property name="windowIcon">
+   <iconset resource="qjacktrip.qrc">
+    <normaloff>:/qjacktrip/icon.png</normaloff>:/qjacktrip/icon.png</iconset>
+  </property>
+  <widget class="QWidget" name="centralWidget">
+   <layout class="QGridLayout" name="gridLayout">
+    <item row="3" column="1">
+     <widget class="QLabel" name="ipLabel">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Preferred" vsizetype="Maximum">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="toolTip">
+       <string>If running as a server, this is the address you should supply to the other clients.
+(You will need to enable any port forwarding on your router manually.)</string>
+      </property>
+      <property name="text">
+       <string>Looking up external IP address...</string>
+      </property>
+     </widget>
+    </item>
+    <item row="8" column="0" colspan="2">
+     <layout class="QHBoxLayout" name="buttonLayout">
+      <item>
+       <widget class="QPushButton" name="connectButton">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="text">
+         <string>Connect</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="disconnectButton">
+        <property name="enabled">
+         <bool>false</bool>
+        </property>
+        <property name="text">
+         <string>Disconnect</string>
+        </property>
+       </widget>
+      </item>
+      <item>
+       <widget class="QPushButton" name="exitButton">
+        <property name="text">
+         <string>Exit</string>
+        </property>
+       </widget>
+      </item>
+     </layout>
+    </item>
+    <item row="6" column="0" colspan="2">
+     <widget class="QGroupBox" name="inputGroupBox">
+      <property name="title">
+       <string>Input Channels</string>
+      </property>
+     </widget>
+    </item>
+    <item row="1" column="0">
+     <widget class="QLabel" name="addressLabel">
+      <property name="enabled">
+       <bool>true</bool>
+      </property>
+      <property name="text">
+       <string>&amp;Address of server</string>
+      </property>
+      <property name="buddy">
+       <cstring>addressComboBox</cstring>
+      </property>
+     </widget>
+    </item>
+    <item row="0" column="0">
+     <widget class="QLabel" name="typeLabel">
+      <property name="text">
+       <string>&amp;Run JackTrip as</string>
+      </property>
+      <property name="buddy">
+       <cstring>typeComboBox</cstring>
+      </property>
+     </widget>
+    </item>
+    <item row="5" column="0" colspan="2">
+     <widget class="QTabWidget" name="optionsTabWidget">
+      <property name="enabled">
+       <bool>true</bool>
+      </property>
+      <property name="minimumSize">
+       <size>
+        <width>391</width>
+        <height>0</height>
+       </size>
+      </property>
+      <property name="currentIndex">
+       <number>0</number>
+      </property>
+      <property name="tabsClosable">
+       <bool>false</bool>
+      </property>
+      <property name="movable">
+       <bool>false</bool>
+      </property>
+      <widget class="QWidget" name="basicTab">
+       <property name="enabled">
+        <bool>true</bool>
+       </property>
+       <attribute name="title">
+        <string>Basic options</string>
+       </attribute>
+       <layout class="QGridLayout" name="gridLayout_3">
+        <item row="3" column="0" colspan="2">
+         <widget class="QGroupBox" name="autoPatchGroupBox">
+          <property name="title">
+           <string/>
+          </property>
+          <layout class="QGridLayout" name="gridLayout_10">
+           <item row="0" column="0" rowspan="2" colspan="2">
+            <widget class="QLabel" name="autoPatchLabel">
+             <property name="sizePolicy">
+              <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+               <horstretch>0</horstretch>
+               <verstretch>0</verstretch>
+              </sizepolicy>
+             </property>
+             <property name="text">
+              <string>Hub auto&amp;patch mode</string>
+             </property>
+             <property name="buddy">
+              <cstring>autoPatchComboBox</cstring>
+             </property>
+            </widget>
+           </item>
+           <item row="0" column="2">
+            <widget class="QComboBox" name="autoPatchComboBox">
+             <property name="toolTip">
+              <string>Select how you want audio to be routed by the hub server.</string>
+             </property>
+             <property name="currentIndex">
+              <number>0</number>
+             </property>
+             <item>
+              <property name="text">
+               <string>Server to clients</string>
+              </property>
+             </item>
+             <item>
+              <property name="text">
+               <string>Client loopback</string>
+              </property>
+             </item>
+             <item>
+              <property name="text">
+               <string>Client fan out/in but no loopback</string>
+              </property>
+             </item>
+             <item>
+              <property name="text">
+               <string>Full Mix</string>
+              </property>
+             </item>
+             <item>
+              <property name="text">
+               <string>No auto patching</string>
+              </property>
+             </item>
+            </widget>
+           </item>
+           <item row="3" column="0" colspan="3">
+            <widget class="QCheckBox" name="upmixCheckBox">
+             <property name="toolTip">
+              <string>For clients that only send one channel of audio, relay their signal in stereo.</string>
+             </property>
+             <property name="text">
+              <string>&amp;Upmix mono clients to stereo</string>
+             </property>
+            </widget>
+           </item>
+           <item row="2" column="0" colspan="3">
+            <widget class="QCheckBox" name="patchServerCheckBox">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Include the server in the audio patching. This allows an ensemble member to
+play from this machine. (Available in client fan out/in and full mix modes.)</string>
+             </property>
+             <property name="text">
+              <string>&amp;Include server in patching</string>
+             </property>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+        </item>
+        <item row="2" column="0" colspan="2">
+         <widget class="QGroupBox" name="channelGroupBox">
+          <layout class="QGridLayout" name="gridLayout_9">
+           <item row="4" column="1">
+            <widget class="QSpinBox" name="channelRecvSpinBox">
+             <property name="sizePolicy">
+              <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
+               <horstretch>0</horstretch>
+               <verstretch>0</verstretch>
+              </sizepolicy>
+             </property>
+             <property name="toolTip">
+              <string>Number of audio channels toaccept from the network.</string>
+             </property>
+             <property name="minimum">
+              <number>1</number>
+             </property>
+             <property name="value">
+              <number>2</number>
+             </property>
+            </widget>
+           </item>
+           <item row="4" column="0">
+            <widget class="QLabel" name="channelRecvLabel">
+             <property name="sizePolicy">
+              <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+               <horstretch>0</horstretch>
+               <verstretch>0</verstretch>
+              </sizepolicy>
+             </property>
+             <property name="text">
+              <string>&amp;Received from network:</string>
+             </property>
+             <property name="buddy">
+              <cstring>channelRecvSpinBox</cstring>
+             </property>
+            </widget>
+           </item>
+           <item row="2" column="0" colspan="2">
+            <widget class="QLabel" name="channelLabel">
+             <property name="sizePolicy">
+              <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+               <horstretch>0</horstretch>
+               <verstretch>0</verstretch>
+              </sizepolicy>
+             </property>
+             <property name="text">
+              <string>&amp;Number of channels</string>
+             </property>
+             <property name="buddy">
+              <cstring>channelSendSpinBox</cstring>
+             </property>
+            </widget>
+           </item>
+           <item row="3" column="0">
+            <widget class="QLabel" name="channelSendLabel">
+             <property name="text">
+              <string>&amp;Sent to network:</string>
+             </property>
+             <property name="buddy">
+              <cstring>channelSendSpinBox</cstring>
+             </property>
+            </widget>
+           </item>
+           <item row="3" column="1">
+            <widget class="QSpinBox" name="channelSendSpinBox">
+             <property name="toolTip">
+              <string>Number of audio channels to send to the network.</string>
+             </property>
+             <property name="minimum">
+              <number>1</number>
+             </property>
+             <property name="value">
+              <number>2</number>
+             </property>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+        </item>
+        <item row="6" column="0" colspan="2">
+         <widget class="QCheckBox" name="timeoutCheckBox">
+          <property name="toolTip">
+           <string>Stop JackTrip if no network traffic has been received for 10 seconds.</string>
+          </property>
+          <property name="text">
+           <string>&amp;Disconnect after 10 seconds of no network activity</string>
+          </property>
+         </widget>
+        </item>
+        <item row="8" column="0" colspan="2">
+         <widget class="QGroupBox" name="authGroupBox">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="title">
+           <string/>
+          </property>
+          <layout class="QGridLayout" name="gridLayout_6">
+           <item row="4" column="1">
+            <widget class="QLineEdit" name="passwordEdit">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Enter your password. (This will not be shown or saved.)</string>
+             </property>
+             <property name="echoMode">
+              <enum>QLineEdit::Password</enum>
+             </property>
+             <property name="cursorPosition">
+              <number>0</number>
+             </property>
+            </widget>
+           </item>
+           <item row="0" column="0" colspan="2">
+            <widget class="QCheckBox" name="authCheckBox">
+             <property name="toolTip">
+              <string>Supply a username and password to connect to the server.</string>
+             </property>
+             <property name="text">
+              <string>&amp;Use Authentication</string>
+             </property>
+            </widget>
+           </item>
+           <item row="3" column="1">
+            <widget class="QLineEdit" name="usernameEdit">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Enter your username for the server.</string>
+             </property>
+            </widget>
+           </item>
+           <item row="4" column="0">
+            <widget class="QLabel" name="passwordLabel">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>&amp;Password</string>
+             </property>
+             <property name="buddy">
+              <cstring>passwordEdit</cstring>
+             </property>
+            </widget>
+           </item>
+           <item row="3" column="0">
+            <widget class="QLabel" name="usernameLabel">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>&amp;Username</string>
+             </property>
+             <property name="buddy">
+              <cstring>usernameEdit</cstring>
+             </property>
+            </widget>
+           </item>
+           <item row="1" column="0" colspan="2">
+            <widget class="QLabel" name="authNotVSLabel">
+             <property name="text">
+              <string>(This is for JackTrip's inbuilt authentication system, not for the Virtual Studio. To connect to a Virtual Studio server, press the &quot;Virtual Studio Mode&quot; button below.)</string>
+             </property>
+             <property name="wordWrap">
+              <bool>true</bool>
+             </property>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+        </item>
+        <item row="9" column="0">
+         <spacer name="basicVerticalSpacer">
+          <property name="orientation">
+           <enum>Qt::Vertical</enum>
+          </property>
+          <property name="sizeType">
+           <enum>QSizePolicy::Expanding</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>20</width>
+            <height>40</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="7" column="0" colspan="2">
+         <widget class="QGroupBox" name="requireAuthGroupBox">
+          <property name="title">
+           <string/>
+          </property>
+          <layout class="QGridLayout" name="gridLayout_7">
+           <item row="3" column="2">
+            <widget class="QPushButton" name="keyBrowse">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>Browse</string>
+             </property>
+            </widget>
+           </item>
+           <item row="1" column="0" colspan="3">
+            <widget class="QLabel" name="authDisclaimerLabel">
+             <property name="text">
+              <string>(This is a work in progress and needs to be manually configured outside of the app. Only use if you know what you're doing.)</string>
+             </property>
+             <property name="wordWrap">
+              <bool>true</bool>
+             </property>
+            </widget>
+           </item>
+           <item row="2" column="0">
+            <widget class="QLabel" name="certLabel">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>&amp;Certificate File</string>
+             </property>
+             <property name="buddy">
+              <cstring>certEdit</cstring>
+             </property>
+            </widget>
+           </item>
+           <item row="3" column="0">
+            <widget class="QLabel" name="keyLabel">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>&amp;Key File</string>
+             </property>
+             <property name="buddy">
+              <cstring>keyEdit</cstring>
+             </property>
+            </widget>
+           </item>
+           <item row="3" column="1">
+            <widget class="QLineEdit" name="keyEdit">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Choose the private key that the server should use for the SSL connection.</string>
+             </property>
+            </widget>
+           </item>
+           <item row="2" column="1">
+            <widget class="QLineEdit" name="certEdit">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Choose the certificate file that the server should use to establish an initial SSL connection.</string>
+             </property>
+            </widget>
+           </item>
+           <item row="2" column="2">
+            <widget class="QPushButton" name="certBrowse">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>Browse</string>
+             </property>
+            </widget>
+           </item>
+           <item row="0" column="0" colspan="2">
+            <widget class="QCheckBox" name="requireAuthCheckBox">
+             <property name="toolTip">
+              <string>Require clients to connect with a username and password.</string>
+             </property>
+             <property name="text">
+              <string>&amp;Require Authentication</string>
+             </property>
+            </widget>
+           </item>
+           <item row="4" column="0">
+            <widget class="QLabel" name="credsLabel">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>&amp;Credentials File</string>
+             </property>
+             <property name="buddy">
+              <cstring>credsEdit</cstring>
+             </property>
+            </widget>
+           </item>
+           <item row="4" column="1">
+            <widget class="QLineEdit" name="credsEdit">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Choose the file containing the list of usernames and passwords.</string>
+             </property>
+            </widget>
+           </item>
+           <item row="4" column="2">
+            <widget class="QPushButton" name="credsBrowse">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>Browse</string>
+             </property>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+        </item>
+        <item row="11" column="0" colspan="2">
+         <layout class="QHBoxLayout" name="aboutLayout">
+          <item>
+           <spacer name="aboutSpacer">
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+          <item>
+           <widget class="QPushButton" name="vsModeButton">
+            <property name="text">
+             <string>&amp;Virtual Studio Mode</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QPushButton" name="aboutButton">
+            <property name="text">
+             <string>About</string>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+        <item row="5" column="0" colspan="2">
+         <widget class="QCheckBox" name="zeroCheckBox">
+          <property name="toolTip">
+           <string>Silence the audio when there's a buffer underrun.</string>
+          </property>
+          <property name="text">
+           <string>Set buffer to &amp;zero when underrun occurs</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QWidget" name="advancedTab">
+       <property name="enabled">
+        <bool>true</bool>
+       </property>
+       <attribute name="title">
+        <string>Advanced options</string>
+       </attribute>
+       <layout class="QGridLayout" name="gridLayout_4">
+        <item row="5" column="0">
+         <widget class="QLabel" name="queueLengthLabel">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>&amp;Queue Buffer Length</string>
+          </property>
+          <property name="buddy">
+           <cstring>queueLengthSpinBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="1" colspan="2">
+         <widget class="QLineEdit" name="clientNameEdit">
+          <property name="toolTip">
+           <string>Set the name of the Jack client.</string>
+          </property>
+          <property name="maxLength">
+           <number>64</number>
+          </property>
+          <property name="placeholderText">
+           <string>JackTrip</string>
+          </property>
+         </widget>
+        </item>
+        <item row="13" column="0">
+         <spacer name="advancedVerticalSpacer">
+          <property name="orientation">
+           <enum>Qt::Vertical</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>20</width>
+            <height>40</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="7" column="0">
+         <widget class="QLabel" name="resolutionLabel">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>Audio &amp;Bit Resolution</string>
+          </property>
+          <property name="buddy">
+           <cstring>resolutionComboBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="1" colspan="2">
+         <widget class="QLineEdit" name="remoteNameEdit">
+          <property name="toolTip">
+           <string>Set the name of the Jack client as it will appear on the hub server.</string>
+          </property>
+          <property name="maxLength">
+           <number>64</number>
+          </property>
+         </widget>
+        </item>
+        <item row="7" column="1" colspan="2">
+         <widget class="QComboBox" name="resolutionComboBox">
+          <property name="toolTip">
+           <string>Select the audio bit resolution.</string>
+          </property>
+          <property name="currentIndex">
+           <number>1</number>
+          </property>
+          <item>
+           <property name="text">
+            <string>8</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>16</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>24</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>32</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+        <item row="9" column="0" colspan="3">
+         <widget class="QCheckBox" name="realTimeCheckBox">
+          <property name="toolTip">
+           <string>Use real time priority for the networking threads.</string>
+          </property>
+          <property name="text">
+           <string>Enable real&amp;time priority for networking threads</string>
+          </property>
+          <property name="checked">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="11" column="1">
+         <widget class="QSpinBox" name="ioStatsSpinBox">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="toolTip">
+           <string>Choose how often stats should be reported.</string>
+          </property>
+          <property name="minimum">
+           <number>1</number>
+          </property>
+          <property name="maximum">
+           <number>120</number>
+          </property>
+         </widget>
+        </item>
+        <item row="4" column="1" colspan="2">
+         <widget class="QSpinBox" name="basePortSpinBox">
+          <property name="toolTip">
+           <string>Set the base UDP port to be used by connecting hub clients. The default is 61002.
+(You should manually set this if running multiple hub servers on the same machine.)</string>
+          </property>
+          <property name="minimum">
+           <number>1024</number>
+          </property>
+          <property name="maximum">
+           <number>65535</number>
+          </property>
+          <property name="value">
+           <number>61002</number>
+          </property>
+         </widget>
+        </item>
+        <item row="14" column="0" colspan="3">
+         <layout class="QHBoxLayout" name="useDefaultsLayout">
+          <item>
+           <spacer name="useDefaultsSpacer">
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+          <item>
+           <widget class="QPushButton" name="commandLineButton">
+            <property name="toolTip">
+             <string>Show the equivalent command line for the current settings.</string>
+            </property>
+            <property name="text">
+             <string>Get Command &amp;Line</string>
+            </property>
+           </widget>
+          </item>
+          <item>
+           <widget class="QPushButton" name="useDefaultsButton">
+            <property name="text">
+             <string>Use &amp;Defaults</string>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+        <item row="1" column="0">
+         <widget class="QLabel" name="remoteNameLabel">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>Remote Client &amp;Name</string>
+          </property>
+          <property name="buddy">
+           <cstring>remoteNameEdit</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="11" column="0">
+         <widget class="QCheckBox" name="ioStatsCheckBox">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="toolTip">
+           <string>Display IO stats in another window.</string>
+          </property>
+          <property name="text">
+           <string>Display &amp;IO Stats</string>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="0">
+         <widget class="QLabel" name="remotePortLabel">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>Remote &amp;Port</string>
+          </property>
+          <property name="buddy">
+           <cstring>remotePortSpinBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="0">
+         <widget class="QLabel" name="clientNameLabel">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>Custom Client &amp;Name</string>
+          </property>
+          <property name="buddy">
+           <cstring>clientNameEdit</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="6" column="0">
+         <widget class="QLabel" name="redundancyLabel">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>&amp;Redundancy</string>
+          </property>
+          <property name="buddy">
+           <cstring>redundancySpinBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="8" column="0" colspan="3">
+         <widget class="QCheckBox" name="connectAudioCheckBox">
+          <property name="toolTip">
+           <string>Connect the Jack client to the default system audio ports.</string>
+          </property>
+          <property name="text">
+           <string>&amp;Connect default audio ports</string>
+          </property>
+          <property name="checked">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="1" colspan="2">
+         <widget class="QSpinBox" name="localPortSpinBox">
+          <property name="toolTip">
+           <string>Set the local port to use for the connection. The default is 4464.
+(Useful for running multiple hub clients behind the same router.)</string>
+          </property>
+          <property name="minimum">
+           <number>1024</number>
+          </property>
+          <property name="maximum">
+           <number>65535</number>
+          </property>
+          <property name="value">
+           <number>4464</number>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="1" colspan="2">
+         <widget class="QSpinBox" name="remotePortSpinBox">
+          <property name="toolTip">
+           <string>Set the remote port to use for the connection. The default is 4464.</string>
+          </property>
+          <property name="minimum">
+           <number>1024</number>
+          </property>
+          <property name="maximum">
+           <number>65535</number>
+          </property>
+          <property name="value">
+           <number>4464</number>
+          </property>
+         </widget>
+        </item>
+        <item row="4" column="0">
+         <widget class="QLabel" name="basePortLabel">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>&amp;UDP Base Port</string>
+          </property>
+          <property name="buddy">
+           <cstring>basePortSpinBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="0">
+         <widget class="QLabel" name="localPortLabel">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>&amp;Local Port</string>
+          </property>
+          <property name="buddy">
+           <cstring>localPortSpinBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="6" column="1" colspan="2">
+         <widget class="QSpinBox" name="redundancySpinBox">
+          <property name="toolTip">
+           <string>Number of redundant packets to be sent to avoid glitches related to packet loss.</string>
+          </property>
+          <property name="minimum">
+           <number>1</number>
+          </property>
+          <property name="value">
+           <number>1</number>
+          </property>
+         </widget>
+        </item>
+        <item row="11" column="2">
+         <widget class="QLabel" name="ioStatsLabel">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>Reporting &amp;Interval (s)</string>
+          </property>
+          <property name="buddy">
+           <cstring>ioStatsSpinBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="5" column="1" colspan="2">
+         <widget class="QSpinBox" name="queueLengthSpinBox">
+          <property name="toolTip">
+           <string>Set the queue buffer length, in packet size.</string>
+          </property>
+          <property name="minimum">
+           <number>2</number>
+          </property>
+          <property name="maximum">
+           <number>999</number>
+          </property>
+          <property name="value">
+           <number>4</number>
+          </property>
+         </widget>
+        </item>
+        <item row="12" column="0" colspan="3">
+         <widget class="QCheckBox" name="verboseCheckBox">
+          <property name="toolTip">
+           <string>Display debugging information that would normally appear on the console.</string>
+          </property>
+          <property name="text">
+           <string>Show &amp;Debug Information</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QWidget" name="backendTab">
+       <attribute name="title">
+        <string>Audio Backend</string>
+       </attribute>
+       <layout class="QGridLayout" name="gridLayout_11">
+        <item row="9" column="0" colspan="2">
+         <layout class="QHBoxLayout" name="deviceManagementLayout">
+          <item>
+           <spacer name="backendTabSpacer">
+            <property name="orientation">
+             <enum>Qt::Horizontal</enum>
+            </property>
+            <property name="sizeHint" stdset="0">
+             <size>
+              <width>40</width>
+              <height>20</height>
+             </size>
+            </property>
+           </spacer>
+          </item>
+          <item>
+           <widget class="QPushButton" name="refreshDevicesButton">
+            <property name="enabled">
+             <bool>false</bool>
+            </property>
+            <property name="text">
+             <string>&amp;Refresh Device List</string>
+            </property>
+           </widget>
+          </item>
+         </layout>
+        </item>
+        <item row="0" column="1">
+         <widget class="QComboBox" name="backendComboBox">
+          <property name="toolTip">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Choose the audio backend to use. JACK is the default and is well tested, but requires the JACK audio server to be installed.&lt;/p&gt;&lt;p&gt;RtAudio is still a work in progress, but it works with your operating system's native audio drivers and requires no additional software.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+          </property>
+          <item>
+           <property name="text">
+            <string>JACK</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>RtAudio</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+        <item row="3" column="1">
+         <widget class="QComboBox" name="inputDeviceComboBox">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="6" column="1">
+         <spacer name="backendSpacer">
+          <property name="orientation">
+           <enum>Qt::Vertical</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>20</width>
+            <height>444</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="3" column="0">
+         <widget class="QLabel" name="inputDeviceLabel">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>&amp;Input Device:</string>
+          </property>
+          <property name="buddy">
+           <cstring>inputDeviceComboBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="1">
+         <widget class="QComboBox" name="bufferSizeComboBox">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="toolTip">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Set the driver's buffer size to use with the RtAudio backend.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+          </property>
+          <property name="currentIndex">
+           <number>3</number>
+          </property>
+          <item>
+           <property name="text">
+            <string>16</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>32</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>64</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>128</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>256</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>512</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>1024</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+        <item row="0" column="0">
+         <widget class="QLabel" name="backendLabel">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>Audio &amp;Backend:</string>
+          </property>
+          <property name="buddy">
+           <cstring>backendComboBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="0">
+         <widget class="QLabel" name="bufferSizeLabel">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>&amp;Buffer Size:</string>
+          </property>
+          <property name="buddy">
+           <cstring>bufferSizeComboBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="1">
+         <widget class="QComboBox" name="sampleRateComboBox">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="toolTip">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Set the audio sample rate to use with the RtAudio backend. This setting should be the same on both ends of the connection.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+          </property>
+          <property name="currentText">
+           <string>48000</string>
+          </property>
+          <property name="currentIndex">
+           <number>3</number>
+          </property>
+          <item>
+           <property name="text">
+            <string>22050</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>32000</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>44100</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>48000</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>88200</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>96000</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>192000</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+        <item row="1" column="0">
+         <widget class="QLabel" name="sampleRateLabel">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>&amp;Sampling Rate:</string>
+          </property>
+          <property name="buddy">
+           <cstring>sampleRateComboBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="4" column="0">
+         <widget class="QLabel" name="outputDeviceLabel">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>&amp;Output Device:</string>
+          </property>
+          <property name="buddy">
+           <cstring>outputDeviceComboBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="4" column="1">
+         <widget class="QComboBox" name="outputDeviceComboBox">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="5" column="0" colspan="2">
+         <widget class="QLabel" name="backendWarningLabel">
+          <property name="text">
+           <string>These settings are ignored in hub server mode which requires JACK to operate.</string>
+          </property>
+          <property name="wordWrap">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QWidget" name="JitterTab">
+       <attribute name="title">
+        <string>Jitter Buffer</string>
+       </attribute>
+       <layout class="QGridLayout" name="gridLayout_8">
+        <item row="10" column="2">
+         <widget class="QLabel" name="packetsLabel">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="text">
+           <string>Packets</string>
+          </property>
+         </widget>
+        </item>
+        <item row="8" column="0" colspan="3">
+         <widget class="QCheckBox" name="autoQueueCheckBox">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="text">
+           <string>Automatically set the &amp;queue length</string>
+          </property>
+          <property name="checked">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="9" column="0" colspan="2">
+         <widget class="QLabel" name="autoQueueLabel">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>Aim to &amp;drop no more than one in every</string>
+          </property>
+          <property name="buddy">
+           <cstring>autoQueueSpinBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="5" column="0" colspan="3">
+         <widget class="Line" name="bufferLine">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+         </widget>
+        </item>
+        <item row="9" column="2">
+         <widget class="QSpinBox" name="autoQueueSpinBox">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="maximum">
+           <number>1000000</number>
+          </property>
+          <property name="singleStep">
+           <number>100</number>
+          </property>
+          <property name="value">
+           <number>500</number>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="0" colspan="3">
+         <widget class="QCheckBox" name="broadcastCheckBox">
+          <property name="toolTip">
+           <string>Enable a second, broadcast output with a higher queue length
+for better quality at the expense of latency.</string>
+          </property>
+          <property name="text">
+           <string>Enable &amp;Broadcast Output</string>
+          </property>
+         </widget>
+        </item>
+        <item row="12" column="0">
+         <spacer name="verticalSpacer">
+          <property name="orientation">
+           <enum>Qt::Vertical</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>20</width>
+            <height>40</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="11" column="0" colspan="3">
+         <widget class="QLabel" name="autoQueueExplanationLabel">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="text">
+           <string>This will override the queue buffer length entered in the advanced options tab. (The default value is 500.)</string>
+          </property>
+          <property name="wordWrap">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="0" colspan="2">
+         <widget class="QCheckBox" name="jitterCheckBox">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="toolTip">
+           <string>Enable the new jitter buffer. This is now the default.</string>
+          </property>
+          <property name="text">
+           <string>Enable &amp;Jitter Buffer</string>
+          </property>
+          <property name="checked">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="2" column="1" colspan="2">
+         <widget class="QComboBox" name="bufferStrategyComboBox">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="toolTip">
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Buffer strategy 1 attempts to drop as little audio data as possible without exceeding the maximum queue length. It operates very similarly to the original buffer implementation with a few fixes to drop even less audio.&lt;/p&gt;&lt;p&gt;Buffer strategy 2 is optimized to keep the latency stable, so that the delay experienced over the connection doesn't fluctuate and is as predictable as possible. The trade off is that more audio might be dropped, but the difference should be negligible with the right queue length.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+          </property>
+          <property name="currentIndex">
+           <number>0</number>
+          </property>
+          <item>
+           <property name="text">
+            <string>1 (adaptable latency)</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>2 (stable latency)</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>3 (loss concealment)</string>
+           </property>
+          </item>
+          <item>
+           <property name="text">
+            <string>4 (same as 3)</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+        <item row="2" column="0">
+         <widget class="QLabel" name="bufferStrategyLabel">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Maximum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>Use buffer &amp;strategy</string>
+          </property>
+          <property name="buddy">
+           <cstring>bufferStrategyComboBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="4" column="0">
+         <widget class="QLabel" name="broadcastQueueLabel">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="text">
+           <string>Broadcast &amp;Queue Length</string>
+          </property>
+          <property name="buddy">
+           <cstring>broadcastQueueSpinBox</cstring>
+          </property>
+         </widget>
+        </item>
+        <item row="4" column="1" colspan="2">
+         <widget class="QSpinBox" name="broadcastQueueSpinBox">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="toolTip">
+           <string>Set the broadcast queue buffer length, in packet size.</string>
+          </property>
+          <property name="minimum">
+           <number>2</number>
+          </property>
+          <property name="maximum">
+           <number>999</number>
+          </property>
+          <property name="value">
+           <number>8</number>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QWidget" name="pluginsTab">
+       <attribute name="title">
+        <string>Plugins</string>
+       </attribute>
+       <layout class="QVBoxLayout" name="verticalLayout">
+        <item>
+         <widget class="QGroupBox" name="incomingGroupBox">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="title">
+           <string>Incoming</string>
+          </property>
+          <layout class="QGridLayout" name="gridLayout_5">
+           <item row="1" column="1">
+            <widget class="QSlider" name="inZitarevWetnessSlider">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Set the wet/dry mix.</string>
+             </property>
+             <property name="maximum">
+              <number>100</number>
+             </property>
+             <property name="orientation">
+              <enum>Qt::Horizontal</enum>
+             </property>
+             <property name="tickPosition">
+              <enum>QSlider::TicksBelow</enum>
+             </property>
+             <property name="tickInterval">
+              <number>20</number>
+             </property>
+            </widget>
+           </item>
+           <item row="0" column="2">
+            <widget class="QLabel" name="inFreeverbLabel">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>Wetness</string>
+             </property>
+            </widget>
+           </item>
+           <item row="4" column="0">
+            <widget class="QCheckBox" name="inLimiterCheckBox">
+             <property name="toolTip">
+              <string>Enable the limiter on incoming audio.</string>
+             </property>
+             <property name="text">
+              <string>&amp;Limiter</string>
+             </property>
+            </widget>
+           </item>
+           <item row="0" column="0">
+            <widget class="QCheckBox" name="inFreeverbCheckBox">
+             <property name="toolTip">
+              <string>Enable the freeverb plugin on incoming audio.</string>
+             </property>
+             <property name="text">
+              <string>&amp;Freeverb</string>
+             </property>
+            </widget>
+           </item>
+           <item row="1" column="2" alignment="Qt::AlignHCenter">
+            <widget class="QLabel" name="inZitarevLabel">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>Wetness</string>
+             </property>
+            </widget>
+           </item>
+           <item row="3" column="0">
+            <widget class="QCheckBox" name="inCompressorCheckBox">
+             <property name="toolTip">
+              <string>Enable the compressor plugin on incoming audio.</string>
+             </property>
+             <property name="text">
+              <string>&amp;Compressor</string>
+             </property>
+            </widget>
+           </item>
+           <item row="1" column="0">
+            <widget class="QCheckBox" name="inZitarevCheckBox">
+             <property name="toolTip">
+              <string>Enable the zitarev reverb plugin on incoming audio.</string>
+             </property>
+             <property name="text">
+              <string>&amp;Zitarev</string>
+             </property>
+            </widget>
+           </item>
+           <item row="0" column="1">
+            <widget class="QSlider" name="inFreeverbWetnessSlider">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Set the wet/dry mix.</string>
+             </property>
+             <property name="maximum">
+              <number>100</number>
+             </property>
+             <property name="singleStep">
+              <number>1</number>
+             </property>
+             <property name="orientation">
+              <enum>Qt::Horizontal</enum>
+             </property>
+             <property name="invertedAppearance">
+              <bool>false</bool>
+             </property>
+             <property name="tickPosition">
+              <enum>QSlider::TicksBelow</enum>
+             </property>
+             <property name="tickInterval">
+              <number>20</number>
+             </property>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+        </item>
+        <item>
+         <widget class="QGroupBox" name="outgoingGroupBox">
+          <property name="enabled">
+           <bool>true</bool>
+          </property>
+          <property name="title">
+           <string>Outgoing</string>
+          </property>
+          <layout class="QGridLayout" name="gridLayout_2">
+           <item row="0" column="0">
+            <widget class="QCheckBox" name="outFreeverbCheckBox">
+             <property name="toolTip">
+              <string>Enable the freeverb plugin on outgoing audio.</string>
+             </property>
+             <property name="text">
+              <string>Free&amp;verb</string>
+             </property>
+            </widget>
+           </item>
+           <item row="0" column="1">
+            <widget class="QSlider" name="outFreeverbWetnessSlider">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Set the wet/dry mix.</string>
+             </property>
+             <property name="maximum">
+              <number>100</number>
+             </property>
+             <property name="singleStep">
+              <number>1</number>
+             </property>
+             <property name="orientation">
+              <enum>Qt::Horizontal</enum>
+             </property>
+             <property name="invertedAppearance">
+              <bool>false</bool>
+             </property>
+             <property name="tickPosition">
+              <enum>QSlider::TicksBelow</enum>
+             </property>
+             <property name="tickInterval">
+              <number>20</number>
+             </property>
+            </widget>
+           </item>
+           <item row="0" column="2">
+            <widget class="QLabel" name="outFreeverbLabel">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>Wetness</string>
+             </property>
+            </widget>
+           </item>
+           <item row="1" column="0">
+            <widget class="QCheckBox" name="outZitarevCheckBox">
+             <property name="toolTip">
+              <string>Enable the zitarev reverb plugin on outgoing audio.</string>
+             </property>
+             <property name="text">
+              <string>Zi&amp;tarev</string>
+             </property>
+            </widget>
+           </item>
+           <item row="1" column="1">
+            <widget class="QSlider" name="outZitarevWetnessSlider">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Set the wet/dry mix.</string>
+             </property>
+             <property name="maximum">
+              <number>100</number>
+             </property>
+             <property name="orientation">
+              <enum>Qt::Horizontal</enum>
+             </property>
+             <property name="tickPosition">
+              <enum>QSlider::TicksBelow</enum>
+             </property>
+             <property name="tickInterval">
+              <number>20</number>
+             </property>
+            </widget>
+           </item>
+           <item row="1" column="2">
+            <widget class="QLabel" name="outZitarevLabel">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Moisture is the essence of wetness,
+and wetness is the essence of beauty.</string>
+             </property>
+             <property name="text">
+              <string>Wetness</string>
+             </property>
+            </widget>
+           </item>
+           <item row="2" column="0">
+            <widget class="QCheckBox" name="outCompressorCheckBox">
+             <property name="toolTip">
+              <string>Enable the compressor plugin on outgoing audio.</string>
+             </property>
+             <property name="text">
+              <string>Com&amp;pressor</string>
+             </property>
+            </widget>
+           </item>
+           <item row="3" column="0">
+            <widget class="QCheckBox" name="outLimiterCheckBox">
+             <property name="toolTip">
+              <string>Enable the limiter on outgoing audio.</string>
+             </property>
+             <property name="text">
+              <string>Li&amp;miter</string>
+             </property>
+            </widget>
+           </item>
+           <item row="3" column="1">
+            <widget class="QSpinBox" name="outClientsSpinBox">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="toolTip">
+              <string>Enter the anticipated number of clients that will be connected to the server.</string>
+             </property>
+             <property name="minimum">
+              <number>1</number>
+             </property>
+             <property name="maximum">
+              <number>100</number>
+             </property>
+             <property name="value">
+              <number>2</number>
+             </property>
+            </widget>
+           </item>
+           <item row="3" column="2">
+            <widget class="QLabel" name="outLimiterLabel">
+             <property name="enabled">
+              <bool>false</bool>
+             </property>
+             <property name="text">
+              <string>Clients</string>
+             </property>
+            </widget>
+           </item>
+          </layout>
+         </widget>
+        </item>
+        <item>
+         <spacer name="pluginsVerticalSpacer">
+          <property name="orientation">
+           <enum>Qt::Vertical</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>20</width>
+            <height>40</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+       </layout>
+      </widget>
+      <widget class="QWidget" name="scriptingTab">
+       <attribute name="title">
+        <string>Scripting</string>
+       </attribute>
+       <layout class="QGridLayout" name="gridLayout_12">
+        <item row="3" column="0">
+         <widget class="QLineEdit" name="disconnectScriptEdit">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="0">
+         <widget class="QLineEdit" name="connectScriptEdit">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+         </widget>
+        </item>
+        <item row="1" column="1">
+         <widget class="QPushButton" name="connectScriptBrowse">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="text">
+           <string>Browse</string>
+          </property>
+         </widget>
+        </item>
+        <item row="0" column="0" colspan="2">
+         <widget class="QCheckBox" name="connectScriptCheckBox">
+          <property name="text">
+           <string>Execute script on &amp;connection</string>
+          </property>
+         </widget>
+        </item>
+        <item row="5" column="0">
+         <spacer name="scriptingVerticalSpacer">
+          <property name="orientation">
+           <enum>Qt::Vertical</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>20</width>
+            <height>40</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item row="2" column="0" colspan="2">
+         <widget class="QCheckBox" name="disconnectScriptCheckBox">
+          <property name="text">
+           <string>Execute script on &amp;disconnection</string>
+          </property>
+         </widget>
+        </item>
+        <item row="3" column="1">
+         <widget class="QPushButton" name="disconnectScriptBrowse">
+          <property name="enabled">
+           <bool>false</bool>
+          </property>
+          <property name="text">
+           <string>Browse</string>
+          </property>
+         </widget>
+        </item>
+        <item row="4" column="0" colspan="2">
+         <widget class="QLabel" name="environmentVariableLabel">
+          <property name="text">
+           <string>Any scripts that you write will have access to the following environment variables:
+
+JT_CLIENT_NAME - The name of the created JACK client
+JT_SEND_CHANNELS - The number of audio channels sent to the network
+JT_RECV_CHANNELS - The number of audio channels received from the network</string>
+          </property>
+          <property name="wordWrap">
+           <bool>true</bool>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </widget>
+    </item>
+    <item row="0" column="1">
+     <widget class="QComboBox" name="typeComboBox">
+      <property name="toolTip">
+       <string>To connect to a p2p (peer to peer) server you need to run as a p2p client.
+To connect to a hub server you need to run as a hub client.</string>
+      </property>
+      <property name="currentIndex">
+       <number>2</number>
+      </property>
+      <item>
+       <property name="text">
+        <string>P2P Client</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>P2P Server</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Hub Client</string>
+       </property>
+      </item>
+      <item>
+       <property name="text">
+        <string>Hub Server</string>
+       </property>
+      </item>
+     </widget>
+    </item>
+    <item row="1" column="1">
+     <widget class="QComboBox" name="addressComboBox">
+      <property name="sizePolicy">
+       <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+        <horstretch>0</horstretch>
+        <verstretch>0</verstretch>
+       </sizepolicy>
+      </property>
+      <property name="toolTip">
+       <string>Enter the IP address or the hostname of the server you want to connect to.</string>
+      </property>
+      <property name="editable">
+       <bool>true</bool>
+      </property>
+      <property name="maxCount">
+       <number>5</number>
+      </property>
+      <property name="insertPolicy">
+       <enum>QComboBox::NoInsert</enum>
+      </property>
+     </widget>
+    </item>
+    <item row="7" column="0" colspan="2">
+     <widget class="QGroupBox" name="outputGroupBox">
+      <property name="title">
+       <string>Output Channels</string>
+      </property>
+     </widget>
+    </item>
+   </layout>
+  </widget>
+  <widget class="QMenuBar" name="menuBar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>409</width>
+     <height>30</height>
+    </rect>
+   </property>
+  </widget>
+  <widget class="QStatusBar" name="statusBar"/>
+ </widget>
+ <tabstops>
+  <tabstop>typeComboBox</tabstop>
+  <tabstop>addressComboBox</tabstop>
+  <tabstop>connectButton</tabstop>
+  <tabstop>disconnectButton</tabstop>
+  <tabstop>exitButton</tabstop>
+  <tabstop>optionsTabWidget</tabstop>
+  <tabstop>channelSendSpinBox</tabstop>
+  <tabstop>channelRecvSpinBox</tabstop>
+  <tabstop>autoPatchComboBox</tabstop>
+  <tabstop>patchServerCheckBox</tabstop>
+  <tabstop>upmixCheckBox</tabstop>
+  <tabstop>zeroCheckBox</tabstop>
+  <tabstop>timeoutCheckBox</tabstop>
+  <tabstop>requireAuthCheckBox</tabstop>
+  <tabstop>certEdit</tabstop>
+  <tabstop>certBrowse</tabstop>
+  <tabstop>keyEdit</tabstop>
+  <tabstop>keyBrowse</tabstop>
+  <tabstop>credsEdit</tabstop>
+  <tabstop>credsBrowse</tabstop>
+  <tabstop>authCheckBox</tabstop>
+  <tabstop>usernameEdit</tabstop>
+  <tabstop>passwordEdit</tabstop>
+  <tabstop>vsModeButton</tabstop>
+  <tabstop>aboutButton</tabstop>
+  <tabstop>clientNameEdit</tabstop>
+  <tabstop>remoteNameEdit</tabstop>
+  <tabstop>localPortSpinBox</tabstop>
+  <tabstop>remotePortSpinBox</tabstop>
+  <tabstop>basePortSpinBox</tabstop>
+  <tabstop>queueLengthSpinBox</tabstop>
+  <tabstop>redundancySpinBox</tabstop>
+  <tabstop>resolutionComboBox</tabstop>
+  <tabstop>connectAudioCheckBox</tabstop>
+  <tabstop>realTimeCheckBox</tabstop>
+  <tabstop>ioStatsCheckBox</tabstop>
+  <tabstop>ioStatsSpinBox</tabstop>
+  <tabstop>verboseCheckBox</tabstop>
+  <tabstop>commandLineButton</tabstop>
+  <tabstop>useDefaultsButton</tabstop>
+  <tabstop>backendComboBox</tabstop>
+  <tabstop>sampleRateComboBox</tabstop>
+  <tabstop>bufferSizeComboBox</tabstop>
+  <tabstop>inputDeviceComboBox</tabstop>
+  <tabstop>outputDeviceComboBox</tabstop>
+  <tabstop>refreshDevicesButton</tabstop>
+  <tabstop>jitterCheckBox</tabstop>
+  <tabstop>bufferStrategyComboBox</tabstop>
+  <tabstop>broadcastCheckBox</tabstop>
+  <tabstop>broadcastQueueSpinBox</tabstop>
+  <tabstop>autoQueueCheckBox</tabstop>
+  <tabstop>autoQueueSpinBox</tabstop>
+  <tabstop>inFreeverbCheckBox</tabstop>
+  <tabstop>inFreeverbWetnessSlider</tabstop>
+  <tabstop>inZitarevCheckBox</tabstop>
+  <tabstop>inZitarevWetnessSlider</tabstop>
+  <tabstop>inCompressorCheckBox</tabstop>
+  <tabstop>inLimiterCheckBox</tabstop>
+  <tabstop>outFreeverbCheckBox</tabstop>
+  <tabstop>outFreeverbWetnessSlider</tabstop>
+  <tabstop>outZitarevCheckBox</tabstop>
+  <tabstop>outZitarevWetnessSlider</tabstop>
+  <tabstop>outCompressorCheckBox</tabstop>
+  <tabstop>outLimiterCheckBox</tabstop>
+  <tabstop>outClientsSpinBox</tabstop>
+  <tabstop>connectScriptCheckBox</tabstop>
+  <tabstop>connectScriptEdit</tabstop>
+  <tabstop>connectScriptBrowse</tabstop>
+  <tabstop>disconnectScriptCheckBox</tabstop>
+  <tabstop>disconnectScriptEdit</tabstop>
+  <tabstop>disconnectScriptBrowse</tabstop>
+ </tabstops>
+ <resources>
+  <include location="qjacktrip.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/src/gui/qjacktrip_novs.qrc b/src/gui/qjacktrip_novs.qrc
new file mode 100644 (file)
index 0000000..179c85a
--- /dev/null
@@ -0,0 +1,7 @@
+<RCC>
+  <qresource prefix="qjacktrip">
+    <file>about@2x.png</file>
+    <file>about.png</file>
+    <file>icon.png</file>
+  </qresource>
+</RCC>
diff --git a/src/gui/quiet.svg b/src/gui/quiet.svg
new file mode 100644 (file)
index 0000000..b2ea070
--- /dev/null
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg viewBox="0 0 10.05 12" width="10.05" height="12" fill="none" xmlns="http://www.w3.org/2000/svg">
+  <path d="M 0 8.074 L 0 3.96 L 2.743 3.96 L 6.171 0.531 L 6.171 11.503 L 2.743 8.074 L 0 8.074 Z M 7.2 8.897 L 7.2 3.12 C 7.829 3.314 8.329 3.68 8.7 4.217 C 9.071 4.754 9.257 5.354 9.257 6.017 C 9.257 6.669 9.069 7.263 8.691 7.8 C 8.314 8.337 7.817 8.703 7.2 8.897 Z M 5.143 3.137 L 3.206 4.989 L 1.029 4.989 L 1.029 7.046 L 3.206 7.046 L 5.143 8.914 L 5.143 3.137 Z" fill="#353637"/>
+</svg>
\ No newline at end of file
diff --git a/src/gui/refresh.svg b/src/gui/refresh.svg
new file mode 100644 (file)
index 0000000..d2c5a92
--- /dev/null
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24">
+<path d="M12 20q-3.35 0-5.675-2.325Q4 15.35 4 12q0-3.35 2.325-5.675Q8.65 4 12 4q1.725 0 3.3.713 1.575.712 2.7 2.037V4h2v7h-7V9h4.2q-.8-1.4-2.187-2.2Q13.625 6 12 6 9.5 6 7.75 7.75T6 12q0 2.5 1.75 4.25T12 18q1.925 0 3.475-1.1T17.65 14h2.1q-.7 2.65-2.85 4.325Q14.75 20 12 20Z"/>
+</svg>
\ No newline at end of file
diff --git a/src/gui/sentiment_very_dissatisfied.svg b/src/gui/sentiment_very_dissatisfied.svg
new file mode 100644 (file)
index 0000000..5af1e4b
--- /dev/null
@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M12 13.5C9.67 13.5 7.69 14.96 6.89 17H17.11C16.31 14.96 14.33 13.5 12 13.5ZM7.82 12L8.88 10.94L9.94 12L11 10.94L9.94 9.88L11 8.82L9.94 7.76L8.88 8.82L7.82 7.76L6.76 8.82L7.82 9.88L6.76 10.94L7.82 12ZM11.99 2C6.47 2 2 6.47 2 12C2 17.53 6.47 22 11.99 22C17.51 22 22 17.53 22 12C22 6.47 17.52 2 11.99 2ZM12 20C7.58 20 4 16.42 4 12C4 7.58 7.58 4 12 4C16.42 4 20 7.58 20 12C20 16.42 16.42 20 12 20ZM16.18 7.76L15.12 8.82L14.06 7.76L13 8.82L14.06 9.88L13 10.94L14.06 12L15.12 10.94L16.18 12L17.24 10.94L16.18 9.88L17.24 8.82L16.18 7.76Z" fill="black"/>
+</svg>
diff --git a/src/gui/share.svg b/src/gui/share.svg
new file mode 100644 (file)
index 0000000..77f6e6c
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
\ No newline at end of file
diff --git a/src/gui/speed.svg b/src/gui/speed.svg
new file mode 100644 (file)
index 0000000..7ab86e4
--- /dev/null
@@ -0,0 +1,4 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M20.3642 8.55102L19.1342 10.401C19.7274 11.5841 20.0178 12.8958 19.9794 14.2187C19.941 15.5416 19.575 16.8343 18.9142 17.981H5.05424C4.19541 16.4911 3.83954 14.7641 4.03938 13.0561C4.23923 11.348 4.98415 9.74984 6.16372 8.49844C7.34329 7.24705 8.89471 6.40906 10.588 6.10871C12.2813 5.80837 14.0262 6.06165 15.5642 6.83102L17.4142 5.60102C15.5307 4.39323 13.2966 3.85202 11.0691 4.06395C8.8417 4.27588 6.74969 5.22871 5.12773 6.77003C3.50578 8.31134 2.4476 10.3521 2.12246 12.5658C1.79732 14.7796 2.22399 17.0384 3.33424 18.981C3.50875 19.2833 3.75933 19.5346 4.06107 19.7101C4.36282 19.8855 4.70521 19.9789 5.05424 19.981H18.9042C19.2567 19.9824 19.6032 19.8907 19.9087 19.7151C20.2143 19.5395 20.468 19.2862 20.6442 18.981C21.5656 17.3849 22.028 15.5653 21.9804 13.723C21.9327 11.8807 21.3769 10.0873 20.3742 8.54102L20.3642 8.55102Z" fill="black"/>
+<path d="M10.5742 15.391C10.76 15.577 10.9806 15.7245 11.2234 15.8251C11.4662 15.9258 11.7264 15.9776 11.9892 15.9776C12.2521 15.9776 12.5123 15.9258 12.7551 15.8251C12.9979 15.7245 13.2185 15.577 13.4042 15.391L19.0642 6.90102L10.5742 12.561C10.3883 12.7468 10.2408 12.9673 10.1401 13.2101C10.0395 13.4529 9.98767 13.7132 9.98767 13.976C9.98767 14.2388 10.0395 14.4991 10.1401 14.7419C10.2408 14.9847 10.3883 15.2053 10.5742 15.391Z" fill="black"/>
+</svg>
diff --git a/src/gui/star.svg b/src/gui/star.svg
new file mode 100644 (file)
index 0000000..971dd91
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 17.27l5.17 3.12c.38.23.85-.11.75-.54l-1.37-5.88 4.56-3.95c.33-.29.16-.84-.29-.88l-6.01-.51-2.35-5.54c-.17-.41-.75-.41-.92 0L9.19 8.63l-6.01.51c-.44.04-.62.59-.28.88l4.56 3.95-1.37 5.88c-.1.43.37.77.75.54L12 17.27z"/></svg>
\ No newline at end of file
diff --git a/src/gui/start.svg b/src/gui/start.svg
new file mode 100644 (file)
index 0000000..f75a587
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24" fill="#000000"><path d="M8 6.82v10.36c0 .79.87 1.27 1.54.84l8.14-5.18c.62-.39.62-1.29 0-1.69L9.54 5.98C8.87 5.55 8 6.03 8 6.82z" /></svg>
\ No newline at end of file
diff --git a/src/gui/textbuf.cpp b/src/gui/textbuf.cpp
new file mode 100644 (file)
index 0000000..7e916a3
--- /dev/null
@@ -0,0 +1,69 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2021 Aaron Wyatt.
+
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#include "textbuf.h"
+
+void textbuf::setOutStream(std::ostream* output)
+{
+    m_outStream = output;
+}
+
+int textbuf::overflow(int c)
+{
+    // Output our buffer.
+    putChars(pbase(), pptr());
+
+    if (c != traits_t::eof()) {
+        char out = c;
+        putChars(&out, &out + 1);
+    }
+
+    // Set buffer to empty again
+    setp(m_buf, m_buf + BUF_SIZE);
+
+    return c;
+}
+
+int textbuf::sync()
+{
+    // Flush our buffer.
+    putChars(pbase(), pptr());
+    setp(m_buf, m_buf + BUF_SIZE);
+    return 0;
+}
+
+void textbuf::putChars(const char* begin, const char* end)
+{
+    if (m_outStream) {
+        for (const char* c = begin; c < end; c++) {
+            *m_outStream << *c;
+        }
+        m_outStream->flush();
+    }
+
+    // Send a signal here rather than writing directly to our
+    // QTextEdit to avoid any issues with threading.
+    emit outputString(QString(QByteArray(begin, end - begin)));
+}
diff --git a/src/gui/textbuf.h b/src/gui/textbuf.h
new file mode 100644 (file)
index 0000000..ea7f37e
--- /dev/null
@@ -0,0 +1,66 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2021 Aaron Wyatt.
+
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#ifndef TEXTBUF_H
+#define TEXTBUF_H
+
+#include <QPlainTextEdit>
+#include <iostream>
+#include <streambuf>
+
+// Extension of a stream buffer to output to a QTextEdit via a signal
+class textbuf
+    : public QObject
+    , public std::basic_streambuf<char, std::char_traits<char>>
+{
+    Q_OBJECT
+
+   public:
+    textbuf(QObject* parent = nullptr) : QObject(parent)
+    {
+        setp(m_buf, m_buf + BUF_SIZE);
+    }
+
+    void setOutStream(std::ostream* output);
+
+   signals:
+    void outputString(const QString& output);
+
+   protected:
+    virtual int overflow(int c = traits_t::eof());
+    virtual int sync();
+
+   private:
+    typedef std::char_traits<char> traits_t;
+
+    static const size_t BUF_SIZE = 64;
+    char m_buf[BUF_SIZE];
+
+    std::ostream* m_outStream = nullptr;
+
+    void putChars(const char* begin, const char* end);
+};
+
+#endif  // TEXTBUF_H
diff --git a/src/gui/video.svg b/src/gui/video.svg
new file mode 100644 (file)
index 0000000..a1b644f
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 0 24 24" width="48px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M21 3H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 13H3V5h18v11z"/></svg>
\ No newline at end of file
diff --git a/src/gui/virtualstudio.cpp b/src/gui/virtualstudio.cpp
new file mode 100644 (file)
index 0000000..7223a99
--- /dev/null
@@ -0,0 +1,1638 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file virtualstudio.cpp
+ * \author Matt Horton, based on code by Aaron Wyatt
+ * \date March 2022
+ */
+
+#include "virtualstudio.h"
+
+#include <QDebug>
+#include <QDesktopServices>
+#include <QMessageBox>
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QSettings>
+#include <QSslSocket>
+#include <QSysInfo>
+#include <algorithm>
+#include <iostream>
+
+// TODO: remove me; including this to work-around this bug
+// https://bugreports.qt.io/browse/QTBUG-55199
+#include <QSvgGenerator>
+
+#include "../JackTrip.h"
+#include "../Settings.h"
+#include "../jacktrip_globals.h"
+#include "WebSocketTransport.h"
+#include "about.h"
+#include "qjacktrip.h"
+#include "vsApi.h"
+#include "vsAudio.h"
+#include "vsAuth.h"
+#include "vsDevice.h"
+#include "vsWebSocket.h"
+
+#ifdef __APPLE__
+#include "vsMacPermissions.h"
+#else
+#include "vsPermissions.h"
+#endif
+
+#ifdef _WIN32
+#include <wingdi.h>
+#endif
+
+VirtualStudio::VirtualStudio(bool firstRun, QObject* parent)
+    : QObject(parent)
+    , m_audioConfigPtr(
+          new VsAudio(this))  // this needs to be constructed before loadSettings()
+    , m_showFirstRun(firstRun)
+{
+    // load or initialize persisted settings
+    loadSettings();
+
+    // TODO: remove me; this is a hack for this bug
+    // https://bugreports.qt.io/browse/QTBUG-55199
+    QSvgGenerator svgImageHack;
+
+    // use a singleton QNetworkAccessManager
+    // WARNING: using a raw pointer and intentionally leaking this because
+    // it crashes at shutdown if you try to destruct it directly or try
+    // calling QObject::deleteLater()
+    m_networkAccessManagerPtr = new QNetworkAccessManager;
+
+    // instantiate API
+    m_api.reset(new VsApi(m_networkAccessManagerPtr));
+    m_api->setApiHost(PROD_API_HOST);
+    if (m_testMode) {
+        m_api->setApiHost(TEST_API_HOST);
+    }
+
+    // instantiate auth
+    m_auth.reset(new VsAuth(m_networkAccessManagerPtr, m_api.data()));
+    connect(m_auth.data(), &VsAuth::authSucceeded, this,
+            &VirtualStudio::slotAuthSucceeded);
+    connect(m_auth.data(), &VsAuth::refreshTokenFailed, this, [=]() {
+        m_auth->authenticate(QStringLiteral(""));  // retry without using refresh token
+    });
+    connect(m_auth.data(), &VsAuth::fetchUserInfoFailed, this, [=]() {
+        m_auth->authenticate(QStringLiteral(""));  // retry without using refresh token
+    });
+    connect(m_auth.data(), &VsAuth::deviceCodeExpired, this, [=]() {
+        m_auth->authenticate(QStringLiteral(""));  // retry without using refresh token
+    });
+
+    m_webChannelServer.reset(new QWebSocketServer(
+        QStringLiteral("Qt6 Virtual Studio Server"), QWebSocketServer::NonSecureMode));
+    connect(m_webChannelServer.data(), &QWebSocketServer::newConnection, this, [=]() {
+        m_webChannel->connectTo(
+            new WebSocketTransport(m_webChannelServer->nextPendingConnection()));
+    });
+
+    m_webChannel.reset(new QWebChannel());
+    m_webChannel->registerObject(QStringLiteral("virtualstudio"), this);
+
+    // Load our font for our qml interface
+    QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Regular.ttf"));
+    QFontDatabase::addApplicationFont(QStringLiteral(":/vs/Poppins-Bold.ttf"));
+
+    // Set our font scaling to convert points to pixels
+    m_fontScale = float(4.0 / 3.0);
+
+    // Initialize timer needed for network outage indicator
+    m_networkOutageTimer.setTimerType(Qt::CoarseTimer);
+    m_networkOutageTimer.setSingleShot(true);
+    m_networkOutageTimer.setInterval(5000);
+    m_networkOutageTimer.callOnTimeout([&]() {
+        if (m_devicePtr.isNull())
+            return;
+        m_devicePtr->setNetworkOutage(false);
+        emit updatedNetworkOutage(false);
+    });
+
+    if ((m_uiMode == QJackTrip::UNSET && vsFtux())
+        || (m_uiMode == QJackTrip::VIRTUAL_STUDIO)) {
+        m_windowState = QStringLiteral("login");
+    }
+
+    // register QML types
+    qmlRegisterType<VsServerInfo>("org.jacktrip.jacktrip", 1, 0, "VsServerInfo");
+
+    // setup QML view
+    m_view.engine()->rootContext()->setContextProperty(QStringLiteral("virtualstudio"),
+                                                       this);
+    m_view.engine()->rootContext()->setContextProperty(QStringLiteral("auth"),
+                                                       m_auth.get());
+    m_view.engine()->rootContext()->setContextProperty(QStringLiteral("audio"),
+                                                       m_audioConfigPtr.get());
+    m_view.engine()->rootContext()->setContextProperty(
+        QStringLiteral("permissions"),
+        QVariant::fromValue(&m_audioConfigPtr->getPermissions()));
+    m_view.setSource(QUrl(QStringLiteral("qrc:/vs/vs.qml")));
+    m_view.setMinimumSize(QSize(800, 640));
+    // m_view.setMaximumSize(QSize(696, 577));
+    m_view.setResizeMode(QQuickView::SizeRootObjectToView);
+    m_view.resize(800 * m_uiScale, 640 * m_uiScale);
+
+    // Connect our timers
+    connect(&m_refreshTimer, &QTimer::timeout, this, [&]() {
+        emit periodicRefresh();
+    });
+    connect(&m_heartbeatTimer, &QTimer::timeout, this, &VirtualStudio::sendHeartbeat,
+            Qt::QueuedConnection);
+
+    // QueuedConnection since refreshFinished is sometimes signaled from a network reply
+    // thread
+    connect(this, &VirtualStudio::refreshFinished, this, &VirtualStudio::joinStudio,
+            Qt::QueuedConnection);
+
+    // handle audio config errors
+    connect(&m_audioConfigPtr->getWorker(), &VsAudioWorker::signalError, this,
+            &VirtualStudio::processError, Qt::QueuedConnection);
+
+    // when connected to server, trigger UI modal when feedback is detected
+    connect(m_audioConfigPtr.get(), &VsAudio::feedbackDetected, this,
+            &VirtualStudio::detectedFeedbackLoop, Qt::QueuedConnection);
+
+    // call exit() when the UI window is closed
+    connect(&m_view, &VsQuickView::windowClose, this, &VirtualStudio::exit,
+            Qt::QueuedConnection);
+
+    if ((m_uiMode == QJackTrip::UNSET && vsFtux())
+        || (m_uiMode == QJackTrip::VIRTUAL_STUDIO)) {
+        login();
+    }
+}
+
+void VirtualStudio::setStandardWindow(QSharedPointer<QJackTrip> window)
+{
+    m_standardWindow = window;
+}
+
+void VirtualStudio::setCLISettings(QSharedPointer<Settings> settings)
+{
+    m_cliSettings = settings;
+}
+
+void VirtualStudio::show()
+{
+    if (m_checkSsl) {
+        // Check our available SSL version
+        QString sslVersion = QSslSocket::sslLibraryVersionString();
+        // Important: this needs to be output with qDebug rather than to std::cout
+        // otherwise it may get passed to an existing JackTrip instance in place of our
+        // deeplink. (Need to find the root cause of this.)
+        qDebug() << "SSL Library: " << sslVersion;
+        if (sslVersion.isEmpty()) {
+            QMessageBox msgBox;
+            msgBox.setText(
+                QStringLiteral("OpenSSL was not found. You will not be able to connect "
+                               "to the Virtual Studio server."));
+            msgBox.setWindowTitle(QStringLiteral("SSL Error"));
+            msgBox.exec();
+        }
+        m_checkSsl = false;
+    }
+
+    while (m_view.status() == QQuickView::Loading) {
+        // I don't think there is any need to load network data, but just in case
+        // See https://doc.qt.io/qt-6/qquickview.html#Status-enum
+        qDebug() << "JackTrip is still loading the QML view";
+        QThread::sleep(1);
+    }
+
+    if (m_view.status() != QQuickView::Ready) {
+        QMessageBox msgBox;
+        msgBox.setText(
+            "JackTrip detected that some modules required for the "
+            "Virtual Studio mode are missing on your system. "
+            "Click \"OK\" to proceed to classic mode.\n\n"
+            "Details: JackTrip failed to load the QML view. "
+            "This is likely caused by missing QML plugins. "
+            "Please consult support.jacktrip.com for possible solutions.");
+        msgBox.setWindowTitle(QStringLiteral("JackTrip Is Missing QML Modules"));
+        connect(&msgBox, &QMessageBox::finished, this, &VirtualStudio::toStandard,
+                Qt::QueuedConnection);
+        msgBox.exec();
+        return;
+    }
+
+    raiseToTop();
+}
+
+void VirtualStudio::raiseToTop()
+{
+    if (m_view.status() != QQuickView::Ready)
+        return;
+    m_view.show();             // Restore from systray
+    m_view.raise();            // raise to top
+    m_view.requestActivate();  // focus on window
+}
+
+int VirtualStudio::webChannelPort()
+{
+    return m_webChannelPort;
+}
+
+bool VirtualStudio::showFirstRun()
+{
+    return m_showFirstRun;
+}
+
+void VirtualStudio::setShowFirstRun(bool show)
+{
+    if (m_showFirstRun == show)
+        return;
+    m_showFirstRun = show;
+    emit showFirstRunChanged();
+}
+
+bool VirtualStudio::hasRefreshToken()
+{
+    return !m_refreshToken.isEmpty();
+}
+
+QString VirtualStudio::versionString()
+{
+    return QLatin1String(gVersion);
+}
+
+QString VirtualStudio::logoSection()
+{
+    return m_logoSection;
+}
+
+QString VirtualStudio::connectedErrorMsg()
+{
+    return m_connectedErrorMsg;
+}
+
+void VirtualStudio::setConnectedErrorMsg(const QString& msg)
+{
+    if (m_connectedErrorMsg == msg)
+        return;
+    m_connectedErrorMsg = msg;
+    emit connectedErrorMsgChanged();
+}
+
+bool VirtualStudio::networkOutage()
+{
+    return m_devicePtr.isNull() ? false : m_devicePtr->getNetworkOutage();
+}
+
+QJsonObject VirtualStudio::regions()
+{
+    return m_regions;
+}
+
+QJsonObject VirtualStudio::userMetadata()
+{
+    return m_userMetadata;
+}
+
+QString VirtualStudio::connectionState()
+{
+    return m_connectionState;
+}
+
+QJsonObject VirtualStudio::networkStats()
+{
+    return m_networkStats;
+}
+
+QString VirtualStudio::updateChannel()
+{
+    return m_updateChannel;
+}
+
+void VirtualStudio::setUpdateChannel(const QString& channel)
+{
+    if (m_updateChannel == channel)
+        return;
+    m_updateChannel = channel;
+    emit updateChannelChanged();
+}
+
+bool VirtualStudio::showInactive()
+{
+    return m_showInactive;
+}
+
+void VirtualStudio::setShowInactive(bool inactive)
+{
+    if (m_showInactive == inactive)
+        return;
+    m_showInactive = inactive;
+    emit showInactiveChanged();
+
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("ShowInactive"), m_showInactive);
+    settings.endGroup();
+}
+
+bool VirtualStudio::showSelfHosted()
+{
+    return m_showSelfHosted;
+}
+
+void VirtualStudio::setShowSelfHosted(bool selfHosted)
+{
+    if (m_showSelfHosted == selfHosted)
+        return;
+    m_showSelfHosted = selfHosted;
+    emit showSelfHostedChanged();
+
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("ShowSelfHosted"), m_showSelfHosted);
+    settings.endGroup();
+}
+
+bool VirtualStudio::showCreateStudio()
+{
+    return m_showCreateStudio;
+}
+
+void VirtualStudio::setShowCreateStudio(bool createStudio)
+{
+    if (m_showCreateStudio == createStudio)
+        return;
+    m_showCreateStudio = createStudio;
+    emit showCreateStudioChanged();
+}
+
+bool VirtualStudio::showDeviceSetup()
+{
+    return m_showDeviceSetup;
+}
+
+void VirtualStudio::setShowDeviceSetup(bool show)
+{
+    if (m_showDeviceSetup == show)
+        return;
+    m_showDeviceSetup = show;
+    emit showDeviceSetupChanged();
+}
+
+QString VirtualStudio::windowState()
+{
+    return m_windowState;
+}
+
+void VirtualStudio::setWindowState(QString state)
+{
+    if (m_windowState == state)
+        return;
+    m_windowState = state;
+    emit windowStateUpdated();
+}
+
+QString VirtualStudio::apiHost()
+{
+    return m_apiHost;
+}
+
+void VirtualStudio::setApiHost(QString host)
+{
+    if (m_apiHost == host)
+        return;
+    m_apiHost = host;
+    emit apiHostChanged();
+}
+
+bool VirtualStudio::vsFtux()
+{
+    return m_vsFtux;
+}
+
+bool VirtualStudio::isExiting()
+{
+    return m_isExiting;
+}
+
+void VirtualStudio::collectFeedbackSurvey(QString serverId, int rating, QString message)
+{
+    QJsonObject feedback;
+
+    QString sysInfo = QString("[platform=%1").arg(QSysInfo::prettyProductName());
+#ifdef RT_AUDIO
+    QString inputDevice =
+        QString::fromStdString(m_audioConfigPtr->getInputDevice().toStdString());
+    if (!inputDevice.isEmpty()) {
+        sysInfo.append(QString(",input=%1").arg(inputDevice));
+    }
+    QString outputDevice =
+        QString::fromStdString(m_audioConfigPtr->getOutputDevice().toStdString());
+    if (!outputDevice.isEmpty()) {
+        sysInfo.append(QString(",output=%1").arg(outputDevice));
+    }
+#endif
+    sysInfo.append("]");
+
+    feedback.insert(QStringLiteral("rating"), rating);
+    if (message.isEmpty()) {
+        feedback.insert(QStringLiteral("message"), sysInfo);
+    } else {
+        feedback.insert(QStringLiteral("message"), message + " " + sysInfo);
+    }
+
+    QJsonDocument data = QJsonDocument(feedback);
+    m_api->submitServerFeedback(serverId, data.toJson());
+    return;
+}
+
+bool VirtualStudio::showWarnings()
+{
+    return m_showWarnings;
+}
+
+void VirtualStudio::setShowWarnings(bool show)
+{
+    if (m_showWarnings == show)
+        return;
+    m_showWarnings = show;
+    emit showWarningsChanged();
+}
+
+float VirtualStudio::fontScale()
+{
+    return m_fontScale;
+}
+
+float VirtualStudio::uiScale()
+{
+    return m_uiScale;
+}
+
+void VirtualStudio::setUiScale(float scale)
+{
+    if (scale == m_uiScale)
+        return;
+    m_uiScale = scale;
+    emit uiScaleChanged();
+}
+
+bool VirtualStudio::darkMode()
+{
+    return m_darkMode;
+}
+
+void VirtualStudio::setDarkMode(bool dark)
+{
+    if (dark == m_darkMode)
+        return;
+    m_darkMode = dark;
+    emit darkModeChanged();
+}
+
+bool VirtualStudio::collapseDeviceControls()
+{
+    return m_collapseDeviceControls;
+}
+
+void VirtualStudio::setCollapseDeviceControls(bool collapseDeviceControls)
+{
+    if (m_collapseDeviceControls == collapseDeviceControls)
+        return;
+    m_collapseDeviceControls = collapseDeviceControls;
+    emit collapseDeviceControlsChanged(collapseDeviceControls);
+}
+
+bool VirtualStudio::testMode()
+{
+    return m_testMode;
+}
+
+void VirtualStudio::setTestMode(bool test)
+{
+    if (m_testMode == test)
+        return;
+
+    QString userEmail = m_userMetadata[QStringLiteral("email")].toString();
+    if (m_userMetadata.isEmpty() || userEmail == ""
+        || !userEmail.endsWith("@jacktrip.org")) {
+        qDebug() << "Not allowed";
+        return;
+    }
+
+    // deregister app
+    if (!m_devicePtr.isNull()) {
+        m_devicePtr->removeApp();
+        m_devicePtr->disconnect();
+        m_devicePtr.reset();
+    }
+
+    m_testMode = test;
+
+    // clear existing auth state
+    m_auth->logout();
+
+    // Clear existing registers - any existing instance data will be overwritten
+    // when m_auth->authenticate finishes and slotAuthSucceeded() is called again
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("TestMode"), m_testMode);
+    settings.remove(QStringLiteral("RefreshToken"));
+    settings.remove(QStringLiteral("UserId"));
+    settings.endGroup();
+
+    // stop timers, clear data, etc.
+    resetState();
+
+    // clear user data
+    m_userMetadata = QJsonObject();
+    m_userId.clear();
+
+    // re-run authentication. This should not require another browser flow since
+    // we're starting with the existing refresh token
+    m_auth->authenticate(m_refreshToken);
+    emit testModeChanged();
+}
+
+QString VirtualStudio::studioToJoin()
+{
+    return m_studioToJoin;
+}
+
+void VirtualStudio::setStudioToJoin(const QString& id)
+{
+    if (m_studioToJoin == id)
+        return;
+    m_studioToJoin = id;
+    emit studioToJoinChanged();
+}
+
+bool VirtualStudio::noUpdater()
+{
+#ifdef NO_UPDATER
+    return true;
+#else
+    return false;
+#endif
+}
+
+bool VirtualStudio::psiBuild()
+{
+#ifdef PSI
+    return true;
+#else
+    return false;
+#endif
+}
+
+QString VirtualStudio::failedMessage()
+{
+    return m_failedMessage;
+}
+
+void VirtualStudio::joinStudio()
+{
+    // nothing to do unless on setup or connected windows with studio to join
+    if ((m_windowState != "setup" && m_windowState != "connected")
+        || !m_auth->isAuthenticated() || m_studioToJoin.isEmpty())
+        return;
+
+    // make sure we've retrieved a list of servers
+    QMutexLocker locker(&m_refreshMutex);
+    if (m_servers.isEmpty()) {
+        // No servers yet. Making sure we have them.
+        // getServerList emits refreshFinished which
+        // will come back to this function.
+        locker.unlock();
+        getServerList(true);
+        return;
+    }
+
+    // pop studioToJoin
+    const QString targetId = m_studioToJoin;
+    setStudioToJoin("");
+
+    // stop audio if already running (settings or setup windows)
+    m_audioConfigPtr->stopAudio(true);
+
+    // find and populate data for current studio
+    VsServerInfoPointer sPtr;
+    for (const VsServerInfoPointer& s : m_servers) {
+        if (s->id() == targetId) {
+            sPtr = s;
+            break;
+        }
+    }
+    locker.unlock();
+
+    if (sPtr.isNull()) {
+        m_failedMessage = "Unable to find studio " + targetId;
+        emit failedMessageChanged();
+        emit failed();
+        return;
+    }
+
+    m_currentStudio = *sPtr;
+    emit currentStudioChanged();
+
+    if (m_windowState == "setup") {
+        m_audioConfigPtr->setSampleRate(m_currentStudio.sampleRate());
+        m_audioConfigPtr->startAudio();
+        return;
+    }
+
+    // m_windowState == "connected"
+    connectToStudio();
+}
+
+void VirtualStudio::toStandard()
+{
+    if (m_standardWindow.isNull())
+        qDebug() << "Unable to switch modes: standard window is missing!";
+
+    m_view.hide();
+    m_standardWindow->show();
+
+    QSettings settings;
+    m_uiMode = QJackTrip::STANDARD;
+    settings.setValue(QStringLiteral("UiMode"), m_uiMode);
+
+    // stop timers, clear data, etc.
+    resetState();
+    setWindowState(QStringLiteral("start"));
+    m_auth->logout();
+
+    if (m_showFirstRun) {
+        m_showFirstRun = false;
+        emit showFirstRunChanged();
+    }
+}
+
+void VirtualStudio::toVirtualStudio()
+{
+    QSettings settings;
+    m_uiMode = QJackTrip::VIRTUAL_STUDIO;
+    settings.setValue(QStringLiteral("UiMode"), m_uiMode);
+
+    if (m_windowState == "start") {
+        setWindowState(QStringLiteral("login"));
+    }
+    if (m_windowState == "login") {
+        login();
+    }
+}
+
+void VirtualStudio::login()
+{
+    if (m_refreshToken.isEmpty()) {
+        m_auth->authenticate(QStringLiteral(""));
+    } else {
+        m_auth->authenticate(m_refreshToken);
+    }
+}
+
+void VirtualStudio::logout()
+{
+    // deregister app
+    if (!m_devicePtr.isNull()) {
+        m_devicePtr->removeApp();
+        m_devicePtr->disconnect();
+        m_devicePtr.reset();
+    }
+
+    QUrl logoutURL = QUrl("https://auth.jacktrip.org/v2/logout");
+    QUrlQuery query;
+    query.addQueryItem(QStringLiteral("client_id"), AUTH_CLIENT_ID);
+    if (m_testMode) {
+        query.addQueryItem(QStringLiteral("returnTo"),
+                           QStringLiteral("https://next-test.jacktrip.com/"));
+    } else {
+        query.addQueryItem(QStringLiteral("returnTo"),
+                           QStringLiteral("https://www.jacktrip.com/"));
+    }
+
+    logoutURL.setQuery(query);
+    QDesktopServices::openUrl(logoutURL);
+
+    m_auth->logout();
+
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.remove(QStringLiteral("RefreshToken"));
+    settings.remove(QStringLiteral("UserId"));
+    settings.endGroup();
+
+    // stop timers, clear data, etc.
+    resetState();
+
+    // clear user data
+    m_refreshToken.clear();
+    m_userMetadata = QJsonObject();
+    m_userId.clear();
+    emit hasRefreshTokenChanged();
+
+    // reset window state
+    setWindowState(QStringLiteral("login"));
+    login();  // called to retrieve new code flow token
+}
+
+void VirtualStudio::refreshStudios(int index, bool signalRefresh)
+{
+    getSubscriptions();
+    getServerList(signalRefresh, index);
+}
+
+void VirtualStudio::loadSettings()
+{
+    QSettings settings;
+    m_uiMode = static_cast<QJackTrip::uiModeT>(
+        settings.value(QStringLiteral("UiMode"), QJackTrip::UNSET).toInt());
+    setUpdateChannel(
+        settings.value(QStringLiteral("UpdateChannel"), "stable").toString().toLower());
+
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    m_refreshToken   = settings.value(QStringLiteral("RefreshToken"), "").toString();
+    m_userId         = settings.value(QStringLiteral("UserId"), "").toString();
+    m_testMode       = settings.value(QStringLiteral("TestMode"), false).toBool();
+    m_showInactive   = settings.value(QStringLiteral("ShowInactive"), true).toBool();
+    m_showSelfHosted = settings.value(QStringLiteral("ShowSelfHosted"), false).toBool();
+
+    // use setters to emit signals for these if they change; otherwise, the
+    // user interface will not revert back after cancelling settings changes
+    setUiScale(settings.value(QStringLiteral("UiScale"), 1).toFloat());
+    setDarkMode(settings.value(QStringLiteral("DarkMode"), false).toBool());
+    setShowDeviceSetup(settings.value(QStringLiteral("ShowDeviceSetup"), true).toBool());
+    setShowWarnings(settings.value(QStringLiteral("ShowWarnings"), true).toBool());
+    settings.endGroup();
+
+    m_audioConfigPtr->loadSettings();
+}
+
+void VirtualStudio::saveSettings()
+{
+    QSettings settings;
+    settings.setValue(QStringLiteral("UpdateChannel"), m_updateChannel);
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("UiScale"), m_uiScale);
+    settings.setValue(QStringLiteral("DarkMode"), m_darkMode);
+    settings.setValue(QStringLiteral("ShowDeviceSetup"), m_showDeviceSetup);
+    settings.setValue(QStringLiteral("ShowWarnings"), m_showWarnings);
+    settings.endGroup();
+
+    m_audioConfigPtr->saveSettings();
+}
+
+void VirtualStudio::connectToStudio()
+{
+    m_refreshTimer.stop();
+
+    m_networkStats = QJsonObject();
+    emit networkStatsChanged();
+
+    m_onConnectedScreen = true;
+
+    m_studioSocketPtr.reset(new VsWebSocket(
+        QUrl(QStringLiteral("wss://%1/api/servers/%2?auth_code=%3")
+                 .arg(m_api->getApiHost(), m_currentStudio.id(), m_auth->accessToken())),
+        m_auth->accessToken(), QString(), QString()));
+    connect(m_studioSocketPtr.get(), &VsWebSocket::textMessageReceived, this,
+            &VirtualStudio::handleWebsocketMessage);
+    connect(m_studioSocketPtr.get(), &VsWebSocket::disconnected, this,
+            &VirtualStudio::restartStudioSocket);
+    m_studioSocketPtr->openSocket();
+
+    // Check if we have an address for our server
+    if (m_currentStudio.status() != "Ready") {
+        m_connectionState = QStringLiteral("Waiting...");
+        emit connectionStateChanged();
+    } else {
+        completeConnection();
+    }
+
+    m_reconnectState = ReconnectState::NOT_RECONNECTING;
+}
+
+void VirtualStudio::completeConnection()
+{
+    // sanity check
+    if (m_currentStudio.id() == ""
+        || m_currentStudio.status() == QStringLiteral("Disabled")) {
+        processError("Studio session has ended");
+        return;
+    }
+
+    // these shouldn't happen
+    if (m_currentStudio.status() != "Ready") {
+        processError("Studio session is not ready");
+        return;
+    }
+    if (m_currentStudio.host().isEmpty()) {
+        processError("Studio host is unknown");
+        return;
+    }
+
+    // always connect with audio device controls open
+    setCollapseDeviceControls(false);
+
+    m_jackTripRunning = true;
+    m_connectionState = QStringLiteral("Preparing audio...");
+    emit connectionStateChanged();
+    try {
+        bool useRtAudio       = m_audioConfigPtr->getUseRtAudio();
+        std::string input     = "";
+        std::string output    = "";
+        int buffer_size       = 0;
+        int inputMixMode      = -1;
+        int baseInputChannel  = 0;
+        int numInputChannels  = 2;
+        int baseOutputChannel = 0;
+        int numOutputChannels = 2;
+#ifdef RT_AUDIO
+        if (useRtAudio) {
+            // pre-populate device cache and validate first, if using rtaudio
+            if (!m_audioConfigPtr->getDeviceModelsInitialized())
+                m_audioConfigPtr->refreshDevices(true);
+            // initialize jacktrip using audio settings
+            input             = m_audioConfigPtr->getInputDevice().toStdString();
+            output            = m_audioConfigPtr->getOutputDevice().toStdString();
+            buffer_size       = m_audioConfigPtr->getBufferSize();
+            inputMixMode      = m_audioConfigPtr->getInputMixMode();
+            baseInputChannel  = m_audioConfigPtr->getBaseInputChannel();
+            numInputChannels  = m_audioConfigPtr->getNumInputChannels();
+            baseOutputChannel = m_audioConfigPtr->getBaseOutputChannel();
+            numOutputChannels = m_audioConfigPtr->getNumOutputChannels();
+        }
+#endif
+
+        // increment buffer_strategy by 1 for array-index mapping
+        int buffer_strategy = m_audioConfigPtr->getBufferStrategy() + 1;
+        // adjust buffer_strategy for PLC "auto" mode menu item
+        if (buffer_strategy == 4 || buffer_strategy == 5) {
+            buffer_strategy = 3;
+        }
+
+        // create a new JackTrip instance
+        JackTrip* jackTrip = m_devicePtr->initJackTrip(
+            useRtAudio, input, output, baseInputChannel, numInputChannels,
+            baseOutputChannel, numOutputChannels, inputMixMode, buffer_size,
+            buffer_strategy, &m_currentStudio);
+        if (jackTrip == 0) {
+            processError("Could not bind port");
+            return;
+        }
+        jackTrip->setIOStatTimeout(m_cliSettings->getIOStatTimeout());
+        m_audioConfigPtr->setSampleRate(jackTrip->getSampleRate());
+
+        // this passes ownership to JackTrip
+        jackTrip->setAudioInterface(m_audioConfigPtr->newAudioInterface(jackTrip));
+
+        QObject::connect(jackTrip, &JackTrip::signalProcessesStopped, this,
+                         &VirtualStudio::connectionFinished, Qt::QueuedConnection);
+        QObject::connect(jackTrip, &JackTrip::signalError, this,
+                         &VirtualStudio::processError, Qt::QueuedConnection);
+        QObject::connect(jackTrip, &JackTrip::signalReceivedConnectionFromPeer, this,
+                         &VirtualStudio::receivedConnectionFromPeer,
+                         Qt::QueuedConnection);
+        QObject::connect(jackTrip, &JackTrip::signalUdpWaitingTooLong, this,
+                         &VirtualStudio::udpWaitingTooLong, Qt::QueuedConnection);
+
+        m_connectionState = QStringLiteral("Connecting...");
+        emit connectionStateChanged();
+        m_devicePtr->startJackTrip(m_currentStudio);
+
+        // update device error messages and warnings based on latest results
+        // this is necessary because we may have never loaded audio settings,
+        // or the state may have changed via the connected change devices screen
+        m_audioConfigPtr->setDevicesWarningMsg(jackTrip->getDevicesWarningMsg());
+        m_audioConfigPtr->setDevicesErrorMsg(jackTrip->getDevicesErrorMsg());
+        m_audioConfigPtr->setDevicesWarningHelpUrl(jackTrip->getDevicesWarningHelpUrl());
+        m_audioConfigPtr->setDevicesErrorHelpUrl(jackTrip->getDevicesErrorHelpUrl());
+        m_audioConfigPtr->setHighLatencyFlag(jackTrip->getHighLatencyFlag());
+
+    } catch (const std::exception& e) {
+        // Let the user know what our exception was.
+        m_connectionState = QStringLiteral("JackTrip Error");
+        emit connectionStateChanged();
+
+        processError(QString::fromUtf8(e.what()));
+        return;
+    }
+
+#ifdef __APPLE__
+    m_noNap.disableNap();
+#endif
+}
+
+void VirtualStudio::triggerReconnect(bool refresh)
+{
+    if (!m_jackTripRunning || m_devicePtr.isNull()) {
+        if (refresh)
+            m_audioConfigPtr->refreshDevices(true);
+        else
+            m_audioConfigPtr->validateDevices();
+        return;
+    }
+
+    // this needs to be synchronous to avoid both trying
+    // to use the audio interfaces at the same time
+    // note that connectionFinished() checks m_reconnectState
+    // and uses that to update audio, then reconnect
+    m_reconnectState  = refresh ? ReconnectState::RECONNECTING_REFRESH
+                                : ReconnectState::RECONNECTING_VALIDATE;
+    m_connectionState = QStringLiteral("Reconnecting...");
+    emit connectionStateChanged();
+
+    // keep device enabled while stopping jacktrip
+    m_devicePtr->stopJackTrip(true);
+}
+
+void VirtualStudio::disconnect()
+{
+    // stop jackrip if it's running
+    if (m_jackTripRunning) {
+        m_devicePtr->stopJackTrip(false);
+        // persist any volume level or device changes
+        m_audioConfigPtr->saveSettings();
+    }
+
+    m_connectionState = QStringLiteral("Disconnected");
+    emit connectionStateChanged();
+    setConnectedErrorMsg("");
+
+    if (m_isExiting) {
+        emit signalExit();
+        return;
+    }
+
+    // if this occurs on the setup or settings screen (for example, due to an issue with
+    // devices) then don't emit disconnected, as that would move you back to the "Browse"
+    // screen
+    if (m_onConnectedScreen) {
+        m_onConnectedScreen = false;
+        emit disconnected();
+        // Refresh studios and restart timer
+        refreshStudios(0, true);
+        m_refreshTimer.start();
+    }
+
+    if (!m_jackTripRunning) {
+        return;
+    }
+    m_jackTripRunning = false;
+
+    if (!m_studioSocketPtr.isNull()) {
+        m_studioSocketPtr->closeSocket();
+        m_studioSocketPtr->disconnect();
+        m_studioSocketPtr.reset();
+    }
+
+    // reset network statistics
+    m_networkStats = QJsonObject();
+
+    if (!m_currentStudio.id().isEmpty()) {
+        emit openFeedbackSurveyModal(m_currentStudio.id());
+        m_currentStudio.setId("");
+        emit currentStudioChanged();
+    }
+
+#ifdef __APPLE__
+    m_noNap.enableNap();
+#endif
+}
+
+void VirtualStudio::createStudio()
+{
+    setWindowState(QStringLiteral("create_studio"));
+}
+
+void VirtualStudio::editProfile()
+{
+    QUrl url = QUrl(QStringLiteral("https://%1/profile").arg(m_api->getApiHost()));
+    QDesktopServices::openUrl(url);
+}
+
+void VirtualStudio::showAbout()
+{
+    About about;
+    about.exec();
+}
+
+void VirtualStudio::openLink(const QString& link)
+{
+    QUrl url = QUrl(link);
+    QDesktopServices::openUrl(url);
+}
+
+void VirtualStudio::handleDeeplinkRequest(const QUrl& link)
+{
+    // check link is valid
+    QString studioId;
+    if (link.scheme() != QLatin1String("jacktrip") || link.path().length() <= 1) {
+        qDebug() << "Ignoring invalid deeplink to" << link;
+        return;
+    }
+    if (link.host() == QLatin1String("join")) {
+        studioId = link.path().remove(0, 1);
+    } else if (link.host().isEmpty() && link.path().startsWith("join/")) {
+        studioId = link.path().remove(0, 5);
+    } else {
+        qDebug() << "Ignoring invalid deeplink to" << link;
+        return;
+    }
+
+    // check if already connected (ignore)
+    if (m_windowState == "connected" || m_windowState == "change_devices") {
+        qDebug() << "Already connected; ignoring deeplink to" << link;
+        return;
+    }
+
+    if (m_windowState == "setup"
+        && (m_studioToJoin == studioId || m_currentStudio.id() == studioId)) {
+        qDebug() << "Already preparing to connect; ignoring deeplink to" << link;
+        return;
+    }
+
+    qDebug() << "Handling deeplink to" << link;
+    setStudioToJoin(studioId);
+    raiseToTop();
+
+    // Switch to virtual studio mode, if necessary
+    // Note that this doesn't change the startup preference
+    if (m_uiMode != QJackTrip::VIRTUAL_STUDIO) {
+        m_standardWindow->hide();
+        if (m_windowState == "start") {
+            setWindowState(QStringLiteral("login"));
+        }
+        if (m_windowState == "login") {
+            login();
+        }
+    }
+
+    // automatically navigate if on certain window screens
+    // note that the studio creation happens inside of the web view,
+    // and the app doesn't really know anything about it. we depend
+    // on the web app triggering a deep link join event, which is
+    // handled here. it's unlikely that the new studio has been
+    // noticed yet, so we don't join right away; otherwise we'd just
+    // get an unknown studio error. instead, we trigger a refresh and
+    // rely on it to kick off the join afterwards.
+    if (m_windowState == "browse" || m_windowState == "create_studio"
+        || m_windowState == "settings" || m_windowState == "setup"
+        || m_windowState == "failed") {
+        if (showDeviceSetup()) {
+            setWindowState("setup");
+        } else {
+            setWindowState("connected");
+        }
+        refreshStudios(0, true);
+    }
+
+    // otherwise, assume we are on setup screens and can let the normal flow handle it
+}
+
+void VirtualStudio::exit()
+{
+    // if multiple close events are received, emit the signalExit to force-quit the app
+    if (m_isExiting) {
+        emit signalExit();
+    }
+
+    // triggering isExitingChanged will force any WebEngine things to close properly
+    m_isExiting = true;
+    emit isExitingChanged();
+
+    // stop timers, clear data, etc.
+    resetState();
+
+    if (m_onConnectedScreen) {
+        // manually disconnect on self-managed studios
+        if (!m_currentStudio.id().isEmpty() && !m_currentStudio.isManaged()) {
+            disconnect();
+        }
+    } else {
+        emit signalExit();
+    }
+}
+
+void VirtualStudio::slotAuthSucceeded()
+{
+    // Make sure window is on top (instead of browser, during first auth)
+    raiseToTop();
+
+    // Determine which API host to use
+    m_apiHost = PROD_API_HOST;
+    if (m_testMode) {
+        m_apiHost = TEST_API_HOST;
+    }
+    m_api->setApiHost(m_apiHost);
+
+    // Get refresh token and userId
+    m_refreshToken = m_auth->refreshToken();
+    m_userId       = m_auth->userId();
+    emit hasRefreshTokenChanged();
+
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("RefreshToken"), m_refreshToken);
+    settings.setValue(QStringLiteral("UserId"), m_userId);
+    settings.endGroup();
+
+    // initialize new VsDevice and wire up signals/slots before registering app
+    m_devicePtr.reset(new VsDevice(m_auth, m_api, m_audioConfigPtr));
+    connect(m_devicePtr.get(), &VsDevice::updateNetworkStats, this,
+            &VirtualStudio::updatedStats);
+    connect(m_audioConfigPtr.get(), &VsAudio::updatedInputVolume, m_devicePtr.get(),
+            &VsDevice::syncDeviceSettings);
+    connect(m_audioConfigPtr.get(), &VsAudio::updatedInputMuted, m_devicePtr.get(),
+            &VsDevice::syncDeviceSettings);
+    connect(m_audioConfigPtr.get(), &VsAudio::updatedOutputVolume, m_devicePtr.get(),
+            &VsDevice::syncDeviceSettings);
+    connect(m_audioConfigPtr.get(), &VsAudio::updatedMonitorVolume, m_devicePtr.get(),
+            &VsDevice::syncDeviceSettings);
+
+    m_devicePtr->registerApp();
+
+    if (!m_webChannelServer->listen(QHostAddress::LocalHost)) {
+        // shouldn't happen
+        std::cout << "ERROR: Failed to start server!" << std::endl;
+    }
+    m_webChannelPort = m_webChannelServer->serverPort();
+    emit webChannelPortChanged(m_webChannelPort);
+    std::cout << "QWebChannel listening on port: " << m_webChannelPort << std::endl;
+
+    getRegions();
+    getUserMetadata();
+    getSubscriptions();
+    getServerList(false);
+}
+
+void VirtualStudio::connectionFinished()
+{
+    if (!m_devicePtr.isNull()
+        && (m_reconnectState == ReconnectState::RECONNECTING_VALIDATE
+            || m_reconnectState == ReconnectState::RECONNECTING_REFRESH)) {
+        if (m_devicePtr->hasTerminated()) {
+            if (m_reconnectState == ReconnectState::RECONNECTING_REFRESH) {
+                m_audioConfigPtr->refreshDevices(true);
+            } else {
+                m_audioConfigPtr->validateDevices(true);
+            }
+            connectToStudio();
+        }
+        return;
+    }
+    m_reconnectState = ReconnectState::NOT_RECONNECTING;
+
+    // use disconnect function to handle reset of all internal flags and timers
+    disconnect();
+}
+
+void VirtualStudio::processError(const QString& errorMessage)
+{
+    static const QString RtAudioErrorMsg = QStringLiteral("RtAudio Error");
+    static const QString JackAudioErrorMsg =
+        QStringLiteral("The Jack server was shut down");
+
+    const bool shouldSwitchToRtAudio =
+        (errorMessage == QLatin1String("Maybe the JACK server is not running?"));
+
+    QMessageBox msgBox;
+    if (shouldSwitchToRtAudio) {
+        // Report the other end quitting as a regular occurance rather than an error.
+        msgBox.setText("The JACK server is not running. Switching back to RtAudio.");
+        msgBox.setWindowTitle(QStringLiteral("No JACK server"));
+    } else if (errorMessage == QLatin1String("Peer Stopped")) {
+        // Report the other end quitting as a regular occurance rather than an error.
+        msgBox.setText("The Studio has been stopped.");
+        msgBox.setWindowTitle(QStringLiteral("Disconnected"));
+    } else if (errorMessage.startsWith(RtAudioErrorMsg)) {
+        if (errorMessage.length() > RtAudioErrorMsg.length() + 2) {
+            const QString details(errorMessage.sliced(RtAudioErrorMsg.length() + 2));
+            if (details.contains(QStringLiteral("device was disconnected"))
+                || details.contains(
+                    QStringLiteral("Unable to retrieve capture buffer"))) {
+                msgBox.setText(QStringLiteral("Your audio interface was disconnected."));
+            } else {
+                msgBox.setText(details);
+            }
+        } else {
+            msgBox.setText(errorMessage);
+        }
+        msgBox.setWindowTitle(QStringLiteral("Audio Interface Error"));
+    } else if (errorMessage.startsWith(JackAudioErrorMsg)) {
+        if (errorMessage.length() > JackAudioErrorMsg.length() + 2) {
+            msgBox.setText(errorMessage.sliced(JackAudioErrorMsg.length() + 2));
+        } else {
+            msgBox.setText(QStringLiteral("The JACK Audio Server was stopped."));
+        }
+        msgBox.setWindowTitle(QStringLiteral("Jack Audio Error"));
+    } else {
+        msgBox.setText(QStringLiteral("Error: ").append(errorMessage));
+        msgBox.setWindowTitle(QStringLiteral("Doh!"));
+    }
+    msgBox.exec();
+
+    if (m_jackTripRunning)
+        connectionFinished();
+}
+
+void VirtualStudio::receivedConnectionFromPeer()
+{
+    // Connect via API
+    m_connectionState = QStringLiteral("Connected");
+    emit connectionStateChanged();
+    std::cout << "Received connection" << std::endl;
+    emit connected();
+}
+
+void VirtualStudio::handleWebsocketMessage(const QString& msg)
+{
+    QJsonObject serverState = QJsonDocument::fromJson(msg.toUtf8()).object();
+    QString serverStatus    = serverState[QStringLiteral("status")].toString();
+    bool serverEnabled      = serverState[QStringLiteral("enabled")].toBool();
+    QString serverCloudId   = serverState[QStringLiteral("cloudId")].toString();
+
+    // server notifications are also transmitted along this websocket, so ignore data if
+    // it contains "message"
+    QString message = serverState[QStringLiteral("message")].toString();
+    if (!message.isEmpty()) {
+        return;
+    }
+    if (m_currentStudio.id() == "") {
+        return;
+    }
+    m_currentStudio.setStatus(serverStatus);
+    m_currentStudio.setEnabled(serverEnabled);
+    m_currentStudio.setCloudId(serverCloudId);
+    if (!m_jackTripRunning) {
+        if (serverStatus == QLatin1String("Ready") && m_onConnectedScreen) {
+            m_currentStudio.setHost(serverState[QStringLiteral("serverHost")].toString());
+            m_currentStudio.setPort(serverState[QStringLiteral("serverPort")].toInt());
+            m_currentStudio.setSessionId(
+                serverState[QStringLiteral("sessionId")].toString());
+            completeConnection();
+        }
+    }
+
+    emit currentStudioChanged();
+}
+
+void VirtualStudio::restartStudioSocket()
+{
+    if (m_onConnectedScreen) {
+        if (!m_studioSocketPtr.isNull()) {
+            m_studioSocketPtr->openSocket();
+        }
+    }
+}
+
+void VirtualStudio::updatedStats(const QJsonObject& stats)
+{
+    QJsonObject newStats;
+    for (int i = 0; i < stats.keys().size(); i++) {
+        QString key = stats.keys().at(i);
+        newStats.insert(key, stats[key].toDouble());
+    }
+
+    m_networkStats = newStats;
+    emit networkStatsChanged();
+    return;
+}
+
+void VirtualStudio::udpWaitingTooLong()
+{
+    if (m_devicePtr.isNull())
+        return;
+    m_networkOutageTimer.start();
+    m_devicePtr->setNetworkOutage(true);
+    emit updatedNetworkOutage(true);
+}
+
+void VirtualStudio::sendHeartbeat()
+{
+    if (!m_devicePtr.isNull() && m_connectionState != "Connecting..."
+        && m_connectionState != "Preparing audio...") {
+        m_devicePtr->sendHeartbeat();
+    }
+}
+
+void VirtualStudio::resetState()
+{
+    m_webChannelServer->close();
+    m_refreshTimer.stop();
+    m_heartbeatTimer.stop();
+    m_startTimer.stop();
+    m_networkOutageTimer.stop();
+    m_firstRefresh = true;
+}
+
+void VirtualStudio::getServerList(bool signalRefresh, int index)
+{
+    // only allow one thread to refresh at a time
+    QMutexLocker refreshLock(&m_refreshMutex);
+    if (m_refreshInProgress)
+        return;
+    m_refreshInProgress = true;
+    refreshLock.unlock();
+
+    // Get the serverId of the server at the top of our screen if we know it
+    QString topServerId;
+    if (index >= 0 && index < m_serverModel.count()) {
+        topServerId = m_serverModel.at(index)->id();
+    }
+
+    QNetworkReply* reply = m_api->getServers();
+    connect(
+        reply, &QNetworkReply::finished, this, [&, reply, topServerId, signalRefresh]() {
+            if (reply->error() != QNetworkReply::NoError) {
+                if (signalRefresh) {
+                    emit refreshFinished(index);
+                }
+                std::cerr << "Error: " << reply->errorString().toStdString() << std::endl;
+                reply->deleteLater();
+                QMutexLocker refreshLock(&m_refreshMutex);
+                m_refreshInProgress = false;
+                return;
+            }
+
+            QByteArray response      = reply->readAll();
+            QJsonDocument serverList = QJsonDocument::fromJson(response);
+            reply->deleteLater();
+            if (!serverList.isArray()) {
+                if (signalRefresh) {
+                    emit refreshFinished(index);
+                }
+                std::cerr << "Error: Not an array" << std::endl;
+                QMutexLocker refreshLock(&m_refreshMutex);
+                m_refreshInProgress = false;
+                return;
+            }
+
+            QJsonArray servers = serverList.array();
+            // Divide our servers by category initially so that they're easier to sort
+            QVector<VsServerInfoPointer> yourServers;
+            QVector<VsServerInfoPointer> subServers;
+            QVector<VsServerInfoPointer> pubServers;
+            int skippedStudios = 0;
+
+            QMutexLocker refreshLock(&m_refreshMutex);  // protect m_servers
+            m_servers.clear();
+            for (int i = 0; i < servers.count(); i++) {
+                if (servers.at(i)[QStringLiteral("type")].toString().contains(
+                        QStringLiteral("JackTrip"))) {
+                    QSharedPointer<VsServerInfo> serverInfo(new VsServerInfo(this));
+                    serverInfo->setIsAdmin(
+                        servers.at(i)[QStringLiteral("admin")].toBool());
+                    serverInfo->setName(servers.at(i)[QStringLiteral("name")].toString());
+                    serverInfo->setHost(
+                        servers.at(i)[QStringLiteral("serverHost")].toString());
+                    serverInfo->setIsManaged(
+                        servers.at(i)[QStringLiteral("managed")].toBool());
+                    serverInfo->setStatus(
+                        servers.at(i)[QStringLiteral("status")].toString());
+                    serverInfo->setPort(
+                        servers.at(i)[QStringLiteral("serverPort")].toInt());
+                    serverInfo->setIsPublic(
+                        servers.at(i)[QStringLiteral("public")].toBool());
+                    serverInfo->setRegion(
+                        servers.at(i)[QStringLiteral("region")].toString());
+                    serverInfo->setPeriod(
+                        servers.at(i)[QStringLiteral("period")].toInt());
+                    serverInfo->setSampleRate(
+                        servers.at(i)[QStringLiteral("sampleRate")].toInt());
+                    serverInfo->setQueueBuffer(
+                        servers.at(i)[QStringLiteral("queueBuffer")].toInt());
+                    serverInfo->setBannerURL(
+                        servers.at(i)[QStringLiteral("bannerURL")].toString());
+                    serverInfo->setId(servers.at(i)[QStringLiteral("id")].toString());
+                    serverInfo->setSessionId(
+                        servers.at(i)[QStringLiteral("sessionId")].toString());
+                    serverInfo->setStreamId(
+                        servers.at(i)[QStringLiteral("streamId")].toString());
+                    serverInfo->setInviteKey(
+                        servers.at(i)[QStringLiteral("inviteKey")].toString());
+                    serverInfo->setCloudId(
+                        servers.at(i)[QStringLiteral("cloudId")].toString());
+                    serverInfo->setEnabled(
+                        servers.at(i)[QStringLiteral("enabled")].toBool());
+                    serverInfo->setIsOwner(
+                        servers.at(i)[QStringLiteral("owner")].toBool());
+
+                    // Always add servers to m_servers
+                    m_servers.append(serverInfo);
+
+                    // Only add servers to the model that we want to show
+                    if (serverInfo->isAdmin() || serverInfo->isOwner()) {
+                        if (filterStudio(*serverInfo)) {
+                            ++skippedStudios;
+                        } else {
+                            yourServers.append(serverInfo);
+                            serverInfo->setSection(VsServerInfo::YOUR_STUDIOS);
+                        }
+                    } else if (m_subscribedServers.contains(serverInfo->id())) {
+                        if (filterStudio(*serverInfo)) {
+                            ++skippedStudios;
+                        } else {
+                            subServers.append(serverInfo);
+                            serverInfo->setSection(VsServerInfo::SUBSCRIBED_STUDIOS);
+                        }
+                    } else {
+                        if (!filterStudio(*serverInfo)) {
+                            pubServers.append(serverInfo);
+                            serverInfo->setSection(VsServerInfo::PUBLIC_STUDIOS);
+                        }
+                        // don't count public studios in skipped count
+                    }
+                }
+            }
+            refreshLock.unlock();
+
+            // sort studios in each section by name
+            auto serverSorter = [](VsServerInfoPointer first,
+                                   VsServerInfoPointer second) {
+                return *first < *second;
+            };
+            std::sort(yourServers.begin(), yourServers.end(), serverSorter);
+            std::sort(subServers.begin(), subServers.end(), serverSorter);
+            std::sort(pubServers.begin(), pubServers.end(), serverSorter);
+
+            // If we don't have any owned servers, move the JackTrip logo to an
+            // appropriate section header.
+            if (yourServers.isEmpty()) {
+                if (subServers.isEmpty()) {
+                    m_logoSection = QStringLiteral("Public Studios");
+
+                    if (skippedStudios == 0) {
+                        // This is a new user
+                        setShowCreateStudio(true);
+                    } else {
+                        // This is not a new user. One or more studios were filtered.
+                        setShowCreateStudio(false);
+                    }
+                } else {
+                    m_logoSection = QStringLiteral("Subscribed Studios");
+                }
+            } else {
+                m_logoSection = QStringLiteral("Your Studios");
+            }
+            emit logoSectionChanged();
+
+            m_serverModel.clear();
+            for (const VsServerInfoPointer& s : yourServers) {
+                m_serverModel.append(s.get());
+            }
+            for (const VsServerInfoPointer& s : subServers) {
+                m_serverModel.append(s.get());
+            }
+            for (const VsServerInfoPointer& s : pubServers) {
+                m_serverModel.append(s.get());
+            }
+            emit serverModelChanged();
+            int index = -1;
+            if (!topServerId.isEmpty()) {
+                for (int i = 0; i < m_serverModel.count(); i++) {
+                    if (m_serverModel.at(i)->id() == topServerId) {
+                        index = i;
+                        break;
+                    }
+                }
+            }
+            if (m_firstRefresh) {
+                m_refreshTimer.setInterval(5000);
+                m_refreshTimer.start();
+                m_heartbeatTimer.setInterval(5000);
+                m_heartbeatTimer.start();
+                m_firstRefresh = false;
+            }
+            m_refreshInProgress = false;
+            if (signalRefresh) {
+                emit refreshFinished(index);
+            }
+        });
+}
+
+bool VirtualStudio::filterStudio(const VsServerInfo& serverInfo) const
+{
+    // Return true if we want to filter the studio out of the display model
+    bool activeStudio = serverInfo.status() == QLatin1String("Ready");
+    bool hostedStudio = serverInfo.isManaged();
+    if (!m_showSelfHosted && !hostedStudio) {
+        return true;
+    }
+    if (!m_showInactive && !activeStudio) {
+        return true;
+    }
+    return false;
+}
+
+void VirtualStudio::getSubscriptions()
+{
+    if (m_userId.isEmpty()) {
+        qDebug() << "Invalid user ID";
+        return;
+    }
+    QNetworkReply* reply = m_api->getSubscriptions(m_userId);
+    connect(reply, &QNetworkReply::finished, this, [&, reply]() {
+        if (reply->error() != QNetworkReply::NoError) {
+            std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+            reply->deleteLater();
+            return;
+        }
+
+        QByteArray response            = reply->readAll();
+        QJsonDocument subscriptionList = QJsonDocument::fromJson(response);
+        if (!subscriptionList.isArray()) {
+            std::cout << "Error: Not an array" << std::endl;
+            reply->deleteLater();
+            return;
+        }
+        m_subscribedServers.clear();
+        QJsonArray subscriptions = subscriptionList.array();
+        for (int i = 0; i < subscriptions.count(); i++) {
+            m_subscribedServers.insert(
+                subscriptions.at(i)[QStringLiteral("serverId")].toString(), true);
+        }
+        reply->deleteLater();
+    });
+}
+
+void VirtualStudio::getRegions()
+{
+    QNetworkReply* reply = m_api->getRegions(m_userId);
+    connect(reply, &QNetworkReply::finished, this, [&, reply]() {
+        if (reply->error() != QNetworkReply::NoError) {
+            std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+            reply->deleteLater();
+            return;
+        }
+
+        m_regions = QJsonDocument::fromJson(reply->readAll()).object();
+        emit regionsChanged();
+        reply->deleteLater();
+    });
+}
+
+void VirtualStudio::getUserMetadata()
+{
+    QNetworkReply* reply = m_api->getUser(m_userId);
+    connect(reply, &QNetworkReply::finished, this, [&, reply]() {
+        if (reply->error() != QNetworkReply::NoError) {
+            std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+            reply->deleteLater();
+            return;
+        }
+
+        m_userMetadata = QJsonDocument::fromJson(reply->readAll()).object();
+        emit userMetadataChanged();
+        reply->deleteLater();
+    });
+}
+
+bool VirtualStudio::readyToJoin()
+{
+    // FTUX shows warnings and device setup views
+    // if any of these enabled, do not immediately join
+    return m_windowState == "connected"
+           && (m_connectionState == QStringLiteral("Waiting...")
+               || m_connectionState == QStringLiteral("Disconnected"));
+}
+
+void VirtualStudio::detectedFeedbackLoop()
+{
+    emit feedbackDetected();
+}
+
+VirtualStudio::~VirtualStudio()
+{
+    QDesktopServices::unsetUrlHandler("jacktrip");
+    // stop the audio worker thread before destructing other things
+    m_audioConfigPtr.reset();
+}
diff --git a/src/gui/virtualstudio.h b/src/gui/virtualstudio.h
new file mode 100644 (file)
index 0000000..fbd02c6
--- /dev/null
@@ -0,0 +1,344 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file virtualstudio.h
+ * \author Matt Horton, based on code by Aaron Wyatt
+ * \date March 2022
+ */
+
+#ifndef VIRTUALSTUDIO_H
+#define VIRTUALSTUDIO_H
+
+#include <QJsonObject>
+#include <QMap>
+#include <QMutex>
+#include <QNetworkAccessManager>
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <QString>
+#include <QStringList>
+#include <QTimer>
+#include <QUrl>
+#include <QVector>
+#include <QWebChannel>
+#include <QWebSocketServer>
+
+#include "../Settings.h"
+#include "qjacktrip.h"
+#include "vsConstants.h"
+#include "vsQuickView.h"
+#include "vsServerInfo.h"
+
+#ifdef __APPLE__
+#include "NoNap.h"
+#endif
+
+class JackTrip;
+class VsAudio;
+class VsApi;
+class VsAuth;
+class VsDevice;
+class VsWebSocket;
+
+typedef QSharedPointer<VsServerInfo> VsServerInfoPointer;
+
+class VirtualStudio : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(int webChannelPort READ webChannelPort NOTIFY webChannelPortChanged)
+    Q_PROPERTY(bool showFirstRun READ showFirstRun WRITE setShowFirstRun NOTIFY
+                   showFirstRunChanged)
+    Q_PROPERTY(bool hasRefreshToken READ hasRefreshToken NOTIFY hasRefreshTokenChanged)
+    Q_PROPERTY(QString versionString READ versionString CONSTANT)
+    Q_PROPERTY(QString logoSection READ logoSection NOTIFY logoSectionChanged)
+    Q_PROPERTY(
+        QString connectedErrorMsg READ connectedErrorMsg NOTIFY connectedErrorMsgChanged)
+
+    Q_PROPERTY(
+        QVector<VsServerInfo*> serverModel READ getServerModel NOTIFY serverModelChanged)
+    Q_PROPERTY(VsServerInfo* currentStudio READ currentStudio NOTIFY currentStudioChanged)
+    Q_PROPERTY(QString studioToJoin READ studioToJoin WRITE setStudioToJoin NOTIFY
+                   studioToJoinChanged)
+    Q_PROPERTY(QJsonObject regions READ regions NOTIFY regionsChanged)
+    Q_PROPERTY(QJsonObject userMetadata READ userMetadata NOTIFY userMetadataChanged)
+    Q_PROPERTY(bool showInactive READ showInactive WRITE setShowInactive NOTIFY
+                   showInactiveChanged)
+    Q_PROPERTY(bool showSelfHosted READ showSelfHosted WRITE setShowSelfHosted NOTIFY
+                   showSelfHostedChanged)
+    Q_PROPERTY(bool showCreateStudio READ showCreateStudio WRITE setShowCreateStudio
+                   NOTIFY showCreateStudioChanged)
+    Q_PROPERTY(QString connectionState READ connectionState NOTIFY connectionStateChanged)
+    Q_PROPERTY(QJsonObject networkStats READ networkStats NOTIFY networkStatsChanged)
+    Q_PROPERTY(bool networkOutage READ networkOutage NOTIFY updatedNetworkOutage)
+
+    Q_PROPERTY(QString updateChannel READ updateChannel WRITE setUpdateChannel NOTIFY
+                   updateChannelChanged)
+    Q_PROPERTY(float fontScale READ fontScale CONSTANT)
+    Q_PROPERTY(float uiScale READ uiScale WRITE setUiScale NOTIFY uiScaleChanged)
+    Q_PROPERTY(bool darkMode READ darkMode WRITE setDarkMode NOTIFY darkModeChanged)
+    Q_PROPERTY(bool collapseDeviceControls READ collapseDeviceControls WRITE
+                   setCollapseDeviceControls NOTIFY collapseDeviceControlsChanged)
+    Q_PROPERTY(bool testMode READ testMode WRITE setTestMode NOTIFY testModeChanged)
+    Q_PROPERTY(bool showDeviceSetup READ showDeviceSetup WRITE setShowDeviceSetup NOTIFY
+                   showDeviceSetupChanged)
+    Q_PROPERTY(bool showWarnings READ showWarnings WRITE setShowWarnings NOTIFY
+                   showWarningsChanged)
+    Q_PROPERTY(bool isExiting READ isExiting NOTIFY isExitingChanged)
+    Q_PROPERTY(bool noUpdater READ noUpdater CONSTANT)
+    Q_PROPERTY(bool psiBuild READ psiBuild CONSTANT)
+    Q_PROPERTY(QString failedMessage READ failedMessage NOTIFY failedMessageChanged)
+    Q_PROPERTY(QString windowState READ windowState WRITE setWindowState NOTIFY
+                   windowStateUpdated)
+    Q_PROPERTY(QString apiHost READ apiHost WRITE setApiHost NOTIFY apiHostChanged)
+    Q_PROPERTY(bool vsFtux READ vsFtux CONSTANT)
+    Q_PROPERTY(
+        QStringList updateChannelComboModel READ getUpdateChannelComboModel CONSTANT)
+
+   public:
+    explicit VirtualStudio(bool firstRun = false, QObject* parent = nullptr);
+    ~VirtualStudio() override;
+
+    void setStandardWindow(QSharedPointer<QJackTrip> window);
+    void setCLISettings(QSharedPointer<Settings> settings);
+    void show();
+    void raiseToTop();
+
+    int webChannelPort();
+    bool showFirstRun();
+    void setShowFirstRun(bool show);
+    bool hasRefreshToken();
+    QString versionString();
+    QString logoSection();
+    QString connectedErrorMsg();
+    void setConnectedErrorMsg(const QString& msg);
+    const QVector<VsServerInfo*>& getServerModel() const { return m_serverModel; }
+    VsServerInfo* currentStudio() { return &m_currentStudio; }
+    QJsonObject regions();
+    QJsonObject userMetadata();
+    QString connectionState();
+    QJsonObject networkStats();
+    QString updateChannel();
+    void setUpdateChannel(const QString& channel);
+    bool showInactive();
+    void setShowInactive(bool inactive);
+    bool showSelfHosted();
+    void setShowSelfHosted(bool selfHosted);
+    bool showCreateStudio();
+    void setShowCreateStudio(bool createStudio);
+    float fontScale();
+    float uiScale();
+    void setUiScale(float scale);
+    bool darkMode();
+    void setDarkMode(bool dark);
+    bool collapseDeviceControls();
+    void setCollapseDeviceControls(bool collapseDeviceControls);
+    bool testMode();
+    void setTestMode(bool test);
+    QString studioToJoin();
+    void setStudioToJoin(const QString& id);
+    bool showDeviceSetup();
+    void setShowDeviceSetup(bool show);
+    bool showWarnings();
+    void setShowWarnings(bool show);
+    bool noUpdater();
+    bool psiBuild();
+    QString failedMessage();
+    bool networkOutage();
+    bool backendAvailable();
+    QString windowState();
+    QString apiHost();
+    void setApiHost(QString host);
+    bool vsFtux();
+    bool isExiting();
+    const QStringList& getUpdateChannelComboModel() const
+    {
+        return m_updateChannelOptions;
+    }
+
+   public slots:
+    void toStandard();
+    void toVirtualStudio();
+    void login();
+    void logout();
+    void refreshStudios(int index, bool signalRefresh = false);
+    void loadSettings();
+    void saveSettings();
+    void triggerReconnect(bool refresh);
+    void createStudio();
+    void editProfile();
+    void showAbout();
+    void openLink(const QString& url);
+    void handleDeeplinkRequest(const QUrl& url);
+    void udpWaitingTooLong();
+    void setWindowState(QString state);
+    void joinStudio();
+    void disconnect();
+    void collectFeedbackSurvey(QString serverId, int rating, QString message);
+
+   signals:
+    void failed();
+    void connected();
+    void disconnected();
+    void refreshFinished(int index);
+    void webChannelPortChanged(int webChannelPort);
+    void showFirstRunChanged();
+    void hasRefreshTokenChanged();
+    void logoSectionChanged();
+    void connectedErrorMsgChanged();
+    void serverModelChanged();
+    void currentStudioChanged();
+    void regionsChanged();
+    void userMetadataChanged();
+    void showInactiveChanged();
+    void showSelfHostedChanged();
+    void showCreateStudioChanged();
+    void connectionStateChanged();
+    void networkStatsChanged();
+    void updateChannelChanged();
+    void showDeviceSetupChanged();
+    void showWarningsChanged();
+    void uiScaleChanged();
+    void collapseDeviceControlsChanged(bool collapseDeviceControls);
+    void newScale();
+    void darkModeChanged();
+    void testModeChanged();
+    void signalExit();
+    void periodicRefresh();
+    void failedMessageChanged();
+    void studioToJoinChanged();
+    void updatedNetworkOutage(bool outage);
+    void windowStateUpdated();
+    void isExitingChanged();
+    void apiHostChanged();
+    void feedbackDetected();
+    void openFeedbackSurveyModal(QString serverId);
+
+   private slots:
+    void slotAuthSucceeded();
+    void receivedConnectionFromPeer();
+    void handleWebsocketMessage(const QString& msg);
+    void restartStudioSocket();
+    void updatedStats(const QJsonObject& stats);
+    void processError(const QString& errorMessage);
+    void detectedFeedbackLoop();
+    void sendHeartbeat();
+    void connectionFinished();
+    void exit();
+
+   private:
+    void resetState();
+    void getServerList(bool signalRefresh = false, int index = -1);
+    bool filterStudio(const VsServerInfo& serverInfo) const;
+    void getSubscriptions();
+    void getRegions();
+    void getUserMetadata();
+    bool readyToJoin();
+    void connectToStudio();
+    void completeConnection();
+
+   private:
+    enum ReconnectState {
+        NOT_RECONNECTING = 0,
+        RECONNECTING_VALIDATE,
+        RECONNECTING_REFRESH
+    };
+
+    VsQuickView m_view;
+    VsServerInfo m_currentStudio;
+    QNetworkAccessManager* m_networkAccessManagerPtr;
+    QSharedPointer<QJackTrip> m_standardWindow;
+    QSharedPointer<Settings> m_cliSettings;
+    QSharedPointer<VsAuth> m_auth;
+    QSharedPointer<VsApi> m_api;
+    QScopedPointer<VsDevice> m_devicePtr;
+    QScopedPointer<VsWebSocket> m_studioSocketPtr;
+    QSharedPointer<VsAudio> m_audioConfigPtr;
+    QVector<VsServerInfoPointer> m_servers;
+    QVector<VsServerInfo*> m_serverModel;  //< qml doesn't like smart pointers
+    QScopedPointer<QWebSocketServer> m_webChannelServer;
+    QScopedPointer<QWebChannel> m_webChannel;
+    QMap<QString, bool> m_subscribedServers;
+    QJsonObject m_regions;
+    QJsonObject m_userMetadata;
+    QJsonObject m_networkStats;
+    QTimer m_startTimer;
+    QTimer m_refreshTimer;
+    QTimer m_heartbeatTimer;
+    QTimer m_networkOutageTimer;
+    QMutex m_refreshMutex;
+    QString m_studioToJoin;
+    QString m_updateChannel;
+    QString m_refreshToken;
+    QString m_userId;
+    QString m_apiHost               = PROD_API_HOST;
+    ReconnectState m_reconnectState = ReconnectState::NOT_RECONNECTING;
+    QJackTrip::uiModeT m_uiMode     = QJackTrip::UNSET;
+
+    bool m_firstRefresh           = true;
+    bool m_jackTripRunning        = false;
+    bool m_showFirstRun           = false;
+    bool m_checkSsl               = true;
+    bool m_refreshInProgress      = false;
+    bool m_onConnectedScreen      = false;
+    bool m_isExiting              = false;
+    bool m_showInactive           = true;
+    bool m_showSelfHosted         = false;
+    bool m_showCreateStudio       = false;
+    bool m_showDeviceSetup        = true;
+    bool m_showWarnings           = true;
+    bool m_darkMode               = false;
+    bool m_collapseDeviceControls = false;
+    bool m_testMode               = false;
+    bool m_authenticated          = false;
+    float m_fontScale             = 1;
+    float m_uiScale               = 1;
+    uint32_t m_webChannelPort     = 1;
+
+    QString m_failedMessage            = QStringLiteral("");
+    QString m_windowState              = QStringLiteral("start");
+    QString m_connectedErrorMsg        = QStringLiteral("");
+    QString m_logoSection              = QStringLiteral("Your Studios");
+    QString m_connectionState          = QStringLiteral("Waiting...");
+    QStringList m_updateChannelOptions = {"Stable", "Edge"};
+
+#ifdef __APPLE__
+    NoNap m_noNap;
+#endif
+
+#ifdef VS_FTUX
+    bool m_vsFtux = true;
+#else
+    bool m_vsFtux = false;
+#endif
+};
+
+#endif  // VIRTUALSTUDIO_H
diff --git a/src/gui/vs.qml b/src/gui/vs.qml
new file mode 100644 (file)
index 0000000..9eee794
--- /dev/null
@@ -0,0 +1,312 @@
+import QtQuick
+import QtQuick.Controls
+
+Rectangle {
+    property string backgroundColour: virtualstudio.darkMode ? "#272525" : "#FAFBFB"
+    property string textColour: virtualstudio.darkMode ? "#FAFBFB" : "#0F0D0D"
+
+    color: backgroundColour
+    state: virtualstudio.windowState
+    anchors.fill: parent
+
+    id: window
+    states: [
+        State {
+            name: "start"
+            PropertyChanges { target: startScreen; x: 0 }
+            PropertyChanges { target: loginScreen; x: window.width; }
+            PropertyChanges { target: recommendationsScreen; x: window.width }
+            PropertyChanges { target: permissionsScreen; x: window.width }
+            PropertyChanges { target: setupScreen; x: window.width }
+            PropertyChanges { target: browseScreen; x: window.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: createStudioScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+            PropertyChanges { target: changeDevicesScreen; x: 2*window.width }
+            PropertyChanges { target: failedScreen; x: window.width }
+        },
+
+        State {
+            name: "login"
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: loginScreen; x: 0; }
+            PropertyChanges { target: recommendationsScreen; x: window.width }
+            PropertyChanges { target: permissionsScreen; x: window.width }
+            PropertyChanges { target: setupScreen; x: window.width }
+            PropertyChanges { target: browseScreen; x: window.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: createStudioScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+            PropertyChanges { target: changeDevicesScreen; x: 2*window.width }
+            PropertyChanges { target: failedScreen; x: window.width }
+        },
+
+        State {
+            name: "recommendations"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: recommendationsScreen; x: 0 }
+            PropertyChanges { target: permissionsScreen; x: window.width }
+            PropertyChanges { target: setupScreen; x: window.width }
+            PropertyChanges { target: browseScreen; x: window.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: createStudioScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+            PropertyChanges { target: changeDevicesScreen; x: 2*window.width }
+            PropertyChanges { target: failedScreen; x: window.width }
+        },
+
+        State {
+            name: "permissions"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
+            PropertyChanges { target: permissionsScreen; x: 0 }
+            PropertyChanges { target: setupScreen; x: window.width }
+            PropertyChanges { target: browseScreen; x: window.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: createStudioScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+            PropertyChanges { target: changeDevicesScreen; x: 2*window.width }
+            PropertyChanges { target: failedScreen; x: window.width }
+        },
+
+        State {
+            name: "setup"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
+            PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width }
+            PropertyChanges { target: setupScreen; x: 0 }
+            PropertyChanges { target: browseScreen; x: -browseScreen.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: createStudioScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+            PropertyChanges { target: changeDevicesScreen; x: 2*window.width }
+            PropertyChanges { target: failedScreen; x: window.width }
+        },
+
+        State {
+            name: "browse"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
+            PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width }
+            PropertyChanges { target: setupScreen; x: window.width }
+            PropertyChanges { target: browseScreen; x: 0 }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: createStudioScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+            PropertyChanges { target: changeDevicesScreen; x: 2*window.width }
+            PropertyChanges { target: failedScreen; x: window.width }
+        },
+
+        State {
+            name: "settings"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
+            PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width }
+            PropertyChanges { target: setupScreen; x: window.width }
+            PropertyChanges { target: browseScreen; x: -browseScreen.width }
+            PropertyChanges { target: settingsScreen; x: 0 }
+            PropertyChanges { target: createStudioScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+            PropertyChanges { target: changeDevicesScreen; x: 2*window.width }
+            PropertyChanges { target: failedScreen; x: window.width }
+        },
+
+        State {
+            name: "create_studio"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
+            PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width }
+            PropertyChanges { target: setupScreen; x: window.width }
+            PropertyChanges { target: browseScreen; x: -browseScreen.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: createStudioScreen; x: 0 }
+            PropertyChanges { target: connectedScreen; x: window.width }
+            PropertyChanges { target: changeDevicesScreen; x: window.width }
+            PropertyChanges { target: failedScreen; x: window.width }
+        },
+
+        State {
+            name: "connected"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
+            PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width }
+            PropertyChanges { target: setupScreen; x: 0 }
+            PropertyChanges { target: browseScreen; x: -browseScreen.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: createStudioScreen; x: -createStudioScreen.width }
+            PropertyChanges { target: connectedScreen; x: 0 }
+            PropertyChanges { target: changeDevicesScreen; x: window.width }
+            PropertyChanges { target: failedScreen; x: window.width }
+        },
+
+        State {
+            name: "change_devices"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
+            PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width }
+            PropertyChanges { target: setupScreen; x: 0 }
+            PropertyChanges { target: browseScreen; x: -browseScreen.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: createStudioScreen; x: -createStudioScreen.width }
+            PropertyChanges { target: connectedScreen; x: 0 }
+            PropertyChanges { target: changeDevicesScreen; x: 0 }
+            PropertyChanges { target: failedScreen; x: window.width }
+        },
+
+        State {
+            name: "failed"
+            PropertyChanges { target: loginScreen; x: -loginScreen.width }
+            PropertyChanges { target: startScreen; x: -startScreen.width }
+            PropertyChanges { target: recommendationsScreen; x: -recommendationsScreen.width }
+            PropertyChanges { target: permissionsScreen; x: -permissionsScreen.width }
+            PropertyChanges { target: setupScreen; x: -setupScreen.width }
+            PropertyChanges { target: browseScreen; x: -browseScreen.width }
+            PropertyChanges { target: settingsScreen; x: window.width }
+            PropertyChanges { target: createStudioScreen; x: window.width }
+            PropertyChanges { target: connectedScreen; x: window.width }
+            PropertyChanges { target: changeDevicesScreen; x: 2*window.width }
+            PropertyChanges { target: failedScreen; x: 0 }
+        }
+    ]
+
+    transitions: Transition {
+        NumberAnimation { properties: "x"; duration: 500; easing.type: Easing.InOutQuad }
+    }
+
+    FirstLaunch {
+        id: startScreen
+    }
+
+    Login {
+        id: loginScreen
+    }
+
+    Recommendations {
+        id: recommendationsScreen
+    }
+
+    Permissions {
+        id: permissionsScreen
+    }
+
+    Browse {
+        id: browseScreen
+    }
+
+    Setup {
+        id: setupScreen
+    }
+
+    Settings {
+        id: settingsScreen
+    }
+
+    Connected {
+        id: connectedScreen
+    }
+
+    ChangeDevices {
+        id: changeDevicesScreen
+    }
+
+    CreateStudio {
+        id: createStudioScreen
+    }
+
+    Failed {
+        id: failedScreen
+    }
+
+    onWidthChanged: {
+        if (virtualstudio.windowState === "start") {
+            startScreen.x = 0
+        } else if (virtualstudio.windowState === "login") {
+            loginScreen.x = 0
+        } else if (virtualstudio.windowState === "recommendations") {
+            recommendationsScreen.x = 0;
+        } else if (virtualstudio.windowState === "permissions") {
+            permissionsScreen.x = 0;
+        } else if (virtualstudio.windowState === "setup") {
+            setupScreen.x = 0
+        } else if (virtualstudio.windowState === "browse") {
+            browseScreen.x = 0
+        } else if (virtualstudio.windowState === "settings") {
+            settingsScreen.x = 0
+        } else if (virtualstudio.windowState === "create_studio") {
+            createStudioScreen.x = 0
+        } else if (virtualstudio.windowState === "connected") {
+            connectedScreen.x = 0
+        } else if (virtualstudio.windowState === "change_devices") {
+            changeDevicesScreen.x = 0
+        } else if (virtualstudio.windowState === "failed") {
+            failedScreen.x = 0
+        }
+    }
+
+    onHeightChanged: {
+        if (virtualstudio.windowState === "start") {
+            startScreen.x = 0
+        } else if (virtualstudio.windowState === "login") {
+            loginScreen.x = 0
+        } else if (virtualstudio.windowState === "recommendations") {
+            recommendationsScreen.x = 0;
+        } else if (virtualstudio.windowState === "permissions") {
+            permissionsScreen.x = 0;
+        } else if (virtualstudio.windowState === "setup") {
+            setupScreen.x = 0
+        } else if (virtualstudio.windowState === "browse") {
+            browseScreen.x = 0
+        } else if (virtualstudio.windowState === "settings") {
+            settingsScreen.x = 0
+        } else if (virtualstudio.windowState === "create_studio") {
+            createStudioScreen.x = 0
+        } else if (virtualstudio.windowState === "connected") {
+            connectedScreen.x = 0
+        } else if (virtualstudio.windowState === "change_devices") {
+            changeDevicesScreen.x = 0
+        } else if (virtualstudio.windowState === "failed") {
+            failedScreen.x = 0
+        }
+    }
+
+    Connections {
+        target: auth
+        function onAuthSucceeded() {
+            if (virtualstudio.windowState !== "login") {
+                // can happen on settings screen when switching between prod and test
+                return;
+            }
+            if (virtualstudio.showWarnings) {
+                virtualstudio.windowState = "recommendations";
+            } else if (virtualstudio.studioToJoin === "") {
+                virtualstudio.windowState = "browse";
+            } else {
+                virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
+                virtualstudio.joinStudio();
+            }
+        }
+    }
+    Connections {
+        target: virtualstudio
+        function onConnected() {
+            if (virtualstudio.windowState == "change_devices") {
+                return;
+            }
+            virtualstudio.windowState = "connected";
+        }
+        function onFailed() {
+            virtualstudio.windowState = "failed";
+        }
+        function onDisconnected() {
+            virtualstudio.windowState = "browse";
+        }
+    }
+}
diff --git a/src/gui/vsApi.cpp b/src/gui/vsApi.cpp
new file mode 100644 (file)
index 0000000..2a50de2
--- /dev/null
@@ -0,0 +1,163 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsApi.cpp
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#include "vsApi.h"
+
+#include "../jacktrip_globals.h"
+
+VsApi::VsApi(QNetworkAccessManager* networkAccessManager)
+{
+    m_networkAccessManager = networkAccessManager;
+}
+
+QNetworkReply* VsApi::getAuth0UserInfo()
+{
+    return get(QUrl("https://auth.jacktrip.org/userinfo"));
+}
+
+QNetworkReply* VsApi::getUser(const QString& userId)
+{
+    return get(QUrl(QString("https://%1/api/users/%2").arg(m_apiHost, userId)));
+}
+
+QNetworkReply* VsApi::getServers()
+{
+    return get(QUrl(QString("https://%1/api/servers").arg(m_apiHost)));
+}
+
+QNetworkReply* VsApi::getSubscriptions(const QString& userId)
+{
+    return get(
+        QUrl(QString("https://%1/api/users/%2/subscriptions").arg(m_apiHost, userId)));
+}
+
+QNetworkReply* VsApi::getRegions(const QString& userId)
+{
+    return get(QUrl(QString("https://%1/api/users/%2/regions").arg(m_apiHost, userId)));
+}
+
+QNetworkReply* VsApi::getDevice(const QString& deviceId)
+{
+    return get(QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId)));
+}
+
+QNetworkReply* VsApi::postDevice(const QByteArray& data)
+{
+    return post(QUrl(QString("https://%1/api/devices").arg(m_apiHost)), data);
+}
+
+QNetworkReply* VsApi::postDeviceHeartbeat(const QString& deviceId, const QByteArray& data)
+{
+    return post(
+        QUrl(QString("https://%1/api/devices/%2/heartbeat").arg(m_apiHost, deviceId)),
+        data);
+}
+
+QNetworkReply* VsApi::submitServerFeedback(const QString& serverId,
+                                           const QByteArray& data)
+{
+    return post(
+        QUrl(QString("https://%1/api/servers/%2/feedback").arg(m_apiHost, serverId)),
+        data);
+}
+
+QNetworkReply* VsApi::updateServer(const QString& serverId, const QByteArray& data)
+{
+    return put(QUrl(QString("https://%1/api/servers/%2").arg(m_apiHost, serverId)), data);
+}
+
+QNetworkReply* VsApi::updateDevice(const QString& deviceId, const QByteArray& data)
+{
+    return put(QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId)), data);
+}
+
+QNetworkReply* VsApi::deleteDevice(const QString& deviceId)
+{
+    return deleteResource(
+        QUrl(QString("https://%1/api/devices/%2").arg(m_apiHost, deviceId)));
+}
+
+QNetworkReply* VsApi::get(const QUrl& url)
+{
+    QNetworkRequest request = QNetworkRequest(url);
+    request.setRawHeader(QByteArray("User-Agent"),
+                         QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
+    request.setRawHeader(QByteArray("Authorization"),
+                         QString("Bearer %1").arg(m_accessToken).toUtf8());
+
+    QNetworkReply* reply = m_networkAccessManager->get(request);
+    return reply;
+}
+
+QNetworkReply* VsApi::post(const QUrl& url, const QByteArray& data)
+{
+    QNetworkRequest request = QNetworkRequest(url);
+    request.setRawHeader(QByteArray("User-Agent"),
+                         QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
+    request.setRawHeader(QByteArray("Authorization"),
+                         QString("Bearer %1").arg(m_accessToken).toUtf8());
+    request.setRawHeader(QByteArray("Content-Type"),
+                         QString("application/json").toUtf8());
+
+    QNetworkReply* reply = m_networkAccessManager->post(request, data);
+    return reply;
+}
+
+QNetworkReply* VsApi::put(const QUrl& url, const QByteArray& data)
+{
+    QNetworkRequest request = QNetworkRequest(url);
+    request.setRawHeader(QByteArray("User-Agent"),
+                         QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
+    request.setRawHeader(QByteArray("Authorization"),
+                         QString("Bearer %1").arg(m_accessToken).toUtf8());
+    request.setRawHeader(QByteArray("Content-Type"),
+                         QString("application/json").toUtf8());
+    QNetworkReply* reply = m_networkAccessManager->put(request, data);
+    return reply;
+}
+
+QNetworkReply* VsApi::deleteResource(const QUrl& url)
+{
+    QNetworkRequest request = QNetworkRequest(url);
+    request.setRawHeader(QByteArray("User-Agent"),
+                         QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
+    request.setRawHeader(QByteArray("Authorization"),
+                         QString("Bearer %1").arg(m_accessToken).toUtf8());
+
+    QNetworkReply* reply = m_networkAccessManager->deleteResource(request);
+    return reply;
+}
\ No newline at end of file
diff --git a/src/gui/vsApi.h b/src/gui/vsApi.h
new file mode 100644 (file)
index 0000000..2d64f0d
--- /dev/null
@@ -0,0 +1,89 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsApi.h
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#ifndef VSAPI_H
+#define VSAPI_H
+
+#include <QEventLoop>
+#include <QJsonParseError>
+#include <QMap>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QString>
+#include <QUrl>
+#include <QVariant>
+#include <iostream>
+
+class VsApi : public QObject
+{
+    Q_OBJECT
+
+   public:
+    VsApi(QNetworkAccessManager* networkAccessManager);
+    void setAccessToken(QString token) { m_accessToken = token; };
+    void setApiHost(QString host) { m_apiHost = host; }
+    QString getApiHost() { return m_apiHost; }
+
+    QNetworkReply* getAuth0UserInfo();
+    QNetworkReply* getUser(const QString& userId);
+    QNetworkReply* getServers();
+    QNetworkReply* getSubscriptions(const QString& userId);
+    QNetworkReply* getRegions(const QString& userId);
+    QNetworkReply* getDevice(const QString& deviceId);
+
+    QNetworkReply* postDevice(const QByteArray& data);
+    QNetworkReply* postDeviceHeartbeat(const QString& deviceId, const QByteArray& data);
+    QNetworkReply* submitServerFeedback(const QString& serverId, const QByteArray& data);
+
+    QNetworkReply* updateServer(const QString& serverId, const QByteArray& data);
+    QNetworkReply* updateDevice(const QString& deviceId, const QByteArray& data);
+
+    QNetworkReply* deleteDevice(const QString& deviceId);
+
+   private:
+    QNetworkReply* get(const QUrl& url);
+    QNetworkReply* put(const QUrl& url, const QByteArray& data);
+    QNetworkReply* post(const QUrl& url, const QByteArray& data);
+    QNetworkReply* deleteResource(const QUrl& url);
+
+    QString m_accessToken;
+    QString m_apiHost;
+    QNetworkAccessManager* m_networkAccessManager;
+};
+
+#endif  // VSAPI_H
\ No newline at end of file
diff --git a/src/gui/vsAudio.cpp b/src/gui/vsAudio.cpp
new file mode 100644 (file)
index 0000000..a9fda8f
--- /dev/null
@@ -0,0 +1,1420 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsAudio.cpp
+ * \author Matt Horton
+ * \date September 2022
+ */
+
+#include "vsAudio.h"
+
+#include <QDebug>
+#include <QEventLoop>
+#include <QJsonObject>
+#include <QSettings>
+#include <QThread>
+
+#ifdef USE_WEAK_JACK
+#include "weak_libjack.h"
+#endif
+
+#ifndef NO_JACK
+#include "../JackAudioInterface.h"
+#endif
+
+#ifdef __APPLE__
+#include "vsMacPermissions.h"
+#else
+#include "vsPermissions.h"
+#endif
+
+#ifndef NO_FEEDBACK
+#include "../Analyzer.h"
+#endif
+
+#include "../JackTrip.h"
+#include "../Meter.h"
+#include "../Monitor.h"
+#include "../Tone.h"
+#include "../Volume.h"
+#include "AudioInterfaceMode.h"
+
+// generic function to wait for a signal to be emitted
+template<typename SignalSenderPtr, typename SignalFuncPtr>
+static inline void WaitForSignal(SignalSenderPtr sender, SignalFuncPtr signal)
+{
+    QTimer timer;
+    timer.setTimerType(Qt::CoarseTimer);
+    timer.setSingleShot(true);
+
+    QEventLoop loop;
+    QObject::connect(sender, signal, &loop, &QEventLoop::quit);
+    QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+    timer.start(10000);  // wait for max 10 seconds
+    loop.exec();
+}
+
+// Constructor
+VsAudio::VsAudio(QObject* parent)
+    : QObject(parent)
+    , m_inputMeterLevels(2, 0)
+    , m_outputMeterLevels(2, 0)
+    , m_inputComboModel(QJsonArray::fromStringList(QStringList(QLatin1String(""))))
+    , m_outputComboModel(QJsonArray::fromStringList(QStringList(QLatin1String(""))))
+    , m_inputChannelsComboModel(
+          QJsonArray::fromStringList(QStringList(QLatin1String(""))))
+    , m_outputChannelsComboModel(
+          QJsonArray::fromStringList(QStringList(QLatin1String(""))))
+    , m_inputMixModeComboModel(QJsonArray::fromStringList(QStringList(QLatin1String(""))))
+    , m_audioWorkerPtr(new VsAudioWorker(this))
+    , m_workerThreadPtr(nullptr)
+    , m_inputMeterPluginPtr(nullptr)
+    , m_outputMeterPluginPtr(nullptr)
+    , m_inputVolumePluginPtr(nullptr)
+    , m_outputVolumePluginPtr(nullptr)
+    , m_monitorPluginPtr(nullptr)
+    , mHasErrors(false)
+{
+    loadSettings();
+
+    QJsonObject element;
+    element.insert(QString::fromStdString("label"), QString::fromStdString("Mono"));
+    element.insert(QString::fromStdString("value"),
+                   static_cast<int>(AudioInterface::MONO));
+    m_inputMixModeComboModel = QJsonArray();
+    m_inputMixModeComboModel.push_back(element);
+
+    element = QJsonObject();
+    element.insert(QString::fromStdString("label"), QString::fromStdString("1"));
+    element.insert(QString::fromStdString("baseChannel"), QVariant(0).toInt());
+    element.insert(QString::fromStdString("numChannels"), QVariant(1).toInt());
+    m_inputChannelsComboModel = QJsonArray();
+    m_inputChannelsComboModel.push_back(element);
+
+    element = QJsonObject();
+    element.insert(QString::fromStdString("label"), QString::fromStdString("1 & 2"));
+    element.insert(QString::fromStdString("baseChannel"), QVariant(0).toInt());
+    element.insert(QString::fromStdString("numChannels"), QVariant(2).toInt());
+    m_outputChannelsComboModel = QJsonArray();
+    m_outputChannelsComboModel.push_back(element);
+
+    // Initialize timers needed for clip indicators
+    m_inputClipTimer.setTimerType(Qt::CoarseTimer);
+    m_inputClipTimer.setSingleShot(true);
+    m_inputClipTimer.setInterval(3000);
+    m_outputClipTimer.setTimerType(Qt::CoarseTimer);
+    m_outputClipTimer.setSingleShot(true);
+    m_outputClipTimer.setInterval(3000);
+    m_inputClipTimer.callOnTimeout([&]() {
+        if (m_inputClipped) {
+            m_inputClipped = false;
+            emit updatedInputClipped(m_inputClipped);
+        }
+    });
+    m_outputClipTimer.callOnTimeout([&]() {
+        if (m_outputClipped) {
+            m_outputClipped = false;
+            emit updatedOutputClipped(m_outputClipped);
+        }
+    });
+
+    // move audio worker to its own thread
+    m_workerThreadPtr = new QThread;
+    m_workerThreadPtr->setObjectName("VsAudioWorker");
+    m_workerThreadPtr->start();
+    m_audioWorkerPtr->moveToThread(m_workerThreadPtr);
+
+    // connect worker signals to slots
+    connect(this, &VsAudio::signalStartAudio, m_audioWorkerPtr.get(),
+            &VsAudioWorker::openAudioInterface, Qt::QueuedConnection);
+    connect(this, &VsAudio::signalStopAudio, m_audioWorkerPtr.get(),
+            &VsAudioWorker::closeAudioInterface, Qt::QueuedConnection);
+#ifdef RT_AUDIO
+    connect(this, &VsAudio::signalRefreshDevices, m_audioWorkerPtr.get(),
+            &VsAudioWorker::refreshDevices, Qt::QueuedConnection);
+    connect(this, &VsAudio::signalValidateDevices, m_audioWorkerPtr.get(),
+            &VsAudioWorker::validateDevices, Qt::QueuedConnection);
+    connect(m_audioWorkerPtr.get(), &VsAudioWorker::signalUpdatedDeviceModels, this,
+            &VsAudio::setDeviceModels, Qt::QueuedConnection);
+#endif
+
+    // Add permissions for Mac
+#ifdef __APPLE__
+    m_permissionsPtr.reset(new VsMacPermissions());
+    if (m_permissionsPtr->micPermissionChecked()
+        && m_permissionsPtr->micPermission() == "unknown") {
+        m_permissionsPtr->getMicPermission();
+    }
+#else
+    m_permissionsPtr.reset(new VsPermissions());
+#endif
+}
+
+VsAudio::~VsAudio()
+{
+    if (m_workerThreadPtr == nullptr)
+        return;
+    m_workerThreadPtr->quit();
+    WaitForSignal(m_workerThreadPtr, &QThread::finished);
+    m_workerThreadPtr->deleteLater();
+}
+
+bool VsAudio::backendAvailable() const
+{
+    if constexpr ((isBackendAvailable<AudioInterfaceMode::JACK>()
+                   || isBackendAvailable<AudioInterfaceMode::RTAUDIO>())) {
+        return true;
+    } else {
+        return false;
+    }
+}
+
+bool VsAudio::jackIsAvailable() const
+{
+    if constexpr (isBackendAvailable<AudioInterfaceMode::JACK>()) {
+#ifdef USE_WEAK_JACK
+        // Check if Jack is available
+        return (have_libjack() == 0);
+#else
+        return true;
+#endif
+    } else {
+        return false;
+    }
+}
+
+void VsAudio::setAudioReady(bool ready)
+{
+    if (ready == m_audioReady)
+        return;
+    m_audioReady = ready;
+    emit signalAudioReadyChanged();
+    if (m_audioReady)
+        emit signalAudioIsReady();
+    else
+        emit signalAudioIsNotReady();
+}
+
+void VsAudio::setScanningDevices(bool b)
+{
+    if (b == m_scanningDevices)
+        return;
+    m_scanningDevices = b;
+    emit signalScanningDevicesChanged();
+}
+
+void VsAudio::setAudioBackend(const QString& backend)
+{
+    bool useRtAudio = (backend == QStringLiteral("RtAudio"));
+    if (useRtAudio) {
+        if (getUseRtAudio())
+            return;
+        m_backend = AudioBackendType::RTAUDIO;
+    } else {
+        if (!getUseRtAudio())
+            return;
+        m_backend = AudioBackendType::JACK;
+    }
+    emit audioBackendChanged(useRtAudio);
+}
+
+void VsAudio::setFeedbackDetectionEnabled(bool enabled)
+{
+    if (m_feedbackDetectionEnabled == enabled)
+        return;
+    m_feedbackDetectionEnabled = enabled;
+    emit feedbackDetectionEnabledChanged();
+}
+
+void VsAudio::setSampleRate(int sampleRate)
+{
+    if (m_audioSampleRate == sampleRate)
+        return;
+    m_audioSampleRate = sampleRate;
+    emit sampleRateChanged();
+}
+
+void VsAudio::setBufferSize(int bufSize)
+{
+    if (m_audioBufferSize == bufSize)
+        return;
+    m_audioBufferSize = bufSize;
+    emit bufferSizeChanged();
+}
+
+void VsAudio::setBufferStrategy(int bufStrategy)
+{
+    if (m_bufferStrategy == bufStrategy)
+        return;
+    m_bufferStrategy = bufStrategy;
+    emit bufferStrategyChanged();
+}
+
+void VsAudio::setNumInputChannels(int numChannels)
+{
+    if (numChannels == m_numInputChannels)
+        return;
+    m_numInputChannels = numChannels;
+    emit numInputChannelsChanged(numChannels);
+}
+
+void VsAudio::setNumOutputChannels(int numChannels)
+{
+    if (numChannels == m_numOutputChannels)
+        return;
+    m_numOutputChannels = numChannels;
+    emit numOutputChannelsChanged(numChannels);
+}
+
+void VsAudio::setBaseInputChannel(int baseChannel)
+{
+    if (baseChannel == m_baseInputChannel)
+        return;
+    m_baseInputChannel = baseChannel;
+    emit baseInputChannelChanged(baseChannel);
+    return;
+}
+
+void VsAudio::setBaseOutputChannel(int baseChannel)
+{
+    if (baseChannel == m_baseOutputChannel)
+        return;
+    m_baseOutputChannel = baseChannel;
+    emit baseOutputChannelChanged(baseChannel);
+    return;
+}
+
+void VsAudio::setInputMixMode(const int mode)
+{
+    if (mode == m_inputMixMode)
+        return;
+    m_inputMixMode = mode;
+    emit inputMixModeChanged(mode);
+    return;
+}
+
+void VsAudio::setInputMuted(bool muted)
+{
+    if (m_inMuted == muted)
+        return;
+    m_inMuted = muted;
+    emit updatedInputMuted(muted);
+}
+
+void VsAudio::setInputVolume(float multiplier)
+{
+    if (multiplier == m_inMultiplier)
+        return;
+    m_inMultiplier = multiplier;
+    emit updatedInputVolume(multiplier);
+}
+
+void VsAudio::setOutputVolume(float multiplier)
+{
+    if (multiplier == m_outMultiplier)
+        return;
+    m_outMultiplier = multiplier;
+    emit updatedOutputVolume(multiplier);
+}
+
+void VsAudio::setMonitorVolume(float multiplier)
+{
+    if (multiplier == m_monMultiplier)
+        return;
+    m_monMultiplier = multiplier;
+    emit updatedMonitorVolume(multiplier);
+}
+
+void VsAudio::setInputDevice([[maybe_unused]] const QString& device)
+{
+    if (!getUseRtAudio())
+        return;
+#ifdef RT_AUDIO
+    if (device == m_inputDevice)
+        return;
+    m_inputDevice = device;
+    emit inputDeviceChanged(m_inputDevice);
+#endif
+}
+
+void VsAudio::setOutputDevice([[maybe_unused]] const QString& device)
+{
+    if (!getUseRtAudio())
+        return;
+#ifdef RT_AUDIO
+    if (device == m_outputDevice)
+        return;
+    m_outputDevice = device;
+    emit outputDeviceChanged(m_outputDevice);
+#endif
+}
+
+void VsAudio::setDevicesErrorMsg(const QString& msg)
+{
+    if (m_devicesErrorMsg == msg)
+        return;
+    m_devicesErrorMsg = msg;
+    emit devicesErrorChanged();
+}
+
+void VsAudio::setDevicesWarningMsg(const QString& msg)
+{
+    if (m_devicesWarningMsg == msg)
+        return;
+    m_devicesWarningMsg = msg;
+    emit devicesWarningChanged();
+}
+
+void VsAudio::setDevicesErrorHelpUrl(const QString& url)
+{
+    if (m_devicesErrorHelpUrl == url)
+        return;
+    m_devicesErrorHelpUrl = url;
+    emit devicesErrorHelpUrlChanged();
+}
+
+void VsAudio::setDevicesWarningHelpUrl(const QString& url)
+{
+    if (m_devicesWarningHelpUrl == url)
+        return;
+    m_devicesWarningHelpUrl = url;
+    emit devicesWarningHelpUrlChanged();
+}
+
+void VsAudio::setHighLatencyFlag(bool highLatencyFlag)
+{
+    if (m_highLatencyFlag == highLatencyFlag)
+        return;
+    m_highLatencyFlag = highLatencyFlag;
+    emit highLatencyFlagChanged(highLatencyFlag);
+}
+
+void VsAudio::startAudio(bool block)
+{
+    // note this is also used for restartAudio()
+    emit signalStartAudio();
+    if (!block)
+        return;
+    WaitForSignal(this, &VsAudio::signalAudioIsReady);
+}
+
+void VsAudio::stopAudio(bool block)
+{
+    if (!getAudioReady())
+        return;
+    emit signalStopAudio();
+    if (!block)
+        return;
+    WaitForSignal(this, &VsAudio::signalAudioIsNotReady);
+}
+
+void VsAudio::refreshDevices(bool block)
+{
+    if (!getUseRtAudio())
+        return;
+    emit signalRefreshDevices();
+    if (!block)
+        return;
+    WaitForSignal(m_audioWorkerPtr.get(), &VsAudioWorker::signalDevicesValidated);
+}
+
+void VsAudio::validateDevices(bool block)
+{
+    if (!getUseRtAudio())
+        return;
+    emit signalValidateDevices();
+    if (!block)
+        return;
+    WaitForSignal(m_audioWorkerPtr.get(), &VsAudioWorker::signalDevicesValidated);
+}
+
+void VsAudio::loadSettings()
+{
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("Audio"));
+    setInputVolume(settings.value(QStringLiteral("InMultiplier"), 1).toFloat());
+    setOutputVolume(settings.value(QStringLiteral("OutMultiplier"), 1).toFloat());
+    setMonitorVolume(settings.value(QStringLiteral("MonMultiplier"), 0).toFloat());
+    // note: we should always reset input muted to false; otherwise, bad things
+    setInputMuted(false);
+    // setInputMuted(settings.value(QStringLiteral("InMuted"), false).toBool());
+
+    // load audio backend
+    AudioBackendType audioBackend;
+    if constexpr (isBackendAvailable<AudioInterfaceMode::ALL>()) {
+        audioBackend =
+            (settings.value(QStringLiteral("Backend"), AudioBackendType::RTAUDIO).toInt()
+             == 1)
+                ? AudioBackendType::RTAUDIO
+                : AudioBackendType::JACK;
+    } else if constexpr (isBackendAvailable<AudioInterfaceMode::RTAUDIO>()) {
+        audioBackend = AudioBackendType::RTAUDIO;
+    } else {
+        audioBackend = AudioBackendType::JACK;
+    }
+    if (audioBackend != m_backend) {
+        setAudioBackend(audioBackend == AudioBackendType::RTAUDIO
+                            ? QStringLiteral("RtAudio")
+                            : QStringLiteral("JACK"));
+    }
+
+    // load input and output devices
+    QString inputDevice  = settings.value(QStringLiteral("InputDevice"), "").toString();
+    QString outputDevice = settings.value(QStringLiteral("OutputDevice"), "").toString();
+    if (inputDevice == QStringLiteral("(default)")) {
+        inputDevice = "";
+    }
+    if (outputDevice == QStringLiteral("(default)")) {
+        outputDevice = "";
+    }
+    setInputDevice(inputDevice);
+    setOutputDevice(outputDevice);
+
+    // use default base channel 0, if the setting does not exist
+    setBaseInputChannel(settings.value(QStringLiteral("BaseInputChannel"), 0).toInt());
+    setBaseOutputChannel(settings.value(QStringLiteral("BaseOutputChannel"), 0).toInt());
+
+    // Handle migration scenarios. Assume this is a new user
+    // if we have m_inputDevice == "" and m_outputDevice == ""
+    if (m_inputDevice == "" && m_outputDevice == "") {
+        // for fresh installs, use mono by default
+        setNumInputChannels(
+            settings.value(QStringLiteral("NumInputChannels"), 1).toInt());
+        setInputMixMode(settings
+                            .value(QStringLiteral("InputMixMode"),
+                                   static_cast<int>(AudioInterface::MONO))
+                            .toInt());
+
+        // use 2 channels for output
+        setNumOutputChannels(
+            settings.value(QStringLiteral("NumOutputChannels"), 2).toInt());
+    } else {
+        // existing installs - keep using stereo
+        setNumInputChannels(
+            settings.value(QStringLiteral("NumInputChannels"), 2).toInt());
+        setInputMixMode(settings
+                            .value(QStringLiteral("InputMixMode"),
+                                   static_cast<int>(AudioInterface::STEREO))
+                            .toInt());
+
+        // use 2 channels for output
+        setNumOutputChannels(
+            settings.value(QStringLiteral("NumOutputChannels"), 2).toInt());
+    }
+
+    setBufferSize(settings.value(QStringLiteral("BufferSize"), 128).toInt());
+    int buffer_strategy = settings.value(QStringLiteral("BufferStrategy"), 2).toInt();
+    if (buffer_strategy == 3 || buffer_strategy == 4)
+        buffer_strategy = 2;
+    setBufferStrategy(buffer_strategy);
+    setFeedbackDetectionEnabled(
+        settings.value(QStringLiteral("FeedbackDetectionEnabled"), true).toBool());
+    settings.endGroup();
+}
+
+void VsAudio::saveSettings()
+{
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("Audio"));
+    settings.setValue(QStringLiteral("InMultiplier"), m_inMultiplier);
+    settings.setValue(QStringLiteral("OutMultiplier"), m_outMultiplier);
+    settings.setValue(QStringLiteral("MonMultiplier"), m_monMultiplier);
+    // settings.setValue(QStringLiteral("InMuted"), m_inMuted);
+    settings.setValue(QStringLiteral("Backend"), getUseRtAudio() ? 1 : 0);
+    settings.setValue(QStringLiteral("InputDevice"), m_inputDevice);
+    settings.setValue(QStringLiteral("OutputDevice"), m_outputDevice);
+    settings.setValue(QStringLiteral("BaseInputChannel"), m_baseInputChannel);
+    settings.setValue(QStringLiteral("NumInputChannels"), m_numInputChannels);
+    settings.setValue(QStringLiteral("InputMixMode"), m_inputMixMode);
+    settings.setValue(QStringLiteral("BaseOutputChannel"), m_baseOutputChannel);
+    settings.setValue(QStringLiteral("NumOutputChannels"), m_numOutputChannels);
+    settings.setValue(QStringLiteral("BufferSize"), m_audioBufferSize);
+    settings.setValue(QStringLiteral("BufferStrategy"), m_bufferStrategy);
+    settings.setValue(QStringLiteral("FeedbackDetectionEnabled"),
+                      m_feedbackDetectionEnabled);
+    settings.endGroup();
+}
+
+void VsAudio::detectedFeedbackLoop()
+{
+    setInputMuted(true);
+    setMonitorVolume(0);
+    emit feedbackDetected();
+}
+
+void VsAudio::updatedInputVuMeasurements(const float* valuesInDecibels, int numChannels)
+{
+    bool detectedClip = false;
+
+    // Always output 2 meter readings to the UI
+    for (int i = 0; i < 2; i++) {
+        // Determine decibel reading
+        float dB = m_meterMin;
+        if (i < numChannels) {
+            dB = std::max(m_meterMin, valuesInDecibels[i]);
+        }
+
+        // Produce a normalized value from 0 to 1
+        m_inputMeterLevels[i] = (dB - m_meterMin) / (m_meterMax - m_meterMin);
+
+        // Signal a clip if we haven't done so already
+        if (dB >= -0.05 && !detectedClip) {
+            m_inputClipTimer.start();
+            m_inputClipped = true;
+            emit updatedInputClipped(m_inputClipped);
+            detectedClip = true;
+        }
+    }
+
+#ifdef RT_AUDIO
+    // For certain specific cases, copy the first channel's value into the second
+    // channel's value
+    if (getUseRtAudio()
+        && ((m_inputMixMode == static_cast<int>(AudioInterface::MONO)
+             && m_numInputChannels == 1)
+            || (m_inputMixMode == static_cast<int>(AudioInterface::MIXTOMONO)
+                && m_numInputChannels == 2))) {
+        m_inputMeterLevels[1] = m_inputMeterLevels[0];
+    }
+#endif
+
+    emit updatedInputMeterLevels(m_inputMeterLevels);
+}
+
+void VsAudio::updatedOutputVuMeasurements(const float* valuesInDecibels, int numChannels)
+{
+    bool detectedClip = false;
+
+    // Always output 2 meter readings to the UI
+    for (int i = 0; i < 2; i++) {
+        // Determine decibel reading
+        float dB = m_meterMin;
+        if (i < numChannels) {
+            dB = std::max(m_meterMin, valuesInDecibels[i]);
+        }
+
+        // Produce a normalized value from 0 to 1
+        m_outputMeterLevels[i] = (dB - m_meterMin) / (m_meterMax - m_meterMin);
+
+        // Signal a clip if we haven't done so already
+        if (dB >= -0.05 && !detectedClip) {
+            m_outputClipTimer.start();
+            m_outputClipped = true;
+            emit updatedOutputClipped(m_outputClipped);
+            detectedClip = true;
+        }
+    }
+#ifdef RT_AUDIO
+    if (m_numOutputChannels == 1) {
+        m_outputMeterLevels[1] = m_outputMeterLevels[0];
+    }
+#endif
+    emit updatedOutputMeterLevels(m_outputMeterLevels);
+}
+
+void VsAudio::appendProcessPlugins(AudioInterface& audioInterface, bool forJackTrip,
+                                   int numInputChannels, int numOutputChannels)
+{
+    // Make sure clip timers are stopped
+    m_inputClipTimer.stop();
+    m_outputClipTimer.stop();
+
+    // Reset meters
+    m_inputMeterLevels[0] = m_inputMeterLevels[1] = 0;
+    m_outputMeterLevels[0] = m_outputMeterLevels[1] = 0;
+    m_inputClipped = m_outputClipped = false;
+    emit updatedInputMeterLevels(m_inputMeterLevels);
+    emit updatedOutputMeterLevels(m_outputMeterLevels);
+    emit updatedInputClipped(m_inputClipped);
+    emit updatedOutputClipped(m_outputClipped);
+    setInputMuted(false);
+
+    // Create plugins
+    m_inputMeterPluginPtr   = new Meter(numInputChannels);
+    m_outputMeterPluginPtr  = new Meter(numOutputChannels);
+    m_inputVolumePluginPtr  = new Volume(numInputChannels);
+    m_outputVolumePluginPtr = new Volume(numOutputChannels);
+
+    // initialize input and output volumes
+    m_outputVolumePluginPtr->volumeUpdated(m_outMultiplier);
+    m_inputVolumePluginPtr->volumeUpdated(m_inMultiplier);
+    m_inputVolumePluginPtr->muteUpdated(m_inMuted);
+
+    // Connect plugins for communication with UI
+    connect(m_inputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
+            &VsAudio::updatedInputVuMeasurements);
+    connect(m_outputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
+            &VsAudio::updatedOutputVuMeasurements);
+    connect(this, &VsAudio::updatedInputVolume, m_inputVolumePluginPtr,
+            &Volume::volumeUpdated);
+    connect(this, &VsAudio::updatedOutputVolume, m_outputVolumePluginPtr,
+            &Volume::volumeUpdated);
+    connect(this, &VsAudio::updatedInputMuted, m_inputVolumePluginPtr,
+            &Volume::muteUpdated);
+
+    // Note that plugin ownership is passed to the JackTrip class
+    // In particular, the AudioInterface that it uses to connect
+    audioInterface.appendProcessPluginToNetwork(m_inputVolumePluginPtr);
+    audioInterface.appendProcessPluginToNetwork(m_inputMeterPluginPtr);
+
+    if (forJackTrip) {
+        // plugins for stream going to audio interface
+        audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr);
+
+        // Setup monitor
+        // Note: Constructor determines how many internal monitor buffers to allocate
+        m_monitorPluginPtr = new Monitor(std::max(numInputChannels, numOutputChannels));
+        m_monitorPluginPtr->volumeUpdated(m_monMultiplier);
+        audioInterface.appendProcessPluginToMonitor(m_monitorPluginPtr);
+        connect(this, &VsAudio::updatedMonitorVolume, m_monitorPluginPtr,
+                &Monitor::volumeUpdated);
+
+#ifndef NO_FEEDBACK
+        // Setup output analyzer
+        if (m_feedbackDetectionEnabled) {
+            m_outputAnalyzerPluginPtr = new Analyzer(numOutputChannels);
+            m_outputAnalyzerPluginPtr->setIsMonitoringAnalyzer(true);
+            audioInterface.appendProcessPluginToMonitor(m_outputAnalyzerPluginPtr);
+            connect(m_outputAnalyzerPluginPtr, &Analyzer::signalFeedbackDetected, this,
+                    &VsAudio::detectedFeedbackLoop);
+        }
+#endif
+
+        // Setup output meter
+        // Note: Add this to monitor process to include self-volume
+        m_outputMeterPluginPtr->setIsMonitoringMeter(true);
+        audioInterface.appendProcessPluginToMonitor(m_outputMeterPluginPtr);
+
+    } else {
+        // tone plugin is used to test audio output
+        Tone* outputTonePluginPtr = new Tone(getNumOutputChannels());
+        connect(this, &VsAudio::signalPlayOutputAudio, outputTonePluginPtr,
+                &Tone::triggerPlayback);
+        audioInterface.appendProcessPluginFromNetwork(outputTonePluginPtr);
+
+        // plugins for stream going to audio interface
+        audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr);
+        audioInterface.appendProcessPluginFromNetwork(m_outputMeterPluginPtr);
+    }
+}
+
+void VsAudio::setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel)
+{
+    m_inputComboModel  = inputComboModel;
+    m_outputComboModel = outputComboModel;
+    emit inputComboModelChanged();
+    emit outputComboModelChanged();
+    if (!m_deviceModelsInitialized) {
+        m_deviceModelsInitialized = true;
+        emit deviceModelsInitializedChanged(true);
+    }
+}
+
+void VsAudio::setInputChannelsComboModel(QJsonArray& model)
+{
+    m_inputChannelsComboModel = model;
+    emit inputChannelsComboModelChanged();
+}
+
+void VsAudio::setOutputChannelsComboModel(QJsonArray& model)
+{
+    m_outputChannelsComboModel = model;
+    emit outputChannelsComboModelChanged();
+}
+
+void VsAudio::setInputMixModeComboModel(QJsonArray& model)
+{
+    m_inputMixModeComboModel = model;
+    emit inputMixModeComboModelChanged();
+}
+
+void VsAudio::updateDeviceMessages(AudioInterface& audioInterface)
+{
+    QString devicesWarningMsg =
+        QString::fromStdString(audioInterface.getDevicesWarningMsg());
+    QString devicesErrorMsg = QString::fromStdString(audioInterface.getDevicesErrorMsg());
+    QString devicesWarningHelpUrl =
+        QString::fromStdString(audioInterface.getDevicesWarningHelpUrl());
+    QString devicesErrorHelpUrl =
+        QString::fromStdString(audioInterface.getDevicesErrorHelpUrl());
+
+    if (devicesWarningMsg != "") {
+        qDebug() << "Devices Warning: " << devicesWarningMsg;
+        if (devicesWarningHelpUrl != "") {
+            qDebug() << "Learn More: " << devicesWarningHelpUrl;
+        }
+    }
+
+    if (devicesErrorMsg != "") {
+        qDebug() << "Devices Error: " << devicesErrorMsg;
+        if (devicesErrorHelpUrl != "") {
+            qDebug() << "Learn More: " << devicesErrorHelpUrl;
+        }
+    }
+
+    setDevicesWarningMsg(devicesWarningMsg);
+    setDevicesErrorMsg(devicesErrorMsg);
+    setDevicesWarningHelpUrl(devicesWarningHelpUrl);
+    setDevicesErrorHelpUrl(devicesErrorHelpUrl);
+    setHighLatencyFlag(audioInterface.getHighLatencyFlag());
+}
+
+AudioInterface* VsAudio::newAudioInterface(JackTrip* jackTripPtr)
+{
+    // Create AudioInterface Client Object
+    AudioInterface* ifPtr = nullptr;
+    if (m_backend == VsAudio::AudioBackendType::JACK) {
+        if (!isBackendAvailable<AudioInterfaceMode::JACK>()) {
+            throw std::runtime_error(
+                "JackTrip was not compiled with support for the Jack backend. "
+                "In order to use Jack, you'll need to "
+                "rebuild with Jack support.");
+        }
+        if (!jackIsAvailable()) {
+            throw std::runtime_error(
+                "Unable to load the Jack client library. "
+                "In order to use Jack, you'll need to first install it.");
+        }
+        qDebug() << "Using JACK backend";
+        ifPtr = newJackAudioInterface(jackTripPtr);
+    } else if (m_backend == VsAudio::AudioBackendType::RTAUDIO) {
+        if (!isBackendAvailable<AudioInterfaceMode::RTAUDIO>()) {
+            throw std::runtime_error(
+                "JackTrip was not compiled with support for the RtAudio backend. "
+                "In order to use RtAudio, you'll need to "
+                "rebuild with RtAudio support.");
+        }
+        qDebug() << "Using RtAudio backend";
+        ifPtr = newRtAudioInterface(jackTripPtr);
+    } else {
+        throw std::runtime_error("Unknown audio backend");
+    }
+
+    mHasErrors = false;
+    ifPtr->setErrorCallback([this, jackTripPtr](const std::string& errorText) {
+        this->errorCallback(errorText, jackTripPtr);
+    });
+
+    // AudioInterface::setup() can return a different buffer size
+    // if the audio interface doesn't support the one that was requested
+    if (ifPtr->getBufferSizeInSamples() != uint32_t(getBufferSize())) {
+        setBufferSize(ifPtr->getBufferSizeInSamples());
+    }
+
+    std::cout << "The Sampling Rate is: " << ifPtr->getSampleRate() << std::endl;
+    std::cout << gPrintSeparator << std::endl;
+    int AudioBufferSizeInBytes = ifPtr->getBufferSizeInSamples() * sizeof(sample_t);
+    std::cout << "The Audio Buffer Size is: " << ifPtr->getBufferSizeInSamples()
+              << " samples" << std::endl;
+    std::cout << "                      or: " << AudioBufferSizeInBytes << " bytes"
+              << std::endl;
+    std::cout << gPrintSeparator << std::endl;
+    std::cout << "The Number of Channels is: " << ifPtr->getNumInputChannels()
+              << std::endl;
+    std::cout << gPrintSeparator << std::endl;
+
+    // setup audio plugins
+    appendProcessPlugins(*ifPtr, jackTripPtr != nullptr, getNumInputChannels(),
+                         getNumOutputChannels());
+
+    return ifPtr;
+}
+
+AudioInterface* VsAudio::newJackAudioInterface([[maybe_unused]] JackTrip* jackTripPtr)
+{
+    AudioInterface* ifPtr = nullptr;
+#ifndef NO_JACK
+    static const int numJackChannels = 2;
+    if constexpr (isBackendAvailable<AudioInterfaceMode::ALL>()
+                  || isBackendAvailable<AudioInterfaceMode::JACK>()) {
+        QVarLengthArray<int> inputChans;
+        QVarLengthArray<int> outputChans;
+        inputChans.resize(numJackChannels);
+        outputChans.resize(numJackChannels);
+
+        for (int i = 0; i < numJackChannels; i++) {
+            inputChans[i] = 1 + i;
+        }
+        for (int i = 0; i < numJackChannels; i++) {
+            outputChans[i] = 1 + i;
+        }
+
+        ifPtr = new JackAudioInterface(inputChans, outputChans, m_audioBitResolution,
+                                       jackTripPtr != nullptr, jackTripPtr);
+        ifPtr->setClientName(QStringLiteral("JackTrip"));
+#if defined(__unix__)
+        AudioInterface::setPipewireLatency(getBufferSize(), getSampleRate());
+#endif
+        ifPtr->setup(true);
+    }
+#endif
+    return ifPtr;
+}
+
+AudioInterface* VsAudio::newRtAudioInterface([[maybe_unused]] JackTrip* jackTripPtr)
+{
+    AudioInterface* ifPtr = nullptr;
+#ifdef RT_AUDIO
+    QVarLengthArray<int> inputChans;
+    QVarLengthArray<int> outputChans;
+    inputChans.resize(getNumInputChannels());
+    outputChans.resize(getNumOutputChannels());
+
+    for (int i = 0; i < getNumInputChannels(); i++) {
+        inputChans[i] = getBaseInputChannel() + i;
+    }
+    for (int i = 0; i < getNumOutputChannels(); i++) {
+        outputChans[i] = getBaseOutputChannel() + i;
+    }
+
+    ifPtr = new RtAudioInterface(
+        inputChans, outputChans,
+        static_cast<AudioInterface::inputMixModeT>(getInputMixMode()),
+        m_audioBitResolution, jackTripPtr != nullptr, jackTripPtr);
+    ifPtr->setSampleRate(getSampleRate());
+    ifPtr->setInputDevice(getInputDevice().toStdString());
+    ifPtr->setOutputDevice(getOutputDevice().toStdString());
+    ifPtr->setBufferSizeInSamples(getBufferSize());
+
+    QVector<RtAudioDevice> devices = m_audioWorkerPtr->getDevices();
+    if (!devices.empty())
+        static_cast<RtAudioInterface*>(ifPtr)->setRtAudioDevices(devices);
+
+#if defined(__unix__)
+    AudioInterface::setPipewireLatency(getBufferSize(), ifPtr->getSampleRate());
+#endif
+
+    // Note: setup might change the number of channels and/or buffer size
+    ifPtr->setup(true);
+
+    // TODO: Add check for if base input channel needs to change
+    if (jackTripPtr != nullptr && getNumInputChannels() == 2
+        && getInputMixMode() == AudioInterface::MIXTOMONO)
+        jackTripPtr->setNumInputChannels(1);
+
+#endif  // RT_AUDIO
+    return ifPtr;
+}
+
+void VsAudio::errorCallback(const std::string& errorText,
+                            [[maybe_unused]] JackTrip* jackTripPtr)
+{
+    const QString errorMsg(QString::fromStdString(errorText));
+    setDevicesErrorMsg(errorMsg);
+#ifdef _WIN32
+    // handle special case for Windows ASIO drivers that trigger
+    // asynchronous errors shortly after you try opening the
+    // RtAudio stream with a different sample rate (only for audio tester)
+    if (jackTripPtr == nullptr && getUseRtAudio()
+        && errorMsg.contains("sample rate changed")) {
+        // only refresh devices once
+        if (mHasErrors)
+            return;
+        mHasErrors = true;
+        // asynchronously refresh devices
+        refreshDevices(false);
+    }
+#else
+    mHasErrors = true;
+#endif
+}
+
+// VsAudioWorker methods
+
+VsAudioWorker::VsAudioWorker(VsAudio* ptr) : m_parentPtr(ptr) {}
+
+void VsAudioWorker::openAudioInterface()
+{
+#ifdef __APPLE__
+    if (m_parentPtr->m_permissionsPtr->micPermission() != "granted") {
+        return;
+    }
+#endif
+
+    if constexpr (!(isBackendAvailable<AudioInterfaceMode::JACK>()
+                    || isBackendAvailable<AudioInterfaceMode::RTAUDIO>())) {
+        return;
+    }
+
+    if (!m_audioInterfacePtr.isNull()) {
+        std::cout << "Restarting Audio" << std::endl;
+        closeAudioInterface();
+    } else {
+        std::cout << "Starting Audio" << std::endl;
+    }
+
+    unsigned int maxTries = 2;
+#ifdef RT_AUDIO
+    // Update devices, if not already initialized
+    if (getUseRtAudio()) {
+        if (!m_parentPtr->getDeviceModelsInitialized()) {
+            updateDeviceModels();
+            maxTries = 1;
+        }
+    }
+#endif
+    for (unsigned int tryNum = 0; tryNum < maxTries; ++tryNum) {
+#ifdef RT_AUDIO
+        if (tryNum > 0) {
+            if (getUseRtAudio()) {
+                updateDeviceModels();
+            } else {
+                m_parentPtr->setAudioBackend("RtAudio");
+                updateDeviceModels();
+            }
+        }
+#endif
+        try {
+            // create and setup a new audio interface
+            m_audioInterfacePtr.reset(m_parentPtr->newAudioInterface());
+            // success if it doesn't throw
+            break;
+        } catch (const std::exception& e) {
+            emit signalError(QString::fromUtf8(e.what()));
+        }
+    }
+
+    if (m_audioInterfacePtr.isNull()) {
+        return;
+    }
+
+    // initialize plugins and start the audio callback process
+    m_audioInterfacePtr->initPlugins(false);
+    m_audioInterfacePtr->startProcess();
+    m_audioInterfacePtr->connectDefaultPorts();
+
+    m_parentPtr->updateDeviceMessages(*m_audioInterfacePtr);
+    m_parentPtr->setAudioReady(true);
+}
+
+void VsAudioWorker::closeAudioInterface()
+{
+    if (m_audioInterfacePtr.isNull())
+        return;
+    std::cout << "Stopping Audio" << std::endl;
+    try {
+        m_audioInterfacePtr->stopProcess();
+    } catch (const std::exception& e) {
+        emit signalError(QString::fromUtf8(e.what()));
+    }
+    m_audioInterfacePtr.clear();
+    m_parentPtr->setAudioReady(false);
+}
+
+#ifdef RT_AUDIO
+
+void VsAudioWorker::refreshDevices()
+{
+    if (!getUseRtAudio())
+        return;
+    bool restartAudio = !m_audioInterfacePtr.isNull();
+    if (restartAudio)
+        closeAudioInterface();
+    updateDeviceModels();
+    if (restartAudio)
+        openAudioInterface();
+}
+
+void VsAudioWorker::updateDeviceModels()
+{
+    if (!getUseRtAudio())
+        return;
+
+    // note: audio must not be active when scanning devices
+    m_parentPtr->setScanningDevices(true);
+    closeAudioInterface();
+    RtAudioInterface::scanDevices(m_devices);
+
+    QStringList inputDeviceCategories;
+    QStringList outputDeviceCategories;
+
+    getDeviceList(m_devices, m_inputDeviceList, inputDeviceCategories,
+                  m_inputDeviceChannels, true);
+    getDeviceList(m_devices, m_outputDeviceList, outputDeviceCategories,
+                  m_outputDeviceChannels, false);
+
+    QJsonArray inputComboModel =
+        formatDeviceList(m_inputDeviceList, inputDeviceCategories, m_inputDeviceChannels);
+    QJsonArray outputComboModel = formatDeviceList(
+        m_outputDeviceList, outputDeviceCategories, m_outputDeviceChannels);
+
+    validateDevices();
+
+    // let VsAudio know that things have been updated
+    m_parentPtr->setScanningDevices(false);
+    emit signalUpdatedDeviceModels(inputComboModel, outputComboModel);
+}
+
+void VsAudioWorker::getDeviceList(const QVector<RtAudioDevice>& devices,
+                                  QStringList& list, QStringList& categories,
+                                  QList<int>& channels, bool isInput)
+{
+    categories.clear();
+    channels.clear();
+    list.clear();
+
+    // do not include blacklisted audio interfaces
+    // these are known to be unstable and cause JackTrip to crash
+    QVector<QString> blacklisted_devices = {
+#ifdef _WIN32
+        // Realtek ASIO: seems to crash any computer that tries to use it
+        QString::fromUtf8("Realtek ASIO"),
+        QString::fromUtf8("Generic Low Latency ASIO Driver"),
+#endif
+        // JackRouter: crashes if not running; use Jack backend instead
+        QString::fromUtf8("JackRouter"),
+    };
+
+    for (int n = 0; n < devices.size(); ++n) {
+#ifdef _WIN32
+        if (devices[n].api == RtAudio::UNIX_JACK) {
+            continue;
+        }
+#endif
+        const QString deviceName(QString::fromStdString(devices[n].name));
+
+        // Don't include duplicate entries
+        if (list.contains(deviceName)) {
+            continue;
+        }
+
+        // Skip if no channels available
+        if ((isInput && devices[n].inputChannels == 0)
+            || (!isInput && devices[n].outputChannels == 0)) {
+            continue;
+        }
+
+        // Skip blacklisted devices
+        if (blacklisted_devices.contains(deviceName)) {
+            std::cout << "RTAudio: blacklisted " << (isInput ? "input" : "output")
+                      << " device: " << devices[n].name << std::endl;
+            continue;
+        }
+
+        // Good to go!
+        if (isInput) {
+            list.append(deviceName);
+            channels.append(devices[n].inputChannels);
+        } else {
+            list.append(deviceName);
+            channels.append(devices[n].outputChannels);
+        }
+
+        switch (devices[n].api) {
+        case RtAudio::WINDOWS_ASIO:
+            categories.append("Low-Latency (ASIO)");
+            break;
+        case RtAudio::WINDOWS_WASAPI:
+            categories.append("High-Latency (WASAPI)");
+            break;
+        case RtAudio::WINDOWS_DS:
+            categories.append("High-Latency (DirectSound)");
+            break;
+        case RtAudio::LINUX_ALSA:
+            categories.append("Low-Latency (ALSA)");
+            break;
+        case RtAudio::LINUX_PULSE:
+            categories.append("High-Latency (Pulse)");
+            break;
+        case RtAudio::LINUX_OSS:
+            categories.append("High-Latency (OSS)");
+            break;
+        default:
+            categories.append("");
+            break;
+        }
+    }
+}
+
+QJsonArray VsAudioWorker::formatDeviceList(const QStringList& devices,
+                                           const QStringList& categories,
+                                           const QList<int>& channels)
+{
+    QStringList uniqueCategories = QStringList(categories);
+    uniqueCategories.removeDuplicates();
+
+    bool containsCategories = true;
+    if (uniqueCategories.size() == 0) {
+        containsCategories = false;
+    } else if (uniqueCategories.size() == 1 && uniqueCategories.at(0) == "") {
+        containsCategories = false;
+    }
+
+    QJsonArray items;
+    for (int i = 0; i < uniqueCategories.size(); i++) {
+        QString category = uniqueCategories.at(i);
+
+        if (containsCategories) {
+            QJsonObject header = QJsonObject();
+            header.insert(QString::fromStdString("text"), category);
+            header.insert(QString::fromStdString("type"),
+                          QString::fromStdString("header"));
+            header.insert(QString::fromStdString("category"), category);
+            items.push_back(header);
+        }
+
+        for (int j = 0; j < devices.size(); j++) {
+            if (categories.at(j).toStdString() == category.toStdString()) {
+                QJsonObject element = QJsonObject();
+                element.insert(QString::fromStdString("text"), devices.at(j));
+                element.insert(QString::fromStdString("type"),
+                               QString::fromStdString("element"));
+                element.insert(QString::fromStdString("channels"), channels.at(j));
+                element.insert(QString::fromStdString("category"), category);
+                items.push_back(element);
+            }
+        }
+    }
+
+    return items;
+}
+
+void VsAudioWorker::validateDevices()
+{
+    if (!getUseRtAudio())
+        return;
+    validateInputDevicesState();
+    validateOutputDevicesState();
+    emit signalDevicesValidated();
+}
+
+void VsAudioWorker::validateInputDevicesState()
+{
+    if (!getUseRtAudio()) {
+        return;
+    }
+    if (m_inputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) {
+        return;
+    }
+
+    // Given input device list, check that the currently set device
+    // actually exists
+    if (getInputDevice() == QStringLiteral("")
+        || m_inputDeviceList.indexOf(getInputDevice()) == -1) {
+        m_parentPtr->setInputDevice(m_inputDeviceList[0]);
+    }
+
+    // Given the currently selected input device, reset the available input channel
+    // options
+    int indexOfInput = m_inputDeviceList.indexOf(getInputDevice());
+    if (indexOfInput == -1) {
+        std::cerr << "Invalid state. Input device index should never be -1" << std::endl;
+        return;
+    }
+
+    int numDevicesChannelsAvailable = m_inputDeviceChannels.at(indexOfInput);
+    if (numDevicesChannelsAvailable < 1) {
+        std::cerr << "Invalid state. Number of channels should never be less than 1"
+                  << std::endl;
+        return;
+    } else if (numDevicesChannelsAvailable == 1) {
+        // Set the input mix mode to just have "Mono" as the option
+        QJsonObject inputMixModeComboElement = QJsonObject();
+        inputMixModeComboElement.insert(QString::fromStdString("label"),
+                                        QString::fromStdString("Mono"));
+        inputMixModeComboElement.insert(QString::fromStdString("value"),
+                                        static_cast<int>(AudioInterface::MONO));
+        QJsonArray inputMixModeComboModel;
+        inputMixModeComboModel.push_back(inputMixModeComboElement);
+        m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
+
+        // Set the input channels combo to only have channel 1 as an option
+        QJsonObject inputChannelsComboElement;
+        inputChannelsComboElement.insert(QString::fromStdString("label"),
+                                         QString::fromStdString("1"));
+        inputChannelsComboElement.insert(QString::fromStdString("baseChannel"),
+                                         QVariant(0).toInt());
+        inputChannelsComboElement.insert(QString::fromStdString("numChannels"),
+                                         QVariant(1).toInt());
+        QJsonArray inputChannelsComboModel;
+        inputChannelsComboModel.push_back(inputChannelsComboElement);
+        m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel);
+
+        // Set the only allowed options for these variables automatically
+        m_parentPtr->setBaseInputChannel(0);
+        m_parentPtr->setNumInputChannels(1);
+        m_parentPtr->setInputMixMode(static_cast<int>(AudioInterface::MONO));
+    } else {
+        // set the input channels selector to have the options based on the currently
+        // selected device
+        QJsonArray inputChannelsComboModel;
+        for (int i = 0; i < numDevicesChannelsAvailable; i++) {
+            QJsonObject element = QJsonObject();
+            element.insert(QString::fromStdString("label"), QVariant(i + 1).toString());
+            element.insert(QString::fromStdString("baseChannel"), QVariant(i).toInt());
+            element.insert(QString::fromStdString("numChannels"), QVariant(1).toInt());
+            inputChannelsComboModel.push_back(element);
+        }
+        for (int i = 0; i < numDevicesChannelsAvailable; i++) {
+            if (i % 2 == 0) {
+                QJsonObject element = QJsonObject();
+                element.insert(
+                    QString::fromStdString("label"),
+                    QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString());
+                element.insert(QString::fromStdString("baseChannel"),
+                               QVariant(i).toInt());
+                element.insert(QString::fromStdString("numChannels"),
+                               QVariant(2).toInt());
+                inputChannelsComboModel.push_back(element);
+            }
+        }
+        m_parentPtr->setInputChannelsComboModel(inputChannelsComboModel);
+
+        // if the current m_baseInputChannel or m_numInputChannels is invalid based on
+        // this device's option, use the first two channels by default
+        if (getBaseInputChannel() + getNumInputChannels() > numDevicesChannelsAvailable) {
+            // we're in the case where numDevicesChannelsAvailable >= 2, so always have
+            // the ability to use the first 2 channels
+            m_parentPtr->setBaseInputChannel(0);
+            m_parentPtr->setNumInputChannels(2);
+        }
+        if (getNumInputChannels() != 1) {
+            // Set the input mix mode to have two options: "Stereo" and "Mix to Mono" if
+            // we're using 2 channels
+            QJsonObject inputMixModeComboElement1 = QJsonObject();
+            inputMixModeComboElement1.insert(QString::fromStdString("label"),
+                                             QString::fromStdString("Stereo"));
+            inputMixModeComboElement1.insert(QString::fromStdString("value"),
+                                             static_cast<int>(AudioInterface::STEREO));
+            QJsonObject inputMixModeComboElement2 = QJsonObject();
+            inputMixModeComboElement2.insert(QString::fromStdString("label"),
+                                             QString::fromStdString("Mix to Mono"));
+            inputMixModeComboElement2.insert(QString::fromStdString("value"),
+                                             static_cast<int>(AudioInterface::MIXTOMONO));
+            QJsonArray inputMixModeComboModel;
+            inputMixModeComboModel.push_back(inputMixModeComboElement1);
+            inputMixModeComboModel.push_back(inputMixModeComboElement2);
+            m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
+
+            // if m_inputMixMode is an invalid value, set it to "stereo" by default
+            // given that we are using 2 channels
+            if (getInputMixMode() != static_cast<int>(AudioInterface::STEREO)
+                && getInputMixMode() != static_cast<int>(AudioInterface::MIXTOMONO)) {
+                m_parentPtr->setInputMixMode(static_cast<int>(AudioInterface::STEREO));
+            }
+        } else {
+            // Set the input mix mode to just have "Mono" as the option if we're using 1
+            // channel
+            QJsonObject inputMixModeComboElement = QJsonObject();
+            inputMixModeComboElement.insert(QString::fromStdString("label"),
+                                            QString::fromStdString("Mono"));
+            inputMixModeComboElement.insert(QString::fromStdString("value"),
+                                            static_cast<int>(AudioInterface::MONO));
+            QJsonArray inputMixModeComboModel;
+            inputMixModeComboModel.push_back(inputMixModeComboElement);
+            m_parentPtr->setInputMixModeComboModel(inputMixModeComboModel);
+
+            // if m_inputMixMode is an invalid value, set it to AudioInterface::MONO
+            if (getInputMixMode() != static_cast<int>(AudioInterface::MONO)) {
+                m_parentPtr->setInputMixMode(static_cast<int>(AudioInterface::MONO));
+            }
+        }
+    }
+}
+
+void VsAudioWorker::validateOutputDevicesState()
+{
+    if (!getUseRtAudio()) {
+        return;
+    }
+    if (m_outputDeviceList.size() == 0 || m_outputDeviceList.size() == 0) {
+        return;
+    }
+
+    // Given output device list, check that the currently set device
+    // actually exists
+    if (getOutputDevice() == QStringLiteral("")
+        || m_outputDeviceList.indexOf(getOutputDevice()) == -1) {
+        m_parentPtr->setOutputDevice(m_outputDeviceList[0]);
+    }
+
+    // Given the currently selected output device, reset the available output channel
+    // options
+    int indexOfOutput = m_outputDeviceList.indexOf(getOutputDevice());
+    if (indexOfOutput == -1) {
+        std::cerr << "Invalid state. Output device index should never be -1" << std::endl;
+        return;
+    }
+
+    int numDevicesChannelsAvailable = m_outputDeviceChannels.at(indexOfOutput);
+    if (numDevicesChannelsAvailable < 1) {
+        std::cerr << "Invalid state. Number of channels should never be less than 1"
+                  << std::endl;
+        return;
+    } else if (numDevicesChannelsAvailable == 1) {
+        // Set the output channels combo to only have channel 1 as an option
+        QJsonObject outputChannelsComboElement = QJsonObject();
+        outputChannelsComboElement.insert(QString::fromStdString("label"),
+                                          QString::fromStdString("1"));
+        outputChannelsComboElement.insert(QString::fromStdString("baseChannel"),
+                                          QVariant(0).toInt());
+        outputChannelsComboElement.insert(QString::fromStdString("numChannels"),
+                                          QVariant(1).toInt());
+        QJsonArray outputChannelsComboModel;
+        outputChannelsComboModel.push_back(outputChannelsComboElement);
+        m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel);
+
+        // Set the only allowed options for these variables automatically
+        m_parentPtr->setBaseOutputChannel(0);
+        m_parentPtr->setNumOutputChannels(1);
+    } else {
+        // set the output channels selector to have the options based on the currently
+        // selected device
+        QJsonArray outputChannelsComboModel;
+        for (int i = 0; i < numDevicesChannelsAvailable; i++) {
+            if (i % 2 == 0) {
+                QJsonObject element = QJsonObject();
+                element.insert(
+                    QString::fromStdString("label"),
+                    QVariant(i + 1).toString() + " & " + QVariant(i + 2).toString());
+                element.insert(QString::fromStdString("baseChannel"),
+                               QVariant(i).toInt());
+                element.insert(QString::fromStdString("numChannels"),
+                               QVariant(2).toInt());
+                outputChannelsComboModel.push_back(element);
+            }
+        }
+        m_parentPtr->setOutputChannelsComboModel(outputChannelsComboModel);
+
+        // if the current m_baseOutputChannel or m_numOutputChannels is invalid based on
+        // this device's option, use the first two channels by default
+        if (getBaseOutputChannel() + getNumOutputChannels()
+            > numDevicesChannelsAvailable) {
+            // we're in the case where numDevicesChannelsAvailable >= 2, so always have
+            // the ability to use the first 2 channels
+            m_parentPtr->setBaseOutputChannel(0);
+            m_parentPtr->setNumOutputChannels(2);
+        }
+    }
+}
+
+#endif  // RT_AUDIO
diff --git a/src/gui/vsAudio.h b/src/gui/vsAudio.h
new file mode 100644 (file)
index 0000000..ab53fb6
--- /dev/null
@@ -0,0 +1,454 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsAudio.h
+ * \author Matt Horton
+ * \date September 2022
+ */
+
+#ifndef VSDAUDIO_H
+#define VSDAUDIO_H
+
+#include <QJsonArray>
+#include <QList>
+#include <QObject>
+#include <QSharedPointer>
+#include <QString>
+#include <QStringList>
+#include <QTimer>
+
+#include "../AudioInterface.h"
+#include "../jacktrip_globals.h"
+
+#ifdef RT_AUDIO
+#include "../RtAudioInterface.h"
+#endif
+
+class Analyzer;
+class JackTrip;
+class Meter;
+class Monitor;
+class QThread;
+class Tone;
+class Volume;
+class VsPermissions;
+class VsAudioWorker;
+
+class VsAudio : public QObject
+{
+    Q_OBJECT
+
+    // state shared with QML
+    Q_PROPERTY(bool audioReady READ getAudioReady NOTIFY signalAudioReadyChanged)
+    Q_PROPERTY(bool scanningDevices READ getScanningDevices WRITE setScanningDevices
+                   NOTIFY signalScanningDevicesChanged)
+    Q_PROPERTY(bool feedbackDetectionEnabled READ getFeedbackDetectionEnabled WRITE
+                   setFeedbackDetectionEnabled NOTIFY feedbackDetectionEnabledChanged)
+    Q_PROPERTY(bool deviceModelsInitialized READ getDeviceModelsInitialized NOTIFY
+                   deviceModelsInitializedChanged)
+    Q_PROPERTY(bool backendAvailable READ backendAvailable CONSTANT)
+    Q_PROPERTY(QString audioBackend READ getAudioBackend WRITE setAudioBackend NOTIFY
+                   audioBackendChanged)
+    Q_PROPERTY(
+        int sampleRate READ getSampleRate WRITE setSampleRate NOTIFY sampleRateChanged)
+    Q_PROPERTY(
+        int bufferSize READ getBufferSize WRITE setBufferSize NOTIFY bufferSizeChanged)
+    Q_PROPERTY(int bufferStrategy READ getBufferStrategy WRITE setBufferStrategy NOTIFY
+                   bufferStrategyChanged)
+    Q_PROPERTY(int numInputChannels READ getNumInputChannels WRITE setNumInputChannels
+                   NOTIFY numInputChannelsChanged)
+    Q_PROPERTY(int numOutputChannels READ getNumOutputChannels WRITE setNumOutputChannels
+                   NOTIFY numOutputChannelsChanged)
+    Q_PROPERTY(int baseInputChannel READ getBaseInputChannel WRITE setBaseInputChannel
+                   NOTIFY baseInputChannelChanged)
+    Q_PROPERTY(int baseOutputChannel READ getBaseOutputChannel WRITE setBaseOutputChannel
+                   NOTIFY baseOutputChannelChanged)
+    Q_PROPERTY(int inputMixMode READ getInputMixMode WRITE setInputMixMode NOTIFY
+                   inputMixModeChanged)
+    Q_PROPERTY(
+        bool inputMuted READ getInputMuted WRITE setInputMuted NOTIFY updatedInputMuted)
+    Q_PROPERTY(bool inputClipped READ getInputClipped NOTIFY updatedInputClipped)
+    Q_PROPERTY(bool outputClipped READ getOutputClipped NOTIFY updatedOutputClipped)
+    Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY
+                   updatedInputVolume)
+    Q_PROPERTY(float outputVolume READ getOutputVolume WRITE setOutputVolume NOTIFY
+                   updatedOutputVolume)
+    Q_PROPERTY(float monitorVolume READ getMonitorVolume WRITE setMonitorVolume NOTIFY
+                   updatedMonitorVolume)
+    Q_PROPERTY(QString inputDevice READ getInputDevice WRITE setInputDevice NOTIFY
+                   inputDeviceChanged)
+    Q_PROPERTY(QString outputDevice READ getOutputDevice WRITE setOutputDevice NOTIFY
+                   outputDeviceChanged)
+    Q_PROPERTY(QVector<float> inputMeterLevels READ getInputMeterLevels NOTIFY
+                   updatedInputMeterLevels)
+    Q_PROPERTY(QVector<float> outputMeterLevels READ getOutputMeterLevels NOTIFY
+                   updatedOutputMeterLevels)
+    Q_PROPERTY(
+        QJsonArray inputComboModel READ getInputComboModel NOTIFY inputComboModelChanged)
+    Q_PROPERTY(QJsonArray outputComboModel READ getOutputComboModel NOTIFY
+                   outputComboModelChanged)
+    Q_PROPERTY(QJsonArray inputChannelsComboModel READ getInputChannelsComboModel NOTIFY
+                   inputChannelsComboModelChanged)
+    Q_PROPERTY(QJsonArray outputChannelsComboModel READ getOutputChannelsComboModel NOTIFY
+                   outputChannelsComboModelChanged)
+    Q_PROPERTY(QJsonArray inputMixModeComboModel READ getInputMixModeComboModel NOTIFY
+                   inputMixModeComboModelChanged)
+    Q_PROPERTY(QStringList feedbackDetectionComboModel READ getFeedbackDetectionComboModel
+                   CONSTANT)
+    Q_PROPERTY(QStringList bufferSizeComboModel READ getBufferSizeComboModel CONSTANT)
+    Q_PROPERTY(
+        QStringList bufferStrategyComboModel READ getBufferStrategyComboModel CONSTANT)
+    Q_PROPERTY(QStringList audioBackendComboModel READ getAudioBackendComboModel CONSTANT)
+    Q_PROPERTY(
+        QString devicesWarning READ getDevicesWarningMsg NOTIFY devicesWarningChanged)
+    Q_PROPERTY(QString devicesError READ getDevicesErrorMsg NOTIFY devicesErrorChanged)
+    Q_PROPERTY(QString devicesWarningHelpUrl READ getDevicesWarningHelpUrl NOTIFY
+                   devicesWarningHelpUrlChanged)
+    Q_PROPERTY(QString devicesErrorHelpUrl READ getDevicesErrorHelpUrl NOTIFY
+                   devicesErrorHelpUrlChanged)
+    Q_PROPERTY(bool highLatencyFlag READ getHighLatencyFlag NOTIFY highLatencyFlagChanged)
+
+   public:
+    enum AudioBackendType {
+        JACK = 0,  ///< Jack Mode
+        RTAUDIO    ///< RtAudio Mode
+    };
+
+    // Constructor
+    explicit VsAudio(QObject* parent = nullptr);
+    virtual ~VsAudio();
+
+    // allow VirtualStudio to get Permissions to bind to QML view
+    VsPermissions& getPermissions() { return *m_permissionsPtr; }
+    VsAudioWorker& getWorker() { return *m_audioWorkerPtr; }
+
+    //  allow VirtualStudio to create new audio interfaces
+    AudioInterface* newAudioInterface(JackTrip* jackTripPtr = nullptr);
+
+    // allow VirtualStudio to load and save settings
+    void loadSettings();
+    void saveSettings();
+
+    // getters for state shared with QML
+    bool backendAvailable() const;
+    bool jackIsAvailable() const;
+    bool getAudioReady() const { return m_audioReady; }
+    bool getScanningDevices() const { return m_scanningDevices; }
+    bool getFeedbackDetectionEnabled() const { return m_feedbackDetectionEnabled; }
+    bool getDeviceModelsInitialized() const { return m_deviceModelsInitialized; }
+    bool getUseRtAudio() const { return m_backend == AudioBackendType::RTAUDIO; }
+    QString getAudioBackend() const
+    {
+        return getUseRtAudio() ? QStringLiteral("RtAudio") : QStringLiteral("JACK");
+    }
+    int getSampleRate() const { return m_audioSampleRate; }
+    int getBufferSize() const { return m_audioBufferSize; }
+    int getBufferStrategy() const { return m_bufferStrategy; }
+    int getNumInputChannels() const { return getUseRtAudio() ? m_numInputChannels : 2; }
+    int getNumOutputChannels() const { return getUseRtAudio() ? m_numOutputChannels : 2; }
+    int getBaseInputChannel() const { return getUseRtAudio() ? m_baseInputChannel : 0; }
+    int getBaseOutputChannel() const { return getUseRtAudio() ? m_baseOutputChannel : 0; }
+    int getInputMixMode() const { return getUseRtAudio() ? m_inputMixMode : 0; }
+    bool getInputMuted() const { return m_inMuted; }
+    bool getInputClipped() const { return m_inputClipped; }
+    bool getOutputClipped() const { return m_outputClipped; }
+    float getInputVolume() const { return m_inMultiplier; }
+    float getOutputVolume() const { return m_outMultiplier; }
+    float getMonitorVolume() const { return m_monMultiplier; }
+    const QString& getInputDevice() const { return m_inputDevice; }
+    const QString& getOutputDevice() const { return m_outputDevice; }
+    const QVector<float>& getInputMeterLevels() const { return m_inputMeterLevels; }
+    const QVector<float>& getOutputMeterLevels() const { return m_outputMeterLevels; }
+    const QJsonArray& getInputComboModel() const { return m_inputComboModel; }
+    const QJsonArray& getOutputComboModel() const { return m_outputComboModel; }
+    const QJsonArray& getInputChannelsComboModel() const
+    {
+        return m_inputChannelsComboModel;
+    }
+    const QJsonArray& getOutputChannelsComboModel() const
+    {
+        return m_outputChannelsComboModel;
+    }
+    const QJsonArray& getInputMixModeComboModel() const
+    {
+        return m_inputMixModeComboModel;
+    }
+    const QStringList& getFeedbackDetectionComboModel() const
+    {
+        return m_feedbackDetectionComboModel;
+    }
+    const QStringList& getBufferSizeComboModel() const { return m_bufferSizeComboModel; }
+    const QStringList& getBufferStrategyComboModel() const
+    {
+        return m_bufferStrategyComboModel;
+    }
+    const QStringList& getAudioBackendComboModel() const
+    {
+        return m_audioBackendComboModel;
+    }
+    const QString& getDevicesWarningMsg() const { return m_devicesWarningMsg; }
+    const QString& getDevicesErrorMsg() const { return m_devicesErrorMsg; }
+    const QString& getDevicesWarningHelpUrl() const { return m_devicesWarningHelpUrl; }
+    const QString& getDevicesErrorHelpUrl() const { return m_devicesErrorHelpUrl; }
+    bool getHighLatencyFlag() const { return m_highLatencyFlag; }
+   public slots:
+
+    // setters for state shared with QML
+    void setFeedbackDetectionEnabled(bool enabled);
+    void setAudioBackend(const QString& backend);
+    void setSampleRate(int sampleRate);
+    void setBufferSize(int bufSize);
+    void setBufferStrategy(int bufStrategy);
+    void setNumInputChannels(int numChannels);
+    void setNumOutputChannels(int numChannels);
+    void setBaseInputChannel(int baseChannel);
+    void setBaseOutputChannel(int baseChannel);
+    void setInputMixMode(int mode);
+    void setInputMuted(bool muted);
+    void setInputVolume(float multiplier);
+    void setOutputVolume(float multiplier);
+    void setMonitorVolume(float multiplier);
+    void setInputDevice(const QString& device);
+    void setOutputDevice(const QString& device);
+    void setDevicesErrorMsg(const QString& msg);
+    void setDevicesWarningMsg(const QString& msg);
+    void setDevicesErrorHelpUrl(const QString& url);
+    void setDevicesWarningHelpUrl(const QString& url);
+    void setHighLatencyFlag(bool highLatency);
+
+    // public methods accessible by QML
+    void startAudio(bool block = false);
+    void stopAudio(bool block = false);
+    void refreshDevices(bool block = false);
+    void validateDevices(bool block = false);
+    void restartAudio(bool block = false) { return startAudio(block); }
+    void playOutputAudio() { emit signalPlayOutputAudio(); }
+
+   signals:
+
+    // signals for QML state changes
+    void signalAudioReadyChanged();
+    void signalAudioIsReady();
+    void signalAudioIsNotReady();
+    void signalScanningDevicesChanged();
+    void deviceModelsInitializedChanged(bool initialized);
+    void audioBackendChanged(bool useRtAudio);
+    void sampleRateChanged();
+    void bufferSizeChanged();
+    void bufferStrategyChanged();
+    void numInputChannelsChanged(int numChannels);
+    void numOutputChannelsChanged(int numChannels);
+    void baseInputChannelChanged(int baseChannel);
+    void baseOutputChannelChanged(int baseChannel);
+    void inputMixModeChanged(int mode);
+    void updatedInputMuted(bool muted);
+    void updatedInputClipped(bool clip);
+    void updatedOutputClipped(bool clip);
+    void updatedInputVolume(float multiplier);
+    void updatedOutputVolume(float multiplier);
+    void updatedMonitorVolume(float multiplier);
+    void inputDeviceChanged(QString device);
+    void outputDeviceChanged(QString device);
+    void updatedInputMeterLevels(const QVector<float>& levels);
+    void updatedOutputMeterLevels(const QVector<float>& levels);
+    void feedbackDetectionEnabledChanged();
+    void feedbackDetected();
+    void inputComboModelChanged();
+    void outputComboModelChanged();
+    void inputChannelsComboModelChanged();
+    void outputChannelsComboModelChanged();
+    void inputMixModeComboModelChanged();
+    void devicesWarningChanged();
+    void devicesErrorChanged();
+    void devicesWarningHelpUrlChanged();
+    void devicesErrorHelpUrlChanged();
+    void highLatencyFlagChanged(bool highLatencyFlag);
+
+    // other signals to perform actions
+    void signalPlayOutputAudio();
+    void signalStartAudio();
+    void signalStopAudio();
+    void signalRefreshDevices();
+    void signalValidateDevices();
+    void signalDevicesValidated();
+
+   private slots:
+    void setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel);
+    void setInputChannelsComboModel(QJsonArray& model);
+    void setOutputChannelsComboModel(QJsonArray& model);
+    void setInputMixModeComboModel(QJsonArray& model);
+
+   private:
+    // private methods
+    void setAudioReady(bool ready);
+    void setScanningDevices(bool b);
+    void detectedFeedbackLoop();
+    void updatedInputVuMeasurements(const float* valuesInDecibels, int numChannels);
+    void updatedOutputVuMeasurements(const float* valuesInDecibels, int numChannels);
+    void appendProcessPlugins(AudioInterface& audioInterface, bool forJackTrip,
+                              int numInputChannels, int numOutputChannels);
+    void updateDeviceMessages(AudioInterface& audioInterface);
+    AudioInterface* newJackAudioInterface(JackTrip* jackTripPtr = nullptr);
+    AudioInterface* newRtAudioInterface(JackTrip* jackTripPtr = nullptr);
+    void errorCallback(const std::string& errorText, JackTrip* jackTripPtr = nullptr);
+
+    // range for volume meters
+    static constexpr float m_meterMax = 0.0;
+    static constexpr float m_meterMin = -64.0;
+
+    // audio bit resolution
+    static constexpr AudioInterface::audioBitResolutionT m_audioBitResolution =
+        AudioInterface::BIT16;
+
+    // state shared with QML
+    AudioBackendType m_backend      = AudioBackendType::JACK;
+    bool m_audioReady               = false;
+    bool m_scanningDevices          = false;
+    bool m_feedbackDetectionEnabled = true;
+    bool m_deviceModelsInitialized  = false;
+    int m_audioSampleRate           = gDefaultSampleRate;
+    int m_audioBufferSize =
+        gDefaultBufferSizeInSamples;  ///< Audio buffer size to process on each callback
+    int m_bufferStrategy    = 0;
+    int m_numInputChannels  = gDefaultNumInChannels;
+    int m_numOutputChannels = gDefaultNumOutChannels;
+    int m_baseInputChannel  = 0;
+    int m_baseOutputChannel = 0;
+    int m_inputMixMode      = 0;
+    bool m_inMuted          = false;
+    bool m_inputClipped     = false;
+    bool m_outputClipped    = false;
+    float m_inMultiplier    = 1.0;
+    float m_outMultiplier   = 1.0;
+    float m_monMultiplier   = 0;
+
+    QString m_inputDevice;
+    QString m_outputDevice;
+    QVector<float> m_inputMeterLevels;
+    QVector<float> m_outputMeterLevels;
+    QJsonArray m_inputComboModel;
+    QJsonArray m_outputComboModel;
+    QJsonArray m_inputChannelsComboModel;
+    QJsonArray m_outputChannelsComboModel;
+    QJsonArray m_inputMixModeComboModel;
+    QString m_devicesWarningMsg     = QStringLiteral("");
+    QString m_devicesErrorMsg       = QStringLiteral("");
+    QString m_devicesWarningHelpUrl = QStringLiteral("");
+    QString m_devicesErrorHelpUrl   = QStringLiteral("");
+    bool m_highLatencyFlag          = false;
+
+    // other state not shared with QML
+    QSharedPointer<VsPermissions> m_permissionsPtr;
+    QScopedPointer<VsAudioWorker> m_audioWorkerPtr;
+    QThread* m_workerThreadPtr;
+    QTimer m_inputClipTimer;
+    QTimer m_outputClipTimer;
+    Meter* m_inputMeterPluginPtr;
+    Meter* m_outputMeterPluginPtr;
+    Volume* m_inputVolumePluginPtr;
+    Volume* m_outputVolumePluginPtr;
+    Monitor* m_monitorPluginPtr;
+    bool mHasErrors;  ///< true if one or more error callbacks have been triggered
+
+#ifndef NO_FEEDBACK
+    Analyzer* m_outputAnalyzerPluginPtr;
+#endif
+
+    QStringList m_audioBackendComboModel      = {"JACK", "RtAudio"};
+    QStringList m_feedbackDetectionComboModel = {"Enabled", "Disabled"};
+    QStringList m_bufferSizeComboModel = {"16", "32", "64", "128", "256", "512", "1024"};
+    QStringList m_bufferStrategyComboModel = {
+        "Adaptable Latency (Old)", "Stable Latency (Old)", "Loss Concealment (Default)"};
+
+    friend class VsAudioWorker;
+};
+
+/// VsAudioWorker uses a separate thread to help VsAudio
+class VsAudioWorker : public QObject
+{
+    Q_OBJECT
+
+   public:
+    VsAudioWorker(VsAudio* ptr);
+    virtual ~VsAudioWorker() {}
+
+   signals:
+    void signalDevicesValidated();
+    void signalUpdatedDeviceModels(QJsonArray inputComboModel,
+                                   QJsonArray outputComboModel);
+    void signalError(const QString& errorMessage);
+
+   public slots:
+    void openAudioInterface();
+    void closeAudioInterface();
+#ifdef RT_AUDIO
+    void refreshDevices();
+    void validateDevices();
+
+   private:
+    void updateDeviceModels();
+    void validateInputDevicesState();
+    void validateOutputDevicesState();
+    static void getDeviceList(const QVector<RtAudioDevice>& devices, QStringList& list,
+                              QStringList& categories, QList<int>& channels,
+                              bool isInput);
+    static QJsonArray formatDeviceList(const QStringList& devices,
+                                       const QStringList& categories,
+                                       const QList<int>& channels);
+    QVector<RtAudioDevice> m_devices;
+
+   public:
+    QVector<RtAudioDevice> getDevices() const { return m_devices; }
+#endif
+
+   private:
+    // parent getter wrappers
+    bool getUseRtAudio() const { return m_parentPtr->getUseRtAudio(); }
+    int getNumInputChannels() const { return m_parentPtr->getNumInputChannels(); }
+    int getNumOutputChannels() const { return m_parentPtr->getNumOutputChannels(); }
+    int getBaseInputChannel() const { return m_parentPtr->getBaseInputChannel(); }
+    int getBaseOutputChannel() const { return m_parentPtr->getBaseOutputChannel(); }
+    int getSampleRate() const { return m_parentPtr->getSampleRate(); }
+    int getBufferSize() const { return m_parentPtr->getBufferSize(); }
+    int getInputMixMode() const { return m_parentPtr->getInputMixMode(); }
+    const QString& getInputDevice() const { return m_parentPtr->getInputDevice(); }
+    const QString& getOutputDevice() const { return m_parentPtr->getOutputDevice(); }
+
+    VsAudio* m_parentPtr;
+    QSharedPointer<AudioInterface> m_audioInterfacePtr;
+    QList<int> m_inputDeviceChannels;
+    QList<int> m_outputDeviceChannels;
+    QStringList m_inputDeviceList;
+    QStringList m_outputDeviceList;
+};
+
+#endif  // VSDAUDIO_H
diff --git a/src/gui/vsAuth.cpp b/src/gui/vsAuth.cpp
new file mode 100644 (file)
index 0000000..970855c
--- /dev/null
@@ -0,0 +1,298 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsAuth.cpp
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#include "vsAuth.h"
+
+#include "./vsConstants.h"
+
+VsAuth::VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api)
+    : m_clientId(AUTH_CLIENT_ID), m_authorizationServerHost(AUTH_SERVER_HOST)
+{
+    m_networkAccessManager = networkAccessManager;
+    m_api                  = api;
+    m_deviceCodeFlow.reset(new VsDeviceCodeFlow(networkAccessManager));
+
+    connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowInitialized, this,
+            &VsAuth::initializedCodeFlow);
+    connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowError, this,
+            &VsAuth::handleAuthFailed);
+    connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::onCompletedCodeFlow, this,
+            &VsAuth::codeFlowCompleted);
+    connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowTimedOut, this,
+            &VsAuth::codeExpired);
+
+    m_verificationUrl = QStringLiteral("https://auth.jacktrip.org/activate");
+}
+
+void VsAuth::authenticate(QString currentRefreshToken)
+{
+    if (currentRefreshToken.isEmpty()) {
+        // if no refresh token, initialize device flow
+        m_deviceCodeFlow->grant();
+    } else {
+        m_attemptingRefreshToken = true;
+        emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
+
+        // otherwise, use refresh token to gain a new access token
+        m_refreshToken = currentRefreshToken;
+        refreshAccessToken(m_refreshToken);
+    }
+}
+
+void VsAuth::initializedCodeFlow(QString code, QString verificationUrl)
+{
+    m_verificationCode    = code;
+    m_verificationUrl     = verificationUrl;
+    m_authenticationStage = QStringLiteral("polling");
+
+    emit updatedAuthenticationStage(m_authenticationStage);
+    emit updatedVerificationCode(m_verificationCode);
+    emit updatedVerificationUrl(m_verificationUrl);
+}
+
+void VsAuth::fetchUserInfo(QString accessToken)
+{
+    QNetworkReply* reply = m_api->getAuth0UserInfo();
+    connect(reply, &QNetworkReply::finished, this, [=]() {
+        if (reply->error() != QNetworkReply::NoError) {
+            std::cout << "VsAuth::fetchUserInfo Error: "
+                      << reply->errorString().toStdString() << std::endl;
+            handleAuthFailed();  // handle failure
+            emit fetchUserInfoFailed();
+            reply->deleteLater();
+            return;
+        }
+
+        QByteArray response    = reply->readAll();
+        QJsonDocument userInfo = QJsonDocument::fromJson(response);
+        QString userId         = userInfo.object()[QStringLiteral("sub")].toString();
+
+        reply->deleteLater();
+
+        if (userId.isEmpty()) {
+            std::cout << "VsAuth::fetchUserInfo Error: empty userId" << std::endl;
+            handleAuthFailed();  // handle failure
+            emit fetchUserInfoFailed();
+            return;
+        }
+
+        handleAuthSucceeded(userId, accessToken);
+    });
+}
+
+void VsAuth::refreshAccessToken(QString refreshToken)
+{
+    qDebug() << "Refreshing access token";
+    m_authenticationStage = QStringLiteral("refreshing");
+    emit updatedAuthenticationStage(m_authenticationStage);
+
+    QNetworkRequest request = QNetworkRequest(
+        QUrl(QString("https://%1/oauth/token").arg(m_authorizationServerHost)));
+
+    request.setRawHeader(QByteArray("Content-Type"),
+                         QByteArray("application/x-www-form-urlencoded"));
+
+    QString data = QString("grant_type=refresh_token&client_id=%1&refresh_token=%2")
+                       .arg(m_clientId, refreshToken);
+
+    // send request
+    QNetworkReply* reply = m_networkAccessManager->post(request, data.toUtf8());
+
+    connect(reply, &QNetworkReply::finished, this, [=]() {
+        QByteArray buffer = reply->readAll();
+
+        // Error: failed to get device code
+        if (reply->error()) {
+            std::cout << "Failed to get new access token: " << buffer.toStdString()
+                      << std::endl;
+            handleAuthFailed();  // handle failure
+            emit refreshTokenFailed();
+            reply->deleteLater();
+            return;
+        }
+
+        // parse JSON from string response
+        QJsonParseError parseError;
+        QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError);
+        if (parseError.error) {
+            std::cout << "Error parsing JSON for Access Token: "
+                      << parseError.errorString().toStdString() << std::endl;
+            handleAuthFailed();  // handle failure
+            emit refreshTokenFailed();
+            reply->deleteLater();
+            return;
+        }
+
+        // received access token
+        QJsonObject object  = data.object();
+        QString accessToken = object.value(QLatin1String("access_token")).toString();
+        m_api->setAccessToken(accessToken);  // set access token
+        reply->deleteLater();
+        if (m_userId.isEmpty()) {
+            fetchUserInfo(accessToken);  // get user ID from Auth0
+        } else {
+            handleRefreshSucceeded(accessToken);
+        }
+    });
+}
+
+void VsAuth::resetCode()
+{
+    if (!m_verificationCode.isEmpty()) {
+        m_deviceCodeFlow->cancelCodeFlow();
+        m_deviceCodeFlow->grant();
+    }
+}
+
+void VsAuth::codeFlowCompleted(QString accessToken, QString refreshToken)
+{
+    m_refreshToken = refreshToken;
+    m_api->setAccessToken(accessToken);
+    fetchUserInfo(accessToken);
+}
+
+void VsAuth::codeExpired()
+{
+    emit deviceCodeExpired();
+}
+
+void VsAuth::handleRefreshSucceeded(QString accessToken)
+{
+    qDebug() << "Successfully refreshed access token";
+
+    m_accessToken            = accessToken;
+    m_authenticationStage    = QStringLiteral("success");
+    m_attemptingRefreshToken = false;
+
+    emit updatedAuthenticationStage(m_authenticationStage);
+    emit updatedVerificationCode(m_verificationCode);
+    emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
+}
+
+void VsAuth::handleAuthSucceeded(QString userId, QString accessToken)
+{
+    // Success case: we got our access token (either through the refresh token or device
+    // code flow), and fetched the user ID
+    std::cout << "Successfully authenticated Virtual Studio user" << std::endl;
+    std::cout << "User ID: " << userId.toStdString() << std::endl;
+
+    if (m_authenticationStage == QStringLiteral("polling")) {
+        m_authenticationMethod = QStringLiteral("code flow");
+    } else {
+        m_authenticationMethod = QStringLiteral("refresh token");
+    }
+
+    m_userId                 = userId;
+    m_verificationCode       = QStringLiteral("");
+    m_accessToken            = accessToken;
+    m_authenticationStage    = QStringLiteral("success");
+    m_attemptingRefreshToken = false;
+    m_isAuthenticated        = true;
+
+    emit updatedUserId(m_userId);
+    emit updatedAuthenticationStage(m_authenticationStage);
+    emit updatedVerificationCode(m_verificationCode);
+    emit updatedIsAuthenticated(m_isAuthenticated);
+    emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
+    emit updatedAuthenticationMethod(m_authenticationMethod);
+
+    // notify UI and virtual studio class of success
+    emit authSucceeded();
+}
+
+void VsAuth::handleAuthFailed()
+{
+    // this might get called because there was an error getting the access token,
+    // or there was an issue fetching the user ID. We need both to say
+    // that authentication succeeded
+    std::cout << "Failed to authenticate user" << std::endl;
+
+    m_userId                 = QStringLiteral("");
+    m_verificationCode       = QStringLiteral("");
+    m_accessToken            = QStringLiteral("");
+    m_authenticationStage    = QStringLiteral("failed");
+    m_authenticationMethod   = QStringLiteral("");
+    m_attemptingRefreshToken = false;
+    m_isAuthenticated        = false;
+
+    emit updatedUserId(m_userId);
+    emit updatedAuthenticationStage(m_authenticationStage);
+    emit updatedVerificationCode(m_verificationCode);
+    emit updatedIsAuthenticated(m_isAuthenticated);
+    emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
+    emit updatedAuthenticationMethod(m_authenticationMethod);
+
+    // notify UI and virtual studio class of failure
+    emit authFailed();
+}
+
+void VsAuth::cancelAuthenticationFlow()
+{
+    qDebug() << "Canceling authentication flow";
+    m_deviceCodeFlow->cancelCodeFlow();
+
+    m_userId              = QStringLiteral("");
+    m_verificationCode    = QStringLiteral("");
+    m_accessToken         = QStringLiteral("");
+    m_authenticationStage = QStringLiteral("unauthenticated");
+    m_isAuthenticated     = false;
+
+    emit updatedUserId(m_userId);
+    emit updatedAuthenticationStage(m_authenticationStage);
+    emit updatedVerificationCode(m_verificationCode);
+    emit updatedIsAuthenticated(m_isAuthenticated);
+}
+
+void VsAuth::logout()
+{
+    if (!m_isAuthenticated) {
+        std::cout << "Warning: attempting to logout while not authenticated" << std::endl;
+    }
+    qDebug() << "Logging out";
+
+    // reset auth state
+    m_userId              = QStringLiteral("");
+    m_verificationCode    = QStringLiteral("");
+    m_accessToken         = QStringLiteral("");
+    m_authenticationStage = QStringLiteral("unauthenticated");
+    m_isAuthenticated     = false;
+
+    emit updatedUserId(m_userId);
+    emit updatedAuthenticationStage(m_authenticationStage);
+    emit updatedVerificationCode(m_verificationCode);
+    emit updatedIsAuthenticated(m_isAuthenticated);
+}
\ No newline at end of file
diff --git a/src/gui/vsAuth.h b/src/gui/vsAuth.h
new file mode 100644 (file)
index 0000000..61ccb85
--- /dev/null
@@ -0,0 +1,133 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsAuth.h
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#ifndef VSAUTH_H
+#define VSAUTH_H
+
+#include <QNetworkAccessManager>
+#include <QQmlContext>
+#include <QQmlEngine>
+#include <QString>
+#include <iostream>
+
+#include "vsApi.h"
+#include "vsDeviceCodeFlow.h"
+
+class VsAuth : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QString authenticationStage READ authenticationStage NOTIFY
+                   updatedAuthenticationStage);
+    Q_PROPERTY(QString verificationCode READ deviceCode NOTIFY updatedVerificationCode);
+    Q_PROPERTY(
+        QString verificationUrl READ deviceVerificationUrl NOTIFY updatedVerificationUrl);
+    Q_PROPERTY(bool isAuthenticated READ isAuthenticated NOTIFY updatedIsAuthenticated);
+    Q_PROPERTY(QString authenticationMethod READ authenticationMethod NOTIFY
+                   updatedAuthenticationMethod);
+    Q_PROPERTY(bool attemptingRefreshToken READ attemptingRefreshToken NOTIFY
+                   updatedAttemptingRefreshToken);
+    Q_PROPERTY(QString userId READ userId NOTIFY updatedUserId);
+    Q_PROPERTY(QString accessToken READ accessToken CONSTANT);
+
+   public:
+    VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api);
+
+    void authenticate(QString currentRefreshToken);
+    void refreshAccessToken(QString refreshToken);
+    Q_INVOKABLE void resetCode();
+    void logout();
+
+   public slots:
+    void cancelAuthenticationFlow();
+
+    // getter methods
+    QString authenticationStage() { return m_authenticationStage; };
+    QString deviceCode() { return m_verificationCode; };
+    QString deviceVerificationUrl() { return m_verificationUrl; };
+    bool isAuthenticated() { return m_isAuthenticated; };
+    QString userId() { return m_userId; };
+    QString accessToken() { return m_accessToken; };
+    QString refreshToken() { return m_refreshToken; };
+    QString authenticationMethod() { return m_authenticationMethod; }
+    bool attemptingRefreshToken() { return m_attemptingRefreshToken; }
+
+   signals:
+    void updatedAuthenticationStage(QString authenticationStage);
+    void updatedVerificationCode(QString deviceCode);
+    void updatedVerificationUrl(QUrl verificationUrl);
+    void updatedIsAuthenticated(bool isAuthenticated);
+    void updatedUserId(QString userId);
+    void updatedAuthenticationMethod(QString grant);
+    void updatedAttemptingRefreshToken(bool attemptingRefreshToken);
+    void authSucceeded();
+    void authFailed();
+    void refreshTokenFailed();
+    void fetchUserInfoFailed();
+    void deviceCodeExpired();
+
+   private slots:
+    void handleRefreshSucceeded(QString accessToken);
+    void handleAuthSucceeded(QString userId, QString accessToken);
+    void handleAuthFailed();
+    void initializedCodeFlow(QString code, QString verificationUrl);
+    void codeFlowCompleted(QString accessToken, QString refreshToken);
+    void codeExpired();
+
+   private:
+    void fetchUserInfo(QString accessToken);
+
+    QString m_clientId;
+    QString m_authorizationServerHost;
+
+    QString m_authenticationStage = QStringLiteral("unauthenticated");
+    QString m_verificationCode    = QStringLiteral("");
+    QString m_verificationUrl;
+    QString m_authenticationMethod = QStringLiteral("");
+
+    bool m_attemptingRefreshToken = false;
+    bool m_isAuthenticated        = false;
+    QString m_userId;
+    QString m_accessToken;
+    QString m_refreshToken;
+
+    QNetworkAccessManager* m_networkAccessManager;
+    VsApi* m_api;
+    QScopedPointer<VsDeviceCodeFlow> m_deviceCodeFlow;
+};
+
+#endif
\ No newline at end of file
diff --git a/src/gui/vsConstants.h b/src/gui/vsConstants.h
new file mode 100644 (file)
index 0000000..63f3f39
--- /dev/null
@@ -0,0 +1,51 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsConstants.h
+ * \author Nelson Wang
+ * \date Oct 2022
+ */
+
+#ifndef VSCONSTANTS_H
+#define VSCONSTANTS_H
+
+#include <QString>
+
+const QString AUTH_AUTHORIZE_URI = QStringLiteral("https://auth.jacktrip.org/authorize");
+const QString AUTH_TOKEN_URI   = QStringLiteral("https://auth.jacktrip.org/oauth/token");
+const QString AUTH_AUDIENCE    = QStringLiteral("https://api.jacktrip.org");
+const QString AUTH_CLIENT_ID   = QStringLiteral("cROUJag0UVKDaJ6jRAKRzlVjKVFNU39I");
+const QString PROD_API_HOST    = QStringLiteral("app.jacktrip.com");
+const QString TEST_API_HOST    = QStringLiteral("test.jacktrip.com");
+const QString AUTH_SERVER_HOST = QStringLiteral("auth.jacktrip.org");
+
+#endif  // VSCONSTANTS_H
diff --git a/src/gui/vsDeeplink.cpp b/src/gui/vsDeeplink.cpp
new file mode 100644 (file)
index 0000000..d3cafd1
--- /dev/null
@@ -0,0 +1,212 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsDeeplink.cpp
+ * \author Aaron Wyatt, based on code by Matt Horton
+ * \date February 2023
+ */
+
+#include "vsDeeplink.h"
+
+#include <QCoreApplication>
+#include <QDebug>
+#include <QDesktopServices>
+#include <QDir>
+#include <QEventLoop>
+#include <QMutexLocker>
+#include <QSettings>
+#include <QTimer>
+
+VsDeeplink::VsDeeplink(const QString& deeplink) : m_deeplink(deeplink)
+{
+    setUrlScheme();
+    checkForInstance();
+    QDesktopServices::setUrlHandler(QStringLiteral("jacktrip"), this, "handleUrl");
+}
+
+VsDeeplink::~VsDeeplink()
+{
+    QDesktopServices::unsetUrlHandler(QStringLiteral("jacktrip"));
+}
+
+bool VsDeeplink::waitForReady()
+{
+    while (!m_isReady) {
+        QTimer timer;
+        timer.setTimerType(Qt::CoarseTimer);
+        timer.setSingleShot(true);
+
+        QEventLoop loop;
+        QObject::connect(this, &VsDeeplink::signalIsReady, &loop, &QEventLoop::quit);
+        QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+        timer.start(100);  // wait for 100ms
+        loop.exec();
+    }
+    return m_readyToExit;
+}
+
+void VsDeeplink::readyForSignals()
+{
+    m_readyForSignals = true;
+    if (!m_deeplink.isEmpty()) {
+        emit signalDeeplink(m_deeplink);
+        m_deeplink.clear();
+    }
+}
+
+void VsDeeplink::handleUrl(const QUrl& url)
+{
+    if (m_readyForSignals) {
+        emit signalDeeplink(url);
+    } else {
+        m_deeplink = url;
+    }
+}
+
+void VsDeeplink::checkForInstance()
+{
+    // Create socket
+    m_instanceCheckSocket.reset(new QLocalSocket(this));
+    QObject::connect(m_instanceCheckSocket.data(), &QLocalSocket::connected, this,
+                     &VsDeeplink::connectionReceived, Qt::QueuedConnection);
+    // Create instanceServer to prevent new instances from being created
+    void (QLocalSocket::*errorFunc)(QLocalSocket::LocalSocketError);
+#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
+    errorFunc = &QLocalSocket::error;
+#else
+    errorFunc = &QLocalSocket::errorOccurred;
+#endif
+    QObject::connect(m_instanceCheckSocket.data(), errorFunc, this,
+                     &VsDeeplink::connectionFailed);
+    // Check for existing instance
+    m_instanceCheckSocket->connectToServer("jacktripExists");
+}
+
+void VsDeeplink::connectionReceived()
+{
+    // another jacktrip instance is running
+    if (!m_deeplink.isEmpty()) {
+        // pass deeplink to existing instance before quitting
+        QString deeplinkStr   = m_deeplink.toString();
+        QByteArray baDeeplink = deeplinkStr.toLocal8Bit();
+        qint64 writeBytes     = m_instanceCheckSocket->write(baDeeplink);
+        if (writeBytes < 0) {
+            qDebug() << "sending deeplink failed";
+        } else {
+            qDebug() << "Sent deeplink request to remote instance";
+        }
+
+        // make sure it isn't processed again
+        m_deeplink.clear();
+
+        // End process if another instance exists
+        m_readyToExit = true;
+    }
+
+    m_instanceCheckSocket->waitForBytesWritten();
+    m_instanceCheckSocket->disconnectFromServer();  // remove next
+
+    // let main thread know we are finished
+    m_isReady = true;
+    emit signalIsReady();
+}
+
+void VsDeeplink::connectionFailed(QLocalSocket::LocalSocketError socketError)
+{
+    switch (socketError) {
+    case QLocalSocket::ServerNotFoundError:
+    case QLocalSocket::SocketTimeoutError:
+    case QLocalSocket::ConnectionRefusedError:
+        // no other jacktrip instance is running, so we will take over handling deep links
+        qDebug() << "Listening for deep link requests";
+        m_instanceServer.reset(new QLocalServer(this));
+        m_instanceServer->setSocketOptions(QLocalServer::WorldAccessOption);
+        m_instanceServer->listen("jacktripExists");
+        QObject::connect(m_instanceServer.data(), &QLocalServer::newConnection, this,
+                         &VsDeeplink::handleDeeplinkRequest, Qt::QueuedConnection);
+        break;
+    case QLocalSocket::PeerClosedError:
+        break;
+    default:
+        qDebug() << m_instanceCheckSocket->errorString();
+    }
+
+    // let main thread know we are finished
+    m_isReady = true;
+    emit signalIsReady();
+}
+
+void VsDeeplink::handleDeeplinkRequest()
+{
+    while (m_instanceServer->hasPendingConnections()) {
+        // Receive URL from 2nd instance
+        QLocalSocket* connectedSocket = m_instanceServer->nextPendingConnection();
+
+        if (connectedSocket == nullptr || !connectedSocket->waitForConnected()) {
+            qDebug() << "Deeplink socket: never received connection";
+            return;
+        }
+
+        if (!connectedSocket->waitForReadyRead()
+            && connectedSocket->bytesAvailable() <= 0) {
+            qDebug() << "Deeplink socket: not ready and no bytes available: "
+                     << connectedSocket->errorString();
+            return;
+        }
+
+        if (connectedSocket->bytesAvailable() < (int)sizeof(quint16)) {
+            qDebug() << "Deeplink socket: ready but no bytes available";
+            break;
+        }
+
+        QByteArray in(connectedSocket->readAll());
+        QString urlString(in);
+        handleUrl(urlString);
+    }
+}
+
+void VsDeeplink::setUrlScheme()
+{
+#ifdef _WIN32
+    // Set url scheme in registry
+    QString path = QDir::toNativeSeparators(qApp->applicationFilePath());
+
+    QSettings set("HKEY_CURRENT_USER\\Software\\Classes", QSettings::NativeFormat);
+    set.beginGroup("jacktrip");
+    set.setValue("Default", "URL:JackTrip Protocol");
+    set.setValue("DefaultIcon/Default", path);
+    set.setValue("URL Protocol", "");
+    set.setValue("shell/open/command/Default",
+                 QString("\"%1\"").arg(path) + " --gui --deeplink \"%1\"");
+    set.endGroup();
+#endif
+}
diff --git a/src/gui/vsDeeplink.h b/src/gui/vsDeeplink.h
new file mode 100644 (file)
index 0000000..8eb932e
--- /dev/null
@@ -0,0 +1,113 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2023 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsDeeplink.h
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date August 2023
+ */
+
+#ifndef __VSDEEPLINK_H__
+#define __VSDEEPLINK_H__
+
+#include <QLocalServer>
+#include <QLocalSocket>
+#include <QScopedPointer>
+#include <QString>
+#include <QUrl>
+
+class VsDeeplink : public QObject
+{
+    Q_OBJECT
+
+   public:
+    // construct with an instance of the application, to parse command line args
+    VsDeeplink(const QString& deeplink);
+
+    // virtual destructor since it inherits from QObject
+    // this is used to unregister url handler
+    virtual ~VsDeeplink();
+
+    // blocks main thread until local socket server is ready
+    // returns true if a deeplink was handled and we should exit now
+    bool waitForReady();
+
+    // used to let us know VirtualStudio is ready to process deeplink signals
+    void readyForSignals();
+
+    // returns deeplink extracted from command line, if any
+    const QUrl& getDeeplink() const { return m_deeplink; }
+
+   signals:
+
+    // signalIsReady is emitted when the local socket server is ready
+    void signalIsReady();
+
+    // signalDeeplink is emitted when we want the local instance to process a deeplink
+    void signalDeeplink(const QUrl& url);
+
+   private slots:
+
+    // handleUrl is called to trigger processing of a deeplink
+    void handleUrl(const QUrl& url);
+
+    // checks to see if another instance of jacktrip is available to process requests.
+    // if there is, this will send any command line deeplinks to it and exit.
+    // if there isn't, this will start listening for requests.
+    void checkForInstance();
+
+    // called if a connection was established with another instance of VS
+    void connectionReceived();
+
+    // called if unable to connect to another instance of VS
+    void connectionFailed(QLocalSocket::LocalSocketError socketError);
+
+    // called by local socket server to process deeplink requests
+    void handleDeeplinkRequest();
+
+   private:
+    // sets url scheme for windows machines; does nothing on other platforms
+    static void setUrlScheme();
+
+    // used to check if there is a virtual studio instance already running
+    QScopedPointer<QLocalSocket> m_instanceCheckSocket;
+
+    // used to listen for deeplink requests via local socket connections
+    QScopedPointer<QLocalServer> m_instanceServer;
+
+    // used to synchronize with main thread at startup
+    bool m_isReady         = false;
+    bool m_readyForSignals = false;
+    bool m_readyToExit     = false;
+    QUrl m_deeplink;
+};
+
+#endif  // __VSDEEPLINK_H__
diff --git a/src/gui/vsDevice.cpp b/src/gui/vsDevice.cpp
new file mode 100644 (file)
index 0000000..79b99b9
--- /dev/null
@@ -0,0 +1,601 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsDevice.cpp
+ * \author Matt Horton
+ * \date June 2022
+ */
+
+#include "vsDevice.h"
+
+#include <QEventLoop>
+
+// Constructor
+VsDevice::VsDevice(QSharedPointer<VsAuth>& auth, QSharedPointer<VsApi>& api,
+                   QSharedPointer<VsAudio>& audio, QObject* parent)
+    : QObject(parent)
+    , m_auth(auth)
+    , m_api(api)
+    , m_audioConfigPtr(audio)
+    , m_sendVolumeTimer(this)
+{
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    m_apiPrefix = settings.value(QStringLiteral("ApiPrefix"), "").toString();
+    m_apiSecret = settings.value(QStringLiteral("ApiSecret"), "").toString();
+    m_appUUID   = settings.value(QStringLiteral("AppUUID"), "").toString();
+    m_appID     = settings.value(QStringLiteral("AppID"), "").toString();
+    settings.endGroup();
+
+    if (!m_appID.isEmpty()) {
+        std::cout << "Device ID: " << m_appID.toStdString() << std::endl;
+    }
+
+    m_sendVolumeTimer.setSingleShot(true);
+    connect(&m_sendVolumeTimer, &QTimer::timeout, this, &VsDevice::sendLevels);
+
+    // Set server levels to stored versions
+    sendLevels();
+}
+
+VsDevice::~VsDevice()
+{
+    m_sendVolumeTimer.stop();
+    stopJackTrip(false);
+}
+
+// registerApp idempotently registers an emulated device belonging to the current user
+void VsDevice::registerApp()
+{
+    if (m_appUUID == "") {
+        m_appUUID = QUuid::createUuid().toString(QUuid::StringFormat::WithoutBraces);
+    }
+
+    // check if device exists
+    QNetworkReply* reply = m_api->getDevice(m_appID);
+    connect(reply, &QNetworkReply::finished, this, [=]() {
+        // Got error
+        if (reply->error() != QNetworkReply::NoError) {
+            QVariant statusCode =
+                reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
+            if (!statusCode.isValid()) {
+                std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+                reply->deleteLater();
+                return;
+            }
+
+            int status = statusCode.toInt();
+            // Device does not exist
+            if (status >= 400 && status < 500) {
+                std::cout << "Device not found. Creating new device." << std::endl;
+
+                if (m_apiPrefix == "" || m_apiSecret == "") {
+                    m_apiPrefix = randomString(7);
+                    m_apiSecret = randomString(22);
+                }
+
+                registerJTAsDevice();
+            } else {
+                // Other error status. Won't create device.
+                std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+                reply->deleteLater();
+                return;
+            }
+        } else if (m_apiPrefix != "" && m_apiSecret != "") {
+            sendHeartbeat();
+        }
+
+        QSettings settings;
+        settings.beginGroup(QStringLiteral("VirtualStudio"));
+        settings.setValue(QStringLiteral("AppUUID"), m_appUUID);
+        settings.setValue(QStringLiteral("ApiPrefix"), m_apiPrefix);
+        settings.setValue(QStringLiteral("ApiSecret"), m_apiSecret);
+        settings.endGroup();
+        if (!m_appID.isEmpty()) {
+            updateState("");
+        }
+
+        reply->deleteLater();
+    });
+}
+
+// removeApp deletes the emulated device
+void VsDevice::removeApp()
+{
+    if (m_appID.isEmpty()) {
+        return;
+    }
+
+    QNetworkReply* reply = m_api->deleteDevice(m_appID);
+    QEventLoop loop;
+    connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
+    loop.exec();
+
+    if (reply->error() != QNetworkReply::NoError) {
+        std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+    } else {
+        m_appID.clear();
+        m_appUUID.clear();
+        m_apiPrefix.clear();
+        m_apiSecret.clear();
+
+        QSettings settings;
+        settings.beginGroup(QStringLiteral("VirtualStudio"));
+        settings.remove(QStringLiteral("AppID"));
+        settings.remove(QStringLiteral("AppUUID"));
+        settings.remove(QStringLiteral("ApiPrefix"));
+        settings.remove(QStringLiteral("ApiSecret"));
+        settings.endGroup();
+    }
+    reply->deleteLater();
+}
+
+// sendHeartbeat is reponsible for sending liveness heartbeats to the API
+void VsDevice::sendHeartbeat()
+{
+    QString now = QDateTime::currentDateTimeUtc().toString(Qt::ISODate);
+
+    QJsonObject json = {
+        {QLatin1String("stats_updated_at"), now},
+        {QLatin1String("mac"), m_appUUID},
+        {QLatin1String("version"), QLatin1String(gVersion)},
+        {QLatin1String("type"), "jacktrip_app"},
+        {QLatin1String("apiPrefix"), m_apiPrefix},
+        {QLatin1String("apiSecret"), m_apiSecret},
+    };
+
+    // Add stats to heartbeat body
+    if (!m_pinger.isNull() && m_pinger->active()) {
+        VsPinger::PingStat stats = m_pinger->getPingStats();
+
+        // API server expects RTTs to be in int64 nanoseconds, so we must convert
+        // from milliseconds to nanoseconds
+        int ns_per_ms = 1000000;
+
+        json.insert(QLatin1String("pkts_sent"), (int)stats.packetsSent);
+        json.insert(QLatin1String("pkts_recv"), (int)stats.packetsReceived);
+        json.insert(QLatin1String("min_rtt"), (qint64)(stats.minRtt * ns_per_ms));
+        json.insert(QLatin1String("max_rtt"), (qint64)(stats.maxRtt * ns_per_ms));
+        json.insert(QLatin1String("avg_rtt"), (qint64)(stats.avgRtt * ns_per_ms));
+        json.insert(QLatin1String("stddev_rtt"), (qint64)(stats.stdDevRtt * ns_per_ms));
+        json.insert(QLatin1String("high_latency"),
+                    m_audioConfigPtr->getHighLatencyFlag());
+        json.insert(QLatin1String("network_outage"), m_networkOutage);
+
+        // For the internal application UI, ms will suffice. No conversion needed
+        QJsonObject pingStats = {};
+        pingStats.insert(QLatin1String("packetsSent"), (int)stats.packetsSent);
+        pingStats.insert(QLatin1String("packetsReceived"), (int)stats.packetsReceived);
+        pingStats.insert(QLatin1String("minRtt"), ((int)(10 * stats.minRtt)) / 10.0);
+        pingStats.insert(QLatin1String("maxRtt"), ((int)(10 * stats.maxRtt)) / 10.0);
+        pingStats.insert(QLatin1String("avgRtt"), ((int)(10 * stats.avgRtt)) / 10.0);
+        pingStats.insert(QLatin1String("stdDevRtt"),
+                         ((int)(10 * stats.stdDevRtt)) / 10.0);
+        pingStats.insert(QLatin1String("highLatency"),
+                         m_audioConfigPtr->getHighLatencyFlag());
+        emit updateNetworkStats(pingStats);
+    }
+
+    QJsonDocument request = QJsonDocument(json);
+
+    if (!m_deviceSocketPtr.isNull() && m_deviceSocketPtr->isValid()) {
+        // Send heartbeat via websocket
+        m_deviceSocketPtr->sendMessage(request.toJson());
+    } else {
+        if (enabled()) {
+            if (m_deviceSocketPtr.isNull()) {
+                qDebug() << "Heartbeat not sent";
+            } else {
+                qDebug() << "Heartbeat not sent; trying to reopen socket";
+                m_deviceSocketPtr->openSocket();
+            }
+        }
+    }
+}
+
+bool VsDevice::hasTerminated()
+{
+    return m_jackTrip.isNull();
+}
+
+// updateState updates the emulated device with the provided state
+void VsDevice::updateState(const QString& serverId)
+{
+    m_deviceAgentConfig.insert("serverId", serverId);
+    m_deviceAgentConfig.insert("enabled", !serverId.isEmpty());
+    QJsonObject json = {
+        {QLatin1String("serverId"), serverId},
+        {QLatin1String("enabled"), !serverId.isEmpty()},
+    };
+    QJsonDocument request = QJsonDocument(json);
+    QNetworkReply* reply  = m_api->updateDevice(m_appID, request.toJson());
+    connect(reply, &QNetworkReply::finished, this, [=]() {
+        if (reply->error() != QNetworkReply::NoError) {
+            std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+        }
+        reply->deleteLater();
+    });
+}
+
+void VsDevice::sendLevels()
+{
+    if (m_appID.isEmpty()) {
+        return;
+    }
+    // Add latest volume and mute values to heartbeat body
+    QJsonObject json = {{QLatin1String("version"), QLatin1String(gVersion)},
+                        {QLatin1String("captureVolume"),
+                         (int)(m_audioConfigPtr->getInputVolume() * 100.0)},
+                        {QLatin1String("captureMute"), m_audioConfigPtr->getInputMuted()},
+                        {QLatin1String("playbackVolume"),
+                         (int)(m_audioConfigPtr->getOutputVolume() * 100.0)},
+                        {QLatin1String("playbackMute"), false},
+                        {QLatin1String("monitorVolume"),
+                         (int)(m_audioConfigPtr->getMonitorVolume() * 100.0)}};
+
+    QJsonDocument request = QJsonDocument(json);
+    QNetworkReply* reply  = m_api->updateDevice(m_appID, request.toJson());
+    connect(reply, &QNetworkReply::finished, this, [=]() {
+        if (reply->error() != QNetworkReply::NoError) {
+            std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+        }
+        reply->deleteLater();
+    });
+}
+
+// initJackTrip spawns a new jacktrip process with the desired settings
+JackTrip* VsDevice::initJackTrip(
+    [[maybe_unused]] bool useRtAudio, [[maybe_unused]] std::string input,
+    [[maybe_unused]] std::string output, [[maybe_unused]] int baseInputChannel,
+    [[maybe_unused]] int numChannelsIn, [[maybe_unused]] int baseOutputChannel,
+    [[maybe_unused]] int numChannelsOut, [[maybe_unused]] int inputMixMode,
+    [[maybe_unused]] int bufferSize, [[maybe_unused]] int bufferStrategy,
+    VsServerInfo* studioInfo)
+{
+    m_jackTrip.reset(
+        new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, baseInputChannel,
+                     numChannelsIn, baseOutputChannel, numChannelsOut,
+                     static_cast<AudioInterface::inputMixModeT>(inputMixMode),
+#ifdef WAIR  // wair
+                     0,
+#endif  // endwhere
+                     4, 1));
+    m_jackTrip->setConnectDefaultAudioPorts(true);
+#ifdef RT_AUDIO
+    if (useRtAudio) {
+        m_jackTrip->setAudiointerfaceMode(JackTrip::RTAUDIO);
+        m_jackTrip->setAudioBufferSizeInSamples(bufferSize);
+        m_jackTrip->setInputDevice(input);
+        m_jackTrip->setOutputDevice(output);
+    }
+#endif
+    m_jackTrip->setSampleRate(studioInfo->sampleRate());
+    int bindPort = selectBindPort();
+    if (bindPort == 0) {
+        return 0;
+    }
+    m_jackTrip->setBindPorts(bindPort);
+    m_jackTrip->setRemoteClientName(m_appID);
+    m_jackTrip->setBufferStrategy(bufferStrategy);
+    m_jackTrip->setBufferQueueLength(-500);  // use -q auto
+    m_jackTrip->setPeerAddress(studioInfo->host());
+    m_jackTrip->setPeerPorts(studioInfo->port());
+    m_jackTrip->setPeerHandshakePort(studioInfo->port());
+
+    QObject::connect(m_jackTrip.data(), &JackTrip::signalProcessesStopped, this,
+                     &VsDevice::handleJackTripError, Qt::QueuedConnection);
+    QObject::connect(m_jackTrip.data(), &JackTrip::signalError, this,
+                     &VsDevice::handleJackTripError, Qt::QueuedConnection);
+
+    return m_jackTrip.data();
+}
+
+// startJackTrip starts the current jacktrip process if applicable
+void VsDevice::startJackTrip(const VsServerInfo& studioInfo)
+{
+    m_stopping      = false;
+    m_networkOutage = false;
+    updateState(studioInfo.id());
+
+    // setup websocket listener
+    m_deviceSocketPtr.reset(
+        new VsWebSocket(QUrl(QStringLiteral("wss://%1/api/devices/%2/heartbeat")
+                                 .arg(m_api->getApiHost(), m_appID)),
+                        m_auth->accessToken(), m_apiPrefix, m_apiSecret));
+    connect(m_deviceSocketPtr.get(), &VsWebSocket::textMessageReceived, this,
+            &VsDevice::onTextMessageReceived);
+    connect(m_deviceSocketPtr.get(), &VsWebSocket::disconnected, this,
+            &VsDevice::restartDeviceSocket);
+    m_deviceSocketPtr->openSocket();
+
+    if (!m_jackTrip.isNull()) {
+#ifdef WAIRTOHUB                      // WAIR
+        m_jackTrip->startProcess(0);  // for WAIR compatibility, ID in jack client name
+#else
+        m_jackTrip->startProcess();
+#endif  // endwhere
+    }
+
+    // intialize the pinger used to generate network latency statistics for
+    // Virtual Studio
+    QString host = studioInfo.host();
+    if (studioInfo.isManaged()) {
+        host = studioInfo.sessionId();
+        host.append(QString::fromStdString(".jacktrip.cloud"));
+    }
+    if (studioInfo.isManaged() || !studioInfo.sessionId().isEmpty()) {
+        m_pinger.reset(new VsPinger(QString::fromStdString("wss"), host,
+                                    QString::fromStdString("/ping")));
+    }
+}
+
+// stopJackTrip stops the current jacktrip process if applicable
+void VsDevice::stopJackTrip(bool isReconnecting)
+{
+    // check if another process has already initiated
+    QMutexLocker stopLock(&m_stopMutex);
+    if (m_stopping)
+        return;
+    m_stopping = true;
+
+    // only clear state if we are not reconnecting
+    if (!isReconnecting)
+        updateState("");
+
+    // stop the Virtual Studio pinger
+    if (!m_pinger.isNull()) {
+        m_pinger->stop();
+        m_pinger->unsetToken();
+    }
+
+    if (!m_jackTrip.isNull()) {
+        if (!m_deviceSocketPtr.isNull()) {
+            m_deviceSocketPtr->closeSocket();
+        }
+        m_jackTrip->stop();
+        m_jackTrip.reset();
+    }
+}
+
+// reconcileAgentConfig updates the internal DeviceAgentConfig structure
+void VsDevice::reconcileAgentConfig(QJsonDocument newState)
+{
+    // Only sync if the incoming type matches DeviceAgentConfig:
+    // https://github.com/jacktrip/jacktrip-agent/blob/fd3940c293daf16d8467c62b39a30779d21a0a22/pkg/client/devices.go#L87
+    QJsonObject newObject = newState.object();
+    if (!newObject.contains("enabled")) {
+        return;
+    }
+    for (auto it = newObject.constBegin(); it != newObject.constEnd(); it++) {
+        // if currently enabled but new config is not enabled, disconnect immediately
+        if (enabled() && it.key() == "enabled" && !it.value().toBool()
+            && !m_jackTrip.isNull()) {
+            stopJackTrip(false);
+        }
+        m_deviceAgentConfig.insert(it.key(), it.value());
+    }
+}
+
+// syncDeviceSettings updates volume/mute controls against the API
+void VsDevice::syncDeviceSettings()
+{
+    m_sendVolumeTimer.start(100);
+}
+
+// handleJackTripError is a slot intended to be triggered on jacktrip process signals
+void VsDevice::handleJackTripError()
+{
+    stopJackTrip(false);
+}
+
+// onTextMessageReceived is a slot intended to be triggered by new incoming WSS messages
+void VsDevice::onTextMessageReceived(const QString& message)
+{
+    QJsonDocument newState = QJsonDocument::fromJson(message.toUtf8());
+    QJsonObject newObj     = newState.object();
+
+    // We have a heartbeat from which we can read the studio auth token
+    // Use it to set up and start the pinger connection
+    QString token = newState["authToken"].toString();
+    if (!m_pinger.isNull() && !m_pinger->active() && !token.isEmpty()) {
+        m_pinger->setToken(token);
+        m_pinger->start();
+    }
+
+    // capture (input) volume
+    m_audioConfigPtr->setInputVolume(
+        (float)(newObj[QStringLiteral("captureVolume")].toDouble() / 100.0));
+    m_audioConfigPtr->setInputMuted(newObj[QStringLiteral("captureMute")].toBool());
+
+    // playback (output) volume
+    m_audioConfigPtr->setOutputVolume(
+        (float)(newObj[QStringLiteral("playbackVolume")].toDouble() / 100.0));
+
+    // monitor volume
+    m_audioConfigPtr->setMonitorVolume(
+        (float)(newObj[QStringLiteral("monitorVolume")].toDouble() / 100.0));
+
+    reconcileAgentConfig(newState);
+}
+
+void VsDevice::restartDeviceSocket()
+{
+    if (m_deviceAgentConfig[QStringLiteral("serverId")].toString() != "") {
+        if (!m_deviceSocketPtr.isNull()) {
+            m_deviceSocketPtr->openSocket();
+        }
+    }
+}
+
+// registerJTAsDevice creates the emulated device belonging to the current user
+void VsDevice::registerJTAsDevice()
+{
+    /*
+        REGISTER JT APP AS A DEVICE ON VIRTUAL STUDIO
+
+        Defaults:
+        period - 128 - set by studio = buffer size
+        queueBuffer - 0 - set by studio = net queue
+        devicePort - 4464
+        reverb - 0 - off
+        limiter - false
+        compressor - false
+        quality - 2 - high
+        captureMute - false - unused right now
+        captureVolume - 100 - unused right now
+        playbackMute - false - unused right now
+        playbackVolume - 100 - unused right now
+        monitorMute - false - unsure if we should enable
+        monitorVolume - 0 - unsure if we should enable
+        name - "JackTrip App"
+        alsaName - "jacktripapp"
+        overlay - "jacktrip_app"
+        mac - UUID tied to app session
+        version - app version - will need to update in heartbeat
+        apiPrefix - random 7 character string tied to app session
+        apiSecret - random 22 character string tied to app session
+    */
+
+    QJsonObject json = {
+        // TODO: Fix me
+        //{QLatin1String("period"), m_bufferOptions[bufferSize()].toInt()},
+        {QLatin1String("period"), 128},
+        {QLatin1String("queueBuffer"), 0},
+        {QLatin1String("devicePort"), 4464},
+        {QLatin1String("reverb"), 0},
+        {QLatin1String("limiter"), false},
+        {QLatin1String("compressor"), false},
+        {QLatin1String("quality"), 2},
+        {QLatin1String("captureMute"), false},
+        {QLatin1String("captureVolume"), 100},
+        {QLatin1String("playbackMute"), false},
+        {QLatin1String("playbackVolume"), 100},
+        {QLatin1String("monitorMute"), false},
+        {QLatin1String("monitorVolume"), 100},
+        {QLatin1String("alsaName"), "jacktripapp"},
+        {QLatin1String("overlay"), "jacktrip_app"},
+        {QLatin1String("mac"), m_appUUID},
+        {QLatin1String("version"), QLatin1String(gVersion)},
+        {QLatin1String("apiPrefix"), m_apiPrefix},
+        {QLatin1String("apiSecret"), m_apiSecret},
+#if defined(Q_OS_MACOS)
+        {QLatin1String("name"), "JackTrip App (macOS)"},
+#elif defined(Q_OS_WIN)
+        {QLatin1String("name"), "JackTrip App (Windows)"},
+#else
+        {QLatin1String("name"), "JackTrip App"},
+#endif  // Q_OS_WIN
+    };
+    QJsonDocument request = QJsonDocument(json);
+
+    QNetworkReply* reply = m_api->postDevice(request.toJson());
+    connect(reply, &QNetworkReply::finished, this, [=]() {
+        if (reply->error() != QNetworkReply::NoError) {
+            std::cout << "Error: " << reply->errorString().toStdString() << std::endl;
+            reply->deleteLater();
+            return;
+        } else {
+            QJsonDocument response = QJsonDocument::fromJson(reply->readAll());
+            QJsonObject newObject  = response.object();
+
+            m_appID = newObject[QStringLiteral("id")].toString();
+
+            // capture (input) volume
+            m_audioConfigPtr->setInputVolume(
+                (float)(newObject[QStringLiteral("captureVolume")].toDouble() / 100.0));
+            m_audioConfigPtr->setInputMuted(
+                newObject[QStringLiteral("captureMute")].toBool());
+
+            // playback (output) volume
+            m_audioConfigPtr->setOutputVolume(
+                (float)(newObject[QStringLiteral("playbackVolume")].toDouble() / 100.0));
+
+            // monitor volume
+            m_audioConfigPtr->setMonitorVolume(
+                (float)(newObject[QStringLiteral("monitorVolume")].toDouble() / 100.0));
+
+            QSettings settings;
+            settings.beginGroup(QStringLiteral("VirtualStudio"));
+            settings.setValue(QStringLiteral("AppID"), m_appID);
+            settings.endGroup();
+
+            std::cout << "Device ID: " << m_appID.toStdString() << std::endl;
+            sendHeartbeat();
+        }
+
+        reply->deleteLater();
+    });
+}
+
+// enabled returns whether or not the client is connected to a studio
+bool VsDevice::enabled()
+{
+    return m_deviceAgentConfig[QStringLiteral("enabled")].toBool();
+}
+
+// randomString generates a random sequence of characters
+QString VsDevice::randomString(int stringLength)
+{
+    QString str        = "";
+    static bool seeded = false;
+    QString allow_symbols(
+        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
+
+    if (!seeded) {
+        m_randomizer.seed((QTime::currentTime().msec()));
+        seeded = true;
+    }
+
+    for (int i = 0; i < stringLength; ++i) {
+        str.append(allow_symbols.at(m_randomizer.generate() % (allow_symbols.length())));
+    }
+
+    return str;
+}
+
+// selectBindPort finds the next open bind port to use for jacktrip
+int VsDevice::selectBindPort()
+{
+    int candidate = gDefaultPort;
+    if (m_jackTrip.isNull()) {
+        return candidate;
+    }
+    int attempt = 0;
+    while (attempt <= 5000) {
+        candidate = QRandomGenerator::global()->bounded(gBindPortLow, gBindPortHigh + 1);
+        attempt++;
+        if (!m_jackTrip->checkIfPortIsBinded(candidate)) {
+            return candidate;
+        }
+    }
+    return 0;
+}
diff --git a/src/gui/vsDevice.h b/src/gui/vsDevice.h
new file mode 100644 (file)
index 0000000..99a2fe6
--- /dev/null
@@ -0,0 +1,122 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsDevice.h
+ * \author Matt Horton
+ * \date June 2022
+ */
+
+#ifndef VSDEVICE_H
+#define VSDEVICE_H
+
+#include <QMutex>
+#include <QObject>
+#include <QString>
+#include <QTimer>
+#include <QUuid>
+#include <QtWebSockets>
+
+#include "../JackTrip.h"
+#include "../jacktrip_globals.h"
+#include "vsApi.h"
+#include "vsAudio.h"
+#include "vsAuth.h"
+#include "vsConstants.h"
+#include "vsPinger.h"
+#include "vsServerInfo.h"
+#include "vsWebSocket.h"
+
+class VsDevice : public QObject
+{
+    Q_OBJECT
+
+   public:
+    // Constructor
+    explicit VsDevice(QSharedPointer<VsAuth>& auth, QSharedPointer<VsApi>& api,
+                      QSharedPointer<VsAudio>& audio, QObject* parent = nullptr);
+    virtual ~VsDevice();
+
+    // Public functions
+    void registerApp();
+    void removeApp();
+    void sendHeartbeat();
+    bool hasTerminated();
+    JackTrip* initJackTrip(bool useRtAudio, std::string input, std::string output,
+                           int baseInputChannel, int numChannelsIn, int baseOutputChannel,
+                           int numChannelsOut, int inputMixMode, int bufferSize,
+                           int bufferStrategy, VsServerInfo* studioInfo);
+    void startJackTrip(const VsServerInfo& studioInfo);
+    void stopJackTrip(bool isReconnecting = false);
+    void reconcileAgentConfig(QJsonDocument newState);
+    void setNetworkOutage(bool outage = true) { m_networkOutage = outage; }
+    bool getNetworkOutage() const { return m_networkOutage; }
+
+   signals:
+    void updateNetworkStats(QJsonObject stats);
+
+   public slots:
+    void syncDeviceSettings();
+
+   private slots:
+    void handleJackTripError();
+    void onTextMessageReceived(const QString& message);
+    void restartDeviceSocket();
+    void sendLevels();
+
+   private:
+    void updateState(const QString& serverId);
+    void registerJTAsDevice();
+    bool enabled();
+    int selectBindPort();
+    QString randomString(int stringLength);
+
+    QSharedPointer<VsAuth> m_auth;
+    QSharedPointer<VsApi> m_api;
+    QSharedPointer<VsAudio> m_audioConfigPtr;
+    QScopedPointer<VsPinger> m_pinger;
+
+    QString m_appID;
+    QString m_appUUID;
+    QString m_token;
+    QString m_apiPrefix;
+    QString m_apiSecret;
+    QMutex m_stopMutex;
+    QJsonObject m_deviceAgentConfig;
+    QScopedPointer<VsWebSocket> m_deviceSocketPtr;
+    QScopedPointer<JackTrip> m_jackTrip;
+    QRandomGenerator m_randomizer;
+    QTimer m_sendVolumeTimer;
+    bool m_networkOutage = false;
+    bool m_stopping      = false;
+};
+
+#endif  // VSDEVICE_H
diff --git a/src/gui/vsDeviceCodeFlow.cpp b/src/gui/vsDeviceCodeFlow.cpp
new file mode 100644 (file)
index 0000000..dbc17e5
--- /dev/null
@@ -0,0 +1,249 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsDeviceCodeFlow.cpp
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#include "./vsDeviceCodeFlow.h"
+
+#include "./vsConstants.h"
+
+VsDeviceCodeFlow::VsDeviceCodeFlow(QNetworkAccessManager* networkAccessManager)
+    : m_clientId(AUTH_CLIENT_ID)
+    , m_audience(AUTH_AUDIENCE)
+    , m_authorizationServerHost(AUTH_SERVER_HOST)
+    , m_authenticationError(false)
+    , m_netManager(networkAccessManager)
+{
+    // start polling when the device flow has been initialized
+    connect(this, &VsDeviceCodeFlow::deviceCodeFlowInitialized, this,
+            &VsDeviceCodeFlow::startPolling);
+    connect(&m_tokenPollingTimer, &QTimer::timeout, this,
+            &VsDeviceCodeFlow::onPollingTimerTick);
+    connect(&m_deviceFlowExpirationTimer, &QTimer::timeout, this,
+            &VsDeviceCodeFlow::onDeviceCodeExpired);
+
+    m_tokenPollingTimer.setSingleShot(false);
+    m_deviceFlowExpirationTimer.setSingleShot(true);
+}
+
+void VsDeviceCodeFlow::grant()
+{
+    initDeviceAuthorizationCodeFlow();
+}
+
+void VsDeviceCodeFlow::initDeviceAuthorizationCodeFlow()
+{
+    // form initial request for device authorization code
+    QNetworkRequest request = QNetworkRequest(
+        QUrl(QString("https://%1/oauth/device/code").arg(m_authorizationServerHost)));
+
+    request.setRawHeader(QByteArray("Content-Type"),
+                         QByteArray("application/x-www-form-urlencoded"));
+
+    QString data =
+        QString("client_id=%1&scope=%2&audience=%3")
+            .arg(m_clientId,
+                 QLatin1String("openid profile email offline_access read:servers"),
+                 m_audience);
+
+    // send request
+    QNetworkReply* reply = m_netManager->post(request, data.toUtf8());
+    connect(reply, &QNetworkReply::finished, this, [=]() {
+        bool success = processDeviceCodeNetworkReply(reply);
+        if (success) {
+            // notify success along with user code and verification URL
+            emit deviceCodeFlowInitialized(m_userCode, m_verificationUriComplete);
+        } else if (m_authenticationError) {
+            // notify failure
+            emit deviceCodeFlowError();
+        }
+        reply->deleteLater();
+    });
+}
+
+void VsDeviceCodeFlow::startPolling()
+{
+    if (m_pollingInterval <= 0 || m_deviceCodeValidityDuration <= 0) {
+        std::cout << "Could not start polling. This should not print and indicates a bug."
+                  << std::endl;
+        return;
+    }
+
+    // poll on a regular interval, up until the expiration of the code
+    m_tokenPollingTimer.setInterval(m_pollingInterval * 1000);
+    m_deviceFlowExpirationTimer.setInterval(m_deviceCodeValidityDuration * 1000);
+
+    m_tokenPollingTimer.start();
+    m_deviceFlowExpirationTimer.start();
+}
+
+void VsDeviceCodeFlow::stopPolling()
+{
+    if (m_tokenPollingTimer.isActive()) {
+        m_tokenPollingTimer.stop();
+    }
+    if (m_deviceFlowExpirationTimer.isActive()) {
+        m_deviceFlowExpirationTimer.stop();
+    }
+}
+
+void VsDeviceCodeFlow::onPollingTimerTick()
+{
+    // form request to /oauth/token
+    QNetworkRequest request = QNetworkRequest(
+        QUrl(QString("https://%1/oauth/token").arg(m_authorizationServerHost)));
+
+    request.setRawHeader(QByteArray("Content-Type"),
+                         QByteArray("application/x-www-form-urlencoded"));
+
+    QString data =
+        QString("client_id=%1&device_code=%2&grant_type=%3")
+            .arg(m_clientId, m_deviceCode,
+                 QLatin1String("urn:ietf:params:oauth:grant-type:device_code"));
+
+    // send send request for token
+    QNetworkReply* reply = m_netManager->post(request, data.toUtf8());
+    connect(reply, &QNetworkReply::finished, this, [=]() {
+        bool success = processPollingOAuthTokenNetworkReply(reply);
+        if (m_authenticationError) {
+            // shouldn't happen
+            emit deviceCodeFlowError();
+        } else if (success) {
+            // flow successfully completed
+            emit onCompletedCodeFlow(m_accessToken, m_refreshToken);
+            // cleanup
+            stopPolling();
+            cleanupDeviceCodeFlow();
+        }
+        reply->deleteLater();
+    });
+}
+
+void VsDeviceCodeFlow::onDeviceCodeExpired()
+{
+    emit deviceCodeFlowTimedOut();
+
+    std::cout << "Device Code has expired." << std::endl;
+    stopPolling();
+    cleanupDeviceCodeFlow();
+}
+
+void VsDeviceCodeFlow::cancelCodeFlow()
+{
+    stopPolling();
+    cleanupDeviceCodeFlow();
+}
+
+bool VsDeviceCodeFlow::processDeviceCodeNetworkReply(QNetworkReply* reply)
+{
+    QByteArray buffer = reply->readAll();
+
+    // Error: failed to get device code
+    if (reply->error()) {
+        std::cout << "Failed to get device code: " << buffer.toStdString() << std::endl;
+        m_authenticationError = true;
+        return false;
+    }
+
+    // parse JSON from string response
+    QJsonParseError parseError;
+    QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError);
+    if (parseError.error) {
+        std::cout << "Error parsing JSON for Device Code: "
+                  << parseError.errorString().toStdString() << std::endl;
+        m_authenticationError = true;
+        return false;
+    }
+
+    // get fields
+    QJsonObject object = data.object();
+    m_deviceCode       = object.value(QLatin1String("device_code")).toString();
+    m_userCode         = object.value(QLatin1String("user_code")).toString();
+    m_verificationUri  = object.value(QLatin1String("verification_uri")).toString();
+    m_verificationUriComplete =
+        object.value(QLatin1String("verification_uri_complete")).toString();
+    m_pollingInterval =
+        object.value(QLatin1String("interval")).toInt(2);  // default to 2s
+    m_deviceCodeValidityDuration =
+        object.value(QLatin1String("expires_in")).toInt(900);  // default to 900s
+
+    // return true if success
+    return true;
+}
+
+bool VsDeviceCodeFlow::processPollingOAuthTokenNetworkReply(QNetworkReply* reply)
+{
+    QByteArray buffer = reply->readAll();
+
+    // Error: failed to get device code (this is expected)
+    if (reply->error()) {
+        return false;
+    }
+
+    // parse JSON from string response
+    QJsonParseError parseError;
+    QJsonDocument data = QJsonDocument::fromJson(buffer, &parseError);
+    if (parseError.error) {
+        std::cout << "Error parsing JSON for access token: "
+                  << parseError.errorString().toStdString() << std::endl;
+        return false;
+    }
+
+    // get fields
+    QJsonObject object    = data.object();
+    m_idToken             = object.value(QLatin1String("id_token")).toString();
+    m_accessToken         = object.value(QLatin1String("access_token")).toString();
+    m_refreshToken        = object.value(QLatin1String("refresh_token")).toString();
+    m_authenticationError = false;
+
+    // return true if success
+    return true;
+}
+
+void VsDeviceCodeFlow::cleanupDeviceCodeFlow()
+{
+    m_deviceCode              = QStringLiteral("");
+    m_userCode                = QStringLiteral("");
+    m_verificationUri         = QStringLiteral("https://auth.jacktrip.org/activate");
+    m_verificationUriComplete = QStringLiteral("");
+
+    m_pollingInterval            = -1;
+    m_deviceCodeValidityDuration = -1;
+}
+
+QString VsDeviceCodeFlow::accessToken()
+{
+    return m_accessToken;
+}
\ No newline at end of file
diff --git a/src/gui/vsDeviceCodeFlow.h b/src/gui/vsDeviceCodeFlow.h
new file mode 100644 (file)
index 0000000..eaadc7a
--- /dev/null
@@ -0,0 +1,109 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsDeviceCodeFlow.h
+ * \author Dominick Hing
+ * \date May 2023
+ */
+
+#ifndef VSDEVICECODEFLOW_H
+#define VSDEVICECODEFLOW_H
+
+#include <QEventLoop>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonParseError>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QSettings>
+#include <QString>
+#include <QTimer>
+#include <iostream>
+
+#include "vsDeviceCodeFlow.h"
+
+class VsDeviceCodeFlow : public QObject
+{
+    Q_OBJECT
+
+   public:
+    explicit VsDeviceCodeFlow(QNetworkAccessManager* networkAccessManager);
+    virtual ~VsDeviceCodeFlow() { stopPolling(); }
+
+    void grant();
+    void refreshAccessToken(){};
+    void initDeviceAuthorizationCodeFlow();
+
+    bool processDeviceCodeNetworkReply(QNetworkReply* reply);
+    bool processPollingOAuthTokenNetworkReply(QNetworkReply* reply);
+    void startPolling();
+    void stopPolling();
+    void onPollingTimerTick();
+    void onDeviceCodeExpired();
+    void cancelCodeFlow();
+    void cleanupDeviceCodeFlow();
+
+    bool authenticated();
+    QString accessToken();
+
+   signals:
+    void deviceCodeFlowInitialized(QString code, QString verificationUrl);
+    void deviceCodeFlowError();
+    void deviceCodeFlowTimedOut();
+    void onCompletedCodeFlow(QString accessToken, QString refreshToken);
+
+   private:
+    QString m_clientId;
+    QString m_audience;
+    QString m_authorizationServerHost;
+
+    // state used specifically in the device code flow
+    QString m_deviceCode;
+    QString m_userCode;
+    QString m_verificationUri;
+    QString m_verificationUriComplete;
+    int m_pollingInterval            = -1;  // seconds
+    int m_deviceCodeValidityDuration = -1;  // seconds
+
+    QTimer m_tokenPollingTimer;
+    QTimer m_deviceFlowExpirationTimer;
+
+    // authentication state variables
+    bool m_authenticationError;
+    QString m_refreshToken;
+    QString m_accessToken;
+    QString m_idToken;
+
+    QScopedPointer<QNetworkAccessManager> m_netManager;
+};
+
+#endif  // VSDEVICECODEFLOW
\ No newline at end of file
diff --git a/src/gui/vsMacPermissions.h b/src/gui/vsMacPermissions.h
new file mode 100644 (file)
index 0000000..1360fa5
--- /dev/null
@@ -0,0 +1,64 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+/**
+ * \file vsMacPermissions.h
+ * \author Matt Horton
+ * \date Oct 2022
+ */
+
+#ifndef __VSMACPERMISSIONS_H__
+#define __VSMACPERMISSIONS_H__
+
+#include <objc/objc.h>
+
+#include <QDebug>
+#include <QObject>
+#include <QString>
+
+#include "vsPermissions.h"
+
+class VsMacPermissions : public VsPermissions
+{
+    Q_OBJECT
+
+   public:
+    explicit VsMacPermissions();
+
+    bool micPermissionChecked() override;
+    Q_INVOKABLE void getMicPermission() override;
+    Q_INVOKABLE void openSystemPrivacy();
+
+   private:
+    QString m_micPermission     = "unknown";
+    bool m_micPermissionChecked = false;
+};
+
+#endif  // __VSMACPERMISSIONS_H__
diff --git a/src/gui/vsMacPermissions.mm b/src/gui/vsMacPermissions.mm
new file mode 100644 (file)
index 0000000..a29c4ba
--- /dev/null
@@ -0,0 +1,104 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+/**
+ * \file vsMacPermissions.mm
+ * \author Matt Horton
+ * \date Oct 2022
+ */
+
+#include "vsMacPermissions.h"
+#include <Foundation/Foundation.h>
+#include <AVFoundation/AVFoundation.h>
+#include <QDesktopServices>
+#include <QSettings>
+#include <QUrl>
+
+VsMacPermissions::VsMacPermissions()
+{
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    m_micPermissionChecked = settings.value(QStringLiteral("MicPermissionChecked"), false).toBool();
+    settings.endGroup();
+}
+
+bool VsMacPermissions::micPermissionChecked()
+{
+    if (m_micPermissionChecked) {
+        getMicPermission();
+    }
+    return m_micPermissionChecked;
+}
+
+void VsMacPermissions::getMicPermission()
+{
+    if (@available(macOS 10.14, *)) {
+        // Request permission to access.
+        switch ([AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio])
+        {
+            case AVAuthorizationStatusAuthorized:
+            {
+                // The user has previously granted access.
+                setMicPermission(QStringLiteral("granted"));
+                break;
+            }
+            case AVAuthorizationStatusNotDetermined:
+            {
+                // The app hasn't yet asked the user for access.
+                [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
+                    if (granted) {
+                        setMicPermission(QStringLiteral("granted"));
+                    } else {
+                        setMicPermission(QStringLiteral("denied"));
+                    }
+                }];
+                setMicPermission(QStringLiteral("unknown"));
+                break;
+            }
+            case AVAuthorizationStatusDenied:
+            {
+                // The user has previously denied access.
+                setMicPermission(QStringLiteral("denied"));
+            }
+            case AVAuthorizationStatusRestricted:
+            {
+                // The user can't grant access due to restrictions.
+                setMicPermission(QStringLiteral("denied"));
+            }
+        }
+    } else {
+        setMicPermission(QStringLiteral("granted"));
+    }
+}
+
+void VsMacPermissions::openSystemPrivacy()
+{
+    QDesktopServices::openUrl(QUrl("x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone"));
+}
diff --git a/src/gui/vsPermissions.cpp b/src/gui/vsPermissions.cpp
new file mode 100644 (file)
index 0000000..a978a5a
--- /dev/null
@@ -0,0 +1,68 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+/**
+ * \file vsPermissions.mm
+ * \author Matt Horton
+ * \date Oct 2022
+ */
+
+#include "vsPermissions.h"
+
+#include <QDesktopServices>
+#include <QSettings>
+#include <QUrl>
+
+QString VsPermissions::micPermission()
+{
+    return m_micPermission;
+}
+
+bool VsPermissions::micPermissionChecked()
+{
+    return m_micPermissionChecked;
+}
+
+void VsPermissions::getMicPermission()
+{
+    setMicPermission("granted");
+}
+
+void VsPermissions::setMicPermission(QString status)
+{
+    m_micPermission        = status;
+    m_micPermissionChecked = true;
+    emit micPermissionUpdated();
+
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("MicPermissionChecked"), m_micPermissionChecked);
+    settings.endGroup();
+}
diff --git a/src/gui/vsPermissions.h b/src/gui/vsPermissions.h
new file mode 100644 (file)
index 0000000..cec2d97
--- /dev/null
@@ -0,0 +1,70 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+/**
+ * \file vsPermissions.h
+ * \author Matt Horton
+ * \date Nov 2022
+ */
+
+#ifndef __VSPERMISSIONS_H__
+#define __VSPERMISSIONS_H__
+
+#include <QDebug>
+#include <QObject>
+#include <QString>
+
+class VsPermissions : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(QString micPermission READ micPermission NOTIFY micPermissionUpdated)
+
+   public:
+    VsPermissions() = default;  // define here and there
+
+    QString micPermission();              // define here
+    virtual bool micPermissionChecked();  // define here and there
+    Q_INVOKABLE virtual void getMicPermission();
+    void setMicPermission(QString status);  // define here
+
+   signals:
+    void micPermissionUpdated();  // leave here
+
+   protected:
+#if __APPLE__
+    QString m_micPermission     = "unknown";
+    bool m_micPermissionChecked = false;
+#else
+    QString m_micPermission     = "granted";
+    bool m_micPermissionChecked = true;
+#endif
+};
+
+#endif  // __VSPERMISSIONS_H__
diff --git a/src/gui/vsPing.cpp b/src/gui/vsPing.cpp
new file mode 100644 (file)
index 0000000..2d1ec56
--- /dev/null
@@ -0,0 +1,82 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsPinger.cpp
+ * \author Dominick Hing
+ * \date July 2022
+ */
+
+#include "vsPing.h"
+
+#include <iostream>
+
+using std::cout;
+using std::endl;
+
+// NOTE: It's better not to use
+// using namespace std;
+// because some functions (like exit()) get confused with QT functions
+
+//*******************************************************************************
+VsPing::VsPing(uint32_t pingNum, uint32_t timeout_msec) : mPingNumber(pingNum)
+{
+    connect(&mTimer, &QTimer::timeout, this, &VsPing::onTimeout);
+
+    mTimer.setTimerType(Qt::PreciseTimer);
+    mTimer.setSingleShot(true);
+    mTimer.setInterval(timeout_msec);
+    mTimer.start();
+}
+
+void VsPing::send()
+{
+    QDateTime now = QDateTime::currentDateTime();
+    mSent         = now;
+}
+
+void VsPing::receive()
+{
+    QDateTime now = QDateTime::currentDateTime();
+    if (!mTimedOut) {
+        mTimer.stop();
+        mReceivedReply = true;
+        mReceived      = now;
+    }
+}
+
+void VsPing::onTimeout()
+{
+    if (!mReceivedReply) {
+        mTimedOut = true;
+        emit timeout(mPingNumber);
+    }
+}
diff --git a/src/gui/vsPing.h b/src/gui/vsPing.h
new file mode 100644 (file)
index 0000000..228f814
--- /dev/null
@@ -0,0 +1,84 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsPing.h
+ * \author Dominick Hing
+ * \date July 2022
+ */
+
+#ifndef VSPING_H
+#define VSPING_H
+
+#include <QAbstractSocket>
+#include <QDateTime>
+#include <QObject>
+#include <QTimer>
+#include <QtWebSockets>
+#include <stdexcept>
+
+/** \brief A helper class for VsPinger
+ *
+ */
+class VsPing : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    explicit VsPing(uint32_t pingNum, uint32_t timeout_msec);
+    virtual ~VsPing() { mTimer.stop(); }
+    uint32_t pingNumber() { return mPingNumber; }
+
+    QDateTime sentTimestamp() { return mSent; }
+    QDateTime receivedTimestamp() { return mReceived; }
+    bool receivedReply() { return mReceivedReply; }
+    bool timedOut() { return mTimedOut; }
+
+    void send();
+    void receive();
+
+   private:
+    uint32_t mPingNumber;
+    QDateTime mSent;
+    QDateTime mReceived;
+
+    QTimer mTimer;
+    bool mTimedOut      = false;
+    bool mReceivedReply = false;
+
+   public slots:
+    void onTimeout();
+
+   signals:
+    void timeout(uint32_t pingNum);
+};
+
+#endif  // VSPING_H
\ No newline at end of file
diff --git a/src/gui/vsPinger.cpp b/src/gui/vsPinger.cpp
new file mode 100644 (file)
index 0000000..95207b6
--- /dev/null
@@ -0,0 +1,317 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsPinger.cpp
+ * \author Dominick Hing
+ * \date July 2022
+ */
+
+#include "vsPinger.h"
+
+#include <iostream>
+
+using std::cout;
+using std::endl;
+
+// NOTE: It's better not to use
+// using namespace std;
+// because some functions (like exit()) get confused with QT functions
+
+//*******************************************************************************
+VsPinger::VsPinger(QString scheme, QString host, QString path)
+{
+    mURL.setScheme(scheme);
+    mURL.setHost(host);
+    mURL.setPath(path);
+
+    mTimer.setTimerType(Qt::PreciseTimer);
+
+    connect(&mSocket, &QWebSocket::binaryMessageReceived, this,
+            &VsPinger::onReceivePingMessage);
+    connect(&mSocket, &QWebSocket::connected, this, &VsPinger::onConnected);
+#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
+    connect(&mSocket,
+            QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::errorOccurred), this,
+            &VsPinger::onError);
+#else
+    connect(&mSocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
+            this, &VsPinger::onError);
+#endif
+    connect(&mTimer, &QTimer::timeout, this, &VsPinger::onPingTimer);
+}
+
+//*******************************************************************************
+void VsPinger::start()
+{
+    // fail to start if no token is supplied
+    if (mToken.toStdString() == "") {
+        std::cout << "Error: auth token is not set" << std::endl;
+        return;
+    }
+
+    mTimer.setInterval(mPingInterval);
+
+    QString authVal = "Bearer ";
+    authVal.append(mToken);
+
+    QNetworkRequest req = QNetworkRequest(QUrl(mURL));
+    req.setRawHeader(QByteArray("Upgrade"), QByteArray("websocket"));
+    req.setRawHeader(QByteArray("Connection"), QByteArray("upgrade"));
+    req.setRawHeader(QByteArray("Authorization"), authVal.toUtf8());
+    mSocket.open(req);
+
+    mStarted = true;
+}
+
+//*******************************************************************************
+void VsPinger::stop()
+{
+    mStarted = false;
+    mError   = false;
+    mTimer.stop();
+    mSocket.close(QWebSocketProtocol::CloseCodeNormal, NULL);
+}
+
+//*******************************************************************************
+void VsPinger::setToken(QString token)
+{
+    if (mStarted) {
+        std::cout << "Error: cannot set token while pinger is active." << std::endl;
+        return;
+    }
+
+    mToken      = token;
+    mAuthorized = true;
+};
+
+//*******************************************************************************
+void VsPinger::unsetToken()
+{
+    if (mStarted) {
+        std::cout << "Error: cannot unset token while pinger is active." << std::endl;
+        return;
+    }
+
+    mToken      = QString();
+    mAuthorized = false;
+}
+
+//*******************************************************************************
+void VsPinger::sendPingMessage(const QByteArray& message)
+{
+    if (mAuthorized && !mError) {
+        mSocket.sendBinaryMessage(message);
+    }
+}
+
+//*******************************************************************************
+void VsPinger::updateStats()
+{
+    PingStat stat;
+    stat.packetsReceived = 0;
+    stat.packetsSent     = 0;
+
+    uint32_t count = 0;
+
+    std::vector<uint32_t> vec_expired;
+    std::vector<qint64> vec_rtt;
+    std::map<uint32_t, VsPing*>::reverse_iterator it;
+    for (it = mPings.rbegin(); it != mPings.rend(); ++it) {
+        VsPing* ping = it->second;
+
+        // mark this ping as ready to delete, since it will no longer be used in stats
+        if (count >= mPingNumPerInterval) {
+            vec_expired.push_back(ping->pingNumber());
+            count++;
+        } else if (ping->timedOut() || ping->receivedReply()) {
+            // Only include in statistics pings that have timed out or been received.
+            // All others are pending and are not considered in statistics
+            stat.packetsSent++;
+            if (ping->receivedReply()) {
+                stat.packetsReceived++;
+            }
+
+            QDateTime sent     = ping->sentTimestamp();
+            QDateTime received = ping->receivedTimestamp();
+            qint64 diff        = sent.msecsTo(received);
+
+            // don't include case where dif = 0 in stats, mark as expired instead
+            if (diff != 0) {
+                vec_rtt.push_back(diff);
+            } else {
+                vec_expired.push_back(ping->pingNumber());
+            }
+
+            count++;
+        }
+    }
+
+    // Deleted pings marked as expired by freeing the Ping object
+    // and clearing the map item
+    for (std::vector<uint32_t>::iterator it_expired = vec_expired.begin();
+         it_expired != vec_expired.end(); it_expired++) {
+        uint32_t expiredPingNum = *it_expired;
+        delete mPings.at(expiredPingNum);
+        mPings.erase(expiredPingNum);
+    }
+
+    // Update RTT stats
+    double min_rtt    = 0.0;
+    double max_rtt    = 0.0;
+    double avg_rtt    = 0.0;
+    double stddev_rtt = 0.0;
+
+    // avoid edge case due to min_rtt and max_rtt being at the numeric limits
+    // when vector size is 0
+    if (vec_rtt.size() == 0) {
+        stat.maxRtt    = 0;
+        stat.minRtt    = 0;
+        stat.avgRtt    = 0;
+        stat.stdDevRtt = 0;
+
+        // Update mStats
+        mStats = stat;
+        return;
+    }
+
+    for (std::vector<qint64>::iterator it_rtt = vec_rtt.begin(); it_rtt != vec_rtt.end();
+         it_rtt++) {
+        double rtt = (double)*it_rtt;
+        if (rtt < min_rtt || min_rtt == 0.0) {
+            min_rtt = rtt;
+        }
+        if (rtt > max_rtt || max_rtt == 0.0) {
+            max_rtt = rtt;
+        }
+
+        avg_rtt += rtt / vec_rtt.size();
+    }
+
+    for (std::vector<qint64>::iterator it_rtt = vec_rtt.begin(); it_rtt != vec_rtt.end();
+         it_rtt++) {
+        double rtt = (double)*it_rtt;
+        stddev_rtt += (rtt - avg_rtt) * (rtt - avg_rtt);
+    }
+    stddev_rtt /= vec_rtt.size();
+    stddev_rtt = sqrt(stddev_rtt);
+
+    stat.maxRtt    = max_rtt;
+    stat.minRtt    = min_rtt;
+    stat.avgRtt    = avg_rtt;
+    stat.stdDevRtt = stddev_rtt;
+
+    // Update mStats
+    mStats = stat;
+    return;
+}
+
+//*******************************************************************************
+VsPinger::PingStat VsPinger::getPingStats()
+{
+    return mStats;
+}
+
+//*******************************************************************************
+void VsPinger::onError(QAbstractSocket::SocketError error)
+{
+    cout << "WebSocket Error: " << error << endl;
+    mError   = true;
+    mStarted = false;
+    mTimer.stop();
+}
+
+//*******************************************************************************
+void VsPinger::onConnected()
+{
+    // start the ping timer after the connection is established
+    mTimer.start();
+}
+
+//*******************************************************************************
+void VsPinger::onPingTimer()
+{
+    updateStats();
+
+    QByteArray bytes = QByteArray::number(mPingCount);
+    QDateTime now    = QDateTime::currentDateTime();
+    this->sendPingMessage(bytes);
+
+    VsPing* ping = new VsPing(mPingCount, mPingInterval);
+    ping->send();
+    mPings[mPingCount] = ping;
+
+    connect(ping, &VsPing::timeout, this, &VsPinger::onPingTimeout);
+
+    mLastPacketSent = mPingCount;
+    mPingCount++;
+}
+
+//*******************************************************************************
+void VsPinger::onPingTimeout(uint32_t pingNum)
+{
+    std::map<uint32_t, VsPing*>::iterator it = mPings.find(pingNum);
+    if (it == mPings.end()) {
+        return;
+    }
+
+    updateStats();
+}
+
+//*******************************************************************************
+void VsPinger::onReceivePingMessage(const QByteArray& message)
+{
+    QDateTime now    = QDateTime::currentDateTime();
+    uint32_t pingNum = message.toUInt();
+
+    // locate the appropriate corresponding ping message
+    std::map<uint32_t, VsPing*>::iterator it = mPings.find(pingNum);
+    if (it == mPings.end()) {
+        return;
+    }
+
+    VsPing* ping = (*it).second;
+
+    // do not apply to pings that have timed out
+    if (!ping->timedOut()) {
+        // update ping data
+        ping->receive();
+
+        // update vsPinger
+        mHasReceivedPing    = true;
+        mLastPacketReceived = pingNum;
+        if (pingNum > mLargestPingNumReceived) {
+            mLargestPingNumReceived = pingNum;
+        }
+    }
+
+    updateStats();
+}
\ No newline at end of file
diff --git a/src/gui/vsPinger.h b/src/gui/vsPinger.h
new file mode 100644 (file)
index 0000000..c171e7d
--- /dev/null
@@ -0,0 +1,122 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsPinger.h
+ * \author Dominick Hing
+ * \date July 2022
+ */
+
+#ifndef VSPINGER_H
+#define VSPINGER_H
+
+#include <QAbstractSocket>
+#include <QDateTime>
+#include <QObject>
+#include <QTimer>
+#include <QUrl>
+#include <QtWebSockets>
+#include <stdexcept>
+#include <vector>
+
+#include "vsPing.h"
+
+/** \brief VsPinger for generating latency statistics between
+ * Virtual Studio devices and Virtual Studio Servers
+ *
+ */
+class VsPinger : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    /** \brief The class constructor
+     * \param scheme The protocol scheme for the pinger
+     * \param host The hostname of the server
+     * \param path The path to ping the server on
+     */
+    explicit VsPinger(QString scheme, QString host, QString path);
+    virtual ~VsPinger() { stop(); }
+    void start();
+    void stop();
+    bool active() { return mStarted; };
+    void setToken(QString token);
+    void unsetToken();
+
+    struct PingStat {
+        uint32_t packetsReceived = 0;
+        uint32_t packetsSent     = 0;
+        double minRtt            = 0.0;
+        double maxRtt            = 0.0;
+        double avgRtt            = 0.0;
+        double stdDevRtt         = 0.0;
+    };
+
+    PingStat getPingStats();
+
+   private:
+    QWebSocket mSocket;
+    QUrl mURL;
+    QString mToken;
+    bool mAuthorized = false;
+    bool mStarted    = false;
+    bool mError      = false;
+
+    QTimer mTimer;
+    uint32_t mPingCount                = 0;
+    const uint32_t mPingNumPerInterval = 5;
+    const uint32_t mPingInterval       = 1000;
+    const uint32_t mPingTimeout        = 1000;
+
+    std::map<uint32_t, VsPing*> mPings;
+
+    uint32_t mLastPacketSent;
+    uint32_t mLastPacketReceived;
+    uint32_t mLargestPingNumReceived =
+        0;  // is 0 if no ping has been received, otherwise, is the largest ping number
+            // received
+    bool mHasReceivedPing = false;  // used for edge case where we have't received a ping
+                                    // yet (mLargestPingNumReceived = 0)
+
+    PingStat mStats;
+
+    void sendPingMessage(const QByteArray& message);
+    void updateStats();
+
+   private slots:
+    void onError(QAbstractSocket::SocketError error);
+    void onConnected();
+    void onPingTimer();
+    void onPingTimeout(uint32_t pingNum);
+    void onReceivePingMessage(const QByteArray& message);
+};
+
+#endif  // VSPINGER_H
\ No newline at end of file
diff --git a/src/gui/vsQmlClipboard.h b/src/gui/vsQmlClipboard.h
new file mode 100644 (file)
index 0000000..285b350
--- /dev/null
@@ -0,0 +1,26 @@
+#ifndef VSQMLCLIPBOARD_H
+#define VSQMLCLIPBOARD_H
+
+#include <QApplication>
+#include <QClipboard>
+#include <QObject>
+
+class VsQmlClipboard : public QObject
+{
+    Q_OBJECT
+   public:
+    explicit VsQmlClipboard(QObject* parent = 0) : QObject(parent)
+    {
+        clipboard = QApplication::clipboard();
+    }
+
+    Q_INVOKABLE void setText(QString text)
+    {
+        clipboard->setText(text, QClipboard::Clipboard);
+    }
+
+   private:
+    QClipboard* clipboard;
+};
+
+#endif  // VSQMLCLIPBOARD_H
\ No newline at end of file
diff --git a/src/gui/vsQuickView.cpp b/src/gui/vsQuickView.cpp
new file mode 100644 (file)
index 0000000..e0a9281
--- /dev/null
@@ -0,0 +1,68 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsQuickView.cpp
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#include "vsQuickView.h"
+
+#include <QDesktopServices>
+#include <iostream>
+
+VsQuickView::VsQuickView(QWindow* parent) : QQuickView(parent)
+{
+#ifdef Q_OS_MACOS
+    auto* quit = new QAction("&Quit", this);
+
+    QMenuBar* menuBar = new QMenuBar(nullptr);
+    QMenu* appName    = menuBar->addMenu("&JackTrip");
+    appName->addAction(quit);
+
+    connect(quit, &QAction::triggered, this, &VsQuickView::closeWindow);
+#endif
+}
+
+bool VsQuickView::event(QEvent* event)
+{
+    if (event->type() == QEvent::Close || event->type() == QEvent::Quit) {
+        emit windowClose();
+        event->ignore();
+    }
+    return QQuickView::event(event);
+}
+
+void VsQuickView::closeWindow()
+{
+    emit windowClose();
+}
diff --git a/src/gui/vsQuickView.h b/src/gui/vsQuickView.h
new file mode 100644 (file)
index 0000000..ab80a73
--- /dev/null
@@ -0,0 +1,64 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsQuickView.h
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#ifndef VSQUICKVIEW_H
+#define VSQUICKVIEW_H
+
+#include <QQuickView>
+#ifdef Q_OS_MACOS
+#include <QAction>
+#include <QMenu>
+#include <QMenuBar>
+#include <QObject>
+#endif
+
+class VsQuickView : public QQuickView
+{
+    Q_OBJECT
+
+   public:
+    VsQuickView(QWindow* parent = nullptr);
+    bool event(QEvent* event) override;
+
+   signals:
+    void windowClose();
+
+   private slots:
+    void closeWindow();
+};
+
+#endif  // VSQUICKVIEW_H
diff --git a/src/gui/vsServerInfo.cpp b/src/gui/vsServerInfo.cpp
new file mode 100644 (file)
index 0000000..9ea5010
--- /dev/null
@@ -0,0 +1,321 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsServerInfo.cpp
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#include "vsServerInfo.h"
+
+VsServerInfo::VsServerInfo(QObject* parent) : QObject(parent) {}
+
+VsServerInfo& VsServerInfo::operator=(const VsServerInfo& info)
+{
+    m_section     = info.m_section;
+    m_name        = info.m_name;
+    m_host        = info.m_host;
+    m_port        = info.m_port;
+    m_enabled     = info.m_enabled;
+    m_owner       = info.m_owner;
+    m_admin       = info.m_admin;
+    m_isManaged   = info.m_isManaged;
+    m_isPublic    = info.m_isPublic;
+    m_region      = info.m_region;
+    m_period      = info.m_period;
+    m_sampleRate  = info.m_sampleRate;
+    m_queueBuffer = info.m_queueBuffer;
+    m_bannerURL   = info.m_bannerURL;
+    m_id          = info.m_id;
+    m_sessionId   = info.m_sessionId;
+    m_streamId    = info.m_streamId;
+    m_status      = info.m_status;
+    m_cloudId     = info.m_cloudId;
+    m_inviteKey   = info.m_inviteKey;
+    return *this;
+}
+
+VsServerInfo::serverSectionT VsServerInfo::section()
+{
+    return m_section;
+}
+
+QString VsServerInfo::type() const
+{
+    if (m_section == YOUR_STUDIOS) {
+        return QStringLiteral("Your Studios");
+    } else if (m_section == SUBSCRIBED_STUDIOS) {
+        return QStringLiteral("Subscribed Studios");
+    } else {
+        return QStringLiteral("Public Studios");
+    }
+}
+
+void VsServerInfo::setSection(serverSectionT section)
+{
+    m_section = section;
+}
+
+QString VsServerInfo::name() const
+{
+    return m_name;
+}
+
+void VsServerInfo::setName(const QString& name)
+{
+    m_name = name;
+}
+
+QString VsServerInfo::host() const
+{
+    return m_host;
+}
+
+QString VsServerInfo::status() const
+{
+    return m_status;
+}
+
+bool VsServerInfo::canConnect() const
+{
+    return !m_host.isEmpty() && m_status == "Ready";
+}
+
+bool VsServerInfo::canStart() const
+{
+    return m_owner || m_admin;
+}
+
+void VsServerInfo::setHost(const QString& host)
+{
+    m_host = host;
+    emit canConnectChanged();
+}
+
+void VsServerInfo::setStatus(const QString& status)
+{
+    m_status = status;
+    emit canConnectChanged();
+}
+
+quint16 VsServerInfo::port() const
+{
+    return m_port;
+}
+
+void VsServerInfo::setPort(quint16 port)
+{
+    m_port = port;
+}
+
+bool VsServerInfo::enabled() const
+{
+    return m_enabled;
+}
+
+void VsServerInfo::setEnabled(bool enabled)
+{
+    m_enabled = enabled;
+}
+
+bool VsServerInfo::isOwner() const
+{
+    return m_owner;
+}
+
+void VsServerInfo::setIsOwner(bool owner)
+{
+    m_owner = owner;
+}
+
+bool VsServerInfo::isAdmin() const
+{
+    return m_admin;
+}
+
+void VsServerInfo::setIsAdmin(bool admin)
+{
+    m_admin = admin;
+}
+
+bool VsServerInfo::isPublic() const
+{
+    return m_isPublic;
+}
+
+void VsServerInfo::setIsPublic(bool isPublic)
+{
+    m_isPublic = isPublic;
+}
+
+QString VsServerInfo::region() const
+{
+    return m_region;
+}
+
+QString VsServerInfo::flag() const
+{
+    QStringList parts = m_region.split(QStringLiteral("-"));
+    if (parts.count() > 1) {
+        QString countryCode = parts.at(1).toUpper();
+        if (countryCode == QStringLiteral("TF")) {
+            countryCode = QStringLiteral("TW");
+        }
+        return QStringLiteral("flags/%1.svg").arg(countryCode);
+    }
+    // Have a fallback here
+    return QStringLiteral("flags/US.svg");
+}
+
+QString VsServerInfo::location() const
+{
+    return m_region;
+}
+
+void VsServerInfo::setRegion(const QString& region)
+{
+    m_region = region;
+}
+
+bool VsServerInfo::isManaged() const
+{
+    return m_isManaged;
+}
+
+void VsServerInfo::setIsManaged(bool isManaged)
+{
+    m_isManaged = isManaged;
+}
+
+quint16 VsServerInfo::period() const
+{
+    return m_period;
+}
+
+void VsServerInfo::setPeriod(quint16 period)
+{
+    m_period = period;
+}
+
+quint32 VsServerInfo::sampleRate() const
+{
+    return m_sampleRate;
+}
+
+void VsServerInfo::setSampleRate(quint32 sampleRate)
+{
+    m_sampleRate = sampleRate;
+}
+
+quint16 VsServerInfo::queueBuffer() const
+{
+    return m_queueBuffer;
+}
+
+void VsServerInfo::setQueueBuffer(quint16 queueBuffer)
+{
+    m_queueBuffer = queueBuffer;
+}
+
+QString VsServerInfo::bannerURL() const
+{
+    return m_bannerURL;
+}
+
+void VsServerInfo::setBannerURL(const QString& bannerURL)
+{
+    m_bannerURL = bannerURL;
+}
+
+QString VsServerInfo::id() const
+{
+    return m_id;
+}
+
+void VsServerInfo::setId(const QString& id)
+{
+    m_id = id;
+}
+
+QString VsServerInfo::sessionId() const
+{
+    return m_sessionId;
+}
+
+void VsServerInfo::setSessionId(const QString& sessionId)
+{
+    m_sessionId = (sessionId == "undefined") ? "" : sessionId;
+}
+
+QString VsServerInfo::streamId() const
+{
+    return m_streamId;
+}
+
+void VsServerInfo::setStreamId(const QString& streamId)
+{
+    m_streamId = (streamId == "undefined") ? "" : streamId;
+}
+
+QString VsServerInfo::inviteKey() const
+{
+    return m_inviteKey;
+}
+
+void VsServerInfo::setInviteKey(const QString& inviteKey)
+{
+    m_inviteKey = inviteKey;
+}
+
+QString VsServerInfo::cloudId() const
+{
+    return m_cloudId;
+}
+
+void VsServerInfo::setCloudId(const QString& cloudId)
+{
+    m_cloudId = cloudId;
+}
+
+bool VsServerInfo::operator<(const VsServerInfo& other) const
+{
+    if (status() == QStringLiteral("Ready")) {
+        if (other.status() != QStringLiteral("Ready")) {
+            return true;
+        }
+    } else if (other.status() == QStringLiteral("Ready")) {
+        return false;
+    }
+    return name() < other.name();
+}
+
+VsServerInfo::~VsServerInfo() = default;
diff --git a/src/gui/vsServerInfo.h b/src/gui/vsServerInfo.h
new file mode 100644 (file)
index 0000000..5112c64
--- /dev/null
@@ -0,0 +1,164 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsServerInfo.h
+ * \author Aaron Wyatt
+ * \date March 2022
+ */
+
+#ifndef VSSERVERINFO_H
+#define VSSERVERINFO_H
+
+#include <QObject>
+
+class VsServerInfo : public QObject
+{
+    Q_OBJECT
+
+    Q_PROPERTY(QString type READ type CONSTANT)
+    Q_PROPERTY(QString name READ name CONSTANT)
+    // Q_PROPERTY(QString host READ host CONSTANT)
+    Q_PROPERTY(bool canConnect READ canConnect NOTIFY canConnectChanged)
+    Q_PROPERTY(bool canStart READ canStart CONSTANT)
+    // Q_PROPERTY(quint16 port READ port CONSTANT)
+    Q_PROPERTY(bool isPublic READ isPublic CONSTANT)
+    Q_PROPERTY(QString flag READ flag CONSTANT)
+    Q_PROPERTY(QString bannerURL READ bannerURL CONSTANT)
+    Q_PROPERTY(QString location READ location CONSTANT)
+    Q_PROPERTY(bool isAdmin READ isAdmin CONSTANT)
+    Q_PROPERTY(bool isManaged READ isManaged CONSTANT)
+    Q_PROPERTY(quint16 period READ period CONSTANT)
+    Q_PROPERTY(quint32 sampleRate READ sampleRate CONSTANT)
+    Q_PROPERTY(quint16 queueBuffer READ queueBuffer CONSTANT)
+    Q_PROPERTY(QString sessionId READ sessionId CONSTANT)
+    Q_PROPERTY(QString streamId READ streamId CONSTANT)
+    Q_PROPERTY(QString status READ status CONSTANT)
+    Q_PROPERTY(bool enabled READ enabled CONSTANT)
+    Q_PROPERTY(QString cloudId READ cloudId CONSTANT)
+    Q_PROPERTY(QString id READ id CONSTANT)
+    Q_PROPERTY(QString inviteKey READ inviteKey CONSTANT)
+
+   public:
+    enum serverSectionT { YOUR_STUDIOS, SUBSCRIBED_STUDIOS, PUBLIC_STUDIOS };
+
+    explicit VsServerInfo(QObject* parent = nullptr);
+    VsServerInfo& operator=(const VsServerInfo& info);
+    ~VsServerInfo() override;
+
+    serverSectionT section();
+    QString type() const;
+    void setSection(serverSectionT section);
+    QString name() const;
+    void setName(const QString& name);
+    QString host() const;
+    bool canConnect() const;
+    bool canStart() const;
+    void setHost(const QString& host);
+    quint16 port() const;
+    void setPort(quint16 port);
+    bool enabled() const;
+    void setEnabled(bool enabled);
+    bool isOwner() const;
+    void setIsOwner(bool owner);
+    bool isAdmin() const;
+    void setIsAdmin(bool admin);
+    bool isPublic() const;
+    void setIsPublic(bool isPublic);
+    QString region() const;
+    QString flag() const;
+    QString location() const;
+    void setRegion(const QString& region);
+    bool isManaged() const;
+    void setIsManaged(bool isManageable);
+    quint16 period() const;
+    void setPeriod(quint16 period);
+    quint32 sampleRate() const;
+    void setSampleRate(quint32 sampleRate);
+    quint16 queueBuffer() const;
+    void setQueueBuffer(quint16 queueBuffer);
+    QString bannerURL() const;
+    void setBannerURL(const QString& bannerURL);
+    QString id() const;
+    void setId(const QString& id);
+    QString sessionId() const;
+    void setSessionId(const QString& sessionId);
+    QString streamId() const;
+    void setStreamId(const QString& streamId);
+    QString status() const;
+    void setStatus(const QString& status);
+    QString inviteKey() const;
+    void setInviteKey(const QString& inviteKey);
+    QString cloudId() const;
+    void setCloudId(const QString& cloudId);
+    bool operator<(const VsServerInfo& other) const;
+
+   signals:
+    void canConnectChanged();
+
+   private:
+    serverSectionT m_section = PUBLIC_STUDIOS;
+    QString m_name;
+    QString m_host;
+    quint16 m_port;
+    bool m_enabled;
+    bool m_owner;
+    bool m_admin;
+    bool m_isManaged;
+    bool m_isPublic;
+    QString m_region;
+    quint16 m_period;
+    quint32 m_sampleRate;
+    quint16 m_queueBuffer;
+    QString m_bannerURL;
+    QString m_id;
+    QString m_sessionId;
+    QString m_streamId;
+    QString m_status;
+    QString m_cloudId;
+    QString m_inviteKey;
+
+    /* Remaining JSON fields
+    "loopback": true,
+    "stereo": true,
+    "type": "JackTrip",
+    "size": "c5.large",
+    "mixBranch": "main",
+    "mixCode": "SimpleMix(~maxClients).masterVolume_(1).connect.start;",
+    "ownerId": "string",
+    "subStatus": "Active",
+    "createdAt": "2021-09-07T17:15:38Z",
+    "expiresAt": "2021-09-07T17:15:38Z",
+    "updatedAt": "2021-09-07T17:15:38Z"
+    */
+};
+
+#endif  // VSSERVERINFO_H
diff --git a/src/gui/vsWebSocket.cpp b/src/gui/vsWebSocket.cpp
new file mode 100644 (file)
index 0000000..d3ef34d
--- /dev/null
@@ -0,0 +1,146 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsWebSocket.cpp
+ * \author Matt Horton
+ * \date June 2022
+ */
+
+#include "vsWebSocket.h"
+
+#include <QDebug>
+#include <iostream>
+
+// Constructor
+VsWebSocket::VsWebSocket(const QUrl& url, QString token, QString apiPrefix,
+                         QString apiSecret, QObject* parent)
+    : QObject(parent)
+    , m_url(url)
+    , m_token(token)
+    , m_apiPrefix(apiPrefix)
+    , m_apiSecret(apiSecret)
+{
+    m_webSocket.reset(new QWebSocket());
+    connect(m_webSocket.get(), &QWebSocket::disconnected, this,
+            &VsWebSocket::disconnected);
+    connect(m_webSocket.get(),
+            QOverload<const QList<QSslError>&>::of(&QWebSocket::sslErrors), this,
+            &VsWebSocket::onSslErrors);
+#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
+    connect(m_webSocket.get(),
+            QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::errorOccurred), this,
+            &VsWebSocket::onError);
+#else
+    connect(m_webSocket.get(),
+            QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error), this,
+            &VsWebSocket::onError);
+#endif
+    connect(m_webSocket.get(), &QWebSocket::textMessageReceived, this,
+            &VsWebSocket::textMessageReceived);
+}
+
+VsWebSocket::~VsWebSocket()
+{
+    if (isValid()) {
+        closeSocket();
+    }
+    if (!m_webSocket.isNull()) {
+        m_webSocket->disconnect();
+        m_webSocket.reset();
+    }
+}
+
+void VsWebSocket::openSocket()
+{
+    if (isValid()) {
+        return;
+    }
+
+    QNetworkRequest req = QNetworkRequest(QUrl(m_url));
+    QString authVal     = "Bearer ";
+    authVal.append(m_token);
+    req.setRawHeader(QByteArray("Upgrade"), QByteArray("websocket"));
+    req.setRawHeader(QByteArray("Connection"), QByteArray("Upgrade"));
+    req.setRawHeader(QByteArray("Authorization"), authVal.toUtf8());
+    req.setRawHeader(QByteArray("Origin"), QByteArray("http://jacktrip.local"));
+    req.setRawHeader(QByteArray("APIPrefix"), m_apiPrefix.toUtf8());
+    req.setRawHeader(QByteArray("APISecret"), m_apiSecret.toUtf8());
+
+    if (!m_webSocket.isNull()) {
+        m_webSocket->open(req);
+        qDebug() << "Opened websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery);
+    }
+}
+
+void VsWebSocket::closeSocket()
+{
+    if (!m_webSocket.isNull()
+        && m_webSocket->state() != QAbstractSocket::UnconnectedState) {
+        qDebug() << "Closing websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery);
+        m_webSocket->abort();
+    }
+}
+
+void VsWebSocket::onError(QAbstractSocket::SocketError error)
+{
+    // RemoteHostClosedError may be expected due to finite connection durations
+    // ConnectionRefusedError may be expected if the server-side endpoint is closed
+    if (error != QAbstractSocket::RemoteHostClosedError) {
+        qDebug() << "Websocket error: " << error;
+    }
+    if (!m_webSocket.isNull()) {
+        m_webSocket->abort();
+    }
+}
+
+void VsWebSocket::onSslErrors(const QList<QSslError>& errors)
+{
+    for (int i = 0; i < errors.size(); ++i) {
+        qDebug() << "SSL error: " << errors.at(i);
+    }
+    if (!m_webSocket.isNull()) {
+        m_webSocket->abort();
+    }
+}
+
+void VsWebSocket::sendMessage(const QByteArray& message)
+{
+    if (isValid()) {
+        m_webSocket->sendBinaryMessage(message);
+    }
+}
+
+bool VsWebSocket::isValid()
+{
+    return !m_webSocket.isNull()
+           && m_webSocket->state() == QAbstractSocket::ConnectedState;
+}
diff --git a/src/gui/vsWebSocket.h b/src/gui/vsWebSocket.h
new file mode 100644 (file)
index 0000000..61b19c6
--- /dev/null
@@ -0,0 +1,81 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2022 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file vsWebSocket.h
+ * \author Matt Horton
+ * \date June 2022
+ */
+
+#ifndef VSWEBSOCKET_H
+#define VSWEBSOCKET_H
+
+#include <QList>
+#include <QObject>
+#include <QScopedPointer>
+#include <QSslError>
+#include <QString>
+#include <QUrl>
+#include <QtWebSockets>
+
+class VsWebSocket : public QObject
+{
+    Q_OBJECT
+
+   public:
+    // Constructor
+    explicit VsWebSocket(const QUrl& url, QString token, QString apiPrefix,
+                         QString apiSecret, QObject* parent = nullptr);
+    virtual ~VsWebSocket();
+
+    // Public functions
+    void openSocket();
+    void closeSocket();
+    void sendMessage(const QByteArray& message);
+    bool isValid();
+
+   signals:
+    void textMessageReceived(const QString& message);
+    void disconnected();
+
+   private slots:
+    void onError(QAbstractSocket::SocketError error);
+    void onSslErrors(const QList<QSslError>& errors);
+
+   private:
+    QScopedPointer<QWebSocket> m_webSocket;
+    QUrl m_url;
+    QString m_token;
+    QString m_apiPrefix;
+    QString m_apiSecret;
+};
+
+#endif  // VSWEBSOCKET_H
diff --git a/src/gui/vuMeter.cpp b/src/gui/vuMeter.cpp
new file mode 100644 (file)
index 0000000..1e19ea7
--- /dev/null
@@ -0,0 +1,78 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2022 Aaron Wyatt.
+
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#include "vuMeter.h"
+
+#include <cmath>
+
+VuMeter::VuMeter(QWidget* parent) : QWidget(parent), m_level(0)
+{
+    m_greenOn.setRgb(97, 197, 84);
+    m_greenOff.setRgb(29, 67, 24);
+    m_yellowOn.setRgb(245, 191, 79);
+    m_yellowOff.setRgb(85, 65, 22);
+    m_redOn.setRgb(242, 27, 27);
+    m_redOff.setRgb(84, 4, 4);
+
+    m_threshhold1 = std::round(m_bins * 0.6);
+    m_threshhold2 = std::round(m_bins * 0.8);
+}
+
+void VuMeter::setLevel(qreal level)
+{
+    m_level = level;
+    update();
+}
+
+void VuMeter::paintEvent([[maybe_unused]] QPaintEvent* event)
+{
+    quint32 binWidth = std::floor((width() - ((m_bins - 1) * m_margins)) / m_bins);
+    QPainter painter(this);
+
+    painter.setPen(Qt::NoPen);
+    quint32 level = std::round(m_level * (m_bins + 1));
+    for (quint32 i = 0; i < m_bins; i++) {
+        bool on = level > i;
+        if (on) {
+            if (i < m_threshhold1) {
+                painter.setBrush(m_greenOn);
+            } else if (i < m_threshhold2) {
+                painter.setBrush(m_yellowOn);
+            } else {
+                painter.setBrush(m_redOn);
+            }
+        } else {
+            if (i < m_threshhold1) {
+                painter.setBrush(m_greenOff);
+            } else if (i < m_threshhold2) {
+                painter.setBrush(m_yellowOff);
+            } else {
+                painter.setBrush(m_redOff);
+            }
+        }
+
+        painter.drawRect((binWidth + m_margins) * i, 0, binWidth, height());
+    }
+}
diff --git a/src/gui/vuMeter.h b/src/gui/vuMeter.h
new file mode 100644 (file)
index 0000000..d6d3693
--- /dev/null
@@ -0,0 +1,60 @@
+//*****************************************************************
+/*
+  QJackTrip: Bringing a graphical user interface to JackTrip, a
+  system for high quality audio network performance over the
+  internet.
+
+  Copyright (c) 2022 Aaron Wyatt.
+
+  This file is part of QJackTrip.
+
+  QJackTrip is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  QJackTrip is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with QJackTrip.  If not, see <https://www.gnu.org/licenses/>.
+*/
+//*****************************************************************
+
+#ifndef VUMETER_H
+#define VUMETER_H
+
+#include <QPainter>
+#include <QWidget>
+
+class VuMeter : public QWidget
+{
+    Q_OBJECT
+
+   public:
+    VuMeter(QWidget* parent = nullptr);
+    ~VuMeter() override = default;
+
+    void setLevel(qreal level);
+
+   protected:
+    void paintEvent(QPaintEvent* event) override;
+
+   private:
+    qreal m_level;
+    QColor m_greenOn;
+    QColor m_greenOff;
+    QColor m_yellowOn;
+    QColor m_yellowOff;
+    QColor m_redOn;
+    QColor m_redOff;
+
+    quint32 m_bins    = 30;
+    quint32 m_margins = 2;
+    quint32 m_threshhold1;
+    quint32 m_threshhold2;
+};
+
+#endif  // VUMETER_H
diff --git a/src/gui/warning.svg b/src/gui/warning.svg
new file mode 100644 (file)
index 0000000..e532c5f
--- /dev/null
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="M92-120q-9 0-15.652-4.125Q69.696-128.25 66-135q-4.167-6.6-4.583-14.3Q61-157 66-165l388-670q5-8 11.5-11.5T480-850q8 0 14.5 3.5T506-835l388 670q5 8 4.583 15.7-.416 7.7-4.583 14.3-3.696 6.75-10.348 10.875Q877-120 868-120H92Zm52-60h672L480-760 144-180Zm340.175-57q12.825 0 21.325-8.675 8.5-8.676 8.5-21.5 0-12.825-8.675-21.325-8.676-8.5-21.5-8.5-12.825 0-21.325 8.675-8.5 8.676-8.5 21.5 0 12.825 8.675 21.325 8.676 8.5 21.5 8.5Zm0-111q12.825 0 21.325-8.625T514-378v-164q0-12.75-8.675-21.375-8.676-8.625-21.5-8.625-12.825 0-21.325 8.625T454-542v164q0 12.75 8.675 21.375 8.676 8.625 21.5 8.625ZM480-470Z"/></svg>
\ No newline at end of file
diff --git a/src/gui/wedge.svg b/src/gui/wedge.svg
new file mode 100644 (file)
index 0000000..230cdd2
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="13.758333mm"
+   height="21.960417mm"
+   viewBox="0 0 13.758333 21.960417"
+   version="1.1"
+   id="svg5"
+   inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
+   sodipodi:docname="wedge.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview7"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:zoom="2.0630341"
+     inkscape:cx="-46.291044"
+     inkscape:cy="47.26049"
+     inkscape:window-width="1920"
+     inkscape:window-height="1007"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1" />
+  <defs
+     id="defs2" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       style="fill:#0c1424;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 0,0 h 13.758333 l -2.38125,21.960417 H 0 Z"
+       id="wedge"
+       sodipodi:nodetypes="ccccc" />
+  </g>
+</svg>
diff --git a/src/gui/wedge_inactive.svg b/src/gui/wedge_inactive.svg
new file mode 100644 (file)
index 0000000..68ecbcf
--- /dev/null
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="13.758333mm"
+   height="21.960417mm"
+   viewBox="0 0 13.758333 21.960417"
+   version="1.1"
+   id="svg5"
+   inkscape:version="1.1.2 (b8e25be8, 2022-02-05)"
+   sodipodi:docname="wedge_inactive.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview7"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:document-units="mm"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:zoom="4.0967594"
+     inkscape:cx="0.36614306"
+     inkscape:cy="41.008022"
+     inkscape:window-width="1312"
+     inkscape:window-height="856"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="layer1" />
+  <defs
+     id="defs2" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <path
+       style="fill:#b3b3b3;fill-opacity:1;stroke:none;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+       d="m 0,0 h 13.758333 l -2.38125,21.960417 H 0 Z"
+       id="wedge"
+       sodipodi:nodetypes="ccccc" />
+  </g>
+</svg>
diff --git a/src/jacktrip_globals.cpp b/src/jacktrip_globals.cpp
new file mode 100644 (file)
index 0000000..6a31fbf
--- /dev/null
@@ -0,0 +1,180 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file globals.cpp
+ * \author Juan-Pablo Caceres
+ * \date August 2008
+ */
+
+#include <iostream>
+
+#if defined(__linux__)
+#include <sched.h>
+#include <sys/types.h>
+#include <unistd.h>
+#endif  //__linux__
+
+#if defined(__APPLE__)
+#include <mach/mach.h>
+#include <mach/mach_time.h>
+#include <mach/thread_policy.h>
+#include <sys/qos.h>
+#endif  //__APPLE__
+
+#if defined(_WIN32)
+// Windows libraries that rely on the ordering of includes…
+// clang-format off
+#include <windows.h>
+#include <processthreadsapi.h>
+// clang-format on
+#endif
+#include "jacktrip_globals.h"
+
+#if defined(__APPLE__)
+
+// The following function is taken from the chromium source code
+// https://github.com/chromium/chromium/blob/master/base/threading/platform_thread_mac.mm
+// For the following macOS implementation of the function setRealtimeProcessPriority()
+// only: Copyright (c) 2012 The Chromium Authors. All rights reserved.
+
+// Enables time-contraint policy and priority suitable for low-latency,
+// glitch-resistant audio.
+void setRealtimeProcessPriority(int bufferSize, int sampleRate)
+{
+    // Set thread QoS to allow maximum performance.
+    pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0);
+
+    // Increase thread priority to real-time.
+
+    // Please note that the thread_policy_set() calls may fail in
+    // rare cases if the kernel decides the system is under heavy load
+    // and is unable to handle boosting the thread priority.
+    // In these cases we just return early and go on with life.
+
+    mach_port_t mach_thread_id = mach_thread_self();
+
+    // Make thread fixed priority.
+    thread_extended_policy_data_t policy;
+    policy.timeshare     = 0;  // Set to 1 for a non-fixed thread.
+    kern_return_t result = thread_policy_set(mach_thread_id, THREAD_EXTENDED_POLICY,
+                                             reinterpret_cast<thread_policy_t>(&policy),
+                                             THREAD_EXTENDED_POLICY_COUNT);
+    if (result != KERN_SUCCESS) {
+        std::cerr << "Failed to make thread fixed priority. " << result << std::endl;
+        return;
+    }
+
+    // Set to relatively high priority. (BASEPRI_FOREGROUND = 47)
+    thread_precedence_policy_data_t precedence;
+    precedence.importance = 52;
+    result                = thread_policy_set(mach_thread_id, THREAD_PRECEDENCE_POLICY,
+                                              reinterpret_cast<thread_policy_t>(&precedence),
+                                              THREAD_PRECEDENCE_POLICY_COUNT);
+    if (result != KERN_SUCCESS) {
+        std::cerr << "Failed to set thread priority. " << result << std::endl;
+        return;
+    }
+
+    // Most important, set real-time constraints.
+
+    // Define the guaranteed and max fraction of time for the audio thread.
+    // These "duty cycle" values can range from 0 to 1.  A value of 0.5
+    // means the scheduler would give half the time to the thread.
+    // These values have empirically been found to yield good behavior.
+    // Good means that audio performance is high and other threads won't starve.
+    // const double kGuaranteedAudioDutyCycle = 0.75;
+    const double kGuaranteedAudioDutyCycle = 0.5;
+    const double kMaxAudioDutyCycle        = 0.85;
+
+    // Define constants determining how much time the audio thread can
+    // use in a given time quantum.  All times are in milliseconds.
+
+    // Work out how many milliseconds we have in each buffer cycle
+    const double kTimeQuantum = 1000 * (double)bufferSize / (double)sampleRate;
+
+    // Time guaranteed each quantum.
+    const double kAudioTimeNeeded = kGuaranteedAudioDutyCycle * kTimeQuantum;
+
+    // Maximum time each quantum.
+    const double kMaxTimeAllowed = kMaxAudioDutyCycle * kTimeQuantum;
+
+    // Get the conversion factor from milliseconds to absolute time
+    // which is what the time-constraints call needs.
+    mach_timebase_info_data_t tb_info;
+    mach_timebase_info(&tb_info);
+    double ms_to_abs_time =
+        (static_cast<double>(tb_info.denom) / tb_info.numer) * 1000000;
+
+    thread_time_constraint_policy_data_t time_constraints;
+    time_constraints.period      = kTimeQuantum * ms_to_abs_time;
+    time_constraints.computation = kAudioTimeNeeded * ms_to_abs_time;
+    time_constraints.constraint  = kMaxTimeAllowed * ms_to_abs_time;
+    time_constraints.preemptible = 0;
+
+    result = thread_policy_set(mach_thread_id, THREAD_TIME_CONSTRAINT_POLICY,
+                               reinterpret_cast<thread_policy_t>(&time_constraints),
+                               THREAD_TIME_CONSTRAINT_POLICY_COUNT);
+    if (result != KERN_SUCCESS)
+        std::cerr << "Failed to set thread realtime constraints. " << result << std::endl;
+
+    return;
+}
+
+#elif defined(_WIN32)
+void setRealtimeProcessPriority()
+{
+    if (SetPriorityClass(GetCurrentProcess(), REALTIME_PRIORITY_CLASS) == 0) {
+        std::cerr << "Failed to set process priority class." << std::endl;
+    }
+    if (SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL) == 0) {
+        std::cerr << "Failed to set thread priority." << std::endl;
+    }
+}
+#else
+//*******************************************************************************
+void setRealtimeProcessPriority()
+{
+    int priority = sched_get_priority_max(SCHED_FIFO);  // 99 is the highest possible
+#ifdef __UBUNTU__
+    priority     = 95;  // anything higher is silently ignored by Ubuntu 18.04
+#endif
+    priority     = 3;
+
+    struct sched_param sp = {.sched_priority = priority};
+
+    if (sched_setscheduler(0, SCHED_FIFO, &sp) == -1) {
+        std::cerr << "Failed to set the scheduler policy and priority." << std::endl;
+        ;
+    }
+}
+
+#endif
diff --git a/src/jacktrip_globals.h b/src/jacktrip_globals.h
new file mode 100644 (file)
index 0000000..a7bc3e3
--- /dev/null
@@ -0,0 +1,146 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file jacktrip_globals.h
+ * \author Juan-Pablo Caceres
+ * \date June 2008
+ */
+
+#ifndef __JACKTRIP_GLOBALS_H__
+#define __JACKTRIP_GLOBALS_H__
+
+#include "AudioInterface.h"
+
+constexpr const char* const gVersion = "2.3.0";  ///< JackTrip version
+
+//*******************************************************************************
+/// \name Default Values
+//@{
+constexpr int gDefaultNumInChannels  = 2;
+constexpr int gDefaultNumOutChannels = 2;
+
+#define PROTOCOL_STACK QHostAddress::AnyIPv4  // as opposed to Any
+// #define WAIR_AUDIO_NAME "JackTrip" // for jack connection
+constexpr const char* WAIR_AUDIO_NAME = "JackTrip";  // keep legacy for WAIR
+constexpr int gMAX_WAIRS              = 128;  // FIXME, should agree with maxThreadCount
+// jmess revision needed for string parse if > 1 digit
+
+// hubpatch = 3 for TUB ensemble patching
+///////////////////////////////
+// test NUC as server
+// const QString gDOMAIN_TRIPLE = QString("130.149.23"); // for TUB multiclient hub
+// const int gMIN_TUB = 245; // lowest client address
+// const int gMAX_TUB = 245; // highest client address
+///////////////////////////////
+// test Riviera as server
+constexpr const char* gDOMAIN_TRIPLE = "192.168.0";  // for TUB multiclient hub
+constexpr int gMIN_TUB               = 11;           // lowest client address
+constexpr int gMAX_TUB               = 20;           // highest client address
+
+#ifdef WAIR  // wair
+// uses hub mode
+// hard wire the number of netrev (comb filter) channels
+#define NUMNETREVCHANSbecauseNOTINRECEIVEDheader 16  // for jacktripworker, jmess
+constexpr int gDefaultNumNetRevChannels   = NUMNETREVCHANSbecauseNOTINRECEIVEDheader;
+constexpr int gDefaultAddCombFilterLength = 0;
+constexpr int gDefaultCombFilterFeedback  = 0;
+#endif  // endwhere
+
+// const JackAudioInterface::audioBitResolutionT gDefaultBitResolutionMode =
+//    JackAudioInterface::BIT16;
+constexpr AudioInterface::audioBitResolutionT gDefaultBitResolutionMode =
+    AudioInterface::BIT16;
+constexpr int gDefaultQueueLength              = 4;
+constexpr int gDefaultOutputQueueLength        = 4;
+constexpr uint32_t gDefaultSampleRate          = 48000;
+constexpr int gDefaultDeviceID                 = -1;
+constexpr uint32_t gDefaultBufferSizeInSamples = 128;
+constexpr const char* gDefaultLocalAddress     = "";
+constexpr int gDefaultRedundancy               = 1;
+constexpr int gTimeOutMultiThreadedServer      = 10000;  // seconds
+constexpr int gWaitCounter                     = 60;
+constexpr int gUdpWaitTimeout                  = 30;  // milliseconds
+//@}
+
+//*******************************************************************************
+/// \name Network related ports
+//@{
+constexpr int gDefaultPort  = 4464;  ///< Default JackTrip Port
+constexpr int gBindPortLow  = 3464;  ///< lowest Bindport
+constexpr int gBindPortHigh = 5464;  ///< highest Bindport
+// const int gInputPort_0 = 4464; ///< Input base port
+// const int gOutputPort_0 = 4465; ///< Output base port
+// const int gDefaultSendPort = 4464; ///< Default for to use to send packet
+//@}
+
+//*******************************************************************************
+/// \name Separator for terminal printing
+//@{
+constexpr const char* const gPrintSeparator =
+    "---------------------------------------------------------";
+//@}
+
+//*******************************************************************************
+/// \name Global flags
+//@{
+extern int gVerboseFlag;  ///< Verbose mode flag declaration
+//@}
+
+//*******************************************************************************
+/// \name JackAudio
+//@{
+constexpr int gJackBitResolution = 32;  ///< Audio Bit Resolution of the Jack Server
+constexpr const char* gJackDefaultClientName = "JackTrip";
+constexpr int gMaxRemoteNameLength           = 64;
+//@}
+
+//*******************************************************************************
+/// \name Global Functions
+
+#ifdef __APPLE__
+void setRealtimeProcessPriority(int bufferSize = gDefaultBufferSizeInSamples,
+                                int sampleRate = gDefaultSampleRate);
+#else
+void setRealtimeProcessPriority();
+#endif
+
+//*******************************************************************************
+/// \name JackTrip Server parameters
+//@{
+/// Maximum Threads that can be run at the same time
+constexpr int gMaxThreads = 1024;
+
+/// Public well-known UDP port to where the clients will connect
+constexpr int gServerUdpPort = 4464;
+//@}
+
+#endif
diff --git a/src/jacktrip_types.h b/src/jacktrip_types.h
new file mode 100644 (file)
index 0000000..ea90e1f
--- /dev/null
@@ -0,0 +1,87 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file jacktrip_types_jacktrip.h
+ * \author Juan-Pablo Caceres
+ * \date June 2008
+ */
+
+#ifndef __JACKTRIP_TYPES_H__
+#define __JACKTRIP_TYPES_H__
+
+// #include <jack/types.h>
+#include <QtGlobal>  //For QT4 types
+
+// namespace JackTripNamespace
+//{
+
+//-------------------------------------------------------------------------------
+/** \name Audio typedefs
+ *
+ */
+//-------------------------------------------------------------------------------
+//@{
+/// Audio sample type
+// typedef jack_default_audio_sample_t sample_t;
+typedef float sample_t;
+//@}
+
+//-------------------------------------------------------------------------------
+/** \name Typedefs that guaranty some specific bit length
+ *
+ * It uses the QT4 types. This can be changed in the future, keeping
+ * compatibility for the rest of the code.
+ */
+//-------------------------------------------------------------------------------
+//@{
+/// Typedef for <tt>unsigned char</tt>. This type is guaranteed to be 8-bit.
+typedef quint8 uint8_t;
+/// Typedef for <tt>unsigned short</tt>. This type is guaranteed to be 16-bit.
+typedef quint16 uint16_t;
+/// Typedef for <tt>unsigned int</tt>. This type is guaranteed to be 32-bit.
+typedef quint32 uint32_t;
+/// \brief Typedef for <tt>unsigned long long int</tt>. This type is guaranteed to
+/// be 64-bit.
+// typedef quint64 uint64_t;
+/// Typedef for <tt>signed char</tt>. This type is guaranteed to be 8-bit.
+typedef qint8 int8_t;
+/// Typedef for <tt>signed short</tt>. This type is guaranteed to be 16-bit.
+typedef qint16 int16_t;
+/// Typedef for <tt>signed int</tt>. This type is guaranteed to be 32-bit.
+typedef qint32 int32_t;
+/// \brief Typedef for <tt>long long int</tt>. This type is guaranteed to
+/// be 64-bit.
+// typedef qint64 int64_t;
+//@}
+//} // end of namespace JackTripNamespace
+
+#endif
diff --git a/src/limiterdsp.h b/src/limiterdsp.h
new file mode 100644 (file)
index 0000000..b6371d1
--- /dev/null
@@ -0,0 +1,2134 @@
+/* ------------------------------------------------------------
+name: "limiterdsp"
+Code generated with Faust 2.41.1 (https://faust.grame.fr)
+Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn limiterdsp -es 1 -mcd 16
+-single -ftz 0
+------------------------------------------------------------ */
+
+#ifndef __limiterdsp_H__
+#define __limiterdsp_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 <string>
+#include <vector>
+
+/************************************************************************
+ ************************************************************************
+    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.41.1"
+
+// 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<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> 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 <xmmintrin.h>
+#endif
+
+class FAUST_API ScopedNoDenormals
+{
+   private:
+    intptr_t fpsr;
+
+    void setFpStatusRegister(intptr_t fpsr_aux) noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("msr fpcr, %0" : : "ri"(fpsr_aux));
+#elif defined(__SSE__)
+        _mm_setcsr(static_cast<uint32_t>(fpsr_aux));
+#endif
+    }
+
+    void getFpStatusRegister() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("mrs %0, fpcr" : "=r"(fpsr));
+#elif defined(__SSE__)
+        fpsr          = static_cast<intptr_t>(_mm_getcsr());
+#endif
+    }
+
+   public:
+    ScopedNoDenormals() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        intptr_t mask = (1 << 24 /* FZ */);
+#else
+#if defined(__SSE__)
+#if defined(__SSE2__)
+        intptr_t mask = 0x8040;
+#else
+        intptr_t mask = 0x8000;
+#endif
+#else
+        intptr_t mask = 0x0000;
+#endif
+#endif
+        getFpStatusRegister();
+        setFpStatusRegister(fpsr | mask);
+    }
+
+    ~ScopedNoDenormals() noexcept { setFpStatusRegister(fpsr); }
+};
+
+#define AVOIDDENORMALS ScopedNoDenormals();
+
+#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 <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** 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<typename REAL>
+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<FAUSTFLOAT> {
+    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 <algorithm>
+#include <map>
+#include <regex>
+#include <set>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class FAUST_API PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+    std::vector<std::string> fFullPaths;
+    std::map<std::string, std::string> 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<std::string>
+            uniquePaths;  // all full paths transformed but made unique with a prefix
+        std::map<std::string, std::string>
+            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<std::string, int> 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<std::string> collisionSet;
+            std::map<std::string, std::string> 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<char>& 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 <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// 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<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? 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<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(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<double>(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<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(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<double>(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<UpdatableValueConverter*> 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<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> 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<std::string, std::string> 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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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<ZoneControl*>* 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<ZoneControl*>* 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<ZoneControl*>* 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
+    //-------------------------------------------------------------------------------
+    int getParamsCount() { return int(fItems.size()); }
+
+    int getParamIndex(const char* path_aux)
+    {
+        std::string path = std::string(path_aux);
+        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;
+    }
+
+    const char* getParamLabel(int p) { return fItems[uint(p)].fLabel.c_str(); }
+    const char* getParamShortname(int p) { return fItems[uint(p)].fShortname.c_str(); }
+    const char* getParamAddress(int p) { return fItems[uint(p)].fPath.c_str(); }
+
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> metadata = fMetaData[uint(p)];
+        for (const auto& it : metadata) {
+            res[it.first.c_str()] = it.second.c_str();
+        }
+        return res;
+    }
+
+    const char* getMetadata(int p, const char* key)
+    {
+        return (fMetaData[uint(p)].find(key) != fMetaData[uint(p)].end())
+                   ? fMetaData[uint(p)][key].c_str()
+                   : "";
+    }
+    FAUSTFLOAT getParamMin(int p) { return fItems[uint(p)].fMin; }
+    FAUSTFLOAT getParamMax(int p) { return fItems[uint(p)].fMax; }
+    FAUSTFLOAT getParamStep(int p) { return fItems[uint(p)].fStep; }
+    FAUSTFLOAT getParamInit(int p) { return fItems[uint(p)].fInit; }
+
+    FAUSTFLOAT* getParamZone(int p) { return fItems[uint(p)].fZone; }
+
+    FAUSTFLOAT getParamValue(int p) { return *fItems[uint(p)].fZone; }
+    FAUSTFLOAT getParamValue(const char* path)
+    {
+        int index = getParamIndex(path);
+        if (index >= 0) {
+            return getParamValue(index);
+        } else {
+            fprintf(stderr, "getParamValue : '%s' not found\n",
+                    (path == nullptr ? "NULL" : path));
+            return FAUSTFLOAT(0);
+        }
+    }
+
+    void setParamValue(int p, FAUSTFLOAT v) { *fItems[uint(p)].fZone = v; }
+    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
+     * @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
+     * @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
+     * @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
+     * @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; }
+
+    // getScreenColor() : -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/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+#include <math.h>
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifndef FAUSTCLASS
+#define FAUSTCLASS limiterdsp
+#endif
+
+#ifdef __APPLE__
+#define exp10f __exp10f
+#define exp10  __exp10
+#endif
+
+#if defined(_WIN32)
+#define RESTRICT __restrict
+#else
+#define RESTRICT __restrict__
+#endif
+
+class limiterdsp : public dsp
+{
+   private:
+    int fSampleRate;
+    float fConst1;
+    float fConst2;
+    FAUSTFLOAT fHslider0;
+    int IOTA0;
+    float fVec0[32];
+    float fConst3;
+    int iRec2[2];
+    float fRec3[2];
+    float fRec1[2];
+    float fConst4;
+    float fConst5;
+    float fRec0[2];
+    int iConst6;
+
+   public:
+    void metadata(Meta* m)
+    {
+        m->declare("analyzers.lib/name", "Faust Analyzer Library");
+        m->declare("analyzers.lib/version", "0.2");
+        m->declare("basics.lib/name", "Faust Basic Element Library");
+        m->declare("basics.lib/peakholder:author", "Dario Sanfilippo");
+        m->declare("basics.lib/peakholder:copyright",
+                   "Copyright (C) 2022 Dario Sanfilippo <sanfilippo.dario@gmail.com>");
+        m->declare("basics.lib/peakholder:license", "MIT-style STK-4.3 license");
+        m->declare("basics.lib/version", "0.8");
+        m->declare("compile_options",
+                   "-a faust2header.cpp -lang cpp -i -inpl -cn limiterdsp -es 1 -mcd 16 "
+                   "-single -ftz 0");
+        m->declare("compressors.lib/limiter_lad_N:author", "Dario Sanfilippo");
+        m->declare(
+            "compressors.lib/limiter_lad_N:copyright",
+            "Copyright (C) 2020 Dario Sanfilippo       <sanfilippo.dario@gmail.com>");
+        m->declare("compressors.lib/limiter_lad_N:license", "GPLv3 license");
+        m->declare("compressors.lib/limiter_lad_mono:author", "Dario Sanfilippo");
+        m->declare(
+            "compressors.lib/limiter_lad_mono:copyright",
+            "Copyright (C) 2020 Dario Sanfilippo       <sanfilippo.dario@gmail.com>");
+        m->declare("compressors.lib/limiter_lad_mono:license", "GPLv3 license");
+        m->declare("compressors.lib/name", "Faust Compressor Effect Library");
+        m->declare("compressors.lib/version", "0.2");
+        m->declare("filename", "limiterdsp.dsp");
+        m->declare("maths.lib/author", "GRAME");
+        m->declare("maths.lib/copyright", "GRAME");
+        m->declare("maths.lib/license", "LGPL with exception");
+        m->declare("maths.lib/name", "Faust Math Library");
+        m->declare("maths.lib/version", "2.5");
+        m->declare("name", "limiterdsp");
+        m->declare("platform.lib/name", "Generic Platform Library");
+        m->declare("platform.lib/version", "0.2");
+        m->declare("routes.lib/name", "Faust Signal Routing Library");
+        m->declare("routes.lib/version", "0.2");
+        m->declare("signals.lib/name", "Faust Signal Routing Library");
+        m->declare("signals.lib/version", "0.3");
+    }
+
+    virtual int getNumInputs() { return 1; }
+    virtual int getNumOutputs() { return 1; }
+
+    static void classInit(int /*sample_rate*/) {}
+
+    virtual void instanceConstants(int sample_rate)
+    {
+        fSampleRate = sample_rate;
+        float fConst0 =
+            std::min<float>(192000.0f, std::max<float>(1.0f, float(fSampleRate)));
+        fConst1 = std::exp(0.0f - 100000.0f / fConst0);
+        fConst2 = 1.0f - fConst1;
+        fConst3 = 0.100000001f * fConst0;
+        fConst4 = std::exp(0.0f - 4.0f / fConst0);
+        fConst5 = 1.0f - fConst4;
+        iConst6 = int(9.99999975e-05f * fConst0);
+    }
+
+    virtual void instanceResetUserInterface() { fHslider0 = FAUSTFLOAT(2.0f); }
+
+    virtual void instanceClear()
+    {
+        IOTA0 = 0;
+        for (int l0 = 0; l0 < 32; l0 = l0 + 1) {
+            fVec0[l0] = 0.0f;
+        }
+        for (int l1 = 0; l1 < 2; l1 = l1 + 1) {
+            iRec2[l1] = 0;
+        }
+        for (int l2 = 0; l2 < 2; l2 = l2 + 1) {
+            fRec3[l2] = 0.0f;
+        }
+        for (int l3 = 0; l3 < 2; l3 = l3 + 1) {
+            fRec1[l3] = 0.0f;
+        }
+        for (int l4 = 0; l4 < 2; l4 = l4 + 1) {
+            fRec0[l4] = 0.0f;
+        }
+    }
+
+    virtual void init(int sample_rate)
+    {
+        classInit(sample_rate);
+        instanceInit(sample_rate);
+    }
+    virtual void instanceInit(int sample_rate)
+    {
+        instanceConstants(sample_rate);
+        instanceResetUserInterface();
+        instanceClear();
+    }
+
+    virtual limiterdsp* clone() { return new limiterdsp(); }
+
+    virtual int getSampleRate() { return fSampleRate; }
+
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        ui_interface->openVerticalBox("limiterdsp");
+        ui_interface->declare(&fHslider0, "0", "");
+        ui_interface->addHorizontalSlider("NumClientsAssumed", &fHslider0,
+                                          FAUSTFLOAT(2.0f), FAUSTFLOAT(1.0f),
+                                          FAUSTFLOAT(64.0f), FAUSTFLOAT(1.0f));
+        ui_interface->closeBox();
+    }
+
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs)
+    {
+        FAUSTFLOAT* input0  = inputs[0];
+        FAUSTFLOAT* output0 = outputs[0];
+        float fSlow0        = 1.0f / std::sqrt(float(fHslider0));
+        for (int i0 = 0; i0 < count; i0 = i0 + 1) {
+            float fTemp0      = float(input0[i0]);
+            float fTemp1      = fSlow0 * fTemp0;
+            fVec0[IOTA0 & 31] = fTemp1;
+            float fTemp2      = std::fabs(std::fabs(fTemp1));
+            int iTemp3        = (fTemp2 >= fRec3[1]) | (float(iRec2[1]) >= fConst3);
+            int iThen0        = iRec2[1] + 1;
+            iRec2[0]          = ((iTemp3) ? 0 : iThen0);
+            fRec3[0]          = ((iTemp3) ? fTemp2 : fRec3[1]);
+            fRec1[0]          = fConst2 * fRec3[0] + fConst1 * fRec1[1];
+            float fTemp4      = std::fabs(fRec1[0]);
+            fRec0[0]    = std::max<float>(fTemp4, fConst4 * fRec0[1] + fConst5 * fTemp4);
+            output0[i0] = FAUSTFLOAT(
+                std::min<float>(1.0f, 0.5f / std::max<float>(fRec0[0], 1.1920929e-07f))
+                * fVec0[(IOTA0 - iConst6) & 31]);
+            IOTA0    = IOTA0 + 1;
+            iRec2[1] = iRec2[0];
+            fRec3[1] = fRec3[0];
+            fRec1[1] = fRec1[0];
+            fRec0[1] = fRec0[0];
+        }
+    }
+};
+
+#endif
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644 (file)
index 0000000..58d91dd
--- /dev/null
@@ -0,0 +1,540 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008-2021 Juan-Pablo Caceres, Chris Chafe.
+  SoundWIRE group at CCRMA, Stanford University.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file main.cpp
+ * \author Aaron Wyatt, based on jacktrip_main by Juan-Pablo Caceres
+ * \date July 2020
+ */
+
+#ifndef NO_GUI
+#include <QApplication>
+#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+#include <QQuickStyle>
+#endif
+
+#ifndef NO_UPDATER
+#include "dblsqd/feed.h"
+#include "dblsqd/update_dialog.h"
+#endif
+
+#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+#include <QDebug>
+#include <QDir>
+#include <QFile>
+#include <QQmlEngine>
+#include <QQuickView>
+#include <QSGRendererInterface>
+#include <QSettings>
+#include <QStandardPaths>
+#include <QTextStream>
+// TODO: Add support for QtWebView
+// #include <QtWebView>
+#include <QtWebEngineQuick/qtwebenginequickglobal.h>
+
+#include "JTApplication.h"
+#include "gui/virtualstudio.h"
+#include "gui/vsDeeplink.h"
+#include "gui/vsQmlClipboard.h"
+#endif  // NO_VS && QT_VERSION
+
+#include "gui/qjacktrip.h"
+#else
+#include <QCoreApplication>
+#endif
+#include <QLoggingCategory>
+#include <QScopedPointer>
+#include <csignal>
+#include <iostream>
+
+#include "Settings.h"
+#include "UdpHubListener.h"
+#include "jacktrip_globals.h"
+
+#ifdef _WIN32
+#include <psapi.h>
+#include <tlhelp32.h>
+#include <windows.h>
+#endif
+
+#ifndef NO_GUI
+#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+static QTextStream* ts;
+static QFile outFile;
+#endif  // NO_VS && QT_VERSION
+#endif  // NO_GUI
+
+QCoreApplication* createApplication(int& argc, char* argv[])
+{
+    // Check for some specific, GUI related command line options.
+    bool forceGui = false;
+    for (int i = 1; i < argc; i++) {
+        if (strncmp(argv[i], "--gui", 5) == 0 || strncmp(argv[i], "--deeplink", 10) == 0
+            || strncmp(argv[i], "--classic-gui", 13) == 0
+            || strncmp(argv[i], "jacktrip://", 11) == 0) {
+            forceGui = true;
+        } else if (strncmp(argv[i], "--test-gui", 10) == 0) {
+            // Command line option to test if the binary has been built with GUI support.
+            // Exits immediately. Exits with an error if GUI support has not been built
+            // in.
+#ifdef NO_GUI
+            std::cout << "This version of JackTrip has been built without GUI support."
+                      << std::endl;
+            std::cout << "(To run JackTrip normally, please omit the --test-gui option.)"
+                      << std::endl;
+            std::exit(1);
+#else
+            std::cout << "This version of JackTrip has been built with GUI support."
+                      << std::endl;
+            std::cout << "(To run JackTrip normally, please omit the --test-gui option.)"
+                      << std::endl;
+            std::exit(0);
+#endif
+        }
+    }
+
+    // If we have command line arguments and aren't forcing the GUI run on the command
+    // line.
+    if (argc == 1 || forceGui) {
+#ifdef NO_GUI
+        if (forceGui) {
+            std::cout << "This version of jacktrip has not been built with GUI support."
+                      << std::endl;
+            std::exit(1);
+        } else {
+            return new QCoreApplication(argc, argv);
+        }
+#else
+#if defined(__unix__)
+        // Check if X or Wayland environment variables are set.
+        if (std::getenv("WAYLAND_DISPLAY") == nullptr
+            && std::getenv("DISPLAY") == nullptr) {
+            std::cout << "ERROR: Display not found. Make sure X or Wayland is running or "
+                         "try running jacktrip in command line mode."
+                      << std::endl;
+            std::cout << "(To display a list of command line options run \"jacktrip -h\")"
+                      << std::endl;
+            std::exit(1);
+        }
+#endif
+#if defined(Q_OS_MACOS) && !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+        // Turn on high DPI support.
+#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
+        JTApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+#endif
+        // Fix for display scaling like 125% or 150% on Windows
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
+        QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
+            Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
+#endif  // QT_VERSION
+
+        // Initialize webengine
+        QtWebEngineQuick::initialize();
+        // TODO: Add support for QtWebView
+        // qputenv("QT_WEBVIEW_PLUGIN", "native");
+        // QtWebView::initialize();
+
+        return new JTApplication(argc, argv);
+#else
+        // Turn on high DPI support.
+#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
+        QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
+#endif
+        // Fix for display scaling like 125% or 150% on Windows
+#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
+#if defined(NO_VS) && defined(_WIN32)
+        QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
+            Qt::HighDpiScaleFactorRoundingPolicy::RoundPreferFloor);
+#else
+        QGuiApplication::setHighDpiScaleFactorRoundingPolicy(
+            Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
+#endif  // NO_VS
+#endif  // QT_VERSION
+
+#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+        // Enables resource sharing between the OpenGL contexts
+        QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
+        // QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
+        // QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
+
+        // QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D11);
+        QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGL);
+
+        // Initialize webengine
+        QtWebEngineQuick::initialize();
+        // TODO: Add support for QtWebView
+        // qputenv("QT_WEBVIEW_PLUGIN", "native");
+        // QtWebView::initialize();
+#endif
+
+        return new QApplication(argc, argv);
+#endif  // Q_OS_MACOS
+#endif  // NO_GUI
+    } else {
+        return new QCoreApplication(argc, argv);
+    }
+}
+
+void qtMessageHandler([[maybe_unused]] QtMsgType type,
+                      [[maybe_unused]] const QMessageLogContext& context,
+                      const QString& msg)
+{
+    std::cerr << msg.toStdString() << std::endl;
+#ifndef NO_GUI
+#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+    // Writes to file in order to debug bundles and executables
+    *ts << msg << Qt::endl;
+#endif  // NO_VS && QT_VERSION
+#endif  // NO_GUI
+}
+
+void outputError(const QString& msg)
+{
+    std::cerr << "Error: " << msg.toStdString() << std::endl;
+}
+
+#ifndef _WIN32
+static int setupUnixSignalHandler(void (*handler)(int))
+{
+    // Setup our SIGINT handler.
+    struct sigaction sigInt;
+    sigInt.sa_handler = handler;
+    sigemptyset(&sigInt.sa_mask);
+    sigInt.sa_flags = 0;
+    sigInt.sa_flags |= SA_RESTART;
+
+    int result = 0;
+    if (sigaction(SIGINT, &sigInt, 0)) {
+        std::cout << "Unable to register SIGINT handler" << std::endl;
+        result |= 1;
+    }
+    if (sigaction(SIGTERM, &sigInt, 0)) {
+        std::cout << "Unable to register SIGTERM handler" << std::endl;
+        result |= 2;
+    }
+    return result;
+}
+#else
+bool isHubServer = false;
+
+BOOL WINAPI windowsCtrlHandler(DWORD fdwCtrlType)
+{
+    switch (fdwCtrlType) {
+    case CTRL_C_EVENT:
+        if (isHubServer) {
+            UdpHubListener::sigIntHandler(0);
+        } else {
+            JackTrip::sigIntHandler(0);
+        }
+        return true;
+    default:
+        return false;
+    }
+}
+
+bool isRunFromCmd()
+{
+    // Get our parent process pid
+    HANDLE h = NULL;
+    PROCESSENTRY32 pe;
+    ZeroMemory(&pe, sizeof(PROCESSENTRY32));
+    DWORD pid = GetCurrentProcessId();
+    DWORD ppid = 0;
+    pe.dwSize = sizeof(PROCESSENTRY32);
+    h = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+    if (Process32First(h, &pe)) {
+        do {
+            // Loop through the list of processes until we find ours.
+            if (pe.th32ProcessID == pid) {
+                ppid = pe.th32ParentProcessID;
+                break;
+            }
+        } while (Process32Next(h, &pe));
+    }
+    CloseHandle(h);
+
+    // Get the name of our parent process;
+    char pname[MAX_PATH] = {0};
+    DWORD size = MAX_PATH;
+    h = NULL;
+    h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, ppid);
+    if (h) {
+        if (QueryFullProcessImageNameA(h, 0, pname, &size)) {
+            CloseHandle(h);
+
+            // Check if our parent process is a command line.
+            if (size >= 14 && strncmp(pname + size - 14, "powershell.exe", 14) == 0) {
+                return true;
+            }
+            if (size >= 7 && strncmp(pname + size - 7, "cmd.exe", 7) == 0) {
+                return true;
+            }
+            if (size >= 6 && strncmp(pname + size - 6, "wt.exe", 6) == 0) {
+                return true;
+            }
+            // a few extras for msys/cygwin/etc
+            if (size >= 8 && strncmp(pname + size - 8, "bash.exe", 8) == 0) {
+                return true;
+            }
+            if (size >= 6 && strncmp(pname + size - 6, "sh.exe", 6) == 0) {
+                return true;
+            }
+            if (size >= 7 && strncmp(pname + size - 7, "zsh.exe", 7) == 0) {
+                return true;
+            }
+        } else {
+            CloseHandle(h);
+        }
+    }
+
+    return false;
+}
+#endif
+
+int main(int argc, char* argv[])
+{
+    QScopedPointer<QCoreApplication> app(createApplication(argc, argv));
+    QScopedPointer<JackTrip> jackTrip;
+    QScopedPointer<UdpHubListener> udpHub;
+#ifndef NO_GUI
+    QSharedPointer<QJackTrip> window;
+#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+    QQuickStyle::setStyle("Basic");
+#endif  // QT_VERSION
+
+#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+    QSharedPointer<VirtualStudio> vsPtr;
+    QScopedPointer<VsDeeplink> vsDeeplinkPtr;
+#endif  // NO_VS && QT_VERSION
+
+#if defined(Q_OS_MACOS) && !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+    if (qobject_cast<JTApplication*>(app.data())) {
+#else
+    if (qobject_cast<QApplication*>(app.data())) {
+#endif
+        // Start the GUI if there are no command line options.
+#ifdef _WIN32
+        // Remove the console that appears if we're on windows and not running from a
+        // command line.
+        if (!isRunFromCmd()) {
+            std::cout << "This extra window is caused by a bug in Microsoft Windows. "
+                      << "It can safely be ignored or closed." << std::endl
+                      << std::endl
+                      << "To fix this bug, please upgrade to the latest version of "
+                      << "Windows Terminal available in the Microsoft App Store:"
+                      << std::endl
+                      << "https://aka.ms/terminal" << std::endl;
+
+            FreeConsole();
+        }
+#endif  // _WIN32
+        app->setOrganizationName(QStringLiteral("jacktrip"));
+        app->setOrganizationDomain(QStringLiteral("jacktrip.org"));
+        app->setApplicationName(QStringLiteral("JackTrip"));
+        app->setApplicationVersion(gVersion);
+
+        QSharedPointer<Settings> cliSettings;
+        cliSettings.reset(new Settings(true));
+        cliSettings->parseInput(argc, argv);
+
+#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+        // Register clipboard Qml type
+        qmlRegisterType<VsQmlClipboard>("VS", 1, 0, "Clipboard");
+
+        // prepare handler for deeplinks jacktrip://join/<StudioID>
+        vsDeeplinkPtr.reset(new VsDeeplink(cliSettings->getDeeplink()));
+        if (!vsDeeplinkPtr->getDeeplink().isEmpty()) {
+            bool readyForExit = vsDeeplinkPtr->waitForReady();
+            if (readyForExit)
+                return 0;
+        }
+
+        // Check which mode we are running in
+        QSettings settings;
+        int uiMode = QJackTrip::UNSET;
+        if (!vsDeeplinkPtr->getDeeplink().isEmpty()) {
+            uiMode = QJackTrip::VIRTUAL_STUDIO;
+        } else if (cliSettings->guiForceClassicMode()) {
+            uiMode = QJackTrip::STANDARD;
+            // force settings change; otherwise, virtual studio
+            // window will still be displayed
+            settings.setValue(QStringLiteral("UiMode"), uiMode);
+        } else {
+            uiMode = settings.value(QStringLiteral("UiMode"), QJackTrip::UNSET).toInt();
+        }
+
+        window.reset(new QJackTrip(cliSettings, !vsDeeplinkPtr->getDeeplink().isEmpty()));
+#else
+        window.reset(new QJackTrip(cliSettings));
+#endif  // NO_VS
+        QObject::connect(window.data(), &QJackTrip::signalExit, app.data(),
+                         &QCoreApplication::quit, Qt::QueuedConnection);
+
+#if !defined(NO_VS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+        vsPtr.reset(new VirtualStudio(uiMode == QJackTrip::UNSET));
+        QObject::connect(vsPtr.data(), &VirtualStudio::signalExit, app.data(),
+                         &QCoreApplication::quit, Qt::QueuedConnection);
+        vsPtr->setStandardWindow(window);
+        vsPtr->setCLISettings(cliSettings);
+        window->setVs(vsPtr);
+        QObject::connect(vsDeeplinkPtr.get(), &VsDeeplink::signalDeeplink, vsPtr.get(),
+                         &VirtualStudio::handleDeeplinkRequest, Qt::QueuedConnection);
+        vsDeeplinkPtr->readyForSignals();
+
+        if (uiMode == QJackTrip::UNSET) {
+            vsPtr->show();
+        } else if (uiMode == QJackTrip::VIRTUAL_STUDIO) {
+            vsPtr->show();
+        } else {
+            window->show();
+        }
+
+        // Log to file
+        QString logPath(
+            QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
+        QDir logDir;
+        if (!logDir.exists(logPath)) {
+            logDir.mkpath(logPath);
+        }
+        QString fileLoc(logPath.append("/log.txt"));
+        qDebug() << "Log file location:" << fileLoc;
+        outFile.setFileName(fileLoc);
+        if (!outFile.open(QIODevice::WriteOnly | QIODevice::Append)) {
+            qDebug() << "Log file open failed:" << outFile.errorString();
+        }
+        ts = new QTextStream(&outFile);
+        qInstallMessageHandler(qtMessageHandler);
+#else
+        window->show();
+#endif  // NO_VS
+
+#if !defined(NO_UPDATER) && !defined(__unix__)
+#ifndef PSI
+#if defined(NO_VS) || QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
+
+        // This wasn't set up earlier in NO_VS builds. Create it here.
+        QSettings settings;
+#endif
+        QString updateChannel = settings.value(QStringLiteral("UpdateChannel"), "stable")
+                                    .toString()
+                                    .toLower();
+        QString baseUrl = QStringLiteral("https://files.jacktrip.org/app-releases/%1")
+                              .arg(updateChannel);
+#else
+        QString baseUrl = QStringLiteral("https://nuages.psi-borg.org/jacktrip");
+#endif  // PSI
+        // Setup auto-update feed
+        dblsqd::Feed* feed = new dblsqd::Feed();
+#ifdef Q_OS_WIN
+        feed->setUrl(QUrl(QString("%1/%2-manifests.json").arg(baseUrl, "win")));
+#endif
+#ifdef Q_OS_MACOS
+        feed->setUrl(QUrl(QString("%1/%2-manifests.json").arg(baseUrl, "mac")));
+#endif
+        if (feed) {
+            dblsqd::UpdateDialog* updateDialog = new dblsqd::UpdateDialog(feed);
+            updateDialog->setIcon(":/qjacktrip/icon.png");
+        }
+#endif  // NO_UPDATER
+    } else {
+#endif  // NO_GUI
+        // Otherwise use the non-GUI version, and parse our command line.
+        try {
+            Settings settings;
+            settings.parseInput(argc, argv);
+
+#ifndef PSI
+            if (gVerboseFlag) {
+                QLoggingCategory::setFilterRules(QStringLiteral("*.debug=true"));
+            }
+#endif
+
+            // Either start our hub server or our jacktrip process as appropriate.
+            if (settings.isHubServer()) {
+                udpHub.reset(settings.getConfiguredHubServer());
+                if (gVerboseFlag)
+                    std::cout << "Settings:startJackTrip before udphub->start"
+                              << std::endl;
+                QObject::connect(udpHub.data(), &UdpHubListener::signalStopped,
+                                 app.data(), &QCoreApplication::quit,
+                                 Qt::QueuedConnection);
+                QObject::connect(udpHub.data(), &UdpHubListener::signalError,
+                                 outputError);
+                QObject::connect(udpHub.data(), &UdpHubListener::signalError, app.data(),
+                                 &QCoreApplication::quit, Qt::QueuedConnection);
+#ifndef _WIN32
+                setupUnixSignalHandler(UdpHubListener::sigIntHandler);
+#else
+            isHubServer = true;
+            SetConsoleCtrlHandler(windowsCtrlHandler, true);
+#endif
+                udpHub->start();
+            } else {
+                jackTrip.reset(settings.getConfiguredJackTrip());
+                if (gVerboseFlag)
+                    std::cout << "Settings:startJackTrip before mJackTrip->startProcess"
+                              << std::endl;
+                QObject::connect(jackTrip.data(), &JackTrip::signalProcessesStopped,
+                                 app.data(), &QCoreApplication::quit,
+                                 Qt::QueuedConnection);
+                QObject::connect(jackTrip.data(), &JackTrip::signalError, outputError);
+                QObject::connect(jackTrip.data(), &JackTrip::signalError, app.data(),
+                                 &QCoreApplication::quit, Qt::QueuedConnection);
+#ifndef _WIN32
+                setupUnixSignalHandler(JackTrip::sigIntHandler);
+#else
+            std::cout << SetConsoleCtrlHandler(windowsCtrlHandler, true) << std::endl;
+#endif
+#ifdef WAIRTOHUB  // WAIR
+                jackTrip->startProcess(
+                    0);  // for WAIR compatibility, ID in jack client name
+#else
+            jackTrip->startProcess();
+#endif  // endwhere
+            }
+
+            if (gVerboseFlag)
+                std::cout << "step 6" << std::endl;
+            if (gVerboseFlag)
+                std::cout << "jmain before app->exec()" << std::endl;
+        } catch (const std::exception& e) {
+            std::cerr << "ERROR:" << std::endl;
+            std::cerr << e.what() << std::endl;
+            std::cerr << "Exiting JackTrip..." << std::endl;
+            std::cerr << gPrintSeparator << std::endl;
+            return -1;
+        }
+#ifndef NO_GUI
+    }
+#endif  // NO_GUI
+
+    return app->exec();
+}
diff --git a/src/makeXcodeproj.sh b/src/makeXcodeproj.sh
new file mode 100755 (executable)
index 0000000..461de41
--- /dev/null
@@ -0,0 +1 @@
+qmake -spec macx-xcode ../jacktrip.pro
diff --git a/src/meterdsp.h b/src/meterdsp.h
new file mode 100644 (file)
index 0000000..68ab248
--- /dev/null
@@ -0,0 +1,2062 @@
+/* ------------------------------------------------------------
+author: "Dominick Hing"
+license: "MIT Style STK-4.2"
+name: "meter"
+version: "1.0"
+Code generated with Faust 2.41.1 (https://faust.grame.fr)
+Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn meterdsp -es 1 -mcd 16
+-single -ftz 0
+------------------------------------------------------------ */
+
+#ifndef __meterdsp_H__
+#define __meterdsp_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 <string>
+#include <vector>
+
+/************************************************************************
+ ************************************************************************
+    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.41.1"
+
+// 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<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> 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 <xmmintrin.h>
+#endif
+
+class FAUST_API ScopedNoDenormals
+{
+   private:
+    intptr_t fpsr;
+
+    void setFpStatusRegister(intptr_t fpsr_aux) noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("msr fpcr, %0" : : "ri"(fpsr_aux));
+#elif defined(__SSE__)
+        _mm_setcsr(static_cast<uint32_t>(fpsr_aux));
+#endif
+    }
+
+    void getFpStatusRegister() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("mrs %0, fpcr" : "=r"(fpsr));
+#elif defined(__SSE__)
+        fpsr          = static_cast<intptr_t>(_mm_getcsr());
+#endif
+    }
+
+   public:
+    ScopedNoDenormals() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        intptr_t mask = (1 << 24 /* FZ */);
+#else
+#if defined(__SSE__)
+#if defined(__SSE2__)
+        intptr_t mask = 0x8040;
+#else
+        intptr_t mask = 0x8000;
+#endif
+#else
+        intptr_t mask = 0x0000;
+#endif
+#endif
+        getFpStatusRegister();
+        setFpStatusRegister(fpsr | mask);
+    }
+
+    ~ScopedNoDenormals() noexcept { setFpStatusRegister(fpsr); }
+};
+
+#define AVOIDDENORMALS ScopedNoDenormals();
+
+#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 <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** 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<typename REAL>
+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<FAUSTFLOAT> {
+    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 <algorithm>
+#include <map>
+#include <regex>
+#include <set>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class FAUST_API PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+    std::vector<std::string> fFullPaths;
+    std::map<std::string, std::string> 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<std::string>
+            uniquePaths;  // all full paths transformed but made unique with a prefix
+        std::map<std::string, std::string>
+            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<std::string, int> 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<std::string> collisionSet;
+            std::map<std::string, std::string> 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<char>& 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 <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// 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<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? 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<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(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<double>(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<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(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<double>(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<UpdatableValueConverter*> 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<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> 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<std::string, std::string> 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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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<ZoneControl*>* 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<ZoneControl*>* 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<ZoneControl*>* 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
+    //-------------------------------------------------------------------------------
+    int getParamsCount() { return int(fItems.size()); }
+
+    int getParamIndex(const char* path_aux)
+    {
+        std::string path = std::string(path_aux);
+        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;
+    }
+
+    const char* getParamLabel(int p) { return fItems[uint(p)].fLabel.c_str(); }
+    const char* getParamShortname(int p) { return fItems[uint(p)].fShortname.c_str(); }
+    const char* getParamAddress(int p) { return fItems[uint(p)].fPath.c_str(); }
+
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> metadata = fMetaData[uint(p)];
+        for (const auto& it : metadata) {
+            res[it.first.c_str()] = it.second.c_str();
+        }
+        return res;
+    }
+
+    const char* getMetadata(int p, const char* key)
+    {
+        return (fMetaData[uint(p)].find(key) != fMetaData[uint(p)].end())
+                   ? fMetaData[uint(p)][key].c_str()
+                   : "";
+    }
+    FAUSTFLOAT getParamMin(int p) { return fItems[uint(p)].fMin; }
+    FAUSTFLOAT getParamMax(int p) { return fItems[uint(p)].fMax; }
+    FAUSTFLOAT getParamStep(int p) { return fItems[uint(p)].fStep; }
+    FAUSTFLOAT getParamInit(int p) { return fItems[uint(p)].fInit; }
+
+    FAUSTFLOAT* getParamZone(int p) { return fItems[uint(p)].fZone; }
+
+    FAUSTFLOAT getParamValue(int p) { return *fItems[uint(p)].fZone; }
+    FAUSTFLOAT getParamValue(const char* path)
+    {
+        int index = getParamIndex(path);
+        if (index >= 0) {
+            return getParamValue(index);
+        } else {
+            fprintf(stderr, "getParamValue : '%s' not found\n",
+                    (path == nullptr ? "NULL" : path));
+            return FAUSTFLOAT(0);
+        }
+    }
+
+    void setParamValue(int p, FAUSTFLOAT v) { *fItems[uint(p)].fZone = v; }
+    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
+     * @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
+     * @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
+     * @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
+     * @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; }
+
+    // getScreenColor() : -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/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+/* link with : "" */
+#include <math.h>
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifndef FAUSTCLASS
+#define FAUSTCLASS meterdsp
+#endif
+
+#ifdef __APPLE__
+#define exp10f __exp10f
+#define exp10  __exp10
+#endif
+
+#if defined(_WIN32)
+#define RESTRICT __restrict
+#else
+#define RESTRICT __restrict__
+#endif
+
+class meterdsp : public dsp
+{
+   private:
+    int fSampleRate;
+
+   public:
+    void metadata(Meta* m)
+    {
+        m->declare("author", "Dominick Hing");
+        m->declare("basics.lib/name", "Faust Basic Element Library");
+        m->declare("basics.lib/version", "0.8");
+        m->declare("compile_options",
+                   "-a faust2header.cpp -lang cpp -i -inpl -cn meterdsp -es 1 -mcd 16 "
+                   "-single -ftz 0");
+        m->declare("description", "VU Meter Faust Plugin for JackTrip");
+        m->declare("filename", "meterdsp.dsp");
+        m->declare("license", "MIT Style STK-4.2");
+        m->declare("maths.lib/author", "GRAME");
+        m->declare("maths.lib/copyright", "GRAME");
+        m->declare("maths.lib/license", "LGPL with exception");
+        m->declare("maths.lib/name", "Faust Math Library");
+        m->declare("maths.lib/version", "2.5");
+        m->declare("name", "meter");
+        m->declare("version", "1.0");
+    }
+
+    virtual int getNumInputs() { return 1; }
+    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 meterdsp* clone() { return new meterdsp(); }
+
+    virtual int getSampleRate() { return fSampleRate; }
+
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        ui_interface->openVerticalBox("meter");
+        ui_interface->closeBox();
+    }
+
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs)
+    {
+        FAUSTFLOAT* input0  = inputs[0];
+        FAUSTFLOAT* output0 = outputs[0];
+        for (int i0 = 0; i0 < count; i0 = i0 + 1) {
+            float fTemp0 = float(input0[i0]);
+            float fTemp1 =
+                20.0f
+                * std::log10(std::max<float>(1.17549435e-38f,
+                                             std::max<float>(9.99999975e-05f, fTemp0)));
+            float fTemp2 = 100.0f * float(copysignf(float(fTemp1), 1.0f));
+            output0[i0]  = FAUSTFLOAT(float(copysignf(
+                float(0.00999999978f
+                      * float(int(fTemp2) + (fTemp2 - std::floor(fTemp2) >= 0.5f))),
+                float(fTemp1))));
+        }
+    }
+};
+
+#endif
diff --git a/src/monitordsp.h b/src/monitordsp.h
new file mode 100644 (file)
index 0000000..44bbe6e
--- /dev/null
@@ -0,0 +1,2224 @@
+/* ------------------------------------------------------------
+author: "Dominick Hing, adapted from 'Volume Control' by Matt Horton"
+license: "MIT Style STK-4.2"
+name: "monitor"
+version: "1.0"
+Code generated with Faust 2.54.9 (https://faust.grame.fr)
+Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn monitordsp -es 1 -mcd 16
+-single -ftz 0
+------------------------------------------------------------ */
+
+#ifndef __monitordsp_H__
+#define __monitordsp_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 <string>
+#include <vector>
+
+/************************************************************************
+ 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 __export__
+#define __export__
+
+#define FAUSTVERSION "2.54.9"
+
+// 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<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> getIncludePathnames() = 0;
+    virtual std::vector<std::string> getWarningMessages()  = 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 <xmmintrin.h>
+#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<uint32_t>(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<intptr_t>(_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 <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** 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<typename REAL>
+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<FAUSTFLOAT> {
+    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 <algorithm>
+#include <map>
+#include <regex>
+#include <set>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class FAUST_API PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+    std::vector<std::string> fFullPaths;
+    std::map<std::string, std::string> 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<std::string>
+            uniquePaths;  // all full paths transformed but made unique with a prefix
+        std::map<std::string, std::string>
+            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<std::string, int> 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<std::string> collisionSet;
+            std::map<std::string, std::string> 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<char>& 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 <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// 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<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? 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<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(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<double>(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<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(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<double>(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<UpdatableValueConverter*> 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<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> 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<std::string, std::string> 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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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<ZoneControl*>* 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<ZoneControl*>* 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<ZoneControl*>* 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<key, value>
+     */
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> 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/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+#include <math.h>
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifndef FAUSTCLASS
+#define FAUSTCLASS monitordsp
+#endif
+
+#ifdef __APPLE__
+#define exp10f __exp10f
+#define exp10  __exp10
+#endif
+
+#if defined(_WIN32)
+#define RESTRICT __restrict
+#else
+#define RESTRICT __restrict__
+#endif
+
+class monitordsp : public dsp
+{
+   private:
+    FAUSTFLOAT fHslider0;
+    FAUSTFLOAT fCheckbox0;
+    int fSampleRate;
+    float fConst0;
+    float fConst1;
+    float fRec0[2];
+
+   public:
+    void metadata(Meta* m)
+    {
+        m->declare("author",
+                   "Dominick Hing, adapted from 'Volume Control' by Matt Horton");
+        m->declare("basics.lib/name", "Faust Basic Element Library");
+        m->declare("basics.lib/version", "0.9");
+        m->declare("compile_options",
+                   "-a faust2header.cpp -lang cpp -i -inpl -cn monitordsp -es 1 -mcd 16 "
+                   "-single -ftz 0");
+        m->declare("description",
+                   "Volume Control Faust Plugin for JackTrip, based on Faust examples");
+        m->declare("filename", "monitordsp.dsp");
+        m->declare("license", "MIT Style STK-4.2");
+        m->declare("maths.lib/author", "GRAME");
+        m->declare("maths.lib/copyright", "GRAME");
+        m->declare("maths.lib/license", "LGPL with exception");
+        m->declare("maths.lib/name", "Faust Math Library");
+        m->declare("maths.lib/version", "2.5");
+        m->declare("name", "monitor");
+        m->declare("platform.lib/name", "Generic Platform Library");
+        m->declare("platform.lib/version", "0.3");
+        m->declare("signals.lib/name", "Faust Signal Routing Library");
+        m->declare("signals.lib/version", "0.3");
+        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;
+        fConst0 =
+            44.1f / std::min<float>(1.92e+05f, std::max<float>(1.0f, float(fSampleRate)));
+        fConst1 = 1.0f - fConst0;
+    }
+
+    virtual void instanceResetUserInterface()
+    {
+        fHslider0  = FAUSTFLOAT(0.0f);
+        fCheckbox0 = FAUSTFLOAT(0.0f);
+    }
+
+    virtual void instanceClear()
+    {
+        for (int l0 = 0; l0 < 2; l0 = l0 + 1) {
+            fRec0[l0] = 0.0f;
+        }
+    }
+
+    virtual void init(int sample_rate)
+    {
+        classInit(sample_rate);
+        instanceInit(sample_rate);
+    }
+    virtual void instanceInit(int sample_rate)
+    {
+        instanceConstants(sample_rate);
+        instanceResetUserInterface();
+        instanceClear();
+    }
+
+    virtual monitordsp* clone() { return new monitordsp(); }
+
+    virtual int getSampleRate() { return fSampleRate; }
+
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        ui_interface->openVerticalBox("Monitor");
+        ui_interface->declare(&fHslider0, "0", "");
+        ui_interface->addHorizontalSlider("Volume", &fHslider0, FAUSTFLOAT(0.0f),
+                                          FAUSTFLOAT(-4e+01f), FAUSTFLOAT(0.0f),
+                                          FAUSTFLOAT(0.1f));
+        ui_interface->declare(&fCheckbox0, "1", "");
+        ui_interface->addCheckButton("Mute", &fCheckbox0);
+        ui_interface->closeBox();
+    }
+
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs)
+    {
+        FAUSTFLOAT* input0  = inputs[0];
+        FAUSTFLOAT* input1  = inputs[1];
+        FAUSTFLOAT* output0 = outputs[0];
+        float fSlow0        = float(fHslider0);
+        int iSlow1          = fSlow0 == -4e+01f;
+        int iSlow2          = int(float(fCheckbox0));
+        float fSlow3        = fConst0 * std::pow(1e+01f, 0.05f * fSlow0);
+        for (int i0 = 0; i0 < count; i0 = i0 + 1) {
+            float fTemp0 = float(input1[i0]);
+            float fTemp1 = float(input0[i0]);
+            fRec0[0]     = fSlow3 + fConst1 * fRec0[1];
+            output0[i0]  = FAUSTFLOAT(
+                fTemp0 + ((iSlow1) ? 0.0f : ((iSlow2) ? 0.0f : fTemp1 * fRec0[0])));
+            fRec0[1] = fRec0[0];
+        }
+    }
+};
+
+#endif
diff --git a/src/stereotomonodsp.h b/src/stereotomonodsp.h
new file mode 100644 (file)
index 0000000..1633a9f
--- /dev/null
@@ -0,0 +1,2177 @@
+/* ------------------------------------------------------------
+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 <string>
+#include <vector>
+
+/************************************************************************
+ ************************************************************************
+    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<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> 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 <xmmintrin.h>
+#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<uint32_t>(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<intptr_t>(_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 <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** 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<typename REAL>
+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<FAUSTFLOAT> {
+    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 <algorithm>
+#include <map>
+#include <regex>
+#include <set>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class FAUST_API PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+    std::vector<std::string> fFullPaths;
+    std::map<std::string, std::string> 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<std::string>
+            uniquePaths;  // all full paths transformed but made unique with a prefix
+        std::map<std::string, std::string>
+            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<std::string, int> 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<std::string> collisionSet;
+            std::map<std::string, std::string> 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<char>& 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 <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// 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<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? 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<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(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<double>(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<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(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<double>(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<UpdatableValueConverter*> 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<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> 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<std::string, std::string> 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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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<ZoneControl*>* 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<ZoneControl*>* 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<ZoneControl*>* 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<key, value>
+     */
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> 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/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#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
diff --git a/src/tonedsp.h b/src/tonedsp.h
new file mode 100644 (file)
index 0000000..84af3a1
--- /dev/null
@@ -0,0 +1,2407 @@
+/* ------------------------------------------------------------
+name: "tonedsp"
+Code generated with Faust 2.50.6 (https://faust.grame.fr)
+Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn tonedsp -es 1 -mcd 16
+-single -ftz 0
+------------------------------------------------------------ */
+
+#ifndef __tonedsp_H__
+#define __tonedsp_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 <string>
+#include <vector>
+
+/************************************************************************
+ ************************************************************************
+    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<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> 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 <xmmintrin.h>
+#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<uint32_t>(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<intptr_t>(_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 <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** 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<typename REAL>
+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<FAUSTFLOAT> {
+    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 <algorithm>
+#include <map>
+#include <regex>
+#include <set>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class FAUST_API PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+    std::vector<std::string> fFullPaths;
+    std::map<std::string, std::string> 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<std::string>
+            uniquePaths;  // all full paths transformed but made unique with a prefix
+        std::map<std::string, std::string>
+            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<std::string, int> 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<std::string> collisionSet;
+            std::map<std::string, std::string> 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<char>& 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 <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// 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<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? 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<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(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<double>(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<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(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<double>(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<UpdatableValueConverter*> 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<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> 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<std::string, std::string> 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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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<ZoneControl*>* 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<ZoneControl*>* 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<ZoneControl*>* 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<key, value>
+     */
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> 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/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+#include <math.h>
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifndef FAUSTCLASS
+#define FAUSTCLASS tonedsp
+#endif
+
+#ifdef __APPLE__
+#define exp10f __exp10f
+#define exp10  __exp10
+#endif
+
+#if defined(_WIN32)
+#define RESTRICT __restrict
+#else
+#define RESTRICT __restrict__
+#endif
+
+class tonedspSIG0
+{
+   private:
+    int iVec0[2];
+    int iRec0[2];
+
+   public:
+    int getNumInputstonedspSIG0() { return 0; }
+    int getNumOutputstonedspSIG0() { return 1; }
+
+    void instanceInittonedspSIG0(int /*sample_rate*/)
+    {
+        for (int l0 = 0; l0 < 2; l0 = l0 + 1) {
+            iVec0[l0] = 0;
+        }
+        for (int l1 = 0; l1 < 2; l1 = l1 + 1) {
+            iRec0[l1] = 0;
+        }
+    }
+
+    void filltonedspSIG0(int count, float* table)
+    {
+        for (int i1 = 0; i1 < count; i1 = i1 + 1) {
+            iVec0[0]  = 1;
+            iRec0[0]  = (iVec0[1] + iRec0[1]) % 65536;
+            table[i1] = std::sin(9.58738e-05f * float(iRec0[0]));
+            iVec0[1]  = iVec0[0];
+            iRec0[1]  = iRec0[0];
+        }
+    }
+};
+
+static tonedspSIG0* newtonedspSIG0()
+{
+    return (tonedspSIG0*)new tonedspSIG0();
+}
+static void deletetonedspSIG0(tonedspSIG0* dsp)
+{
+    delete dsp;
+}
+
+static float ftbl0tonedspSIG0[65536];
+
+class tonedsp : public dsp
+{
+   private:
+    int fSampleRate;
+    float fConst0;
+    float fConst1;
+    FAUSTFLOAT fHslider0;
+    FAUSTFLOAT fHslider1;
+    float fRec1[2];
+    float fConst2;
+    float fConst3;
+    FAUSTFLOAT fButton0;
+    float fVec1[2];
+    int iRec2[2];
+    float fConst4;
+    FAUSTFLOAT fHslider2;
+    float fConst5;
+    float fRec3[2];
+    float fRec4[2];
+    float fConst6;
+    float fRec5[2];
+    float fConst7;
+    float fRec6[2];
+    float fConst8;
+    float fRec7[2];
+    float fConst9;
+    float fRec8[2];
+    float fConst10;
+
+   public:
+    void metadata(Meta* m)
+    {
+        m->declare("basics.lib/name", "Faust Basic Element Library");
+        m->declare("basics.lib/version", "0.8");
+        m->declare("compile_options",
+                   "-a faust2header.cpp -lang cpp -i -inpl -cn tonedsp -es 1 -mcd 16 "
+                   "-single -ftz 0");
+        m->declare("envelopes.lib/ar:author", "Yann Orlarey, Stéphane Letz");
+        m->declare("envelopes.lib/author", "GRAME");
+        m->declare("envelopes.lib/copyright", "GRAME");
+        m->declare("envelopes.lib/license", "LGPL with exception");
+        m->declare("envelopes.lib/name", "Faust Envelope Library");
+        m->declare("envelopes.lib/version", "0.2");
+        m->declare("filename", "tonedsp.dsp");
+        m->declare("maths.lib/author", "GRAME");
+        m->declare("maths.lib/copyright", "GRAME");
+        m->declare("maths.lib/license", "LGPL with exception");
+        m->declare("maths.lib/name", "Faust Math Library");
+        m->declare("maths.lib/version", "2.5");
+        m->declare("name", "tonedsp");
+        m->declare("oscillators.lib/name", "Faust Oscillator Library");
+        m->declare("oscillators.lib/version", "0.3");
+        m->declare("platform.lib/name", "Generic Platform Library");
+        m->declare("platform.lib/version", "0.2");
+        m->declare("signals.lib/name", "Faust Signal Routing Library");
+        m->declare("signals.lib/version", "0.3");
+        m->declare("synths.lib/additiveDrum:author", "Romain Michon");
+        m->declare("synths.lib/name", "Faust Synthesizer Library");
+        m->declare("synths.lib/version", "0.1");
+    }
+
+    virtual int getNumInputs() { return 0; }
+    virtual int getNumOutputs() { return 1; }
+
+    static void classInit(int sample_rate)
+    {
+        tonedspSIG0* sig0 = newtonedspSIG0();
+        sig0->instanceInittonedspSIG0(sample_rate);
+        sig0->filltonedspSIG0(65536, ftbl0tonedspSIG0);
+        deletetonedspSIG0(sig0);
+    }
+
+    virtual void instanceConstants(int sample_rate)
+    {
+        fSampleRate = sample_rate;
+        fConst0  = std::min<float>(1.92e+05f, std::max<float>(1.0f, float(fSampleRate)));
+        fConst1  = 1.0f / fConst0;
+        fConst2  = std::max<float>(1.0f, 0.001f * fConst0);
+        fConst3  = 1.0f / fConst2;
+        fConst4  = 44.1f / fConst0;
+        fConst5  = 1.0f - fConst4;
+        fConst6  = 0.8666667f * fConst0;
+        fConst7  = 0.73333335f * fConst0;
+        fConst8  = 0.6f * fConst0;
+        fConst9  = 0.46666667f * fConst0;
+        fConst10 = 0.33333334f * fConst0;
+    }
+
+    virtual void instanceResetUserInterface()
+    {
+        fHslider0 = FAUSTFLOAT(4e+02f);
+        fHslider1 = FAUSTFLOAT(0.0f);
+        fButton0  = FAUSTFLOAT(0.0f);
+        fHslider2 = FAUSTFLOAT(2.5f);
+    }
+
+    virtual void instanceClear()
+    {
+        for (int l2 = 0; l2 < 2; l2 = l2 + 1) {
+            fRec1[l2] = 0.0f;
+        }
+        for (int l3 = 0; l3 < 2; l3 = l3 + 1) {
+            fVec1[l3] = 0.0f;
+        }
+        for (int l4 = 0; l4 < 2; l4 = l4 + 1) {
+            iRec2[l4] = 0;
+        }
+        for (int l5 = 0; l5 < 2; l5 = l5 + 1) {
+            fRec3[l5] = 0.0f;
+        }
+        for (int l6 = 0; l6 < 2; l6 = l6 + 1) {
+            fRec4[l6] = 0.0f;
+        }
+        for (int l7 = 0; l7 < 2; l7 = l7 + 1) {
+            fRec5[l7] = 0.0f;
+        }
+        for (int l8 = 0; l8 < 2; l8 = l8 + 1) {
+            fRec6[l8] = 0.0f;
+        }
+        for (int l9 = 0; l9 < 2; l9 = l9 + 1) {
+            fRec7[l9] = 0.0f;
+        }
+        for (int l10 = 0; l10 < 2; l10 = l10 + 1) {
+            fRec8[l10] = 0.0f;
+        }
+    }
+
+    virtual void init(int sample_rate)
+    {
+        classInit(sample_rate);
+        instanceInit(sample_rate);
+    }
+    virtual void instanceInit(int sample_rate)
+    {
+        instanceConstants(sample_rate);
+        instanceResetUserInterface();
+        instanceClear();
+    }
+
+    virtual tonedsp* clone() { return new tonedsp(); }
+
+    virtual int getSampleRate() { return fSampleRate; }
+
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        ui_interface->openVerticalBox("tonedsp");
+        ui_interface->addHorizontalSlider("freq", &fHslider0, FAUSTFLOAT(4e+02f),
+                                          FAUSTFLOAT(5e+01f), FAUSTFLOAT(2e+03f),
+                                          FAUSTFLOAT(0.01f));
+        ui_interface->addButton("gate", &fButton0);
+        ui_interface->declare(&fHslider2, "acc", "0 0 -10 0 10");
+        ui_interface->addHorizontalSlider("res", &fHslider2, FAUSTFLOAT(2.5f),
+                                          FAUSTFLOAT(0.01f), FAUSTFLOAT(5.0f),
+                                          FAUSTFLOAT(0.01f));
+        ui_interface->addHorizontalSlider("y", &fHslider1, FAUSTFLOAT(0.0f),
+                                          FAUSTFLOAT(0.0f), FAUSTFLOAT(1.0f),
+                                          FAUSTFLOAT(0.01f));
+        ui_interface->closeBox();
+    }
+
+    virtual void compute(int count, FAUSTFLOAT** /*inputs*/, FAUSTFLOAT** outputs)
+    {
+        FAUSTFLOAT* output0 = outputs[0];
+        float fSlow0        = float(fHslider0);
+        float fSlow1        = float(fHslider1);
+        float fSlow2        = fConst1 * fSlow0 * (0.8333333f * fSlow1 + 1.0f);
+        float fSlow3        = float(fButton0);
+        float fSlow4        = fConst4 * float(fHslider2);
+        float fSlow5        = fConst1 * fSlow0 * (1.6666666f * fSlow1 + 1.0f);
+        float fSlow6        = fConst1 * fSlow0 * (2.5f * fSlow1 + 1.0f);
+        float fSlow7        = fConst1 * fSlow0 * (3.3333333f * fSlow1 + 1.0f);
+        float fSlow8        = fConst1 * fSlow0 * (4.1666665f * fSlow1 + 1.0f);
+        float fSlow9        = fConst1 * fSlow0 * (5.0f * fSlow1 + 1.0f);
+        for (int i0 = 0; i0 < count; i0 = i0 + 1) {
+            fRec1[0] = fSlow2 + (fRec1[1] - std::floor(fSlow2 + fRec1[1]));
+            fVec1[0] = fSlow3;
+            iRec2[0] =
+                (iRec2[1] + (iRec2[1] > 0)) * (fSlow3 <= fVec1[1]) + (fSlow3 > fVec1[1]);
+            float fTemp0 = float(iRec2[0]);
+            float fTemp1 = fConst3 * fTemp0;
+            float fTemp2 = fConst2 - fTemp0;
+            fRec3[0]     = fSlow4 + fConst5 * fRec3[1];
+            fRec4[0]     = fSlow5 + (fRec4[1] - std::floor(fSlow5 + fRec4[1]));
+            fRec5[0]     = fSlow6 + (fRec5[1] - std::floor(fSlow6 + fRec5[1]));
+            fRec6[0]     = fSlow7 + (fRec6[1] - std::floor(fSlow7 + fRec6[1]));
+            fRec7[0]     = fSlow8 + (fRec7[1] - std::floor(fSlow8 + fRec7[1]));
+            fRec8[0]     = fSlow9 + (fRec8[1] - std::floor(fSlow9 + fRec8[1]));
+            output0[i0]  = FAUSTFLOAT(
+                0.05f
+                * (0.44444445f * ftbl0tonedspSIG0[int(65536.0f * fRec1[0])]
+                       * std::max<float>(
+                           0.0f,
+                           std::min<float>(
+                               fTemp1,
+                               fTemp2 / std::max<float>(1.0f, fConst0 * fRec3[0]) + 1.0f))
+                   + ftbl0tonedspSIG0[int(65536.0f * fRec4[0])]
+                         * (0.0f
+                            - 0.11111111f
+                                  * std::max<float>(
+                                      0.0f,
+                                      std::min<float>(
+                                          fTemp1, fTemp2
+                                                          / std::max<float>(
+                                                              1.0f, fConst6 * fRec3[0])
+                                                      + 1.0f)))
+                   + ftbl0tonedspSIG0[int(65536.0f * fRec5[0])]
+                         * (0.0f
+                            - 0.6666667f
+                                  * std::max<float>(
+                                      0.0f,
+                                      std::min<float>(
+                                          fTemp1, fTemp2
+                                                          / std::max<float>(
+                                                              1.0f, fConst7 * fRec3[0])
+                                                      + 1.0f)))
+                   + ftbl0tonedspSIG0[int(65536.0f * fRec6[0])]
+                         * (0.0f
+                            - 1.2222222f
+                                  * std::max<float>(
+                                      0.0f,
+                                      std::min<float>(
+                                          fTemp1, fTemp2
+                                                          / std::max<float>(
+                                                              1.0f, fConst8 * fRec3[0])
+                                                      + 1.0f)))
+                   + ftbl0tonedspSIG0[int(65536.0f * fRec7[0])]
+                         * (0.0f
+                            - 1.7777778f
+                                  * std::max<float>(
+                                      0.0f,
+                                      std::min<float>(
+                                          fTemp1, fTemp2
+                                                          / std::max<float>(
+                                                              1.0f, fConst9 * fRec3[0])
+                                                      + 1.0f)))
+                   + ftbl0tonedspSIG0[int(65536.0f * fRec8[0])]
+                         * (0.0f
+                            - 2.3333333f
+                                  * std::max<float>(
+                                      0.0f,
+                                      std::min<float>(
+                                          fTemp1, fTemp2
+                                                          / std::max<float>(
+                                                              1.0f, fConst10 * fRec3[0])
+                                                      + 1.0f)))));
+            fRec1[1] = fRec1[0];
+            fVec1[1] = fVec1[0];
+            iRec2[1] = iRec2[0];
+            fRec3[1] = fRec3[0];
+            fRec4[1] = fRec4[0];
+            fRec5[1] = fRec5[0];
+            fRec6[1] = fRec6[0];
+            fRec7[1] = fRec7[0];
+            fRec8[1] = fRec8[0];
+        }
+    }
+};
+
+#endif
diff --git a/src/volumedsp.h b/src/volumedsp.h
new file mode 100644 (file)
index 0000000..88db980
--- /dev/null
@@ -0,0 +1,2092 @@
+/* ------------------------------------------------------------
+author: "Matt Horton, adapted from GRAME"
+license: "MIT Style STK-4.2"
+name: "volume"
+version: "1.0"
+Code generated with Faust 2.41.1 (https://faust.grame.fr)
+Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn volumedsp -es 1 -mcd 16
+-single -ftz 0
+------------------------------------------------------------ */
+
+#ifndef __volumedsp_H__
+#define __volumedsp_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 <string>
+#include <vector>
+
+/************************************************************************
+ ************************************************************************
+    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.41.1"
+
+// 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<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> 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 <xmmintrin.h>
+#endif
+
+class FAUST_API ScopedNoDenormals
+{
+   private:
+    intptr_t fpsr;
+
+    void setFpStatusRegister(intptr_t fpsr_aux) noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("msr fpcr, %0" : : "ri"(fpsr_aux));
+#elif defined(__SSE__)
+        _mm_setcsr(static_cast<uint32_t>(fpsr_aux));
+#endif
+    }
+
+    void getFpStatusRegister() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("mrs %0, fpcr" : "=r"(fpsr));
+#elif defined(__SSE__)
+        fpsr          = static_cast<intptr_t>(_mm_getcsr());
+#endif
+    }
+
+   public:
+    ScopedNoDenormals() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        intptr_t mask = (1 << 24 /* FZ */);
+#else
+#if defined(__SSE__)
+#if defined(__SSE2__)
+        intptr_t mask = 0x8040;
+#else
+        intptr_t mask = 0x8000;
+#endif
+#else
+        intptr_t mask = 0x0000;
+#endif
+#endif
+        getFpStatusRegister();
+        setFpStatusRegister(fpsr | mask);
+    }
+
+    ~ScopedNoDenormals() noexcept { setFpStatusRegister(fpsr); }
+};
+
+#define AVOIDDENORMALS ScopedNoDenormals();
+
+#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 <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** 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<typename REAL>
+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<FAUSTFLOAT> {
+    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 <algorithm>
+#include <map>
+#include <regex>
+#include <set>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class FAUST_API PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+    std::vector<std::string> fFullPaths;
+    std::map<std::string, std::string> 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<std::string>
+            uniquePaths;  // all full paths transformed but made unique with a prefix
+        std::map<std::string, std::string>
+            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<std::string, int> 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<std::string> collisionSet;
+            std::map<std::string, std::string> 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<char>& 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 <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// 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<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? 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<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(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<double>(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<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(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<double>(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<UpdatableValueConverter*> 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<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> 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<std::string, std::string> 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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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<ZoneControl*>* 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<ZoneControl*>* 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<ZoneControl*>* 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
+    //-------------------------------------------------------------------------------
+    int getParamsCount() { return int(fItems.size()); }
+
+    int getParamIndex(const char* path_aux)
+    {
+        std::string path = std::string(path_aux);
+        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;
+    }
+
+    const char* getParamLabel(int p) { return fItems[uint(p)].fLabel.c_str(); }
+    const char* getParamShortname(int p) { return fItems[uint(p)].fShortname.c_str(); }
+    const char* getParamAddress(int p) { return fItems[uint(p)].fPath.c_str(); }
+
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> metadata = fMetaData[uint(p)];
+        for (const auto& it : metadata) {
+            res[it.first.c_str()] = it.second.c_str();
+        }
+        return res;
+    }
+
+    const char* getMetadata(int p, const char* key)
+    {
+        return (fMetaData[uint(p)].find(key) != fMetaData[uint(p)].end())
+                   ? fMetaData[uint(p)][key].c_str()
+                   : "";
+    }
+    FAUSTFLOAT getParamMin(int p) { return fItems[uint(p)].fMin; }
+    FAUSTFLOAT getParamMax(int p) { return fItems[uint(p)].fMax; }
+    FAUSTFLOAT getParamStep(int p) { return fItems[uint(p)].fStep; }
+    FAUSTFLOAT getParamInit(int p) { return fItems[uint(p)].fInit; }
+
+    FAUSTFLOAT* getParamZone(int p) { return fItems[uint(p)].fZone; }
+
+    FAUSTFLOAT getParamValue(int p) { return *fItems[uint(p)].fZone; }
+    FAUSTFLOAT getParamValue(const char* path)
+    {
+        int index = getParamIndex(path);
+        if (index >= 0) {
+            return getParamValue(index);
+        } else {
+            fprintf(stderr, "getParamValue : '%s' not found\n",
+                    (path == nullptr ? "NULL" : path));
+            return FAUSTFLOAT(0);
+        }
+    }
+
+    void setParamValue(int p, FAUSTFLOAT v) { *fItems[uint(p)].fZone = v; }
+    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
+     * @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
+     * @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
+     * @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
+     * @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; }
+
+    // getScreenColor() : -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/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+#include <math.h>
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifndef FAUSTCLASS
+#define FAUSTCLASS volumedsp
+#endif
+
+#ifdef __APPLE__
+#define exp10f __exp10f
+#define exp10  __exp10
+#endif
+
+#if defined(_WIN32)
+#define RESTRICT __restrict
+#else
+#define RESTRICT __restrict__
+#endif
+
+class volumedsp : public dsp
+{
+   private:
+    FAUSTFLOAT fHslider0;
+    FAUSTFLOAT fCheckbox0;
+    int fSampleRate;
+    float fConst0;
+    float fConst1;
+    float fRec0[2];
+
+   public:
+    void metadata(Meta* m)
+    {
+        m->declare("author", "Matt Horton, adapted from GRAME");
+        m->declare("basics.lib/name", "Faust Basic Element Library");
+        m->declare("basics.lib/version", "0.8");
+        m->declare("compile_options",
+                   "-a faust2header.cpp -lang cpp -i -inpl -cn volumedsp -es 1 -mcd 16 "
+                   "-single -ftz 0");
+        m->declare("description",
+                   "Volume Control Faust Plugin for JackTrip, based on Faust examples");
+        m->declare("filename", "volumedsp.dsp");
+        m->declare("license", "MIT Style STK-4.2");
+        m->declare("maths.lib/author", "GRAME");
+        m->declare("maths.lib/copyright", "GRAME");
+        m->declare("maths.lib/license", "LGPL with exception");
+        m->declare("maths.lib/name", "Faust Math Library");
+        m->declare("maths.lib/version", "2.5");
+        m->declare("name", "volume");
+        m->declare("platform.lib/name", "Generic Platform Library");
+        m->declare("platform.lib/version", "0.2");
+        m->declare("signals.lib/name", "Faust Signal Routing Library");
+        m->declare("signals.lib/version", "0.3");
+        m->declare("version", "1.0");
+    }
+
+    virtual int getNumInputs() { return 1; }
+    virtual int getNumOutputs() { return 1; }
+
+    static void classInit(int /*sample_rate*/) {}
+
+    virtual void instanceConstants(int sample_rate)
+    {
+        fSampleRate = sample_rate;
+        fConst0     = 44.0999985f
+                  / std::min<float>(192000.0f, std::max<float>(1.0f, float(fSampleRate)));
+        fConst1 = 1.0f - fConst0;
+    }
+
+    virtual void instanceResetUserInterface()
+    {
+        fHslider0  = FAUSTFLOAT(0.0f);
+        fCheckbox0 = FAUSTFLOAT(0.0f);
+    }
+
+    virtual void instanceClear()
+    {
+        for (int l0 = 0; l0 < 2; l0 = l0 + 1) {
+            fRec0[l0] = 0.0f;
+        }
+    }
+
+    virtual void init(int sample_rate)
+    {
+        classInit(sample_rate);
+        instanceInit(sample_rate);
+    }
+    virtual void instanceInit(int sample_rate)
+    {
+        instanceConstants(sample_rate);
+        instanceResetUserInterface();
+        instanceClear();
+    }
+
+    virtual volumedsp* clone() { return new volumedsp(); }
+
+    virtual int getSampleRate() { return fSampleRate; }
+
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        ui_interface->openVerticalBox("Volume Control");
+        ui_interface->declare(&fHslider0, "0", "");
+        ui_interface->addHorizontalSlider("Volume", &fHslider0, FAUSTFLOAT(0.0f),
+                                          FAUSTFLOAT(-40.0f), FAUSTFLOAT(0.0f),
+                                          FAUSTFLOAT(0.100000001f));
+        ui_interface->declare(&fCheckbox0, "1", "");
+        ui_interface->addCheckButton("Mute", &fCheckbox0);
+        ui_interface->closeBox();
+    }
+
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs)
+    {
+        FAUSTFLOAT* input0  = inputs[0];
+        FAUSTFLOAT* output0 = outputs[0];
+        float fSlow0        = float(fHslider0);
+        int iSlow1          = fSlow0 == -40.0f;
+        int iSlow2          = int(float(fCheckbox0));
+        float fSlow3        = fConst0 * std::pow(10.0f, 0.0500000007f * fSlow0);
+        for (int i0 = 0; i0 < count; i0 = i0 + 1) {
+            float fTemp0 = float(input0[i0]);
+            fRec0[0]     = fSlow3 + fConst1 * fRec0[1];
+            float fThen0 = fTemp0 * fRec0[0];
+            float fThen1 = ((iSlow2) ? 0.0f : fThen0);
+            output0[i0]  = FAUSTFLOAT(((iSlow1) ? 0.0f : fThen1));
+            fRec0[1]     = fRec0[0];
+        }
+    }
+};
+
+#endif
diff --git a/src/zitarevdsp.h b/src/zitarevdsp.h
new file mode 100644 (file)
index 0000000..b7e839a
--- /dev/null
@@ -0,0 +1,2853 @@
+/* ------------------------------------------------------------
+name: "zitarevdsp"
+Code generated with Faust 2.41.1 (https://faust.grame.fr)
+Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn zitarevdsp -es 1 -mcd 16
+-single -ftz 0
+------------------------------------------------------------ */
+
+#ifndef __zitarevdsp_H__
+#define __zitarevdsp_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 <string>
+#include <vector>
+
+/************************************************************************
+ ************************************************************************
+    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.41.1"
+
+// 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<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> 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 <xmmintrin.h>
+#endif
+
+class FAUST_API ScopedNoDenormals
+{
+   private:
+    intptr_t fpsr;
+
+    void setFpStatusRegister(intptr_t fpsr_aux) noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("msr fpcr, %0" : : "ri"(fpsr_aux));
+#elif defined(__SSE__)
+        _mm_setcsr(static_cast<uint32_t>(fpsr_aux));
+#endif
+    }
+
+    void getFpStatusRegister() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("mrs %0, fpcr" : "=r"(fpsr));
+#elif defined(__SSE__)
+        fpsr          = static_cast<intptr_t>(_mm_getcsr());
+#endif
+    }
+
+   public:
+    ScopedNoDenormals() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        intptr_t mask = (1 << 24 /* FZ */);
+#else
+#if defined(__SSE__)
+#if defined(__SSE2__)
+        intptr_t mask = 0x8040;
+#else
+        intptr_t mask = 0x8000;
+#endif
+#else
+        intptr_t mask = 0x0000;
+#endif
+#endif
+        getFpStatusRegister();
+        setFpStatusRegister(fpsr | mask);
+    }
+
+    ~ScopedNoDenormals() noexcept { setFpStatusRegister(fpsr); }
+};
+
+#define AVOIDDENORMALS ScopedNoDenormals();
+
+#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 <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** 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<typename REAL>
+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<FAUSTFLOAT> {
+    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 <algorithm>
+#include <map>
+#include <regex>
+#include <set>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class FAUST_API PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+    std::vector<std::string> fFullPaths;
+    std::map<std::string, std::string> 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<std::string>
+            uniquePaths;  // all full paths transformed but made unique with a prefix
+        std::map<std::string, std::string>
+            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<std::string, int> 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<std::string> collisionSet;
+            std::map<std::string, std::string> 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<char>& 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 <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// 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<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? 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<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(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<double>(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<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(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<double>(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<UpdatableValueConverter*> 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<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> 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<std::string, std::string> 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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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<ZoneControl*>* 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<ZoneControl*>* 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<ZoneControl*>* 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
+    //-------------------------------------------------------------------------------
+    int getParamsCount() { return int(fItems.size()); }
+
+    int getParamIndex(const char* path_aux)
+    {
+        std::string path = std::string(path_aux);
+        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;
+    }
+
+    const char* getParamLabel(int p) { return fItems[uint(p)].fLabel.c_str(); }
+    const char* getParamShortname(int p) { return fItems[uint(p)].fShortname.c_str(); }
+    const char* getParamAddress(int p) { return fItems[uint(p)].fPath.c_str(); }
+
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> metadata = fMetaData[uint(p)];
+        for (const auto& it : metadata) {
+            res[it.first.c_str()] = it.second.c_str();
+        }
+        return res;
+    }
+
+    const char* getMetadata(int p, const char* key)
+    {
+        return (fMetaData[uint(p)].find(key) != fMetaData[uint(p)].end())
+                   ? fMetaData[uint(p)][key].c_str()
+                   : "";
+    }
+    FAUSTFLOAT getParamMin(int p) { return fItems[uint(p)].fMin; }
+    FAUSTFLOAT getParamMax(int p) { return fItems[uint(p)].fMax; }
+    FAUSTFLOAT getParamStep(int p) { return fItems[uint(p)].fStep; }
+    FAUSTFLOAT getParamInit(int p) { return fItems[uint(p)].fInit; }
+
+    FAUSTFLOAT* getParamZone(int p) { return fItems[uint(p)].fZone; }
+
+    FAUSTFLOAT getParamValue(int p) { return *fItems[uint(p)].fZone; }
+    FAUSTFLOAT getParamValue(const char* path)
+    {
+        int index = getParamIndex(path);
+        if (index >= 0) {
+            return getParamValue(index);
+        } else {
+            fprintf(stderr, "getParamValue : '%s' not found\n",
+                    (path == nullptr ? "NULL" : path));
+            return FAUSTFLOAT(0);
+        }
+    }
+
+    void setParamValue(int p, FAUSTFLOAT v) { *fItems[uint(p)].fZone = v; }
+    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
+     * @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
+     * @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
+     * @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
+     * @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; }
+
+    // getScreenColor() : -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/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+#include <math.h>
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifndef FAUSTCLASS
+#define FAUSTCLASS zitarevdsp
+#endif
+
+#ifdef __APPLE__
+#define exp10f __exp10f
+#define exp10  __exp10
+#endif
+
+#if defined(_WIN32)
+#define RESTRICT __restrict
+#else
+#define RESTRICT __restrict__
+#endif
+
+static float zitarevdsp_faustpower2_f(float value)
+{
+    return value * value;
+}
+
+class zitarevdsp : public dsp
+{
+   private:
+    int IOTA0;
+    float fVec0[16384];
+    float fVec1[16384];
+    int fSampleRate;
+    float fConst1;
+    FAUSTFLOAT fVslider0;
+    float fConst2;
+    float fRec0[2];
+    FAUSTFLOAT fVslider1;
+    float fRec1[2];
+    float fConst3;
+    FAUSTFLOAT fVslider2;
+    FAUSTFLOAT fVslider3;
+    FAUSTFLOAT fVslider4;
+    FAUSTFLOAT fVslider5;
+    float fConst5;
+    FAUSTFLOAT fVslider6;
+    FAUSTFLOAT fVslider7;
+    FAUSTFLOAT fVslider8;
+    float fConst6;
+    FAUSTFLOAT fVslider9;
+    float fRec15[2];
+    float fRec14[2];
+    float fVec2[32768];
+    int iConst8;
+    float fConst9;
+    FAUSTFLOAT fVslider10;
+    float fVec3[2048];
+    int iConst10;
+    float fRec12[2];
+    float fConst12;
+    float fRec19[2];
+    float fRec18[2];
+    float fVec4[32768];
+    int iConst14;
+    float fVec5[4096];
+    int iConst15;
+    float fRec16[2];
+    float fConst17;
+    float fRec23[2];
+    float fRec22[2];
+    float fVec6[16384];
+    int iConst19;
+    float fVec7[4096];
+    int iConst20;
+    float fRec20[2];
+    float fConst22;
+    float fRec27[2];
+    float fRec26[2];
+    float fVec8[32768];
+    int iConst24;
+    float fVec9[4096];
+    int iConst25;
+    float fRec24[2];
+    float fConst27;
+    float fRec31[2];
+    float fRec30[2];
+    float fVec10[16384];
+    int iConst29;
+    float fVec11[2048];
+    int iConst30;
+    float fRec28[2];
+    float fConst32;
+    float fRec35[2];
+    float fRec34[2];
+    float fVec12[16384];
+    int iConst34;
+    float fVec13[4096];
+    int iConst35;
+    float fRec32[2];
+    float fConst37;
+    float fRec39[2];
+    float fRec38[2];
+    float fVec14[16384];
+    int iConst39;
+    float fVec15[4096];
+    int iConst40;
+    float fRec36[2];
+    float fConst42;
+    float fRec43[2];
+    float fRec42[2];
+    float fVec16[16384];
+    int iConst44;
+    float fVec17[2048];
+    int iConst45;
+    float fRec40[2];
+    float fRec4[3];
+    float fRec5[3];
+    float fRec6[3];
+    float fRec7[3];
+    float fRec8[3];
+    float fRec9[3];
+    float fRec10[3];
+    float fRec11[3];
+    float fRec3[3];
+    float fRec2[3];
+    float fRec45[3];
+    float fRec44[3];
+
+   public:
+    void metadata(Meta* m)
+    {
+        m->declare("basics.lib/name", "Faust Basic Element Library");
+        m->declare("basics.lib/version", "0.8");
+        m->declare("compile_options",
+                   "-a faust2header.cpp -lang cpp -i -inpl -cn zitarevdsp -es 1 -mcd 16 "
+                   "-single -ftz 0");
+        m->declare("delays.lib/name", "Faust Delay Library");
+        m->declare("delays.lib/version", "0.1");
+        m->declare("filename", "zitarevdsp.dsp");
+        m->declare("filters.lib/allpass_comb:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/allpass_comb:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/allpass_comb:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/fir:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/fir:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/fir:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/iir:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/iir:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/iir:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/lowpass0_highpass1", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/lowpass0_highpass1:author", "Julius O. Smith III");
+        m->declare("filters.lib/lowpass:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/lowpass:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/lowpass:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/name", "Faust Filters Library");
+        m->declare("filters.lib/peak_eq_rm:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/peak_eq_rm:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/peak_eq_rm:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/tf1:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/tf1:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/tf1:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/tf1s:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/tf1s:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/tf1s:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/tf2:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/tf2:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/tf2:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/version", "0.3");
+        m->declare("maths.lib/author", "GRAME");
+        m->declare("maths.lib/copyright", "GRAME");
+        m->declare("maths.lib/license", "LGPL with exception");
+        m->declare("maths.lib/name", "Faust Math Library");
+        m->declare("maths.lib/version", "2.5");
+        m->declare("name", "zitarevdsp");
+        m->declare("platform.lib/name", "Generic Platform Library");
+        m->declare("platform.lib/version", "0.2");
+        m->declare("reverbs.lib/name", "Faust Reverb Library");
+        m->declare("reverbs.lib/version", "0.2");
+        m->declare("routes.lib/hadamard:author", "Remy Muller, revised by Romain Michon");
+        m->declare("routes.lib/name", "Faust Signal Routing Library");
+        m->declare("routes.lib/version", "0.2");
+        m->declare("signals.lib/name", "Faust Signal Routing Library");
+        m->declare("signals.lib/version", "0.3");
+    }
+
+    virtual int getNumInputs() { return 2; }
+    virtual int getNumOutputs() { return 2; }
+
+    static void classInit(int /*sample_rate*/) {}
+
+    virtual void instanceConstants(int sample_rate)
+    {
+        fSampleRate = sample_rate;
+        float fConst0 =
+            std::min<float>(192000.0f, std::max<float>(1.0f, float(fSampleRate)));
+        fConst1       = 44.0999985f / fConst0;
+        fConst2       = 1.0f - fConst1;
+        fConst3       = 6.28318548f / fConst0;
+        float fConst4 = std::floor(0.219990999f * fConst0 + 0.5f);
+        fConst5       = (0.0f - 6.90775537f * fConst4) / fConst0;
+        fConst6       = 3.14159274f / fConst0;
+        float fConst7 = std::floor(0.0191229992f * fConst0 + 0.5f);
+        iConst8 =
+            int(std::min<float>(16384.0f, std::max<float>(0.0f, fConst4 - fConst7)));
+        fConst9  = 0.00100000005f * fConst0;
+        iConst10 = int(std::min<float>(1024.0f, std::max<float>(0.0f, fConst7 + -1.0f)));
+        float fConst11 = std::floor(0.256891012f * fConst0 + 0.5f);
+        fConst12       = (0.0f - 6.90775537f * fConst11) / fConst0;
+        float fConst13 = std::floor(0.0273330007f * fConst0 + 0.5f);
+        iConst14 =
+            int(std::min<float>(16384.0f, std::max<float>(0.0f, fConst11 - fConst13)));
+        iConst15 = int(std::min<float>(2048.0f, std::max<float>(0.0f, fConst13 + -1.0f)));
+        float fConst16 = std::floor(0.192303002f * fConst0 + 0.5f);
+        fConst17       = (0.0f - 6.90775537f * fConst16) / fConst0;
+        float fConst18 = std::floor(0.0292910002f * fConst0 + 0.5f);
+        iConst19 =
+            int(std::min<float>(8192.0f, std::max<float>(0.0f, fConst16 - fConst18)));
+        iConst20 = int(std::min<float>(2048.0f, std::max<float>(0.0f, fConst18 + -1.0f)));
+        float fConst21 = std::floor(0.210389003f * fConst0 + 0.5f);
+        fConst22       = (0.0f - 6.90775537f * fConst21) / fConst0;
+        float fConst23 = std::floor(0.0244210009f * fConst0 + 0.5f);
+        iConst24 =
+            int(std::min<float>(16384.0f, std::max<float>(0.0f, fConst21 - fConst23)));
+        iConst25 = int(std::min<float>(2048.0f, std::max<float>(0.0f, fConst23 + -1.0f)));
+        float fConst26 = std::floor(0.125f * fConst0 + 0.5f);
+        fConst27       = (0.0f - 6.90775537f * fConst26) / fConst0;
+        float fConst28 = std::floor(0.0134579996f * fConst0 + 0.5f);
+        iConst29 =
+            int(std::min<float>(8192.0f, std::max<float>(0.0f, fConst26 - fConst28)));
+        iConst30 = int(std::min<float>(1024.0f, std::max<float>(0.0f, fConst28 + -1.0f)));
+        float fConst31 = std::floor(0.127837002f * fConst0 + 0.5f);
+        fConst32       = (0.0f - 6.90775537f * fConst31) / fConst0;
+        float fConst33 = std::floor(0.0316039994f * fConst0 + 0.5f);
+        iConst34 =
+            int(std::min<float>(8192.0f, std::max<float>(0.0f, fConst31 - fConst33)));
+        iConst35 = int(std::min<float>(2048.0f, std::max<float>(0.0f, fConst33 + -1.0f)));
+        float fConst36 = std::floor(0.174713001f * fConst0 + 0.5f);
+        fConst37       = (0.0f - 6.90775537f * fConst36) / fConst0;
+        float fConst38 = std::floor(0.0229039993f * fConst0 + 0.5f);
+        iConst39 =
+            int(std::min<float>(8192.0f, std::max<float>(0.0f, fConst36 - fConst38)));
+        iConst40 = int(std::min<float>(2048.0f, std::max<float>(0.0f, fConst38 + -1.0f)));
+        float fConst41 = std::floor(0.153128996f * fConst0 + 0.5f);
+        fConst42       = (0.0f - 6.90775537f * fConst41) / fConst0;
+        float fConst43 = std::floor(0.0203460008f * fConst0 + 0.5f);
+        iConst44 =
+            int(std::min<float>(8192.0f, std::max<float>(0.0f, fConst41 - fConst43)));
+        iConst45 = int(std::min<float>(1024.0f, std::max<float>(0.0f, fConst43 + -1.0f)));
+    }
+
+    virtual void instanceResetUserInterface()
+    {
+        fVslider0  = FAUSTFLOAT(-3.0f);
+        fVslider1  = FAUSTFLOAT(0.0f);
+        fVslider2  = FAUSTFLOAT(1500.0f);
+        fVslider3  = FAUSTFLOAT(0.0f);
+        fVslider4  = FAUSTFLOAT(315.0f);
+        fVslider5  = FAUSTFLOAT(0.0f);
+        fVslider6  = FAUSTFLOAT(2.0f);
+        fVslider7  = FAUSTFLOAT(6000.0f);
+        fVslider8  = FAUSTFLOAT(3.0f);
+        fVslider9  = FAUSTFLOAT(200.0f);
+        fVslider10 = FAUSTFLOAT(60.0f);
+    }
+
+    virtual void instanceClear()
+    {
+        IOTA0 = 0;
+        for (int l0 = 0; l0 < 16384; l0 = l0 + 1) {
+            fVec0[l0] = 0.0f;
+        }
+        for (int l1 = 0; l1 < 16384; l1 = l1 + 1) {
+            fVec1[l1] = 0.0f;
+        }
+        for (int l2 = 0; l2 < 2; l2 = l2 + 1) {
+            fRec0[l2] = 0.0f;
+        }
+        for (int l3 = 0; l3 < 2; l3 = l3 + 1) {
+            fRec1[l3] = 0.0f;
+        }
+        for (int l4 = 0; l4 < 2; l4 = l4 + 1) {
+            fRec15[l4] = 0.0f;
+        }
+        for (int l5 = 0; l5 < 2; l5 = l5 + 1) {
+            fRec14[l5] = 0.0f;
+        }
+        for (int l6 = 0; l6 < 32768; l6 = l6 + 1) {
+            fVec2[l6] = 0.0f;
+        }
+        for (int l7 = 0; l7 < 2048; l7 = l7 + 1) {
+            fVec3[l7] = 0.0f;
+        }
+        for (int l8 = 0; l8 < 2; l8 = l8 + 1) {
+            fRec12[l8] = 0.0f;
+        }
+        for (int l9 = 0; l9 < 2; l9 = l9 + 1) {
+            fRec19[l9] = 0.0f;
+        }
+        for (int l10 = 0; l10 < 2; l10 = l10 + 1) {
+            fRec18[l10] = 0.0f;
+        }
+        for (int l11 = 0; l11 < 32768; l11 = l11 + 1) {
+            fVec4[l11] = 0.0f;
+        }
+        for (int l12 = 0; l12 < 4096; l12 = l12 + 1) {
+            fVec5[l12] = 0.0f;
+        }
+        for (int l13 = 0; l13 < 2; l13 = l13 + 1) {
+            fRec16[l13] = 0.0f;
+        }
+        for (int l14 = 0; l14 < 2; l14 = l14 + 1) {
+            fRec23[l14] = 0.0f;
+        }
+        for (int l15 = 0; l15 < 2; l15 = l15 + 1) {
+            fRec22[l15] = 0.0f;
+        }
+        for (int l16 = 0; l16 < 16384; l16 = l16 + 1) {
+            fVec6[l16] = 0.0f;
+        }
+        for (int l17 = 0; l17 < 4096; l17 = l17 + 1) {
+            fVec7[l17] = 0.0f;
+        }
+        for (int l18 = 0; l18 < 2; l18 = l18 + 1) {
+            fRec20[l18] = 0.0f;
+        }
+        for (int l19 = 0; l19 < 2; l19 = l19 + 1) {
+            fRec27[l19] = 0.0f;
+        }
+        for (int l20 = 0; l20 < 2; l20 = l20 + 1) {
+            fRec26[l20] = 0.0f;
+        }
+        for (int l21 = 0; l21 < 32768; l21 = l21 + 1) {
+            fVec8[l21] = 0.0f;
+        }
+        for (int l22 = 0; l22 < 4096; l22 = l22 + 1) {
+            fVec9[l22] = 0.0f;
+        }
+        for (int l23 = 0; l23 < 2; l23 = l23 + 1) {
+            fRec24[l23] = 0.0f;
+        }
+        for (int l24 = 0; l24 < 2; l24 = l24 + 1) {
+            fRec31[l24] = 0.0f;
+        }
+        for (int l25 = 0; l25 < 2; l25 = l25 + 1) {
+            fRec30[l25] = 0.0f;
+        }
+        for (int l26 = 0; l26 < 16384; l26 = l26 + 1) {
+            fVec10[l26] = 0.0f;
+        }
+        for (int l27 = 0; l27 < 2048; l27 = l27 + 1) {
+            fVec11[l27] = 0.0f;
+        }
+        for (int l28 = 0; l28 < 2; l28 = l28 + 1) {
+            fRec28[l28] = 0.0f;
+        }
+        for (int l29 = 0; l29 < 2; l29 = l29 + 1) {
+            fRec35[l29] = 0.0f;
+        }
+        for (int l30 = 0; l30 < 2; l30 = l30 + 1) {
+            fRec34[l30] = 0.0f;
+        }
+        for (int l31 = 0; l31 < 16384; l31 = l31 + 1) {
+            fVec12[l31] = 0.0f;
+        }
+        for (int l32 = 0; l32 < 4096; l32 = l32 + 1) {
+            fVec13[l32] = 0.0f;
+        }
+        for (int l33 = 0; l33 < 2; l33 = l33 + 1) {
+            fRec32[l33] = 0.0f;
+        }
+        for (int l34 = 0; l34 < 2; l34 = l34 + 1) {
+            fRec39[l34] = 0.0f;
+        }
+        for (int l35 = 0; l35 < 2; l35 = l35 + 1) {
+            fRec38[l35] = 0.0f;
+        }
+        for (int l36 = 0; l36 < 16384; l36 = l36 + 1) {
+            fVec14[l36] = 0.0f;
+        }
+        for (int l37 = 0; l37 < 4096; l37 = l37 + 1) {
+            fVec15[l37] = 0.0f;
+        }
+        for (int l38 = 0; l38 < 2; l38 = l38 + 1) {
+            fRec36[l38] = 0.0f;
+        }
+        for (int l39 = 0; l39 < 2; l39 = l39 + 1) {
+            fRec43[l39] = 0.0f;
+        }
+        for (int l40 = 0; l40 < 2; l40 = l40 + 1) {
+            fRec42[l40] = 0.0f;
+        }
+        for (int l41 = 0; l41 < 16384; l41 = l41 + 1) {
+            fVec16[l41] = 0.0f;
+        }
+        for (int l42 = 0; l42 < 2048; l42 = l42 + 1) {
+            fVec17[l42] = 0.0f;
+        }
+        for (int l43 = 0; l43 < 2; l43 = l43 + 1) {
+            fRec40[l43] = 0.0f;
+        }
+        for (int l44 = 0; l44 < 3; l44 = l44 + 1) {
+            fRec4[l44] = 0.0f;
+        }
+        for (int l45 = 0; l45 < 3; l45 = l45 + 1) {
+            fRec5[l45] = 0.0f;
+        }
+        for (int l46 = 0; l46 < 3; l46 = l46 + 1) {
+            fRec6[l46] = 0.0f;
+        }
+        for (int l47 = 0; l47 < 3; l47 = l47 + 1) {
+            fRec7[l47] = 0.0f;
+        }
+        for (int l48 = 0; l48 < 3; l48 = l48 + 1) {
+            fRec8[l48] = 0.0f;
+        }
+        for (int l49 = 0; l49 < 3; l49 = l49 + 1) {
+            fRec9[l49] = 0.0f;
+        }
+        for (int l50 = 0; l50 < 3; l50 = l50 + 1) {
+            fRec10[l50] = 0.0f;
+        }
+        for (int l51 = 0; l51 < 3; l51 = l51 + 1) {
+            fRec11[l51] = 0.0f;
+        }
+        for (int l52 = 0; l52 < 3; l52 = l52 + 1) {
+            fRec3[l52] = 0.0f;
+        }
+        for (int l53 = 0; l53 < 3; l53 = l53 + 1) {
+            fRec2[l53] = 0.0f;
+        }
+        for (int l54 = 0; l54 < 3; l54 = l54 + 1) {
+            fRec45[l54] = 0.0f;
+        }
+        for (int l55 = 0; l55 < 3; l55 = l55 + 1) {
+            fRec44[l55] = 0.0f;
+        }
+    }
+
+    virtual void init(int sample_rate)
+    {
+        classInit(sample_rate);
+        instanceInit(sample_rate);
+    }
+    virtual void instanceInit(int sample_rate)
+    {
+        instanceConstants(sample_rate);
+        instanceResetUserInterface();
+        instanceClear();
+    }
+
+    virtual zitarevdsp* clone() { return new zitarevdsp(); }
+
+    virtual int getSampleRate() { return fSampleRate; }
+
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        ui_interface->declare(0, "0", "");
+        ui_interface->declare(0, "tooltip",
+                              "~ ZITA REV1 FEEDBACK DELAY NETWORK (FDN) & SCHROEDER  "
+                              "ALLPASS-COMB REVERBERATOR (8x8). See Faust's reverbs.lib "
+                              "for documentation and  references");
+        ui_interface->openHorizontalBox("Zita_Rev1");
+        ui_interface->declare(0, "1", "");
+        ui_interface->openHorizontalBox("Input");
+        ui_interface->declare(&fVslider10, "1", "");
+        ui_interface->declare(&fVslider10, "style", "knob");
+        ui_interface->declare(&fVslider10, "tooltip",
+                              "Delay in ms   before reverberation begins");
+        ui_interface->declare(&fVslider10, "unit", "ms");
+        ui_interface->addVerticalSlider("In Delay", &fVslider10, FAUSTFLOAT(60.0f),
+                                        FAUSTFLOAT(20.0f), FAUSTFLOAT(100.0f),
+                                        FAUSTFLOAT(1.0f));
+        ui_interface->closeBox();
+        ui_interface->declare(0, "2", "");
+        ui_interface->openHorizontalBox("Decay Times in Bands (see tooltips)");
+        ui_interface->declare(&fVslider9, "1", "");
+        ui_interface->declare(&fVslider9, "scale", "log");
+        ui_interface->declare(&fVslider9, "style", "knob");
+        ui_interface->declare(
+            &fVslider9, "tooltip",
+            "Crossover frequency (Hz) separating low and middle frequencies");
+        ui_interface->declare(&fVslider9, "unit", "Hz");
+        ui_interface->addVerticalSlider("LF X", &fVslider9, FAUSTFLOAT(200.0f),
+                                        FAUSTFLOAT(50.0f), FAUSTFLOAT(1000.0f),
+                                        FAUSTFLOAT(1.0f));
+        ui_interface->declare(&fVslider8, "2", "");
+        ui_interface->declare(&fVslider8, "scale", "log");
+        ui_interface->declare(&fVslider8, "style", "knob");
+        ui_interface->declare(
+            &fVslider8, "tooltip",
+            "T60 = time (in seconds) to decay 60dB in low-frequency band");
+        ui_interface->declare(&fVslider8, "unit", "s");
+        ui_interface->addVerticalSlider("Low RT60", &fVslider8, FAUSTFLOAT(3.0f),
+                                        FAUSTFLOAT(1.0f), FAUSTFLOAT(8.0f),
+                                        FAUSTFLOAT(0.100000001f));
+        ui_interface->declare(&fVslider6, "3", "");
+        ui_interface->declare(&fVslider6, "scale", "log");
+        ui_interface->declare(&fVslider6, "style", "knob");
+        ui_interface->declare(&fVslider6, "tooltip",
+                              "T60 = time (in seconds) to decay 60dB in middle band");
+        ui_interface->declare(&fVslider6, "unit", "s");
+        ui_interface->addVerticalSlider("Mid RT60", &fVslider6, FAUSTFLOAT(2.0f),
+                                        FAUSTFLOAT(1.0f), FAUSTFLOAT(8.0f),
+                                        FAUSTFLOAT(0.100000001f));
+        ui_interface->declare(&fVslider7, "4", "");
+        ui_interface->declare(&fVslider7, "scale", "log");
+        ui_interface->declare(&fVslider7, "style", "knob");
+        ui_interface->declare(&fVslider7, "tooltip",
+                              "Frequency (Hz) at which the high-frequency T60 is half "
+                              "the middle-band's T60");
+        ui_interface->declare(&fVslider7, "unit", "Hz");
+        ui_interface->addVerticalSlider("HF Damping", &fVslider7, FAUSTFLOAT(6000.0f),
+                                        FAUSTFLOAT(1500.0f), FAUSTFLOAT(23520.0f),
+                                        FAUSTFLOAT(1.0f));
+        ui_interface->closeBox();
+        ui_interface->declare(0, "3", "");
+        ui_interface->openHorizontalBox("RM Peaking Equalizer 1");
+        ui_interface->declare(&fVslider4, "1", "");
+        ui_interface->declare(&fVslider4, "scale", "log");
+        ui_interface->declare(&fVslider4, "style", "knob");
+        ui_interface->declare(
+            &fVslider4, "tooltip",
+            "Center-frequency of second-order Regalia-Mitra peaking equalizer section 1");
+        ui_interface->declare(&fVslider4, "unit", "Hz");
+        ui_interface->addVerticalSlider("Eq1 Freq", &fVslider4, FAUSTFLOAT(315.0f),
+                                        FAUSTFLOAT(40.0f), FAUSTFLOAT(2500.0f),
+                                        FAUSTFLOAT(1.0f));
+        ui_interface->declare(&fVslider5, "2", "");
+        ui_interface->declare(&fVslider5, "style", "knob");
+        ui_interface->declare(&fVslider5, "tooltip",
+                              "Peak level   in dB of second-order Regalia-Mitra peaking "
+                              "equalizer section 1");
+        ui_interface->declare(&fVslider5, "unit", "dB");
+        ui_interface->addVerticalSlider("Eq1 Level", &fVslider5, FAUSTFLOAT(0.0f),
+                                        FAUSTFLOAT(-15.0f), FAUSTFLOAT(15.0f),
+                                        FAUSTFLOAT(0.100000001f));
+        ui_interface->closeBox();
+        ui_interface->declare(0, "4", "");
+        ui_interface->openHorizontalBox("RM Peaking Equalizer 2");
+        ui_interface->declare(&fVslider2, "1", "");
+        ui_interface->declare(&fVslider2, "scale", "log");
+        ui_interface->declare(&fVslider2, "style", "knob");
+        ui_interface->declare(
+            &fVslider2, "tooltip",
+            "Center-frequency of second-order Regalia-Mitra peaking equalizer section 2");
+        ui_interface->declare(&fVslider2, "unit", "Hz");
+        ui_interface->addVerticalSlider("Eq2 Freq", &fVslider2, FAUSTFLOAT(1500.0f),
+                                        FAUSTFLOAT(160.0f), FAUSTFLOAT(10000.0f),
+                                        FAUSTFLOAT(1.0f));
+        ui_interface->declare(&fVslider3, "2", "");
+        ui_interface->declare(&fVslider3, "style", "knob");
+        ui_interface->declare(&fVslider3, "tooltip",
+                              "Peak level   in dB of second-order Regalia-Mitra peaking "
+                              "equalizer section 2");
+        ui_interface->declare(&fVslider3, "unit", "dB");
+        ui_interface->addVerticalSlider("Eq2 Level", &fVslider3, FAUSTFLOAT(0.0f),
+                                        FAUSTFLOAT(-15.0f), FAUSTFLOAT(15.0f),
+                                        FAUSTFLOAT(0.100000001f));
+        ui_interface->closeBox();
+        ui_interface->declare(0, "5", "");
+        ui_interface->openHorizontalBox("Output");
+        ui_interface->declare(&fVslider1, "1", "");
+        ui_interface->declare(&fVslider1, "style", "knob");
+        ui_interface->declare(&fVslider1, "tooltip", "Dry/Wet Mix: 0 = dry, 1 = wet");
+        ui_interface->addVerticalSlider("Wet", &fVslider1, FAUSTFLOAT(0.0f),
+                                        FAUSTFLOAT(0.0f), FAUSTFLOAT(1.0f),
+                                        FAUSTFLOAT(0.00999999978f));
+        ui_interface->declare(&fVslider0, "2", "");
+        ui_interface->declare(&fVslider0, "style", "knob");
+        ui_interface->declare(&fVslider0, "tooltip", "Output scale   factor");
+        ui_interface->declare(&fVslider0, "unit", "dB");
+        ui_interface->addVerticalSlider("Level", &fVslider0, FAUSTFLOAT(-3.0f),
+                                        FAUSTFLOAT(-70.0f), FAUSTFLOAT(20.0f),
+                                        FAUSTFLOAT(0.100000001f));
+        ui_interface->closeBox();
+        ui_interface->closeBox();
+    }
+
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs)
+    {
+        FAUSTFLOAT* input0  = inputs[0];
+        FAUSTFLOAT* input1  = inputs[1];
+        FAUSTFLOAT* output0 = outputs[0];
+        FAUSTFLOAT* output1 = outputs[1];
+        float fSlow0        = fConst1 * std::pow(10.0f, 0.0500000007f * float(fVslider0));
+        float fSlow1        = fConst1 * float(fVslider1);
+        float fSlow2        = float(fVslider2);
+        float fSlow3        = std::pow(10.0f, 0.0500000007f * float(fVslider3));
+        float fSlow4        = fConst3 * fSlow2 / std::sqrt(std::max<float>(0.0f, fSlow3));
+        float fSlow5        = (1.0f - fSlow4) / (fSlow4 + 1.0f);
+        float fSlow6        = float(fVslider4);
+        float fSlow7        = std::pow(10.0f, 0.0500000007f * float(fVslider5));
+        float fSlow8        = fConst3 * fSlow6 / std::sqrt(std::max<float>(0.0f, fSlow7));
+        float fSlow9        = (1.0f - fSlow8) / (fSlow8 + 1.0f);
+        float fSlow10       = float(fVslider6);
+        float fSlow11       = std::exp(fConst5 / fSlow10);
+        float fSlow12       = zitarevdsp_faustpower2_f(fSlow11);
+        float fSlow13       = std::cos(fConst3 * float(fVslider7));
+        float fSlow14       = 1.0f - fSlow12 * fSlow13;
+        float fSlow15       = 1.0f - fSlow12;
+        float fSlow16       = std::sqrt(std::max<float>(
+            0.0f, zitarevdsp_faustpower2_f(fSlow14) / zitarevdsp_faustpower2_f(fSlow15)
+                      + -1.0f));
+        float fSlow17       = fSlow14 / fSlow15;
+        float fSlow18       = fSlow11 * (fSlow16 + 1.0f - fSlow17);
+        float fSlow19       = float(fVslider8);
+        float fSlow20       = std::exp(fConst5 / fSlow19) / fSlow11 + -1.0f;
+        float fSlow21       = 1.0f / std::tan(fConst6 * float(fVslider9));
+        float fSlow22       = 1.0f / (fSlow21 + 1.0f);
+        float fSlow23       = 1.0f - fSlow21;
+        float fSlow24       = fSlow17 - fSlow16;
+        int iSlow25         = int(
+            std::min<float>(8192.0f, std::max<float>(0.0f, fConst9 * float(fVslider10))));
+        float fSlow26 = std::exp(fConst12 / fSlow10);
+        float fSlow27 = zitarevdsp_faustpower2_f(fSlow26);
+        float fSlow28 = 1.0f - fSlow27 * fSlow13;
+        float fSlow29 = 1.0f - fSlow27;
+        float fSlow30 = std::sqrt(std::max<float>(
+            0.0f, zitarevdsp_faustpower2_f(fSlow28) / zitarevdsp_faustpower2_f(fSlow29)
+                      + -1.0f));
+        float fSlow31 = fSlow28 / fSlow29;
+        float fSlow32 = fSlow26 * (fSlow30 + 1.0f - fSlow31);
+        float fSlow33 = std::exp(fConst12 / fSlow19) / fSlow26 + -1.0f;
+        float fSlow34 = fSlow31 - fSlow30;
+        float fSlow35 = std::exp(fConst17 / fSlow10);
+        float fSlow36 = zitarevdsp_faustpower2_f(fSlow35);
+        float fSlow37 = 1.0f - fSlow36 * fSlow13;
+        float fSlow38 = 1.0f - fSlow36;
+        float fSlow39 = std::sqrt(std::max<float>(
+            0.0f, zitarevdsp_faustpower2_f(fSlow37) / zitarevdsp_faustpower2_f(fSlow38)
+                      + -1.0f));
+        float fSlow40 = fSlow37 / fSlow38;
+        float fSlow41 = fSlow35 * (fSlow39 + 1.0f - fSlow40);
+        float fSlow42 = std::exp(fConst17 / fSlow19) / fSlow35 + -1.0f;
+        float fSlow43 = fSlow40 - fSlow39;
+        float fSlow44 = std::exp(fConst22 / fSlow10);
+        float fSlow45 = zitarevdsp_faustpower2_f(fSlow44);
+        float fSlow46 = 1.0f - fSlow45 * fSlow13;
+        float fSlow47 = 1.0f - fSlow45;
+        float fSlow48 = std::sqrt(std::max<float>(
+            0.0f, zitarevdsp_faustpower2_f(fSlow46) / zitarevdsp_faustpower2_f(fSlow47)
+                      + -1.0f));
+        float fSlow49 = fSlow46 / fSlow47;
+        float fSlow50 = fSlow44 * (fSlow48 + 1.0f - fSlow49);
+        float fSlow51 = std::exp(fConst22 / fSlow19) / fSlow44 + -1.0f;
+        float fSlow52 = fSlow49 - fSlow48;
+        float fSlow53 = std::exp(fConst27 / fSlow10);
+        float fSlow54 = zitarevdsp_faustpower2_f(fSlow53);
+        float fSlow55 = 1.0f - fSlow54 * fSlow13;
+        float fSlow56 = 1.0f - fSlow54;
+        float fSlow57 = std::sqrt(std::max<float>(
+            0.0f, zitarevdsp_faustpower2_f(fSlow55) / zitarevdsp_faustpower2_f(fSlow56)
+                      + -1.0f));
+        float fSlow58 = fSlow55 / fSlow56;
+        float fSlow59 = fSlow53 * (fSlow57 + 1.0f - fSlow58);
+        float fSlow60 = std::exp(fConst27 / fSlow19) / fSlow53 + -1.0f;
+        float fSlow61 = fSlow58 - fSlow57;
+        float fSlow62 = std::exp(fConst32 / fSlow10);
+        float fSlow63 = zitarevdsp_faustpower2_f(fSlow62);
+        float fSlow64 = 1.0f - fSlow63 * fSlow13;
+        float fSlow65 = 1.0f - fSlow63;
+        float fSlow66 = std::sqrt(std::max<float>(
+            0.0f, zitarevdsp_faustpower2_f(fSlow64) / zitarevdsp_faustpower2_f(fSlow65)
+                      + -1.0f));
+        float fSlow67 = fSlow64 / fSlow65;
+        float fSlow68 = fSlow62 * (fSlow66 + 1.0f - fSlow67);
+        float fSlow69 = std::exp(fConst32 / fSlow19) / fSlow62 + -1.0f;
+        float fSlow70 = fSlow67 - fSlow66;
+        float fSlow71 = std::exp(fConst37 / fSlow10);
+        float fSlow72 = zitarevdsp_faustpower2_f(fSlow71);
+        float fSlow73 = 1.0f - fSlow72 * fSlow13;
+        float fSlow74 = 1.0f - fSlow72;
+        float fSlow75 = std::sqrt(std::max<float>(
+            0.0f, zitarevdsp_faustpower2_f(fSlow73) / zitarevdsp_faustpower2_f(fSlow74)
+                      + -1.0f));
+        float fSlow76 = fSlow73 / fSlow74;
+        float fSlow77 = fSlow71 * (fSlow75 + 1.0f - fSlow76);
+        float fSlow78 = std::exp(fConst37 / fSlow19) / fSlow71 + -1.0f;
+        float fSlow79 = fSlow76 - fSlow75;
+        float fSlow80 = std::exp(fConst42 / fSlow10);
+        float fSlow81 = zitarevdsp_faustpower2_f(fSlow80);
+        float fSlow82 = 1.0f - fSlow81 * fSlow13;
+        float fSlow83 = 1.0f - fSlow81;
+        float fSlow84 = std::sqrt(std::max<float>(
+            0.0f, zitarevdsp_faustpower2_f(fSlow82) / zitarevdsp_faustpower2_f(fSlow83)
+                      + -1.0f));
+        float fSlow85 = fSlow82 / fSlow83;
+        float fSlow86 = fSlow80 * (fSlow84 + 1.0f - fSlow85);
+        float fSlow87 = std::exp(fConst42 / fSlow19) / fSlow80 + -1.0f;
+        float fSlow88 = fSlow85 - fSlow84;
+        float fSlow89 = 0.0f - std::cos(fConst3 * fSlow6) * (fSlow9 + 1.0f);
+        float fSlow90 = 0.0f - std::cos(fConst3 * fSlow2) * (fSlow5 + 1.0f);
+        for (int i0 = 0; i0 < count; i0 = i0 + 1) {
+            float fTemp0         = float(input0[i0]);
+            fVec0[IOTA0 & 16383] = fTemp0;
+            float fTemp1         = float(input1[i0]);
+            fVec1[IOTA0 & 16383] = fTemp1;
+            fRec0[0]             = fSlow0 + fConst2 * fRec0[1];
+            fRec1[0]             = fSlow1 + fConst2 * fRec1[1];
+            fRec15[0] = 0.0f - fSlow22 * (fSlow23 * fRec15[1] - (fRec11[1] + fRec11[2]));
+            fRec14[0] = fSlow18 * (fRec11[1] + fSlow20 * fRec15[0]) + fSlow24 * fRec14[1];
+            fVec2[IOTA0 & 32767] = 0.353553385f * fRec14[0] + 9.99999968e-21f;
+            float fTemp2         = 0.300000012f * fVec1[(IOTA0 - iSlow25) & 16383];
+            float fTemp3 =
+                (0.600000024f * fRec12[1] + fVec2[(IOTA0 - iConst8) & 32767]) - fTemp2;
+            fVec3[IOTA0 & 2047] = fTemp3;
+            fRec12[0]           = fVec3[(IOTA0 - iConst10) & 2047];
+            float fRec13        = 0.0f - 0.600000024f * fTemp3;
+            fRec19[0] = 0.0f - fSlow22 * (fSlow23 * fRec19[1] - (fRec7[1] + fRec7[2]));
+            fRec18[0] = fSlow32 * (fRec7[1] + fSlow33 * fRec19[0]) + fSlow34 * fRec18[1];
+            fVec4[IOTA0 & 32767] = 0.353553385f * fRec18[0] + 9.99999968e-21f;
+            float fTemp4 =
+                (0.600000024f * fRec16[1] + fVec4[(IOTA0 - iConst14) & 32767]) - fTemp2;
+            fVec5[IOTA0 & 4095] = fTemp4;
+            fRec16[0]           = fVec5[(IOTA0 - iConst15) & 4095];
+            float fRec17        = 0.0f - 0.600000024f * fTemp4;
+            fRec23[0] = 0.0f - fSlow22 * (fSlow23 * fRec23[1] - (fRec9[1] + fRec9[2]));
+            fRec22[0] = fSlow41 * (fRec9[1] + fSlow42 * fRec23[0]) + fSlow43 * fRec22[1];
+            fVec6[IOTA0 & 16383] = 0.353553385f * fRec22[0] + 9.99999968e-21f;
+            float fTemp5 =
+                fVec6[(IOTA0 - iConst19) & 16383] + fTemp2 + 0.600000024f * fRec20[1];
+            fVec7[IOTA0 & 4095] = fTemp5;
+            fRec20[0]           = fVec7[(IOTA0 - iConst20) & 4095];
+            float fRec21        = 0.0f - 0.600000024f * fTemp5;
+            fRec27[0] = 0.0f - fSlow22 * (fSlow23 * fRec27[1] - (fRec5[1] + fRec5[2]));
+            fRec26[0] = fSlow50 * (fRec5[1] + fSlow51 * fRec27[0]) + fSlow52 * fRec26[1];
+            fVec8[IOTA0 & 32767] = 0.353553385f * fRec26[0] + 9.99999968e-21f;
+            float fTemp6 =
+                fTemp2 + 0.600000024f * fRec24[1] + fVec8[(IOTA0 - iConst24) & 32767];
+            fVec9[IOTA0 & 4095] = fTemp6;
+            fRec24[0]           = fVec9[(IOTA0 - iConst25) & 4095];
+            float fRec25        = 0.0f - 0.600000024f * fTemp6;
+            fRec31[0] = 0.0f - fSlow22 * (fSlow23 * fRec31[1] - (fRec10[1] + fRec10[2]));
+            fRec30[0] = fSlow59 * (fRec10[1] + fSlow60 * fRec31[0]) + fSlow61 * fRec30[1];
+            fVec10[IOTA0 & 16383] = 0.353553385f * fRec30[0] + 9.99999968e-21f;
+            float fTemp7          = 0.300000012f * fVec0[(IOTA0 - iSlow25) & 16383];
+            float fTemp8 =
+                fVec10[(IOTA0 - iConst29) & 16383] - (fTemp7 + 0.600000024f * fRec28[1]);
+            fVec11[IOTA0 & 2047] = fTemp8;
+            fRec28[0]            = fVec11[(IOTA0 - iConst30) & 2047];
+            float fRec29         = 0.600000024f * fTemp8;
+            fRec35[0] = 0.0f - fSlow22 * (fSlow23 * fRec35[1] - (fRec6[1] + fRec6[2]));
+            fRec34[0] = fSlow68 * (fRec6[1] + fSlow69 * fRec35[0]) + fSlow70 * fRec34[1];
+            fVec12[IOTA0 & 16383] = 0.353553385f * fRec34[0] + 9.99999968e-21f;
+            float fTemp9 =
+                fVec12[(IOTA0 - iConst34) & 16383] - (fTemp7 + 0.600000024f * fRec32[1]);
+            fVec13[IOTA0 & 4095] = fTemp9;
+            fRec32[0]            = fVec13[(IOTA0 - iConst35) & 4095];
+            float fRec33         = 0.600000024f * fTemp9;
+            fRec39[0] = 0.0f - fSlow22 * (fSlow23 * fRec39[1] - (fRec8[1] + fRec8[2]));
+            fRec38[0] = fSlow77 * (fRec8[1] + fSlow78 * fRec39[0]) + fSlow79 * fRec38[1];
+            fVec14[IOTA0 & 16383] = 0.353553385f * fRec38[0] + 9.99999968e-21f;
+            float fTemp10 =
+                (fTemp7 + fVec14[(IOTA0 - iConst39) & 16383]) - 0.600000024f * fRec36[1];
+            fVec15[IOTA0 & 4095] = fTemp10;
+            fRec36[0]            = fVec15[(IOTA0 - iConst40) & 4095];
+            float fRec37         = 0.600000024f * fTemp10;
+            fRec43[0] = 0.0f - fSlow22 * (fSlow23 * fRec43[1] - (fRec4[1] + fRec4[2]));
+            fRec42[0] = fSlow86 * (fRec4[1] + fSlow87 * fRec43[0]) + fSlow88 * fRec42[1];
+            fVec16[IOTA0 & 16383] = 0.353553385f * fRec42[0] + 9.99999968e-21f;
+            float fTemp11 =
+                (fVec16[(IOTA0 - iConst44) & 16383] + fTemp7) - 0.600000024f * fRec40[1];
+            fVec17[IOTA0 & 2047] = fTemp11;
+            fRec40[0]            = fVec17[(IOTA0 - iConst45) & 2047];
+            float fRec41         = 0.600000024f * fTemp11;
+            float fTemp12        = fRec40[1] + fRec36[1];
+            float fTemp13 =
+                fRec29 + fRec33 + fRec37 + fRec41 + fRec28[1] + fTemp12 + fRec32[1];
+            fRec4[0] = fRec12[1] + fRec16[1] + fRec20[1] + fRec24[1] + fRec13 + fRec17
+                       + fRec21 + fRec25 + fTemp13;
+            fRec5[0] = fTemp13
+                       - (fRec12[1] + fRec16[1] + fRec20[1] + fRec24[1] + fRec13 + fRec17
+                          + fRec25 + fRec21);
+            float fTemp14 = fRec37 + fRec41 + fTemp12;
+            float fTemp15 = fRec29 + fRec33 + fRec32[1] + fRec28[1];
+            fRec6[0]      = (fRec20[1] + fRec24[1] + fRec21 + fRec25 + fTemp14)
+                       - (fRec12[1] + fRec16[1] + fRec13 + fRec17 + fTemp15);
+            fRec7[0] = (fRec12[1] + fRec16[1] + fRec13 + fRec17 + fTemp14)
+                       - (fRec20[1] + fRec24[1] + fRec21 + fRec25 + fTemp15);
+            float fTemp16 = fRec33 + fRec41 + fRec40[1] + fRec32[1];
+            float fTemp17 = fRec29 + fRec37 + fRec36[1] + fRec28[1];
+            fRec8[0]      = (fRec16[1] + fRec24[1] + fRec17 + fRec25 + fTemp16)
+                       - (fRec12[1] + fRec20[1] + fRec13 + fRec21 + fTemp17);
+            fRec9[0] = (fRec12[1] + fRec20[1] + fRec13 + fRec21 + fTemp16)
+                       - (fRec16[1] + fRec24[1] + fRec17 + fRec25 + fTemp17);
+            float fTemp18 = fRec29 + fRec41 + fRec40[1] + fRec28[1];
+            float fTemp19 = fRec33 + fRec37 + fRec36[1] + fRec32[1];
+            fRec10[0]     = (fRec12[1] + fRec24[1] + fRec13 + fRec25 + fTemp18)
+                        - (fRec16[1] + fRec20[1] + fRec17 + fRec21 + fTemp19);
+            fRec11[0] = (fRec16[1] + fRec20[1] + fRec17 + fRec21 + fTemp18)
+                        - (fRec12[1] + fRec24[1] + fRec13 + fRec25 + fTemp19);
+            float fTemp20 = 0.370000005f * (fRec5[0] + fRec6[0]);
+            float fTemp21 = fSlow89 * fRec3[1];
+            fRec3[0]      = fTemp20 - (fTemp21 + fSlow9 * fRec3[2]);
+            float fTemp22 = fSlow9 * fRec3[0];
+            float fTemp23 = 0.5f
+                            * (fTemp22 + fRec3[2] + fTemp20 + fTemp21
+                               + fSlow7 * ((fTemp22 + fTemp21 + fRec3[2]) - fTemp20));
+            float fTemp24 = fSlow90 * fRec2[1];
+            fRec2[0]      = fTemp23 - (fTemp24 + fSlow5 * fRec2[2]);
+            float fTemp25 = fSlow5 * fRec2[0];
+            float fTemp26 = 1.0f - fRec1[0];
+            output0[i0]   = FAUSTFLOAT(
+                fRec0[0]
+                * (0.5f * fRec1[0]
+                       * (fTemp25 + fRec2[2] + fTemp23 + fTemp24
+                          + fSlow3 * ((fTemp25 + fTemp24 + fRec2[2]) - fTemp23))
+                   + fTemp0 * fTemp26));
+            float fTemp27 = 0.370000005f * (fRec5[0] - fRec6[0]);
+            float fTemp28 = fSlow89 * fRec45[1];
+            fRec45[0]     = fTemp27 - (fTemp28 + fSlow9 * fRec45[2]);
+            float fTemp29 = fSlow9 * fRec45[0];
+            float fTemp30 = 0.5f
+                            * (fTemp29 + fRec45[2] + fTemp27 + fTemp28
+                               + fSlow7 * ((fTemp29 + fTemp28 + fRec45[2]) - fTemp27));
+            float fTemp31 = fSlow90 * fRec44[1];
+            fRec44[0]     = fTemp30 - (fTemp31 + fSlow5 * fRec44[2]);
+            float fTemp32 = fSlow5 * fRec44[0];
+            output1[i0]   = FAUSTFLOAT(
+                fRec0[0]
+                * (0.5f * fRec1[0]
+                       * (fTemp32 + fRec44[2] + fTemp30 + fTemp31
+                          + fSlow3 * ((fTemp32 + fTemp31 + fRec44[2]) - fTemp30))
+                   + fTemp1 * fTemp26));
+            IOTA0     = IOTA0 + 1;
+            fRec0[1]  = fRec0[0];
+            fRec1[1]  = fRec1[0];
+            fRec15[1] = fRec15[0];
+            fRec14[1] = fRec14[0];
+            fRec12[1] = fRec12[0];
+            fRec19[1] = fRec19[0];
+            fRec18[1] = fRec18[0];
+            fRec16[1] = fRec16[0];
+            fRec23[1] = fRec23[0];
+            fRec22[1] = fRec22[0];
+            fRec20[1] = fRec20[0];
+            fRec27[1] = fRec27[0];
+            fRec26[1] = fRec26[0];
+            fRec24[1] = fRec24[0];
+            fRec31[1] = fRec31[0];
+            fRec30[1] = fRec30[0];
+            fRec28[1] = fRec28[0];
+            fRec35[1] = fRec35[0];
+            fRec34[1] = fRec34[0];
+            fRec32[1] = fRec32[0];
+            fRec39[1] = fRec39[0];
+            fRec38[1] = fRec38[0];
+            fRec36[1] = fRec36[0];
+            fRec43[1] = fRec43[0];
+            fRec42[1] = fRec42[0];
+            fRec40[1] = fRec40[0];
+            fRec4[2]  = fRec4[1];
+            fRec4[1]  = fRec4[0];
+            fRec5[2]  = fRec5[1];
+            fRec5[1]  = fRec5[0];
+            fRec6[2]  = fRec6[1];
+            fRec6[1]  = fRec6[0];
+            fRec7[2]  = fRec7[1];
+            fRec7[1]  = fRec7[0];
+            fRec8[2]  = fRec8[1];
+            fRec8[1]  = fRec8[0];
+            fRec9[2]  = fRec9[1];
+            fRec9[1]  = fRec9[0];
+            fRec10[2] = fRec10[1];
+            fRec10[1] = fRec10[0];
+            fRec11[2] = fRec11[1];
+            fRec11[1] = fRec11[0];
+            fRec3[2]  = fRec3[1];
+            fRec3[1]  = fRec3[0];
+            fRec2[2]  = fRec2[1];
+            fRec2[1]  = fRec2[0];
+            fRec45[2] = fRec45[1];
+            fRec45[1] = fRec45[0];
+            fRec44[2] = fRec44[1];
+            fRec44[1] = fRec44[0];
+        }
+    }
+};
+
+#endif
diff --git a/src/zitarevmonodsp.h b/src/zitarevmonodsp.h
new file mode 100644 (file)
index 0000000..4df8807
--- /dev/null
@@ -0,0 +1,2860 @@
+/* ------------------------------------------------------------
+name: "zitarevmonodsp"
+Code generated with Faust 2.41.1 (https://faust.grame.fr)
+Compilation options: -a faust2header.cpp -lang cpp -i -inpl -cn zitarevmonodsp -es 1 -mcd
+16 -single -ftz 0
+------------------------------------------------------------ */
+
+#ifndef __zitarevmonodsp_H__
+#define __zitarevmonodsp_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 <string>
+#include <vector>
+
+/************************************************************************
+ ************************************************************************
+    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.41.1"
+
+// 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<std::string> getLibraryList()      = 0;
+    virtual std::vector<std::string> 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 <xmmintrin.h>
+#endif
+
+class FAUST_API ScopedNoDenormals
+{
+   private:
+    intptr_t fpsr;
+
+    void setFpStatusRegister(intptr_t fpsr_aux) noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("msr fpcr, %0" : : "ri"(fpsr_aux));
+#elif defined(__SSE__)
+        _mm_setcsr(static_cast<uint32_t>(fpsr_aux));
+#endif
+    }
+
+    void getFpStatusRegister() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        asm volatile("mrs %0, fpcr" : "=r"(fpsr));
+#elif defined(__SSE__)
+        fpsr          = static_cast<intptr_t>(_mm_getcsr());
+#endif
+    }
+
+   public:
+    ScopedNoDenormals() noexcept
+    {
+#if defined(__arm64__) || defined(__aarch64__)
+        intptr_t mask = (1 << 24 /* FZ */);
+#else
+#if defined(__SSE__)
+#if defined(__SSE2__)
+        intptr_t mask = 0x8040;
+#else
+        intptr_t mask = 0x8000;
+#endif
+#else
+        intptr_t mask = 0x0000;
+#endif
+#endif
+        getFpStatusRegister();
+        setFpStatusRegister(fpsr | mask);
+    }
+
+    ~ScopedNoDenormals() noexcept { setFpStatusRegister(fpsr); }
+};
+
+#define AVOIDDENORMALS ScopedNoDenormals();
+
+#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 <stdio.h>
+
+#include <map>
+#include <sstream>
+#include <string>
+#include <vector>
+
+/************************** 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<typename REAL>
+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<FAUSTFLOAT> {
+    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 <algorithm>
+#include <map>
+#include <regex>
+#include <set>
+#include <string>
+#include <vector>
+
+/*******************************************************************************
+ * PathBuilder : Faust User Interface
+ * Helper class to build complete hierarchical path for UI items.
+ ******************************************************************************/
+
+class FAUST_API PathBuilder
+{
+   protected:
+    std::vector<std::string> fControlsLevel;
+    std::vector<std::string> fFullPaths;
+    std::map<std::string, std::string> 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<std::string>
+            uniquePaths;  // all full paths transformed but made unique with a prefix
+        std::map<std::string, std::string>
+            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<std::string, int> 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<std::string> collisionSet;
+            std::map<std::string, std::string> 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<char>& 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 <assert.h>
+#include <float.h>
+
+#include <algorithm>  // std::max
+#include <cmath>
+#include <vector>
+
+//--------------------------------------------------------------------------------------
+// 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<double>(x, y)), fHi(std::max<double>(x, y))
+        {
+        }
+        double operator()(double x) { return (x < fLo) ? fLo : (x > fHi) ? 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<double>(DBL_MIN, fmin)),
+                               std::log(std::max<double>(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<double>(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<double>(DBL_MAX, std::exp(fmin)),
+                               std::min<double>(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<double>(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<UpdatableValueConverter*> 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<Item> fItems;
+
+    std::vector<std::map<std::string, std::string> > fMetaData;
+    std::vector<ZoneControl*> fAcc[3];
+    std::vector<ZoneControl*> 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<std::string, std::string> 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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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 : <axe> <curve> <amin> <amid> <amax>]..."
+        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<ZoneControl*>* 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<ZoneControl*>* 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<ZoneControl*>* 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
+    //-------------------------------------------------------------------------------
+    int getParamsCount() { return int(fItems.size()); }
+
+    int getParamIndex(const char* path_aux)
+    {
+        std::string path = std::string(path_aux);
+        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;
+    }
+
+    const char* getParamLabel(int p) { return fItems[uint(p)].fLabel.c_str(); }
+    const char* getParamShortname(int p) { return fItems[uint(p)].fShortname.c_str(); }
+    const char* getParamAddress(int p) { return fItems[uint(p)].fPath.c_str(); }
+
+    std::map<const char*, const char*> getMetadata(int p)
+    {
+        std::map<const char*, const char*> res;
+        std::map<std::string, std::string> metadata = fMetaData[uint(p)];
+        for (const auto& it : metadata) {
+            res[it.first.c_str()] = it.second.c_str();
+        }
+        return res;
+    }
+
+    const char* getMetadata(int p, const char* key)
+    {
+        return (fMetaData[uint(p)].find(key) != fMetaData[uint(p)].end())
+                   ? fMetaData[uint(p)][key].c_str()
+                   : "";
+    }
+    FAUSTFLOAT getParamMin(int p) { return fItems[uint(p)].fMin; }
+    FAUSTFLOAT getParamMax(int p) { return fItems[uint(p)].fMax; }
+    FAUSTFLOAT getParamStep(int p) { return fItems[uint(p)].fStep; }
+    FAUSTFLOAT getParamInit(int p) { return fItems[uint(p)].fInit; }
+
+    FAUSTFLOAT* getParamZone(int p) { return fItems[uint(p)].fZone; }
+
+    FAUSTFLOAT getParamValue(int p) { return *fItems[uint(p)].fZone; }
+    FAUSTFLOAT getParamValue(const char* path)
+    {
+        int index = getParamIndex(path);
+        if (index >= 0) {
+            return getParamValue(index);
+        } else {
+            fprintf(stderr, "getParamValue : '%s' not found\n",
+                    (path == nullptr ? "NULL" : path));
+            return FAUSTFLOAT(0);
+        }
+    }
+
+    void setParamValue(int p, FAUSTFLOAT v) { *fItems[uint(p)].fZone = v; }
+    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
+     * @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
+     * @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
+     * @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
+     * @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; }
+
+    // getScreenColor() : -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/name/name.h>
+
+//----------------------------------------------------------------------------
+//  FAUST Generated Code
+//----------------------------------------------------------------------------
+
+#ifndef FAUSTFLOAT
+#define FAUSTFLOAT float
+#endif
+
+#include <math.h>
+
+#include <algorithm>
+#include <cmath>
+#include <cstdint>
+
+#ifndef FAUSTCLASS
+#define FAUSTCLASS zitarevmonodsp
+#endif
+
+#ifdef __APPLE__
+#define exp10f __exp10f
+#define exp10  __exp10
+#endif
+
+#if defined(_WIN32)
+#define RESTRICT __restrict
+#else
+#define RESTRICT __restrict__
+#endif
+
+static float zitarevmonodsp_faustpower2_f(float value)
+{
+    return value * value;
+}
+
+class zitarevmonodsp : public dsp
+{
+   private:
+    int IOTA0;
+    float fVec0[16384];
+    int fSampleRate;
+    float fConst1;
+    FAUSTFLOAT fVslider0;
+    float fConst2;
+    float fRec0[2];
+    FAUSTFLOAT fVslider1;
+    float fRec1[2];
+    float fConst3;
+    FAUSTFLOAT fVslider2;
+    FAUSTFLOAT fVslider3;
+    FAUSTFLOAT fVslider4;
+    FAUSTFLOAT fVslider5;
+    float fConst5;
+    FAUSTFLOAT fVslider6;
+    FAUSTFLOAT fVslider7;
+    FAUSTFLOAT fVslider8;
+    float fConst6;
+    FAUSTFLOAT fVslider9;
+    float fRec15[2];
+    float fRec14[2];
+    float fVec1[32768];
+    int iConst8;
+    float fConst9;
+    FAUSTFLOAT fVslider10;
+    float fVec2[2048];
+    int iConst10;
+    float fRec12[2];
+    float fConst12;
+    float fRec19[2];
+    float fRec18[2];
+    float fVec3[32768];
+    int iConst14;
+    float fVec4[4096];
+    int iConst15;
+    float fRec16[2];
+    float fConst17;
+    float fRec23[2];
+    float fRec22[2];
+    float fVec5[16384];
+    int iConst19;
+    float fVec6[4096];
+    int iConst20;
+    float fRec20[2];
+    float fConst22;
+    float fRec27[2];
+    float fRec26[2];
+    float fVec7[32768];
+    int iConst24;
+    float fVec8[4096];
+    int iConst25;
+    float fRec24[2];
+    float fConst27;
+    float fRec31[2];
+    float fRec30[2];
+    float fVec9[16384];
+    int iConst29;
+    float fVec10[2048];
+    int iConst30;
+    float fRec28[2];
+    float fConst32;
+    float fRec35[2];
+    float fRec34[2];
+    float fVec11[16384];
+    int iConst34;
+    float fVec12[4096];
+    int iConst35;
+    float fRec32[2];
+    float fConst37;
+    float fRec39[2];
+    float fRec38[2];
+    float fVec13[16384];
+    int iConst39;
+    float fVec14[4096];
+    int iConst40;
+    float fRec36[2];
+    float fConst42;
+    float fRec43[2];
+    float fRec42[2];
+    float fVec15[16384];
+    int iConst44;
+    float fVec16[2048];
+    int iConst45;
+    float fRec40[2];
+    float fRec4[3];
+    float fRec5[3];
+    float fRec6[3];
+    float fRec7[3];
+    float fRec8[3];
+    float fRec9[3];
+    float fRec10[3];
+    float fRec11[3];
+    float fRec3[3];
+    float fRec2[3];
+    float fRec45[3];
+    float fRec44[3];
+
+   public:
+    void metadata(Meta* m)
+    {
+        m->declare("basics.lib/name", "Faust Basic Element Library");
+        m->declare("basics.lib/version", "0.8");
+        m->declare("compile_options",
+                   "-a faust2header.cpp -lang cpp -i -inpl -cn zitarevmonodsp -es 1 -mcd "
+                   "16 -single -ftz 0");
+        m->declare("delays.lib/name", "Faust Delay Library");
+        m->declare("delays.lib/version", "0.1");
+        m->declare("filename", "zitarevmonodsp.dsp");
+        m->declare("filters.lib/allpass_comb:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/allpass_comb:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/allpass_comb:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/fir:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/fir:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/fir:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/iir:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/iir:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/iir:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/lowpass0_highpass1", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/lowpass0_highpass1:author", "Julius O. Smith III");
+        m->declare("filters.lib/lowpass:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/lowpass:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/lowpass:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/name", "Faust Filters Library");
+        m->declare("filters.lib/peak_eq_rm:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/peak_eq_rm:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/peak_eq_rm:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/tf1:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/tf1:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/tf1:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/tf1s:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/tf1s:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/tf1s:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/tf2:author", "Julius O. Smith III");
+        m->declare(
+            "filters.lib/tf2:copyright",
+            "Copyright (C) 2003-2019 by Julius O. Smith III <jos@ccrma.stanford.edu>");
+        m->declare("filters.lib/tf2:license", "MIT-style STK-4.3 license");
+        m->declare("filters.lib/version", "0.3");
+        m->declare("maths.lib/author", "GRAME");
+        m->declare("maths.lib/copyright", "GRAME");
+        m->declare("maths.lib/license", "LGPL with exception");
+        m->declare("maths.lib/name", "Faust Math Library");
+        m->declare("maths.lib/version", "2.5");
+        m->declare("name", "zitarevmonodsp");
+        m->declare("platform.lib/name", "Generic Platform Library");
+        m->declare("platform.lib/version", "0.2");
+        m->declare("reverbs.lib/name", "Faust Reverb Library");
+        m->declare("reverbs.lib/version", "0.2");
+        m->declare("routes.lib/hadamard:author", "Remy Muller, revised by Romain Michon");
+        m->declare("routes.lib/name", "Faust Signal Routing Library");
+        m->declare("routes.lib/version", "0.2");
+        m->declare("signals.lib/name", "Faust Signal Routing Library");
+        m->declare("signals.lib/version", "0.3");
+    }
+
+    virtual int getNumInputs() { return 1; }
+    virtual int getNumOutputs() { return 1; }
+
+    static void classInit(int /*sample_rate*/) {}
+
+    virtual void instanceConstants(int sample_rate)
+    {
+        fSampleRate = sample_rate;
+        float fConst0 =
+            std::min<float>(192000.0f, std::max<float>(1.0f, float(fSampleRate)));
+        fConst1       = 44.0999985f / fConst0;
+        fConst2       = 1.0f - fConst1;
+        fConst3       = 6.28318548f / fConst0;
+        float fConst4 = std::floor(0.219990999f * fConst0 + 0.5f);
+        fConst5       = (0.0f - 6.90775537f * fConst4) / fConst0;
+        fConst6       = 3.14159274f / fConst0;
+        float fConst7 = std::floor(0.0191229992f * fConst0 + 0.5f);
+        iConst8 =
+            int(std::min<float>(16384.0f, std::max<float>(0.0f, fConst4 - fConst7)));
+        fConst9  = 0.00100000005f * fConst0;
+        iConst10 = int(std::min<float>(1024.0f, std::max<float>(0.0f, fConst7 + -1.0f)));
+        float fConst11 = std::floor(0.256891012f * fConst0 + 0.5f);
+        fConst12       = (0.0f - 6.90775537f * fConst11) / fConst0;
+        float fConst13 = std::floor(0.0273330007f * fConst0 + 0.5f);
+        iConst14 =
+            int(std::min<float>(16384.0f, std::max<float>(0.0f, fConst11 - fConst13)));
+        iConst15 = int(std::min<float>(2048.0f, std::max<float>(0.0f, fConst13 + -1.0f)));
+        float fConst16 = std::floor(0.192303002f * fConst0 + 0.5f);
+        fConst17       = (0.0f - 6.90775537f * fConst16) / fConst0;
+        float fConst18 = std::floor(0.0292910002f * fConst0 + 0.5f);
+        iConst19 =
+            int(std::min<float>(8192.0f, std::max<float>(0.0f, fConst16 - fConst18)));
+        iConst20 = int(std::min<float>(2048.0f, std::max<float>(0.0f, fConst18 + -1.0f)));
+        float fConst21 = std::floor(0.210389003f * fConst0 + 0.5f);
+        fConst22       = (0.0f - 6.90775537f * fConst21) / fConst0;
+        float fConst23 = std::floor(0.0244210009f * fConst0 + 0.5f);
+        iConst24 =
+            int(std::min<float>(16384.0f, std::max<float>(0.0f, fConst21 - fConst23)));
+        iConst25 = int(std::min<float>(2048.0f, std::max<float>(0.0f, fConst23 + -1.0f)));
+        float fConst26 = std::floor(0.125f * fConst0 + 0.5f);
+        fConst27       = (0.0f - 6.90775537f * fConst26) / fConst0;
+        float fConst28 = std::floor(0.0134579996f * fConst0 + 0.5f);
+        iConst29 =
+            int(std::min<float>(8192.0f, std::max<float>(0.0f, fConst26 - fConst28)));
+        iConst30 = int(std::min<float>(1024.0f, std::max<float>(0.0f, fConst28 + -1.0f)));
+        float fConst31 = std::floor(0.127837002f * fConst0 + 0.5f);
+        fConst32       = (0.0f - 6.90775537f * fConst31) / fConst0;
+        float fConst33 = std::floor(0.0316039994f * fConst0 + 0.5f);
+        iConst34 =
+            int(std::min<float>(8192.0f, std::max<float>(0.0f, fConst31 - fConst33)));
+        iConst35 = int(std::min<float>(2048.0f, std::max<float>(0.0f, fConst33 + -1.0f)));
+        float fConst36 = std::floor(0.174713001f * fConst0 + 0.5f);
+        fConst37       = (0.0f - 6.90775537f * fConst36) / fConst0;
+        float fConst38 = std::floor(0.0229039993f * fConst0 + 0.5f);
+        iConst39 =
+            int(std::min<float>(8192.0f, std::max<float>(0.0f, fConst36 - fConst38)));
+        iConst40 = int(std::min<float>(2048.0f, std::max<float>(0.0f, fConst38 + -1.0f)));
+        float fConst41 = std::floor(0.153128996f * fConst0 + 0.5f);
+        fConst42       = (0.0f - 6.90775537f * fConst41) / fConst0;
+        float fConst43 = std::floor(0.0203460008f * fConst0 + 0.5f);
+        iConst44 =
+            int(std::min<float>(8192.0f, std::max<float>(0.0f, fConst41 - fConst43)));
+        iConst45 = int(std::min<float>(1024.0f, std::max<float>(0.0f, fConst43 + -1.0f)));
+    }
+
+    virtual void instanceResetUserInterface()
+    {
+        fVslider0  = FAUSTFLOAT(-3.0f);
+        fVslider1  = FAUSTFLOAT(0.0f);
+        fVslider2  = FAUSTFLOAT(1500.0f);
+        fVslider3  = FAUSTFLOAT(0.0f);
+        fVslider4  = FAUSTFLOAT(315.0f);
+        fVslider5  = FAUSTFLOAT(0.0f);
+        fVslider6  = FAUSTFLOAT(2.0f);
+        fVslider7  = FAUSTFLOAT(6000.0f);
+        fVslider8  = FAUSTFLOAT(3.0f);
+        fVslider9  = FAUSTFLOAT(200.0f);
+        fVslider10 = FAUSTFLOAT(60.0f);
+    }
+
+    virtual void instanceClear()
+    {
+        IOTA0 = 0;
+        for (int l0 = 0; l0 < 16384; l0 = l0 + 1) {
+            fVec0[l0] = 0.0f;
+        }
+        for (int l1 = 0; l1 < 2; l1 = l1 + 1) {
+            fRec0[l1] = 0.0f;
+        }
+        for (int l2 = 0; l2 < 2; l2 = l2 + 1) {
+            fRec1[l2] = 0.0f;
+        }
+        for (int l3 = 0; l3 < 2; l3 = l3 + 1) {
+            fRec15[l3] = 0.0f;
+        }
+        for (int l4 = 0; l4 < 2; l4 = l4 + 1) {
+            fRec14[l4] = 0.0f;
+        }
+        for (int l5 = 0; l5 < 32768; l5 = l5 + 1) {
+            fVec1[l5] = 0.0f;
+        }
+        for (int l6 = 0; l6 < 2048; l6 = l6 + 1) {
+            fVec2[l6] = 0.0f;
+        }
+        for (int l7 = 0; l7 < 2; l7 = l7 + 1) {
+            fRec12[l7] = 0.0f;
+        }
+        for (int l8 = 0; l8 < 2; l8 = l8 + 1) {
+            fRec19[l8] = 0.0f;
+        }
+        for (int l9 = 0; l9 < 2; l9 = l9 + 1) {
+            fRec18[l9] = 0.0f;
+        }
+        for (int l10 = 0; l10 < 32768; l10 = l10 + 1) {
+            fVec3[l10] = 0.0f;
+        }
+        for (int l11 = 0; l11 < 4096; l11 = l11 + 1) {
+            fVec4[l11] = 0.0f;
+        }
+        for (int l12 = 0; l12 < 2; l12 = l12 + 1) {
+            fRec16[l12] = 0.0f;
+        }
+        for (int l13 = 0; l13 < 2; l13 = l13 + 1) {
+            fRec23[l13] = 0.0f;
+        }
+        for (int l14 = 0; l14 < 2; l14 = l14 + 1) {
+            fRec22[l14] = 0.0f;
+        }
+        for (int l15 = 0; l15 < 16384; l15 = l15 + 1) {
+            fVec5[l15] = 0.0f;
+        }
+        for (int l16 = 0; l16 < 4096; l16 = l16 + 1) {
+            fVec6[l16] = 0.0f;
+        }
+        for (int l17 = 0; l17 < 2; l17 = l17 + 1) {
+            fRec20[l17] = 0.0f;
+        }
+        for (int l18 = 0; l18 < 2; l18 = l18 + 1) {
+            fRec27[l18] = 0.0f;
+        }
+        for (int l19 = 0; l19 < 2; l19 = l19 + 1) {
+            fRec26[l19] = 0.0f;
+        }
+        for (int l20 = 0; l20 < 32768; l20 = l20 + 1) {
+            fVec7[l20] = 0.0f;
+        }
+        for (int l21 = 0; l21 < 4096; l21 = l21 + 1) {
+            fVec8[l21] = 0.0f;
+        }
+        for (int l22 = 0; l22 < 2; l22 = l22 + 1) {
+            fRec24[l22] = 0.0f;
+        }
+        for (int l23 = 0; l23 < 2; l23 = l23 + 1) {
+            fRec31[l23] = 0.0f;
+        }
+        for (int l24 = 0; l24 < 2; l24 = l24 + 1) {
+            fRec30[l24] = 0.0f;
+        }
+        for (int l25 = 0; l25 < 16384; l25 = l25 + 1) {
+            fVec9[l25] = 0.0f;
+        }
+        for (int l26 = 0; l26 < 2048; l26 = l26 + 1) {
+            fVec10[l26] = 0.0f;
+        }
+        for (int l27 = 0; l27 < 2; l27 = l27 + 1) {
+            fRec28[l27] = 0.0f;
+        }
+        for (int l28 = 0; l28 < 2; l28 = l28 + 1) {
+            fRec35[l28] = 0.0f;
+        }
+        for (int l29 = 0; l29 < 2; l29 = l29 + 1) {
+            fRec34[l29] = 0.0f;
+        }
+        for (int l30 = 0; l30 < 16384; l30 = l30 + 1) {
+            fVec11[l30] = 0.0f;
+        }
+        for (int l31 = 0; l31 < 4096; l31 = l31 + 1) {
+            fVec12[l31] = 0.0f;
+        }
+        for (int l32 = 0; l32 < 2; l32 = l32 + 1) {
+            fRec32[l32] = 0.0f;
+        }
+        for (int l33 = 0; l33 < 2; l33 = l33 + 1) {
+            fRec39[l33] = 0.0f;
+        }
+        for (int l34 = 0; l34 < 2; l34 = l34 + 1) {
+            fRec38[l34] = 0.0f;
+        }
+        for (int l35 = 0; l35 < 16384; l35 = l35 + 1) {
+            fVec13[l35] = 0.0f;
+        }
+        for (int l36 = 0; l36 < 4096; l36 = l36 + 1) {
+            fVec14[l36] = 0.0f;
+        }
+        for (int l37 = 0; l37 < 2; l37 = l37 + 1) {
+            fRec36[l37] = 0.0f;
+        }
+        for (int l38 = 0; l38 < 2; l38 = l38 + 1) {
+            fRec43[l38] = 0.0f;
+        }
+        for (int l39 = 0; l39 < 2; l39 = l39 + 1) {
+            fRec42[l39] = 0.0f;
+        }
+        for (int l40 = 0; l40 < 16384; l40 = l40 + 1) {
+            fVec15[l40] = 0.0f;
+        }
+        for (int l41 = 0; l41 < 2048; l41 = l41 + 1) {
+            fVec16[l41] = 0.0f;
+        }
+        for (int l42 = 0; l42 < 2; l42 = l42 + 1) {
+            fRec40[l42] = 0.0f;
+        }
+        for (int l43 = 0; l43 < 3; l43 = l43 + 1) {
+            fRec4[l43] = 0.0f;
+        }
+        for (int l44 = 0; l44 < 3; l44 = l44 + 1) {
+            fRec5[l44] = 0.0f;
+        }
+        for (int l45 = 0; l45 < 3; l45 = l45 + 1) {
+            fRec6[l45] = 0.0f;
+        }
+        for (int l46 = 0; l46 < 3; l46 = l46 + 1) {
+            fRec7[l46] = 0.0f;
+        }
+        for (int l47 = 0; l47 < 3; l47 = l47 + 1) {
+            fRec8[l47] = 0.0f;
+        }
+        for (int l48 = 0; l48 < 3; l48 = l48 + 1) {
+            fRec9[l48] = 0.0f;
+        }
+        for (int l49 = 0; l49 < 3; l49 = l49 + 1) {
+            fRec10[l49] = 0.0f;
+        }
+        for (int l50 = 0; l50 < 3; l50 = l50 + 1) {
+            fRec11[l50] = 0.0f;
+        }
+        for (int l51 = 0; l51 < 3; l51 = l51 + 1) {
+            fRec3[l51] = 0.0f;
+        }
+        for (int l52 = 0; l52 < 3; l52 = l52 + 1) {
+            fRec2[l52] = 0.0f;
+        }
+        for (int l53 = 0; l53 < 3; l53 = l53 + 1) {
+            fRec45[l53] = 0.0f;
+        }
+        for (int l54 = 0; l54 < 3; l54 = l54 + 1) {
+            fRec44[l54] = 0.0f;
+        }
+    }
+
+    virtual void init(int sample_rate)
+    {
+        classInit(sample_rate);
+        instanceInit(sample_rate);
+    }
+    virtual void instanceInit(int sample_rate)
+    {
+        instanceConstants(sample_rate);
+        instanceResetUserInterface();
+        instanceClear();
+    }
+
+    virtual zitarevmonodsp* clone() { return new zitarevmonodsp(); }
+
+    virtual int getSampleRate() { return fSampleRate; }
+
+    virtual void buildUserInterface(UI* ui_interface)
+    {
+        ui_interface->declare(0, "0", "");
+        ui_interface->declare(0, "tooltip",
+                              "~ ZITA REV1 FEEDBACK DELAY NETWORK (FDN) & SCHROEDER  "
+                              "ALLPASS-COMB REVERBERATOR (8x8). See Faust's reverbs.lib "
+                              "for documentation and  references");
+        ui_interface->openHorizontalBox("Zita_Rev1");
+        ui_interface->declare(0, "1", "");
+        ui_interface->openHorizontalBox("Input");
+        ui_interface->declare(&fVslider10, "1", "");
+        ui_interface->declare(&fVslider10, "style", "knob");
+        ui_interface->declare(&fVslider10, "tooltip",
+                              "Delay in ms   before reverberation begins");
+        ui_interface->declare(&fVslider10, "unit", "ms");
+        ui_interface->addVerticalSlider("In Delay", &fVslider10, FAUSTFLOAT(60.0f),
+                                        FAUSTFLOAT(20.0f), FAUSTFLOAT(100.0f),
+                                        FAUSTFLOAT(1.0f));
+        ui_interface->closeBox();
+        ui_interface->declare(0, "2", "");
+        ui_interface->openHorizontalBox("Decay Times in Bands (see tooltips)");
+        ui_interface->declare(&fVslider9, "1", "");
+        ui_interface->declare(&fVslider9, "scale", "log");
+        ui_interface->declare(&fVslider9, "style", "knob");
+        ui_interface->declare(
+            &fVslider9, "tooltip",
+            "Crossover frequency (Hz) separating low and middle frequencies");
+        ui_interface->declare(&fVslider9, "unit", "Hz");
+        ui_interface->addVerticalSlider("LF X", &fVslider9, FAUSTFLOAT(200.0f),
+                                        FAUSTFLOAT(50.0f), FAUSTFLOAT(1000.0f),
+                                        FAUSTFLOAT(1.0f));
+        ui_interface->declare(&fVslider8, "2", "");
+        ui_interface->declare(&fVslider8, "scale", "log");
+        ui_interface->declare(&fVslider8, "style", "knob");
+        ui_interface->declare(
+            &fVslider8, "tooltip",
+            "T60 = time (in seconds) to decay 60dB in low-frequency band");
+        ui_interface->declare(&fVslider8, "unit", "s");
+        ui_interface->addVerticalSlider("Low RT60", &fVslider8, FAUSTFLOAT(3.0f),
+                                        FAUSTFLOAT(1.0f), FAUSTFLOAT(8.0f),
+                                        FAUSTFLOAT(0.100000001f));
+        ui_interface->declare(&fVslider6, "3", "");
+        ui_interface->declare(&fVslider6, "scale", "log");
+        ui_interface->declare(&fVslider6, "style", "knob");
+        ui_interface->declare(&fVslider6, "tooltip",
+                              "T60 = time (in seconds) to decay 60dB in middle band");
+        ui_interface->declare(&fVslider6, "unit", "s");
+        ui_interface->addVerticalSlider("Mid RT60", &fVslider6, FAUSTFLOAT(2.0f),
+                                        FAUSTFLOAT(1.0f), FAUSTFLOAT(8.0f),
+                                        FAUSTFLOAT(0.100000001f));
+        ui_interface->declare(&fVslider7, "4", "");
+        ui_interface->declare(&fVslider7, "scale", "log");
+        ui_interface->declare(&fVslider7, "style", "knob");
+        ui_interface->declare(&fVslider7, "tooltip",
+                              "Frequency (Hz) at which the high-frequency T60 is half "
+                              "the middle-band's T60");
+        ui_interface->declare(&fVslider7, "unit", "Hz");
+        ui_interface->addVerticalSlider("HF Damping", &fVslider7, FAUSTFLOAT(6000.0f),
+                                        FAUSTFLOAT(1500.0f), FAUSTFLOAT(23520.0f),
+                                        FAUSTFLOAT(1.0f));
+        ui_interface->closeBox();
+        ui_interface->declare(0, "3", "");
+        ui_interface->openHorizontalBox("RM Peaking Equalizer 1");
+        ui_interface->declare(&fVslider4, "1", "");
+        ui_interface->declare(&fVslider4, "scale", "log");
+        ui_interface->declare(&fVslider4, "style", "knob");
+        ui_interface->declare(
+            &fVslider4, "tooltip",
+            "Center-frequency of second-order Regalia-Mitra peaking equalizer section 1");
+        ui_interface->declare(&fVslider4, "unit", "Hz");
+        ui_interface->addVerticalSlider("Eq1 Freq", &fVslider4, FAUSTFLOAT(315.0f),
+                                        FAUSTFLOAT(40.0f), FAUSTFLOAT(2500.0f),
+                                        FAUSTFLOAT(1.0f));
+        ui_interface->declare(&fVslider5, "2", "");
+        ui_interface->declare(&fVslider5, "style", "knob");
+        ui_interface->declare(&fVslider5, "tooltip",
+                              "Peak level   in dB of second-order Regalia-Mitra peaking "
+                              "equalizer section 1");
+        ui_interface->declare(&fVslider5, "unit", "dB");
+        ui_interface->addVerticalSlider("Eq1 Level", &fVslider5, FAUSTFLOAT(0.0f),
+                                        FAUSTFLOAT(-15.0f), FAUSTFLOAT(15.0f),
+                                        FAUSTFLOAT(0.100000001f));
+        ui_interface->closeBox();
+        ui_interface->declare(0, "4", "");
+        ui_interface->openHorizontalBox("RM Peaking Equalizer 2");
+        ui_interface->declare(&fVslider2, "1", "");
+        ui_interface->declare(&fVslider2, "scale", "log");
+        ui_interface->declare(&fVslider2, "style", "knob");
+        ui_interface->declare(
+            &fVslider2, "tooltip",
+            "Center-frequency of second-order Regalia-Mitra peaking equalizer section 2");
+        ui_interface->declare(&fVslider2, "unit", "Hz");
+        ui_interface->addVerticalSlider("Eq2 Freq", &fVslider2, FAUSTFLOAT(1500.0f),
+                                        FAUSTFLOAT(160.0f), FAUSTFLOAT(10000.0f),
+                                        FAUSTFLOAT(1.0f));
+        ui_interface->declare(&fVslider3, "2", "");
+        ui_interface->declare(&fVslider3, "style", "knob");
+        ui_interface->declare(&fVslider3, "tooltip",
+                              "Peak level   in dB of second-order Regalia-Mitra peaking "
+                              "equalizer section 2");
+        ui_interface->declare(&fVslider3, "unit", "dB");
+        ui_interface->addVerticalSlider("Eq2 Level", &fVslider3, FAUSTFLOAT(0.0f),
+                                        FAUSTFLOAT(-15.0f), FAUSTFLOAT(15.0f),
+                                        FAUSTFLOAT(0.100000001f));
+        ui_interface->closeBox();
+        ui_interface->declare(0, "5", "");
+        ui_interface->openHorizontalBox("Output");
+        ui_interface->declare(&fVslider1, "1", "");
+        ui_interface->declare(&fVslider1, "style", "knob");
+        ui_interface->declare(&fVslider1, "tooltip", "Dry/Wet Mix: 0 = dry, 1 = wet");
+        ui_interface->addVerticalSlider("Wet", &fVslider1, FAUSTFLOAT(0.0f),
+                                        FAUSTFLOAT(0.0f), FAUSTFLOAT(1.0f),
+                                        FAUSTFLOAT(0.00999999978f));
+        ui_interface->declare(&fVslider0, "2", "");
+        ui_interface->declare(&fVslider0, "style", "knob");
+        ui_interface->declare(&fVslider0, "tooltip", "Output scale   factor");
+        ui_interface->declare(&fVslider0, "unit", "dB");
+        ui_interface->addVerticalSlider("Level", &fVslider0, FAUSTFLOAT(-3.0f),
+                                        FAUSTFLOAT(-70.0f), FAUSTFLOAT(20.0f),
+                                        FAUSTFLOAT(0.100000001f));
+        ui_interface->closeBox();
+        ui_interface->closeBox();
+    }
+
+    virtual void compute(int count, FAUSTFLOAT** inputs, FAUSTFLOAT** outputs)
+    {
+        FAUSTFLOAT* input0  = inputs[0];
+        FAUSTFLOAT* output0 = outputs[0];
+        float fSlow0        = fConst1 * std::pow(10.0f, 0.0500000007f * float(fVslider0));
+        float fSlow1        = fConst1 * float(fVslider1);
+        float fSlow2        = float(fVslider2);
+        float fSlow3        = std::pow(10.0f, 0.0500000007f * float(fVslider3));
+        float fSlow4        = fConst3 * fSlow2 / std::sqrt(std::max<float>(0.0f, fSlow3));
+        float fSlow5        = (1.0f - fSlow4) / (fSlow4 + 1.0f);
+        float fSlow6        = float(fVslider4);
+        float fSlow7        = std::pow(10.0f, 0.0500000007f * float(fVslider5));
+        float fSlow8        = fConst3 * fSlow6 / std::sqrt(std::max<float>(0.0f, fSlow7));
+        float fSlow9        = (1.0f - fSlow8) / (fSlow8 + 1.0f);
+        float fSlow10       = float(fVslider6);
+        float fSlow11       = std::exp(fConst5 / fSlow10);
+        float fSlow12       = zitarevmonodsp_faustpower2_f(fSlow11);
+        float fSlow13       = std::cos(fConst3 * float(fVslider7));
+        float fSlow14       = 1.0f - fSlow12 * fSlow13;
+        float fSlow15       = 1.0f - fSlow12;
+        float fSlow16       = std::sqrt(std::max<float>(
+            0.0f,
+            zitarevmonodsp_faustpower2_f(fSlow14) / zitarevmonodsp_faustpower2_f(fSlow15)
+                + -1.0f));
+        float fSlow17       = fSlow14 / fSlow15;
+        float fSlow18       = fSlow11 * (fSlow16 + 1.0f - fSlow17);
+        float fSlow19       = float(fVslider8);
+        float fSlow20       = std::exp(fConst5 / fSlow19) / fSlow11 + -1.0f;
+        float fSlow21       = 1.0f / std::tan(fConst6 * float(fVslider9));
+        float fSlow22       = 1.0f / (fSlow21 + 1.0f);
+        float fSlow23       = 1.0f - fSlow21;
+        float fSlow24       = fSlow17 - fSlow16;
+        int iSlow25         = int(
+            std::min<float>(8192.0f, std::max<float>(0.0f, fConst9 * float(fVslider10))));
+        float fSlow26 = std::exp(fConst12 / fSlow10);
+        float fSlow27 = zitarevmonodsp_faustpower2_f(fSlow26);
+        float fSlow28 = 1.0f - fSlow27 * fSlow13;
+        float fSlow29 = 1.0f - fSlow27;
+        float fSlow30 = std::sqrt(std::max<float>(
+            0.0f,
+            zitarevmonodsp_faustpower2_f(fSlow28) / zitarevmonodsp_faustpower2_f(fSlow29)
+                + -1.0f));
+        float fSlow31 = fSlow28 / fSlow29;
+        float fSlow32 = fSlow26 * (fSlow30 + 1.0f - fSlow31);
+        float fSlow33 = std::exp(fConst12 / fSlow19) / fSlow26 + -1.0f;
+        float fSlow34 = fSlow31 - fSlow30;
+        float fSlow35 = std::exp(fConst17 / fSlow10);
+        float fSlow36 = zitarevmonodsp_faustpower2_f(fSlow35);
+        float fSlow37 = 1.0f - fSlow36 * fSlow13;
+        float fSlow38 = 1.0f - fSlow36;
+        float fSlow39 = std::sqrt(std::max<float>(
+            0.0f,
+            zitarevmonodsp_faustpower2_f(fSlow37) / zitarevmonodsp_faustpower2_f(fSlow38)
+                + -1.0f));
+        float fSlow40 = fSlow37 / fSlow38;
+        float fSlow41 = fSlow35 * (fSlow39 + 1.0f - fSlow40);
+        float fSlow42 = std::exp(fConst17 / fSlow19) / fSlow35 + -1.0f;
+        float fSlow43 = fSlow40 - fSlow39;
+        float fSlow44 = std::exp(fConst22 / fSlow10);
+        float fSlow45 = zitarevmonodsp_faustpower2_f(fSlow44);
+        float fSlow46 = 1.0f - fSlow45 * fSlow13;
+        float fSlow47 = 1.0f - fSlow45;
+        float fSlow48 = std::sqrt(std::max<float>(
+            0.0f,
+            zitarevmonodsp_faustpower2_f(fSlow46) / zitarevmonodsp_faustpower2_f(fSlow47)
+                + -1.0f));
+        float fSlow49 = fSlow46 / fSlow47;
+        float fSlow50 = fSlow44 * (fSlow48 + 1.0f - fSlow49);
+        float fSlow51 = std::exp(fConst22 / fSlow19) / fSlow44 + -1.0f;
+        float fSlow52 = fSlow49 - fSlow48;
+        float fSlow53 = std::exp(fConst27 / fSlow10);
+        float fSlow54 = zitarevmonodsp_faustpower2_f(fSlow53);
+        float fSlow55 = 1.0f - fSlow54 * fSlow13;
+        float fSlow56 = 1.0f - fSlow54;
+        float fSlow57 = std::sqrt(std::max<float>(
+            0.0f,
+            zitarevmonodsp_faustpower2_f(fSlow55) / zitarevmonodsp_faustpower2_f(fSlow56)
+                + -1.0f));
+        float fSlow58 = fSlow55 / fSlow56;
+        float fSlow59 = fSlow53 * (fSlow57 + 1.0f - fSlow58);
+        float fSlow60 = std::exp(fConst27 / fSlow19) / fSlow53 + -1.0f;
+        float fSlow61 = fSlow58 - fSlow57;
+        float fSlow62 = std::exp(fConst32 / fSlow10);
+        float fSlow63 = zitarevmonodsp_faustpower2_f(fSlow62);
+        float fSlow64 = 1.0f - fSlow63 * fSlow13;
+        float fSlow65 = 1.0f - fSlow63;
+        float fSlow66 = std::sqrt(std::max<float>(
+            0.0f,
+            zitarevmonodsp_faustpower2_f(fSlow64) / zitarevmonodsp_faustpower2_f(fSlow65)
+                + -1.0f));
+        float fSlow67 = fSlow64 / fSlow65;
+        float fSlow68 = fSlow62 * (fSlow66 + 1.0f - fSlow67);
+        float fSlow69 = std::exp(fConst32 / fSlow19) / fSlow62 + -1.0f;
+        float fSlow70 = fSlow67 - fSlow66;
+        float fSlow71 = std::exp(fConst37 / fSlow10);
+        float fSlow72 = zitarevmonodsp_faustpower2_f(fSlow71);
+        float fSlow73 = 1.0f - fSlow72 * fSlow13;
+        float fSlow74 = 1.0f - fSlow72;
+        float fSlow75 = std::sqrt(std::max<float>(
+            0.0f,
+            zitarevmonodsp_faustpower2_f(fSlow73) / zitarevmonodsp_faustpower2_f(fSlow74)
+                + -1.0f));
+        float fSlow76 = fSlow73 / fSlow74;
+        float fSlow77 = fSlow71 * (fSlow75 + 1.0f - fSlow76);
+        float fSlow78 = std::exp(fConst37 / fSlow19) / fSlow71 + -1.0f;
+        float fSlow79 = fSlow76 - fSlow75;
+        float fSlow80 = std::exp(fConst42 / fSlow10);
+        float fSlow81 = zitarevmonodsp_faustpower2_f(fSlow80);
+        float fSlow82 = 1.0f - fSlow81 * fSlow13;
+        float fSlow83 = 1.0f - fSlow81;
+        float fSlow84 = std::sqrt(std::max<float>(
+            0.0f,
+            zitarevmonodsp_faustpower2_f(fSlow82) / zitarevmonodsp_faustpower2_f(fSlow83)
+                + -1.0f));
+        float fSlow85 = fSlow82 / fSlow83;
+        float fSlow86 = fSlow80 * (fSlow84 + 1.0f - fSlow85);
+        float fSlow87 = std::exp(fConst42 / fSlow19) / fSlow80 + -1.0f;
+        float fSlow88 = fSlow85 - fSlow84;
+        float fSlow89 = 0.0f - std::cos(fConst3 * fSlow6) * (fSlow9 + 1.0f);
+        float fSlow90 = 0.0f - std::cos(fConst3 * fSlow2) * (fSlow5 + 1.0f);
+        for (int i0 = 0; i0 < count; i0 = i0 + 1) {
+            float fTemp0         = float(input0[i0]);
+            fVec0[IOTA0 & 16383] = fTemp0;
+            fRec0[0]             = fSlow0 + fConst2 * fRec0[1];
+            fRec1[0]             = fSlow1 + fConst2 * fRec1[1];
+            fRec15[0] = 0.0f - fSlow22 * (fSlow23 * fRec15[1] - (fRec11[1] + fRec11[2]));
+            fRec14[0] = fSlow18 * (fRec11[1] + fSlow20 * fRec15[0]) + fSlow24 * fRec14[1];
+            fVec1[IOTA0 & 32767] = 0.353553385f * fRec14[0] + 9.99999968e-21f;
+            float fTemp1         = 0.300000012f * fVec0[(IOTA0 - iSlow25) & 16383];
+            float fTemp2 =
+                (0.600000024f * fRec12[1] + fVec1[(IOTA0 - iConst8) & 32767]) - fTemp1;
+            fVec2[IOTA0 & 2047] = fTemp2;
+            fRec12[0]           = fVec2[(IOTA0 - iConst10) & 2047];
+            float fRec13        = 0.0f - 0.600000024f * fTemp2;
+            fRec19[0] = 0.0f - fSlow22 * (fSlow23 * fRec19[1] - (fRec7[1] + fRec7[2]));
+            fRec18[0] = fSlow32 * (fRec7[1] + fSlow33 * fRec19[0]) + fSlow34 * fRec18[1];
+            fVec3[IOTA0 & 32767] = 0.353553385f * fRec18[0] + 9.99999968e-21f;
+            float fTemp3 =
+                (0.600000024f * fRec16[1] + fVec3[(IOTA0 - iConst14) & 32767]) - fTemp1;
+            fVec4[IOTA0 & 4095] = fTemp3;
+            fRec16[0]           = fVec4[(IOTA0 - iConst15) & 4095];
+            float fRec17        = 0.0f - 0.600000024f * fTemp3;
+            fRec23[0] = 0.0f - fSlow22 * (fSlow23 * fRec23[1] - (fRec9[1] + fRec9[2]));
+            fRec22[0] = fSlow41 * (fRec9[1] + fSlow42 * fRec23[0]) + fSlow43 * fRec22[1];
+            fVec5[IOTA0 & 16383] = 0.353553385f * fRec22[0] + 9.99999968e-21f;
+            float fTemp4 =
+                fVec5[(IOTA0 - iConst19) & 16383] + fTemp1 + 0.600000024f * fRec20[1];
+            fVec6[IOTA0 & 4095] = fTemp4;
+            fRec20[0]           = fVec6[(IOTA0 - iConst20) & 4095];
+            float fRec21        = 0.0f - 0.600000024f * fTemp4;
+            fRec27[0] = 0.0f - fSlow22 * (fSlow23 * fRec27[1] - (fRec5[1] + fRec5[2]));
+            fRec26[0] = fSlow50 * (fRec5[1] + fSlow51 * fRec27[0]) + fSlow52 * fRec26[1];
+            fVec7[IOTA0 & 32767] = 0.353553385f * fRec26[0] + 9.99999968e-21f;
+            float fTemp5 =
+                fVec7[(IOTA0 - iConst24) & 32767] + fTemp1 + 0.600000024f * fRec24[1];
+            fVec8[IOTA0 & 4095] = fTemp5;
+            fRec24[0]           = fVec8[(IOTA0 - iConst25) & 4095];
+            float fRec25        = 0.0f - 0.600000024f * fTemp5;
+            fRec31[0] = 0.0f - fSlow22 * (fSlow23 * fRec31[1] - (fRec10[1] + fRec10[2]));
+            fRec30[0] = fSlow59 * (fRec10[1] + fSlow60 * fRec31[0]) + fSlow61 * fRec30[1];
+            fVec9[IOTA0 & 16383] = 0.353553385f * fRec30[0] + 9.99999968e-21f;
+            float fTemp6 =
+                fVec9[(IOTA0 - iConst29) & 16383] - (fTemp1 + 0.600000024f * fRec28[1]);
+            fVec10[IOTA0 & 2047] = fTemp6;
+            fRec28[0]            = fVec10[(IOTA0 - iConst30) & 2047];
+            float fRec29         = 0.600000024f * fTemp6;
+            fRec35[0] = 0.0f - fSlow22 * (fSlow23 * fRec35[1] - (fRec6[1] + fRec6[2]));
+            fRec34[0] = fSlow68 * (fRec6[1] + fSlow69 * fRec35[0]) + fSlow70 * fRec34[1];
+            fVec11[IOTA0 & 16383] = 0.353553385f * fRec34[0] + 9.99999968e-21f;
+            float fTemp7 =
+                fVec11[(IOTA0 - iConst34) & 16383] - (fTemp1 + 0.600000024f * fRec32[1]);
+            fVec12[IOTA0 & 4095] = fTemp7;
+            fRec32[0]            = fVec12[(IOTA0 - iConst35) & 4095];
+            float fRec33         = 0.600000024f * fTemp7;
+            fRec39[0] = 0.0f - fSlow22 * (fSlow23 * fRec39[1] - (fRec8[1] + fRec8[2]));
+            fRec38[0] = fSlow77 * (fRec8[1] + fSlow78 * fRec39[0]) + fSlow79 * fRec38[1];
+            fVec13[IOTA0 & 16383] = 0.353553385f * fRec38[0] + 9.99999968e-21f;
+            float fTemp8 =
+                (fTemp1 + fVec13[(IOTA0 - iConst39) & 16383]) - 0.600000024f * fRec36[1];
+            fVec14[IOTA0 & 4095] = fTemp8;
+            fRec36[0]            = fVec14[(IOTA0 - iConst40) & 4095];
+            float fRec37         = 0.600000024f * fTemp8;
+            fRec43[0] = 0.0f - fSlow22 * (fSlow23 * fRec43[1] - (fRec4[1] + fRec4[2]));
+            fRec42[0] = fSlow86 * (fRec4[1] + fSlow87 * fRec43[0]) + fSlow88 * fRec42[1];
+            fVec15[IOTA0 & 16383] = 0.353553385f * fRec42[0] + 9.99999968e-21f;
+            float fTemp9 =
+                (fVec15[(IOTA0 - iConst44) & 16383] + fTemp1) - 0.600000024f * fRec40[1];
+            fVec16[IOTA0 & 2047] = fTemp9;
+            fRec40[0]            = fVec16[(IOTA0 - iConst45) & 2047];
+            float fRec41         = 0.600000024f * fTemp9;
+            float fTemp10        = fRec41 + fRec37;
+            float fTemp11        = fRec29 + fRec33 + fTemp10;
+            fRec4[0] = fRec12[1] + fRec16[1] + fRec20[1] + fRec24[1] + fRec28[1]
+                       + fRec32[1] + fRec36[1] + fRec40[1] + fRec13 + fRec17 + fRec21
+                       + fRec25 + fTemp11;
+            fRec5[0] = (fRec28[1] + fRec32[1] + fRec36[1] + fRec40[1] + fTemp11)
+                       - (fRec12[1] + fRec16[1] + fRec20[1] + fRec24[1] + fRec13 + fRec17
+                          + fRec25 + fRec21);
+            float fTemp12 = fRec33 + fRec29;
+            fRec6[0] = (fRec20[1] + fRec24[1] + fRec36[1] + fRec40[1] + fRec21 + fRec25
+                        + fTemp10)
+                       - (fRec12[1] + fRec16[1] + fRec28[1] + fRec32[1] + fRec13 + fRec17
+                          + fTemp12);
+            fRec7[0] = (fRec12[1] + fRec16[1] + fRec36[1] + fRec40[1] + fRec13 + fRec17
+                        + fTemp10)
+                       - (fRec20[1] + fRec24[1] + fRec28[1] + fRec32[1] + fRec21 + fRec25
+                          + fTemp12);
+            float fTemp13 = fRec41 + fRec33;
+            float fTemp14 = fRec37 + fRec29;
+            fRec8[0] = (fRec16[1] + fRec24[1] + fRec32[1] + fRec40[1] + fRec17 + fRec25
+                        + fTemp13)
+                       - (fRec12[1] + fRec20[1] + fRec28[1] + fRec36[1] + fRec13 + fRec21
+                          + fTemp14);
+            fRec9[0] = (fRec12[1] + fRec20[1] + fRec32[1] + fRec40[1] + fRec13 + fRec21
+                        + fTemp13)
+                       - (fRec16[1] + fRec24[1] + fRec28[1] + fRec36[1] + fRec17 + fRec25
+                          + fTemp14);
+            float fTemp15 = fRec41 + fRec29;
+            float fTemp16 = fRec37 + fRec33;
+            fRec10[0] = (fRec12[1] + fRec24[1] + fRec28[1] + fRec40[1] + fRec13 + fRec25
+                         + fTemp15)
+                        - (fRec16[1] + fRec20[1] + fRec32[1] + fRec36[1] + fRec17 + fRec21
+                           + fTemp16);
+            fRec11[0] = (fRec16[1] + fRec20[1] + fRec28[1] + fRec40[1] + fRec17 + fRec21
+                         + fTemp15)
+                        - (fRec12[1] + fRec24[1] + fRec32[1] + fRec36[1] + fRec13 + fRec25
+                           + fTemp16);
+            float fTemp17 = 0.370000005f * (fRec5[0] + fRec6[0]);
+            float fTemp18 = fSlow89 * fRec3[1];
+            fRec3[0]      = fTemp17 - (fTemp18 + fSlow9 * fRec3[2]);
+            float fTemp19 = fSlow9 * fRec3[0];
+            float fTemp20 = 0.5f
+                            * (fTemp19 + fRec3[2] + fTemp17 + fTemp18
+                               + fSlow7 * ((fTemp19 + fTemp18 + fRec3[2]) - fTemp17));
+            float fTemp21 = fSlow90 * fRec2[1];
+            fRec2[0]      = fTemp20 - (fTemp21 + fSlow5 * fRec2[2]);
+            float fTemp22 = fSlow5 * fRec2[0];
+            float fTemp23 = fTemp0 * (1.0f - fRec1[0]);
+            float fTemp24 = 0.370000005f * (fRec5[0] - fRec6[0]);
+            float fTemp25 = fSlow89 * fRec45[1];
+            fRec45[0]     = fTemp24 - (fTemp25 + fSlow9 * fRec45[2]);
+            float fTemp26 = fSlow9 * fRec45[0];
+            float fTemp27 = 0.5f
+                            * (fTemp26 + fRec45[2] + fTemp24 + fTemp25
+                               + fSlow7 * ((fTemp26 + fTemp25 + fRec45[2]) - fTemp24));
+            float fTemp28 = fSlow90 * fRec44[1];
+            fRec44[0]     = fTemp27 - (fTemp28 + fSlow5 * fRec44[2]);
+            float fTemp29 = fSlow5 * fRec44[0];
+            output0[i0]   = FAUSTFLOAT(
+                fRec0[0]
+                * (0.5f * fRec1[0]
+                       * (fTemp22 + fRec2[2] + fTemp20 + fTemp21
+                          + fSlow3 * ((fTemp22 + fTemp21 + fRec2[2]) - fTemp20))
+                   + fTemp23 + fTemp23
+                   + 0.5f * fRec1[0]
+                         * (fTemp29 + fRec44[2] + fTemp27 + fTemp28
+                            + fSlow3 * ((fTemp29 + fTemp28 + fRec44[2]) - fTemp27))));
+            IOTA0     = IOTA0 + 1;
+            fRec0[1]  = fRec0[0];
+            fRec1[1]  = fRec1[0];
+            fRec15[1] = fRec15[0];
+            fRec14[1] = fRec14[0];
+            fRec12[1] = fRec12[0];
+            fRec19[1] = fRec19[0];
+            fRec18[1] = fRec18[0];
+            fRec16[1] = fRec16[0];
+            fRec23[1] = fRec23[0];
+            fRec22[1] = fRec22[0];
+            fRec20[1] = fRec20[0];
+            fRec27[1] = fRec27[0];
+            fRec26[1] = fRec26[0];
+            fRec24[1] = fRec24[0];
+            fRec31[1] = fRec31[0];
+            fRec30[1] = fRec30[0];
+            fRec28[1] = fRec28[0];
+            fRec35[1] = fRec35[0];
+            fRec34[1] = fRec34[0];
+            fRec32[1] = fRec32[0];
+            fRec39[1] = fRec39[0];
+            fRec38[1] = fRec38[0];
+            fRec36[1] = fRec36[0];
+            fRec43[1] = fRec43[0];
+            fRec42[1] = fRec42[0];
+            fRec40[1] = fRec40[0];
+            fRec4[2]  = fRec4[1];
+            fRec4[1]  = fRec4[0];
+            fRec5[2]  = fRec5[1];
+            fRec5[1]  = fRec5[0];
+            fRec6[2]  = fRec6[1];
+            fRec6[1]  = fRec6[0];
+            fRec7[2]  = fRec7[1];
+            fRec7[1]  = fRec7[0];
+            fRec8[2]  = fRec8[1];
+            fRec8[1]  = fRec8[0];
+            fRec9[2]  = fRec9[1];
+            fRec9[1]  = fRec9[0];
+            fRec10[2] = fRec10[1];
+            fRec10[1] = fRec10[0];
+            fRec11[2] = fRec11[1];
+            fRec11[1] = fRec11[0];
+            fRec3[2]  = fRec3[1];
+            fRec3[1]  = fRec3[0];
+            fRec2[2]  = fRec2[1];
+            fRec2[1]  = fRec2[0];
+            fRec45[2] = fRec45[1];
+            fRec45[1] = fRec45[0];
+            fRec44[2] = fRec44[1];
+            fRec44[1] = fRec44[0];
+        }
+    }
+};
+
+#endif
diff --git a/subprojects/packagefiles/rtaudio-coreaudio-stream-channels.patch b/subprojects/packagefiles/rtaudio-coreaudio-stream-channels.patch
new file mode 100644 (file)
index 0000000..fe7c1fa
--- /dev/null
@@ -0,0 +1,11 @@
+--- a/RtAudio.cpp      2024-02-11 15:05:31
++++ b/RtAudio.cpp      2024-02-11 15:07:49
+@@ -1782,7 +1782,7 @@
+   stream_.deviceFormat[mode] = RTAUDIO_FLOAT32;
+   if ( streamCount == 1 )
+-    stream_.nDeviceChannels[mode] = description.mChannelsPerFrame;
++    stream_.nDeviceChannels[mode] = streamChannels;
+   else // multiple streams
+     stream_.nDeviceChannels[mode] = channels;
+   stream_.nUserChannels[mode] = channels;
diff --git a/subprojects/packagefiles/rtaudio-remove-input-disconnect-listener.patch b/subprojects/packagefiles/rtaudio-remove-input-disconnect-listener.patch
new file mode 100644 (file)
index 0000000..8809fd4
--- /dev/null
@@ -0,0 +1,11 @@
+--- a/RtAudio.cpp      2024-01-11 13:04:29.148565300 -0800
++++ b/RtAudio.cpp      2024-01-11 13:04:42.305228600 -0800
+@@ -1981,7 +1981,7 @@
+         }
+       }
+-      if ( handle->disconnectListenerAdded[0] ) {
++      if ( handle->disconnectListenerAdded[1] ) {
+         property.mSelector = kAudioDevicePropertyDeviceIsAlive;
+         if (AudioObjectRemovePropertyListener( handle->id[1], &property, streamDisconnectListener, (void *) &stream_.callbackInfo ) != noErr) {
+           errorText_ = "RtApiCore::closeStream(): error removing disconnect property listener!";
diff --git a/subprojects/rtaudio.wrap b/subprojects/rtaudio.wrap
new file mode 100644 (file)
index 0000000..283fa85
--- /dev/null
@@ -0,0 +1,9 @@
+[wrap-file]
+directory = rtaudio-6.0.1
+source_url = https://github.com/thestk/rtaudio/archive/refs/tags/6.0.1.tar.gz
+source_filename = 6.0.1.tar.gz
+source_hash = 7206c8b6cee43b474f43d64988fefaadfdcfc4264ed38d8de5f5d0e6ddb0a123
+diff_files = rtaudio-remove-input-disconnect-listener.patch,rtaudio-coreaudio-stream-channels.patch
+
+[provide]
+dependency_names = rtaudio
diff --git a/subprojects/wingetopt.wrap b/subprojects/wingetopt.wrap
new file mode 100644 (file)
index 0000000..dcc94c1
--- /dev/null
@@ -0,0 +1,6 @@
+[wrap-git]
+url=https://github.com/alex85k/wingetopt.git
+revision=head
+
+[provide]
+dependency_names = wingetopt
diff --git a/tests/jacktrip_tests.cpp b/tests/jacktrip_tests.cpp
new file mode 100644 (file)
index 0000000..d0eb2f4
--- /dev/null
@@ -0,0 +1,94 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2008 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 jacktrip_tests.cpp
+ * \author Juan-Pablo Caceres
+ * \date September 2008
+ */
+
+#include <QVector>
+#include <iostream>
+
+#include "JackTripThread.h"
+
+using std::cout;
+using std::endl;
+
+const int num_jacktrips = 5;
+const int base_port     = 4464;
+
+void main_tests(int argc, char** argv);
+void test_threads_server();
+void test_threads_client(const char* peer_address);
+
+void main_tests(int /*argc*/, char** argv)
+{
+    if (argv[1][0] == 's') {
+        test_threads_server();
+    } else if (argv[1][0] == 'c') {
+        test_threads_client("171.64.197.209");
+    }
+}
+
+// Test many servers running at the same time
+void test_threads_server()
+{
+    QVector<JackTripThread*> jacktrips;
+    jacktrips.resize(num_jacktrips);
+    int port_num;
+    for (int i = 0; i < num_jacktrips; i++) {
+        port_num = base_port + i * 10;
+        cout << "Port Number: " << port_num << endl;
+        jacktrips[i] = new JackTripThread(JackTrip::SERVER);
+        jacktrips[i]->setPort(port_num);
+        jacktrips[i]->start(QThread::NormalPriority);
+        // sleep(1);
+    }
+}
+
+// Test many servers running at the same time
+void test_threads_client(const char* peer_address)
+{
+    QVector<JackTripThread*> jacktrips;
+    jacktrips.resize(num_jacktrips);
+    int port_num;
+    for (int i = 0; i < num_jacktrips; i++) {
+        port_num = base_port + i * 10;
+        cout << "Port Number: " << port_num << endl;
+        jacktrips[i] = new JackTripThread(JackTrip::CLIENT);
+        jacktrips[i]->setPort(port_num);
+        jacktrips[i]->setPeerAddress(peer_address);
+        // sleep(1);
+        jacktrips[i]->start(QThread::NormalPriority);
+        // sleep(1);
+    }
+}
diff --git a/win/CodeSignTool/CodeSignTool.sh b/win/CodeSignTool/CodeSignTool.sh
new file mode 100755 (executable)
index 0000000..10c4bc9
--- /dev/null
@@ -0,0 +1 @@
+java -cp "./jar/picocli-4.6.1.jar:./jar/bcprov-jdk15on-1.65.01.jar:./jar/httpclient-4.5.13.jar:./jar/json-simple-1.1.1.jar:./jar/jsign-core-3.1.jar:./jar/commons-io-2.8.0.jar:./jar/bcpkix-jdk15on-1.65.jar:./jar/code_sign_tool-1.2.2.jar:./jar/httpcore-4.4.13.jar:./jar/commons-logging-1.2.jar:./jar/log4j-api-2.17.1.jar:./jar/log4j-core-2.17.1.jar:./jar/poi-4.1.2.jar:./jar/commons-lang3-3.9.jar:./jar/commons-math3-3.6.1.jar:./jar/totp-1.0.jar:./jar/commons-codec-1.15.jar" com.ssl.code.signing.tool.CodeSignTool $@
diff --git a/win/CodeSignTool/conf/code_sign_tool.properties b/win/CodeSignTool/conf/code_sign_tool.properties
new file mode 100644 (file)
index 0000000..5622896
--- /dev/null
@@ -0,0 +1,4 @@
+CLIENT_ID=kaXTRACNijSWsFdRKg_KAfD3fqrBlzMbWs6TwWHwAn8\r
+OAUTH2_ENDPOINT=https://login.ssl.com/oauth2/token\r
+CSC_API_ENDPOINT=https://cs.ssl.com\r
+TSA_URL=http://ts.ssl.com
\ No newline at end of file
diff --git a/win/CodeSignTool/conf/log4j2.xml b/win/CodeSignTool/conf/log4j2.xml
new file mode 100644 (file)
index 0000000..e3ee419
--- /dev/null
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>\r
+<Configuration status="WARN">\r
+    <Properties>\r
+        <Property name="LOG_PATTERN">\r
+            %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n\r
+        </Property>\r
+    </Properties>\r
+    <Appenders>\r
+        <RollingFile name="FileAppenderApp" fileName="./logs/code_signing_tool.log"\r
+                     filePattern="./logs/code_signing_tool-%d{yyyy-MM-dd}.log">\r
+            <PatternLayout>\r
+                <Pattern>${LOG_PATTERN}</Pattern>\r
+            </PatternLayout>\r
+            <Policies>\r
+                <TimeBasedTriggeringPolicy/>\r
+            </Policies>\r
+            <DefaultRolloverStrategy fileIndex="nomax"/>\r
+        </RollingFile>\r
+    </Appenders>\r
+    <Loggers>\r
+        <Root level="info">\r
+            <AppenderRef ref="FileAppenderApp" />\r
+        </Root>\r
+        <!--<Logger name="com.ssl.code.signing.tool" level="info" additivity="false">\r
+            <AppenderRef ref="FileAppenderApp" />\r
+        </Logger>-->\r
+    </Loggers>\r
+</Configuration>
\ No newline at end of file
diff --git a/win/build_installer.bat b/win/build_installer.bat
new file mode 100755 (executable)
index 0000000..52c8229
--- /dev/null
@@ -0,0 +1,79 @@
+@echo off\r
+setlocal EnableDelayedExpansion\r
+\r
+if not defined WIXPATH (\r
+       for /f "delims=" %%a in ('dir /b "C:\Program Files (x86)\Wix Toolset*"') do set WIXPATH=%%a\r
+       set WIXPATH=C:\Program Files ^(x86^)\!WIXPATH!\bin\r
+)\r
+\r
+set PATH=%PATH%;%WIXPATH%\r
+del deploy /s /q\r
+rmdir deploy /s /q\r
+mkdir deploy\r
+\r
+REM get opengl32sw.dll mesa3d llvm build, required by opengl software backend\r
+curl -L -s -o opengl32sw.zip https://files.jacktrip.org/contrib/opengl32sw.zip\r
+unzip opengl32sw.zip\r
+del opengl32sw.zip\r
+move opengl32sw.dll deploy\r
+\r
+copy ..\LICENSE.md deploy\\r
+xcopy ..\LICENSES deploy\LICENSES\\r
+\r
+REM create RTF file with licenses' text\r
+set LICENSEPATH=deploy\license.rtf\r
+echo {\rtf1\ansi\deff0 {\fonttbl {\f0 Calibri;}} \f0\fs22>%LICENSEPATH%\r
+for %%f in (..\LICENSE.md ..\LICENSES\MIT.txt ..\LICENSES\GPL-3.0.txt ..\LICENSES\LGPL-3.0-only.txt ..\LICENSES\AVC.txt) do (\r
+  for /f "delims=" %%x in ('type %%f') do (\r
+    echo %%x\line>>%LICENSEPATH%\r
+  )\r
+  echo \par >>%LICENSEPATH%\r
+)\r
+echo }>>%LICENSEPATH%\r
+\r
+if "%~1"=="/q" (\r
+    copy dialog_alt.bmp deploy\dialog.bmp\r
+) else (\r
+    copy dialog.bmp deploy\\r
+)\r
+if exist ..\builddir\release\jacktrip.exe (set JACKTRIP=..\builddir\release\jacktrip.exe) else (set JACKTRIP=..\builddir\jacktrip.exe)\r
+copy %JACKTRIP% deploy\\r
+cd deploy\r
+\r
+set "WIXDEFINES="\r
+for /f "tokens=*" %%a in ('objdump -p jacktrip.exe ^| findstr Qt5Core.dll') do (\r
+       set DYNAMIC_QT=%%a\r
+       set QTVERSION=5\r
+)\r
+if not defined DYNAMIC_QT (\r
+       for /f "tokens=*" %%a in ('objdump -p jacktrip.exe ^| findstr Qt6Core.dll') do (\r
+               set DYNAMIC_QT=%%a\r
+               set QTVERSION=6\r
+       )\r
+)\r
+if defined DYNAMIC_QT (\r
+       echo Including Qt%QTVERSION% Files\r
+       for /f "tokens=*" %%a in ('objdump -p jacktrip.exe ^| findstr Qt%QTVERSION%Qml.dll') do set VS=%%a\r
+       if defined VS (\r
+               windeployqt -release --qmldir ..\..\src\gui jacktrip.exe\r
+               set WIXDEFINES=%WIXDEFINES% -dvs\r
+       ) else (\r
+               windeployqt -release jacktrip.exe\r
+       )\r
+       set WIXDEFINES=!WIXDEFINES! -ddynamic -dqt%QTVERSION%\r
+)\r
+\r
+copy ..\jacktrip.wxs .\\r
+copy ..\qt%QTVERSION%.wxs .\\r
+.\jacktrip --test-gui\r
+if %ERRORLEVEL% NEQ 0 (\r
+       echo You need to build jacktrip with gui support to build the installer.\r
+       exit /b 1\r
+)\r
+rem Get our version number\r
+for /f "tokens=*" %%a in ('.\jacktrip -v ^| findstr VERSION') do for %%b in (%%~a) do set VERSION=%%b\r
+for /f "tokens=1 delims=-" %%a in ("%VERSION%") do set VERSION=%%a\r
+echo Version=%VERSION%\r
+candle.exe -arch x64 -ext WixUIExtension -ext WixUtilExtension -dVersion=%VERSION%%WIXDEFINES% ..\jacktrip.wxs ..\qt%QTVERSION%.wxs\r
+light.exe -ext WixUIExtension -ext WixUtilExtension -o JackTrip.msi jacktrip.wixobj qt%QTVERSION%.wixobj\r
+endlocal\r
diff --git a/win/dialog.bmp b/win/dialog.bmp
new file mode 100644 (file)
index 0000000..f751877
Binary files /dev/null and b/win/dialog.bmp differ
diff --git a/win/dialog_alt.bmp b/win/dialog_alt.bmp
new file mode 100644 (file)
index 0000000..5d6e642
Binary files /dev/null and b/win/dialog_alt.bmp differ
diff --git a/win/getCsv.sh b/win/getCsv.sh
new file mode 100755 (executable)
index 0000000..f19f830
--- /dev/null
@@ -0,0 +1,2 @@
+#!/bin/sh
+cat files.wxs | awk '/<Component Id/{ printf "%s ", $2; getline; print $4; }' | sed -E 's/Id="(.*)" Source="SourceDir\\(.*)"/\1,\2/'
diff --git a/win/jack.pc b/win/jack.pc
new file mode 100644 (file)
index 0000000..3d2529d
--- /dev/null
@@ -0,0 +1,11 @@
+prefix=/c/Program\ Files/JACK2
+exec_prefix=/c/Program\ Files/JACK2
+libdir=/c/Program\ Files/JACK2/lib
+includedir=/c/Program\ Files/JACK2/include
+server_libs=-L/c/Program\ Files/JACK2/lib -ljackserver64
+
+Name: jack
+Description: the Jack Audio Connection Kit: a low-latency synchronous callback-based media server
+Version: 1.9.19
+Libs: -L/c/Program\ Files/JACK2/lib -ljack64
+Cflags: -I/c/Program\ Files/JACK2/include
diff --git a/win/jacktrip.exe.manifest b/win/jacktrip.exe.manifest
new file mode 100644 (file)
index 0000000..3afa605
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+    <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
+        <application>
+            <!-- Windows 10, Windows 11, Windows Server 2016, Windows Server 2019 and Windows Server 2022 -->
+            <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
+        </application>
+    </compatibility>
+</assembly>
diff --git a/win/jacktrip.ico b/win/jacktrip.ico
new file mode 100644 (file)
index 0000000..6774f3b
Binary files /dev/null and b/win/jacktrip.ico differ
diff --git a/win/jacktrip.wxs b/win/jacktrip.wxs
new file mode 100644 (file)
index 0000000..565f1ac
--- /dev/null
@@ -0,0 +1,54 @@
+<?xml version='1.0' encoding='windows-1252'?>\r
+<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>\r
+    <Product Name='JackTrip' Manufacturer='JackTrip'\r
+        Id='*'\r
+        UpgradeCode='70450e42-8a91-4c28-8f00-52c6b8e86e37'\r
+        Language='1033' Codepage='1252' Version='$(var.Version)'>\r
+\r
+        <Package Id='*' Keywords='Installer' Description='JackTrip Installer'\r
+            Comments='Developed by SoundWIRE Group at CCRMA, Stanford' Manufacturer='JackTrip'\r
+            InstallerVersion='200' Languages='1033' Compressed='yes' SummaryCodepage='1252' />\r
+        <Media Id='1' Cabinet='JackTrip.cab' EmbedCab='yes' DiskPrompt='Disk 1' />\r
+        <Property Id='DiskPrompt' Value='JackTrip Installer File' />\r
+        <MajorUpgrade AllowDowngrades="no" DowngradeErrorMessage="A newer version is already installed."\r
+            AllowSameVersionUpgrades="no" />\r
+\r
+        <Directory Id='TARGETDIR' Name='SourceDir'>\r
+            <Directory Id='ProgramFiles64Folder' Name='PFiles'>\r
+                <Directory Id='INSTALLDIR' Name='JackTrip'>\r
+                </Directory>\r
+            </Directory>\r
+            <Directory Id='ProgramMenuFolder' Name='Programs'>\r
+                <Directory Id='ProgramMenuDir' Name='JackTrip'>\r
+                    <Component Id="ProgramMenuDir" Guid="2f25e96a-47a5-4eca-b367-a78ce536d9b9">\r
+                        <RemoveFolder Id='ProgramMenuDir' On='uninstall' />\r
+                        <RegistryValue Root='HKCU' Key='Software\[Manufacturer]\[ProductName]' Type='string' Value='' KeyPath='yes' />\r
+                    </Component>\r
+                </Directory>\r
+            </Directory>\r
+        </Directory>\r
+\r
+        <Feature Id='NormalInstall' Title='JackTrip' Description='Standard Install'\r
+            Level='1' ConfigurableDirectory='INSTALLDIR'>\r
+            <ComponentGroupRef Id='jacktrip' />\r
+            <ComponentRef Id='ProgramMenuDir' />\r
+        </Feature>\r
+\r
+        <Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch JackTrip" />\r
+        <Property Id="WixShellExecTarget" Value="[INSTALLDIR]\jacktrip.exe" />\r
+        <CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />\r
+\r
+        <WixVariable Id='WixUIDialogBmp' Value='dialog.bmp' />\r
+        <WixVariable Id='WixUILicenseRtf' Value='license.rtf' />\r
+        <Property Id='WIXUI_INSTALLDIR' Value='INSTALLDIR' />\r
+        <UI>\r
+            <UIRef Id='WixUI_InstallDir' />\r
+            <Publish Dialog="ExitDialog"\r
+                Control="Finish" \r
+                Event="DoAction" \r
+                Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>\r
+        </UI>\r
+        <Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>\r
+        <Icon Id='jacktrip.exe' SourceFile='jacktrip.exe' />\r
+    </Product>\r
+</Wix>\r
diff --git a/win/jacktrip_alt.ico b/win/jacktrip_alt.ico
new file mode 100644 (file)
index 0000000..1fdf3e0
Binary files /dev/null and b/win/jacktrip_alt.ico differ
diff --git a/win/meson.build b/win/meson.build
new file mode 100644 (file)
index 0000000..7362bba
--- /dev/null
@@ -0,0 +1,36 @@
+if host_machine.system() == 'windows'
+       windows = import('windows')
+
+       src += windows.compile_resources('qjacktrip.rc',
+                                       depend_files: 'jacktrip.ico',
+                                       include_directories: '.')
+
+       defines += '-D_WIN32_WINNT=0x0A00'
+       defines += '-DWINVER=0x0A00'
+       defines += '-DWIN32_LEAN_AND_MEAN'
+       defines += '-DNOMINMAX'
+
+       deps += compiler.find_library('ws2_32', required: true)
+
+       if compiler.get_id() == 'msvc'
+               opt_var = cmake.subproject_options()
+               if get_option('buildtype') == 'release'
+                       opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Release'})
+               else
+                       opt_var.add_cmake_defines({'CMAKE_BUILD_TYPE': 'Debug'})
+               endif
+               wingetopt = cmake.subproject('wingetopt', options: opt_var)
+               deps += wingetopt.dependency('wingetopt')
+               link_args += 'userenv.lib'
+               link_args += 'Synchronization.lib'
+               link_args += 'Netapi32.lib'
+               link_args += 'Version.lib'
+               link_args += 'Dwrite.lib'
+               link_args += 'Iphlpapi.lib'
+               link_args += 'Secur32.lib'
+               link_args += 'Winhttp.lib'
+               link_args += 'Dnsapi.lib'
+               link_args += 'Iphlpapi.lib'
+       endif
+
+endif
diff --git a/win/qjacktrip.rc b/win/qjacktrip.rc
new file mode 100644 (file)
index 0000000..8d7ab5a
--- /dev/null
@@ -0,0 +1,6 @@
+IDI_ICON1 ICON "jacktrip.ico"
+
+#ifndef RT_MANIFEST
+#define RT_MANIFEST 24
+#endif
+1 RT_MANIFEST "jacktrip.exe.manifest"
\ No newline at end of file
diff --git a/win/qt5.wxs b/win/qt5.wxs
new file mode 100644 (file)
index 0000000..0334793
--- /dev/null
@@ -0,0 +1,1845 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">\r
+    <Fragment>\r
+        <DirectoryRef Id="INSTALLDIR">\r
+            <Component Id="cmpE6579DC78E167F46AB23E0BE3EAA58CE" Guid="27BFCC7D-F4A8-4F71-8A2B-00A8BC74B106">\r
+                <File Id="fil5E6E1243EAE085FE802B6D64690357B0" KeyPath="yes" Source="SourceDir\jacktrip.exe" />\r
+                <Shortcut Id="startmenuJackTrip" Directory="ProgramMenuDir" Name="JackTrip"\r
+                    WorkingDirectory="INSTALLDIR" Icon="jacktrip.exe" IconIndex="0" Advertise="yes" />\r
+            </Component>\r
+            <Component Id="cmp52FE9CFF214054652C281C37201305EC" Guid="{158D5247-525B-417F-B12B-48F13CD7E414}">\r
+                <File Id="fil8AF433A253A49035CB32EA6F957BF7D4" KeyPath="yes" Source="SourceDir\LICENSE.md" />\r
+            </Component>\r
+            <Directory Id="LICENSESDIR" Name="LICENSES">\r
+                <Component Id="cmpA07F2DC814BECD1E5F3FC36DC47718E5" Guid="{36E317A7-BA26-4E2B-99CD-0728CD8779DD}">\r
+                    <File Id="fil39657C9CD79E70F8B3033F0CF1D035A1" KeyPath="yes" Source="SourceDir\LICENSES\LGPL-3.0-only.txt" />\r
+                </Component>\r
+                <Component Id="cmpBF7FA1E27DF091A3CCFA1CD7B7CD94BA" Guid="{2D555037-9C09-4042-8F0C-C6EBAB80681A}">\r
+                    <File Id="fil496C76DCE2246C589E09AC68BCFDFB32" KeyPath="yes" Source="SourceDir\LICENSES\MIT.txt" />\r
+                </Component>\r
+                <Component Id="cmpF915A8D749EFFA98CD8DD582A7C82614" Guid="{A3C72CE1-0570-497B-A84F-F21C53771ABC}">\r
+                    <File Id="filCCF13ED800A4C7B438EC52A50C3A6755" KeyPath="yes" Source="SourceDir\LICENSES\GPL-3.0.txt" />\r
+                </Component>\r
+              </Directory>\r
+<?ifdef rtaudio?>\r
+            <Component Id="cmpD683260746BC875AE3BDED667AA8F954" Guid="20A867E8-36B6-49E3-A1DA-76CFCE967908">\r
+                <File Id="filA7BCB2CDD7D00C9DFB32AEB94F294425" KeyPath="yes" Source="SourceDir\librtaudio.dll" />\r
+            </Component>\r
+<?endif?>\r
+<?ifdef dynamic?>\r
+            <Component Id="cmpF83098612A5B11C5398DBA3894ED590A" Guid="6A0A67A9-3318-4CFB-BC07-5AF3FF7B7E87">\r
+                <File Id="fil526FA994C1850220CFA7BD7FCD995C32" KeyPath="yes" Source="SourceDir\D3Dcompiler_47.dll" />\r
+            </Component>\r
+<?ifdef qt5?>\r
+            <Component Id="cmp5DDCF0CE56D406D1B8B1F93E988AED6B" Guid="4346C308-2912-4CAF-B01B-1FB85781DAD0">\r
+                <File Id="fil0F4CD3729AF07C1C460E292E0F081D0A" KeyPath="yes" Source="SourceDir\libEGL.dll" />\r
+            </Component>\r
+            <Component Id="cmpB0CFEB9FF932521D6A75B6BE6F8D644F" Guid="73A8C873-FBDB-4BC5-BB74-2F82619DC3EC">\r
+                <File Id="fil736048FE57EC840EA5DCE88625C38E8D" KeyPath="yes" Source="SourceDir\libGLESv2.dll" />\r
+            </Component>\r
+<?endif?>\r
+<?ifdef mingw?>\r
+            <Component Id="cmpB60B52D038613C3F20F84A0E1C83D4D4" Guid="62E3CBE5-9787-4A5C-B6CD-B867781773EE">\r
+                <File Id="fil09D765981E2E493119D2DBD2DA179B9D" KeyPath="yes" Source="SourceDir\libcrypto-1_1-x64.dll" />\r
+            </Component>\r
+            <Component Id="cmpDEFDDDBEBEA7255D07EE4400F2CDA2FF" Guid="904EB309-B381-443D-A064-4C2669E8E66E">\r
+                <File Id="fil2ECEB8B8BE6121A2C5659BA0F64D92FC" KeyPath="yes" Source="SourceDir\libgcc_s_seh-1.dll" />\r
+            </Component>\r
+            <Component Id="cmp809509D62BD9EA848EF243F03D83758F" Guid="FE4AFCE9-7DE1-4A82-A96E-42C8B93E04C7">\r
+                <File Id="filCE00A8B273087E758109FEC2AC699195" KeyPath="yes" Source="SourceDir\libssl-1_1-x64.dll" />\r
+            </Component>\r
+            <Component Id="cmp57DDA83E372188A5B760684B961DE685" Guid="96741680-04B3-4A4A-8D55-08C48D6B2201">\r
+                <File Id="filF57E66845C852C7C1EFFE6996027BE3C" KeyPath="yes" Source="SourceDir\libstdc++-6.dll" />\r
+            </Component>\r
+            <Component Id="cmpF76D0CD1138260DB167395B86D0011CF" Guid="88B4D808-2CD2-40E0-9BC4-51093AFD3D61">\r
+                <File Id="fil8C56E6114C3ACED0DF58456A08DC0437" KeyPath="yes" Source="SourceDir\libwinpthread-1.dll" />\r
+            </Component>\r
+            <Component Id="cmpEA16414E09FD1D5068139520966B49EB" Guid="9B14A007-CA41-4C45-BE9E-0B1067DD5311">\r
+                <File Id="filFC8C908E3AD35E8090AFF39C9B0FB744" KeyPath="yes" Source="SourceDir\opengl32sw.dll" />\r
+            </Component>\r
+<?endif mingw?>\r
+<?ifdef qt5?>\r
+            <Component Id="cmp01229088A6F1DEA1BE391E6E7CC3D3EE" Guid="9A610B9C-6322-4045-9C4B-910FFBA38502">\r
+                <File Id="filAD80F4724AAE3D8B0682F5982CC2548D" KeyPath="yes" Source="SourceDir\Qt5Core.dll" />\r
+            </Component>\r
+            <Component Id="cmp1658AEC83DA2CDD0D93D5BAAABBEAD46" Guid="9D7923EE-3849-4FCE-A0C4-A2254AD769D3">\r
+                <File Id="fil7E727B350B33C4EEEFE406E27624E057" KeyPath="yes" Source="SourceDir\Qt5Gui.dll" />\r
+            </Component>\r
+            <Component Id="cmp2DE804C107C75B0C71470A93647D09B5" Guid="F8E604E4-A6B2-444D-9F1F-2D86230D5585">\r
+                <File Id="filD86C7802D20EDF450E8B03BC177BA71F" KeyPath="yes" Source="SourceDir\Qt5Network.dll" />\r
+            </Component>\r
+            <Component Id="cmp82912105030EA0D2D7550CF8AC5677F6" Guid="17A8DAEB-44BD-451C-BEEE-C3C2EF883516">\r
+                <File Id="fil6DA129DEAE11F969B17E1886F9AF9AA0" KeyPath="yes" Source="SourceDir\Qt5Svg.dll" />\r
+            </Component>\r
+            <Component Id="cmp975651B71289ACA70B3EB154C08A27F0" Guid="FD612706-4DF7-439B-B82B-57EFA2657F84">\r
+                <File Id="filE89B28CF105C7A7691C046E6516D540E" KeyPath="yes" Source="SourceDir\Qt5Widgets.dll" />\r
+            </Component>\r
+            <Directory Id="dirFF9CCFA394406B344C7FF767066C0809" Name="bearer">\r
+                <Component Id="cmp3CE5AF7CB2F8B2689D2096769883BC56" Guid="FD4AB5B8-4F89-4C08-819B-58A7E8F6360A">\r
+                    <File Id="fil94C33B546082B4DF43813FFB7569FBF6" KeyPath="yes" Source="SourceDir\bearer\qgenericbearer.dll" />\r
+                </Component>\r
+            </Directory>\r
+<?endif?>\r
+<?ifdef qt6?>\r
+            <Component Id="cmp9147A534CD8EDAB4BA7FDEAEEFE9C511" Guid="{8D05F121-517E-4A1F-86FE-26A735F23F1C}">\r
+                <File Id="fil88DECE330C4A1F60E0198BE9EAD7F35B" KeyPath="yes" Source="SourceDir\Qt6Core.dll" />\r
+            </Component>\r
+            <Component Id="cmp52AB384603E096D6B6A1E1FC66B089D4" Guid="{FA746517-7C88-4473-9162-22E9A8CF6507}">\r
+                <File Id="fil93995D4EFFB081D1CEF92B0D38526E04" KeyPath="yes" Source="SourceDir\Qt6Gui.dll" />\r
+            </Component>\r
+            <Component Id="cmp0E8605D5C55843FCDFE809B5EE8DFDFA" Guid="{499C045E-88C6-43A6-B567-6C3438CA5FFD}">\r
+                <File Id="fil63F02A7FFBCDAAB6EAB6E65E642AC7B0" KeyPath="yes" Source="SourceDir\Qt6Network.dll" />\r
+            </Component>\r
+            <Component Id="cmp5D904885C8104A4049616BBAA95F34A3" Guid="{B47BE8F9-D9B5-4345-A232-F2EE8B6D4931}">\r
+                <File Id="fil30A776C0F63064A6AF65369FF23FDB8C" KeyPath="yes" Source="SourceDir\Qt6Svg.dll" />\r
+            </Component>\r
+            <Component Id="cmpCAE694BB34A9EC66AEA9D10D697F1EAA" Guid="{C1A11A59-3BF3-4562-92C6-304A80B93DAE}">\r
+                <File Id="fil43665A392379256FF1D68CF38B97EA38" KeyPath="yes" Source="SourceDir\Qt6Widgets.dll" />\r
+            </Component>\r
+<?endif qt6?>\r
+            <Directory Id="dirB4D909CA5877E8EBEB32B83B2D15F595" Name="iconengines">\r
+                <Component Id="cmp1AAFF160C8E04AD945D74CA884667ABC" Guid="F9CAC368-2789-40F4-ADB8-0EDF377306DF">\r
+                    <File Id="filB4B2363760DE489E24EBDBD0BC837FDE" KeyPath="yes" Source="SourceDir\iconengines\qsvgicon.dll" />\r
+                </Component>\r
+            </Directory>\r
+            <Directory Id="dir60B76952C227590F998DC42F548339BE" Name="imageformats">\r
+                <Component Id="cmp02252B1C1976959CFEE7FA11B6E36C4C" Guid="67FB3C37-C4EB-404B-84E0-2B85288C9A46">\r
+                    <File Id="fil646F5D906D2B2A321C0C481D1D22C9C7" KeyPath="yes" Source="SourceDir\imageformats\qgif.dll" />\r
+                </Component>\r
+                <Component Id="cmpD9DF9496C7453F1AA6C20CD4A1C94ED7" Guid="6EAE06EC-8FC6-4075-8B7D-2E5E2ECDBC1D">\r
+                    <File Id="fil0A4C8F6CF0C62302928ED23A10F88E84" KeyPath="yes" Source="SourceDir\imageformats\qico.dll" />\r
+                </Component>\r
+                <Component Id="cmp0E8940C551429DC79C890130F6D10B6D" Guid="4E9F8DE6-5789-421B-8299-04C8F95F6804">\r
+                    <File Id="fil080B2419A3875B7348050418715B59D5" KeyPath="yes" Source="SourceDir\imageformats\qjpeg.dll" />\r
+                </Component>\r
+                <Component Id="cmpC6FCE143A8E5EAFEA6119E94128459C6" Guid="E22AD241-DA8F-4016-A34B-3DA0ABB77AE9">\r
+                    <File Id="filE6DE80FB6D1ADCBBC8D5665DCF3A4674" KeyPath="yes" Source="SourceDir\imageformats\qsvg.dll" />\r
+                </Component>\r
+<?ifdef extraimageformats?>\r
+                <Component Id="cmp62CBFF830BF239A4E376FAE29937AFAB" Guid="FCFFB090-6600-4F92-9F71-A9EFDA3F5F05">\r
+                    <File Id="fil2132CC3E0278FB6088A48B8A3C120D44" KeyPath="yes" Source="SourceDir\imageformats\qicns.dll" />\r
+                </Component>\r
+                <Component Id="cmpFCC4763BEDEA49F63E8D8E3A18E86329" Guid="D3ABC0A1-CE5F-4ABB-8B15-E630FCF1C9B1">\r
+                    <File Id="filAABDE938CE7225264D2AD6E5B85F0F35" KeyPath="yes" Source="SourceDir\imageformats\qtga.dll" />\r
+                </Component>\r
+                <Component Id="cmpF278C1175D7FB53021C0126B8F155FC2" Guid="24C00F19-08E6-40E6-913E-AD54EFB81AB7">\r
+                    <File Id="fil102CCD3441784435E39937C763D95F2F" KeyPath="yes" Source="SourceDir\imageformats\qtiff.dll" />\r
+                </Component>\r
+                <Component Id="cmp608F60C2BD04F2C37AB51C3211ABF615" Guid="8A88549A-3B1F-4A94-B721-7ED99998CA95">\r
+                    <File Id="fil8698D33404DE182991C30D32BA39A5EC" KeyPath="yes" Source="SourceDir\imageformats\qwbmp.dll" />\r
+                </Component>\r
+                <Component Id="cmpE6323C810986405043F219CECD6B6C10" Guid="7194531B-2B4C-4DB0-8BED-B89929695F17">\r
+                    <File Id="filB9C6E81C6DBC26365A8C44CD0A36C49F" KeyPath="yes" Source="SourceDir\imageformats\qwebp.dll" />\r
+                </Component>\r
+<?endif extraimageformats?>\r
+            </Directory>\r
+            <Directory Id="dir623D6610C7C3C018A72040463AD22773" Name="platforms">\r
+                <Component Id="cmp9132FD413972F732967B8BC075BC2781" Guid="0DAA1BEB-EA44-4EEC-BCC8-22CE871FC519">\r
+                    <File Id="fil835F5A10B680242272F647CE0A82B16C" KeyPath="yes" Source="SourceDir\platforms\qwindows.dll" />\r
+                </Component>\r
+            </Directory>\r
+            <Directory Id="dirF1334AF64D097D9920D6F285D2DE292C" Name="styles">\r
+                <Component Id="cmp52840D209B2AC1DC45A29A6BA5A75FF8" Guid="3FA00436-A967-45AB-8871-A6DA384044CE">\r
+                    <File Id="fil84E6446D9ECFA487C5A2EC4752E07F04" KeyPath="yes" Source="SourceDir\styles\qwindowsvistastyle.dll" />\r
+                </Component>\r
+            </Directory>\r
+<?ifdef qt6?>\r
+            <Directory Id="dir65521F18679A6D14A7BDE5A8612E8C69" Name="tls">\r
+                <Component Id="cmp83139013FA2C9A99CF2772FD54090FAA" Guid="{60DFDBBA-4832-44FD-B14A-C67EBD4D8119}">\r
+                    <File Id="fil737932C7578FEA0392CDEFB7540D7E44" KeyPath="yes" Source="SourceDir\tls\qcertonlybackend.dll" />\r
+                </Component>\r
+                <Component Id="cmp6E4460E3D6226693FEA5DBBF500895C7" Guid="{CBF26A3F-BA32-4017-BAEF-014AA3C51653}">\r
+                    <File Id="fil02E5C4A93CDAD6084E6372A2EE1F7ABA" KeyPath="yes" Source="SourceDir\tls\qopensslbackend.dll" />\r
+                </Component>\r
+                <Component Id="cmp53306F84AD30BD49CF0C1A68007F9531" Guid="{47907844-2DC3-4E99-B1C9-B9C716E59407}">\r
+                    <File Id="filD182E98DFB41D91EC2F2A531F6CAF918" KeyPath="yes" Source="SourceDir\tls\qschannelbackend.dll" />\r
+                </Component>\r
+            </Directory>\r
+<?endif qt6?>\r
+<?ifdef hastranslations?>\r
+            <Directory Id="dirF1C5FA08924D46D2A43D5134DF70C0A8" Name="translations">\r
+                <Component Id="cmpBCDC6995284E75B9A24E4646E6117744" Guid="51A3610B-4AE8-4020-926E-80B77B650E52">\r
+                    <File Id="fil694FD3DFB9C088844BF9929014423509" KeyPath="yes" Source="SourceDir\translations\qt_ar.qm" />\r
+                </Component>\r
+                <Component Id="cmp3B3D2ADEE08668758CB9FDB1D243AA11" Guid="E84288C8-80B5-43C0-ABC1-13720B58F806">\r
+                    <File Id="fil0877F2432B761D0289C5DB92EC1E528E" KeyPath="yes" Source="SourceDir\translations\qt_bg.qm" />\r
+                </Component>\r
+                <Component Id="cmpDAE87E8CC29C30A200A5D17D81DF1DE7" Guid="BC72F77D-4D7F-4AFD-BC4F-0F9E337182A9">\r
+                    <File Id="fil36E8A56D7DAEC62D31CEA91730D42174" KeyPath="yes" Source="SourceDir\translations\qt_ca.qm" />\r
+                </Component>\r
+                <Component Id="cmp719D62CCB63A2C08F22335557CBAB533" Guid="084DAFF0-3499-4D2E-9685-D0F7573A0723">\r
+                    <File Id="fil5ECAF3F9F1940369AA594EE12825D0FB" KeyPath="yes" Source="SourceDir\translations\qt_cs.qm" />\r
+                </Component>\r
+                <Component Id="cmp8BDD412901C153FC8A2BF7335835AC78" Guid="ED469E4F-52A1-4BDC-83EA-304D779025ED">\r
+                    <File Id="fil035B9FC36AE44B63202E6201D2A9608E" KeyPath="yes" Source="SourceDir\translations\qt_da.qm" />\r
+                </Component>\r
+                <Component Id="cmp56040A1BBEBE8A547F7D6672326FF4EE" Guid="F0F32818-EB50-415C-8302-B3BCFA3D1A5F">\r
+                    <File Id="fil0AA794E087CE581F4CA99D75C295092D" KeyPath="yes" Source="SourceDir\translations\qt_de.qm" />\r
+                </Component>\r
+                <Component Id="cmp42D4B2D8A4C91F9ED463DA42F3895E27" Guid="1444EE4B-AD59-4C33-9B17-3B21B132DAA9">\r
+                    <File Id="filAD08CEBCD34B84E90701D6248FDB5FA9" KeyPath="yes" Source="SourceDir\translations\qt_en.qm" />\r
+                </Component>\r
+                <Component Id="cmpB92DA15530102518B1B98DBF1DAC135D" Guid="10E7AA9E-7DC7-45E5-A52C-9B33DD7CD157">\r
+                    <File Id="filB05A80E55C2FCBFE9999B66E309F0312" KeyPath="yes" Source="SourceDir\translations\qt_es.qm" />\r
+                </Component>\r
+                <Component Id="cmp7210C3ACD533D6A934BD5977AB16AF71" Guid="8D242F4F-56C4-4BBD-82EF-7EC103AB4445">\r
+                    <File Id="fil4B803C16DFCB1C5EA4D308AFD35049E3" KeyPath="yes" Source="SourceDir\translations\qt_fi.qm" />\r
+                </Component>\r
+                <Component Id="cmp0643210AD0A541FA765CE55469F22FBB" Guid="3DAD67C2-7EBC-4FE2-9375-ED04F44B95C2">\r
+                    <File Id="fil65E050731B55F3764817B70B50D1F483" KeyPath="yes" Source="SourceDir\translations\qt_fr.qm" />\r
+                </Component>\r
+                <Component Id="cmpBB98B3096730FFFE87E708A6291ED47C" Guid="4FC0CA17-91C9-4207-B18F-0D3676A4EAA8">\r
+                    <File Id="fil38E65580D7E2598A2AB22B21A55AE808" KeyPath="yes" Source="SourceDir\translations\qt_gd.qm" />\r
+                </Component>\r
+                <Component Id="cmp3666ECDFA614EA095AF7310AECCD44B7" Guid="A4ADC90F-ED1F-49EC-9595-226C90767318">\r
+                    <File Id="filD812B4BB739798D5C3BF25BEFBFBA938" KeyPath="yes" Source="SourceDir\translations\qt_he.qm" />\r
+                </Component>\r
+                <Component Id="cmpC4FA2671255E7459BD46070D88B76FF1" Guid="427500F9-DE40-499D-AA91-34944AD8AD45">\r
+                    <File Id="fil750345F0F25CC28174362BF42A332BFA" KeyPath="yes" Source="SourceDir\translations\qt_hu.qm" />\r
+                </Component>\r
+                <Component Id="cmp8D644C0D5E80E34EBCB6FC3643C879AF" Guid="FA35CBD4-A861-494E-AABC-76262872F717">\r
+                    <File Id="filF6634E490610EB73E2D852D47B43255E" KeyPath="yes" Source="SourceDir\translations\qt_it.qm" />\r
+                </Component>\r
+                <Component Id="cmpA8DDE2BD9E1AB00C9631DE447CBAA11D" Guid="15D64C1E-B407-4531-9684-A785DAF53421">\r
+                    <File Id="fil493967A00B1FFB8D251F7A844D53A64A" KeyPath="yes" Source="SourceDir\translations\qt_ja.qm" />\r
+                </Component>\r
+                <Component Id="cmp23B3657EE14B0ED7D00B271AFF81187C" Guid="6FC090EB-D1CC-423E-9527-DBDF4E7A4B8F">\r
+                    <File Id="fil5A7EE1B788293556A515EAFFC95747DB" KeyPath="yes" Source="SourceDir\translations\qt_ko.qm" />\r
+                </Component>\r
+                <Component Id="cmp2D105F39980CF93A40405E25755346BB" Guid="79E81F16-F627-4CD1-BB41-5C52A141BE5F">\r
+                    <File Id="fil17B2A1B774381BBA3BF85AF1FFB636AA" KeyPath="yes" Source="SourceDir\translations\qt_lv.qm" />\r
+                </Component>\r
+                <Component Id="cmpC3CB042567F53DD612D309E9CA2E10A6" Guid="CED83F6F-60ED-4321-94B6-7917D1DB232C">\r
+                    <File Id="filC674EE989D2B9719C388F537FC8EFFCF" KeyPath="yes" Source="SourceDir\translations\qt_pl.qm" />\r
+                </Component>\r
+                <Component Id="cmp43D57F897A909679308EFC03849D7E2E" Guid="8E42022B-B599-4ABD-8D30-F5E378FE60FE">\r
+                    <File Id="filADA551AF628F8CBA7E3D06EF76F2713A" KeyPath="yes" Source="SourceDir\translations\qt_ru.qm" />\r
+                </Component>\r
+                <Component Id="cmpF5EA7E37332E95C614BD2D7EAA933E5D" Guid="6516FDA0-5035-420D-AADF-41D1E3735EDA">\r
+                    <File Id="fil49E3CA421049811064612E40502A6147" KeyPath="yes" Source="SourceDir\translations\qt_sk.qm" />\r
+                </Component>\r
+                <Component Id="cmp8DAF13208A863A841AEE7A46E79FB87C" Guid="{A628AE90-BA9E-4CB9-86E0-02E6F2A0358B}">\r
+                    <File Id="filD139779D4ABC7CD76AE12C511550A8C3" KeyPath="yes" Source="SourceDir\translations\qt_tr.qm" />\r
+                </Component>\r
+                <Component Id="cmpA214B663A372012F5DCBA6A5CDAE421D" Guid="790AF12A-1970-4B96-BFAD-925734F604EB">\r
+                    <File Id="fil1380C741E2DBC1F60AA4DC32D66B1E6C" KeyPath="yes" Source="SourceDir\translations\qt_uk.qm" />\r
+                </Component>\r
+                <Component Id="cmpFDB70117D8E46EB188997470A3793B5D" Guid="EA2F54B9-EE49-4426-A2EF-EDDF3CAC83FD">\r
+                    <File Id="fil9085627E4E33630ABCDB62A5CC05B794" KeyPath="yes" Source="SourceDir\translations\qt_zh_TW.qm" />\r
+                </Component>\r
+<?ifdef qt6?>\r
+                <Component Id="cmpF0099CF54A0669D87CC17418C3911422" Guid="{3405A05E-CB26-47D2-9C70-C52167AB8FDC}">\r
+                    <File Id="fil71B9851F396CBCC25B18299EDCBF5FF5" KeyPath="yes" Source="SourceDir\translations\qt_fa.qm" />\r
+                </Component>\r
+                <Component Id="cmpE89BF9966FD9EC39B16BC14B5586E0FA" Guid="{818D0415-1B0D-4C20-A21B-634467D7ABED}">\r
+                    <File Id="filB4B324E9728AE54A2B57A18E91814BC1" KeyPath="yes" Source="SourceDir\translations\qt_hr.qm" />\r
+                </Component>\r
+                <Component Id="cmp7F6AC80CE7E39EEDC9A962EAFADD113B" Guid="{E7491B72-07BC-4811-9AA2-9C90EA586475}">\r
+                    <File Id="filAB0862690AA36917007DE3C425A282CD" KeyPath="yes" Source="SourceDir\translations\qt_nl.qm" />\r
+                </Component>\r
+                <Component Id="cmp9EB4F508A604A4AD35BC558C456111DD" Guid="{035418D9-E833-45B1-B33E-15B8C6EA4E03}">\r
+                    <File Id="filA89B168478D587343A282C54242055A0" KeyPath="yes" Source="SourceDir\translations\qt_nn.qm" />\r
+                </Component>\r
+                <Component Id="cmp09D69EFDCF1B331A37D028C962FE6147" Guid="{BBD6ABD9-DDF9-4AC2-B8C1-FB4CB547B14F}">\r
+                    <File Id="fil3DBA8083389C96E9A47E27507D39EA05" KeyPath="yes" Source="SourceDir\translations\qt_pt_BR.qm" />\r
+                </Component>\r
+                <Component Id="cmpA23046CBC7F8B7E62AF4843CBB72DEC9" Guid="{FE5DC38D-3ABF-406D-8FB7-258119E76380}">\r
+                    <File Id="filCAF43146A9F7B1727B4975BB51D0A045" KeyPath="yes" Source="SourceDir\translations\qt_zh_CN.qm" />\r
+                </Component>\r
+<?endif?>\r
+            </Directory>\r
+<?endif hastranslations?>\r
+<?ifdef vs?>\r
+            <Component Id="cmp10C81658E3C8F63B19EF9D1C26B58363" Guid="{67FF3D4C-A3F5-4C19-8988-CB8111149469}">\r
+                <File Id="fil0F208DFAC8D7EB86E7793690C6ECF15E" KeyPath="yes" Source="SourceDir\Qt5Qml.dll" />\r
+            </Component>\r
+            <Component Id="cmp5B4749C387D33EAF993DDC89F9DBC102" Guid="{E1A0612E-1BAA-4304-A53F-04F9E5FC2C95}">\r
+                <File Id="fil4D31B1A954CDE740FB32D9FFC142CA37" KeyPath="yes" Source="SourceDir\Qt5QmlModels.dll" />\r
+            </Component>\r
+            <Component Id="cmp7F83581B3F7835094D371708381AD391" Guid="{F123314E-1319-4B12-90E9-BB27786C2234}">\r
+                <File Id="filEFD45339E9495CECA6AD117B583ED589" KeyPath="yes" Source="SourceDir\Qt5QmlWorkerScript.dll" />\r
+            </Component>\r
+            <Component Id="cmp33B78CE22F0CBCA4F41C5037A792210C" Guid="{5653E0BB-A956-4565-B2AD-11712CC59571}">\r
+                <File Id="fil87DAAA273949BD9AD08743E49398302F" KeyPath="yes" Source="SourceDir\Qt5Quick.dll" />\r
+            </Component>\r
+            <Component Id="cmpB5571DB4C3153CA848ED1238DBCD4281" Guid="{AAD89C99-7211-4F02-B07D-A89CD18DF9D5}">\r
+                <File Id="fil07F4E15C2DC44A3DA783DB682CA4FF5C" KeyPath="yes" Source="SourceDir\Qt5QuickControls2.dll" />\r
+            </Component>\r
+            <Component Id="cmp4C1BE3EACCCE195C8B88434D5DF77DE5" Guid="{C8585769-1336-4CC6-B8CD-5D9CEE77F079}">\r
+                <File Id="filBE9D9480511F5B210D1FE72753A138A6" KeyPath="yes" Source="SourceDir\Qt5QuickTemplates2.dll" />\r
+            </Component>\r
+            <Component Id="cmp937697C855506045D9FF8DF5588CAFEE" Guid="{DD5543D0-2071-11EE-9B4B-0800200C9A66}">\r
+                <File Id="filE314D75F1A97D0D3A74F77E884E64AD5" KeyPath="yes" Source="SourceDir\Qt5WebSockets.dll" />\r
+            </Component>\r
+            <Directory Id="dirAB1B0C3E5160B20B93DD2AC0AE9E5509" Name="qmltooling">\r
+                <Component Id="cmpFEC2C3E75BBEACDB1A6F4AFB808CD58E" Guid="{64ED0E30-E431-4238-BD39-465D306D9520}">\r
+                    <File Id="fil09C44C978E58830A831DCC0DBAF3AEA2" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_debugger.dll" />\r
+                </Component>\r
+                <Component Id="cmp8B3D94901F4D369CD276B339B8B00F70" Guid="{554EAB57-00E2-4FCB-87FF-6F07EF14EDC4}">\r
+                    <File Id="fil3E82AB436354230BADBA9DD68B38C2AD" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_inspector.dll" />\r
+                </Component>\r
+                <Component Id="cmp0B6C2EAABA7EAA14C510F4C9D68A3F40" Guid="{A211CD9B-6E6A-4600-9FA1-8DF999A942AE}">\r
+                    <File Id="fil8CD8981EA2309B09460020F4D9AC275C" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_local.dll" />\r
+                </Component>\r
+                <Component Id="cmpE75E4FEE65DCA51D044DF4B2AEDCD828" Guid="{625FA227-EB72-4C9E-AD73-8FAC837EE2D2}">\r
+                    <File Id="fil2A36EBC77D39FBFF748DE570C1F7DC96" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_messages.dll" />\r
+                </Component>\r
+                <Component Id="cmpA8BBE7010AD624EFBA7166925167402F" Guid="{D044FD19-6C91-4736-80E4-D2DA97CD60B4}">\r
+                    <File Id="filA4CF6CC4639F20D9F436C3DE2C32102E" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_native.dll" />\r
+                </Component>\r
+                <Component Id="cmp00B1DB73ED33EC1DD6658036D3901CB7" Guid="{0FCBA1AB-A703-4399-A518-94B72B55BBCF}">\r
+                    <File Id="fil04B1E67A26C29E877D34C6CDB21D302D" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_nativedebugger.dll" />\r
+                </Component>\r
+                <Component Id="cmp9F481CFBAECB8853AF07C1D5060B77D7" Guid="{C2D900C8-5DE0-4EFA-A124-A3865FFE4149}">\r
+                    <File Id="filCD389466FEC0CB778B4940AD0354D4A0" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_preview.dll" />\r
+                </Component>\r
+                <Component Id="cmp93816105EAC51740B98AF9C1041D28D1" Guid="{D8F9A3EE-B929-4418-A3EE-F3228B05A5F2}">\r
+                    <File Id="filE23CD7951728F9C858C61FDBAFD405D2" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_profiler.dll" />\r
+                </Component>\r
+                <Component Id="cmp5ED50822278A4A846E891B41C07F7B6A" Guid="{FBDFDAD6-886A-479A-8ED1-D04B0028E9EA}">\r
+                    <File Id="fil935ED8A28D14AB20137795A9C347694D" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_quickprofiler.dll" />\r
+                </Component>\r
+                <Component Id="cmpACA0A17DA54991A194B823CE40D06263" Guid="{8AA14A55-6F27-48E4-9A18-90D618AD8194}">\r
+                    <File Id="fil6E61B7F24F3C3FDB83A90CEE46B2C831" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_server.dll" />\r
+                </Component>\r
+                <Component Id="cmp3D2C980EC975DBBCBD13A9178DD1A03B" Guid="{8AA853F3-4BAD-4E76-8F48-CB9FC961AE1F}">\r
+                    <File Id="fil0C06BD9370B146FE43DEFD060222FC12" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_tcp.dll" />\r
+                </Component>\r
+            </Directory>\r
+            <Directory Id="dir6932501040B63B39C33E640A395AEA58" Name="QtGraphicalEffects">\r
+                <Component Id="cmp356189FC3558D542AA8AD79067A42632" Guid="{25EE1F94-BDC5-4BF4-9CF5-4471CB2CDC99}">\r
+                    <File Id="filC50FC92D4690551FD9149804C9524CB6" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\Blend.qml" />\r
+                </Component>\r
+                <Component Id="cmpABC392DBCC9A5A469B77C129899873EE" Guid="{DB29E30C-5362-4A23-BB9D-C1E6B4569979}">\r
+                    <File Id="fil585EA7DCEFEA763B0D34D92C25BF236D" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\BrightnessContrast.qml" />\r
+                </Component>\r
+                <Component Id="cmp740F889D5799A2FF0F93A24058617EB6" Guid="{304F8096-0A76-4DA8-904E-DBC6ECB871EA}">\r
+                    <File Id="fil44DA8C781EB6783323659B96815E6B06" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\Colorize.qml" />\r
+                </Component>\r
+                <Component Id="cmp2799826256C0E84847E94E11C7878018" Guid="{9D087450-2C02-4859-8100-57FD4BE69464}">\r
+                    <File Id="fil79E5BBBAD1145AE67A9F57C761872AB7" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\ColorOverlay.qml" />\r
+                </Component>\r
+                <Component Id="cmp659424F663F96AE55FBA532B4CBB2EC8" Guid="{B9C2D28B-C4EF-45DB-B9BA-E1B9405D76F4}">\r
+                    <File Id="filACFDAF73B5B767483881AB3B6CD4F973" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\ConicalGradient.qml" />\r
+                </Component>\r
+                <Component Id="cmpDF6A99C1FA6B3943D690F21136BA7F9F" Guid="{03C439A4-BE19-49B8-9F1F-972FF89C415D}">\r
+                    <File Id="fil12DD1BF106F38168A5EE7CAFE7622E54" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\Desaturate.qml" />\r
+                </Component>\r
+                <Component Id="cmp81B459304782BB6AC65E381BA13726B0" Guid="{11D487DE-683F-4647-872C-C2E1DAA33042}">\r
+                    <File Id="fil71154C20C340083D34D22ED1F8EFA765" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\DirectionalBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp3CC0196A91D9C805D66BFFF4EA38EBE7" Guid="{16A372C2-116D-42AC-B431-3B220C37B9EE}">\r
+                    <File Id="fil0038BC20F6506BDA5417B08B88C92BC1" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\Displace.qml" />\r
+                </Component>\r
+                <Component Id="cmp67E65C02C04C3567636F47EFAA888D12" Guid="{2358043B-BBC7-450B-B5D3-8C26E5249358}">\r
+                    <File Id="fil7602377B1CFE07CFCA64BC753B0AE930" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\DropShadow.qml" />\r
+                </Component>\r
+                <Component Id="cmpADC8E894229BBB48BBC40D4F033FD791" Guid="{2EF6B35A-FD9D-42BA-A12C-3763684AC39C}">\r
+                    <File Id="filC46A98B519397B6CE253A5C9F8D5E0E4" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\FastBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp0D2D3E713559661141BC5DD9F687F29D" Guid="{7556A68C-05FC-44CB-B9FD-56535DE7653D}">\r
+                    <File Id="filA97688F2BA74A175F3E4EB3F9C1BB027" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\GammaAdjust.qml" />\r
+                </Component>\r
+                <Component Id="cmp3C102DD93EC83164DA87E7820364DDAC" Guid="{6C1D3B9D-A04C-4749-84B6-B276EDC28D20}">\r
+                    <File Id="fil5B2FC5A0BED021E222F6738C10D3081C" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\GaussianBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp4B666BB592E998D07B55C82CF76F5B1C" Guid="{FAC4A87D-2D90-46C6-98C6-D3A5CB94732C}">\r
+                    <File Id="filC5E145603A0E00BF0F22F2B8A67C9A22" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\Glow.qml" />\r
+                </Component>\r
+                <Component Id="cmp4C99F5024E504FEAB151291478878934" Guid="{E0EB60EE-815C-45C3-B5BD-F8D68E099B3B}">\r
+                    <File Id="fil5F059EE301BFBC9CC82E3CCBEDF542CE" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\HueSaturation.qml" />\r
+                </Component>\r
+                <Component Id="cmp84BC34E1E20AAE86B427C9CD3129C4FA" Guid="{0763338E-FBE7-40BC-9591-8F3B5DB576E3}">\r
+                    <File Id="filA56659AFD9BB9DFBFDDA906B787EFCDD" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\InnerShadow.qml" />\r
+                </Component>\r
+                <Component Id="cmpCBA14389AA89D290E481E0F2625854CE" Guid="{406FB202-E76B-4FFD-A1E4-8FEB2F67401F}">\r
+                    <File Id="fil0C15C4F0A2C4CBC263B38BB384C9DA59" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\LevelAdjust.qml" />\r
+                </Component>\r
+                <Component Id="cmp6FFF646F52B60CE854DA0A1B8C47EC55" Guid="{ED6F3223-0243-4539-96C3-8EE89715F16F}">\r
+                    <File Id="fil53C443677D967DDB997226A1954ED5C7" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\LinearGradient.qml" />\r
+                </Component>\r
+                <Component Id="cmp5E04D231070BE2CE70B2672E56C6B9DE" Guid="{04ACB86E-F6EE-4019-BB49-B556AAF08359}">\r
+                    <File Id="filA148939302F08F0A8B0CC79A709DFA15" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\MaskedBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp8C5CA067EC2AB41E7CAFE253F2EC86A8" Guid="{76E67D4A-0F5F-4AAF-8A78-847BF0EACDB4}">\r
+                    <File Id="fil2921C33FE8BBE6511E3EB7CA8C06115F" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\OpacityMask.qml" />\r
+                </Component>\r
+                <Component Id="cmp8441DAF2F2BC211F1F6FD10DCE7B51FA" Guid="{70CBBCF5-9030-4FE8-98F0-6665ABF0D3D7}">\r
+                    <File Id="filC8B8AEF14AC36FE600581D00FE128545" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\plugins.qmltypes" />\r
+                </Component>\r
+                <Component Id="cmpC3A1EC5089BA6B0798970A12E614BBDB" Guid="{3EC761B0-3826-451A-8D11-DD0B77E965DB}">\r
+                    <File Id="filDDA0AA71F77A367B9A901655165B1B51" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\qmldir" />\r
+                </Component>\r
+                <Component Id="cmp854B1173EACBB218B6AF849A7E8A56D7" Guid="{F879D731-522C-4E76-85F8-5DF0BD32421E}">\r
+                    <File Id="fil587B7775B793D6255A1BB1DE826E6A95" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\qtgraphicaleffectsplugin.dll" />\r
+                </Component>\r
+                <Component Id="cmp7D04EA58DD5BF794EB2D3A75609DEFF9" Guid="{2A98610D-386B-433E-ABC0-C878C1A632DA}">\r
+                    <File Id="fil9044BD6346C5BE02A746FAC271ED7427" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\RadialBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp688C34D162C3029BDE9A236F29106F11" Guid="{D34CDD08-E05A-4D45-9800-514F9C53AA0D}">\r
+                    <File Id="fil1B3F7EAA750FA63EF84F1FBA40ADEACB" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\RadialGradient.qml" />\r
+                </Component>\r
+                <Component Id="cmp7F9ABA6F59701379E7E54AB2EF4B01EA" Guid="{618C4845-14BB-41FB-8504-F256C0651929}">\r
+                    <File Id="fil89AD529AFFF6D62301719C7BAA8F28FD" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\RectangularGlow.qml" />\r
+                </Component>\r
+                <Component Id="cmp46E93DFCA823AE21F1FD874F13C0A6CA" Guid="{3248C193-B4CA-4AC1-8941-8096A52FE13F}">\r
+                    <File Id="filD9317E9BA692B66CC2C535F5B3A2F3BB" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\RecursiveBlur.qml" />\r
+                </Component>\r
+                <Component Id="cmp2F1F9DE69090BFD2946A7621F2DA410E" Guid="{2040A763-52C4-4BC3-8C8E-0B7F80C709A5}">\r
+                    <File Id="filBE37116DE14EE120D97D6725D27F4907" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\ThresholdMask.qml" />\r
+                </Component>\r
+                <Component Id="cmpE650E3AF80C0073C2B52BC369D45B79C" Guid="{59F7E175-5A48-4A83-9D2E-13A259A30660}">\r
+                    <File Id="fil4590ECDA88EBA09BB76C6180A1F4FA4B" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\ZoomBlur.qml" />\r
+                </Component>\r
+                <Directory Id="dir796AB3D358CFA364C82060B28476B244" Name="private">\r
+                    <Component Id="cmpB62C6E9BF562754AAC86BB52A361231D" Guid="{DD730ADB-7BC5-40E9-B34E-60DBB65391B6}">\r
+                        <File Id="filEC6B5D0DA92369DBBD0CF163C14FDFBD" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\DropShadowBase.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpA11A92D9D7D10A9221DD05699B7B1B8C" Guid="{9CE273AB-3553-4CE8-9E0D-40202A50CEDC}">\r
+                        <File Id="fil746D3769B6A878D31F7B39B82F020A30" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\FastGlow.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpA9B83EF4B33682EFEA153198F1C44255" Guid="{26AF03C7-77EC-40A4-ACB0-7866EE13492B}">\r
+                        <File Id="fil7DF3A69E24DEEFB702D593D5E4B76C0F" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\FastInnerShadow.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp4951B8E17EA0B3A6B265E2C0E893A50A" Guid="{48B858D5-8008-4CB0-B9AA-B3DB33C43E3D}">\r
+                        <File Id="filA527CD28BB0968D628976B78FEB0CC38" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\FastMaskedBlur.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp2BD742974C0791D6BD1AE4F7E3646D99" Guid="{E43BE5AA-F4D0-4CA7-A32F-3D807202C8BC}">\r
+                        <File Id="fil56212E247BB6F393FBE83AB4A7EAE2FB" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianDirectionalBlur.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpF8459C652CF5C20ED87F853C1ABE1752" Guid="{D85D4DD2-A83E-485D-A94D-66B892DBBC21}">\r
+                        <File Id="fil39F0CB1500963D3395433DA5DFC9B13D" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianGlow.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpCA0AF56CA87A52053FE61B6ECD156E35" Guid="{6E13F7D4-4D44-43F7-BCAA-A77EB901AF5F}">\r
+                        <File Id="filA28BA0B69A49D99C713AC84E1BE3CD39" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianInnerShadow.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpBC9523950CF52702CB1A519123F9E519" Guid="{6F73A2D5-41FF-4C3A-80CB-05DE74669583}">\r
+                        <File Id="filE0A177DDE81AF18FB3CFE57A0727C02E" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\GaussianMaskedBlur.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp302F033AE251BD62E648805EFB1BB536" Guid="{CDCAB63D-67AE-4A62-8FB1-9C0151617C46}">\r
+                        <File Id="fil51BFE00C51A89DC3E74DC04564BCB18A" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmpF3F704D8F2236A0A62B980FA41D3CB5E" Guid="{2CB437C9-31D4-478A-8BFA-9D8EBCD368DB}">\r
+                        <File Id="fil09960BC83EE77CD38744C380783E9547" KeyPath="yes" Source="SourceDir\QtGraphicalEffects\private\qtgraphicaleffectsprivate.dll" />\r
+                    </Component>\r
+                </Directory>\r
+            </Directory>\r
+            <Directory Id="dir898F47F1C6F351486C9AAE4005F1B49F" Name="QtQml">\r
+                <Component Id="cmp00AC1A2BE52EA04246F38C5F0B22A9D0" Guid="{DC866072-ECA9-4CA2-9613-C25D77760BE7}">\r
+                    <File Id="filE33FDBE48E6FC8479D62000A1D4DB79C" KeyPath="yes" Source="SourceDir\QtQml\plugins.qmltypes" />\r
+                </Component>\r
+                <Component Id="cmp0A8C093EE03780F9F7823442F10A6C82" Guid="{109B01C3-36B5-4130-9A37-ACA89A7BB868}">\r
+                    <File Id="filFF531EAEA986913D79F95FFBCB9A3DD8" KeyPath="yes" Source="SourceDir\QtQml\qmldir" />\r
+                </Component>\r
+                <Component Id="cmp44B6838E73CD47A3F0061ECCE2EB48D0" Guid="{29B239A3-D455-40DA-8A21-F9AEAAEAA42B}">\r
+                    <File Id="filA5A3A998AB34A694D3B664958BD2376A" KeyPath="yes" Source="SourceDir\QtQml\qmlplugin.dll" />\r
+                </Component>\r
+                <Directory Id="dir48A5FBBF91E1C202003BAC3F8CB3BF51" Name="Models.2">\r
+                    <Component Id="cmp57A7D8DDBA7FB780E38A3CF0C3DE50F6" Guid="{FC1A6BF2-A27C-4AB4-BC53-31092E0481A9}">\r
+                        <File Id="fil6BB08B36A3E11C569382D84D1E88C9B4" KeyPath="yes" Source="SourceDir\QtQml\Models.2\modelsplugin.dll" />\r
+                    </Component>\r
+                    <Component Id="cmp74FB488898EE30854670C634A6CA57CB" Guid="{928C1715-2F0C-4BA0-9CEE-8DA1D740D679}">\r
+                        <File Id="filBEE366112102E123818634DA4D2DD766" KeyPath="yes" Source="SourceDir\QtQml\Models.2\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmpFF41000DFB8364F307DEC40444A8ED49" Guid="{0E33686C-EA52-43E3-83D4-AF793E79EA25}">\r
+                        <File Id="fil4EF76584838408469F9A719687A6816D" KeyPath="yes" Source="SourceDir\QtQml\Models.2\qmldir" />\r
+                    </Component>\r
+                </Directory>\r
+                <Directory Id="dir7F50D8639A90BC7F159C74A902C07A8A" Name="StateMachine">\r
+                    <Component Id="cmpE8348F57B7F7ED3E102061755EA59678" Guid="{2BFB9277-FB81-4D69-A8D4-94BF7B3492A6}">\r
+                        <File Id="filEA848B17E993426219C6780A6AD0EF90" KeyPath="yes" Source="SourceDir\QtQml\StateMachine\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmp789571C85C4C76BD48E9A471271B5128" Guid="{5ED22F6C-7FCE-4947-9538-4C89ECB8F4CF}">\r
+                        <File Id="fil4203D832E588C73EC48309F8D3AC2BFE" KeyPath="yes" Source="SourceDir\QtQml\StateMachine\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmp07E1D3988D9259288197F71CC0B0559A" Guid="{5D21407C-F3C3-428F-B397-9C4E0D666EF5}">\r
+                        <File Id="filFC54BEA29869B52A91A0A804A6843B4A" KeyPath="yes" Source="SourceDir\QtQml\StateMachine\qtqmlstatemachine.dll" />\r
+                    </Component>\r
+                </Directory>\r
+                <Directory Id="dir38701E48D40F53928079A0277B1BE418" Name="WorkerScript.2">\r
+                    <Component Id="cmp91EB04F6C5AFAF292A0BFCE3570DCBFE" Guid="{4D931278-DD8E-4ACA-B8E0-8436B303B917}">\r
+                        <File Id="fil3AD6975608C156FBDC0A714EFD2E7779" KeyPath="yes" Source="SourceDir\QtQml\WorkerScript.2\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmpDA0DD041411FA5AC356FAD21EBB0BFCC" Guid="{04B2D75A-13D6-47E2-BCE4-0E230B94AE5F}">\r
+                        <File Id="fil7757938E9EFF9E82F19FF17128644874" KeyPath="yes" Source="SourceDir\QtQml\WorkerScript.2\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmpC0C294F18C416B2F09D563A7D2F171C0" Guid="{0B5FC87C-D82F-46BE-AB18-E10BAC7566E4}">\r
+                        <File Id="fil585107DFD90EACA350C802AFD64551B4" KeyPath="yes" Source="SourceDir\QtQml\WorkerScript.2\workerscriptplugin.dll" />\r
+                    </Component>\r
+                </Directory>\r
+            </Directory>\r
+            <Directory Id="dirA71A8D5C38BE16878A4EB5F3B4E1599D" Name="QtQuick">\r
+                <Directory Id="dir1CF0A9FB12A9B3F0BCC43F64CAB69841" Name="Controls.2">\r
+                    <Component Id="cmpBBC07C7AA9E1D56538F0EE5BC883A106" Guid="{18653B7C-05E4-485E-9C62-31D4F2664E1F}">\r
+                        <File Id="filC6250BEA59F89937D08252CE2344D671" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\AbstractButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpA2AD6F575E441894DDF6301F935EF3E7" Guid="{6B3F6F40-74AE-43F1-8EDC-97CC40267B5F}">\r
+                        <File Id="fil7E4C91C7F6D6DE165F5E68148D5B85FA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Action.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp346061900FCE515D6B32CFCD7971325C" Guid="{AE4849A3-BD9E-4D75-84A4-F78DE4AE50E2}">\r
+                        <File Id="filB221D15EF9AD3699086FE43E3C355121" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ActionGroup.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpE5AC86A748B88CA71F59F050376D4BC8" Guid="{3D56EB30-C37B-4040-9341-53A74B36291D}">\r
+                        <File Id="fil8B70502E8A7420E0F20E1C107A446561" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ApplicationWindow.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp26A45023E109A672FAA78FE5C4F827F4" Guid="{F0BE556F-358F-4AA8-B311-8102DE486887}">\r
+                        <File Id="fil2FB69D0E22B7CC46B57FAB19D2931318" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\BusyIndicator.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp92014B7FBE8B02881A6A53384BDCD1C4" Guid="{65C1C88B-5B3E-46E2-965C-DCD4426EFCA5}">\r
+                        <File Id="fil29272699B19CB5F1A2B83C23066B00AC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Button.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp372134AB6EF24BAC7497CC47466F0727" Guid="{B8E80B2D-19A3-434C-A380-DA262254F876}">\r
+                        <File Id="fil9BAAB7742819691F86B3837FCA161A6E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ButtonGroup.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp17FB129D73D5BCA38BEC406E5A7A9853" Guid="{B4EEB089-7AA1-4EAE-BAF3-13837AA5AA71}">\r
+                        <File Id="fil40C33AA9302346640FF8F3431BDFCC16" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\CheckBox.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp59BA1197723375EE2B1A8FC0971407A8" Guid="{C256DDB3-D9F8-47D2-8702-9290C36AB4E4}">\r
+                        <File Id="fil1D4756FDB82E5F7C242AC3DC37F9B2F9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\CheckDelegate.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp9076C49961F4F0F4F5B4F65EF0CE7925" Guid="{6F747374-523D-4121-B79B-E0062B08B433}">\r
+                        <File Id="fil6A4AAD9D20B45D7D2DA5707F1390AF0C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ComboBox.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpCBF4FE8555DE1D61528AF79EA3C5870F" Guid="{26599BF7-BCE2-4202-8B5A-F009B999646C}">\r
+                        <File Id="filC5D80E96EA90020D98A1BA9C7A32D089" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Container.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp63B837CFDE5F5C8ADC92D6A80E367621" Guid="{F67247A2-2EA5-450D-87C4-AED6C9A8E069}">\r
+                        <File Id="fil82056DDF3A22A3C956EB50AD96DE681B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Control.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp73FC7C1B9421F67443A7CA5E03C6F59C" Guid="{2225B539-878A-40DC-986D-742CFC262E18}">\r
+                        <File Id="fil00CD17A8DC26B3D4E21AA1AA951F3910" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\DelayButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp24D33989EF8148F7E8E801168FA75D02" Guid="{88F2FD96-6F60-460D-BBEC-F0B0BE0CE94B}">\r
+                        <File Id="fil0F8C8B52CEAB25AD4FD610A146431D1D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Dial.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp781F94BE6B67AAB9F2DD4A6865F67043" Guid="{12F45E61-A438-49AD-890E-BF5AF913BAED}">\r
+                        <File Id="fil9429984A3AD047F645C68743E4853D3E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Dialog.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpBF8FE1999E64BE53CD0FFE028F2BA8E5" Guid="{C825307D-6856-48E9-952F-D8DD4371E086}">\r
+                        <File Id="filB66714CD3CC8044DE03BFB0C2998771E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\DialogButtonBox.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp508D7E9DE20A057C80595708342CCD11" Guid="{79285B58-ED37-4B36-81A1-2F01EE9A8D30}">\r
+                        <File Id="fil4951F0E0C838BCE74B6A4FB412B33C39" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Drawer.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp814DB5A7174FAD3D492689B3CFFD4FC6" Guid="{C5568152-77D0-43EF-BB9D-0066F205825E}">\r
+                        <File Id="fil26FF980D9A1E103DAEF78DA89A8B39AE" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Frame.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp9FCA0D048C5FEDEF35CFDCF4220D148D" Guid="{552D678A-1984-4C88-89CF-42371097B8C7}">\r
+                        <File Id="fil7EC20FB2AF608FD4D48A9F19F1B92A7D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\GroupBox.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp8302E43F7B461A53125D24EBAB2C8BBA" Guid="{9908593A-2DF6-4B1B-844E-22E9D4AF8FC4}">\r
+                        <File Id="filA490553F561E23B426ED5F47B779E827" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\HorizontalHeaderView.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpDD4764A31C5966C382C9FADCF2747DAE" Guid="{A8C30115-60B3-4519-BCC1-668717555DB0}">\r
+                        <File Id="fil20BF35D7A8C7028231C16A4F8A8B8E7A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ItemDelegate.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp4DC6229A880E85A8EE64B69A175EB7F8" Guid="{59036E4B-ACD7-4733-B14A-EBAA93E714E8}">\r
+                        <File Id="filADF106B97E56C6627AF27930AA08B829" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Label.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpC3E4C72480B21745D1D55462CD128148" Guid="{B1B90609-C99E-4B1A-8B10-13BE83B179BB}">\r
+                        <File Id="filCDF998E5B0CD0C7D86DEDA98E71F235F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Menu.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp9C2C5E7D9EAC8AD7E9FB8BA9B0147BAB" Guid="{BB4200E2-ADED-44D8-A1A1-31A0E8707754}">\r
+                        <File Id="filA5709A532A6415760B623CBD689B3F88" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\MenuBar.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp66ADBF15C183C1EF05BA59EBF57DC04C" Guid="{592C458A-E1E3-4735-A22F-92B5FEECBA7F}">\r
+                        <File Id="fil9818915B5CDD8FDC7B9B723C438DDA64" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\MenuBarItem.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpB85DB993D3D58E717F4EC2A2A5AC4130" Guid="{604A7793-4EC9-4F5F-9BAF-DE22555BFF3A}">\r
+                        <File Id="filFAE639B09FB48AA6FC953557339B53EC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\MenuItem.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp93014FB27329F9718833E30A8DCE2F23" Guid="{EF1D6D21-BDC6-4DDF-9BC0-875EC3FAE40B}">\r
+                        <File Id="fil09A4B643E4AEAFC3F22479FBC0E09388" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\MenuSeparator.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp9D0CC60826A95D19D52A3A08FB44155C" Guid="{DFE9585E-6220-430F-9F1D-1B2130D88FF1}">\r
+                        <File Id="filDFFEC8756E4A921EAD31C7CD841BAC7B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Page.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp06BD88C78B72DD375F0FD8DC37812A7A" Guid="{9DE25AD5-FA94-4DFB-B129-0FFBF312E353}">\r
+                        <File Id="filDC61FDA5B2042E021FC28DFA1C464ED4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\PageIndicator.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp6577B61F7B803092F263CE9BD76D7B19" Guid="{D6D9670E-CD58-40D4-B772-C8AC087E74B3}">\r
+                        <File Id="filD74C331BEAD78D4B70AE295DE481F56E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Pane.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpD880E6EDC6A23CDFF94AB5E0A0018A10" Guid="{A3989AE9-2FD6-470A-B8CF-78D908DBA2E6}">\r
+                        <File Id="filA2017FDECC21B1BD1F3A42BBA6781F78" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmp00D081BB0762FEC39933D1C2960FC831" Guid="{ED79D960-6E3D-4B98-91D1-376FFD6D0C55}">\r
+                        <File Id="fil3BF8126A47717D54F2FC8B0E9EE88747" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Popup.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpD69A2401CBD6D26E44BE1271C8905A24" Guid="{0231D127-E285-4F07-8AB2-AB26F8850A75}">\r
+                        <File Id="fil7827CED2892E4BD2A7216EE8DC487134" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ProgressBar.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp0B7DD63BE81BC1EA8DB09A99C12CB156" Guid="{A7BE0958-70D3-4C14-82E3-FD911E5B2EAF}">\r
+                        <File Id="filBF429532FD2FB957D749DBFBEC0C1781" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmp1EC09C5C4D6CE477EAA2BE5A3798E1CC" Guid="{BA7E711D-1F6E-43CA-9957-C06AF625F0F8}">\r
+                        <File Id="fil8A3E57CDCB38EA2957526857F4BF7833" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\qtquickcontrols2plugin.dll" />\r
+                    </Component>\r
+                    <Component Id="cmp577EBA2061AD5D464AC5B629EF1A63DE" Guid="{248F67F3-A6F9-4619-B355-BB41E97401D1}">\r
+                        <File Id="fil2CF3A6D6E008AC1C946D645F7484E0AA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\RadioButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpFD9033FF82B141429428B759027C0ECB" Guid="{E0382F5D-D17D-4955-81ED-046C720C45AE}">\r
+                        <File Id="filBBA1B0E513D4C6E93929431510FFC51E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\RadioDelegate.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpD9A7A9076E4844ACEB4385E651C6FC27" Guid="{8B8F1FD8-F041-431E-BC04-94955681EC8B}">\r
+                        <File Id="fil92EBFA0D398D93C208D55E0F2DE5C412" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\RangeSlider.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp952D38FA5543D46E8B0B6CAC867E6D19" Guid="{6E73EE8C-FFF9-4D5E-80E4-49DCE2750DD8}">\r
+                        <File Id="fil3BC533F32E89455AE3C47B34F2D990BC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\RoundButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp8360F4A2F8333FCC0A95F36461DAC0AE" Guid="{6FEA34D3-39E4-4BC0-85DB-ECDF94B77477}">\r
+                        <File Id="filACE9F613C573BFC25A9A0E48B6C143C3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ScrollBar.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpA8C48D6004583C73A28027E805ECF241" Guid="{FB1C3A2B-9330-4EA7-A34B-25612A2C2E6A}">\r
+                        <File Id="fil7BD43F79B053411805FC22E0BD5D488F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ScrollIndicator.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpF0F52E0AA644EE5B8A4212507A2A9632" Guid="{40D7143A-FFDF-44AC-ACD4-00A6A5464BE7}">\r
+                        <File Id="fil89005ED22EF40F5C060D464B8096616E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ScrollView.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp1F2A598B5F572A95F619F914ADFDFD01" Guid="{3536777D-FF8B-4EEA-B93C-5EE1301E5039}">\r
+                        <File Id="fil1C7BF5B291EA31328091F13B845FED6A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Slider.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp894CC6997FDC49D32993AF049D19661B" Guid="{30F4723C-B159-4C29-9EA0-C8470CDC3204}">\r
+                        <File Id="fil298110AF9B2ED52166A7A0ACE2C9EEE9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\SpinBox.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp45A86D8F1E2A0DB9085F1C386BE8C441" Guid="{361C717A-E7E1-48E2-B5FD-E934ADF6C5F5}">\r
+                        <File Id="fil7E7C8E62F5F6317023CD5DDFBE502AA0" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\SplitView.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpD7F91C9BF1B37E7ECEFC803745680533" Guid="{2972CE14-8B1C-44AA-B8DE-80551FE96662}">\r
+                        <File Id="fil6479652BE44E448F123964A1BFFDC025" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\StackView.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp6C7380F6F9D3432EF0FF68155BE5B7EA" Guid="{D8A7D60B-63A0-4AD1-950D-055177620665}">\r
+                        <File Id="fil2616AA919B65C7E1223B8EEAE8A0A520" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\SwipeDelegate.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp163F0198648660C0C4DED70616CD3A50" Guid="{1FBF4796-E2E8-4ADB-A545-4BF2807D20B1}">\r
+                        <File Id="filCF410250FBBD03B7BDF88E86CDE7F5FA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\SwipeView.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpF5CA7A4D3DB71A67C3E4BC87D9A553DE" Guid="{81DD052B-A012-47F6-B62F-EA0E79AC7846}">\r
+                        <File Id="fil263B8E09CAABE937FBEBF1178E2C8264" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Switch.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp2599D94F81ED612C05479E9A8F58A189" Guid="{7DB969D4-59FE-4E04-8D11-E3BDEFB46738}">\r
+                        <File Id="filA4030299DEE7EEF863C5BDC4ADA0363B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\SwitchDelegate.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp9557C3464EC921CAC1A43D77B20BA92D" Guid="{DB31EB03-0A74-4002-B530-18B87D842D6D}">\r
+                        <File Id="fil13E41D7725EECC41EB76F1C55FB37034" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\TabBar.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpFDD06D370422F04997F37CDCF9077AAF" Guid="{8EAE7AAA-839E-4BD7-A7D2-135517DD05B0}">\r
+                        <File Id="filC1B3E77F09168E169AC46F312631A9A5" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\TabButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpC5C6ADF92BCA401BFD4AB6B8DF07ABE6" Guid="{E2A3B225-AEDA-4573-B8CD-F3F84AE9A8A0}">\r
+                        <File Id="fil8804492F60CD4E2F68C71219E02292EF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\TextArea.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpC7F75CEAF1EB837B6998ED5DC6550404" Guid="{01405B24-2CEB-418E-9B59-5F96A26C9D50}">\r
+                        <File Id="fil6F19FED3B4412AE010061DF7D4BD3DA0" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\TextField.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp4E0B2D6970453AF152BA060236BD8B71" Guid="{5ED9BA2B-AD36-4E01-9655-9EE6A765360C}">\r
+                        <File Id="fil340E1F0612644D4DC4EADFE9E87F24EF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ToolBar.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp3A32B8F13C49569A0A4AD1D11045BE3A" Guid="{A7D20B7C-B367-4014-9FDE-1DFBDCEB6C9B}">\r
+                        <File Id="filFBEB8E9016BF58F623173CDBF130428D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ToolButton.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp6D11E1D44ADD3049CE735F324F7F4521" Guid="{A972070C-BE88-401A-8BCC-8C6F4C4D9071}">\r
+                        <File Id="fil18978F440E9B6E6F62CE45EEC31C0F8F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ToolSeparator.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp6763D9C183B24CD6659B616DD72994DA" Guid="{BD007679-0CF6-49ED-B858-D57788EFEA1F}">\r
+                        <File Id="filB20E33BEBD71D625CEA0199980F77F55" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\ToolTip.qml" />\r
+                    </Component>\r
+                    <Component Id="cmp0FEA3716333AE56EBB72D727432DFB0A" Guid="{1BEC8363-459E-4097-BCF8-724A52CCFD53}">\r
+                        <File Id="filC8F8B2D82FDC1ED4350CA76D702241C7" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Tumbler.qml" />\r
+                    </Component>\r
+                    <Component Id="cmpF77FF7EB150083CBBB82946EC3F849C5" Guid="{77DC42B7-660D-4EB2-9553-1A8F3A054CB6}">\r
+                        <File Id="filD758C10071B91117472250A6D6E5C07A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\VerticalHeaderView.qml" />\r
+                    </Component>\r
+                    <Directory Id="dir035355762BDDA6A88B77A84B585AD0EF" Name="Fusion">\r
+                        <Component Id="cmp8693A45A9517FDC707611CD9592AD887" Guid="{FE1A6B72-4416-4058-B8DE-FD0ADA5E00AB}">\r
+                            <File Id="fil7A76573ABBE1FDF6842F852511E36ED4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ApplicationWindow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp22009CC98E7BCD73739238DBAD70F5FF" Guid="{EE86CB3D-757B-4591-8A4A-71DFB47EEC98}">\r
+                            <File Id="fil4D6655B7651D7B655CDC465A96569C20" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\BusyIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp006248D9C946635CD2C0D21D711BAF97" Guid="{71322A32-8BC1-4BC3-A2D3-43877F86E8E4}">\r
+                            <File Id="fil4677B542408030BF8A7E55B148B934EA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Button.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8F4980EC013CDADC9677179C1B5FB4DB" Guid="{9C60553B-BC7A-4E02-A56A-58833FC75143}">\r
+                            <File Id="filF44FA16699EA690B580F758E29738AAA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ButtonPanel.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp7CB5E11B511D00866734F2A59242071E" Guid="{AE591101-9987-4988-BB53-276AC7E46073}">\r
+                            <File Id="fil3CF03534ACD9304F06C0597102C48388" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\CheckBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp15339C0E193CBE17FB6CB89DA971471D" Guid="{E5A1CF93-FF79-4BFF-9429-876C326CD587}">\r
+                            <File Id="filB933AB67B806FBFAEE5B4C6EEA24FD2A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\CheckDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp7A634DCF2DA1EDFF9FBAF699758570C3" Guid="{E8EFCE73-1AB6-466D-AD08-C8873EF95506}">\r
+                            <File Id="fil3FDFED2EAAAED2687815DA9661B0C595" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\CheckIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpAA64F717D23C1375E79BE28E5A3DE005" Guid="{EF2F8A2E-4BBF-401E-A1B7-041B7329F812}">\r
+                            <File Id="fil95F73162924608F461FCFA52B66A0A6E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ComboBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp14F754092A68F3FE755963981761F031" Guid="{E0E26331-F0B7-4DFF-89E5-8894EF16DAC2}">\r
+                            <File Id="fil96C211B2F831A30A5AD62EDC6CB0AE86" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\DelayButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8DBD1F81C17E5D01D819E619D30E83CA" Guid="{242B321E-98A7-4685-84E9-8C8BD6CAFC92}">\r
+                            <File Id="fil1CC8736C80CF69BDCE8C8B989F215658" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Dial.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9A97912EFCCFD956EF8F155A81CB494F" Guid="{D2C607E0-8E4B-4F55-B60B-024D8BC34882}">\r
+                            <File Id="fil9912627C86943446F991D141E64E84A3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Dialog.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp37BA96169B5087F3D2C6E769FF5E9270" Guid="{B5153F7B-2A7E-4FB2-B7EA-C68D043B4F76}">\r
+                            <File Id="fil9D217918F4DE6A7DE53939E61C810E92" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\DialogButtonBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE74780CCB3558FB4C37DDE65264DC1F4" Guid="{4F45062D-232A-4157-ABD7-9351742C3994}">\r
+                            <File Id="fil404168979C602A6C3E2A8E77521843CF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Drawer.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp86D5D0E9BB2DE2BDCC6CF459CB7C4E01" Guid="{6FA92936-097C-41B0-A5C2-E7DC100A7908}">\r
+                            <File Id="filCFFF61300EF689F96CFDD9B3A8F7B83D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Frame.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp3951161EB256315C232D7C2B211A5D42" Guid="{FD64BFEF-8970-459F-843E-9D4E07A8DEBD}">\r
+                            <File Id="filAC8A24BE65028BFAF11984F7DB0B168C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\GroupBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6CAF0B6CD8ABE85E62A160036392A2CC" Guid="{88F69974-2FD0-4D7C-8E3D-36CD596B9459}">\r
+                            <File Id="filB0D55ECBE8B840F45E54296721149C79" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\HorizontalHeaderView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp55E217C43921E96FF82040712469E0F3" Guid="{D471DAE1-7C90-4202-91FB-0BE5C9E77506}">\r
+                            <File Id="fil2D7C328BD2280D7E0E3ABE5468F4CEDD" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ItemDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp57277D9EB23963010750499ED2F541B8" Guid="{1096E01B-7229-42C2-A357-E3C45DA54168}">\r
+                            <File Id="fil3FAB3A32AA7A668FFB5482A34769E564" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Label.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp651F7330940689374F3F4500C1F20484" Guid="{0D729967-F2B6-44F7-85D7-461595625C6E}">\r
+                            <File Id="fil2E6E1F91E7A60FB5D16E4C853B247E7A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Menu.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp02C5C41CDED7F94E885A4A2A28EC3600" Guid="{F4E65E26-2552-46BA-AF61-C547429D8F01}">\r
+                            <File Id="fil46C51416AFB762FFEE81E338F1212F54" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\MenuBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp95C32716B4F28A90B070E08CAC81BAC4" Guid="{7B371B0C-5AD7-4E93-B5E1-173CC26FD81C}">\r
+                            <File Id="filDA216EA9FC2E71295498C5082FABF4AF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\MenuBarItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp2CEB59611FF3F33F7AEF7D2EAAE0CB05" Guid="{2F5B5AEE-CA5D-4579-AE12-C90238707035}">\r
+                            <File Id="filCA0597907941481E694D27A9151A67F3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\MenuItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8D1031BFABA660F80B7793BB3761EB75" Guid="{E7361D28-855B-4961-8AA5-71A3E6BD7F08}">\r
+                            <File Id="filD9334919AAFD0FAA024307105F6E2274" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\MenuSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB52CD2CEE9160F5F67372170543F85B4" Guid="{3D125A08-8FBF-4F5F-8632-A08FD9F18975}">\r
+                            <File Id="fil549E037AEFA7D53E8ABEFA528585B365" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Page.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp909217E4E53799CCE36ED3F7B817DF15" Guid="{7CDFBC69-1743-4B4D-9D6D-CC727453AB80}">\r
+                            <File Id="fil622B076063785FD360184B7CCE9C1963" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\PageIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp88C4BDBD3D48F447FF34FD6DF1FC3C41" Guid="{8249DEF8-38C4-4F19-974E-6E455FB45D9F}">\r
+                            <File Id="fil258E3E262CBA06FD489EBCE314036F9D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Pane.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6D134750A96C36CCE37CDAEAC146A810" Guid="{AC624DFB-C752-43D2-B643-A18625D081A8}">\r
+                            <File Id="fil1872A6AD9CAD77362D7D04C024F76B25" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\plugins.qmltypes" />\r
+                        </Component>\r
+                        <Component Id="cmp17431F800533AAD492A8DA91C537AEE5" Guid="{CC784F75-F543-4123-9E10-3097581F6EE6}">\r
+                            <File Id="fil5955CD9FB6B733D9B586B52CCD59F161" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Popup.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5651D28BAA4D7517AB64D608440B8947" Guid="{0E1DA31F-123B-411E-A310-910106CF7103}">\r
+                            <File Id="fil1F6E4572AA5C15F6F2F86B70AD845F39" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ProgressBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1FC8B77EF51F392FB0F6CD269098431D" Guid="{921782D1-C60C-4012-B36C-D44FBFB1A9A1}">\r
+                            <File Id="filDEC4823160DF80F031FFEDD16C5FA73B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\qmldir" />\r
+                        </Component>\r
+                        <Component Id="cmp403E7038C7324A61D8727DC1D939E547" Guid="{F851CBDB-DEF2-4DA0-AB32-F038CA4D94F8}">\r
+                            <File Id="filD279E9A3D628C42CFDEC99CA9E5A2F3D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\qtquickcontrols2fusionstyleplugin.dll" />\r
+                        </Component>\r
+                        <Component Id="cmp98D46B3DC15B713ACF5F53B34EE13068" Guid="{8C570892-F30F-4CB2-AC3A-47382EC6B368}">\r
+                            <File Id="fil50FF1AE0E3D989A4CA249AAF9F8A7B26" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\RadioButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp82998B9C0403FF9F31583386A05AA9BB" Guid="{74DB260E-F5DF-4426-BDFE-14F1B954C661}">\r
+                            <File Id="fil1A72CA8DAF4CE929C79B9574D2F30382" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\RadioDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0D0B1F4459593A8B3D4A1BCBA139D98F" Guid="{FA5F9B73-E11E-4A44-8F7F-1C51E7F48E5D}">\r
+                            <File Id="fil46018A40ED31116940522AF13C6F1FBA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\RadioIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF083A0EDD7D82B7EEFA116A49700462C" Guid="{809F9508-ED4F-401C-89F7-D01F181560F7}">\r
+                            <File Id="fil3E28D7DC0567E0CD67D51AB0878E1837" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\RangeSlider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp094723DF4DF9079CABB07B397F53FC14" Guid="{75821D85-CF6B-464F-90B3-72300BC5245F}">\r
+                            <File Id="fil0C857F8E3C8E3F5D9E839C9D1A2D1BE6" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\RoundButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE46F1304BE1F25953FA1502338B46B6E" Guid="{E57FB784-7FCA-4108-8A90-43C9DAC92E74}">\r
+                            <File Id="fil0A615F671CBAFA25B83B6DDC1A57BE65" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ScrollBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF34990BAAB80CEB1B1FBA3F75A8BB18E" Guid="{BA75016B-BCEE-4639-AC14-960BBBA684CC}">\r
+                            <File Id="fil4926BC97FC5BF303193B003042C8161D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ScrollIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF8164C6D715D603507F60EA7662104BB" Guid="{3DF8922C-C3E9-49D8-A369-F8D21BFB1D94}">\r
+                            <File Id="filBAF585899E98CD52881FF18EA3FB54F6" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Slider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpBC49C3C8C0D1C882B8E4BFA5DDB84097" Guid="{7A79B435-9227-413C-9444-137D88C7B937}">\r
+                            <File Id="filF5CA1B27FCBF261A45FC19EA4D7BA023" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SliderGroove.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp026A749908FF7AE58055B0E1AB393168" Guid="{DE24E255-176E-4087-AFAC-E6F56B5D5EAC}">\r
+                            <File Id="fil929EAB5C19264E6900242F51F02822E8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SliderHandle.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4644F2A5A8ADFEA7ED73D09A41CB9AFF" Guid="{2A5E1174-6741-4BCF-BBA3-CE6FE52F79AC}">\r
+                            <File Id="filBA9EEEC5C7078CFE9E1B0DF8A177B5ED" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SpinBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF0E7DBBAED60C3A0D5E4980C6384C524" Guid="{5DC166C0-8A39-44D0-A9B8-FF474AC6EBDA}">\r
+                            <File Id="fil15BD010BEC93BDA46E1F2686D8FF6EBC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SplitView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF26383485D9640683A7276A7CFD06280" Guid="{BF3FBD86-AD76-426B-A685-CDF8FE76CC5E}">\r
+                            <File Id="fil55D360EE1ABEF5F5BD900B65B188D41F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SwipeDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5D85AEBC11476CF382025D6654F36103" Guid="{029CDF92-C9FE-44CF-B9B6-E4A1673AB30A}">\r
+                            <File Id="fil3EDB2B2C23697DAF9F92FFBA99A84650" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Switch.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp44002125F963B5A1D193E0D787FF6AF6" Guid="{04725E3F-4601-427F-9A74-36F09BC8A604}">\r
+                            <File Id="fil50318A9E962C21697CA18156BA2D0905" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SwitchDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF8E1020B8AEA4B8F647DF3962BB9CF2C" Guid="{FA2434F7-F3ED-4B26-A434-58E112C46E31}">\r
+                            <File Id="fil921E1EA2463721903BCAD40702BF03D1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\SwitchIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6F5B3D19FBA609A3ECE0700E2DB1C805" Guid="{9F1F0447-B481-4AB2-96BF-259633CF6889}">\r
+                            <File Id="fil8B653517ED9597AB20ECDD5E3A75C756" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\TabBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF0883486B5B318E646462C05EB5DBF23" Guid="{E121C66B-7244-4EAB-8225-A363D938EB07}">\r
+                            <File Id="fil17982E06A90DCC3F94109971D657D754" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\TabButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8F7277DF6FE75EF54431CCAB073199B5" Guid="{D75EED9E-1240-47D8-9DC5-5F5832E061F9}">\r
+                            <File Id="filE3100069BCDAE1F6526502196EFBED22" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\TextArea.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpBC3A73EE4B192F1BF88AF2E61E4212EA" Guid="{B137AF9C-F8CC-446F-B263-2DD5A818E699}">\r
+                            <File Id="fil3563463C4219FC7B6DFE6A95F3E9B75A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\TextField.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD48A473B45218C0BDE5C64912EFCBAFC" Guid="{2106F73A-F03F-4BEA-A2B6-D8B6C8632E15}">\r
+                            <File Id="filBA1284C10E3F10A1ECC1A687F9997CB4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ToolBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp86201E518FC2273E24BCD76E296C18E1" Guid="{1027D379-4165-4AC8-B902-6ADAF098ABEB}">\r
+                            <File Id="fil8B324D3244F760FD93C00D0E58BAB57B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ToolButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpFF8739D6C4BEC98E8F1240FFBD0A64B0" Guid="{AAD9D3FB-1258-4397-8DB0-703866C2D3CF}">\r
+                            <File Id="fil47FA961107A0E38C1F9CC57AAFD5CC5C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ToolSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp38720475F2109CBC7EF4F43C365CC5CA" Guid="{BCF56568-1D7B-40A5-ADCE-DD4376A42888}">\r
+                            <File Id="filB001EB30F35E5F650A63D03CC18ECE35" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\ToolTip.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp996D8C56417E10EF44329F7339E474F2" Guid="{1C26B967-00BF-4D70-8844-0A80BB8FD464}">\r
+                            <File Id="fil93134119EC59B8D442C2BE7A07DD5D2B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\Tumbler.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpED22A8F3ACEE04AB6CE44D533F4F300A" Guid="{72F16F90-779D-465A-892E-3B55A338F3AE}">\r
+                            <File Id="filE8C20555ACA351E1325A91CF709D8D00" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Fusion\VerticalHeaderView.qml" />\r
+                        </Component>\r
+                    </Directory>\r
+                    <Directory Id="dirFDC25EE37CF6C040EF6B98E257279EC3" Name="Imagine">\r
+                        <Component Id="cmp838B8853704ACB2A7F45BA00B950F3BB" Guid="{141E5A2D-23F0-4522-864B-D3456E2280FD}">\r
+                            <File Id="filC7285D9D5529D85E5E0E217C2D16EA81" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ApplicationWindow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDF9CC0BB490A823FFADEFFBDAB92C2C7" Guid="{5360026C-8AA3-4FE8-AF6A-CABD86B2B70F}">\r
+                            <File Id="fil9835BFE6BBC02870FA9F628A2E0DB6E4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\BusyIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp73DF7086A467801478C7321A8B435AA2" Guid="{37150093-DBE6-4270-81A5-8D100EB60F79}">\r
+                            <File Id="fil5DA765828D51355DC019424A172FB9C1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Button.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp10D7AC86A56DE2D04356715F35250435" Guid="{4371D289-F3B2-4B73-9E56-9903FA7DCFE5}">\r
+                            <File Id="fil9CB23D5FD7586E7DA94F805DF1529360" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\CheckBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB8BBD8CCBCDE8771AD9E3D6D456FDD6B" Guid="{744F1464-6E9C-4793-981C-73CB5510C194}">\r
+                            <File Id="filC24A1ADF38694A93849B2804D4415C22" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\CheckDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5DA5D6BC1A869E7409CC08ACDEBAA08F" Guid="{6769AE38-D978-44BD-A8C5-113A1244E476}">\r
+                            <File Id="fil3F2D3CBED068F4115DE0002AAC205A4F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ComboBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEFE26691A6CBD74691FA18F89319520B" Guid="{50DC9AD6-A6CB-47B6-85DC-1C319A7F6F27}">\r
+                            <File Id="fil1164CEFD82A18A964BA86D08201F9CE8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\DelayButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF14CA7FF23A8FEBB250D9C1A4DBAB5F2" Guid="{6102C842-3167-40B0-A897-7A562F821FA3}">\r
+                            <File Id="fil535F2A996B2C4FC84039F35960BC8B8C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Dial.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp76B901918023AFA1ECF5AFE1C5AADFD6" Guid="{B925E4BD-BDF8-419D-A21A-C4F593D261B6}">\r
+                            <File Id="fil2A26AB8828D225E79420414A7C55B95B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Dialog.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp3B04385170209415EA0392D6C6D9394D" Guid="{FFC78D64-1508-495B-A0E2-0A2290A40E05}">\r
+                            <File Id="filBB42C691204A19DFDDA38926D9A52E56" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\DialogButtonBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpAC7FCF48DC2DD2FF598D8D8EDFBDDE10" Guid="{68D0C46D-1AB5-4CA5-85E0-2175ADEC7360}">\r
+                            <File Id="fil152D23A298E533A4987600002CF20A9E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Drawer.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp31295E08BF0ADC9F7414B879A59EAB38" Guid="{EA9C98FB-2D3F-4970-95A7-2CFE7B8C3FCE}">\r
+                            <File Id="fil652FE91BEC083A5CA0F16DAC5115ADD1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Frame.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp26ED7398D2B7FD7CE79B20C6F81AA21B" Guid="{C1F1452B-9C22-49DA-AEEC-466D9615DB99}">\r
+                            <File Id="fil4E530D55EE5754394244F29DDD5A4339" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\GroupBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0AF138C48EF9389FB53BCFFF127C63B7" Guid="{99C63B6F-E80E-489E-B320-33CB9324A31C}">\r
+                            <File Id="fil7623E549EBD32B153F482A9ED3D10247" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\HorizontalHeaderView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB84DF029169B6913288463C0CD791FD7" Guid="{0982DCB5-B9A0-4CEB-9E86-DB3A55620327}">\r
+                            <File Id="fil47584D2EBEBF080D3E283A08BD78771C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ItemDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB2768FB92A037D37B0EDB0A50917DB6E" Guid="{C49D785A-76CF-4A89-8FF5-67F8DDA0A0D8}">\r
+                            <File Id="fil85E8BEE2FF4E7D88271DEE95F4B021F6" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Label.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp60DE9C3DB4C8AE187B4D19BDEDC6C16A" Guid="{A7D686DC-8D8F-4482-96F2-6D0C4F5BCFF0}">\r
+                            <File Id="fil21654FC5F7960A777690B781D204AE5F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Menu.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8439239F2F45A8EAE3D84F080943DDD1" Guid="{048C9830-5BC6-4C43-8E25-3C6B349700F9}">\r
+                            <File Id="filB0FB084CE7E619D3AD1FCFC01826933F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\MenuItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp16223EE59A9E78D62203D037E29D698D" Guid="{7B56130B-F94B-4A3A-B065-E6E4B66A9C20}">\r
+                            <File Id="filF1296202126A8FC1A5FF68833D23C1DD" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\MenuSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp018AFFA2C882E3A63FE78C48058C9701" Guid="{76589C5E-289E-46A5-A080-04A8D1FEC084}">\r
+                            <File Id="fil16A10C6B3AC90B78F38FF79B36A1D633" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Page.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD9344D05ADD481EACDED9FFB5EE9D160" Guid="{CF6DB635-DAE3-4E6C-8A89-3F5145779366}">\r
+                            <File Id="fil1D2EF351CF8F0C59F0CF7CD0CF748B0E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\PageIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA9175F4256E542F0F7D7729B65AC8716" Guid="{1B82A4FA-CC2D-45D0-8871-6FDF98408E5C}">\r
+                            <File Id="fil6ED79940CE9EDFACD1AA339B928CD411" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Pane.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp022C885B9AF26895C6BBDBE1AF964DA7" Guid="{D7776A87-CE99-4B5B-BDD0-A6ACC91624F5}">\r
+                            <File Id="filD12EF181236C4FD2D8823E2A06702BBC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\plugins.qmltypes" />\r
+                        </Component>\r
+                        <Component Id="cmpB42CC102A79539E49D9D71AF819BDCFA" Guid="{FB36BE43-4D52-4433-A6B5-F2915F1CBA12}">\r
+                            <File Id="fil9D3116A765C1FFBA2E5F4AF29524555B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Popup.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDE27E4C6B7C8141F7B15529523211BDE" Guid="{5EBA816D-DF14-4E34-A151-FA79A354623A}">\r
+                            <File Id="filC4C7A697145B9CE44EC9CBEE5AD5EB56" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ProgressBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8A1FE21942F63A12FE8C262291D2F118" Guid="{10CFD156-62EE-4E2A-AC14-CDE672E214D2}">\r
+                            <File Id="fil8D5351D7FEFC8E0D7B074BCD4BE7E93A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\qmldir" />\r
+                        </Component>\r
+                        <Component Id="cmp89930EDDF65A84FB647750A3889D3998" Guid="{242DF3DF-4813-4AD4-800B-4A6A7F1E00BA}">\r
+                            <File Id="filE4098FA13958E23AF469CE23DE03ADB7" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\qtquickcontrols2imaginestyleplugin.dll" />\r
+                        </Component>\r
+                        <Component Id="cmp7A7C588B21E8E3817983B40E6C15A8DC" Guid="{9E76F7E0-607B-468F-8E59-B2614AD30602}">\r
+                            <File Id="fil09BC6622E343777DE250D2F47E297ED1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\RadioButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpFAF5864C58442FAA1D5C0A01C83FACE9" Guid="{202BAC2C-95C9-467D-BEB4-1EA7E27E31B6}">\r
+                            <File Id="filB5EFC9A07D2D7FD06A43503D3A79870E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\RadioDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp48641EDC547835A4C015C138C9EE12D6" Guid="{EC581895-46A3-4CFE-AC75-5FEB7C9C2ACF}">\r
+                            <File Id="fil977DBB2F4A4B7DFED8BB0612A7037970" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\RangeSlider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp74D44A9BA04153CCD0A6FA413CA57C4B" Guid="{7166BA46-DB87-4DE6-9C1A-1EB28816178D}">\r
+                            <File Id="fil11ADFCA60637AB6A2C391D9EA6B3C543" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\RoundButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp84CD7E65AAF5D3AD9F421445901CF37D" Guid="{CEAB5076-3A76-4680-AC7F-379B2329E9D8}">\r
+                            <File Id="filD9610FFD3B120D5B16F73DC0CBCFFD2E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ScrollBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpAB0EEB7FDAB89DB33CC1872F2D020B9A" Guid="{21EE7B59-9A55-4660-8849-2BAC9C1651D3}">\r
+                            <File Id="filAE6B882E064E88D269983EB2BF4F7EFF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ScrollIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEA5C0DF0E7CC7FF0C0A116E8ED0DFD39" Guid="{4B3DB6DB-BDC4-400A-8559-032E988E2B47}">\r
+                            <File Id="fil0EB5F1D6A344D46C999C749965A81DF7" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Slider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp48F6BA7469D6C5E8A90EFDC2F1F12A03" Guid="{2F7713CC-29A6-4EC2-AA4B-E2E2970448EE}">\r
+                            <File Id="filE4BBEB9676EAAFD512614F5B243CD4F5" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\SpinBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEACB1D29768FC4AD7754B7A8BB5CEAA4" Guid="{D905B9C9-ADBE-4760-9853-EFD1EC7FBA99}">\r
+                            <File Id="fil2F0169274B3846093E9CE0E0CDA3914D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\SplitView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp40DFBE5C4004C66BD421833A0916A402" Guid="{03C419A3-AF46-4CA9-A7C3-8ECFCD57104A}">\r
+                            <File Id="fil8E7130A25B7C8446BC71EC968B0715CF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\StackView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD46A2953ED55E92D9E0462BE4D66B828" Guid="{FFD1F1D1-32CD-4E77-ACA3-580CD5596381}">\r
+                            <File Id="filCECBBB01A4F76F4742FDB628AB2398D7" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\SwipeDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9BBFEBDFBD798ED64CF1BFB852E57719" Guid="{45F72B4D-8EB6-4BE2-B31A-C9BF307DD2C8}">\r
+                            <File Id="fil3A23A94C3ED39F75C988138BFBEEA2AA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\SwipeView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD60B739F43AAC84324A66CEFFCDDBDFF" Guid="{E0F387DC-7D7F-432C-8EA9-9ED7F865D6CE}">\r
+                            <File Id="fil4751E171C8A0EFA145946D3D11DED958" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Switch.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4AF8FAB5F76A012B1E00956784FB86C0" Guid="{DC4C5366-E401-48CB-9A6E-BE7F33FA39E8}">\r
+                            <File Id="fil0AB6CCC6384F6C6B1A85535F2E435452" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\SwitchDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD3F2F08057724AFA25E4EBF3514406A6" Guid="{CB2E0B76-1F9A-4B9B-904E-FD373A79E56C}">\r
+                            <File Id="fil06EEC5BCC6DCFD868503648FEB22C2E2" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\TabBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5D978D2112EB628D93B78BBB779859CA" Guid="{2F43967B-733D-4C8C-A715-175288FDA3FD}">\r
+                            <File Id="fil23FF41EA1352977BF0D165A14A816D7F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\TabButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6800D8FB1FBB983412E92B80451A1109" Guid="{8109FFCD-D535-4EAD-BA10-8A16D8409AA0}">\r
+                            <File Id="fil9E0F7A4390203BCAA10D42088F80E582" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\TextArea.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp71287354FF8DCC602BB0D3D414E05962" Guid="{71FB3A16-2141-4948-89C3-73AB0B166A7E}">\r
+                            <File Id="fil220878B77DD5BA3F56A6D5C7A5EAD1EF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\TextField.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0DF5FCC7255520F9D24CAC298FA859D0" Guid="{ADFEC3CF-FD46-4295-96F6-30FDEBBFAB2A}">\r
+                            <File Id="fil87EA3DC0E8CE7A5D1B6D1980848D1E5C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ToolBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp3301D802CB29561A15AE015C15DD8543" Guid="{0CEB23DC-F5E8-403F-BFC6-02DAEC745718}">\r
+                            <File Id="fil5D5982165FA435D4FA833ABC64663AF4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ToolButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp00A4060669FB650FE92DDD9A09A857C0" Guid="{F095E727-9F5F-486D-B30C-406D1CE193FD}">\r
+                            <File Id="filD17CC03AD31A7AC8639971FE0D6C45A4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ToolSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp2D3FB42E796C744949D0480EE5C04892" Guid="{211FA71C-4714-49D9-AFFA-CA730B964213}">\r
+                            <File Id="filAA9B4A8954E641AD01238DA365D35C84" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\ToolTip.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp11B5D7ADB851C95DCFE5C71959222497" Guid="{6B13434F-4A6A-4874-95A6-C796BE931A62}">\r
+                            <File Id="filA16915B6759149D53E63B3A6662327D5" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\Tumbler.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp27BC211BA8D2261D578FC390503266AF" Guid="{30C94813-4571-479E-825B-10EDE44E2077}">\r
+                            <File Id="fil64AED8EAC8A4A27B2290235AC66A8987" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Imagine\VerticalHeaderView.qml" />\r
+                        </Component>\r
+                    </Directory>\r
+                    <Directory Id="dir98C439A324E49AE59FF9F7CAFC527818" Name="Material">\r
+                        <Component Id="cmp60AB973400004A31DBB4967567BB3780" Guid="{7D351719-339C-46E2-81CB-905AACB7BEC7}">\r
+                            <File Id="fil64C921AC09AAAAFB16510D0E258259FF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ApplicationWindow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp026D50205EC5D03323420AC1EC10FD89" Guid="{4B779DF7-4593-4A4D-AC2D-38DC4BF88B0B}">\r
+                            <File Id="filDB01E686286D4C2DEE2CFA3FD38FCB40" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\BoxShadow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE0BD5867C82F9D8554FECDDC4B6901C7" Guid="{852AD272-C64B-4C36-96B7-8664046E31AA}">\r
+                            <File Id="filE4A4C7B299CDCB670C96418B8EDDB5D8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\BusyIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9F4E4D4D0534820ED7B1592321BACF51" Guid="{A6935D04-49FA-49DF-82EC-BE62493947D5}">\r
+                            <File Id="fil01470810824935FDCE6313DD60621BF3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Button.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpCB85EA75A98576ACBC265DECF4F7697F" Guid="{12C6E0D2-3BFA-4BC6-8111-800F3AB9059D}">\r
+                            <File Id="fil1D098E5C4AA1CEC913EAFA6AEC902AB9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\CheckBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD8B043EAF161B1EED3C0D0E0E8B0717E" Guid="{C800976E-3768-42C1-9BE6-CC3B47528DC8}">\r
+                            <File Id="fil4E240E0B681A24079C96E7EE1B59BB81" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\CheckDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp006E05B629020152BA06266DCF586F94" Guid="{BCED27FF-F076-445C-A572-6A64493517E1}">\r
+                            <File Id="filB14F8F42CDE72A0055838F894912C34C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\CheckIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6390600916A9D80DEECC6B64C1A5734B" Guid="{A3D8818D-3D8F-4412-A219-82CAD7EA3A78}">\r
+                            <File Id="filAA1D779F78FFD687244475818E1E676D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ComboBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA029FDE3C079115EA43947112C138B8C" Guid="{79E6AF29-37B7-4067-89C7-C733024B28CF}">\r
+                            <File Id="filA865175498A738900D1E48E261E9A7A2" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\CursorDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp050C776C9A28886790788280CCEC3D91" Guid="{E6A61937-0614-478F-A2AE-986106471D6F}">\r
+                            <File Id="fil3A782A789161DA1643DAB87A06031CC9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\DelayButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpBFBC5C595627F87399C1078F9A522E9D" Guid="{B0F3C31E-D1FD-4C88-9603-9DC8E7359565}">\r
+                            <File Id="filAA2365440AA7A931BCC671A9A5ED2CC3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Dial.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp2B0856221A67BDF81C2238000B184A17" Guid="{F1026998-7BAD-4314-85CA-4B879412E77F}">\r
+                            <File Id="fil633492C397BA579319833DD3AAD2951E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Dialog.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp835605203F95C84205B718EDDDE2091E" Guid="{CC7B5354-0ED3-4872-9481-CD24218471F3}">\r
+                            <File Id="filFEB37EE6FA694858E6397E30CA14D53E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\DialogButtonBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD37BF99FBDB8EFD807188027A80B8DA3" Guid="{1C7EC37F-ED5A-4AF4-824B-78897568843C}">\r
+                            <File Id="fil9275FC5F45942812B0181C0AD249DA8D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Drawer.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp31DA9547A00549AAE408F3DADB011332" Guid="{F1E700ED-73BD-4B51-8D93-13745C69E2DC}">\r
+                            <File Id="filEE7349C912899BD32A59C5B164F24EEB" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ElevationEffect.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpC869C8092B8F2AAC631C286328891A94" Guid="{FDCDB596-6781-4C12-AD6E-BA12D276162E}">\r
+                            <File Id="filD25404A21A1FB54078D4A8BDD39139B9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Frame.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0305CAFC1FCE93ED480CC829AAD832AF" Guid="{A75B3F5F-8C84-4A5A-9D6C-B2D06223F0D5}">\r
+                            <File Id="filAFE4512E9BFCE1DBEA4D23E504D69326" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\GroupBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5D66D11EA79AD27A1AE641BF9C7CB9F2" Guid="{3CB05274-E6ED-4762-B185-AAD551464977}">\r
+                            <File Id="fil2F55E2A2B3B694DC0B8F749076B915FC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\HorizontalHeaderView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA817DF6EA38B1DF29BCE614C5450544B" Guid="{90C497D8-B166-45CF-A710-1DD2C5BFFA41}">\r
+                            <File Id="fil6DE65D9A2CA853FB7F9873AC257E7BF1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ItemDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpFB26590AF40355E4D807DA7009B7836D" Guid="{F9B22F54-D0AD-471D-B1EB-96E584134B22}">\r
+                            <File Id="fil677DE17071787C3A48E9C6FBAA519F5B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Label.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpFA168D7F4EFC52948B17DF862E07E25A" Guid="{C5F7BEEF-1B71-4D45-B859-4CA51121EA22}">\r
+                            <File Id="filEA122269BF1F9F0E5DE2853767AC894F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Menu.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp80B129BEE9AED15B717DBBC4E1C91B8D" Guid="{E64751DA-EE5A-4C99-9AD0-24FCDAA4D5CB}">\r
+                            <File Id="fil16F020BBAFC0D9DF9F1A85E68CE222C1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\MenuBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp222BD8179F2D4ECA9CD649598F1665FE" Guid="{B3D9F018-4A95-4520-911A-84862B132195}">\r
+                            <File Id="fil0BBF38951FC82CBE8636981293A2B84A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\MenuBarItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE9263BDE73548B76D4CF00FA421C6394" Guid="{00E8C1F0-E2DF-43CD-B437-E781866A6E5E}">\r
+                            <File Id="fil12E8E206331572B0EC4AD0A4EE328DC8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\MenuItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp665380529EE1C0ED1DE23E8B26850B19" Guid="{85D9EC2F-DABD-4B96-8B43-107FEC2268E8}">\r
+                            <File Id="fil3BCAD81F81A42FD7A740540891FC5A4B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\MenuSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8DD36362549E19ECEA88E3D1B1ACBB85" Guid="{44592660-851B-47BE-AEFA-34A0EC7F3289}">\r
+                            <File Id="fil615CC9ED7E0F011723181A7F99A61740" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Page.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD2EA9700B16C8836D5206D1185C549A0" Guid="{35B14087-6829-4BA2-97DE-4EB8F160A07E}">\r
+                            <File Id="filAD99EB039065C97EF184865D6503E4C1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\PageIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp73D2F27C7DDDD4CF7ADAEBF8E6E42EB1" Guid="{2EBCA9CE-E074-4C0E-BDA0-D9CFE4AA041B}">\r
+                            <File Id="filC2DFBF2CB10C8FCB3D1AF32F2A8D57DD" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Pane.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp7E31792D0EB10DD8AFD37842DFFCA924" Guid="{8A30E34E-392A-406A-8DCA-131248202236}">\r
+                            <File Id="fil1C6676EE896DF9A856E5FE3F9EFAC32C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\plugins.qmltypes" />\r
+                        </Component>\r
+                        <Component Id="cmpA158D23F2833A86832CC8AB04B51E8D0" Guid="{67B192AC-1AB5-4AD6-BFF4-71902B569AD2}">\r
+                            <File Id="fil70BCFDA53E4BE604641DD96490B3F5EE" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Popup.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA62BD30E70FBA79DED1CC038B0404EBC" Guid="{F121CC1F-A122-4191-88EC-836831DD39E9}">\r
+                            <File Id="fil900BC61782FD456E2C7E15DCBA8CAC47" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ProgressBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp78F57F7853BF2F83CA4C27E09AE1E09D" Guid="{5200AA4B-3F84-4020-B572-61028451737C}">\r
+                            <File Id="fil95B30068F5A41AD2EC7F7D95D514FB65" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\qmldir" />\r
+                        </Component>\r
+                        <Component Id="cmpBC000241C708266C86D302A435E66F1C" Guid="{5A4DA7CA-1BF1-4394-A52B-61834B951EC8}">\r
+                            <File Id="filA32CA6F1B47518CE771D5FD69B4BAE80" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RadioButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp3B8600643B709AD9AD113079EDB50C94" Guid="{B6B8BD52-4464-46A3-8A15-2D9DB2F451FA}">\r
+                            <File Id="filFDFEFD51A9E2DF0C75D2E48D4697846D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RadioDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB755C164EDFD73E1498268DB3DA880E5" Guid="{EEC2C85F-94A4-463C-99D2-0C94DDAB1731}">\r
+                            <File Id="fil438F051667D9B5E1DFEC7C42714E7AC5" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RadioIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1CBDAA564E02886155300F7A8AF049CF" Guid="{52FE64E7-76EA-4E72-8ABC-F72A6912CEDD}">\r
+                            <File Id="filEFF497CDD0E8BF7A1A284FE00DBAC232" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RangeSlider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpFAE3866F17FF7039CA9618739C64A1A5" Guid="{1D8BBF80-37C6-4356-981C-E84624F4D130}">\r
+                            <File Id="fil950F62018D52CBBA67D0D037CE077CAF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RectangularGlow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1BEDE14E6357E29206EDC740AE50650E" Guid="{3832C565-CF30-48A8-9F57-1F1203A9D094}">\r
+                            <File Id="fil7B1D1957A48A219B6EA1E51D1B9D5094" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\RoundButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9848706507698E3D2DE99B0C079EAA41" Guid="{F4159B1A-5628-4239-B834-139C4300EE69}">\r
+                            <File Id="fil7B7E8B589CFA5A08CD0BDCFEBBB37F6E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ScrollBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpAF75487C0AF2238D5F5AB27EE11B8C29" Guid="{CC851481-1F5C-40DF-B542-CF2331CFBDE2}">\r
+                            <File Id="fil6C7298CEC9325745BABD8553BD2C314E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ScrollIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF1D853F627859C5B31EEDA150FC58C6E" Guid="{1CCA22C1-4843-4405-84E6-1359524F55C0}">\r
+                            <File Id="filE2D55223B3943A1418684BC06ADC9873" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Slider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp24C324DDA2757C24A5D64657F10D1861" Guid="{036A1520-EE7A-49D0-AB3B-6FDA0821BF57}">\r
+                            <File Id="fil456E349BA3AA06452991F85C418012A0" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SliderHandle.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp24A094BA67CBF70909850C9D82D581FB" Guid="{D516F3DC-29FF-4823-9F3C-2F4DDC120705}">\r
+                            <File Id="filD62846BDB9F588D73844FBE505CDA20B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SpinBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp855A7D3DC1E9E7D07C156736FB529CB6" Guid="{322DCB01-BEB8-423B-B3CF-9260650724A0}">\r
+                            <File Id="filB7F5B7769F99CFB3EEF067B579C69441" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SplitView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF44D1A83846E86F2D4EFFEDC9DA14661" Guid="{A0B9F909-8889-4C14-BD04-3250095E9F92}">\r
+                            <File Id="fil2E77DB68C32D27DC45D541C2515A7DCB" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\StackView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp15902266961B205BB7C49E3F21B0A166" Guid="{D1B88C45-21E8-4C9F-96FF-EDE6CBCB383F}">\r
+                            <File Id="fil6E8DF910D77FBD6DC889D4B17EDB8C73" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SwipeDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp50966487B32C316D078827563E31DB30" Guid="{F008F394-1A7C-4F15-ADE3-4BAFF638F1AB}">\r
+                            <File Id="fil04105F1624C2D26E2FF0F79EEDB6FB6F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SwipeView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA1B9598A2B3B9C203B5780C91F7075D7" Guid="{A0B5114E-0A0C-44EF-BE1D-C9CB730A4EAA}">\r
+                            <File Id="fil8BD5245958D87E18B746A319B5EA03EB" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Switch.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEF587C49FDE4B67961EDEA007128D3DE" Guid="{FBEE1A46-7848-4E35-9344-6D6B062DD6A5}">\r
+                            <File Id="filE64764B9E04E5C2C205F98B593CE79AD" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SwitchDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp793DBF048FBD4343297F1E17756BBF6B" Guid="{8185D55C-9168-4D19-BEAC-34505A4D7106}">\r
+                            <File Id="fil7BF94FDDFE952066A6BF1FC7268E4C41" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\SwitchIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD98F61FC599E1BD66057249B28016A0E" Guid="{254D4BDB-E170-408F-B943-DFF5044C0519}">\r
+                            <File Id="fil35E18FE45A8EF14EF0AF02F9E3BB944D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\TabBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4C6EA3AFEB0324ED3875A46DCCB4EA48" Guid="{48E4E179-8433-4CED-A5C0-9A31EEE63415}">\r
+                            <File Id="filAA08F0B10CF6925991C628327DC46BB4" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\TabButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpA7F194C9D57330A5F72C74CD96507E48" Guid="{576229B2-14D1-4DAC-8482-31499B873B5E}">\r
+                            <File Id="fil5669F09C2045004CC25FD46B2565629E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\TextArea.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp337BDFFE231941F8A3E54BF4AC644B58" Guid="{F791A188-A943-454F-AEE1-FD797A3D801F}">\r
+                            <File Id="filE58155B28FABF154CD98B2C02EC12126" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\TextField.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0255A607D5334E94D517A2FEDDB2105D" Guid="{10D77816-5456-4FF9-B162-3DF69F8E56EA}">\r
+                            <File Id="fil9261EC814FA4325E3177494A396D8066" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ToolBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpBDEB6272B93E7E2FD1982012B5EF3D02" Guid="{7028EB68-4086-4009-BAD5-6D19E4D5EC9E}">\r
+                            <File Id="fil36BCCA29A63BD763D066F4FD320B6D05" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ToolButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6ACA30972AFD6D78448054BA4368AEE7" Guid="{8D2AD71A-31A2-4ED5-B3BD-43E761CB7142}">\r
+                            <File Id="fil21C810954616BBB037440CDD95167F6B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ToolSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0014F4AC9A48147F8EAB3F17B2C144E0" Guid="{DE7EB59D-A01E-453B-B333-887D374C00EB}">\r
+                            <File Id="filE2CD377318429F05238B017CA75F21B1" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\ToolTip.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB1ABCFD82FD1858E7657C34A8104404C" Guid="{8F3123BF-9523-47FF-ABE9-2B9B3F5D0181}">\r
+                            <File Id="fil98EBD20C2BD6799247161723C27818F9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\Tumbler.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6A432A70B05CC4AC38DA38F776B97812" Guid="{4E8C826F-1AAA-4063-9CAE-38614D103CD6}">\r
+                            <File Id="filB345996D2977939507887C6FB257738B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Material\VerticalHeaderView.qml" />\r
+                        </Component>\r
+                    </Directory>\r
+                    <Directory Id="dir8677A4B366B4AB433BB4754EEBBBE2EA" Name="Universal">\r
+                        <Component Id="cmp134038F5FD44A08B4DA0CFD8921DFB42" Guid="{1B4AC1E8-E146-4872-A0B9-90AAF7A63406}">\r
+                            <File Id="filEE54942B1FE1F7E0FE0DA9372D63E2E9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ApplicationWindow.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp34FB0E37566676B69EEEB98F670D23C4" Guid="{09BBA204-8172-4676-B116-B7249D1FB914}">\r
+                            <File Id="filF048A4DBF3EE1F42AB0045720B3A1275" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\BusyIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp166A7C72CA2D82533EBF24FB87FE4BC5" Guid="{5D75BCE9-3E98-4292-BC43-C72DA6C7B3FD}">\r
+                            <File Id="fil9FC3621AAA61D06486256ACFCEDEFA58" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Button.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp11D7406D35C2641BC20018F1FA8ADC45" Guid="{50AC31B7-B2E4-492F-97B4-7125DFA06DCE}">\r
+                            <File Id="fil4CC941157F98FD9DCC1CD03DB1383D81" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\CheckBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp6CA80FCA9CDD4C55F2582BF139AA4B1B" Guid="{C9EF0708-5E19-48B7-BD71-120ACD0FA185}">\r
+                            <File Id="filAA2312432C29EB3BE6C44D5CB9E552C0" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\CheckDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDFBFE88B708CE2CA5904473FE0CDB861" Guid="{0135FF85-D1FF-4305-9E95-CDFA4427F376}">\r
+                            <File Id="fil0D3BF90DE4E4AC4108503B2DB286E7F6" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\CheckIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp8D47A9ABEDE81A100C924FF154224483" Guid="{AD996097-10A0-48E2-AE3F-2A31A8B99BC7}">\r
+                            <File Id="fil3445161ADB5A36CCA4D8DEE8401F79D9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ComboBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0071C7F9D6D4B5DC3218D487DABFC218" Guid="{85A7872B-F79B-43E1-97CC-B30383C2846A}">\r
+                            <File Id="fil1B3A6163FC6D639F2BD22118F8DA406F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\DelayButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp54B17CC907A926962454D9FDEA99E369" Guid="{4492FD9C-0227-4592-931B-1CE255A56335}">\r
+                            <File Id="fil1E9230BCC1B46DE57D1B254EB036939B" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Dial.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDF321BF03AC6D81897FFD4B64045DE88" Guid="{76EBA34B-E864-4B0F-9382-2786ABA98828}">\r
+                            <File Id="fil64F4012F0ABC224742C0420695795D29" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Dialog.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEBBBCD3B40ADAF40F5F7BBDAD1628BAB" Guid="{3CEB3BEF-B036-491B-8156-71DFCF99AEEA}">\r
+                            <File Id="fil1AD8E7E159B82A10473C11ACD1622A84" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\DialogButtonBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4781FFA691E6B55670D19CCA10E68D4A" Guid="{D15EEBBD-53D6-4922-AA40-39A8594785FA}">\r
+                            <File Id="fil4E217CB94A8B78C9D074F1BAEBB7A41C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Drawer.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9D75099F72BC1EA408C69BAE1036713A" Guid="{5F48BA1E-8F54-465F-8738-CD28295DD983}">\r
+                            <File Id="fil9D815A989044908A0CB2481100231444" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Frame.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD507403E3878D92F029FB454CD349781" Guid="{A5180010-062C-494E-82C5-2A7804D7118A}">\r
+                            <File Id="filFA88E95F2ACC7A5522DCF4C107B83B7A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\GroupBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDB50F59C1041F3C78FB11F782D92A59C" Guid="{DC08DC21-9312-4C4D-BF3A-C43E1A675FB1}">\r
+                            <File Id="fil911A2600E1379E8C5A85199969DB74CF" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\HorizontalHeaderView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp9ACED442699FD1B84B592FD9A678FF1F" Guid="{9D19D01E-C44E-4FC6-AAD5-6709B0298D2B}">\r
+                            <File Id="filB5F9B6495241535B1200DA731AF5A5F9" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ItemDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpC959D8B9C731BB4CB168C53232754164" Guid="{AE31E225-63D5-46EC-BFF9-01D536DE1235}">\r
+                            <File Id="filE924AE836BA5C65F25B0FF78B6A2425C" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Label.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF036082D9E70D219B49993BF2078FC9B" Guid="{740E5BA2-9846-417D-81D1-C4F6AB3049A7}">\r
+                            <File Id="fil0FE051A3F7254F4C87AB2787BB8B9195" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Menu.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp610C62A18FB0CBD4A23E6420089991A6" Guid="{D53BB533-7392-4096-8630-CA6D234AC952}">\r
+                            <File Id="fil0078083DF303E6033ED65F84CCCDA8E0" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\MenuBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF0A89D556198AEC41AA931DE764420C9" Guid="{2478D487-C1B4-498E-A15E-8AFC8EBDD54F}">\r
+                            <File Id="fil141BCF473057F39197B9662FCF606CC8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\MenuBarItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1FB4C766D7DFBDC29927513C52FF444C" Guid="{07AC968A-CAD2-4435-B348-325CF309AF6D}">\r
+                            <File Id="fil1E9D45ADE39F5FB611AB5AAB33294D3D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\MenuItem.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpAB3EF735C77E4B6501B74C3070F17E8D" Guid="{9C5FC5C2-834C-48C0-980E-2D5179A53773}">\r
+                            <File Id="filD6ADE8C35F973E505931BD4F09EF0A78" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\MenuSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpEF370FB1E0BE2F7C44ACA77A0B9A380F" Guid="{FEE2D791-0609-42CC-9183-81B0FCFB3AAF}">\r
+                            <File Id="filBEEE5C63314DE5A1C43E6865925031F3" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Page.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpD53F72AC172BCE3F947CB966768B983E" Guid="{55F0B5A9-6568-4369-BEC8-9CE9FAB4C886}">\r
+                            <File Id="filD257FB636047A1ABDD8322D56BF83DED" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\PageIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB58DE0208168537FC63A28BA40094D3E" Guid="{DDA681AA-E7A3-4935-B6F3-A26C591C0F3D}">\r
+                            <File Id="filF940749E1F1250D0E5E454F677E4811A" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Pane.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1E24968394165967A4C933056CC97E8B" Guid="{BCC5C83A-1795-4E9A-9F3B-9EF43B56B4E2}">\r
+                            <File Id="filBBCB3725136BF248504E6130E398CC37" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\plugins.qmltypes" />\r
+                        </Component>\r
+                        <Component Id="cmpA780D5122E140F762FC4C3A0D02E7179" Guid="{8898C5E7-1BF7-40C3-A46D-F097D6A2110A}">\r
+                            <File Id="fil9FC53D09D020635EA1B1AC7DAB106FEB" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Popup.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE61937584021054E6A0A34AFBB281937" Guid="{6937E766-8BD5-415D-B76A-F464FEF6E71B}">\r
+                            <File Id="filA8BB46F9B5116CF916C7B675148EFC4E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ProgressBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB2D199D19AF00559ED6198A864F4874E" Guid="{DED65452-7CC9-485D-807E-43904CBA7FEA}">\r
+                            <File Id="filFB7A404E13EF7802DE1DDCBEBDD42A2E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\qmldir" />\r
+                        </Component>\r
+                        <Component Id="cmpA29A335EDA4A585067146163B51D3643" Guid="{86C75BC9-682C-49B2-A48D-B18AA904AAF9}">\r
+                            <File Id="fil23BEBDFB38598BCC408151AC6B584604" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\qtquickcontrols2universalstyleplugin.dll" />\r
+                        </Component>\r
+                        <Component Id="cmp4F8C12B67EDC519F78AA4840C85494D1" Guid="{4F5352F8-AA69-45F8-B461-8FB3CCEDA086}">\r
+                            <File Id="fil2FF1D14D6D70E386EAEAFC412E469DBA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\RadioButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp30F05444D83E8DAFE75CAAB16F50E3AD" Guid="{A4922AB5-4A0B-4CDE-A60E-6B88921844AB}">\r
+                            <File Id="fil5904A6F1649189A762EE1680E07001FA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\RadioDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp7573B19F4A56CF2B1D359A87D25A39B4" Guid="{2B71C5A1-2DEA-42CF-B0B5-8856B0B47752}">\r
+                            <File Id="fil0FDD8C1A5AF94ACD8822A21F4762C124" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\RadioIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp0B698F624CC72E780FB1FCCBE6340AAD" Guid="{F91E47DD-8607-4DBE-938B-D67E14187DD0}">\r
+                            <File Id="fil8EB7C1B7193D2DDA9E532A1B16502A73" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\RangeSlider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp898F4E8E360375D6D8FE6F606F899441" Guid="{EF555FB1-D8BD-416A-B581-1DCCA58FAABD}">\r
+                            <File Id="fil8455667A31A0B1322CE00154CCF24A08" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\RoundButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp246EDCD16E1C5BC88F69EFDF059C62B6" Guid="{363B06E4-66EB-437D-9454-16D14379D4A1}">\r
+                            <File Id="fil8161E8E97F7F2526D40A4E9426ED9CAC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ScrollBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpB92657C823E717DB4819343891A10BB1" Guid="{3860F820-ABC8-4AA4-A7F9-8B2D10A85511}">\r
+                            <File Id="filF04EF2D65266F90D588563833EE46211" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ScrollIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF4DE6E8F14E6A8E27857D8BC89787BAF" Guid="{DE05151F-0839-4D83-9AB5-7D089240403F}">\r
+                            <File Id="fil4987E569AB3CD95456311C90D8DB245D" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Slider.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpF49A6AC910C2EB6FF0917041E1F96E8A" Guid="{74BB77A1-0A70-4175-84CD-C4266DDF3112}">\r
+                            <File Id="filE8E232330635AC59E8BE3A0694C54370" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\SpinBox.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp1DF54EA158668A0E21013A820D2E7F51" Guid="{56BF8F73-ECAC-4474-BAB8-CDC47BC0A267}">\r
+                            <File Id="filADDD2AFDAE9A339C8A92C247781CEF40" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\SplitView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDFA6653A3AA375D59BE8318C927D5DC4" Guid="{B734122F-CD08-4401-B7BA-5E1D5858EE5A}">\r
+                            <File Id="filAC7F2D3C36D8E02F2FAF7A5578199576" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\StackView.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp20B23CB93B2BC9BF69A97D0994698310" Guid="{E5A49C0D-CAA9-4E51-A610-C46AF31FC864}">\r
+                            <File Id="fil5DFC169D4B5F10EDE586C1C5760B6573" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\SwipeDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp735A0D9DB2D96E80F3352A232FCAB063" Guid="{E77595FB-FEE6-48C0-A749-862C8CEEC8A0}">\r
+                            <File Id="fil0867DDC68FB2A26C4732AA7C6CF0EF5F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Switch.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpDB346892873A00B4024D345FE4C725BA" Guid="{43E075FC-DDDD-43B2-9308-F8174880A344}">\r
+                            <File Id="fil83DE4DE33E72D0F08A1C59200E82B424" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\SwitchDelegate.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp32E0784BA82190D0BD64E488EF265F55" Guid="{EEB2DEA6-7C4F-4CCB-9474-1E5F83FBEEA2}">\r
+                            <File Id="fil26EA260B1D46E42906FBF1BB1CE3B145" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\SwitchIndicator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4215773DF3A614E82EF59C456A6EFA97" Guid="{EBFB2786-8DCA-4B6B-870D-4FC90BD82E23}">\r
+                            <File Id="filA8F80D2C80356344CDCD864FCA88D48E" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\TabBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp72E1D5A67C90BF0538F8015E486898B0" Guid="{BA27BD86-15CA-47C1-8D6A-7998EDE35547}">\r
+                            <File Id="filDF1DA27F8944A4FC08060F351077F7E8" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\TabButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp02A0C8EDA0860CBAE68AE51FAE93EBB1" Guid="{3840413B-B9C9-4DD5-8AD5-D03695BC18C2}">\r
+                            <File Id="fil039EFA8518F7A118F885C10615BAE8BA" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\TextArea.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpC54A96A34D6E8EEB099E5754699E907D" Guid="{14E4461E-D756-4E2D-A569-2AB17F5BD204}">\r
+                            <File Id="filF7DE49C3E03DBE7D7DAEF87FB2CF7EEC" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\TextField.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp2564368EA9BDCD6CBD86BD0B47845CE8" Guid="{D705AE33-5B56-4582-B520-1967673B5A01}">\r
+                            <File Id="fil7BB9016C1239CAC2A691B7E80E155716" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ToolBar.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp4C037A4C1F7E6103342D757CE84FB3B0" Guid="{37196B4C-A886-40EC-B577-360811B5BBA2}">\r
+                            <File Id="filE5A4593700B13AAF5E707064E4DCBD71" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ToolButton.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp2782B54139300C706E3DF5D779A32A4E" Guid="{2096E3B3-6475-456F-8104-7044EE32C396}">\r
+                            <File Id="fil043B63F16633A9DEDFD28882B52AD567" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ToolSeparator.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp5ACAEC60F8EC208DC179E4790FE9090B" Guid="{29854CE8-EAC3-4341-BBF3-6A65CB8B1E5D}">\r
+                            <File Id="fil893C89D4CD3775E4FDF5EBC7DC2F2B64" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\ToolTip.qml" />\r
+                        </Component>\r
+                        <Component Id="cmpE672C34FD6F4C63A61786B77A67D0BCA" Guid="{317CDB5D-8CB9-4E8A-B272-7EF4044501C0}">\r
+                            <File Id="fil7F438968C07C1FD861B594449C04FB30" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\Tumbler.qml" />\r
+                        </Component>\r
+                        <Component Id="cmp05B02931195D1A41BF37EA6B3C26F10B" Guid="{D6329835-9FFD-46EB-8BC2-7E253275209A}">\r
+                            <File Id="fil147142765918ADD0B8E6F1C083AC829F" KeyPath="yes" Source="SourceDir\QtQuick\Controls.2\Universal\VerticalHeaderView.qml" />\r
+                        </Component>\r
+                    </Directory>\r
+                </Directory>\r
+                <Directory Id="dir91DAB6FA29B9FC0AA3F33D5E9F775BC2" Name="Layouts">\r
+                    <Component Id="cmp5FBDC59196ACDF8BDC99FB67B966EB33" Guid="{D8C9AF4A-7179-4745-978B-509993AE272A}">\r
+                        <File Id="fil6633E4E120DA0B6C86F07E8B5E0D813C" KeyPath="yes" Source="SourceDir\QtQuick\Layouts\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmp7599F21DFBFDE604667821D2BEC7A104" Guid="{28609AD7-3A84-4661-A657-44AD9F5C9112}">\r
+                        <File Id="filD17988DD251240A5450B0DF8AAE7D8A9" KeyPath="yes" Source="SourceDir\QtQuick\Layouts\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmp4C010137216B0EB61B4FB2EFCBE29E10" Guid="{FA31D92A-9BB1-4958-8103-8BA6CE201050}">\r
+                        <File Id="filDA7F5228817F450CCF860D99CCB135A2" KeyPath="yes" Source="SourceDir\QtQuick\Layouts\qquicklayoutsplugin.dll" />\r
+                    </Component>\r
+                </Directory>\r
+                <Directory Id="dir05FA62F47E8C43CCE7A3869D5CB62C6D" Name="Templates.2">\r
+                    <Component Id="cmp2214585F3508B759DB1308CC180757FB" Guid="{B480F13B-F2EF-46A5-839E-E43E5F688DD2}">\r
+                        <File Id="filAB37F3DFF1567C85745F8546C34DB8E4" KeyPath="yes" Source="SourceDir\QtQuick\Templates.2\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmpA1474840D69304F1AD34009D58C2B5DA" Guid="{38B68372-B6BE-4B9D-9A2D-757FEDFA3F74}">\r
+                        <File Id="fil58857EAF612484AD6F1B7594F5496CA3" KeyPath="yes" Source="SourceDir\QtQuick\Templates.2\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmp40B743148ECA12B22598A157C6147B6E" Guid="{9450D1E0-9549-4179-9BE3-303787AE3E0D}">\r
+                        <File Id="fil3A4423AFDA5CD5B5725F48FA3C3D437A" KeyPath="yes" Source="SourceDir\QtQuick\Templates.2\qtquicktemplates2plugin.dll" />\r
+                    </Component>\r
+                </Directory>\r
+                <Directory Id="dirA3E21ED94D77D9B21B5BA9468AEE4AB0" Name="Window.2">\r
+                    <Component Id="cmp104D8EDE2CF475C5EA4FE8169E74E299" Guid="{C251DAA6-2564-4B1B-AF03-E008227A46D3}">\r
+                        <File Id="fil0DA074889234E8261DE830FC8ADD1A9B" KeyPath="yes" Source="SourceDir\QtQuick\Window.2\plugins.qmltypes" />\r
+                    </Component>\r
+                    <Component Id="cmpDC8A03C1B37F2CFE043AED01502A741C" Guid="{D5570982-F17E-42B8-A5C7-BDE01B78A561}">\r
+                        <File Id="fil06DE355E21B919381A7CB928407CB593" KeyPath="yes" Source="SourceDir\QtQuick\Window.2\qmldir" />\r
+                    </Component>\r
+                    <Component Id="cmp87D7B0E5B5E51059D964EDCED844F4AD" Guid="{45586400-217C-4389-AE8E-89EA2A8FF29C}">\r
+                        <File Id="filAFCC613AFB897A177641333EC6418959" KeyPath="yes" Source="SourceDir\QtQuick\Window.2\windowplugin.dll" />\r
+                    </Component>\r
+                </Directory>\r
+            </Directory>\r
+            <Directory Id="dir00FAA9E14E20D0621E9EF928F4E50AD3" Name="QtQuick.2">\r
+                <Component Id="cmp80E63D0ACF59FD23F5B051FE5BD003DF" Guid="{B7FA0594-A58C-4959-B5A0-F550B3224D62}">\r
+                    <File Id="fil71AA2CD6AFACEC9523574269EB6E2F80" KeyPath="yes" Source="SourceDir\QtQuick.2\plugins.qmltypes" />\r
+                </Component>\r
+                <Component Id="cmp65C97B5A564AB844DB2F02FFA5B163A4" Guid="{9EA96BC5-BDB5-449F-A6F7-BB9AD9A146F0}">\r
+                    <File Id="fil5B9ECA215F124EDEB5921629049ECBD9" KeyPath="yes" Source="SourceDir\QtQuick.2\qmldir" />\r
+                </Component>\r
+                <Component Id="cmp4A028D7062688CA7458F90092B47AAAC" Guid="{AB143E12-669C-4FC2-A85D-4A67112D8041}">\r
+                    <File Id="fil4ED5B7A970D8897099777F501E3BDC88" KeyPath="yes" Source="SourceDir\QtQuick.2\qtquick2plugin.dll" />\r
+                </Component>\r
+            </Directory>\r
+            <Directory Id="dir28D98C712CB0DD42D15220AE3CD09B89" Name="scenegraph">\r
+                <Component Id="cmpB8094893EB4777EEB8D6692DE6B1F4C8" Guid="{58569060-2073-11EE-9B4B-0800200C9A66}">\r
+                    <File Id="fil794512AFD5ACCF820F3B44008D29A080" KeyPath="yes" Source="SourceDir\scenegraph\qsgd3d12backend.dll" />\r
+                </Component>\r
+            </Directory>\r
+<?endif?>\r
+<?endif?>\r
+        </DirectoryRef>\r
+    </Fragment>\r
+    <Fragment>\r
+        <ComponentGroup Id="jacktrip">\r
+            <ComponentRef Id="cmpE6579DC78E167F46AB23E0BE3EAA58CE" />\r
+            <ComponentRef Id="cmp52FE9CFF214054652C281C37201305EC" />\r
+            <ComponentRef Id="cmpA07F2DC814BECD1E5F3FC36DC47718E5" />\r
+            <ComponentRef Id="cmpBF7FA1E27DF091A3CCFA1CD7B7CD94BA" />\r
+            <ComponentRef Id="cmpF915A8D749EFFA98CD8DD582A7C82614" />\r
+<?ifdef rtaudio?>\r
+            <ComponentRef Id="cmpD683260746BC875AE3BDED667AA8F954" />\r
+<?endif?>\r
+<?ifdef dynamic?>\r
+            <ComponentRef Id="cmpF83098612A5B11C5398DBA3894ED590A" />\r
+<?ifdef qt5?>\r
+            <ComponentRef Id="cmp5DDCF0CE56D406D1B8B1F93E988AED6B" />\r
+            <ComponentRef Id="cmpB0CFEB9FF932521D6A75B6BE6F8D644F" />\r
+<?endif?>\r
+<?ifdef mingw?>\r
+            <ComponentRef Id="cmpB60B52D038613C3F20F84A0E1C83D4D4" />\r
+            <ComponentRef Id="cmpDEFDDDBEBEA7255D07EE4400F2CDA2FF" />\r
+            <ComponentRef Id="cmp809509D62BD9EA848EF243F03D83758F" />\r
+            <ComponentRef Id="cmp57DDA83E372188A5B760684B961DE685" />\r
+            <ComponentRef Id="cmpF76D0CD1138260DB167395B86D0011CF" />\r
+            <ComponentRef Id="cmpEA16414E09FD1D5068139520966B49EB" />\r
+<?endif mingw?>\r
+<?ifdef qt5?>\r
+            <ComponentRef Id="cmp01229088A6F1DEA1BE391E6E7CC3D3EE" />\r
+            <ComponentRef Id="cmp1658AEC83DA2CDD0D93D5BAAABBEAD46" />\r
+            <ComponentRef Id="cmp2DE804C107C75B0C71470A93647D09B5" />\r
+            <ComponentRef Id="cmp82912105030EA0D2D7550CF8AC5677F6" />\r
+            <ComponentRef Id="cmp975651B71289ACA70B3EB154C08A27F0" />\r
+            <ComponentRef Id="cmp3CE5AF7CB2F8B2689D2096769883BC56" />\r
+<?endif?>\r
+<?ifdef qt6?>\r
+            <ComponentRef Id="cmp9147A534CD8EDAB4BA7FDEAEEFE9C511" />\r
+            <ComponentRef Id="cmp52AB384603E096D6B6A1E1FC66B089D4" />\r
+            <ComponentRef Id="cmp0E8605D5C55843FCDFE809B5EE8DFDFA" />\r
+            <ComponentRef Id="cmp5D904885C8104A4049616BBAA95F34A3" />\r
+            <ComponentRef Id="cmpCAE694BB34A9EC66AEA9D10D697F1EAA" />\r
+<?endif?>\r
+            <ComponentRef Id="cmp1AAFF160C8E04AD945D74CA884667ABC" />\r
+            <ComponentRef Id="cmp02252B1C1976959CFEE7FA11B6E36C4C" />\r
+            <ComponentRef Id="cmpD9DF9496C7453F1AA6C20CD4A1C94ED7" />\r
+            <ComponentRef Id="cmp0E8940C551429DC79C890130F6D10B6D" />\r
+            <ComponentRef Id="cmpC6FCE143A8E5EAFEA6119E94128459C6" />\r
+<?ifdef extraimageformats?>\r
+            <ComponentRef Id="cmp62CBFF830BF239A4E376FAE29937AFAB" />\r
+            <ComponentRef Id="cmpFCC4763BEDEA49F63E8D8E3A18E86329" />\r
+            <ComponentRef Id="cmpF278C1175D7FB53021C0126B8F155FC2" />\r
+            <ComponentRef Id="cmp608F60C2BD04F2C37AB51C3211ABF615" />\r
+            <ComponentRef Id="cmpE6323C810986405043F219CECD6B6C10" />\r
+<?endif extraimageformats?>\r
+            <ComponentRef Id="cmp9132FD413972F732967B8BC075BC2781" />\r
+            <ComponentRef Id="cmp52840D209B2AC1DC45A29A6BA5A75FF8" />\r
+<?ifdef qt6?>\r
+            <ComponentRef Id="cmp83139013FA2C9A99CF2772FD54090FAA" />\r
+            <ComponentRef Id="cmp6E4460E3D6226693FEA5DBBF500895C7" />\r
+            <ComponentRef Id="cmp53306F84AD30BD49CF0C1A68007F9531" />\r
+<?endif?>\r
+<?ifdef hastranslations?>\r
+            <ComponentRef Id="cmpBCDC6995284E75B9A24E4646E6117744" />\r
+            <ComponentRef Id="cmp3B3D2ADEE08668758CB9FDB1D243AA11" />\r
+            <ComponentRef Id="cmpDAE87E8CC29C30A200A5D17D81DF1DE7" />\r
+            <ComponentRef Id="cmp719D62CCB63A2C08F22335557CBAB533" />\r
+            <ComponentRef Id="cmp8BDD412901C153FC8A2BF7335835AC78" />\r
+            <ComponentRef Id="cmp56040A1BBEBE8A547F7D6672326FF4EE" />\r
+            <ComponentRef Id="cmp42D4B2D8A4C91F9ED463DA42F3895E27" />\r
+            <ComponentRef Id="cmpB92DA15530102518B1B98DBF1DAC135D" />\r
+            <ComponentRef Id="cmp7210C3ACD533D6A934BD5977AB16AF71" />\r
+            <ComponentRef Id="cmp0643210AD0A541FA765CE55469F22FBB" />\r
+            <ComponentRef Id="cmpBB98B3096730FFFE87E708A6291ED47C" />\r
+            <ComponentRef Id="cmp3666ECDFA614EA095AF7310AECCD44B7" />\r
+            <ComponentRef Id="cmpC4FA2671255E7459BD46070D88B76FF1" />\r
+            <ComponentRef Id="cmp8D644C0D5E80E34EBCB6FC3643C879AF" />\r
+            <ComponentRef Id="cmpA8DDE2BD9E1AB00C9631DE447CBAA11D" />\r
+            <ComponentRef Id="cmp23B3657EE14B0ED7D00B271AFF81187C" />\r
+            <ComponentRef Id="cmp2D105F39980CF93A40405E25755346BB" />\r
+            <ComponentRef Id="cmpC3CB042567F53DD612D309E9CA2E10A6" />\r
+            <ComponentRef Id="cmp43D57F897A909679308EFC03849D7E2E" />\r
+            <ComponentRef Id="cmpF5EA7E37332E95C614BD2D7EAA933E5D" />\r
+            <ComponentRef Id="cmp8DAF13208A863A841AEE7A46E79FB87C" />\r
+            <ComponentRef Id="cmpA214B663A372012F5DCBA6A5CDAE421D" />\r
+            <ComponentRef Id="cmpFDB70117D8E46EB188997470A3793B5D" />\r
+<?ifdef qt6?>\r
+            <ComponentRef Id="cmpF0099CF54A0669D87CC17418C3911422" />\r
+            <ComponentRef Id="cmpE89BF9966FD9EC39B16BC14B5586E0FA" />\r
+            <ComponentRef Id="cmp7F6AC80CE7E39EEDC9A962EAFADD113B" />\r
+            <ComponentRef Id="cmp9EB4F508A604A4AD35BC558C456111DD" />\r
+            <ComponentRef Id="cmp09D69EFDCF1B331A37D028C962FE6147" />\r
+            <ComponentRef Id="cmpA23046CBC7F8B7E62AF4843CBB72DEC9" />\r
+<?endif?>\r
+<?endif hastranslations?>\r
+<?ifdef vs?>\r
+            <ComponentRef Id="cmp10C81658E3C8F63B19EF9D1C26B58363" />\r
+            <ComponentRef Id="cmp5B4749C387D33EAF993DDC89F9DBC102" />\r
+            <ComponentRef Id="cmp7F83581B3F7835094D371708381AD391" />\r
+            <ComponentRef Id="cmp33B78CE22F0CBCA4F41C5037A792210C" />\r
+            <ComponentRef Id="cmpB5571DB4C3153CA848ED1238DBCD4281" />\r
+            <ComponentRef Id="cmp4C1BE3EACCCE195C8B88434D5DF77DE5" />\r
+            <ComponentRef Id="cmp937697C855506045D9FF8DF5588CAFEE" />\r
+            <ComponentRef Id="cmpFEC2C3E75BBEACDB1A6F4AFB808CD58E" />\r
+            <ComponentRef Id="cmp8B3D94901F4D369CD276B339B8B00F70" />\r
+            <ComponentRef Id="cmp0B6C2EAABA7EAA14C510F4C9D68A3F40" />\r
+            <ComponentRef Id="cmpE75E4FEE65DCA51D044DF4B2AEDCD828" />\r
+            <ComponentRef Id="cmpA8BBE7010AD624EFBA7166925167402F" />\r
+            <ComponentRef Id="cmp00B1DB73ED33EC1DD6658036D3901CB7" />\r
+            <ComponentRef Id="cmp9F481CFBAECB8853AF07C1D5060B77D7" />\r
+            <ComponentRef Id="cmp93816105EAC51740B98AF9C1041D28D1" />\r
+            <ComponentRef Id="cmp5ED50822278A4A846E891B41C07F7B6A" />\r
+            <ComponentRef Id="cmpACA0A17DA54991A194B823CE40D06263" />\r
+            <ComponentRef Id="cmp3D2C980EC975DBBCBD13A9178DD1A03B" />\r
+            <ComponentRef Id="cmp356189FC3558D542AA8AD79067A42632" />\r
+            <ComponentRef Id="cmpABC392DBCC9A5A469B77C129899873EE" />\r
+            <ComponentRef Id="cmp740F889D5799A2FF0F93A24058617EB6" />\r
+            <ComponentRef Id="cmp2799826256C0E84847E94E11C7878018" />\r
+            <ComponentRef Id="cmp659424F663F96AE55FBA532B4CBB2EC8" />\r
+            <ComponentRef Id="cmpDF6A99C1FA6B3943D690F21136BA7F9F" />\r
+            <ComponentRef Id="cmp81B459304782BB6AC65E381BA13726B0" />\r
+            <ComponentRef Id="cmp3CC0196A91D9C805D66BFFF4EA38EBE7" />\r
+            <ComponentRef Id="cmp67E65C02C04C3567636F47EFAA888D12" />\r
+            <ComponentRef Id="cmpADC8E894229BBB48BBC40D4F033FD791" />\r
+            <ComponentRef Id="cmp0D2D3E713559661141BC5DD9F687F29D" />\r
+            <ComponentRef Id="cmp3C102DD93EC83164DA87E7820364DDAC" />\r
+            <ComponentRef Id="cmp4B666BB592E998D07B55C82CF76F5B1C" />\r
+            <ComponentRef Id="cmp4C99F5024E504FEAB151291478878934" />\r
+            <ComponentRef Id="cmp84BC34E1E20AAE86B427C9CD3129C4FA" />\r
+            <ComponentRef Id="cmpCBA14389AA89D290E481E0F2625854CE" />\r
+            <ComponentRef Id="cmp6FFF646F52B60CE854DA0A1B8C47EC55" />\r
+            <ComponentRef Id="cmp5E04D231070BE2CE70B2672E56C6B9DE" />\r
+            <ComponentRef Id="cmp8C5CA067EC2AB41E7CAFE253F2EC86A8" />\r
+            <ComponentRef Id="cmp8441DAF2F2BC211F1F6FD10DCE7B51FA" />\r
+            <ComponentRef Id="cmpC3A1EC5089BA6B0798970A12E614BBDB" />\r
+            <ComponentRef Id="cmp854B1173EACBB218B6AF849A7E8A56D7" />\r
+            <ComponentRef Id="cmp7D04EA58DD5BF794EB2D3A75609DEFF9" />\r
+            <ComponentRef Id="cmp688C34D162C3029BDE9A236F29106F11" />\r
+            <ComponentRef Id="cmp7F9ABA6F59701379E7E54AB2EF4B01EA" />\r
+            <ComponentRef Id="cmp46E93DFCA823AE21F1FD874F13C0A6CA" />\r
+            <ComponentRef Id="cmp2F1F9DE69090BFD2946A7621F2DA410E" />\r
+            <ComponentRef Id="cmpE650E3AF80C0073C2B52BC369D45B79C" />\r
+            <ComponentRef Id="cmpB62C6E9BF562754AAC86BB52A361231D" />\r
+            <ComponentRef Id="cmpA11A92D9D7D10A9221DD05699B7B1B8C" />\r
+            <ComponentRef Id="cmpA9B83EF4B33682EFEA153198F1C44255" />\r
+            <ComponentRef Id="cmp4951B8E17EA0B3A6B265E2C0E893A50A" />\r
+            <ComponentRef Id="cmp2BD742974C0791D6BD1AE4F7E3646D99" />\r
+            <ComponentRef Id="cmpF8459C652CF5C20ED87F853C1ABE1752" />\r
+            <ComponentRef Id="cmpCA0AF56CA87A52053FE61B6ECD156E35" />\r
+            <ComponentRef Id="cmpBC9523950CF52702CB1A519123F9E519" />\r
+            <ComponentRef Id="cmp302F033AE251BD62E648805EFB1BB536" />\r
+            <ComponentRef Id="cmpF3F704D8F2236A0A62B980FA41D3CB5E" />\r
+            <ComponentRef Id="cmp00AC1A2BE52EA04246F38C5F0B22A9D0" />\r
+            <ComponentRef Id="cmp0A8C093EE03780F9F7823442F10A6C82" />\r
+            <ComponentRef Id="cmp44B6838E73CD47A3F0061ECCE2EB48D0" />\r
+            <ComponentRef Id="cmp57A7D8DDBA7FB780E38A3CF0C3DE50F6" />\r
+            <ComponentRef Id="cmp74FB488898EE30854670C634A6CA57CB" />\r
+            <ComponentRef Id="cmpFF41000DFB8364F307DEC40444A8ED49" />\r
+            <ComponentRef Id="cmpE8348F57B7F7ED3E102061755EA59678" />\r
+            <ComponentRef Id="cmp789571C85C4C76BD48E9A471271B5128" />\r
+            <ComponentRef Id="cmp07E1D3988D9259288197F71CC0B0559A" />\r
+            <ComponentRef Id="cmp91EB04F6C5AFAF292A0BFCE3570DCBFE" />\r
+            <ComponentRef Id="cmpDA0DD041411FA5AC356FAD21EBB0BFCC" />\r
+            <ComponentRef Id="cmpC0C294F18C416B2F09D563A7D2F171C0" />\r
+            <ComponentRef Id="cmpBBC07C7AA9E1D56538F0EE5BC883A106" />\r
+            <ComponentRef Id="cmpA2AD6F575E441894DDF6301F935EF3E7" />\r
+            <ComponentRef Id="cmp346061900FCE515D6B32CFCD7971325C" />\r
+            <ComponentRef Id="cmpE5AC86A748B88CA71F59F050376D4BC8" />\r
+            <ComponentRef Id="cmp26A45023E109A672FAA78FE5C4F827F4" />\r
+            <ComponentRef Id="cmp92014B7FBE8B02881A6A53384BDCD1C4" />\r
+            <ComponentRef Id="cmp372134AB6EF24BAC7497CC47466F0727" />\r
+            <ComponentRef Id="cmp17FB129D73D5BCA38BEC406E5A7A9853" />\r
+            <ComponentRef Id="cmp59BA1197723375EE2B1A8FC0971407A8" />\r
+            <ComponentRef Id="cmp9076C49961F4F0F4F5B4F65EF0CE7925" />\r
+            <ComponentRef Id="cmpCBF4FE8555DE1D61528AF79EA3C5870F" />\r
+            <ComponentRef Id="cmp63B837CFDE5F5C8ADC92D6A80E367621" />\r
+            <ComponentRef Id="cmp73FC7C1B9421F67443A7CA5E03C6F59C" />\r
+            <ComponentRef Id="cmp24D33989EF8148F7E8E801168FA75D02" />\r
+            <ComponentRef Id="cmp781F94BE6B67AAB9F2DD4A6865F67043" />\r
+            <ComponentRef Id="cmpBF8FE1999E64BE53CD0FFE028F2BA8E5" />\r
+            <ComponentRef Id="cmp508D7E9DE20A057C80595708342CCD11" />\r
+            <ComponentRef Id="cmp814DB5A7174FAD3D492689B3CFFD4FC6" />\r
+            <ComponentRef Id="cmp9FCA0D048C5FEDEF35CFDCF4220D148D" />\r
+            <ComponentRef Id="cmp8302E43F7B461A53125D24EBAB2C8BBA" />\r
+            <ComponentRef Id="cmpDD4764A31C5966C382C9FADCF2747DAE" />\r
+            <ComponentRef Id="cmp4DC6229A880E85A8EE64B69A175EB7F8" />\r
+            <ComponentRef Id="cmpC3E4C72480B21745D1D55462CD128148" />\r
+            <ComponentRef Id="cmp9C2C5E7D9EAC8AD7E9FB8BA9B0147BAB" />\r
+            <ComponentRef Id="cmp66ADBF15C183C1EF05BA59EBF57DC04C" />\r
+            <ComponentRef Id="cmpB85DB993D3D58E717F4EC2A2A5AC4130" />\r
+            <ComponentRef Id="cmp93014FB27329F9718833E30A8DCE2F23" />\r
+            <ComponentRef Id="cmp9D0CC60826A95D19D52A3A08FB44155C" />\r
+            <ComponentRef Id="cmp06BD88C78B72DD375F0FD8DC37812A7A" />\r
+            <ComponentRef Id="cmp6577B61F7B803092F263CE9BD76D7B19" />\r
+            <ComponentRef Id="cmpD880E6EDC6A23CDFF94AB5E0A0018A10" />\r
+            <ComponentRef Id="cmp00D081BB0762FEC39933D1C2960FC831" />\r
+            <ComponentRef Id="cmpD69A2401CBD6D26E44BE1271C8905A24" />\r
+            <ComponentRef Id="cmp0B7DD63BE81BC1EA8DB09A99C12CB156" />\r
+            <ComponentRef Id="cmp1EC09C5C4D6CE477EAA2BE5A3798E1CC" />\r
+            <ComponentRef Id="cmp577EBA2061AD5D464AC5B629EF1A63DE" />\r
+            <ComponentRef Id="cmpFD9033FF82B141429428B759027C0ECB" />\r
+            <ComponentRef Id="cmpD9A7A9076E4844ACEB4385E651C6FC27" />\r
+            <ComponentRef Id="cmp952D38FA5543D46E8B0B6CAC867E6D19" />\r
+            <ComponentRef Id="cmp8360F4A2F8333FCC0A95F36461DAC0AE" />\r
+            <ComponentRef Id="cmpA8C48D6004583C73A28027E805ECF241" />\r
+            <ComponentRef Id="cmpF0F52E0AA644EE5B8A4212507A2A9632" />\r
+            <ComponentRef Id="cmp1F2A598B5F572A95F619F914ADFDFD01" />\r
+            <ComponentRef Id="cmp894CC6997FDC49D32993AF049D19661B" />\r
+            <ComponentRef Id="cmp45A86D8F1E2A0DB9085F1C386BE8C441" />\r
+            <ComponentRef Id="cmpD7F91C9BF1B37E7ECEFC803745680533" />\r
+            <ComponentRef Id="cmp6C7380F6F9D3432EF0FF68155BE5B7EA" />\r
+            <ComponentRef Id="cmp163F0198648660C0C4DED70616CD3A50" />\r
+            <ComponentRef Id="cmpF5CA7A4D3DB71A67C3E4BC87D9A553DE" />\r
+            <ComponentRef Id="cmp2599D94F81ED612C05479E9A8F58A189" />\r
+            <ComponentRef Id="cmp9557C3464EC921CAC1A43D77B20BA92D" />\r
+            <ComponentRef Id="cmpFDD06D370422F04997F37CDCF9077AAF" />\r
+            <ComponentRef Id="cmpC5C6ADF92BCA401BFD4AB6B8DF07ABE6" />\r
+            <ComponentRef Id="cmpC7F75CEAF1EB837B6998ED5DC6550404" />\r
+            <ComponentRef Id="cmp4E0B2D6970453AF152BA060236BD8B71" />\r
+            <ComponentRef Id="cmp3A32B8F13C49569A0A4AD1D11045BE3A" />\r
+            <ComponentRef Id="cmp6D11E1D44ADD3049CE735F324F7F4521" />\r
+            <ComponentRef Id="cmp6763D9C183B24CD6659B616DD72994DA" />\r
+            <ComponentRef Id="cmp0FEA3716333AE56EBB72D727432DFB0A" />\r
+            <ComponentRef Id="cmpF77FF7EB150083CBBB82946EC3F849C5" />\r
+            <ComponentRef Id="cmp8693A45A9517FDC707611CD9592AD887" />\r
+            <ComponentRef Id="cmp22009CC98E7BCD73739238DBAD70F5FF" />\r
+            <ComponentRef Id="cmp006248D9C946635CD2C0D21D711BAF97" />\r
+            <ComponentRef Id="cmp8F4980EC013CDADC9677179C1B5FB4DB" />\r
+            <ComponentRef Id="cmp7CB5E11B511D00866734F2A59242071E" />\r
+            <ComponentRef Id="cmp15339C0E193CBE17FB6CB89DA971471D" />\r
+            <ComponentRef Id="cmp7A634DCF2DA1EDFF9FBAF699758570C3" />\r
+            <ComponentRef Id="cmpAA64F717D23C1375E79BE28E5A3DE005" />\r
+            <ComponentRef Id="cmp14F754092A68F3FE755963981761F031" />\r
+            <ComponentRef Id="cmp8DBD1F81C17E5D01D819E619D30E83CA" />\r
+            <ComponentRef Id="cmp9A97912EFCCFD956EF8F155A81CB494F" />\r
+            <ComponentRef Id="cmp37BA96169B5087F3D2C6E769FF5E9270" />\r
+            <ComponentRef Id="cmpE74780CCB3558FB4C37DDE65264DC1F4" />\r
+            <ComponentRef Id="cmp86D5D0E9BB2DE2BDCC6CF459CB7C4E01" />\r
+            <ComponentRef Id="cmp3951161EB256315C232D7C2B211A5D42" />\r
+            <ComponentRef Id="cmp6CAF0B6CD8ABE85E62A160036392A2CC" />\r
+            <ComponentRef Id="cmp55E217C43921E96FF82040712469E0F3" />\r
+            <ComponentRef Id="cmp57277D9EB23963010750499ED2F541B8" />\r
+            <ComponentRef Id="cmp651F7330940689374F3F4500C1F20484" />\r
+            <ComponentRef Id="cmp02C5C41CDED7F94E885A4A2A28EC3600" />\r
+            <ComponentRef Id="cmp95C32716B4F28A90B070E08CAC81BAC4" />\r
+            <ComponentRef Id="cmp2CEB59611FF3F33F7AEF7D2EAAE0CB05" />\r
+            <ComponentRef Id="cmp8D1031BFABA660F80B7793BB3761EB75" />\r
+            <ComponentRef Id="cmpB52CD2CEE9160F5F67372170543F85B4" />\r
+            <ComponentRef Id="cmp909217E4E53799CCE36ED3F7B817DF15" />\r
+            <ComponentRef Id="cmp88C4BDBD3D48F447FF34FD6DF1FC3C41" />\r
+            <ComponentRef Id="cmp6D134750A96C36CCE37CDAEAC146A810" />\r
+            <ComponentRef Id="cmp17431F800533AAD492A8DA91C537AEE5" />\r
+            <ComponentRef Id="cmp5651D28BAA4D7517AB64D608440B8947" />\r
+            <ComponentRef Id="cmp1FC8B77EF51F392FB0F6CD269098431D" />\r
+            <ComponentRef Id="cmp403E7038C7324A61D8727DC1D939E547" />\r
+            <ComponentRef Id="cmp98D46B3DC15B713ACF5F53B34EE13068" />\r
+            <ComponentRef Id="cmp82998B9C0403FF9F31583386A05AA9BB" />\r
+            <ComponentRef Id="cmp0D0B1F4459593A8B3D4A1BCBA139D98F" />\r
+            <ComponentRef Id="cmpF083A0EDD7D82B7EEFA116A49700462C" />\r
+            <ComponentRef Id="cmp094723DF4DF9079CABB07B397F53FC14" />\r
+            <ComponentRef Id="cmpE46F1304BE1F25953FA1502338B46B6E" />\r
+            <ComponentRef Id="cmpF34990BAAB80CEB1B1FBA3F75A8BB18E" />\r
+            <ComponentRef Id="cmpF8164C6D715D603507F60EA7662104BB" />\r
+            <ComponentRef Id="cmpBC49C3C8C0D1C882B8E4BFA5DDB84097" />\r
+            <ComponentRef Id="cmp026A749908FF7AE58055B0E1AB393168" />\r
+            <ComponentRef Id="cmp4644F2A5A8ADFEA7ED73D09A41CB9AFF" />\r
+            <ComponentRef Id="cmpF0E7DBBAED60C3A0D5E4980C6384C524" />\r
+            <ComponentRef Id="cmpF26383485D9640683A7276A7CFD06280" />\r
+            <ComponentRef Id="cmp5D85AEBC11476CF382025D6654F36103" />\r
+            <ComponentRef Id="cmp44002125F963B5A1D193E0D787FF6AF6" />\r
+            <ComponentRef Id="cmpF8E1020B8AEA4B8F647DF3962BB9CF2C" />\r
+            <ComponentRef Id="cmp6F5B3D19FBA609A3ECE0700E2DB1C805" />\r
+            <ComponentRef Id="cmpF0883486B5B318E646462C05EB5DBF23" />\r
+            <ComponentRef Id="cmp8F7277DF6FE75EF54431CCAB073199B5" />\r
+            <ComponentRef Id="cmpBC3A73EE4B192F1BF88AF2E61E4212EA" />\r
+            <ComponentRef Id="cmpD48A473B45218C0BDE5C64912EFCBAFC" />\r
+            <ComponentRef Id="cmp86201E518FC2273E24BCD76E296C18E1" />\r
+            <ComponentRef Id="cmpFF8739D6C4BEC98E8F1240FFBD0A64B0" />\r
+            <ComponentRef Id="cmp38720475F2109CBC7EF4F43C365CC5CA" />\r
+            <ComponentRef Id="cmp996D8C56417E10EF44329F7339E474F2" />\r
+            <ComponentRef Id="cmpED22A8F3ACEE04AB6CE44D533F4F300A" />\r
+            <ComponentRef Id="cmp838B8853704ACB2A7F45BA00B950F3BB" />\r
+            <ComponentRef Id="cmpDF9CC0BB490A823FFADEFFBDAB92C2C7" />\r
+            <ComponentRef Id="cmp73DF7086A467801478C7321A8B435AA2" />\r
+            <ComponentRef Id="cmp10D7AC86A56DE2D04356715F35250435" />\r
+            <ComponentRef Id="cmpB8BBD8CCBCDE8771AD9E3D6D456FDD6B" />\r
+            <ComponentRef Id="cmp5DA5D6BC1A869E7409CC08ACDEBAA08F" />\r
+            <ComponentRef Id="cmpEFE26691A6CBD74691FA18F89319520B" />\r
+            <ComponentRef Id="cmpF14CA7FF23A8FEBB250D9C1A4DBAB5F2" />\r
+            <ComponentRef Id="cmp76B901918023AFA1ECF5AFE1C5AADFD6" />\r
+            <ComponentRef Id="cmp3B04385170209415EA0392D6C6D9394D" />\r
+            <ComponentRef Id="cmpAC7FCF48DC2DD2FF598D8D8EDFBDDE10" />\r
+            <ComponentRef Id="cmp31295E08BF0ADC9F7414B879A59EAB38" />\r
+            <ComponentRef Id="cmp26ED7398D2B7FD7CE79B20C6F81AA21B" />\r
+            <ComponentRef Id="cmp0AF138C48EF9389FB53BCFFF127C63B7" />\r
+            <ComponentRef Id="cmpB84DF029169B6913288463C0CD791FD7" />\r
+            <ComponentRef Id="cmpB2768FB92A037D37B0EDB0A50917DB6E" />\r
+            <ComponentRef Id="cmp60DE9C3DB4C8AE187B4D19BDEDC6C16A" />\r
+            <ComponentRef Id="cmp8439239F2F45A8EAE3D84F080943DDD1" />\r
+            <ComponentRef Id="cmp16223EE59A9E78D62203D037E29D698D" />\r
+            <ComponentRef Id="cmp018AFFA2C882E3A63FE78C48058C9701" />\r
+            <ComponentRef Id="cmpD9344D05ADD481EACDED9FFB5EE9D160" />\r
+            <ComponentRef Id="cmpA9175F4256E542F0F7D7729B65AC8716" />\r
+            <ComponentRef Id="cmp022C885B9AF26895C6BBDBE1AF964DA7" />\r
+            <ComponentRef Id="cmpB42CC102A79539E49D9D71AF819BDCFA" />\r
+            <ComponentRef Id="cmpDE27E4C6B7C8141F7B15529523211BDE" />\r
+            <ComponentRef Id="cmp8A1FE21942F63A12FE8C262291D2F118" />\r
+            <ComponentRef Id="cmp89930EDDF65A84FB647750A3889D3998" />\r
+            <ComponentRef Id="cmp7A7C588B21E8E3817983B40E6C15A8DC" />\r
+            <ComponentRef Id="cmpFAF5864C58442FAA1D5C0A01C83FACE9" />\r
+            <ComponentRef Id="cmp48641EDC547835A4C015C138C9EE12D6" />\r
+            <ComponentRef Id="cmp74D44A9BA04153CCD0A6FA413CA57C4B" />\r
+            <ComponentRef Id="cmp84CD7E65AAF5D3AD9F421445901CF37D" />\r
+            <ComponentRef Id="cmpAB0EEB7FDAB89DB33CC1872F2D020B9A" />\r
+            <ComponentRef Id="cmpEA5C0DF0E7CC7FF0C0A116E8ED0DFD39" />\r
+            <ComponentRef Id="cmp48F6BA7469D6C5E8A90EFDC2F1F12A03" />\r
+            <ComponentRef Id="cmpEACB1D29768FC4AD7754B7A8BB5CEAA4" />\r
+            <ComponentRef Id="cmp40DFBE5C4004C66BD421833A0916A402" />\r
+            <ComponentRef Id="cmpD46A2953ED55E92D9E0462BE4D66B828" />\r
+            <ComponentRef Id="cmp9BBFEBDFBD798ED64CF1BFB852E57719" />\r
+            <ComponentRef Id="cmpD60B739F43AAC84324A66CEFFCDDBDFF" />\r
+            <ComponentRef Id="cmp4AF8FAB5F76A012B1E00956784FB86C0" />\r
+            <ComponentRef Id="cmpD3F2F08057724AFA25E4EBF3514406A6" />\r
+            <ComponentRef Id="cmp5D978D2112EB628D93B78BBB779859CA" />\r
+            <ComponentRef Id="cmp6800D8FB1FBB983412E92B80451A1109" />\r
+            <ComponentRef Id="cmp71287354FF8DCC602BB0D3D414E05962" />\r
+            <ComponentRef Id="cmp0DF5FCC7255520F9D24CAC298FA859D0" />\r
+            <ComponentRef Id="cmp3301D802CB29561A15AE015C15DD8543" />\r
+            <ComponentRef Id="cmp00A4060669FB650FE92DDD9A09A857C0" />\r
+            <ComponentRef Id="cmp2D3FB42E796C744949D0480EE5C04892" />\r
+            <ComponentRef Id="cmp11B5D7ADB851C95DCFE5C71959222497" />\r
+            <ComponentRef Id="cmp27BC211BA8D2261D578FC390503266AF" />\r
+            <ComponentRef Id="cmp60AB973400004A31DBB4967567BB3780" />\r
+            <ComponentRef Id="cmp026D50205EC5D03323420AC1EC10FD89" />\r
+            <ComponentRef Id="cmpE0BD5867C82F9D8554FECDDC4B6901C7" />\r
+            <ComponentRef Id="cmp9F4E4D4D0534820ED7B1592321BACF51" />\r
+            <ComponentRef Id="cmpCB85EA75A98576ACBC265DECF4F7697F" />\r
+            <ComponentRef Id="cmpD8B043EAF161B1EED3C0D0E0E8B0717E" />\r
+            <ComponentRef Id="cmp006E05B629020152BA06266DCF586F94" />\r
+            <ComponentRef Id="cmp6390600916A9D80DEECC6B64C1A5734B" />\r
+            <ComponentRef Id="cmpA029FDE3C079115EA43947112C138B8C" />\r
+            <ComponentRef Id="cmp050C776C9A28886790788280CCEC3D91" />\r
+            <ComponentRef Id="cmpBFBC5C595627F87399C1078F9A522E9D" />\r
+            <ComponentRef Id="cmp2B0856221A67BDF81C2238000B184A17" />\r
+            <ComponentRef Id="cmp835605203F95C84205B718EDDDE2091E" />\r
+            <ComponentRef Id="cmpD37BF99FBDB8EFD807188027A80B8DA3" />\r
+            <ComponentRef Id="cmp31DA9547A00549AAE408F3DADB011332" />\r
+            <ComponentRef Id="cmpC869C8092B8F2AAC631C286328891A94" />\r
+            <ComponentRef Id="cmp0305CAFC1FCE93ED480CC829AAD832AF" />\r
+            <ComponentRef Id="cmp5D66D11EA79AD27A1AE641BF9C7CB9F2" />\r
+            <ComponentRef Id="cmpA817DF6EA38B1DF29BCE614C5450544B" />\r
+            <ComponentRef Id="cmpFB26590AF40355E4D807DA7009B7836D" />\r
+            <ComponentRef Id="cmpFA168D7F4EFC52948B17DF862E07E25A" />\r
+            <ComponentRef Id="cmp80B129BEE9AED15B717DBBC4E1C91B8D" />\r
+            <ComponentRef Id="cmp222BD8179F2D4ECA9CD649598F1665FE" />\r
+            <ComponentRef Id="cmpE9263BDE73548B76D4CF00FA421C6394" />\r
+            <ComponentRef Id="cmp665380529EE1C0ED1DE23E8B26850B19" />\r
+            <ComponentRef Id="cmp8DD36362549E19ECEA88E3D1B1ACBB85" />\r
+            <ComponentRef Id="cmpD2EA9700B16C8836D5206D1185C549A0" />\r
+            <ComponentRef Id="cmp73D2F27C7DDDD4CF7ADAEBF8E6E42EB1" />\r
+            <ComponentRef Id="cmp7E31792D0EB10DD8AFD37842DFFCA924" />\r
+            <ComponentRef Id="cmpA158D23F2833A86832CC8AB04B51E8D0" />\r
+            <ComponentRef Id="cmpA62BD30E70FBA79DED1CC038B0404EBC" />\r
+            <ComponentRef Id="cmp78F57F7853BF2F83CA4C27E09AE1E09D" />\r
+            <ComponentRef Id="cmpBC000241C708266C86D302A435E66F1C" />\r
+            <ComponentRef Id="cmp3B8600643B709AD9AD113079EDB50C94" />\r
+            <ComponentRef Id="cmpB755C164EDFD73E1498268DB3DA880E5" />\r
+            <ComponentRef Id="cmp1CBDAA564E02886155300F7A8AF049CF" />\r
+            <ComponentRef Id="cmpFAE3866F17FF7039CA9618739C64A1A5" />\r
+            <ComponentRef Id="cmp1BEDE14E6357E29206EDC740AE50650E" />\r
+            <ComponentRef Id="cmp9848706507698E3D2DE99B0C079EAA41" />\r
+            <ComponentRef Id="cmpAF75487C0AF2238D5F5AB27EE11B8C29" />\r
+            <ComponentRef Id="cmpF1D853F627859C5B31EEDA150FC58C6E" />\r
+            <ComponentRef Id="cmp24C324DDA2757C24A5D64657F10D1861" />\r
+            <ComponentRef Id="cmp24A094BA67CBF70909850C9D82D581FB" />\r
+            <ComponentRef Id="cmp855A7D3DC1E9E7D07C156736FB529CB6" />\r
+            <ComponentRef Id="cmpF44D1A83846E86F2D4EFFEDC9DA14661" />\r
+            <ComponentRef Id="cmp15902266961B205BB7C49E3F21B0A166" />\r
+            <ComponentRef Id="cmp50966487B32C316D078827563E31DB30" />\r
+            <ComponentRef Id="cmpA1B9598A2B3B9C203B5780C91F7075D7" />\r
+            <ComponentRef Id="cmpEF587C49FDE4B67961EDEA007128D3DE" />\r
+            <ComponentRef Id="cmp793DBF048FBD4343297F1E17756BBF6B" />\r
+            <ComponentRef Id="cmpD98F61FC599E1BD66057249B28016A0E" />\r
+            <ComponentRef Id="cmp4C6EA3AFEB0324ED3875A46DCCB4EA48" />\r
+            <ComponentRef Id="cmpA7F194C9D57330A5F72C74CD96507E48" />\r
+            <ComponentRef Id="cmp337BDFFE231941F8A3E54BF4AC644B58" />\r
+            <ComponentRef Id="cmp0255A607D5334E94D517A2FEDDB2105D" />\r
+            <ComponentRef Id="cmpBDEB6272B93E7E2FD1982012B5EF3D02" />\r
+            <ComponentRef Id="cmp6ACA30972AFD6D78448054BA4368AEE7" />\r
+            <ComponentRef Id="cmp0014F4AC9A48147F8EAB3F17B2C144E0" />\r
+            <ComponentRef Id="cmpB1ABCFD82FD1858E7657C34A8104404C" />\r
+            <ComponentRef Id="cmp6A432A70B05CC4AC38DA38F776B97812" />\r
+            <ComponentRef Id="cmp134038F5FD44A08B4DA0CFD8921DFB42" />\r
+            <ComponentRef Id="cmp34FB0E37566676B69EEEB98F670D23C4" />\r
+            <ComponentRef Id="cmp166A7C72CA2D82533EBF24FB87FE4BC5" />\r
+            <ComponentRef Id="cmp11D7406D35C2641BC20018F1FA8ADC45" />\r
+            <ComponentRef Id="cmp6CA80FCA9CDD4C55F2582BF139AA4B1B" />\r
+            <ComponentRef Id="cmpDFBFE88B708CE2CA5904473FE0CDB861" />\r
+            <ComponentRef Id="cmp8D47A9ABEDE81A100C924FF154224483" />\r
+            <ComponentRef Id="cmp0071C7F9D6D4B5DC3218D487DABFC218" />\r
+            <ComponentRef Id="cmp54B17CC907A926962454D9FDEA99E369" />\r
+            <ComponentRef Id="cmpDF321BF03AC6D81897FFD4B64045DE88" />\r
+            <ComponentRef Id="cmpEBBBCD3B40ADAF40F5F7BBDAD1628BAB" />\r
+            <ComponentRef Id="cmp4781FFA691E6B55670D19CCA10E68D4A" />\r
+            <ComponentRef Id="cmp9D75099F72BC1EA408C69BAE1036713A" />\r
+            <ComponentRef Id="cmpD507403E3878D92F029FB454CD349781" />\r
+            <ComponentRef Id="cmpDB50F59C1041F3C78FB11F782D92A59C" />\r
+            <ComponentRef Id="cmp9ACED442699FD1B84B592FD9A678FF1F" />\r
+            <ComponentRef Id="cmpC959D8B9C731BB4CB168C53232754164" />\r
+            <ComponentRef Id="cmpF036082D9E70D219B49993BF2078FC9B" />\r
+            <ComponentRef Id="cmp610C62A18FB0CBD4A23E6420089991A6" />\r
+            <ComponentRef Id="cmpF0A89D556198AEC41AA931DE764420C9" />\r
+            <ComponentRef Id="cmp1FB4C766D7DFBDC29927513C52FF444C" />\r
+            <ComponentRef Id="cmpAB3EF735C77E4B6501B74C3070F17E8D" />\r
+            <ComponentRef Id="cmpEF370FB1E0BE2F7C44ACA77A0B9A380F" />\r
+            <ComponentRef Id="cmpD53F72AC172BCE3F947CB966768B983E" />\r
+            <ComponentRef Id="cmpB58DE0208168537FC63A28BA40094D3E" />\r
+            <ComponentRef Id="cmp1E24968394165967A4C933056CC97E8B" />\r
+            <ComponentRef Id="cmpA780D5122E140F762FC4C3A0D02E7179" />\r
+            <ComponentRef Id="cmpE61937584021054E6A0A34AFBB281937" />\r
+            <ComponentRef Id="cmpB2D199D19AF00559ED6198A864F4874E" />\r
+            <ComponentRef Id="cmpA29A335EDA4A585067146163B51D3643" />\r
+            <ComponentRef Id="cmp4F8C12B67EDC519F78AA4840C85494D1" />\r
+            <ComponentRef Id="cmp30F05444D83E8DAFE75CAAB16F50E3AD" />\r
+            <ComponentRef Id="cmp7573B19F4A56CF2B1D359A87D25A39B4" />\r
+            <ComponentRef Id="cmp0B698F624CC72E780FB1FCCBE6340AAD" />\r
+            <ComponentRef Id="cmp898F4E8E360375D6D8FE6F606F899441" />\r
+            <ComponentRef Id="cmp246EDCD16E1C5BC88F69EFDF059C62B6" />\r
+            <ComponentRef Id="cmpB92657C823E717DB4819343891A10BB1" />\r
+            <ComponentRef Id="cmpF4DE6E8F14E6A8E27857D8BC89787BAF" />\r
+            <ComponentRef Id="cmpF49A6AC910C2EB6FF0917041E1F96E8A" />\r
+            <ComponentRef Id="cmp1DF54EA158668A0E21013A820D2E7F51" />\r
+            <ComponentRef Id="cmpDFA6653A3AA375D59BE8318C927D5DC4" />\r
+            <ComponentRef Id="cmp20B23CB93B2BC9BF69A97D0994698310" />\r
+            <ComponentRef Id="cmp735A0D9DB2D96E80F3352A232FCAB063" />\r
+            <ComponentRef Id="cmpDB346892873A00B4024D345FE4C725BA" />\r
+            <ComponentRef Id="cmp32E0784BA82190D0BD64E488EF265F55" />\r
+            <ComponentRef Id="cmp4215773DF3A614E82EF59C456A6EFA97" />\r
+            <ComponentRef Id="cmp72E1D5A67C90BF0538F8015E486898B0" />\r
+            <ComponentRef Id="cmp02A0C8EDA0860CBAE68AE51FAE93EBB1" />\r
+            <ComponentRef Id="cmpC54A96A34D6E8EEB099E5754699E907D" />\r
+            <ComponentRef Id="cmp2564368EA9BDCD6CBD86BD0B47845CE8" />\r
+            <ComponentRef Id="cmp4C037A4C1F7E6103342D757CE84FB3B0" />\r
+            <ComponentRef Id="cmp2782B54139300C706E3DF5D779A32A4E" />\r
+            <ComponentRef Id="cmp5ACAEC60F8EC208DC179E4790FE9090B" />\r
+            <ComponentRef Id="cmpE672C34FD6F4C63A61786B77A67D0BCA" />\r
+            <ComponentRef Id="cmp05B02931195D1A41BF37EA6B3C26F10B" />\r
+            <ComponentRef Id="cmp5FBDC59196ACDF8BDC99FB67B966EB33" />\r
+            <ComponentRef Id="cmp7599F21DFBFDE604667821D2BEC7A104" />\r
+            <ComponentRef Id="cmp4C010137216B0EB61B4FB2EFCBE29E10" />\r
+            <ComponentRef Id="cmp2214585F3508B759DB1308CC180757FB" />\r
+            <ComponentRef Id="cmpA1474840D69304F1AD34009D58C2B5DA" />\r
+            <ComponentRef Id="cmp40B743148ECA12B22598A157C6147B6E" />\r
+            <ComponentRef Id="cmp104D8EDE2CF475C5EA4FE8169E74E299" />\r
+            <ComponentRef Id="cmpDC8A03C1B37F2CFE043AED01502A741C" />\r
+            <ComponentRef Id="cmp87D7B0E5B5E51059D964EDCED844F4AD" />\r
+            <ComponentRef Id="cmp80E63D0ACF59FD23F5B051FE5BD003DF" />\r
+            <ComponentRef Id="cmp65C97B5A564AB844DB2F02FFA5B163A4" />\r
+            <ComponentRef Id="cmp4A028D7062688CA7458F90092B47AAAC" />\r
+            <ComponentRef Id="cmpB8094893EB4777EEB8D6692DE6B1F4C8" />\r
+<?endif?>\r
+<?endif?>\r
+        </ComponentGroup>\r
+    </Fragment>\r
+</Wix>\r
diff --git a/win/qt6-noguids.wxs b/win/qt6-noguids.wxs
new file mode 100644 (file)
index 0000000..00e4918
--- /dev/null
@@ -0,0 +1,2226 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
+    <Fragment>
+        <DirectoryRef Id="TARGETDIR">
+            <Directory Id="dir5986BBEEE82C5D978676757AB9BBF16F" Name="generic" />
+            <Directory Id="dirB10C059F651528B59E1FD268EB08C6D6" Name="iconengines" />
+            <Directory Id="dir20189C824D4DDABC3D9FF9449C089CE0" Name="imageformats" />
+            <Directory Id="dir690B8E6D609B0DB17DB71E8B1FB2FEC5" Name="LICENSES" />
+            <Directory Id="dir4C08960BD61EEE75AE4A87FC65A5DE2A" Name="networkinformation" />
+            <Directory Id="dir826692792B0995C8DF68919F5C684331" Name="platforms" />
+            <Directory Id="dirC15EA267FAD4FC447E84FB3A32CBDB68" Name="position" />
+            <Directory Id="dirC5F8EA44B55B774D108D66814D18D4D1" Name="qml" />
+            <Directory Id="dir9331125B8BF1F459D0AC9F91A6A2F779" Name="qmltooling" />
+            <Directory Id="dir08941CEA6046320FCCF158F759AF80F2" Name="resources" />
+            <Directory Id="dirD0D2BD28387A827DDF373DED43543ABA" Name="sqldrivers" />
+            <Directory Id="dir3BC41331752E78D0C2719C277915294F" Name="styles" />
+            <Directory Id="dir509C75F94B9AF6C35CA00410005C14EE" Name="tls" />
+            <Directory Id="dir1F7AD7AA9B228AED2E74CDD7C3088B5C" Name="translations" />
+            <Directory Id="dir90EFFBA81E5867E3E251FBDCB82DBC0C" Name="webview" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <ComponentGroup Id="jacktrip">
+            <Component Id="cmp5D831CD15043495A344038AE6FBC4FAF" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil7FD1FE597BD52B987AB17EB8CF74B0D5" KeyPath="yes" Source="SourceDir\D3Dcompiler_47.dll" />
+            </Component>
+            <Component Id="cmpAD46F02BC5359E4227248E749EB0E74F" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil572E83C11D35ADB0E4CB1A0A0AF474C6" KeyPath="yes" Source="SourceDir\dialog.bmp" />
+            </Component>
+            <Component Id="cmp61BABE677B13AE7A25F57865DD2CB150" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filD0B8E5384B7EB5609D1B2A67A98FF334" KeyPath="yes" Source="SourceDir\jacktrip.exe" />
+            </Component>
+            <Component Id="cmp5C97B37BB3181F77EDC11261A4463285" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil46101D0C43E1BEF1BE4C5475DA0B2837" KeyPath="yes" Source="SourceDir\JackTrip.msi" />
+            </Component>
+            <Component Id="cmpCD23A5CC7196B582D23DF7AC7DFE061F" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil00C2A327ECCB1D33F7BBF313ADEEFD43" KeyPath="yes" Source="SourceDir\jacktrip.wixobj" />
+            </Component>
+            <Component Id="cmpEF56078548B21E7891727FDE729B864B" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil01A07DE9CBC162E11AA3C8EFB4A4D4B9" KeyPath="yes" Source="SourceDir\JackTrip.wixpdb" />
+            </Component>
+            <Component Id="cmp63BFF65F319CB52D38DBB3FE6B8DE6F6" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil886DC1D217D62357ED536BCFF93E7C0F" KeyPath="yes" Source="SourceDir\jacktrip.wxs" />
+            </Component>
+            <Component Id="cmp64BB59E0E19E219D4D4D5C775D71731F" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil7D228BFB31A8A99EE307AF8BFC2E1841" KeyPath="yes" Source="SourceDir\LICENSE.md" />
+            </Component>
+            <Component Id="cmpE2BC49F17425202019B2C48873956253" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil011E2822115DD1D5BD9A9096D1914F51" KeyPath="yes" Source="SourceDir\license.rtf" />
+            </Component>
+            <Component Id="cmpC2B5C938CDBD7BD0FAE06ACAABDB3B54" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil025966B7C1FB522423ADF8E9CE443022" KeyPath="yes" Source="SourceDir\opengl32sw.dll" />
+            </Component>
+            <Component Id="cmpD16A82FC04DA86995B222E5199ABF99B" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil03546A9C6805C45BEB0A60D29FFC5D22" KeyPath="yes" Source="SourceDir\qt6.wixobj" />
+            </Component>
+            <Component Id="cmp2A0D7AC2E166178EE2B0C41EB50258BA" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil2E03208E0826B1237D2BAB19634F2DC3" KeyPath="yes" Source="SourceDir\qt6.wxs" />
+            </Component>
+            <Component Id="cmp7111706DBFB0E180EC7A8BDC93A90139" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil1A08189509DCFE1E185AEC6B499929BA" KeyPath="yes" Source="SourceDir\Qt6Core.dll" />
+            </Component>
+            <Component Id="cmpD22BE3A3E3A804D9EC17161FD6ED218F" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil0427F270C0F2EA2C38D6BB72F32BC982" KeyPath="yes" Source="SourceDir\Qt6Gui.dll" />
+            </Component>
+            <Component Id="cmp9FB603605D751CA9FE101013C48671B7" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filFE4FFAF140893FFCBD7F595F88958077" KeyPath="yes" Source="SourceDir\Qt6Network.dll" />
+            </Component>
+            <Component Id="cmp62FBAA57D2AA6EAC6A5C151902D999B3" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil720F59CECD8953F508A84670C46B640F" KeyPath="yes" Source="SourceDir\Qt6OpenGL.dll" />
+            </Component>
+            <Component Id="cmp6F8A9D8010540EBE393F23E015D5A792" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil1192A9C8E95EF194DCF3EF7BE626B48E" KeyPath="yes" Source="SourceDir\Qt6Positioning.dll" />
+            </Component>
+            <Component Id="cmpB6D50DE19D6ADD781A08727F7F6266E8" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filCF6DBEF1D6A13C80715404D32B934AD5" KeyPath="yes" Source="SourceDir\Qt6Qml.dll" />
+            </Component>
+            <Component Id="cmp4E9BFD75DF26062E9937847116425997" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filAE1BF7A9D9BF060761C9FAAB52C1582A" KeyPath="yes" Source="SourceDir\Qt6QmlLocalStorage.dll" />
+            </Component>
+            <Component Id="cmp65C2886373B0C96AD79BAD6B11F4BCEC" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil1A464A34B69E38E90B0748094F580925" KeyPath="yes" Source="SourceDir\Qt6QmlModels.dll" />
+            </Component>
+            <Component Id="cmp609AE9C23E17BA20AC06AEA3F3551DAE" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil70941CA86B91BA9374327AE51424DF95" KeyPath="yes" Source="SourceDir\Qt6QmlWorkerScript.dll" />
+            </Component>
+            <Component Id="cmpF560A3BA9C9935A03A5C37CBAFF8321F" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil123F08A484F5DF5CC72BFCCDC197DBF9" KeyPath="yes" Source="SourceDir\Qt6QmlXmlListModel.dll" />
+            </Component>
+            <Component Id="cmpED65C4A3E125070BC17313D1A32F9C76" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filFDA17D38CD6D73072EBAF9E430B8DCD4" KeyPath="yes" Source="SourceDir\Qt6Quick.dll" />
+            </Component>
+            <Component Id="cmpA463F9E3D9E20729888D635B58292550" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filDF57D75497458D7E64C07BB9AD1456C9" KeyPath="yes" Source="SourceDir\Qt6QuickControls2.dll" />
+            </Component>
+            <Component Id="cmp0571EF0C5FB8E314588A9065CF74AE35" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filDC1C47A2ADA5BD76E86CE6CE8EC4F5B4" KeyPath="yes" Source="SourceDir\Qt6QuickControls2Impl.dll" />
+            </Component>
+            <Component Id="cmp2BB41ECBA565E0A0B4EDF020A49025F9" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil57860EF8422A74A3CE88A88BAD0CABB7" KeyPath="yes" Source="SourceDir\Qt6QuickDialogs2.dll" />
+            </Component>
+            <Component Id="cmp73D256572698B6DB61D05CCD551BEF9A" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filC9FA5D0D485CF88CB948BE06870C3B8A" KeyPath="yes" Source="SourceDir\Qt6QuickDialogs2QuickImpl.dll" />
+            </Component>
+            <Component Id="cmpC26015BC765576311A63C5B026DB71FF" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil2911EE010D604A656A176E72D572CCAC" KeyPath="yes" Source="SourceDir\Qt6QuickDialogs2Utils.dll" />
+            </Component>
+            <Component Id="cmp2ABC8A6031A24B9F3C70C18AE40B4CC3" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil0C6C0D67878BFB0A4BD30E472A40969B" KeyPath="yes" Source="SourceDir\Qt6QuickEffects.dll" />
+            </Component>
+            <Component Id="cmpD246CE6E593DB826C58A42EFAB8AC176" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil89BECDEB84C26F37AC111B6D300D2835" KeyPath="yes" Source="SourceDir\Qt6QuickLayouts.dll" />
+            </Component>
+            <Component Id="cmp05C682DFA26731F5F96864E240BB9367" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filF00A542F2EB321619F0825C65334A7F5" KeyPath="yes" Source="SourceDir\Qt6QuickParticles.dll" />
+            </Component>
+            <Component Id="cmpB2448C4481D00046098304AC1C230B93" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil359D410486F61CCCDFEFE2410735AC75" KeyPath="yes" Source="SourceDir\Qt6QuickShapes.dll" />
+            </Component>
+            <Component Id="cmpD3B7B317B2EAD7974F7386A4F4137733" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filA1B7D830C14FF020BDC9FAF8F83402A1" KeyPath="yes" Source="SourceDir\Qt6QuickTemplates2.dll" />
+            </Component>
+            <Component Id="cmpD5FD28F099F28B38A8C5C6C43481E545" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil1A12F96CDBED6EDBD83A1228E643DFE1" KeyPath="yes" Source="SourceDir\Qt6ShaderTools.dll" />
+            </Component>
+            <Component Id="cmpDAA5AE12CA74181FE29953002CC830A3" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filEF75955C67A60E62CD86B26117C750AE" KeyPath="yes" Source="SourceDir\Qt6Sql.dll" />
+            </Component>
+            <Component Id="cmp2BFDE38E15CC8879E8B911A7A296477A" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil08B70D0B09545B2C830255009EF93341" KeyPath="yes" Source="SourceDir\Qt6Svg.dll" />
+            </Component>
+            <Component Id="cmp501A53153FAB3908951DADAE039AB865" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filDAAFC08AB32ED0F61D6C56EA5B665627" KeyPath="yes" Source="SourceDir\Qt6WebChannel.dll" />
+            </Component>
+            <Component Id="cmp5046A1A989E72DACE21F8D481D0D71C5" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil4DF01BF67F8DD9C408204A2F5BD56FF9" KeyPath="yes" Source="SourceDir\Qt6WebEngineCore.dll" />
+            </Component>
+            <Component Id="cmp8015426E62DE3ED33343DB50BE0A9A0D" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil58988C3D49189FE2287F68174F6944A2" KeyPath="yes" Source="SourceDir\Qt6WebEngineQuick.dll" />
+            </Component>
+            <Component Id="cmp268C15DC40B82F983C534C9C3DAABD1B" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filA069DC2EC3525A8E547CF9EFC89A25E6" KeyPath="yes" Source="SourceDir\Qt6WebEngineQuickDelegatesQml.dll" />
+            </Component>
+            <Component Id="cmp7B3BE582AD25B07FE55DD249E6D5C333" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filF8FC62A4978258AB67C456CC256AAA8B" KeyPath="yes" Source="SourceDir\Qt6WebSockets.dll" />
+            </Component>
+            <Component Id="cmpA72EB9BC77F4E13CA8F69B2B9413199C" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil84A612B35A3B234B8729B7998FD0742D" KeyPath="yes" Source="SourceDir\Qt6WebView.dll" />
+            </Component>
+            <Component Id="cmp9CC20E8457A681268437C2AC7D562FE2" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filA8C33CBAC2EC4965ED511DF205E7961F" KeyPath="yes" Source="SourceDir\Qt6WebViewQuick.dll" />
+            </Component>
+            <Component Id="cmpBCCA6FD4EC03D708E3811B7D4723801F" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="filAC348090A125BCB41DB84097A13158A0" KeyPath="yes" Source="SourceDir\Qt6Widgets.dll" />
+            </Component>
+            <Component Id="cmp648D7B3CDDA6427D7237CE400D68C36A" Directory="TARGETDIR" Guid="PUT-GUID-HERE">
+                <File Id="fil71DF3511D06EBF7BFB8342792898BEE4" KeyPath="yes" Source="SourceDir\QtWebEngineProcess.exe" />
+            </Component>
+            <Component Id="cmpE18AE4F92C6862587708BF72BAD1A16B" Directory="dir5986BBEEE82C5D978676757AB9BBF16F" Guid="PUT-GUID-HERE">
+                <File Id="fil0AE27F0E2C805CBF8F3E16544E661FA8" KeyPath="yes" Source="SourceDir\generic\qtuiotouchplugin.dll" />
+            </Component>
+            <Component Id="cmpE3D572FB13B4249EA0FB18BD0F44DE18" Directory="dirB10C059F651528B59E1FD268EB08C6D6" Guid="PUT-GUID-HERE">
+                <File Id="fil7320C9B04E87091E801ED591E25C7F1E" KeyPath="yes" Source="SourceDir\iconengines\qsvgicon.dll" />
+            </Component>
+            <Component Id="cmp5D66BEFD65BDC9AC7D48118D254EBF30" Directory="dir20189C824D4DDABC3D9FF9449C089CE0" Guid="PUT-GUID-HERE">
+                <File Id="fil167B858704F9A353B556ECC7639B590E" KeyPath="yes" Source="SourceDir\imageformats\qgif.dll" />
+            </Component>
+            <Component Id="cmp2A9FEB410F41B453B0CA26766D4AD20C" Directory="dir20189C824D4DDABC3D9FF9449C089CE0" Guid="PUT-GUID-HERE">
+                <File Id="filB7BCDEF234107021D6B84547174B24BC" KeyPath="yes" Source="SourceDir\imageformats\qico.dll" />
+            </Component>
+            <Component Id="cmp71D8FCDDEED93F03008649285B372ADA" Directory="dir20189C824D4DDABC3D9FF9449C089CE0" Guid="PUT-GUID-HERE">
+                <File Id="fil22403FED9C4013C9328A99701DEA8873" KeyPath="yes" Source="SourceDir\imageformats\qjpeg.dll" />
+            </Component>
+            <Component Id="cmpC07A85EAE90864EB28FD65896AE228D2" Directory="dir20189C824D4DDABC3D9FF9449C089CE0" Guid="PUT-GUID-HERE">
+                <File Id="fil310D7F5AA577821D98D425950E96E344" KeyPath="yes" Source="SourceDir\imageformats\qsvg.dll" />
+            </Component>
+            <Component Id="cmpC3E9129E6B641C851B841BF3E6748387" Directory="dir690B8E6D609B0DB17DB71E8B1FB2FEC5" Guid="PUT-GUID-HERE">
+                <File Id="fil853664287ADF8100B5B8E8FB61D63CB0" KeyPath="yes" Source="SourceDir\LICENSES\AVC.txt" />
+            </Component>
+            <Component Id="cmpB46D0962D2340404F851F10CAAECA549" Directory="dir690B8E6D609B0DB17DB71E8B1FB2FEC5" Guid="PUT-GUID-HERE">
+                <File Id="filFCC7617B3CC611B027C8B3FB6DE6EA20" KeyPath="yes" Source="SourceDir\LICENSES\GPL-3.0.txt" />
+            </Component>
+            <Component Id="cmp65AA9556A3F4786F01B316E5DEA9E6F6" Directory="dir690B8E6D609B0DB17DB71E8B1FB2FEC5" Guid="PUT-GUID-HERE">
+                <File Id="filC36144D44D8E4AE0EFCD137DD2752E9D" KeyPath="yes" Source="SourceDir\LICENSES\LGPL-3.0-only.txt" />
+            </Component>
+            <Component Id="cmp727893E2EF55BD50F18FDD0ECF2C824F" Directory="dir690B8E6D609B0DB17DB71E8B1FB2FEC5" Guid="PUT-GUID-HERE">
+                <File Id="fil52FC0319C49E6B01FF651770DA57735F" KeyPath="yes" Source="SourceDir\LICENSES\MIT.txt" />
+            </Component>
+            <Component Id="cmp907AF1F6FA03E2DE91C9FEA025010D55" Directory="dir4C08960BD61EEE75AE4A87FC65A5DE2A" Guid="PUT-GUID-HERE">
+                <File Id="filD108AA20F08DED7C4D3797E896B5D090" KeyPath="yes" Source="SourceDir\networkinformation\qnetworklistmanager.dll" />
+            </Component>
+            <Component Id="cmp830C378ADEFF439CEF6C0F2550FD4D2F" Directory="dir826692792B0995C8DF68919F5C684331" Guid="PUT-GUID-HERE">
+                <File Id="filF7B01E0AFAD9057CB024B85137A38049" KeyPath="yes" Source="SourceDir\platforms\qwindows.dll" />
+            </Component>
+            <Component Id="cmp768C303F890F86F0F1F870D66E04EF1D" Directory="dirC15EA267FAD4FC447E84FB3A32CBDB68" Guid="PUT-GUID-HERE">
+                <File Id="fil9E350077F6BEF422DA69CB20F14812C9" KeyPath="yes" Source="SourceDir\position\qtposition_nmea.dll" />
+            </Component>
+            <Component Id="cmp7C2CB506AB0D7E9648A5F2A7C2C7770A" Directory="dirC15EA267FAD4FC447E84FB3A32CBDB68" Guid="PUT-GUID-HERE">
+                <File Id="filC58409E068EE2AD6FE8C6C57174D6DDD" KeyPath="yes" Source="SourceDir\position\qtposition_positionpoll.dll" />
+            </Component>
+            <Component Id="cmp8539D5D6C3427F1F4C82034569A0F132" Directory="dirC15EA267FAD4FC447E84FB3A32CBDB68" Guid="PUT-GUID-HERE">
+                <File Id="filEF2EB40CA1790B7BDC4320E6F7638744" KeyPath="yes" Source="SourceDir\position\qtposition_winrt.dll" />
+            </Component>
+            <Component Id="cmp0BF1B39B0DF9604C417C59F0C26849DB" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil886A06C8346FCEDB309427F45C47C7A2" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\Blend.qml" />
+            </Component>
+            <Component Id="cmpA4F5F7FBAA6092D9B23D7A86DA914EEF" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="filAA448C1B459D0720F374A6B1B96F738F" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\BrightnessContrast.qml" />
+            </Component>
+            <Component Id="cmp0279BCBDD30B4C6C44CF17FF2C57554F" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil131C97BF2FB605E8AF69F25A5F2B03F1" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\Colorize.qml" />
+            </Component>
+            <Component Id="cmp4F124E7E9DFC88F42758D7A8DDC4FEDE" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="filD1580F6EE199C19C089B8A65E29A49D6" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\ColorOverlay.qml" />
+            </Component>
+            <Component Id="cmp400F9069DE382BAC36209FCBF66A9459" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="filFBACB681C118E8C1F036D2CCCF4CE943" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\ConicalGradient.qml" />
+            </Component>
+            <Component Id="cmpCDDF8FFEE802C8865EBC55B7B0DBB6CE" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="filF35B45A1A450B0549BAF81E355E40B11" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\Desaturate.qml" />
+            </Component>
+            <Component Id="cmp79AA1A0363899DA7492F66E48C9C48A4" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="filA083A38F1B9CE852DEB1263C029CE98B" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\DirectionalBlur.qml" />
+            </Component>
+            <Component Id="cmpA7F203332ABBC3EC7A402FF07AF196CC" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="filE6AD0D99E1AA14FE51D00A5C09C717CE" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\Displace.qml" />
+            </Component>
+            <Component Id="cmpAC6C7513DACC293DBE22003E115E1829" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil808763D4BBA317BC8CC96AFB715EAA5C" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\DropShadow.qml" />
+            </Component>
+            <Component Id="cmp829A7D43222A942FAAC1E34390AB20C7" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil6C2B7CCE2D3FD30A6C5E831858FC36ED" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\FastBlur.qml" />
+            </Component>
+            <Component Id="cmp1B75439E18656D4971DD74295C5A6FE9" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil8ECC49187862A16FCFF10E28310443B9" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\GammaAdjust.qml" />
+            </Component>
+            <Component Id="cmp5F5DA8B0DE474072A479802CCFAAFECD" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil430108EEBBEAA2DDEB424F77B9FB9B2A" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\GaussianBlur.qml" />
+            </Component>
+            <Component Id="cmpA47F3D27B19B18EF22E22C62858B5E5D" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="filFA73A8C700D83E467CC41CCFCFE1D705" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\Glow.qml" />
+            </Component>
+            <Component Id="cmpD78893AC3CDF13AB163A32E432380213" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil4A2470F1F3421C2F1A6C3C5A949C89C2" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\HueSaturation.qml" />
+            </Component>
+            <Component Id="cmp0167F714462880F7DEA8F99E51540614" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="filC330E4696CEFD1D7DB358351D7891F8C" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\InnerShadow.qml" />
+            </Component>
+            <Component Id="cmp306CB3F03C173E5F7C4C5E879CA004BA" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil965E7EC2C181D00DA21047E30607CA5A" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\LevelAdjust.qml" />
+            </Component>
+            <Component Id="cmpD2D5DEE53569513FCE60FA9329F19C10" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil05A53503FC92D8E4D1AEAB21BA8744E1" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\LinearGradient.qml" />
+            </Component>
+            <Component Id="cmp51661D56CE8F6159EC2EF6F5C4423E2A" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil8D1ED956BFFB0E6AA4B8250788A62613" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\MaskedBlur.qml" />
+            </Component>
+            <Component Id="cmp964D238CB7CF2027CB6B00D34C3E22D3" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil2000425A1E4C194D33EDDCECA027DFFA" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\OpacityMask.qml" />
+            </Component>
+            <Component Id="cmp74C1EF4971B64332C02FE2C556E19729" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil7EF6C79CC3325C12E745025BCE0C9FBE" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp242486FD4A56A0063C7DE0D89F6E290A" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil30F83679D1E91454CD2098287A07D8CF" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\qmldir" />
+            </Component>
+            <Component Id="cmpC69C56E9C15ED4985A3B3E94CFE682FE" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil8A82A66E4A720F6A76D85BD109FDB6D4" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\qtgraphicaleffectsplugin.dll" />
+            </Component>
+            <Component Id="cmp9BF26A49C09D0550F21706EAF6A2DE75" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="filC4A9C657EEB31EB6D21EE9A2F85FBBC1" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\RadialBlur.qml" />
+            </Component>
+            <Component Id="cmpC630A8D81781F2B1FB39973C4D294FEE" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil350FD0954A2C72003C274E1CF5910255" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\RadialGradient.qml" />
+            </Component>
+            <Component Id="cmpDE47D419CE84778D35D367DB62B29035" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil66FA38DBB7F8A5EFADC7DF07FF481F27" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\RectangularGlow.qml" />
+            </Component>
+            <Component Id="cmpD553D921E6FBBEF3C45277226BE9A664" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="filB94184A4AAB2C694E4E2F9BFA5C7CB2F" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\RecursiveBlur.qml" />
+            </Component>
+            <Component Id="cmp3810D3211D6B3BAD793BC0DAE002A83E" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="fil5265371F3D4E09D4A5E7E08CB48C1E34" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\ThresholdMask.qml" />
+            </Component>
+            <Component Id="cmpBFBC007CFA4A04FBA6F2BD656FA19C10" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="PUT-GUID-HERE">
+                <File Id="filFBFB37751870916C3295B03320D7F433" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\ZoomBlur.qml" />
+            </Component>
+            <Component Id="cmpCE58CC32A131543BE1958FE0BAD85A10" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="PUT-GUID-HERE">
+                <File Id="filD779BE04A0038E36398257980C7C15C2" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\DropShadowBase.qml" />
+            </Component>
+            <Component Id="cmp934F9881CA679A03ADEB025F6FF93CD4" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="PUT-GUID-HERE">
+                <File Id="fil4651AFBE560B3D1E75CB4E929421A23B" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\FastGlow.qml" />
+            </Component>
+            <Component Id="cmpB878398DBEBC014BD96843EA6A95344B" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="PUT-GUID-HERE">
+                <File Id="fil3DEBA52D9592EB84679B648C2AA2B6AC" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\FastInnerShadow.qml" />
+            </Component>
+            <Component Id="cmp7A07163838464B60F6D2537CA7CE8833" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="PUT-GUID-HERE">
+                <File Id="filE5392E34CA263822C222D0BF9639227C" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\GaussianDirectionalBlur.qml" />
+            </Component>
+            <Component Id="cmp7D423B9F37902140337965C759A1E539" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="PUT-GUID-HERE">
+                <File Id="fil254C6297F09891595F6710F3797AD065" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\GaussianGlow.qml" />
+            </Component>
+            <Component Id="cmpFF2B9D0ECD8FE0E31F0959E9C48B18E4" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="PUT-GUID-HERE">
+                <File Id="fil481983D43BC51BBBD60DDA436F0D66A5" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\GaussianInnerShadow.qml" />
+            </Component>
+            <Component Id="cmp694B7AB632639A0A5938C277A7B201EA" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="PUT-GUID-HERE">
+                <File Id="fil6FC8836A55A46A87A0DB0296C533A676" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\GaussianMaskedBlur.qml" />
+            </Component>
+            <Component Id="cmp8F06226703AF483B2638C63BFB9B19F8" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="PUT-GUID-HERE">
+                <File Id="fil4D90ECF583474B1CB59DAEBD2052BA48" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp7362962F640F2FD7E74F1C095DBDAF30" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="PUT-GUID-HERE">
+                <File Id="fil11A9BFD23D2CD176E57F5EED8210F5EC" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\qmldir" />
+            </Component>
+            <Component Id="cmp2AE301345F7F9CB39B6986A6FE1B7770" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="PUT-GUID-HERE">
+                <File Id="filFEA2206E424A82D72528F1EA358D0B69" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\qtgraphicaleffectsprivateplugin.dll" />
+            </Component>
+            <Component Id="cmp8EABE0B8AEB6B59BBA116FC21CB7E5F2" Directory="dir92990262DE0ED5508C44523003A4B360" Guid="PUT-GUID-HERE">
+                <File Id="fil0117CF5445EF1792DFCE8ED7540F81CA" KeyPath="yes" Source="SourceDir\qml\QtQml\qmldir" />
+            </Component>
+            <Component Id="cmp0C776837FB1ACA4E4665746624A1F66D" Directory="dir92990262DE0ED5508C44523003A4B360" Guid="PUT-GUID-HERE">
+                <File Id="fil4003B93F14AAC88E53C18C94ACEB86B8" KeyPath="yes" Source="SourceDir\qml\QtQml\qmlmetaplugin.dll" />
+            </Component>
+            <Component Id="cmp276C50F9766B97EB467160CC10A6A356" Directory="dirD3E9E07666E50DD0B119271684422637" Guid="PUT-GUID-HERE">
+                <File Id="fil0F05D62E45DA4E9EFA2FBFC0D3553F36" KeyPath="yes" Source="SourceDir\qml\QtQml\Base\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp517B71582230A4698CBF9E3F6048550D" Directory="dirD3E9E07666E50DD0B119271684422637" Guid="PUT-GUID-HERE">
+                <File Id="filEEB49B8A5C313E798DCB3FC5DE04D86F" KeyPath="yes" Source="SourceDir\qml\QtQml\Base\qmldir" />
+            </Component>
+            <Component Id="cmp870C95E4B9F919D6A3987C4F468D8305" Directory="dirD3E9E07666E50DD0B119271684422637" Guid="PUT-GUID-HERE">
+                <File Id="filEED866F0018D6A66CAAAC4D5E08D6EAA" KeyPath="yes" Source="SourceDir\qml\QtQml\Base\qmlplugin.dll" />
+            </Component>
+            <Component Id="cmpA1F1D19580CB3E153905BE6B71792592" Directory="dirAA6B0932D46A7D6E35A8D1E06E10B2B7" Guid="PUT-GUID-HERE">
+                <File Id="filF67E33CF8E5416727B865F3F9EA9A8E7" KeyPath="yes" Source="SourceDir\qml\QtQml\Models\modelsplugin.dll" />
+            </Component>
+            <Component Id="cmp5000F310EFD82E6287E9903FD51B885C" Directory="dirAA6B0932D46A7D6E35A8D1E06E10B2B7" Guid="PUT-GUID-HERE">
+                <File Id="fil2D905E3A0B5933A0F7375843912EED30" KeyPath="yes" Source="SourceDir\qml\QtQml\Models\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp0175AC12A4D0739783808D434C36AFFD" Directory="dirAA6B0932D46A7D6E35A8D1E06E10B2B7" Guid="PUT-GUID-HERE">
+                <File Id="fil5737825369666B91605A34E0CA2760DB" KeyPath="yes" Source="SourceDir\qml\QtQml\Models\qmldir" />
+            </Component>
+            <Component Id="cmp9DE654388B48EFE9A6FA6FDF28BAD73F" Directory="dir92A51CED876E1F4286E6663FD7D54CDA" Guid="PUT-GUID-HERE">
+                <File Id="fil29D45524912C32AD2DBC7786CAB9C815" KeyPath="yes" Source="SourceDir\qml\QtQml\WorkerScript\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp133E0981CE8D29649AE8FB1932031C2C" Directory="dir92A51CED876E1F4286E6663FD7D54CDA" Guid="PUT-GUID-HERE">
+                <File Id="filB1F22C93A956BB2A8EE2FD2B01308963" KeyPath="yes" Source="SourceDir\qml\QtQml\WorkerScript\qmldir" />
+            </Component>
+            <Component Id="cmp08D9C9933EFB91871225B53380202A87" Directory="dir92A51CED876E1F4286E6663FD7D54CDA" Guid="PUT-GUID-HERE">
+                <File Id="filAEEE5B727783E6621123551D42A7F31B" KeyPath="yes" Source="SourceDir\qml\QtQml\WorkerScript\workerscriptplugin.dll" />
+            </Component>
+            <Component Id="cmp30CA6BFC8F040C1F22D888FD79DC9B1B" Directory="dirD48123A7B6E2CFB660EC94F0CBE0276D" Guid="PUT-GUID-HERE">
+                <File Id="fil49ED9A9A100F4B7848AF9A02B69490F3" KeyPath="yes" Source="SourceDir\qml\QtQml\XmlListModel\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp50357C4AABC622F51AD737AB1256FE31" Directory="dirD48123A7B6E2CFB660EC94F0CBE0276D" Guid="PUT-GUID-HERE">
+                <File Id="filFBD23B1E55B401CA6C9778F361D5EC66" KeyPath="yes" Source="SourceDir\qml\QtQml\XmlListModel\qmldir" />
+            </Component>
+            <Component Id="cmp86A326EFCF0E1704C41D4A3BE3B2B50C" Directory="dirD48123A7B6E2CFB660EC94F0CBE0276D" Guid="PUT-GUID-HERE">
+                <File Id="fil52EA3E4F82010DABF3A854BA9390EEAC" KeyPath="yes" Source="SourceDir\qml\QtQml\XmlListModel\qmlxmllistmodelplugin.dll" />
+            </Component>
+            <Component Id="cmpE4E1A33E38557FB60B3F45555FD20ECE" Directory="dir4B7D2A19DD0D33E235AA7BD43615A87E" Guid="PUT-GUID-HERE">
+                <File Id="fil1C0EEC7BC400302C41630BA92C1C307F" KeyPath="yes" Source="SourceDir\qml\QtQuick\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp1ED74D34F649FA02DC949AD36738A154" Directory="dir4B7D2A19DD0D33E235AA7BD43615A87E" Guid="PUT-GUID-HERE">
+                <File Id="fil7CD6FFB1D436DB58018DC2DB86CBA0D2" KeyPath="yes" Source="SourceDir\qml\QtQuick\qmldir" />
+            </Component>
+            <Component Id="cmp4DB15011F21E3B03E2E0F0F9D4BB573D" Directory="dir4B7D2A19DD0D33E235AA7BD43615A87E" Guid="PUT-GUID-HERE">
+                <File Id="filBC92394F04FDB0CE1F9CF582A4F84C36" KeyPath="yes" Source="SourceDir\qml\QtQuick\qtquick2plugin.dll" />
+            </Component>
+            <Component Id="cmp4DCBBC4A26D0C4FB3623DD878E02CFCE" Directory="dir175FB1A22F883092D2FC39E138CDC5FC" Guid="PUT-GUID-HERE">
+                <File Id="fil59DCF0C6D0B4BBBA10A36EB12B34D2A5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpE325B44584CD20BCE520071161F80F06" Directory="dir175FB1A22F883092D2FC39E138CDC5FC" Guid="PUT-GUID-HERE">
+                <File Id="fil6115FDE68D8D8FF3AFCDD9E24BDB4562" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\qmldir" />
+            </Component>
+            <Component Id="cmp20C01608F59E2EEB9AB45A6904212E9A" Directory="dir175FB1A22F883092D2FC39E138CDC5FC" Guid="PUT-GUID-HERE">
+                <File Id="filEA309A6F3A745EE08925BE2C9654289D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\qtquickcontrols2plugin.dll" />
+            </Component>
+            <Component Id="cmp8C93A33CFCA426C5180507A0B98BEBF0" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filBC369AE402DD38E53E6923350FDF5653" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\AbstractButton.qml" />
+            </Component>
+            <Component Id="cmp8A493B71973648EC2B057CA3374E1EE7" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filAFDBFFDF61F1880A7BBC4EF9A8831880" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Action.qml" />
+            </Component>
+            <Component Id="cmp18CDA922328CA34B0C584CBCA1FF9A07" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filF9E61D4DD55ADA9EF9D5FCF3C9742A6D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ActionGroup.qml" />
+            </Component>
+            <Component Id="cmp6A676B107BB40104A39E3A1FC9920DC5" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil9ED46CC79E64F252F39C9F5D48BA97F2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmpC4574DD49EE73BBD7C9D46D19CABCE00" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil428F79F811C7EFFFF5EFD5D1C385E603" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\BusyIndicator.qml" />
+            </Component>
+            <Component Id="cmp5E103D9543FED51F3FEC441D27E30CFD" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filED6BC08EAA7BDC1922E5EF3FA89593F5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Button.qml" />
+            </Component>
+            <Component Id="cmp586B84D1D2ED76699E38F3C103B00E34" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil63B2A244080B179569FD25B2AF2B8541" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ButtonGroup.qml" />
+            </Component>
+            <Component Id="cmpA98BD23094F94C354CF3233853D1FDEF" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filEE471E4AE1024C841C83A1939CBDA962" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Calendar.qml" />
+            </Component>
+            <Component Id="cmp6F97518E97DEEDE1E4CF4A892F43D8F3" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filA9466F387D591B348EBE6EF066AD3CBF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\CalendarModel.qml" />
+            </Component>
+            <Component Id="cmp8D6C1B322664DB792ADAD70140F12A7B" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil86E157C28F2862D0ABF4871B3EA0E1F9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\CheckBox.qml" />
+            </Component>
+            <Component Id="cmpA42169CDD08727E08118CB82A57B045D" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil8639CCFCC7900B80AAD254F318DE147B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\CheckDelegate.qml" />
+            </Component>
+            <Component Id="cmp939FB89E3AE89DB11ADDE1E4DF7ED3BD" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filFAC4CA844ADE51BD382B91DB80B0B1B5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ComboBox.qml" />
+            </Component>
+            <Component Id="cmpC1F61C19635F2E06D9A1CE39041C8532" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filDD413399B6656F652D996E2902E9EFC3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Container.qml" />
+            </Component>
+            <Component Id="cmp7884BA20ED773D52ECA78F771554B413" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil78BC91EA0996852B81A2216964214BC9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Control.qml" />
+            </Component>
+            <Component Id="cmpBAD59A78213E776132B2DCB9B916AB52" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil89ED420BA26EAEE076934735A91F5C7A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\DayOfWeekRow.qml" />
+            </Component>
+            <Component Id="cmp6043FBD10B6CD12C84234368659A1E5E" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil57496F5B57AE6062BBC6398A651E7B29" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\DelayButton.qml" />
+            </Component>
+            <Component Id="cmp39E2417580498483F716655E718C8AEF" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil7B56B2CD7FAD68FC93729CB091088B95" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Dial.qml" />
+            </Component>
+            <Component Id="cmp8230F1ECB5AF410493C4F0F5DDCCFA6F" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil95C9D3E07FA41700EE546DA8D0F58A34" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Dialog.qml" />
+            </Component>
+            <Component Id="cmp1B8E24691E5DE807D238D41F1383C726" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil9461883C6FCD2F2B63E7DF897AB598F1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\DialogButtonBox.qml" />
+            </Component>
+            <Component Id="cmpAE8BB0CE07CF6799D67E194CE71F3B49" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filD9F7FECF0A814191B777A7DD1F8226CF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Drawer.qml" />
+            </Component>
+            <Component Id="cmp58BE65189C75757B4B1AE73EE8BB9ED4" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filCA18B4E8037B6D504FE019B1420FC262" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Frame.qml" />
+            </Component>
+            <Component Id="cmpAB65A995029614C0BEAE885EAC615969" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filCE960895937BB21868B32B21265D5549" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\GroupBox.qml" />
+            </Component>
+            <Component Id="cmp53BB29730C916C703FEB130DE6C869A3" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil6ADB14CFBBA72C53EBBCB499437D0768" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\HorizontalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp9F2A20383C3A799374E705196D5FC47B" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil1381FCD6D7340BA1D8F7061E24E9EE5F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ItemDelegate.qml" />
+            </Component>
+            <Component Id="cmpD26A15138E0B093D70CA9ED2F43A064C" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil4BA7BF07A214D0A01C1F25AADCE273B5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Label.qml" />
+            </Component>
+            <Component Id="cmpBB6421C1FFC022560343F36A95041222" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filC3BFD0172E5E685EC33C8DB87BAD76F9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Menu.qml" />
+            </Component>
+            <Component Id="cmp4DDAC1E3DE44CC8CBCFBBA3D06CB210F" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil7586BC2AE23CD3902193281F02C0248E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\MenuBar.qml" />
+            </Component>
+            <Component Id="cmpE04A95727C978383CBCB49D97A1EE446" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil09AEE5F0185B9FA68C9FE84BDF529F13" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\MenuBarItem.qml" />
+            </Component>
+            <Component Id="cmpED38B1F00D1225238E2214684DB828EA" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filD1F97BD06A655ED84342ABFF251174B7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\MenuItem.qml" />
+            </Component>
+            <Component Id="cmpC3A79272F4A6970E0BE4FC64790E430F" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filD6552C22B9778DB2B92913DCBD9D9498" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmp5867F3DFD6DC4C9602E1434B07383698" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filBF24CB2E7FA2AF9ED3022E6940B3E105" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\MonthGrid.qml" />
+            </Component>
+            <Component Id="cmp0B86A2BA0897185E793369896802E96E" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil8422F050DD9CDA42ECEBB1EE99208842" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Page.qml" />
+            </Component>
+            <Component Id="cmp30F08C16F7664A7FD8B800EA571EC600" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil3F4C9B4E6543791FEA602007B5F61BE1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\PageIndicator.qml" />
+            </Component>
+            <Component Id="cmp371D1295B34F7D85878351584703AAFD" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil4D147319344129E9162821D4F667BCBE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Pane.qml" />
+            </Component>
+            <Component Id="cmpADED35243A1E1A4A4128ED7E402E6B2D" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil92A903FFCC15DB6E98E7B155939EAF13" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp85892AE6398EB099ACD7237FDDBC2663" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil75D7C7EFE499CAC1AF6DEFB6E0E0F660" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Popup.qml" />
+            </Component>
+            <Component Id="cmp41645B8851CC729CE8A2340B2D8EA8C9" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil9208F77249384F46D7811C30F6DE22CC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmp83C3068A93D919B7751F6806A67BC010" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil858FBF534E1F3A1E9989698AF3D874E4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\qmldir" />
+            </Component>
+            <Component Id="cmp2972A3AC06DB36DE9B0A67D2D06F4C28" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil8627AA72A823194DC9BC9F9DE84E4F99" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\qtquickcontrols2basicstyleplugin.dll" />
+            </Component>
+            <Component Id="cmpBA1D1B1B1B7C084AF7E1637AAC309AEC" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil4C6516E5D60D9651A2163C069495D184" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\RadioButton.qml" />
+            </Component>
+            <Component Id="cmp0E92111B9450F4A6D90ADE794FEA6196" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil412584106AC4AE0333ABAF108A63EC27" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\RadioDelegate.qml" />
+            </Component>
+            <Component Id="cmpE47FFACC3F4516133E303BDD889C9D1F" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil6BB3A845EF3A85E599DBC7B0E7FE0EE0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\RangeSlider.qml" />
+            </Component>
+            <Component Id="cmp91A119FC3A906D3D240EEECF8AE9D3D4" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filD9F784F19DF0CD98179945DEEBA488EE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\RoundButton.qml" />
+            </Component>
+            <Component Id="cmp7ABF5829E0A53A285828259AF55FBD93" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil329C26DD4C9844E64AD2F57A60D15DBD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmp1F0564F62D095E1CBFEB84FC30825319" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil1730A57B782FC190B6284D9F98E6C6E0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ScrollIndicator.qml" />
+            </Component>
+            <Component Id="cmp6803EDB4B203595638FEAEBC927CD68D" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil560A2E2DC775F39445BCAB545B6536D4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ScrollView.qml" />
+            </Component>
+            <Component Id="cmp134BAB74B710580F5A2FB00F8191EC6E" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil17A6F2623E603954DBADC1F1A006D289" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmp822617813D99C83FB8C867E6297D7F6A" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil5EC0830FAD7671CEA41A1F19D213AC89" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Slider.qml" />
+            </Component>
+            <Component Id="cmp2E7DE9133BD9260F859E61FFB2A3B393" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil4173273958A65349F2C0391884F3355C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SpinBox.qml" />
+            </Component>
+            <Component Id="cmp9CE74A8AA17FD942ABD5D69C504AA378" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filAF339182D095266C9795FF9DB6C9E3DC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SplitView.qml" />
+            </Component>
+            <Component Id="cmp634D552A24343C8C14E67902DE8E523D" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filDCC67DE7830A892538D3E3895EBEE12E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\StackView.qml" />
+            </Component>
+            <Component Id="cmp77FF8F56519CF853622F66C585276E22" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil10ED65E5FDA6E24A5D3208A737461CB5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SwipeDelegate.qml" />
+            </Component>
+            <Component Id="cmpFA4790A9B18722CD2DCBBF767423720D" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filBA3E75F9D796AA9CF10B12670E0B2EF2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SwipeView.qml" />
+            </Component>
+            <Component Id="cmp0289EFEC9DC411805D9B2A93ED64AD94" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil439E587C6353C4C42DD76A6B3DAE462D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Switch.qml" />
+            </Component>
+            <Component Id="cmpD5C7BF54A25E68511DC6C3250DA7B211" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil8BD9AE0ABF2958B6EC6D3810D7AF36E5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SwitchDelegate.qml" />
+            </Component>
+            <Component Id="cmp90D750FC0F1A5C8EDF2B836213305DAD" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil0DFFC68F79EC37C44365666166200602" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\TabBar.qml" />
+            </Component>
+            <Component Id="cmpA7AB4877150A0204566F9EEBF7C7B311" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filB98379933BF7D2FDD7496601B4A293CE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\TabButton.qml" />
+            </Component>
+            <Component Id="cmpE22A10A2340956EA89848EED7EFC3017" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil484A760FEF941B3D581C380798DF6CA8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\TextArea.qml" />
+            </Component>
+            <Component Id="cmpFB2EA123216AE3660A5E7E0582BB08A3" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filCB1004A8018D061E4DB564F3732B229B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\TextField.qml" />
+            </Component>
+            <Component Id="cmp737444AC8A9F77EE007AB3754674F6BA" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil16DE34E8016A06111873F00C8323EEBB" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ToolBar.qml" />
+            </Component>
+            <Component Id="cmp72FC9871D2AF77FA278880E76F3A90C8" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil9023FEA2B9D5FD153A9510189AEA1451" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ToolButton.qml" />
+            </Component>
+            <Component Id="cmp3778759A7704F3FC7F8FA14C52E6B5BC" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil0EB13D05ACEC73289F34A64FDAB39CF3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ToolSeparator.qml" />
+            </Component>
+            <Component Id="cmpE0DE716FDEBC4752B543632377A2FDC6" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filF0138391AA22E48C27E7C5916D2BD780" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ToolTip.qml" />
+            </Component>
+            <Component Id="cmp1DE1FC193602D7441B20878A9C193CF8" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil522BA53FA3FD00A7D7492FF5D686E026" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\TreeViewDelegate.qml" />
+            </Component>
+            <Component Id="cmpA57EEC60C15F1B7B2C0075C4D44977ED" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil8E71D51A414BD3C3EDC95B87CAD123D5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Tumbler.qml" />
+            </Component>
+            <Component Id="cmpF99329DD035D76C663CC51B0E752050B" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="filAE77B292C8754E49D307852AF1E7CC70" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\VerticalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp5E91A2344113309F2A9E9E8789C9B1CE" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="PUT-GUID-HERE">
+                <File Id="fil8C4570AAF64E317A211B6CE6288A7083" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\WeekNumberColumn.qml" />
+            </Component>
+            <Component Id="cmp2A8B609424E7043FA8994B527D760FD2" Directory="dir78C3D911E47BC1C88AC3A9AC7D40EEC3" Guid="PUT-GUID-HERE">
+                <File Id="fil00DBA91B02D5D1EEE5E70ECAEB18F097" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\impl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp42740A7DFD675250E1973458E1979562" Directory="dir78C3D911E47BC1C88AC3A9AC7D40EEC3" Guid="PUT-GUID-HERE">
+                <File Id="filB52B6F422718033D4700E70BA12B21B9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\impl\qmldir" />
+            </Component>
+            <Component Id="cmp191CA6324FBD06A3B02B1B7F18C11E96" Directory="dir78C3D911E47BC1C88AC3A9AC7D40EEC3" Guid="PUT-GUID-HERE">
+                <File Id="fil94069F412C19B914E634255D82E89B35" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\impl\qtquickcontrols2basicstyleimplplugin.dll" />
+            </Component>
+            <Component Id="cmp93BBA84C3F04B42711C94532FDCAA4F3" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil123F8809BA31C59BD88BF52E11E576EF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmp63A8EEDAF640D7402B7313F0475DBD62" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filECC91990D06DFF209ACEB521F687ED91" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\BusyIndicator.qml" />
+            </Component>
+            <Component Id="cmp957F39DE371F3BFC42D911ED82508CF5" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil9AF4FD98954AFB8061E24AD7EE4E7666" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Button.qml" />
+            </Component>
+            <Component Id="cmpC645E66FCF36896B3810DFD86446472C" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil02B5E701464A49D5F4EF3F2A82B9F4D5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\CheckBox.qml" />
+            </Component>
+            <Component Id="cmp85AFC164A579E3948ADF3B385C296C37" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filAA59C5193AAAE12FBD0C57BD81BC47A0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\CheckDelegate.qml" />
+            </Component>
+            <Component Id="cmpD9E495923467D9338ADFA40E39588CF5" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filB2583FE5C7C545CB3CC2E15AB85BF2FF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ComboBox.qml" />
+            </Component>
+            <Component Id="cmp8A0F3B485218A94493D8E869CFF7287E" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil56662563D7792ECB8A7B190285334F6F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\DelayButton.qml" />
+            </Component>
+            <Component Id="cmp7D57CA3D1A1FB615289A1F17C05DF5CE" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filBA3E3EF2CAA08B7000373BDB72A26DA1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Dial.qml" />
+            </Component>
+            <Component Id="cmpA1042570ABF7ABC2AC03619A9B623C66" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filB88E409B4C3630D5FF606919350D7219" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Dialog.qml" />
+            </Component>
+            <Component Id="cmp2F3BCEF2AD17BB0CF056453E44B1DA5D" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil68C900F0B3D2BF9559972322D2790CDC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\DialogButtonBox.qml" />
+            </Component>
+            <Component Id="cmpB4E47DE043DEAACAE753D8D57844C6C1" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil2D0A397CE8545EC9F538A9D56CE27F95" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Drawer.qml" />
+            </Component>
+            <Component Id="cmp957C9CBB1420E1B0927FDA4A2795C569" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil6C2E832725A3055A70D4F75F006C864B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Frame.qml" />
+            </Component>
+            <Component Id="cmp0ABF4D43B8185E490D481E9AC8CA7891" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filBD63B5E121A287BAA4308A245BE0121D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\GroupBox.qml" />
+            </Component>
+            <Component Id="cmp6BD6F20FA4FA4D7D100E4F2B9D526B2E" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil592D79350DC3B85519CF2F8F0447D2AF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\HorizontalHeaderView.qml" />
+            </Component>
+            <Component Id="cmpECE8405AEA1D11D330D925F66B0227EF" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil2035CE2CE76094FEB365CAB6AAE48C86" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ItemDelegate.qml" />
+            </Component>
+            <Component Id="cmp1F40775110A3758BD37D1603794F0C55" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil040E62548C74B08FC5BBF294DABF6EDE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Label.qml" />
+            </Component>
+            <Component Id="cmpC38E3633293A96FAA056E8B5F510E90D" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filF53AA72457D7ACC1A30AFD0B3341DC72" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Menu.qml" />
+            </Component>
+            <Component Id="cmp895C441C34D01DD38C4FA27C47B768CF" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filBDEC65E4F2D0114683125F8534F7A8F6" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\MenuBar.qml" />
+            </Component>
+            <Component Id="cmpB9740012788E333FF69054EA6423DED5" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil937C336532FDA482261C1DEA1DAB949D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\MenuBarItem.qml" />
+            </Component>
+            <Component Id="cmpA6DC7007132A6609116E60A89A0324CB" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filD93F11A74B5326175FEA10F9C9E66ADE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\MenuItem.qml" />
+            </Component>
+            <Component Id="cmp5B978B6F7BD2D11A34FA999077BF6A8F" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil466EB4CACE10461C5B96ACCE96BAF396" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmp29B865CA91327F5D2E6473F3C1026D41" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil41BCC1BF081DFC0569CA520AACA4EDFC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Page.qml" />
+            </Component>
+            <Component Id="cmp1BCB2E932FE113B7305A0BD8C53E4202" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filFFF887D3478E13F8116D350E78B2E27B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\PageIndicator.qml" />
+            </Component>
+            <Component Id="cmp65D24DF8622A7BC3ACD8701A0AD15EC2" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil6574401211A0A823140A39DE924A9865" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Pane.qml" />
+            </Component>
+            <Component Id="cmp62BB96ABA056ABE59334D9EBDF8365C4" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil61FD0EEA35A1607B76BEEC04B3591435" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp788A8E5607E57B678DBA1AC760058E03" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil8A374607A603219F95044EB9352E2694" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Popup.qml" />
+            </Component>
+            <Component Id="cmpE81BA304B75AC5646F26A5C27AE85FD3" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil598D164233B8457F51539BEAD532B5FD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmp04918B42D3A8771871BAA02F680DEAF7" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filDEA0C64319CB4C98B1468FFFFC95FBEA" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\qmldir" />
+            </Component>
+            <Component Id="cmpA9CA24EFAEEAC38C1A3CF8B626D4CB53" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil7BFDDBFB1FE07B8E14E8E7E6516D5E35" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\qtquickcontrols2fusionstyleplugin.dll" />
+            </Component>
+            <Component Id="cmpB778EDCA12E3AF4E0A069AD702DED3C8" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil15F0F77DD308E5C51C4073DB9FBFAC10" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\RadioButton.qml" />
+            </Component>
+            <Component Id="cmpDF999518BA27DAC2F266DAC0C7A2BB43" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil04058A60CFC802EF1C27C700689E39AA" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\RadioDelegate.qml" />
+            </Component>
+            <Component Id="cmpAA4A6B9850EF1278FFF2F291071BCB2A" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filBEB790C48E1680FF411230854BFE2071" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\RangeSlider.qml" />
+            </Component>
+            <Component Id="cmp4EE7A42C854017155610E46E548CF266" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil56C24855AEBCE47EBFDFC9159091FFD3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\RoundButton.qml" />
+            </Component>
+            <Component Id="cmpF7CA31D8018A5DB693939EE00E979876" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil2D00D86916B210D4330C376EF72E5386" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmpB477A3A433A02C6DFEC309DD492687DB" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil9C56EDCF57BC93E1438CF96D36965311" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ScrollIndicator.qml" />
+            </Component>
+            <Component Id="cmpBBA3EDFEBF816D2097DB4C41F8635EA7" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil57C74AB1BDA9DDDFF66840A5697C689D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ScrollView.qml" />
+            </Component>
+            <Component Id="cmp5CE7C499E65206BD66363385164C9FA2" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filB4403B5072467D366D172E078BC3AE35" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmpF8825C6CF454A874807D5853DAC96CA1" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filBE6B7278916A7A8E6E1319CDE66FEE73" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Slider.qml" />
+            </Component>
+            <Component Id="cmpCB9E142AFC9D19FE67183D55865A3E0F" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filB09B10505BABECAC307D1BE027CEABDF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\SpinBox.qml" />
+            </Component>
+            <Component Id="cmp19D2BC7A1742FE5CE2CE0CCE54535BA6" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil978756DA2C7C3E0E6711132F1C953B65" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\SplitView.qml" />
+            </Component>
+            <Component Id="cmp554C33E63FF2C81D7FBA5ABE29E383FA" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil229E136BCADEF09840D78F2426077261" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\SwipeDelegate.qml" />
+            </Component>
+            <Component Id="cmpD9377D7119EAD1A371E16D26AE8CD5F2" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil034047174E9FB35E9E2E70606E534819" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Switch.qml" />
+            </Component>
+            <Component Id="cmpDB5E78C6CE44D55F53566D40D24BBE1D" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filF2115CC5C5CFA29DB69A1AF7C0933FAF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\SwitchDelegate.qml" />
+            </Component>
+            <Component Id="cmpDE95A8CDA51D40841BFE42981D3A2F91" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filBB9A443F5B8EC06D82355789641B84C3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\TabBar.qml" />
+            </Component>
+            <Component Id="cmp38727874177B251967D83F117A3BEE70" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filF520E186825210895A2EFF8C2EDC0352" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\TabButton.qml" />
+            </Component>
+            <Component Id="cmp1E8AD6A0BCBF8BD1A4460E16B1459646" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filF783B6FA414887CD8CC7F572A4922E24" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\TextArea.qml" />
+            </Component>
+            <Component Id="cmpFC095DCADEB5A89F06586707E07DB35A" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil4B4C38BC9AF9B5C1425906BE0652EEA7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\TextField.qml" />
+            </Component>
+            <Component Id="cmpBB7B13F1D78AE63A37ADA8B3FE512CB8" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filBFD211D2489FC6BD15F406F9D99D6E5C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ToolBar.qml" />
+            </Component>
+            <Component Id="cmpCFA92F895CDBBC5DBF724CDFEBE0765F" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filBCCF0BA09C035137942061AE11C65F4E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ToolButton.qml" />
+            </Component>
+            <Component Id="cmpAAE351D729AC09074760EE362CEAEDFC" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filFED9DC0564E217E5B70C40E88C150DAD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ToolSeparator.qml" />
+            </Component>
+            <Component Id="cmpCA85D444470DB90856D55C682B1B881A" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil94930E3044AC0BB7C0A1CC900C378C2C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ToolTip.qml" />
+            </Component>
+            <Component Id="cmpE809A99452EB9066B44D80787ABDA5A7" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filF8C16A0A803A41821D8C78F410949C15" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\TreeViewDelegate.qml" />
+            </Component>
+            <Component Id="cmp1E706971AB2E6609A1BB91BD7754BE2C" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="fil71552616BD51D68C967A8108C3C34156" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Tumbler.qml" />
+            </Component>
+            <Component Id="cmp71F7BFBAE8BCE4573D3708E017BA3876" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="PUT-GUID-HERE">
+                <File Id="filD7A0393F91A1D2039EEC353CAF154226" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\VerticalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp35F84E364132FF61C34E2B8A4085E59B" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="PUT-GUID-HERE">
+                <File Id="fil55C2B5E883CF11CA03AAB59564BCCF10" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\ButtonPanel.qml" />
+            </Component>
+            <Component Id="cmp7F2E51BEFFDCEE92C30ABC6172B62D21" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="PUT-GUID-HERE">
+                <File Id="filA17D372C53BE030209E1D95C71BDEC1D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\CheckIndicator.qml" />
+            </Component>
+            <Component Id="cmp24EE24A13287A5C8C0D65E0C2D4909B8" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="PUT-GUID-HERE">
+                <File Id="fil9708C1A46FEDBA74EBFD3B0CA72CA6A9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp2392F6C066BC5837827B65D7B6A8F614" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="PUT-GUID-HERE">
+                <File Id="filAFDDCA55B6BE9082C0D7B9F47E4AADD3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\qmldir" />
+            </Component>
+            <Component Id="cmp08A8D7B767927E254C4B586CB5EB9F39" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="PUT-GUID-HERE">
+                <File Id="fil3EE6699BC5D58405963695D2AC6E9FA8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\qtquickcontrols2fusionstyleimplplugin.dll" />
+            </Component>
+            <Component Id="cmp9105753D74B5DD54AA90A2AFB4426FD3" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="PUT-GUID-HERE">
+                <File Id="fil40316BC51D62B7228F615E08D4CD68E2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\RadioIndicator.qml" />
+            </Component>
+            <Component Id="cmp31BD21BD04B2B8CA1EA22E2F26D5D0A5" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="PUT-GUID-HERE">
+                <File Id="fil03586879198A259A3854D5C0A515323E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\SliderGroove.qml" />
+            </Component>
+            <Component Id="cmp9BDE1F68055939DDBE5EE32AA53F5324" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="PUT-GUID-HERE">
+                <File Id="fil412329F904651B548C74F4FE36C67763" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\SliderHandle.qml" />
+            </Component>
+            <Component Id="cmp61E7C47F93C8155B7A99A788B2152A49" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="PUT-GUID-HERE">
+                <File Id="fil47D1126A5491D649BB8E1EC6DDFD01A3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\SwitchIndicator.qml" />
+            </Component>
+            <Component Id="cmpB90BC66FC30D3F0E07924B50F863255C" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil0A1DBE214C9CC60424A3DED42DCEDE67" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmp4EF1DC5A5072FB3F20AFD1AEB793F94F" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filC9F7BA6AB033842F916D7EEED1BB4EB6" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\BusyIndicator.qml" />
+            </Component>
+            <Component Id="cmp1388BC4D80F7D8EDB40817EDC492567C" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil0021EEAEA9EFB1C60F51EB352EC1C26E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Button.qml" />
+            </Component>
+            <Component Id="cmpC3DE992FA17AF296C98959F021430280" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filD00EBDB1540428E75F22BFD9E4AC47C6" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\CheckBox.qml" />
+            </Component>
+            <Component Id="cmp48179523E340069145BBACC26A10AA26" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil755F0ED01B35E28143D26D2FE7933E82" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\CheckDelegate.qml" />
+            </Component>
+            <Component Id="cmp11776F4851E1DD813385E8BC23FFE8C5" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil3FB47A02BBB1F189AEA4CC08EB1A8B43" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ComboBox.qml" />
+            </Component>
+            <Component Id="cmp33DCD47F283028224453A973B4E2F271" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil4933A6622090D5CC8FEB52DBB946075F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\DelayButton.qml" />
+            </Component>
+            <Component Id="cmp81DA3D84F42F162D1646E837E76326A6" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil0B027F6370F5FC42138BDDC3CB832CE5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Dial.qml" />
+            </Component>
+            <Component Id="cmpABD633E81E45041FFDDA75D788CC55DF" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil449068F2A33DADC4E4196B08F8CB3D5C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Dialog.qml" />
+            </Component>
+            <Component Id="cmpD63BB3D65AE4F43E1EB15B836291EB65" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil705B9A4FC934EA39803B96CD4B3C0637" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\DialogButtonBox.qml" />
+            </Component>
+            <Component Id="cmpB834EEB17E0CE0AEBEC6AAFB5F2BED19" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filAFCC88499681ACE1828E53C6EDDCEDD0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Drawer.qml" />
+            </Component>
+            <Component Id="cmp8938DC5D9FAD0154F3772DF16FA88014" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filBB1F17C922B6DEB019DD4A3B26996E9A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Frame.qml" />
+            </Component>
+            <Component Id="cmp520B68523086C7771C12504E26FA66AE" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filF7EB8A18EEC601CC4FD8AE854149B0E0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\GroupBox.qml" />
+            </Component>
+            <Component Id="cmp0DAB42BA97E67306E77D1D2FE4D4A41A" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil0CF735FAC3FC78D02B1F52F9602B92F2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\HorizontalHeaderView.qml" />
+            </Component>
+            <Component Id="cmpBC65480FDE7319E7F5681A481DE4FB61" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil1126D7F43ECDE5F5AD064B7ACCBAD5BF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ItemDelegate.qml" />
+            </Component>
+            <Component Id="cmp517FF3481F92797BC098C4AAF0CEEBDA" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil1A4553431D266FF13F4AD499D78A7609" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Label.qml" />
+            </Component>
+            <Component Id="cmpF7C94F112CAF3E64CA3DB5825ECABFC9" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil16CC0D612CC665C1D31AEDD9FA6D6496" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Menu.qml" />
+            </Component>
+            <Component Id="cmp7314B2EFCC5266DD5976DFC65011E160" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil5218219AD0F35AF3687C64908160315C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\MenuItem.qml" />
+            </Component>
+            <Component Id="cmp9524323CAE6CA0D851C351DF07B54B17" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil6C6B5EF0866BEA8C9DA2C397B53D4C8E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmpAB852F814AE2A0119F099F324F1C20E8" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil34BD1730047582434887F38591E8AC51" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Page.qml" />
+            </Component>
+            <Component Id="cmpC7D6CFA9C1E1EA0C6597CA0B871B074F" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil96D91956C1C9B495B7434B8521D09454" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\PageIndicator.qml" />
+            </Component>
+            <Component Id="cmpC15260C4AD42D1CEF736F1C136F163F5" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil344DEC2D0972A1F9F5D10D3FAE36F6CB" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Pane.qml" />
+            </Component>
+            <Component Id="cmp5B291454617AF23B7D38C994AB449D3D" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil2E691ECCB2F7963410346B040B175081" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp45789E71EBA816DADA30D3BC7E74C1EB" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil1EF2A0CAC51274AC3E8D05B8484ED1CE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Popup.qml" />
+            </Component>
+            <Component Id="cmpB6BB779949BD09A6C7C117B35AAC3C11" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil2C18387AE5BF4F98F09799931104506A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmpCAC7687406AF3756D9B550A0782E82DB" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil210E2A6B681C2F759FC90F73935B62B5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\qmldir" />
+            </Component>
+            <Component Id="cmpBB8909842E7E5F3DD1F13B8B867612A5" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil7153E75CF6D908370B21B71A3EA5CC72" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\qtquickcontrols2imaginestyleplugin.dll" />
+            </Component>
+            <Component Id="cmp651517D86E914D35D824A7FFECD13B3D" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil4C27BF6D20264936550C6FE256B9450D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\RadioButton.qml" />
+            </Component>
+            <Component Id="cmp43AB7C7959383750E6036AB5F1252794" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil8028A5B8B42C09E22E9A1AB464895E82" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\RadioDelegate.qml" />
+            </Component>
+            <Component Id="cmpF37B0ACA1EC94845C16F4C4AFCB5A2B2" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filD5CB57E6FB9A8E4700982D249284FBB1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\RangeSlider.qml" />
+            </Component>
+            <Component Id="cmp59E1C7C4E4607CC6DCE13C51269B732C" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil6F7C1353446DC84FE1E32EC995CA7432" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\RoundButton.qml" />
+            </Component>
+            <Component Id="cmpB744FD5D1538FF6BBF40E66A57806042" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filBEAC9A233159C63C1DCFED5DA94D05F1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmp28D6C075D8931D2EE68394BFCC7B9769" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filC1ACEAA61DED08BBC2585FF5680C4F30" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ScrollIndicator.qml" />
+            </Component>
+            <Component Id="cmpDB9D955B068BFB9543C7EA8C53CF4415" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil1FDB67DBB9BA5B1BF415431442D996C4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ScrollView.qml" />
+            </Component>
+            <Component Id="cmpE20D36DB8EA2F08C50E3A3D297C13511" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil51FC8E7FBCC491FA0221A44E277F8F46" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmpEE3458BDB70A8381167CD9334D836E21" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filEB662C8D7D644D4BAE0BED4BA81BAB24" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Slider.qml" />
+            </Component>
+            <Component Id="cmp72E1DA0B7032E89A531FE9ECCC502479" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filC3083743295587902F4DFD54580585C8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SpinBox.qml" />
+            </Component>
+            <Component Id="cmpF68C8F37149A1B836FF56BA9AB021FB6" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil49E9BF9639FDC3D443E71CAC43BBB93B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SplitView.qml" />
+            </Component>
+            <Component Id="cmp3D21995F3CB052BA85DB46E236E13F80" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filF8009105B5CF6F20079F72CF3B503261" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\StackView.qml" />
+            </Component>
+            <Component Id="cmpDE32BE851FF654E13F8100FC908AE929" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filF511A0B0142A4B8E84AB5AC79B23D664" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SwipeDelegate.qml" />
+            </Component>
+            <Component Id="cmp3798BA5F7F00735D5CD82831987CEA88" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil10D0FBA433FD973A4809BDB8C39246FC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SwipeView.qml" />
+            </Component>
+            <Component Id="cmp65C62E5359CDF461ECD63741FE2281E7" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil5924DC22A75969FB264E08E704EF055E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Switch.qml" />
+            </Component>
+            <Component Id="cmp2B18A4940D87785365EDFE294DC1D4F1" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filCDA34143557E58582C9FB30A1BB5BBDF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SwitchDelegate.qml" />
+            </Component>
+            <Component Id="cmpA85B65AFCC3DD70425F2B555CDEB9C5D" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil1436075F9B2C7C9EA6BDA5B931965B4E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\TabBar.qml" />
+            </Component>
+            <Component Id="cmpA234831EABA20D54678B4AF2AFB001D9" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filE3FA6AD5AC0C59434804CB8F23241AEE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\TabButton.qml" />
+            </Component>
+            <Component Id="cmp85D3E99C234D5C73F5C8891E2E03CD4F" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil8371CD0F022F8EBA56526C6AB1BD5F55" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\TextArea.qml" />
+            </Component>
+            <Component Id="cmp9A7E2FE9312CD8830B3B6A104DBF9527" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filF7491E17C5398B6321A5CA6C195A86B3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\TextField.qml" />
+            </Component>
+            <Component Id="cmp5C181DE0E73A872702E7A5BCBEA45966" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filE8186132C708AE394DEB781D91051FE1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ToolBar.qml" />
+            </Component>
+            <Component Id="cmp8FCAA916FDD499CA06E61F6B4BAB58ED" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil1F09EDC2E28D6848DD0F6B2F109F1B61" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ToolButton.qml" />
+            </Component>
+            <Component Id="cmp2BE8196732C5C2D101C8928A22CD6D15" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil180E626065A3BAB06BE24B814A2A404C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ToolSeparator.qml" />
+            </Component>
+            <Component Id="cmp3BBC88103916373B38F08B749DA30956" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil8527A8105CB7EC9CE3F7098C086C38B2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ToolTip.qml" />
+            </Component>
+            <Component Id="cmpAFB2B07799CF545EFF0B66FDCFC4FF47" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="filCAB0EB96ABFFD0D5C3A967366423849E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Tumbler.qml" />
+            </Component>
+            <Component Id="cmpC6138D71524F53A3542DBA7DD2177CE7" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="PUT-GUID-HERE">
+                <File Id="fil6F5C2672986ACD051C851B373F6527C9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\VerticalHeaderView.qml" />
+            </Component>
+            <Component Id="cmpFE7088B595171444EAD0A7EAD9833039" Directory="dir4E741D8C175690CD9096A89077530C9C" Guid="PUT-GUID-HERE">
+                <File Id="filFDC49910B20D3BEAC0AAD591C870845C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\impl\OpacityMask.qml" />
+            </Component>
+            <Component Id="cmpEEDFF0982D868E8C996B37C3E9592698" Directory="dir4E741D8C175690CD9096A89077530C9C" Guid="PUT-GUID-HERE">
+                <File Id="fil1A4D9CB661D5F7AA80CA699FB6BB887E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\impl\qmldir" />
+            </Component>
+            <Component Id="cmpB7362BFEADB697DA93C3B0E9471FC933" Directory="dir4E741D8C175690CD9096A89077530C9C" Guid="PUT-GUID-HERE">
+                <File Id="filC7915B57A913E31D3C4D427C6E2219F5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\impl\qtquickcontrols2imaginestyleimplplugin.dll" />
+            </Component>
+            <Component Id="cmp7477DF3B20A2314AB9D9D7C2B37516D1" Directory="dir4E741D8C175690CD9096A89077530C9C" Guid="PUT-GUID-HERE">
+                <File Id="filBCA1409917679CAA35888D9AE61821D2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\impl\qtquickcontrols2imaginestyleimplplugin.qmltypes" />
+            </Component>
+            <Component Id="cmp11D7D79C97B237EC913F8032C8F836E4" Directory="dir171007FEE30C4A50EC7ED2CB58E0B5E3" Guid="PUT-GUID-HERE">
+                <File Id="fil8E3F37E1853A667FFA38E60B8B1D9B85" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\impl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp62D853DC85F6B1CDD4296C97C09EEC5E" Directory="dir171007FEE30C4A50EC7ED2CB58E0B5E3" Guid="PUT-GUID-HERE">
+                <File Id="fil86621360A721274A27591FE2D5E332C8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\impl\qmldir" />
+            </Component>
+            <Component Id="cmp4AC6569F2C038B84A66F7107E43CE9A5" Directory="dir171007FEE30C4A50EC7ED2CB58E0B5E3" Guid="PUT-GUID-HERE">
+                <File Id="fil159FB9D3A93DD5E084022CE4FF2A6036" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\impl\qtquickcontrols2implplugin.dll" />
+            </Component>
+            <Component Id="cmpCF13BB5CB6845852387DB0E66E6FB9E0" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filA3CCB43FBDE7526A171F1B23C7D68019" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmpEA8352233733F0363804856322EB0FE6" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filCFCAADBC9A7DF963E5D1F99E31F790F4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\BusyIndicator.qml" />
+            </Component>
+            <Component Id="cmpCB55784D3984D9F2DF6615E789C57FAF" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filAC46DEAA1116ECA656B8A60FE478AFD0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Button.qml" />
+            </Component>
+            <Component Id="cmp01ECF58B0613C06B4BA4FE126C713F0E" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filA63D753CD0F650D43D0A3385004927CE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\CheckBox.qml" />
+            </Component>
+            <Component Id="cmp005997DB9453F1DE5910014AB11D4C75" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil840D7F4F642145ADA3304B0943E075E7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\CheckDelegate.qml" />
+            </Component>
+            <Component Id="cmpD66560236CF24FF97AB65D56AA555530" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil5F51EFB1183F259CBB20408ED17D912A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ComboBox.qml" />
+            </Component>
+            <Component Id="cmp96F28C12C99816E6BF59536C553840AC" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filDE90510FAD5DB3EC855DD77BE31557FE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\DelayButton.qml" />
+            </Component>
+            <Component Id="cmp154E9BD7A64B0DD68CD83B4F23511A21" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filF96A517FD34F4B3451814DEB8A4AFE4D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Dial.qml" />
+            </Component>
+            <Component Id="cmp80E1A825825A58BA318AECDBFA24CF1B" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filECE3A53486E1A49EE2014DED52AD7B05" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Dialog.qml" />
+            </Component>
+            <Component Id="cmp888BEB5962E610A3947D2FF3387E6C37" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil302D513E9406E43BDCC01FCDB1E5A4A9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\DialogButtonBox.qml" />
+            </Component>
+            <Component Id="cmpE557B10B81996EA9E0C090A014620ED1" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filC0FB78AC4AF34DB778DC9626289CAA0C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Drawer.qml" />
+            </Component>
+            <Component Id="cmp190AA6A126CE5FE0DBBDFB2E9E370822" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil7D5D1415252A4B9A51401D53E8436427" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Frame.qml" />
+            </Component>
+            <Component Id="cmp1E05B467AF2E75161A28F5E277E0BC54" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil719B30F98FD710E5E1310DDA4B41C1DC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\GroupBox.qml" />
+            </Component>
+            <Component Id="cmpBD15B6ABA62E41DEE473C65FAC1CDC3E" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil450DEED653A70B5D0787578C3EFF67C0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\HorizontalHeaderView.qml" />
+            </Component>
+            <Component Id="cmpCE84DF6B3EFC1F4DA2580194322E6FF9" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil70892882732D9D6CA5DEBDBBE7A16C32" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ItemDelegate.qml" />
+            </Component>
+            <Component Id="cmp6B02AE0065D2F87E3E1EE0FD80C2F114" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filA8167CF68C0141B8CF10930ABBBDFA24" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Label.qml" />
+            </Component>
+            <Component Id="cmpD5A3C65BA27F1F7D3C1C50FE455F2D2E" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filB2549F45573FCC17A86310628BC6071E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Menu.qml" />
+            </Component>
+            <Component Id="cmpCF1B98B583FE40BEB19FFE3103A22E1B" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil1396E4F58ADC81EDEFD42D691E754D74" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\MenuBar.qml" />
+            </Component>
+            <Component Id="cmp60BC16CE2C100CB3F06A7FB02995E94A" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil74C563C8F2B8EB30CF1980ADAB1D40AD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\MenuBarItem.qml" />
+            </Component>
+            <Component Id="cmpB3740D2D255D2EE71B4E675920B5AFEB" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil951CDA05D439F1C57B4515E40E866D79" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\MenuItem.qml" />
+            </Component>
+            <Component Id="cmp7B7C1CECA8E7E92CA81441EA981CA135" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil56681E7D5B501EBB64DC7ACA76A9B2F5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmpF81A2DDE2AD5C942A50EC687D33B60E5" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil8D8880C57F5A507C6AB72CBF88D14171" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Page.qml" />
+            </Component>
+            <Component Id="cmp794134A6C8E5C4CA118CBC4FD67AE890" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filBDAABFC0E12EA846ED903A9377CCDF54" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\PageIndicator.qml" />
+            </Component>
+            <Component Id="cmpE794B530811F6258FCE9C834CB062851" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil0D80249F03C878DDBE677508709FD698" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Pane.qml" />
+            </Component>
+            <Component Id="cmpB04392B85D826BFDCC105DB22BDB2546" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filC3A2F8CDA348ACDB2AF26C9E6FF8FAA7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp0402A1984DEDA53F483897C1C679BF1F" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filD62B57B4E80B34E2BB014622F70DE27C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Popup.qml" />
+            </Component>
+            <Component Id="cmp918C71F55F3A45A8D28838ACFE6248C5" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filF11F4684DFBFBF9F0FF4D9AD374F0387" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmpB6BEE21772567F79969561FF4BA72410" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil053291A05BDF8AD3704981A7B0898F82" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\qmldir" />
+            </Component>
+            <Component Id="cmp1C864015DC7795834A0057A175C597E1" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil84A915D0D0D5112A88F2D9B399BDCB45" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\qtquickcontrols2materialstyleplugin.dll" />
+            </Component>
+            <Component Id="cmpC97800A6EBF13D95E9FAC300048C560D" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil5F5B5BA5159415A93B17246AAD0F1B2B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\RadioButton.qml" />
+            </Component>
+            <Component Id="cmp810E9C023C627F0FD312E6664F2A25D9" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil5C06D9354E96E77D1D3863FC94773599" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\RadioDelegate.qml" />
+            </Component>
+            <Component Id="cmp519F1EA5B1FD43D841FA07B7E430E445" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil3C4C340CA6E8CEC7266592F48257C70E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\RangeSlider.qml" />
+            </Component>
+            <Component Id="cmpC36777B7421BD8565A73A1CECA4A6EE5" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filCC10D65CA29863504C8CC74493BD6CA5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\RoundButton.qml" />
+            </Component>
+            <Component Id="cmp29EE21BDFB11914175077667D19EFB5B" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil94B9EC0027D8068E4F6709CA3E0C3360" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmp2141E33205B42038E203536124C78B26" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filBAD56A1834C7FCE66FB34D6AEAA39A01" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ScrollIndicator.qml" />
+            </Component>
+            <Component Id="cmpF0443FA976B9A0C13FF2F410172218EF" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filF9E938FE9C5D0D3449F5B405DB07D73E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ScrollView.qml" />
+            </Component>
+            <Component Id="cmp290D27CBECEDEAFCC6D399584107C8C0" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil0B08D3BB58D6C6CA8328496E951B338A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmpECBA9A40396575E8805D705BA6DD87DB" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil5A7B23D53F1D31AF38D118A0E6207972" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Slider.qml" />
+            </Component>
+            <Component Id="cmp9886A67A48D7C9D7684522160D081022" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil460842FDA0D0EC28490A2B7FF0BC3205" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SpinBox.qml" />
+            </Component>
+            <Component Id="cmp985E022A5C9C43CCCB2BFA14A9108961" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil054E98ED8AF7502514CEC672E99F3F50" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SplitView.qml" />
+            </Component>
+            <Component Id="cmpAE2EEFF88C165D2E2D45EBE3A37D558E" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filD3BFACC2DA16F1BDEB5F32A9280F21B3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\StackView.qml" />
+            </Component>
+            <Component Id="cmp9EB09B3CCD9D6D7B9D65629B00942455" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil9ADA5B74CCB596A8EAF3FA017EFB5260" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SwipeDelegate.qml" />
+            </Component>
+            <Component Id="cmpE36B9BE6B18A774CDB849B20C13BAB86" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil3C5CF2C209F301AD82A32857FE65AD48" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SwipeView.qml" />
+            </Component>
+            <Component Id="cmp9734CBE5CCB5307199376FFEB7E2A5A2" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filAEF492CB3B3CD816DB0F56ACA80A50AE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Switch.qml" />
+            </Component>
+            <Component Id="cmp4BD0D0FEC80C2290B78A28E477536FD0" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filCCB96245DBD179FED5867875729BF838" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SwitchDelegate.qml" />
+            </Component>
+            <Component Id="cmpA4621B705888F802DEFBFBBEE22375B1" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filE761FF0B176FD74798A2B0372140C031" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\TabBar.qml" />
+            </Component>
+            <Component Id="cmp0289C3F6859ADC6AF76A91C9F5484C31" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil3877A40E3995B4EB525E3390B8862476" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\TabButton.qml" />
+            </Component>
+            <Component Id="cmpCB1CCFB21E1EB2298E29111F5579D022" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil9D73808A3A40379E7D8386F82262AB99" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\TextArea.qml" />
+            </Component>
+            <Component Id="cmp3A262ACD2D62E8DFF3DFC1ADC239B32A" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil5D746CB66F1EA350A4ED1E78AAE1BE2D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\TextField.qml" />
+            </Component>
+            <Component Id="cmp8088ADB1622AEA4DB7C0B72A37F0EEBB" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil32F2E0A71EAD04B891D2CBC968967C52" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ToolBar.qml" />
+            </Component>
+            <Component Id="cmp12029B0ACE2380A95ADAE86EB4BC13C0" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil1EE7E4109157ADD9974A013DE84F00E0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ToolButton.qml" />
+            </Component>
+            <Component Id="cmp4D6D0C748629626F327BC86EF1951208" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil3AAEBEBB0EEF1D1A8023F8647A9D2908" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ToolSeparator.qml" />
+            </Component>
+            <Component Id="cmp2B3619B85F678EBF8B98B06E6ED1C7EF" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filDB43C5904651368F7B695B52A34B2780" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ToolTip.qml" />
+            </Component>
+            <Component Id="cmpDCB56F79763E84D43053248E9BAC844E" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="fil3467664546CAB8F0F6410A5183CF489C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\TreeViewDelegate.qml" />
+            </Component>
+            <Component Id="cmp8F987B0D48E647C9F0BB5810CC50DEB9" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filE68A2F6161B427B4E53B59ADC42F5A8A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Tumbler.qml" />
+            </Component>
+            <Component Id="cmp4E3A3D737E0A5ACC7A8D42D075170F2A" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="PUT-GUID-HERE">
+                <File Id="filB920B036CABD41BA576BDAEE5F50AE7C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\VerticalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp4974A3B78183896E1AED2DEE9BAE0251" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="filEEB1FD3B3304738741C4BA6D7D64A00A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\BoxShadow.qml" />
+            </Component>
+            <Component Id="cmp6A8F7A322E2F2C6FD2BA43B8B451FFB8" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="fil6589143E2BCB1391E81C601ECA3D476D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\CheckIndicator.qml" />
+            </Component>
+            <Component Id="cmpF62E2C4B7807E0679EE98B3175A6F50F" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="fil89C00A9449C46E4D87A720B60C1F16B1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\CursorDelegate.qml" />
+            </Component>
+            <Component Id="cmp5ABFD007CB27FCCCC6C7F6BF2BF7F1C3" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="fil3A032480D0BC1C9FB9BDFAA859BBB944" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\ElevationEffect.qml" />
+            </Component>
+            <Component Id="cmpBACD13AA12B6FC7BF611DC56FC238999" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="filFDC96EA252569D84275658079E02E9C7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp37DFC6073765C4BA0142EAE156302405" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="filF63CEDA19E2B556BF56331E9847E297E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\qmldir" />
+            </Component>
+            <Component Id="cmp5E6437A6E1FEFDC5F3FACA13467786C6" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="fil553D014E7D65B2D6B1576172992F1B4F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\qtquickcontrols2materialstyleimplplugin.dll" />
+            </Component>
+            <Component Id="cmp2AE92027A7FCDEBF15F1A365C3786D3C" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="filB30B1757D9725E0AA4A920E66578281E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\RadioIndicator.qml" />
+            </Component>
+            <Component Id="cmp36E9DC5315FE3B8AA35070E2DA84DD48" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="fil3F0A3E1797F80CA73C3FA6B42F73C834" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\RectangularGlow.qml" />
+            </Component>
+            <Component Id="cmp754D99341A465EFD63ADC35046E16790" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="fil1174B4B89CC14686B2D704BEF48A73B4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\RoundedElevationEffect.qml" />
+            </Component>
+            <Component Id="cmp082A1EEBA23939825BF1B8C532D1919C" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="filB787BFACAE37AC95BC017EC2F3148E77" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\SliderHandle.qml" />
+            </Component>
+            <Component Id="cmp6A3F490C8D166289D4E591A3FB38B14A" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="PUT-GUID-HERE">
+                <File Id="fil1CC04953B53C4A7D01CBA295531B02AF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\SwitchIndicator.qml" />
+            </Component>
+            <Component Id="cmp51D4953927172FE66AB259A9E3465218" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil60EFB57B56EFABFC5D70302BA7B83C51" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmp50AE987125D48D86F78F809B6B326AF2" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filBC5F989227D32BE957078E299336A32B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\BusyIndicator.qml" />
+            </Component>
+            <Component Id="cmp9FB9149775FE4BFAA9B0238DD797CB99" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filFA519204CB8D32802BE976C955ADE249" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Button.qml" />
+            </Component>
+            <Component Id="cmpDC91AE6EF2520D4C8A306AD82BE8E963" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil9DAEFC0BA4824FF863E0BC607BB3F726" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\CheckBox.qml" />
+            </Component>
+            <Component Id="cmp6354B1BE039A3A801525721FB8B6DA39" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filC688B6E0B44BE8B7183854F811FCA3CF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\CheckDelegate.qml" />
+            </Component>
+            <Component Id="cmp9D0B5875C4045E501F674FF488B8D6E8" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil31451B6BEECF73BDBAE7AB4ED9B8A74C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ComboBox.qml" />
+            </Component>
+            <Component Id="cmp087C60BF1A5ACC5118DF8BF393AD237F" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filCB4C62BF8104E9DD140E2CDEBF1216D1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\DelayButton.qml" />
+            </Component>
+            <Component Id="cmp8C096DE3954F3C717FD6A15BFBB1DFA6" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil621F0D1411277F3863B5713D6DA92E86" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Dial.qml" />
+            </Component>
+            <Component Id="cmp9A29685D109A5F605D3307C06A7E94DB" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filB07409843046124F0BBCEA771662FE93" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Dialog.qml" />
+            </Component>
+            <Component Id="cmp910AA543273FFA2E55BED62330ED3FB2" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil5C2DA019DD85B1AB18DEBB58FD7B17A8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\DialogButtonBox.qml" />
+            </Component>
+            <Component Id="cmpF8DABF55A2291D3AB2E08F172A4A06A2" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil3EB5FC177D81A00809E137EA010E4051" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Drawer.qml" />
+            </Component>
+            <Component Id="cmp96161F632A366081919D6B2A30255DC1" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil9BEE87A9A8B44D9B499D632FAB30219A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Frame.qml" />
+            </Component>
+            <Component Id="cmp4C17868CD5D85912A6F0BE644AB3095C" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil4AC7BD9A0592C11527E2882BBF99DE25" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\GroupBox.qml" />
+            </Component>
+            <Component Id="cmpCB3A44ACDFD39C22F19F1A1C83E28EF1" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil064D0FBC8E9088A72337290D87E14422" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\HorizontalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp9E02917D9B78D87D7ED5986CFCF0BC38" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil6D16367AF986E353A82BEBF5FCB7CF9B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ItemDelegate.qml" />
+            </Component>
+            <Component Id="cmpA2D8FA34582A60D9034870387BD3D03E" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filCCD76566485F6C9478A00AAB064E9C00" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Label.qml" />
+            </Component>
+            <Component Id="cmp728F2B606FDC43D7AEDE271F1E2A81A8" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil42276E9C369EBEEB368374611883C535" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Menu.qml" />
+            </Component>
+            <Component Id="cmp6852AE6BDFAFAB326B6402ABE17B022D" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil03498BED0D38C2F221B4050185FF1698" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\MenuBar.qml" />
+            </Component>
+            <Component Id="cmp3DFC359CF7645BCB15E6A7555D8CE483" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil373F2E2ECEE795B06FDD75C967B8B4B9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\MenuBarItem.qml" />
+            </Component>
+            <Component Id="cmp4F17A89EEB5A0EADBFCF141685503D29" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filDD18D1C6CEECFD677C923B93AC3D0C76" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\MenuItem.qml" />
+            </Component>
+            <Component Id="cmpC44104343636AEE657FF14B1BF500629" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filDF2F69E5F74A66E0838D016BBBF102A7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmp9E3F847F66BAED7CAB8605D5F8042DCC" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filC2B089FE945EB06E7049D5033791F4D7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Page.qml" />
+            </Component>
+            <Component Id="cmp907EDD0EC968E3CB752741291EBA1AE5" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filF940E45C7ADC6D8891FACF4ADCA3060D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\PageIndicator.qml" />
+            </Component>
+            <Component Id="cmp1FEE0C7E5A8B3F19CF1A59898415A197" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil9BB6E26F8F3C859253711636D08442AA" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Pane.qml" />
+            </Component>
+            <Component Id="cmp685E4176CF6CD43E3F2296FE3011A50C" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil2BD53F267B47AABFE9880CC83450CED4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpAF3F2B21903C71CDC9EB17006779D60E" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil71562651D8D0F800311B61E5064E3CC2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Popup.qml" />
+            </Component>
+            <Component Id="cmpA9C2E39114BEEF09DE4535C28964189B" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil11BC81473EBB26C6EBBA5170D5EBDE75" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmpDCB058CAC37112FA04AD8AAE585D409B" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil59652335BE550A0250A3E167B1F40736" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\qmldir" />
+            </Component>
+            <Component Id="cmp33FB18F3293A67E3EBBB2358F64755E0" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil5FC12DA36BBB59BCA1862F9EDA004269" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\qtquickcontrols2universalstyleplugin.dll" />
+            </Component>
+            <Component Id="cmp5C4F612CE6E32CCFFA240B4A0B87918D" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil01F4B5A3F96708A3194B368B53340FD4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\RadioButton.qml" />
+            </Component>
+            <Component Id="cmp1B849263C1213BAC8231151AEF54F69C" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil432F670291BD6028E1969692246B13AA" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\RadioDelegate.qml" />
+            </Component>
+            <Component Id="cmp537E154F45B568C4A7F32AFFA81089E2" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil4B711803D153FDFB6A4A7CF9E2600D80" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\RangeSlider.qml" />
+            </Component>
+            <Component Id="cmpB6C66482626F530FC2503D0F0E59D9BB" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil21A8ACDA637940AC4286A7AF53B8B17D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\RoundButton.qml" />
+            </Component>
+            <Component Id="cmp64A1F7B15057A2E1D19560ABDF9DAF80" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil2FA3E488B17C5AB584728B88EB831B64" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmpCD57DD397AFFFE295449E80AFB9F4E5F" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil7B5AD4F110A9268DB1ED92413E6DEDE8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ScrollIndicator.qml" />
+            </Component>
+            <Component Id="cmp9524C10C5C6DA9FF8F8B6002740132CF" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil0A2F722A7C60AA2DFAF098A341F5CFF2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ScrollView.qml" />
+            </Component>
+            <Component Id="cmp5A0AF016778EF8E4C1D06EA2E10A7EE3" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil03ED63365C5C6CE2B52F3ECB5E0B4B39" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmp29C9420D05B976736364005D792E5DA6" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil0A418C1F9F31EA91E6F9C6D5059AA8BC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Slider.qml" />
+            </Component>
+            <Component Id="cmp416E2860BEED8693C7D2E3FEAAC98CE6" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil4B32E68B346D12238475094812008A27" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\SpinBox.qml" />
+            </Component>
+            <Component Id="cmp9277891754C1B5136DD8569A94B4DF80" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filB45FC92883BB1384879235F2FE3FD0F0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\SplitView.qml" />
+            </Component>
+            <Component Id="cmpF8D9196CFA84283BAA12A5A7C7C09537" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filF381B4329F33E2F4016938E283FDE306" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\StackView.qml" />
+            </Component>
+            <Component Id="cmp7A2E1B8C0BA43F0FCE98C773CC1B4EA5" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filCC2E6195FB7078E99066C2C1AFC2E8EE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\SwipeDelegate.qml" />
+            </Component>
+            <Component Id="cmp4EAF90EE5A418B31FD73F5715EA24DE0" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filF3694E8DA7CF82CD09FDEF364F0B670E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Switch.qml" />
+            </Component>
+            <Component Id="cmp12F9CC77E0C03C07FF302D5D7BA23AD5" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filB0E07FB192EF460722852FFE7407DF82" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\SwitchDelegate.qml" />
+            </Component>
+            <Component Id="cmpBDBF2411D4F7E49220B2BCF845E99AA5" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil3DB068D0F1619CC321E92AC31651AD51" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\TabBar.qml" />
+            </Component>
+            <Component Id="cmp1F9F5B5C7855DEDC823C34BC1A8CC6EC" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil55391B4C477CB9D40328A11E56930971" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\TabButton.qml" />
+            </Component>
+            <Component Id="cmpE9E2E248248D424BFB1DC6CA31BC27D0" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filFFC17C428A0FAED302DE985D00A846C1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\TextArea.qml" />
+            </Component>
+            <Component Id="cmp3FD867BC1A97A8D0378B3C8DE43B974F" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil0D775F39F24A63854675B5109686CD47" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\TextField.qml" />
+            </Component>
+            <Component Id="cmp6E54963F3B620EBC67E5F63FD718977D" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filA23504C4849E7A6ADE28FAD856C5F859" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ToolBar.qml" />
+            </Component>
+            <Component Id="cmp31447F7164FAAB2B0C95D9AECBEF31B0" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil3ED20D130D8B21E93E817825A0936FB9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ToolButton.qml" />
+            </Component>
+            <Component Id="cmp984F077A561593958287F1B765CFFE25" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil926D6452C56D308D9C80B02B00608E66" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ToolSeparator.qml" />
+            </Component>
+            <Component Id="cmpD76004E2F76A20F78C7300677C61D0FA" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filA3B34BD4D8D009E22A6B01C98AA9EF25" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ToolTip.qml" />
+            </Component>
+            <Component Id="cmp644DC3C8747E6DB3DCDBA429C9571ACC" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="fil8A4F194DE6EDCC255FF25B054DAEFF2D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Tumbler.qml" />
+            </Component>
+            <Component Id="cmp2E6AE1BC29B54991354BB7D83580A121" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="PUT-GUID-HERE">
+                <File Id="filF9865D03CD55A2D3C9E1F7A645886DFD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\VerticalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp2D4D3039A8F4786B29CBC59279E11CD6" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="PUT-GUID-HERE">
+                <File Id="filD93A1F8A66A03948D28EB10C08DBC075" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\CheckIndicator.qml" />
+            </Component>
+            <Component Id="cmp11DD90574BA55792E09E85BC875E7CC6" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="PUT-GUID-HERE">
+                <File Id="filDCB41B677B2078FEF88ED999B8C5DE52" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp8B74A6DA01445238C3DB5F8302852C5B" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="PUT-GUID-HERE">
+                <File Id="fil5F817B95E18EA3903E5E492507037529" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\qmldir" />
+            </Component>
+            <Component Id="cmpACCD6EDBF6360766EBEC3D0A26FF4485" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="PUT-GUID-HERE">
+                <File Id="fil00B2852292FF266BFDC78E27246BDF0E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\qtquickcontrols2universalstyleimplplugin.dll" />
+            </Component>
+            <Component Id="cmpE2D975039020B0A28962D1B925A960E1" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="PUT-GUID-HERE">
+                <File Id="filCC47BCAA474BE0F9328DE3C9C733C42E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\RadioIndicator.qml" />
+            </Component>
+            <Component Id="cmpC300DE4CB804EF265FD4219B34BCA1C3" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="PUT-GUID-HERE">
+                <File Id="filA4283A2AAA72A0BAD305878F149F8112" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\SwitchIndicator.qml" />
+            </Component>
+            <Component Id="cmpAAF28E9A2579CFC59E2032E5D93E1329" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil882894436C42BA03390013190DA428DD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmpC40578D06BD93FA899E895CEF21DBA18" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil2AA977FB2D945038560ED99BC2DBF037" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\Button.qml" />
+            </Component>
+            <Component Id="cmpE68756474722B3F21FC5532D1062FB1D" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil83C17F6A6B9AE52616089E09065D4409" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\CheckBox.qml" />
+            </Component>
+            <Component Id="cmp3296BF97A2E0EF541BB34170D2415F01" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="filBCD6667BB8BBFA9456E666B4E33AB0B1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\ComboBox.qml" />
+            </Component>
+            <Component Id="cmp8C1B3E8BDCD066E43D0D7711C6230BF0" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil743C0CB7BC821AB1DE5E10195D4DD7F9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\Frame.qml" />
+            </Component>
+            <Component Id="cmp7837CC5D7098081B4DF4FF165458173D" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil71972C76D5C2B98132AF5B318815F12E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\GroupBox.qml" />
+            </Component>
+            <Component Id="cmpEDDCDA25E43BFDE157B14A1304F2881F" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil276159053BE57D0C24D41C849C2BAE91" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpD23B38A078CFEA9D72897D555FDE6F99" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil2DE71BEB259AC59FA0C9A0D2284808B4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmp3C6D627ED97D630610BE717D30D0D763" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil97BA32DCDFF1EDD5C462E0E34667C383" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\qmldir" />
+            </Component>
+            <Component Id="cmp8F960D4B5204B9EED233F6E3F8B827E2" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil4B2F046B200837613E9C6D66BB296674" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\qtquickcontrols2windowsstyleplugin.dll" />
+            </Component>
+            <Component Id="cmpC77F62D1E6B5F70004BAB55B363F9B11" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="filAE22C9B5BA41390AC226359F794FC06C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\RadioButton.qml" />
+            </Component>
+            <Component Id="cmpEF137D0A83F3A18031FFD79BFE04B9F2" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil685FF36668F4A89EF52829644479A47D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmpE22CC5E1ED8DF1A9868758098747EEFB" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil95C622A6C8EB1C2113128E62F1806D13" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\ScrollView.qml" />
+            </Component>
+            <Component Id="cmp87AE5D4D01002471473080DA66B6CCC0" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil8844CE9FFA700D403EBE27FEB1225120" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmp87BE1C51D8DE5CBD247E1A8892F70F82" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil9887F9433050ECE31852BEB260381494" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\Slider.qml" />
+            </Component>
+            <Component Id="cmpDC4F51A305FA129D657009601950B7A3" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="fil00A561FEF601BE682473E1310E50E3DB" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\SpinBox.qml" />
+            </Component>
+            <Component Id="cmpDF55645037117499DE465427DC8DF07D" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="filF35FCE92DDE8D63D8CFC96EEDBF8F66C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\TextArea.qml" />
+            </Component>
+            <Component Id="cmp2203A2072E1F1CCF26557C70F67B6390" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="PUT-GUID-HERE">
+                <File Id="filE88281D540F6A3D22F1B131E68180E08" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\TextField.qml" />
+            </Component>
+            <Component Id="cmpC437AB325C956B39F47527216D28546B" Directory="dir3390D3B4341886F0DA85EE3BA92439FF" Guid="PUT-GUID-HERE">
+                <File Id="filFC0B5707CF45175E2548BA04998F9D14" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpFD0F8FF1618CF94939B70BB1D4874BD5" Directory="dir3390D3B4341886F0DA85EE3BA92439FF" Guid="PUT-GUID-HERE">
+                <File Id="fil326E1B5A831353646A2BA9A334652C8E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\qmldir" />
+            </Component>
+            <Component Id="cmp19505C3234633AD6D7F2A9FF634021A4" Directory="dir3390D3B4341886F0DA85EE3BA92439FF" Guid="PUT-GUID-HERE">
+                <File Id="fil6CA3AB34BE81C0480807D319DA4D1B52" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\qtquickdialogsplugin.dll" />
+            </Component>
+            <Component Id="cmp26A12B2050D05AC837B4FE95D70CF62A" Directory="dir34D1D6742B95CCA4110D8968A4544048" Guid="PUT-GUID-HERE">
+                <File Id="fil2EF39784619037E8CC60A1D82B089DB1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp247A562FE0339C0E33051AE46663C848" Directory="dir34D1D6742B95CCA4110D8968A4544048" Guid="PUT-GUID-HERE">
+                <File Id="fil73A709C03D74D3472D379DD8D232B321" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qmldir" />
+            </Component>
+            <Component Id="cmpD771E4FD8559DD19611671F07F50AE86" Directory="dir34D1D6742B95CCA4110D8968A4544048" Guid="PUT-GUID-HERE">
+                <File Id="filF7DDABE25D647E77D24BC54ABB437481" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qtquickdialogs2quickimplplugin.dll" />
+            </Component>
+            <Component Id="cmp1F9B87E10A30CED8E63B585060A1FF8B" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="filA0DF6F7D59B18B7A0A1AF7DA6AF2FF68" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmp5CC1A9822278627BA1C6F6C0810F05AF" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="fil9352455FFAFA7537481F68705CDAB62A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\ColorInputs.qml" />
+            </Component>
+            <Component Id="cmpCD8E153BC6DC54FB225F0335FD2F1022" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="fil93D2D8A8DBA12B61620EBAF03A9BE6D6" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FileDialog.qml" />
+            </Component>
+            <Component Id="cmpA43D16647F1AFE697DD7E77561269215" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="fil209068D9C33B43DA14DC86382904CBEE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FileDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmpA5CBE15352FE008097BC876C40461A13" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="fil4540A3A2DE480ED8F7FC352CFA54A0A3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FileDialogDelegateLabel.qml" />
+            </Component>
+            <Component Id="cmpBA80190D493F665368F7F2D88009CA35" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="filD4C93FE2E3ECA41F2B088C15EE8E7B44" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FolderBreadcrumbBar.qml" />
+            </Component>
+            <Component Id="cmp2B690A76E357815CD2278DADED472DCD" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="fil0A290A43252CCACCDD06DC74AD3F0150" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FolderDialog.qml" />
+            </Component>
+            <Component Id="cmpD2D023E7908B3EF1FBC9EE76B53FF954" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="fil9048C8B68F95216847AB9C2BB586267B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FolderDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp3FD2DFA51DAE6BF8C71900CA48779DC8" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="fil090B870ACCCB3E9AC35C9BE4A09AC383" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FolderDialogDelegateLabel.qml" />
+            </Component>
+            <Component Id="cmp89D2DEE36502469CA5E9442966437A3E" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="filB7007AC91E08C3AF1B5D1FB0BF4E0D70" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FontDialog.qml" />
+            </Component>
+            <Component Id="cmp23ABBE7C0C0AFCF242C44EDCA1D7A2B5" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="filBA3B6DBDD6BF053B67B4C49EEC242FCA" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FontDialogContent.qml" />
+            </Component>
+            <Component Id="cmp0B0F513E9ED50F545C66F3A24CC92F08" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="fil65FEDAF25F1E1AC39DC10AD70C88DAB8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\HueGradient.qml" />
+            </Component>
+            <Component Id="cmp41298595F02EF32949F1566F589B454C" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="fil767F03E07565A9F69AF8C06A5EBADD4E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\MessageDialog.qml" />
+            </Component>
+            <Component Id="cmpF435AE4D59420FEEB320C8B81458D76C" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="fil85F4300A5F660E5B427FFD02F328EAF5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\PickerHandle.qml" />
+            </Component>
+            <Component Id="cmpC4C42A5D0D070431E5C9FEED80365921" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="PUT-GUID-HERE">
+                <File Id="fil67AAEC8993AE5CDEFC014103CC45F500" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\SaturationLightnessPicker.qml" />
+            </Component>
+            <Component Id="cmp69A36B9C73762AA365C1BC5F970FC8E5" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="PUT-GUID-HERE">
+                <File Id="fil45A808D742A78F7A926AD621F7283728" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmp7BBF58B50638FEB770B04C7F73171AC6" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="PUT-GUID-HERE">
+                <File Id="fil7153DDC4143EAC85FD70680E09CE21A0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FileDialog.qml" />
+            </Component>
+            <Component Id="cmp773DC90FC00B0677B841101198BF6921" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="PUT-GUID-HERE">
+                <File Id="fil87F4823E5A4E9CAD89C9F18EB7D940C2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FileDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp93F9CB69ACD380F7D59D08819255322B" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="PUT-GUID-HERE">
+                <File Id="filA3DF900A1D2C6A4D414885D446B5C70C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FolderBreadcrumbBar.qml" />
+            </Component>
+            <Component Id="cmp29403D08CB444131141FF50C5977F801" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="PUT-GUID-HERE">
+                <File Id="filEA8B9CAEBE85B9B32212D7D7AC61E3C8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FolderDialog.qml" />
+            </Component>
+            <Component Id="cmp0F3406384273F4D43A9ECCF138617EFD" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="PUT-GUID-HERE">
+                <File Id="filE73DB30D303712F991084FF0EEB941F5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FolderDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp1AD58D86AC860A01A93BF5C9248B5E0B" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="PUT-GUID-HERE">
+                <File Id="fil36CA66A25F1D4A60DCA8851C04360F5A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FontDialog.qml" />
+            </Component>
+            <Component Id="cmp2E10228540CA4421B670586FA6F602D4" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="PUT-GUID-HERE">
+                <File Id="fil103C6B50716464DFCFF67BCDF19D268F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\MessageDialog.qml" />
+            </Component>
+            <Component Id="cmpB06D35D731CDFFF625B04E79D8E88BB4" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="PUT-GUID-HERE">
+                <File Id="fil75D5FBD826D73921FABD447701DE184A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmp53496C379F6D669E9DC8EE0F3AD3E17E" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="PUT-GUID-HERE">
+                <File Id="filD95B88F29ACA98EBCB5B86D8F5FA2505" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FileDialog.qml" />
+            </Component>
+            <Component Id="cmp94CCFDA09967CE02B01DF2FD85424408" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="PUT-GUID-HERE">
+                <File Id="fil1939DC9854DE81CAD586E8EA8C9F11A5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FileDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp28FD3CFF63249E9A333566E0D4403C06" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="PUT-GUID-HERE">
+                <File Id="fil540156639772798B8D6CF3832BA6CEFF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FolderBreadcrumbBar.qml" />
+            </Component>
+            <Component Id="cmp8FB0248D377F04FE065ED8D3F0956920" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="PUT-GUID-HERE">
+                <File Id="fil1B2FC023BAB032527FD158332D7A31A1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FolderDialog.qml" />
+            </Component>
+            <Component Id="cmpEB30E27FA4A0E15D8110F522E6E24AB9" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="PUT-GUID-HERE">
+                <File Id="fil4C6AFCFFF15FA8BF18BBF16D8D04263A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FolderDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmpE831C08038ED1CE79CB86EA78782321B" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="PUT-GUID-HERE">
+                <File Id="fil49DC57941DAE9D048E67CC9D4037E20F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FontDialog.qml" />
+            </Component>
+            <Component Id="cmpC9A55BF0CB886578DBBE11A05E82098E" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="PUT-GUID-HERE">
+                <File Id="fil4366D0DAFCA17243777D0FC31A6D7CCF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\MessageDialog.qml" />
+            </Component>
+            <Component Id="cmp0ABC2788AE2935B5CB49F7CFC3447AE7" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="PUT-GUID-HERE">
+                <File Id="filB61687597342BBBABAE1E94101F3EC6E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmp1B66918327259E9332768B2CEDF63F3F" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="PUT-GUID-HERE">
+                <File Id="fil62168DF788419AC877C9C1D54BABA7E2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FileDialog.qml" />
+            </Component>
+            <Component Id="cmp7D2F50F1ECA2513B10F2D9E268603803" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="PUT-GUID-HERE">
+                <File Id="fil5909CA77FA8E0522A5ABE19D201D339E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FileDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp54231F7DE8257F6F679C508C4A324029" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="PUT-GUID-HERE">
+                <File Id="fil066B22B57C4829B076EF2637C8369609" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FolderBreadcrumbBar.qml" />
+            </Component>
+            <Component Id="cmpEBBD3D3A57C898928DA40D237B8191AE" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="PUT-GUID-HERE">
+                <File Id="fil9364C24A62A278E0DF4A32219457A0E7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FolderDialog.qml" />
+            </Component>
+            <Component Id="cmp4CFF955FC6C7A59A9C3686F6EA07DEB2" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="PUT-GUID-HERE">
+                <File Id="filFAB35638D14AAE200469B556D0704257" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FolderDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmpE18DF027A0BDBB96DACB63898F697766" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="PUT-GUID-HERE">
+                <File Id="filDA5275126CA3326219C5633BBAF43DA5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FontDialog.qml" />
+            </Component>
+            <Component Id="cmpFE745526AAA642BA836DA29C9F77ED9B" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="PUT-GUID-HERE">
+                <File Id="fil54D49371701AEB3AD847CDD309D9B185" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\MessageDialog.qml" />
+            </Component>
+            <Component Id="cmpD4B5BFCAFB6DC32B78A6C9AC75762ACC" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="PUT-GUID-HERE">
+                <File Id="fil4D10401F2E4EA015EF5CE8BEA0AE7121" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmpC67BC79E14952488E29CF9E27BD202FB" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="PUT-GUID-HERE">
+                <File Id="fil35DAD56258028832AFB445B0E0263B84" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FileDialog.qml" />
+            </Component>
+            <Component Id="cmp9F8F91188C1B368A7FE7E51517C5ACC0" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="PUT-GUID-HERE">
+                <File Id="filCCF6364980A7F563A9642915E70CB32B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FileDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp24B89BE2A868B73CC480D3F15E9AA30B" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="PUT-GUID-HERE">
+                <File Id="fil39DD65F761C1D67E92728B9250518B4D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FolderBreadcrumbBar.qml" />
+            </Component>
+            <Component Id="cmp48D9FBFB9413B749E8E5568D27D9E7BA" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="PUT-GUID-HERE">
+                <File Id="fil620700542B96A19799BA7151FA3C37CF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FolderDialog.qml" />
+            </Component>
+            <Component Id="cmpC5B074A0F5386F436C0457318AEBEAC8" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="PUT-GUID-HERE">
+                <File Id="fil26FDE36A980B8A98EB003E08B71AAAFD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FolderDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmpA671B669F6A248C1F3648B5295D227F2" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="PUT-GUID-HERE">
+                <File Id="fil158685A04206B07FCFB74FDBDD3C4579" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FontDialog.qml" />
+            </Component>
+            <Component Id="cmp388F3D53B11EE0449503AEBC67192D85" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="PUT-GUID-HERE">
+                <File Id="filF4BAFBB95A2A38BD425F65092CB041B8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\MessageDialog.qml" />
+            </Component>
+            <Component Id="cmpED7844544FD113D2024E6D3A0A781CAE" Directory="dir7397A50E4D7D313615C4CF7C94C61160" Guid="PUT-GUID-HERE">
+                <File Id="filDB53B5FD7BD0B0F1464E81D6A058D610" KeyPath="yes" Source="SourceDir\qml\QtQuick\Effects\effectsplugin.dll" />
+            </Component>
+            <Component Id="cmp2564A9FB9971981965C804A9765373B9" Directory="dir7397A50E4D7D313615C4CF7C94C61160" Guid="PUT-GUID-HERE">
+                <File Id="fil747AF0BECF09264818C8DF354CF6A40B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Effects\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp6895F64D8B98A38B0AE2C8F5A310BE47" Directory="dir7397A50E4D7D313615C4CF7C94C61160" Guid="PUT-GUID-HERE">
+                <File Id="filF8BAD332FBA7B0262015FEF686BF25A5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Effects\qmldir" />
+            </Component>
+            <Component Id="cmp690AC8265BC3CC9BA6C7C6DBD3E29F84" Directory="dir7EBA39754BD2CE291EC25825433A43D2" Guid="PUT-GUID-HERE">
+                <File Id="filAA4FB44B93EF08B6742B2481420E8468" KeyPath="yes" Source="SourceDir\qml\QtQuick\Layouts\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpC6E50C98991E0EAA0E3A4D95C7D4F3DB" Directory="dir7EBA39754BD2CE291EC25825433A43D2" Guid="PUT-GUID-HERE">
+                <File Id="filB30B70CC7D54752ADC0401EE99EBCA64" KeyPath="yes" Source="SourceDir\qml\QtQuick\Layouts\qmldir" />
+            </Component>
+            <Component Id="cmp4260C68CD0F932092808D3942F80E172" Directory="dir7EBA39754BD2CE291EC25825433A43D2" Guid="PUT-GUID-HERE">
+                <File Id="fil11D74BAB954F6060FE25421FF4DF2D92" KeyPath="yes" Source="SourceDir\qml\QtQuick\Layouts\qquicklayoutsplugin.dll" />
+            </Component>
+            <Component Id="cmpD7C080A5C7175B15B438D5E4851B9267" Directory="dirA6F6BC40CF508C5F92BB49483928E0FC" Guid="PUT-GUID-HERE">
+                <File Id="filDE9CEDD02770FA8D5A6B443C92AF13E2" KeyPath="yes" Source="SourceDir\qml\QtQuick\LocalStorage\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp6F365EE63059CFBDA7386B8F1B4B7E73" Directory="dirA6F6BC40CF508C5F92BB49483928E0FC" Guid="PUT-GUID-HERE">
+                <File Id="fil19E5C5D82A58C106C10F4EF71FBDC561" KeyPath="yes" Source="SourceDir\qml\QtQuick\LocalStorage\qmldir" />
+            </Component>
+            <Component Id="cmp56020DD868F550FF4C2761D9FCAF2820" Directory="dirA6F6BC40CF508C5F92BB49483928E0FC" Guid="PUT-GUID-HERE">
+                <File Id="fil373F860BFC86DCA9B91B06DE6B8312C0" KeyPath="yes" Source="SourceDir\qml\QtQuick\LocalStorage\qmllocalstorageplugin.dll" />
+            </Component>
+            <Component Id="cmpE6642776D930163E5B3F23D206815BB4" Directory="dirF7455B984D3CF2564DED2759788752A0" Guid="PUT-GUID-HERE">
+                <File Id="filE1D8B4CEB7BAAD320C44F6CD0D7101AF" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpF080C580F3B4157111B93C9666E6557C" Directory="dirF7455B984D3CF2564DED2759788752A0" Guid="PUT-GUID-HERE">
+                <File Id="fil7693DA2416BEB023863E57694682496C" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\qmldir" />
+            </Component>
+            <Component Id="cmp1052E5F49C616A54DF531B8121F3D962" Directory="dirF7455B984D3CF2564DED2759788752A0" Guid="PUT-GUID-HERE">
+                <File Id="fil4B9E63D0E3544A93D3C93F417E63A2D7" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\qtquickcontrols2nativestyleplugin.dll" />
+            </Component>
+            <Component Id="cmpC9C0D310B3B9C47633E52904919A68A3" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="fil4E74AF38A57B5DC51A0C8BAA7FA0248A" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultButton.qml" />
+            </Component>
+            <Component Id="cmp8A89A151469884206970043CC9CF4D86" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="fil8ABD292511F2D365661FA57136628A97" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultCheckBox.qml" />
+            </Component>
+            <Component Id="cmp998E8AF42C18DDD09BFF57E42546A8D5" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="filCAF57E8E6C8CC0D9A2780CDD88AD462B" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultComboBox.qml" />
+            </Component>
+            <Component Id="cmpD499A28F8E9F6FEE97F97777DA6CB3AE" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="fil8B58BB6E6F240EEB5AB919B5E8FF3FCC" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultDial.qml" />
+            </Component>
+            <Component Id="cmpDBDF7121809130D7416E032EA09C16B0" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="fil410E97B463184F448D3625740C057F2B" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultFrame.qml" />
+            </Component>
+            <Component Id="cmp2948DD4EC5076256EF0BA0FE75947821" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="fil514160DB4A8D6516426012B1E5DCA0C4" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultGroupBox.qml" />
+            </Component>
+            <Component Id="cmpBAD2FDC63EF91AF984D6977B2A9641A7" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="filC20F91CF453A8F4B5F75C95AADAFFDF4" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultProgressBar.qml" />
+            </Component>
+            <Component Id="cmp6504A26398241286FD87575D3DDDEE30" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="fil82D83C5E4CA154FA270B4DA26EEB3BA6" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultRadioButton.qml" />
+            </Component>
+            <Component Id="cmpF53E23CCF650110414BF5121E32F5801" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="fil203E12EF4C14B14DFCDA757CA082B189" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultScrollBar.qml" />
+            </Component>
+            <Component Id="cmpC43D383FA22807D352ED92706A3B7BB5" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="fil336CE1259605D5F6DD75031302D1D172" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultSlider.qml" />
+            </Component>
+            <Component Id="cmpE1F0EBE68EDB43973B9E52D9C0F96504" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="fil05CB3CD7C420EE0D632AB36B001DDC11" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultSpinBox.qml" />
+            </Component>
+            <Component Id="cmp8D7D9681C26F0A7B52E3C668EA4BF45B" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="fil6324820B417FACA7B03D7631F502BF76" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultTextArea.qml" />
+            </Component>
+            <Component Id="cmpF20525D1576510B72940A1B3A2B872E9" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="filE190EBA3C1D870E6CF7BCBFDC2EF4B9F" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultTextField.qml" />
+            </Component>
+            <Component Id="cmp5C615FF3D38B0DAEA6E54192A348A499" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="PUT-GUID-HERE">
+                <File Id="filB2DF42D1E5EA322F47250AD21CF4E4F5" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultTreeViewDelegate.qml" />
+            </Component>
+            <Component Id="cmp18CB7E4808B561BE6CFA935B7A701259" Directory="dir2105A1F7199CAE1171E138FDED1D8E49" Guid="PUT-GUID-HERE">
+                <File Id="filAF591582B9EB7D4C0EF0E76AB1774FE1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Particles\particlesplugin.dll" />
+            </Component>
+            <Component Id="cmp4F76529422F947A88380E6330781E9A8" Directory="dir2105A1F7199CAE1171E138FDED1D8E49" Guid="PUT-GUID-HERE">
+                <File Id="fil12011A3B6C5641C4DBB765FE3972890F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Particles\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp79A247854241165CFF0438015FC92CBB" Directory="dir2105A1F7199CAE1171E138FDED1D8E49" Guid="PUT-GUID-HERE">
+                <File Id="fil5CFE0F9500667BFD3E26D27A494DA78C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Particles\qmldir" />
+            </Component>
+            <Component Id="cmp451BE08037AE1075B3D0480F83957794" Directory="dir96881D4937C7E7E8DC6A06696EF52C96" Guid="PUT-GUID-HERE">
+                <File Id="filA5E21912CAC2144F8A23EB7C07A64E50" KeyPath="yes" Source="SourceDir\qml\QtQuick\Shapes\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp8068B6605CFFD06999D8AD47565E2117" Directory="dir96881D4937C7E7E8DC6A06696EF52C96" Guid="PUT-GUID-HERE">
+                <File Id="fil13979215E10E260593D3F3CC875D614E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Shapes\qmldir" />
+            </Component>
+            <Component Id="cmpC06754C3C30F7E84E5F94A00CBC4BF8A" Directory="dir96881D4937C7E7E8DC6A06696EF52C96" Guid="PUT-GUID-HERE">
+                <File Id="filC889B1860D61C95441338B48F27D42E8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Shapes\qmlshapesplugin.dll" />
+            </Component>
+            <Component Id="cmp60DA059DBD878BB08B9E9EE991277BD1" Directory="dirA16BBE5DE11DF2FD79B07E2965737D3E" Guid="PUT-GUID-HERE">
+                <File Id="fil57F3B26780163C8F4D0ED41B413DC849" KeyPath="yes" Source="SourceDir\qml\QtQuick\Templates\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp68A90E74F6C13409CCC0C76BF2ECC1D0" Directory="dirA16BBE5DE11DF2FD79B07E2965737D3E" Guid="PUT-GUID-HERE">
+                <File Id="filC14F61F821BEACCCD681664F76075405" KeyPath="yes" Source="SourceDir\qml\QtQuick\Templates\qmldir" />
+            </Component>
+            <Component Id="cmp6B89F1FC6840B89D467D2EBE8B043FE1" Directory="dirA16BBE5DE11DF2FD79B07E2965737D3E" Guid="PUT-GUID-HERE">
+                <File Id="filED4C60CE9B81695ECE44D214DCBCCE02" KeyPath="yes" Source="SourceDir\qml\QtQuick\Templates\qtquicktemplates2plugin.dll" />
+            </Component>
+            <Component Id="cmp69BEF3BE02352CA6B785101AAED71318" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="PUT-GUID-HERE">
+                <File Id="fil1760F73BD6E3E7286EB8F1D3A33C2073" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Component.qml" />
+            </Component>
+            <Component Id="cmp10D98830533C42F8706977851B39A3A0" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="PUT-GUID-HERE">
+                <File Id="filBF176D7C05C63830AFF6998ABFB38A7B" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Enum.qml" />
+            </Component>
+            <Component Id="cmp15249FC563E9C8918952F7D1A2F805C9" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="PUT-GUID-HERE">
+                <File Id="fil2CAD05441DC803A5186E966A1DDA48C6" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Member.qml" />
+            </Component>
+            <Component Id="cmp4833C19054EF2E14E372AD2880495C73" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="PUT-GUID-HERE">
+                <File Id="fil43B1B5A91FA45D5C106A45D805FEFC40" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Method.qml" />
+            </Component>
+            <Component Id="cmp33AC2E102B879ECCC6316EF7AEFDBB99" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="PUT-GUID-HERE">
+                <File Id="fil95F6CB39A7A168DA09B6ABC8A860B095" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Module.qml" />
+            </Component>
+            <Component Id="cmpD956362F874FB96C3A0DFCADA1405E4B" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="PUT-GUID-HERE">
+                <File Id="fil12BB133642A6251AFE56340C20C38432" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Parameter.qml" />
+            </Component>
+            <Component Id="cmp05C5F4FC22BE05FF20AF6B70F2648E30" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="PUT-GUID-HERE">
+                <File Id="filC998D7BD8B71F6CD05288B0CF1CE0D63" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Property.qml" />
+            </Component>
+            <Component Id="cmpA5F05A0EC98A80D47E263C8226347F09" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="PUT-GUID-HERE">
+                <File Id="filB09D42B54721DA6F6C88B761F67D1CD3" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\qmldir" />
+            </Component>
+            <Component Id="cmp69C56DED012A8CF7146BB347775E1111" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="PUT-GUID-HERE">
+                <File Id="fil2C39EC74EEBD3E3FB173F25F6474B71B" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\quicktooling.qmltypes" />
+            </Component>
+            <Component Id="cmp8CF4D885EDBD87E92F14AD9224A64585" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="PUT-GUID-HERE">
+                <File Id="fil619C9C3A532439F43969D239FC47574E" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\quicktoolingplugin.dll" />
+            </Component>
+            <Component Id="cmp40C935D75C83A908961342FA5F190AB4" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="PUT-GUID-HERE">
+                <File Id="fil9A187523ABDC241490F213412E806915" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Signal.qml" />
+            </Component>
+            <Component Id="cmp3E281B1E4C74C596886969A2CCFF6DB0" Directory="dir27DABF0F34E48010C38FDC22DEE2453A" Guid="PUT-GUID-HERE">
+                <File Id="fil620C826946D01508C458662B95E34005" KeyPath="yes" Source="SourceDir\qml\QtQuick\Window\qmldir" />
+            </Component>
+            <Component Id="cmp4C7449C9615F5B56C877F1DD796BCED9" Directory="dir27DABF0F34E48010C38FDC22DEE2453A" Guid="PUT-GUID-HERE">
+                <File Id="fil5C4310277F80C74CE8597AA07AADF489" KeyPath="yes" Source="SourceDir\qml\QtQuick\Window\quickwindow.qmltypes" />
+            </Component>
+            <Component Id="cmp8CAB2B6A300CCCBD332533B4BFBF9125" Directory="dir27DABF0F34E48010C38FDC22DEE2453A" Guid="PUT-GUID-HERE">
+                <File Id="fil8749130B368B4A583386390720832DC9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Window\quickwindowplugin.dll" />
+            </Component>
+            <Component Id="cmpC680AB1168D76A008F6D481AF9C77950" Directory="dir30FC873BE970351C9459A1383DCBEA3D" Guid="PUT-GUID-HERE">
+                <File Id="fil64CB17C538790FF2F41160D864EE25E2" KeyPath="yes" Source="SourceDir\qml\QtWebChannel\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp072B7F91297F86D3BDCFBD50CEADEC08" Directory="dir30FC873BE970351C9459A1383DCBEA3D" Guid="PUT-GUID-HERE">
+                <File Id="fil830FE0B6AE85ABAD33F965FA48D2A9D4" KeyPath="yes" Source="SourceDir\qml\QtWebChannel\qmldir" />
+            </Component>
+            <Component Id="cmpB9B4251EC70936E366F1ADEF4DBA9A35" Directory="dir30FC873BE970351C9459A1383DCBEA3D" Guid="PUT-GUID-HERE">
+                <File Id="fil2433013655774AC94A0236C340127486" KeyPath="yes" Source="SourceDir\qml\QtWebChannel\webchannelplugin.dll" />
+            </Component>
+            <Component Id="cmp136E2541355213ECB53F1748901B5921" Directory="dirD4BB3B3030E6C19000CA3A1BC69B6F39" Guid="PUT-GUID-HERE">
+                <File Id="fil6E5C9BBEEE5288F679B12AE6A49657B2" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpF56EE8D13DFE14559F6E7B9EF1D34569" Directory="dirD4BB3B3030E6C19000CA3A1BC69B6F39" Guid="PUT-GUID-HERE">
+                <File Id="fil8097C7F5EBB660B90F65F96EC62F6342" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\qmldir" />
+            </Component>
+            <Component Id="cmp8E3363215DF077EB827751DB665D9DD1" Directory="dirD4BB3B3030E6C19000CA3A1BC69B6F39" Guid="PUT-GUID-HERE">
+                <File Id="fil2497BDEBFF9ED845670AED37393D4EDD" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\qtwebenginequickplugin.dll" />
+            </Component>
+            <Component Id="cmp7A8772E58D71483AB938D78C8B7D1B79" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="filE1BB18B10EE0470145DDDA0C5E4A3B99" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\AlertDialog.qml" />
+            </Component>
+            <Component Id="cmpEE8B93301235F322A5BD23F0C50EED3F" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="fil3D4C79F57FA6E2E2A3343868C653A6A5" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\AuthenticationDialog.qml" />
+            </Component>
+            <Component Id="cmp2C69E47276AE27A52BD269FB9BE30AAD" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="fil80A7B2A5FCF793D0801C69BCBD98F249" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\AutofillPopup.qml" />
+            </Component>
+            <Component Id="cmpA9795EC6D9B937B8A86B7B2846C91948" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="filA6C1CA6308618936A31EB03CEB74F27A" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmp59759C9323D1558B224AADEA2AAF9C64" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="fil651A6A58BA66409A10279997FDF431A7" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\ConfirmDialog.qml" />
+            </Component>
+            <Component Id="cmp1B4AF93D806ADB8B732F2A27EB652B98" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="filF73B3538DC6FC52FF15D02C2C0E58D32" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\DirectoryPicker.qml" />
+            </Component>
+            <Component Id="cmpF2CCD1A91F993653DF09C0E15EC64A4B" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="fil74DBD34E74C737746E6CAB216E06D48F" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\FilePicker.qml" />
+            </Component>
+            <Component Id="cmpA7D7583F9A23AD1D9C5E1762D7868578" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="filD4288E12E49244FBBCD5FC5EFFE0298C" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\Menu.qml" />
+            </Component>
+            <Component Id="cmp7101449A820EA328AA1A4D1B5E999A0C" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="filBD111FAA5EF2C87B70E794D43B659AF2" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\MenuItem.qml" />
+            </Component>
+            <Component Id="cmpAD3AF866463125CF8655A99CED3AA499" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="fil98058066907D9EBDF12B81A4E59979BD" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmpE19B99C56B831180E601BB3589E3F3EA" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="filE382C586DDB2EA853C109D8235DF6C4F" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\PromptDialog.qml" />
+            </Component>
+            <Component Id="cmp4715158A523F0FB8C16A313FD382AFF9" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="filA500F08171E7AD5EF0B2841D2A86F407" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\qmldir" />
+            </Component>
+            <Component Id="cmp2696C32671FC8FBEF137F3AC34DE43FD" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="filE6605EF825D5763D112ABD3139E76B74" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\qtwebenginequickdelegatesplugin.dll" />
+            </Component>
+            <Component Id="cmp2E51274B580CEE17803AD1CE80B76947" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="fil93F79CE6E26226D4196803E05887053C" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\ToolTip.qml" />
+            </Component>
+            <Component Id="cmp90F62D3657982F0E2E1182816125FED6" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="fil61B872246F6A108B9B7BE4F4F7E90DE8" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\TouchHandle.qml" />
+            </Component>
+            <Component Id="cmp14169E3CF5A8B218146130249FBB815D" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="filFE30BDE0B2C918072AA010AE4AFF6C82" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\TouchSelectionMenu.qml" />
+            </Component>
+            <Component Id="cmpB07E9D9CFF105AE7053ACEA3668A2A8E" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="PUT-GUID-HERE">
+                <File Id="filB51A511EB7DCD4E087D230FD943C82DB" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\WebEngineQuickDelegatesQml.qmltypes" />
+            </Component>
+            <Component Id="cmp740EB509039EF4F9005AA06519EACF79" Directory="dirAB78018B8375BAA818F1C40C8D16F821" Guid="PUT-GUID-HERE">
+                <File Id="fil22604A1353A5B198D4846DC21D1783C9" KeyPath="yes" Source="SourceDir\qml\QtWebView\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpBBC59F3C27766AE0AB5515EFACEFAEC7" Directory="dirAB78018B8375BAA818F1C40C8D16F821" Guid="PUT-GUID-HERE">
+                <File Id="filD3823857AC018E3558C9BEC3252F47AB" KeyPath="yes" Source="SourceDir\qml\QtWebView\qmldir" />
+            </Component>
+            <Component Id="cmp40BFD4D4FCB9AB19F9943260306B2645" Directory="dirAB78018B8375BAA818F1C40C8D16F821" Guid="PUT-GUID-HERE">
+                <File Id="filFD81EBBF5AA65D21E5FC3AC0F9F27DD4" KeyPath="yes" Source="SourceDir\qml\QtWebView\qtwebviewquickplugin.dll" />
+            </Component>
+            <Component Id="cmp3C72ADF7447C02CBC442F4D498F97D6E" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="PUT-GUID-HERE">
+                <File Id="fil1468B4258ED9D02B8E682200C411FD95" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_debugger.dll" />
+            </Component>
+            <Component Id="cmpD130E0EDF6AF950EA6D6A53A1751AA96" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="PUT-GUID-HERE">
+                <File Id="filF63AD8464B4C5A27DCC098B1D1D42511" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_inspector.dll" />
+            </Component>
+            <Component Id="cmp5BDACEF05055E085579E7B097E890197" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="PUT-GUID-HERE">
+                <File Id="filB504D3FAAE942E0495C3640CFD5B377B" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_local.dll" />
+            </Component>
+            <Component Id="cmpBC2D56955BEA8DFE7E62B048E7F30EE8" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="PUT-GUID-HERE">
+                <File Id="fil26713E98F405141391C6BEA20282D73E" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_messages.dll" />
+            </Component>
+            <Component Id="cmpC9297AB1335961FB731B05361407FD09" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="PUT-GUID-HERE">
+                <File Id="fil7BAF56034C328F11684D5D91E5582BA6" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_native.dll" />
+            </Component>
+            <Component Id="cmp19E5AFEAFD4EE414B80BF9256489A85F" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="PUT-GUID-HERE">
+                <File Id="fil715013DA57F3A007F8DF4E53099D3C7A" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_nativedebugger.dll" />
+            </Component>
+            <Component Id="cmp57FF2A44B7244B35C17BB63FE8A1D97F" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="PUT-GUID-HERE">
+                <File Id="fil75DECF1849BEFF9DAC535DA290EA2440" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_preview.dll" />
+            </Component>
+            <Component Id="cmp7F8E313E83EEF3F0A5320389A0D409F0" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="PUT-GUID-HERE">
+                <File Id="filD2DF020A7341138A3174AA9926C94254" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_profiler.dll" />
+            </Component>
+            <Component Id="cmpA1FA5ACBABE6ABAF59E8A15E771A1020" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="PUT-GUID-HERE">
+                <File Id="fil2F21F43E56AEBBDE0C3002B3565087E0" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_quickprofiler.dll" />
+            </Component>
+            <Component Id="cmp3EA7E1216A1E529D7944144D42506A2C" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="PUT-GUID-HERE">
+                <File Id="filFABEED80D5D5652BC0BA2F689F6592E0" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_server.dll" />
+            </Component>
+            <Component Id="cmpCBA063886C021F79C883C543705AD5ED" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="PUT-GUID-HERE">
+                <File Id="fil0839ED3FA6DF360E9FE61C24F7241731" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_tcp.dll" />
+            </Component>
+            <Component Id="cmp7939DBD081402A3D5A1E67BDD3FDC674" Directory="dir08941CEA6046320FCCF158F759AF80F2" Guid="PUT-GUID-HERE">
+                <File Id="fil51E0E82B321287CAC39351EA74609C90" KeyPath="yes" Source="SourceDir\resources\icudtl.dat" />
+            </Component>
+            <Component Id="cmpFCDF3702D9093A75BC161A3A1BB6BB86" Directory="dir08941CEA6046320FCCF158F759AF80F2" Guid="PUT-GUID-HERE">
+                <File Id="fil5E3E79E2C8D58B41AC6EF5891C08A810" KeyPath="yes" Source="SourceDir\resources\qtwebengine_devtools_resources.pak" />
+            </Component>
+            <Component Id="cmp09CCD93B93EDDE01A6B28A19B724194A" Directory="dir08941CEA6046320FCCF158F759AF80F2" Guid="PUT-GUID-HERE">
+                <File Id="fil08082A21F54192390BB8CCF88F7E6B9D" KeyPath="yes" Source="SourceDir\resources\qtwebengine_resources.pak" />
+            </Component>
+            <Component Id="cmp263082AF02EA4B9F434F2501E23AC972" Directory="dir08941CEA6046320FCCF158F759AF80F2" Guid="PUT-GUID-HERE">
+                <File Id="fil24B7F212EFD5AB10B522561407BDDFC9" KeyPath="yes" Source="SourceDir\resources\qtwebengine_resources_100p.pak" />
+            </Component>
+            <Component Id="cmpC2267073DB55586843E13B823F6E0E8B" Directory="dir08941CEA6046320FCCF158F759AF80F2" Guid="PUT-GUID-HERE">
+                <File Id="filFDCD09910FB501B1A747E0A04B8D3E31" KeyPath="yes" Source="SourceDir\resources\qtwebengine_resources_200p.pak" />
+            </Component>
+            <Component Id="cmp48ADD1B8E5492EE4CAF352021174B247" Directory="dirD0D2BD28387A827DDF373DED43543ABA" Guid="PUT-GUID-HERE">
+                <File Id="filDE0A213E8CE9DC57CA82C7CBF94A73CA" KeyPath="yes" Source="SourceDir\sqldrivers\qsqlite.dll" />
+            </Component>
+            <Component Id="cmpFA34FD86FCB201001C1C95C2210B09BB" Directory="dirD0D2BD28387A827DDF373DED43543ABA" Guid="PUT-GUID-HERE">
+                <File Id="filABD7D7F4392312E7C2C8CDC617BC2016" KeyPath="yes" Source="SourceDir\sqldrivers\qsqlodbc.dll" />
+            </Component>
+            <Component Id="cmp5DAC8E92F0D1CE1028B76D02F12152DE" Directory="dir3BC41331752E78D0C2719C277915294F" Guid="PUT-GUID-HERE">
+                <File Id="fil4F3C3348649DA965E980E99B9E82B7FF" KeyPath="yes" Source="SourceDir\styles\qwindowsvistastyle.dll" />
+            </Component>
+            <Component Id="cmpD57693E12A6CF9AFDC034864F2950D6F" Directory="dir509C75F94B9AF6C35CA00410005C14EE" Guid="PUT-GUID-HERE">
+                <File Id="filA3197B081536A45213DBC7CD12A615C4" KeyPath="yes" Source="SourceDir\tls\qcertonlybackend.dll" />
+            </Component>
+            <Component Id="cmpC8A93EB5F0E2881753F8DCDA1B02129B" Directory="dir509C75F94B9AF6C35CA00410005C14EE" Guid="PUT-GUID-HERE">
+                <File Id="fil63F4FB8EB696B5BA01AD976846F45663" KeyPath="yes" Source="SourceDir\tls\qschannelbackend.dll" />
+            </Component>
+            <Component Id="cmpE7627EECD23898BC1AEEEBA80CF4C18E" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil841A28DBCBF2256981B583E30DC241AF" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\am.pak" />
+            </Component>
+            <Component Id="cmp4AA1EF8E6B3E8E5742AC3878237B15BD" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil7B1E977DD95738FCA43A5AF4E3DB820F" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ar.pak" />
+            </Component>
+            <Component Id="cmp489F46694306763E664E68F1A1D9398A" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filF1068C95C51734B905AD68641F8932DE" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\bg.pak" />
+            </Component>
+            <Component Id="cmpCB3806B057A11129336C03B60DB93368" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil5E98C112CCDD7A81098634A800367973" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\bn.pak" />
+            </Component>
+            <Component Id="cmp117BEDE3E308B6346D00371AE16FC727" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil1F344AC58CF6783633B80F8532689D84" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ca.pak" />
+            </Component>
+            <Component Id="cmp1B4BD11EB6D78DE042388FC7B3B10979" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil2DFDD403496C8DA8549416381A275F4B" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\cs.pak" />
+            </Component>
+            <Component Id="cmp8B5804D6EA4CE8594E25FA2EAA53B2CE" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil8998ADC69728900F32D27523BE7C0A7A" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\da.pak" />
+            </Component>
+            <Component Id="cmp14635E19526FBB5CC031D48A21B8DFE4" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filE4885D67548AFA65C78C08300C700419" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\de.pak" />
+            </Component>
+            <Component Id="cmp0DD34B1E211835A0825C4F37AEC7D421" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil544A6F5F8C37D55BB452375A1C857B0E" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\el.pak" />
+            </Component>
+            <Component Id="cmpBB56FFD9B4B03F1B7EFFE608596C1DF0" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filC138B25B3D7A4129D2F01586DDAB4929" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\en-GB.pak" />
+            </Component>
+            <Component Id="cmp7D1B7857542317367F521676B9C49306" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil1726E7077A64870D0E207D3578CBBDF4" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\en-US.pak" />
+            </Component>
+            <Component Id="cmpAC456BCAF3DCFA01198913534FCD2F3E" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil2DFEE5CE81BFF7E138A6954DE8118BBF" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\es-419.pak" />
+            </Component>
+            <Component Id="cmp561D4111326710AC9AA46907E05AE2F8" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil5149EA34D842EEA58E32E2FF3EA9FC74" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\es.pak" />
+            </Component>
+            <Component Id="cmp767F92E764F493FC663E591BBF691924" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil175C98AD989D0A19A6051CECB41755E7" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\et.pak" />
+            </Component>
+            <Component Id="cmp83CE7D31B03F0E640D486C8951C9F88A" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil80A0D15334F4AE3C93E9AB996047F063" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\fa.pak" />
+            </Component>
+            <Component Id="cmpB556D1FABA724DC54358690B4A4F5528" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil9E20F6D165CA887227872340CF842DB7" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\fi.pak" />
+            </Component>
+            <Component Id="cmp2229E25AED1AE14594087A37BF635290" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filF183998CF77694A80E81B414ADAAD6F6" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\fil.pak" />
+            </Component>
+            <Component Id="cmp7E4EE520FFDF02CB789F8284CCF745D8" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil2CEB374D9A3E9F8697A7245C40BAE974" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\fr.pak" />
+            </Component>
+            <Component Id="cmpB06A2E85562494B9B6F676A03E87D654" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filBC5A940DFAF4F4289C03179C3331CA32" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\gu.pak" />
+            </Component>
+            <Component Id="cmpCCDE1181B25851E3340339CF61382B80" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil96390711BE9B947D7B7FA5E0211E42E9" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\he.pak" />
+            </Component>
+            <Component Id="cmp25690B79DEAC48ED058A09275BBAAC7B" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil85370AA4C63C2DECEC33E30E3DB303F9" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\hi.pak" />
+            </Component>
+            <Component Id="cmp6EE71F73860FDA82CE0BA04F55AB4E65" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil8F215425A3BC657C3D4C352ECD1F7101" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\hr.pak" />
+            </Component>
+            <Component Id="cmpD21C6E91A9244444957B19A630773D29" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filE9F1E4FD2A7A617A4E4166414AB45691" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\hu.pak" />
+            </Component>
+            <Component Id="cmp38389365912F0AE1DFACDDC8479C7BF2" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil7D1F559C2C1110D18CA5764F1971BBA7" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\id.pak" />
+            </Component>
+            <Component Id="cmp9BCC30608F875FB2B6809ACBE266A0EB" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil4CA2DD502CC46D09E241817165D07A69" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\it.pak" />
+            </Component>
+            <Component Id="cmp66B3A389FF5706DD47F6031C96870AC3" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil9A44734AE8E81C970FCD4CD1A15562EC" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ja.pak" />
+            </Component>
+            <Component Id="cmpC35D767A0329A304ED9409EE1758B26E" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filBEF8A5F318E161CAB9EB973C81EE8BAD" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\kn.pak" />
+            </Component>
+            <Component Id="cmp4FAE7008D38C2834FF608ECB10F422B3" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil0E05F04BA9603B8A46D202EE87E01968" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ko.pak" />
+            </Component>
+            <Component Id="cmp41E232D5090F2D0E0B1781C90734A625" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filB1E277BB14B69222B7C7DCC91A1E41D0" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\lt.pak" />
+            </Component>
+            <Component Id="cmp68559A181BEB78EFD5137F57CAD5FB00" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil75045C6FE752E32D2CA7E724F92A4C03" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\lv.pak" />
+            </Component>
+            <Component Id="cmp0F1F24602E525646795B185F7B39955A" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil4EAF6A9B314FEFD4868F09695F2883EF" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ml.pak" />
+            </Component>
+            <Component Id="cmpAD12B9F7ABF28F48C3C94ADA02853AD5" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filD4AE47CF3E0E2219C965A01B69346DC6" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\mr.pak" />
+            </Component>
+            <Component Id="cmpA6990FFF2D1086D28FBE7A600D52F67C" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil8DA799DEB9484A9C1D8C766F558D928B" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ms.pak" />
+            </Component>
+            <Component Id="cmp74CC7AE3E2D73D6EB448973554564268" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filD917C45F7D063800C10DFF694350E88F" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\nb.pak" />
+            </Component>
+            <Component Id="cmpCF99C5168606D7B5069B19761014FF91" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil0BBBA2ED5EA505A0761814636A3E14CC" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\nl.pak" />
+            </Component>
+            <Component Id="cmpCF5AA8564BF2B7AECAAE7FFE84D8E4D9" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filBBE0AA8ADCAD675D5BE7906A7557CA33" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\pl.pak" />
+            </Component>
+            <Component Id="cmp1E4EF1965C96EAF1EDF0ED36C03EAE83" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil6A9727728F57E501FC885A71011F615B" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\pt-BR.pak" />
+            </Component>
+            <Component Id="cmp291C58BF5375B658AD36A7D92A0C8479" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil5444B8CC9431DD88F907C57EA9A90572" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\pt-PT.pak" />
+            </Component>
+            <Component Id="cmpAE702D6C4F5F596DB6B15FB36C6D9DAB" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filB15BEEE3DA276ADEA0E8A24C77D5A9C2" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ro.pak" />
+            </Component>
+            <Component Id="cmpDF7385B2F039DBE31A2545C254ABF0F0" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filBCB5F742C4B473A8C6C5CF4917E6144A" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ru.pak" />
+            </Component>
+            <Component Id="cmpDF8F1EB0E2A70BD5F396A75FECB92533" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil9769BAFE384BB1853A2C508DFA05229F" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\sk.pak" />
+            </Component>
+            <Component Id="cmpDB5BD4BF5A46761C6544B87CF7085EEB" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filA84154373398754DDDB4E950A134C5F5" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\sl.pak" />
+            </Component>
+            <Component Id="cmp1C71511A57869D1F5DD784E9E4191BFD" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil5F171B8A37BA8123184CABD8A9F23CFA" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\sr.pak" />
+            </Component>
+            <Component Id="cmpB922A5B2744CD81821403BFD931F8112" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil20AA02ACBBE6A5F32DE18FF2F0A7AEFF" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\sv.pak" />
+            </Component>
+            <Component Id="cmp6DAB86BB3D2953D10957B0FAFF22E155" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil6199BA4B33801E936F1847E792F5CAAB" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\sw.pak" />
+            </Component>
+            <Component Id="cmpFC87AE461BD4B51F4A19E039A3C25CA1" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil38C023D4016EF85C52C0B835FC94F0C7" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ta.pak" />
+            </Component>
+            <Component Id="cmp58B01AD92CF1FEEF6EA0325F60D05AA6" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil09449582AB62FB08D7CD3574754B8C0C" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\te.pak" />
+            </Component>
+            <Component Id="cmpA3A2E454F58F8435858E88AC73495360" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil27FE0443556E7587E13EE46CFBF62A4F" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\th.pak" />
+            </Component>
+            <Component Id="cmp15CD85788F2B7BE37D3D33C7B69D5FF6" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil55F7FECA91CEE1F865B75C6581438FFC" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\tr.pak" />
+            </Component>
+            <Component Id="cmp2D4EB727F69B23FADD8074CB77E51F36" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filC97CBBD646D7E81182BFE9ECB61844CA" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\uk.pak" />
+            </Component>
+            <Component Id="cmp43AF26BE2B604BD873763D73460FB4B9" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil467C07F74FBFC01F0D823F0E76F07FB7" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\vi.pak" />
+            </Component>
+            <Component Id="cmp8CCECC99D6C73FD198EA365D4835864F" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="filD0D4650DF850FAD942030BBC923D8D7A" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\zh-CN.pak" />
+            </Component>
+            <Component Id="cmp9F8E9C0770E5832169033250113D1A21" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="PUT-GUID-HERE">
+                <File Id="fil2FAD3F60E739CE8D347E5ADE50EA653C" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\zh-TW.pak" />
+            </Component>
+            <Component Id="cmp8F41C0E31AFDC29F8BDF013E907B2A5B" Directory="dir90EFFBA81E5867E3E251FBDCB82DBC0C" Guid="PUT-GUID-HERE">
+                <File Id="filCF87AAEC5D091D6D8852B61917DB3BBF" KeyPath="yes" Source="SourceDir\webview\qtwebview_webengine.dll" />
+            </Component>
+        </ComponentGroup>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir171007FEE30C4A50EC7ED2CB58E0B5E3" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir175FB1A22F883092D2FC39E138CDC5FC" Name="Controls" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir1A86B48CD44E777E78B13993BE3DFC29">
+            <Directory Id="dir1A6A7D662F671C349F73CA17E4F3DA62" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir1A86B48CD44E777E78B13993BE3DFC29" Name="Fusion" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir2105A1F7199CAE1171E138FDED1D8E49" Name="Particles" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir27DABF0F34E48010C38FDC22DEE2453A" Name="Window" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir2B55988B75845F194F607E4ED40BB41B" Name="Universal" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir2C126EEEB46329DB6052C65B78F1DA75" Name="tooling" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir93F3A6F9A0F15C62D9983B7251AEF092">
+            <Directory Id="dir2FA83874D84EB850CB9EC2829B8A3243" Name="GraphicalEffects" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dir30FC873BE970351C9459A1383DCBEA3D" Name="QtWebChannel" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir3390D3B4341886F0DA85EE3BA92439FF" Name="Dialogs" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir3390D3B4341886F0DA85EE3BA92439FF">
+            <Directory Id="dir34D1D6742B95CCA4110D8968A4544048" Name="quickimpl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirF7455B984D3CF2564DED2759788752A0">
+            <Directory Id="dir3A2097370D43A6BEED9BF1FBE00B9935" Name="controls" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir463F84444BF926F4998942302E2C59AF" Name="Material" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dir4B7D2A19DD0D33E235AA7BD43615A87E" Name="QtQuick" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir5C7B63749559BE5BFFCAD592A78D8BA7">
+            <Directory Id="dir4E741D8C175690CD9096A89077530C9C" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir547CFC29D0E6DE0A73542C0E043048F6" Name="Basic" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir5C7B63749559BE5BFFCAD592A78D8BA7" Name="Imagine" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir7397A50E4D7D313615C4CF7C94C61160" Name="Effects" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir34D1D6742B95CCA4110D8968A4544048">
+            <Directory Id="dir755D907146AC4C2DC7E3BE88C0F1A70F" Name="qml" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir547CFC29D0E6DE0A73542C0E043048F6">
+            <Directory Id="dir78C3D911E47BC1C88AC3A9AC7D40EEC3" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir2FA83874D84EB850CB9EC2829B8A3243">
+            <Directory Id="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Name="private" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir7EBA39754BD2CE291EC25825433A43D2" Name="Layouts" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir755D907146AC4C2DC7E3BE88C0F1A70F">
+            <Directory Id="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Name="+Imagine" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir755D907146AC4C2DC7E3BE88C0F1A70F">
+            <Directory Id="dir8C55A2520B05F605E480B8F2F2039F1C" Name="+Universal" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir755D907146AC4C2DC7E3BE88C0F1A70F">
+            <Directory Id="dir929495D16D3C46BFD28C6AD595BCD701" Name="+Fusion" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dir92990262DE0ED5508C44523003A4B360" Name="QtQml" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir92990262DE0ED5508C44523003A4B360">
+            <Directory Id="dir92A51CED876E1F4286E6663FD7D54CDA" Name="WorkerScript" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dir93F3A6F9A0F15C62D9983B7251AEF092" Name="Qt5Compat" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir96881D4937C7E7E8DC6A06696EF52C96" Name="Shapes" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirD4BB3B3030E6C19000CA3A1BC69B6F39">
+            <Directory Id="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Name="ControlsDelegates" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir1F7AD7AA9B228AED2E74CDD7C3088B5C">
+            <Directory Id="dirA016FBC978C7F28C3C5CDBC06002A5D6" Name="qtwebengine_locales" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dirA16BBE5DE11DF2FD79B07E2965737D3E" Name="Templates" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir463F84444BF926F4998942302E2C59AF">
+            <Directory Id="dirA55BCC7BED6B60785CEB74AFBA374495" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dirA6F6BC40CF508C5F92BB49483928E0FC" Name="LocalStorage" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir92990262DE0ED5508C44523003A4B360">
+            <Directory Id="dirAA6B0932D46A7D6E35A8D1E06E10B2B7" Name="Models" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dirAB78018B8375BAA818F1C40C8D16F821" Name="QtWebView" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dirB030248D75EA99E75698A1E011FE9CC0" Name="Windows" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir2B55988B75845F194F607E4ED40BB41B">
+            <Directory Id="dirBF720220167083D5C665DD0D131194BE" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir755D907146AC4C2DC7E3BE88C0F1A70F">
+            <Directory Id="dirC980C9942B69E6635E4CF7C57652D12B" Name="+Material" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir92990262DE0ED5508C44523003A4B360">
+            <Directory Id="dirD3E9E07666E50DD0B119271684422637" Name="Base" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir92990262DE0ED5508C44523003A4B360">
+            <Directory Id="dirD48123A7B6E2CFB660EC94F0CBE0276D" Name="XmlListModel" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dirD4BB3B3030E6C19000CA3A1BC69B6F39" Name="QtWebEngine" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dirF7455B984D3CF2564DED2759788752A0" Name="NativeStyle" />
+        </DirectoryRef>
+    </Fragment>
+</Wix>
\ No newline at end of file
diff --git a/win/qt6.wxs b/win/qt6.wxs
new file mode 100644 (file)
index 0000000..583b447
--- /dev/null
@@ -0,0 +1,2203 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
+    <Fragment>
+        <DirectoryRef Id="INSTALLDIR">
+            <Directory Id="dir5986BBEEE82C5D978676757AB9BBF16F" Name="generic" />
+            <Directory Id="dirB10C059F651528B59E1FD268EB08C6D6" Name="iconengines" />
+            <Directory Id="dir20189C824D4DDABC3D9FF9449C089CE0" Name="imageformats" />
+            <Directory Id="dir690B8E6D609B0DB17DB71E8B1FB2FEC5" Name="LICENSES" />
+            <Directory Id="dir4C08960BD61EEE75AE4A87FC65A5DE2A" Name="networkinformation" />
+            <Directory Id="dir826692792B0995C8DF68919F5C684331" Name="platforms" />
+            <Directory Id="dirC15EA267FAD4FC447E84FB3A32CBDB68" Name="position" />
+            <Directory Id="dirC5F8EA44B55B774D108D66814D18D4D1" Name="qml" />
+            <Directory Id="dir9331125B8BF1F459D0AC9F91A6A2F779" Name="qmltooling" />
+            <Directory Id="dir08941CEA6046320FCCF158F759AF80F2" Name="resources" />
+            <Directory Id="dir3BC41331752E78D0C2719C277915294F" Name="styles" />
+            <Directory Id="dir509C75F94B9AF6C35CA00410005C14EE" Name="tls" />
+            <Directory Id="dir1F7AD7AA9B228AED2E74CDD7C3088B5C" Name="translations" />
+            <Directory Id="dir90EFFBA81E5867E3E251FBDCB82DBC0C" Name="webview" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <ComponentGroup Id="jacktrip">
+            <Component Id="cmpBD33227FC5AD6BA1A293384B57B5C3E1" Directory="INSTALLDIR" Guid="{887636EF-438E-4FE7-867F-50D3594FF8A9}">
+                <File Id="filF7FA605CFD2E8074C565F825A465F67E" KeyPath="yes" Source="SourceDir\D3Dcompiler_47.dll" />
+            </Component>
+            <Component Id="cmpAD46F02BC5359E4227248E749EB0E74F" Directory="INSTALLDIR" Guid="{B912A765-5293-4F00-822B-0B355CF6C665}">
+                <File Id="fil572E83C11D35ADB0E4CB1A0A0AF474C6" KeyPath="yes" Source="SourceDir\dialog.bmp" />
+            </Component>
+            <Component Id="cmpE6579DC78E167F46AB23E0BE3EAA58CE" Directory="INSTALLDIR" Guid="{27BFCC7D-F4A8-4F71-8A2B-00A8BC74B106}">
+                <File Id="fil5E6E1243EAE085FE802B6D64690357B0" KeyPath="yes" Source="SourceDir\jacktrip.exe" />
+                <Shortcut Id="startmenuJackTrip" Directory="ProgramMenuDir" Name="JackTrip"
+                    WorkingDirectory="INSTALLDIR" Icon="jacktrip.exe" IconIndex="0" Advertise="yes" />
+            </Component>
+            <Component Id="cmp64BB59E0E19E219D4D4D5C775D71731F" Directory="INSTALLDIR" Guid="{AC1AB986-E75A-4F3D-B849-F0CB485F1573}">
+                <File Id="fil7D228BFB31A8A99EE307AF8BFC2E1841" KeyPath="yes" Source="SourceDir\LICENSE.md" />
+            </Component>
+            <Component Id="cmpE2BC49F17425202019B2C48873956253" Directory="INSTALLDIR" Guid="{4F42E138-BC56-4253-8AE7-47D1C92EA803}">
+                <File Id="fil011E2822115DD1D5BD9A9096D1914F51" KeyPath="yes" Source="SourceDir\license.rtf" />
+            </Component>
+            <Component Id="cmpC2B5C938CDBD7BD0FAE06ACAABDB3B54" Directory="INSTALLDIR" Guid="{E3D42FA3-DD28-4928-AB36-D33EFC3D454E}">
+                <File Id="fil025966B7C1FB522423ADF8E9CE443022" KeyPath="yes" Source="SourceDir\opengl32sw.dll" />
+            </Component>
+            <Component Id="cmp7111706DBFB0E180EC7A8BDC93A90139" Directory="INSTALLDIR" Guid="{B19ABB59-5607-4637-93D8-C03CA180767F}">
+                <File Id="fil1A08189509DCFE1E185AEC6B499929BA" KeyPath="yes" Source="SourceDir\Qt6Core.dll" />
+            </Component>
+            <Component Id="cmpD22BE3A3E3A804D9EC17161FD6ED218F" Directory="INSTALLDIR" Guid="{4119B512-1A33-4A9B-B0AA-69D22F8C9658}">
+                <File Id="fil0427F270C0F2EA2C38D6BB72F32BC982" KeyPath="yes" Source="SourceDir\Qt6Gui.dll" />
+            </Component>
+            <Component Id="cmp9FB603605D751CA9FE101013C48671B7" Directory="INSTALLDIR" Guid="{D15528CB-9F26-434A-AFBC-55A1BDD4372A}">
+                <File Id="filFE4FFAF140893FFCBD7F595F88958077" KeyPath="yes" Source="SourceDir\Qt6Network.dll" />
+            </Component>
+            <Component Id="cmp62FBAA57D2AA6EAC6A5C151902D999B3" Directory="INSTALLDIR" Guid="{638E44A5-87D1-4315-8AF6-F59A52EFA987}">
+                <File Id="fil720F59CECD8953F508A84670C46B640F" KeyPath="yes" Source="SourceDir\Qt6OpenGL.dll" />
+            </Component>
+            <Component Id="cmp6F8A9D8010540EBE393F23E015D5A792" Directory="INSTALLDIR" Guid="{EBE3A0EF-3C2E-4202-B9F7-AABADBF2E45F}">
+                <File Id="fil1192A9C8E95EF194DCF3EF7BE626B48E" KeyPath="yes" Source="SourceDir\Qt6Positioning.dll" />
+            </Component>
+            <Component Id="cmpB6D50DE19D6ADD781A08727F7F6266E8" Directory="INSTALLDIR" Guid="{9CE1CD64-108E-45B7-855E-E9A783CB19E3}">
+                <File Id="filCF6DBEF1D6A13C80715404D32B934AD5" KeyPath="yes" Source="SourceDir\Qt6Qml.dll" />
+            </Component>
+            <Component Id="cmp4E9BFD75DF26062E9937847116425997" Directory="INSTALLDIR" Guid="{3BA3D48C-4E32-4447-B837-0A9DF7AE9067}">
+                <File Id="filAE1BF7A9D9BF060761C9FAAB52C1582A" KeyPath="yes" Source="SourceDir\Qt6QmlLocalStorage.dll" />
+            </Component>
+            <Component Id="cmp65C2886373B0C96AD79BAD6B11F4BCEC" Directory="INSTALLDIR" Guid="{E0C4FC0C-4A28-460A-B092-FCB45FEAA648}">
+                <File Id="fil1A464A34B69E38E90B0748094F580925" KeyPath="yes" Source="SourceDir\Qt6QmlModels.dll" />
+            </Component>
+            <Component Id="cmp609AE9C23E17BA20AC06AEA3F3551DAE" Directory="INSTALLDIR" Guid="{B92EE4BB-8805-4317-BD8D-BF4C53D6306A}">
+                <File Id="fil70941CA86B91BA9374327AE51424DF95" KeyPath="yes" Source="SourceDir\Qt6QmlWorkerScript.dll" />
+            </Component>
+            <Component Id="cmpF560A3BA9C9935A03A5C37CBAFF8321F" Directory="INSTALLDIR" Guid="{CE2EC749-6224-409F-8E35-3ED8816465C9}">
+                <File Id="fil123F08A484F5DF5CC72BFCCDC197DBF9" KeyPath="yes" Source="SourceDir\Qt6QmlXmlListModel.dll" />
+            </Component>
+            <Component Id="cmpED65C4A3E125070BC17313D1A32F9C76" Directory="INSTALLDIR" Guid="{C8F815E9-AB86-4742-8D35-7354A635233F}">
+                <File Id="filFDA17D38CD6D73072EBAF9E430B8DCD4" KeyPath="yes" Source="SourceDir\Qt6Quick.dll" />
+            </Component>
+            <Component Id="cmpA463F9E3D9E20729888D635B58292550" Directory="INSTALLDIR" Guid="{2AD5E6DF-195C-432C-A40D-19196C3E57D2}">
+                <File Id="filDF57D75497458D7E64C07BB9AD1456C9" KeyPath="yes" Source="SourceDir\Qt6QuickControls2.dll" />
+            </Component>
+            <Component Id="cmp0571EF0C5FB8E314588A9065CF74AE35" Directory="INSTALLDIR" Guid="{6C00C0D4-3FF3-4CBC-B5CC-7D5116782A33}">
+                <File Id="filDC1C47A2ADA5BD76E86CE6CE8EC4F5B4" KeyPath="yes" Source="SourceDir\Qt6QuickControls2Impl.dll" />
+            </Component>
+            <Component Id="cmp2BB41ECBA565E0A0B4EDF020A49025F9" Directory="INSTALLDIR" Guid="{E2397D5D-10EE-49D5-B2BC-D25A5286321E}">
+                <File Id="fil57860EF8422A74A3CE88A88BAD0CABB7" KeyPath="yes" Source="SourceDir\Qt6QuickDialogs2.dll" />
+            </Component>
+            <Component Id="cmp73D256572698B6DB61D05CCD551BEF9A" Directory="INSTALLDIR" Guid="{2D33DF32-5940-4433-8273-C071C66C179E}">
+                <File Id="filC9FA5D0D485CF88CB948BE06870C3B8A" KeyPath="yes" Source="SourceDir\Qt6QuickDialogs2QuickImpl.dll" />
+            </Component>
+            <Component Id="cmpC26015BC765576311A63C5B026DB71FF" Directory="INSTALLDIR" Guid="{E0DFA170-69C3-41FB-8E1E-6F17DC027777}">
+                <File Id="fil2911EE010D604A656A176E72D572CCAC" KeyPath="yes" Source="SourceDir\Qt6QuickDialogs2Utils.dll" />
+            </Component>
+            <Component Id="cmp2ABC8A6031A24B9F3C70C18AE40B4CC3" Directory="INSTALLDIR" Guid="{07D87922-39D7-4F44-9E7B-F8ECBB11D86B}">
+                <File Id="fil0C6C0D67878BFB0A4BD30E472A40969B" KeyPath="yes" Source="SourceDir\Qt6QuickEffects.dll" />
+            </Component>
+            <Component Id="cmpD246CE6E593DB826C58A42EFAB8AC176" Directory="INSTALLDIR" Guid="{55D69872-A85E-43F6-912A-BF1E493C8928}">
+                <File Id="fil89BECDEB84C26F37AC111B6D300D2835" KeyPath="yes" Source="SourceDir\Qt6QuickLayouts.dll" />
+            </Component>
+            <Component Id="cmp05C682DFA26731F5F96864E240BB9367" Directory="INSTALLDIR" Guid="{9D8C8DAF-F52F-4FC3-A765-94117661FF1F}">
+                <File Id="filF00A542F2EB321619F0825C65334A7F5" KeyPath="yes" Source="SourceDir\Qt6QuickParticles.dll" />
+            </Component>
+            <Component Id="cmpB2448C4481D00046098304AC1C230B93" Directory="INSTALLDIR" Guid="{78810566-5226-4058-AFB5-478189D94251}">
+                <File Id="fil359D410486F61CCCDFEFE2410735AC75" KeyPath="yes" Source="SourceDir\Qt6QuickShapes.dll" />
+            </Component>
+            <Component Id="cmpD3B7B317B2EAD7974F7386A4F4137733" Directory="INSTALLDIR" Guid="{29B8D38E-CA38-4E5F-A2FF-3D39E7DCC3C0}">
+                <File Id="filA1B7D830C14FF020BDC9FAF8F83402A1" KeyPath="yes" Source="SourceDir\Qt6QuickTemplates2.dll" />
+            </Component>
+            <Component Id="cmpD5FD28F099F28B38A8C5C6C43481E545" Directory="INSTALLDIR" Guid="{46866E68-D822-498F-B97A-F172CB5B3E0D}">
+                <File Id="fil1A12F96CDBED6EDBD83A1228E643DFE1" KeyPath="yes" Source="SourceDir\Qt6ShaderTools.dll" />
+            </Component>
+            <Component Id="cmpDAA5AE12CA74181FE29953002CC830A3" Directory="INSTALLDIR" Guid="{2C9383C2-EBF6-4FFC-B3F5-A014EACC05C7}">
+                <File Id="filEF75955C67A60E62CD86B26117C750AE" KeyPath="yes" Source="SourceDir\Qt6Sql.dll" />
+            </Component>
+            <Component Id="cmp2BFDE38E15CC8879E8B911A7A296477A" Directory="INSTALLDIR" Guid="{5B1DDDD7-72B7-4804-B939-BD805721BF9E}">
+                <File Id="fil08B70D0B09545B2C830255009EF93341" KeyPath="yes" Source="SourceDir\Qt6Svg.dll" />
+            </Component>
+            <Component Id="cmp501A53153FAB3908951DADAE039AB865" Directory="INSTALLDIR" Guid="{F754EC1C-C49A-4BB5-B1F6-44F39D697964}">
+                <File Id="filDAAFC08AB32ED0F61D6C56EA5B665627" KeyPath="yes" Source="SourceDir\Qt6WebChannel.dll" />
+            </Component>
+            <Component Id="cmp5046A1A989E72DACE21F8D481D0D71C5" Directory="INSTALLDIR" Guid="{63DC7AE7-1E3D-403D-9172-777C8F2185B9}">
+                <File Id="fil4DF01BF67F8DD9C408204A2F5BD56FF9" KeyPath="yes" Source="SourceDir\Qt6WebEngineCore.dll" />
+            </Component>
+            <Component Id="cmp8015426E62DE3ED33343DB50BE0A9A0D" Directory="INSTALLDIR" Guid="{F4778D74-570E-4173-912A-CEECCC885B65}">
+                <File Id="fil58988C3D49189FE2287F68174F6944A2" KeyPath="yes" Source="SourceDir\Qt6WebEngineQuick.dll" />
+            </Component>
+            <Component Id="cmp268C15DC40B82F983C534C9C3DAABD1B" Directory="INSTALLDIR" Guid="{1B8AC40A-AC0C-41C5-B5A8-D7C52FF8C206}">
+                <File Id="filA069DC2EC3525A8E547CF9EFC89A25E6" KeyPath="yes" Source="SourceDir\Qt6WebEngineQuickDelegatesQml.dll" />
+            </Component>
+            <Component Id="cmp7B3BE582AD25B07FE55DD249E6D5C333" Directory="INSTALLDIR" Guid="{2D8AA939-E8CB-4EFE-8C38-2D8D6BF0E65C}">
+                <File Id="filF8FC62A4978258AB67C456CC256AAA8B" KeyPath="yes" Source="SourceDir\Qt6WebSockets.dll" />
+            </Component>
+            <Component Id="cmpA72EB9BC77F4E13CA8F69B2B9413199C" Directory="INSTALLDIR" Guid="{2229E93A-8BC6-41F5-BE8E-1C23B56695B9}">
+                <File Id="fil84A612B35A3B234B8729B7998FD0742D" KeyPath="yes" Source="SourceDir\Qt6WebView.dll" />
+            </Component>
+            <Component Id="cmp9CC20E8457A681268437C2AC7D562FE2" Directory="INSTALLDIR" Guid="{5FA68071-6C16-43EC-9531-47C130EDB186}">
+                <File Id="filA8C33CBAC2EC4965ED511DF205E7961F" KeyPath="yes" Source="SourceDir\Qt6WebViewQuick.dll" />
+            </Component>
+            <Component Id="cmpBCCA6FD4EC03D708E3811B7D4723801F" Directory="INSTALLDIR" Guid="{7DF3B16D-52F5-46BB-A9AB-8756ED2C7014}">
+                <File Id="filAC348090A125BCB41DB84097A13158A0" KeyPath="yes" Source="SourceDir\Qt6Widgets.dll" />
+            </Component>
+            <Component Id="cmp648D7B3CDDA6427D7237CE400D68C36A" Directory="INSTALLDIR" Guid="{D6E813F8-FD27-48E7-BB04-6A065B38DC96}">
+                <File Id="fil71DF3511D06EBF7BFB8342792898BEE4" KeyPath="yes" Source="SourceDir\QtWebEngineProcess.exe" />
+            </Component>
+            <Component Id="cmpE18AE4F92C6862587708BF72BAD1A16B" Directory="dir5986BBEEE82C5D978676757AB9BBF16F" Guid="{329172C9-F3DC-44B3-BDC3-1D79829F37CA}">
+                <File Id="fil0AE27F0E2C805CBF8F3E16544E661FA8" KeyPath="yes" Source="SourceDir\generic\qtuiotouchplugin.dll" />
+            </Component>
+            <Component Id="cmpE3D572FB13B4249EA0FB18BD0F44DE18" Directory="dirB10C059F651528B59E1FD268EB08C6D6" Guid="{B7820FB2-5FBA-4761-B35B-113EB413AB63}">
+                <File Id="fil7320C9B04E87091E801ED591E25C7F1E" KeyPath="yes" Source="SourceDir\iconengines\qsvgicon.dll" />
+            </Component>
+            <Component Id="cmp5D66BEFD65BDC9AC7D48118D254EBF30" Directory="dir20189C824D4DDABC3D9FF9449C089CE0" Guid="{82C49CF0-2BCB-41B3-9F9B-8434E59F866C}">
+                <File Id="fil167B858704F9A353B556ECC7639B590E" KeyPath="yes" Source="SourceDir\imageformats\qgif.dll" />
+            </Component>
+            <Component Id="cmp2A9FEB410F41B453B0CA26766D4AD20C" Directory="dir20189C824D4DDABC3D9FF9449C089CE0" Guid="{92936E1E-6A17-4B46-8427-423FDFF2FA6E}">
+                <File Id="filB7BCDEF234107021D6B84547174B24BC" KeyPath="yes" Source="SourceDir\imageformats\qico.dll" />
+            </Component>
+            <Component Id="cmp71D8FCDDEED93F03008649285B372ADA" Directory="dir20189C824D4DDABC3D9FF9449C089CE0" Guid="{9F77CC5D-33B3-4BA5-9BBF-F212CB051C56}">
+                <File Id="fil22403FED9C4013C9328A99701DEA8873" KeyPath="yes" Source="SourceDir\imageformats\qjpeg.dll" />
+            </Component>
+            <Component Id="cmpC07A85EAE90864EB28FD65896AE228D2" Directory="dir20189C824D4DDABC3D9FF9449C089CE0" Guid="{F49D9D5C-5294-42E7-8B0E-EDB8DB957741}">
+                <File Id="fil310D7F5AA577821D98D425950E96E344" KeyPath="yes" Source="SourceDir\imageformats\qsvg.dll" />
+            </Component>
+            <Component Id="cmpC3E9129E6B641C851B841BF3E6748387" Directory="dir690B8E6D609B0DB17DB71E8B1FB2FEC5" Guid="{85000A12-080F-4468-889A-2B35F52AD755}">
+                <File Id="fil853664287ADF8100B5B8E8FB61D63CB0" KeyPath="yes" Source="SourceDir\LICENSES\AVC.txt" />
+            </Component>
+            <Component Id="cmpB46D0962D2340404F851F10CAAECA549" Directory="dir690B8E6D609B0DB17DB71E8B1FB2FEC5" Guid="{3477D6DD-3CEA-4AC0-A526-0546C5A9971F}">
+                <File Id="filFCC7617B3CC611B027C8B3FB6DE6EA20" KeyPath="yes" Source="SourceDir\LICENSES\GPL-3.0.txt" />
+            </Component>
+            <Component Id="cmp65AA9556A3F4786F01B316E5DEA9E6F6" Directory="dir690B8E6D609B0DB17DB71E8B1FB2FEC5" Guid="{F11EAAA8-623C-4DA6-9C2A-D84964A47BE0}">
+                <File Id="filC36144D44D8E4AE0EFCD137DD2752E9D" KeyPath="yes" Source="SourceDir\LICENSES\LGPL-3.0-only.txt" />
+            </Component>
+            <Component Id="cmp727893E2EF55BD50F18FDD0ECF2C824F" Directory="dir690B8E6D609B0DB17DB71E8B1FB2FEC5" Guid="{A4D1E23C-ECA1-46D5-B695-300B878F379B}">
+                <File Id="fil52FC0319C49E6B01FF651770DA57735F" KeyPath="yes" Source="SourceDir\LICENSES\MIT.txt" />
+            </Component>
+            <Component Id="cmp907AF1F6FA03E2DE91C9FEA025010D55" Directory="dir4C08960BD61EEE75AE4A87FC65A5DE2A" Guid="{71C450B7-F43F-4E17-8694-23705AC67046}">
+                <File Id="filD108AA20F08DED7C4D3797E896B5D090" KeyPath="yes" Source="SourceDir\networkinformation\qnetworklistmanager.dll" />
+            </Component>
+            <Component Id="cmp830C378ADEFF439CEF6C0F2550FD4D2F" Directory="dir826692792B0995C8DF68919F5C684331" Guid="{20794704-18E0-49BE-BBD5-15EB1ED6249E}">
+                <File Id="filF7B01E0AFAD9057CB024B85137A38049" KeyPath="yes" Source="SourceDir\platforms\qwindows.dll" />
+            </Component>
+            <Component Id="cmp768C303F890F86F0F1F870D66E04EF1D" Directory="dirC15EA267FAD4FC447E84FB3A32CBDB68" Guid="{A3F20235-2CFF-419E-A9B2-AB4F385AFF27}">
+                <File Id="fil9E350077F6BEF422DA69CB20F14812C9" KeyPath="yes" Source="SourceDir\position\qtposition_nmea.dll" />
+            </Component>
+            <Component Id="cmp7C2CB506AB0D7E9648A5F2A7C2C7770A" Directory="dirC15EA267FAD4FC447E84FB3A32CBDB68" Guid="{7A098C8A-30B2-4DAB-AD6E-CD4BE5DADA72}">
+                <File Id="filC58409E068EE2AD6FE8C6C57174D6DDD" KeyPath="yes" Source="SourceDir\position\qtposition_positionpoll.dll" />
+            </Component>
+            <Component Id="cmp8539D5D6C3427F1F4C82034569A0F132" Directory="dirC15EA267FAD4FC447E84FB3A32CBDB68" Guid="{43FEA413-3848-49B8-848E-15455D1C4803}">
+                <File Id="filEF2EB40CA1790B7BDC4320E6F7638744" KeyPath="yes" Source="SourceDir\position\qtposition_winrt.dll" />
+            </Component>
+            <Component Id="cmp0BF1B39B0DF9604C417C59F0C26849DB" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{81CFDDE4-236D-4CD7-B112-3FBD76350D12}">
+                <File Id="fil886A06C8346FCEDB309427F45C47C7A2" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\Blend.qml" />
+            </Component>
+            <Component Id="cmpA4F5F7FBAA6092D9B23D7A86DA914EEF" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{02F08DC3-3D69-484F-8A45-CA228C163AF2}">
+                <File Id="filAA448C1B459D0720F374A6B1B96F738F" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\BrightnessContrast.qml" />
+            </Component>
+            <Component Id="cmp0279BCBDD30B4C6C44CF17FF2C57554F" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{656A1394-3CFA-407D-9532-25427C2E4F29}">
+                <File Id="fil131C97BF2FB605E8AF69F25A5F2B03F1" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\Colorize.qml" />
+            </Component>
+            <Component Id="cmp4F124E7E9DFC88F42758D7A8DDC4FEDE" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{AE411C51-0343-47F0-B100-0E5F107F146C}">
+                <File Id="filD1580F6EE199C19C089B8A65E29A49D6" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\ColorOverlay.qml" />
+            </Component>
+            <Component Id="cmp400F9069DE382BAC36209FCBF66A9459" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{DC582176-A3AA-4215-B9CB-3C88A96F5350}">
+                <File Id="filFBACB681C118E8C1F036D2CCCF4CE943" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\ConicalGradient.qml" />
+            </Component>
+            <Component Id="cmpCDDF8FFEE802C8865EBC55B7B0DBB6CE" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{8A3849AA-F444-475B-B546-6284FFE75A82}">
+                <File Id="filF35B45A1A450B0549BAF81E355E40B11" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\Desaturate.qml" />
+            </Component>
+            <Component Id="cmp79AA1A0363899DA7492F66E48C9C48A4" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{D21B602B-96F1-45EA-86C6-1FD836F7AC2C}">
+                <File Id="filA083A38F1B9CE852DEB1263C029CE98B" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\DirectionalBlur.qml" />
+            </Component>
+            <Component Id="cmpA7F203332ABBC3EC7A402FF07AF196CC" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{CA15ACAF-1472-4D12-9837-A5A1F4101D6C}">
+                <File Id="filE6AD0D99E1AA14FE51D00A5C09C717CE" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\Displace.qml" />
+            </Component>
+            <Component Id="cmpAC6C7513DACC293DBE22003E115E1829" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{0E2F94AC-C333-472C-9F1E-57EEFC09FD49}">
+                <File Id="fil808763D4BBA317BC8CC96AFB715EAA5C" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\DropShadow.qml" />
+            </Component>
+            <Component Id="cmp829A7D43222A942FAAC1E34390AB20C7" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{FFD808D8-D0B2-4983-9F62-94F99684E759}">
+                <File Id="fil6C2B7CCE2D3FD30A6C5E831858FC36ED" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\FastBlur.qml" />
+            </Component>
+            <Component Id="cmp1B75439E18656D4971DD74295C5A6FE9" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{F61BD65A-E5CB-47A0-830B-F0CD168C45B2}">
+                <File Id="fil8ECC49187862A16FCFF10E28310443B9" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\GammaAdjust.qml" />
+            </Component>
+            <Component Id="cmp5F5DA8B0DE474072A479802CCFAAFECD" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{00B63D4D-81CA-4795-B1F9-1494FA2CE788}">
+                <File Id="fil430108EEBBEAA2DDEB424F77B9FB9B2A" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\GaussianBlur.qml" />
+            </Component>
+            <Component Id="cmpA47F3D27B19B18EF22E22C62858B5E5D" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{258EC385-3720-4611-B683-30112346FA45}">
+                <File Id="filFA73A8C700D83E467CC41CCFCFE1D705" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\Glow.qml" />
+            </Component>
+            <Component Id="cmpD78893AC3CDF13AB163A32E432380213" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{3CF2A0F9-824E-4C28-9F69-02A986CE99FB}">
+                <File Id="fil4A2470F1F3421C2F1A6C3C5A949C89C2" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\HueSaturation.qml" />
+            </Component>
+            <Component Id="cmp0167F714462880F7DEA8F99E51540614" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{4D411B24-2F65-4A70-BEFD-C78E3219D5D7}">
+                <File Id="filC330E4696CEFD1D7DB358351D7891F8C" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\InnerShadow.qml" />
+            </Component>
+            <Component Id="cmp306CB3F03C173E5F7C4C5E879CA004BA" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{D1161DCB-3344-4F4C-B0F5-A6AD90C1D999}">
+                <File Id="fil965E7EC2C181D00DA21047E30607CA5A" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\LevelAdjust.qml" />
+            </Component>
+            <Component Id="cmpD2D5DEE53569513FCE60FA9329F19C10" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{7DA0AB61-D0E8-45F6-96E4-A76A3A668C4A}">
+                <File Id="fil05A53503FC92D8E4D1AEAB21BA8744E1" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\LinearGradient.qml" />
+            </Component>
+            <Component Id="cmp51661D56CE8F6159EC2EF6F5C4423E2A" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{7F8ABA12-5AF6-430D-85CF-B2A1CA18DC91}">
+                <File Id="fil8D1ED956BFFB0E6AA4B8250788A62613" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\MaskedBlur.qml" />
+            </Component>
+            <Component Id="cmp964D238CB7CF2027CB6B00D34C3E22D3" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{4A9B2E63-95F0-41C6-9873-598C1D70D773}">
+                <File Id="fil2000425A1E4C194D33EDDCECA027DFFA" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\OpacityMask.qml" />
+            </Component>
+            <Component Id="cmp74C1EF4971B64332C02FE2C556E19729" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{031BD042-70E0-400A-85F9-51D2639F57EA}">
+                <File Id="fil7EF6C79CC3325C12E745025BCE0C9FBE" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp242486FD4A56A0063C7DE0D89F6E290A" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{D7D4E4C2-1CE9-4D53-85C7-07D5002AC1BD}">
+                <File Id="fil30F83679D1E91454CD2098287A07D8CF" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\qmldir" />
+            </Component>
+            <Component Id="cmpC69C56E9C15ED4985A3B3E94CFE682FE" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{2C998533-744F-41AD-A192-B6363B09851F}">
+                <File Id="fil8A82A66E4A720F6A76D85BD109FDB6D4" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\qtgraphicaleffectsplugin.dll" />
+            </Component>
+            <Component Id="cmp9BF26A49C09D0550F21706EAF6A2DE75" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{156E2246-A6B5-4F95-84E7-427E2B6CAD1C}">
+                <File Id="filC4A9C657EEB31EB6D21EE9A2F85FBBC1" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\RadialBlur.qml" />
+            </Component>
+            <Component Id="cmpC630A8D81781F2B1FB39973C4D294FEE" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{717CFBFD-CC47-4665-847A-30D1F257CF67}">
+                <File Id="fil350FD0954A2C72003C274E1CF5910255" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\RadialGradient.qml" />
+            </Component>
+            <Component Id="cmpDE47D419CE84778D35D367DB62B29035" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{3C43C7E4-880F-4EA3-BE81-85984C9E712B}">
+                <File Id="fil66FA38DBB7F8A5EFADC7DF07FF481F27" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\RectangularGlow.qml" />
+            </Component>
+            <Component Id="cmpD553D921E6FBBEF3C45277226BE9A664" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{55E870F3-B8BE-4357-A0E3-D00DC03BA405}">
+                <File Id="filB94184A4AAB2C694E4E2F9BFA5C7CB2F" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\RecursiveBlur.qml" />
+            </Component>
+            <Component Id="cmp3810D3211D6B3BAD793BC0DAE002A83E" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{3486E477-1BD7-4564-AEC4-7C8544BC7618}">
+                <File Id="fil5265371F3D4E09D4A5E7E08CB48C1E34" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\ThresholdMask.qml" />
+            </Component>
+            <Component Id="cmpBFBC007CFA4A04FBA6F2BD656FA19C10" Directory="dir2FA83874D84EB850CB9EC2829B8A3243" Guid="{CCBB3C6C-33E2-45D6-9E21-8C39DB413DD0}">
+                <File Id="filFBFB37751870916C3295B03320D7F433" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\ZoomBlur.qml" />
+            </Component>
+            <Component Id="cmpCE58CC32A131543BE1958FE0BAD85A10" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="{8CE47AD3-480E-4A76-A3FC-D4E7FFC3E64C}">
+                <File Id="filD779BE04A0038E36398257980C7C15C2" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\DropShadowBase.qml" />
+            </Component>
+            <Component Id="cmp934F9881CA679A03ADEB025F6FF93CD4" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="{937FF69E-8957-463A-9BB9-6253FE8EDF60}">
+                <File Id="fil4651AFBE560B3D1E75CB4E929421A23B" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\FastGlow.qml" />
+            </Component>
+            <Component Id="cmpB878398DBEBC014BD96843EA6A95344B" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="{6952E6B5-81BA-455E-A59E-6C51B52924C8}">
+                <File Id="fil3DEBA52D9592EB84679B648C2AA2B6AC" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\FastInnerShadow.qml" />
+            </Component>
+            <Component Id="cmp7A07163838464B60F6D2537CA7CE8833" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="{1A9B5889-357B-4092-B2C6-CF8FA0342AFD}">
+                <File Id="filE5392E34CA263822C222D0BF9639227C" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\GaussianDirectionalBlur.qml" />
+            </Component>
+            <Component Id="cmp7D423B9F37902140337965C759A1E539" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="{C4C99C61-81F2-44F5-AA2C-59D781B798DD}">
+                <File Id="fil254C6297F09891595F6710F3797AD065" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\GaussianGlow.qml" />
+            </Component>
+            <Component Id="cmpFF2B9D0ECD8FE0E31F0959E9C48B18E4" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="{CE7966A8-9519-420B-9037-34FE730263FB}">
+                <File Id="fil481983D43BC51BBBD60DDA436F0D66A5" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\GaussianInnerShadow.qml" />
+            </Component>
+            <Component Id="cmp694B7AB632639A0A5938C277A7B201EA" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="{8DD2EEF2-4D1E-445B-8D66-35F8E7196E49}">
+                <File Id="fil6FC8836A55A46A87A0DB0296C533A676" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\GaussianMaskedBlur.qml" />
+            </Component>
+            <Component Id="cmp8F06226703AF483B2638C63BFB9B19F8" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="{98E58DBF-8CA4-433C-A9D9-CF92C59B361E}">
+                <File Id="fil4D90ECF583474B1CB59DAEBD2052BA48" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp7362962F640F2FD7E74F1C095DBDAF30" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="{35D16DCE-D763-4E90-B832-CBACA3391DF4}">
+                <File Id="fil11A9BFD23D2CD176E57F5EED8210F5EC" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\qmldir" />
+            </Component>
+            <Component Id="cmp2AE301345F7F9CB39B6986A6FE1B7770" Directory="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Guid="{22CD8C88-8C2C-4F66-B372-1A116136F155}">
+                <File Id="filFEA2206E424A82D72528F1EA358D0B69" KeyPath="yes" Source="SourceDir\qml\Qt5Compat\GraphicalEffects\private\qtgraphicaleffectsprivateplugin.dll" />
+            </Component>
+            <Component Id="cmp8EABE0B8AEB6B59BBA116FC21CB7E5F2" Directory="dir92990262DE0ED5508C44523003A4B360" Guid="{64FE7562-0E53-48AD-99D2-F08C9806A91E}">
+                <File Id="fil0117CF5445EF1792DFCE8ED7540F81CA" KeyPath="yes" Source="SourceDir\qml\QtQml\qmldir" />
+            </Component>
+            <Component Id="cmp0C776837FB1ACA4E4665746624A1F66D" Directory="dir92990262DE0ED5508C44523003A4B360" Guid="{07E220E4-5AAC-4B0F-9A92-169B39003F13}">
+                <File Id="fil4003B93F14AAC88E53C18C94ACEB86B8" KeyPath="yes" Source="SourceDir\qml\QtQml\qmlmetaplugin.dll" />
+            </Component>
+            <Component Id="cmp276C50F9766B97EB467160CC10A6A356" Directory="dirD3E9E07666E50DD0B119271684422637" Guid="{78195003-8786-4500-BD14-58C73367C2A0}">
+                <File Id="fil0F05D62E45DA4E9EFA2FBFC0D3553F36" KeyPath="yes" Source="SourceDir\qml\QtQml\Base\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp517B71582230A4698CBF9E3F6048550D" Directory="dirD3E9E07666E50DD0B119271684422637" Guid="{3DF89265-E7F6-4660-B9A0-F90F723076C3}">
+                <File Id="filEEB49B8A5C313E798DCB3FC5DE04D86F" KeyPath="yes" Source="SourceDir\qml\QtQml\Base\qmldir" />
+            </Component>
+            <Component Id="cmp870C95E4B9F919D6A3987C4F468D8305" Directory="dirD3E9E07666E50DD0B119271684422637" Guid="{3DB97C83-8281-4A7F-8EF2-5FB214AC4AE2}">
+                <File Id="filEED866F0018D6A66CAAAC4D5E08D6EAA" KeyPath="yes" Source="SourceDir\qml\QtQml\Base\qmlplugin.dll" />
+            </Component>
+            <Component Id="cmpA1F1D19580CB3E153905BE6B71792592" Directory="dirAA6B0932D46A7D6E35A8D1E06E10B2B7" Guid="{B7C9CD4F-5D9B-4EEF-8D24-64CC6416102F}">
+                <File Id="filF67E33CF8E5416727B865F3F9EA9A8E7" KeyPath="yes" Source="SourceDir\qml\QtQml\Models\modelsplugin.dll" />
+            </Component>
+            <Component Id="cmp5000F310EFD82E6287E9903FD51B885C" Directory="dirAA6B0932D46A7D6E35A8D1E06E10B2B7" Guid="{11DBB6A3-F03F-41AE-A766-5536881436D3}">
+                <File Id="fil2D905E3A0B5933A0F7375843912EED30" KeyPath="yes" Source="SourceDir\qml\QtQml\Models\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp0175AC12A4D0739783808D434C36AFFD" Directory="dirAA6B0932D46A7D6E35A8D1E06E10B2B7" Guid="{B358035C-2BCB-444C-8EAE-E3A288BBFC51}">
+                <File Id="fil5737825369666B91605A34E0CA2760DB" KeyPath="yes" Source="SourceDir\qml\QtQml\Models\qmldir" />
+            </Component>
+            <Component Id="cmp9DE654388B48EFE9A6FA6FDF28BAD73F" Directory="dir92A51CED876E1F4286E6663FD7D54CDA" Guid="{8EFFAB1C-A1D1-415A-A843-87AFCA87BE68}">
+                <File Id="fil29D45524912C32AD2DBC7786CAB9C815" KeyPath="yes" Source="SourceDir\qml\QtQml\WorkerScript\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp133E0981CE8D29649AE8FB1932031C2C" Directory="dir92A51CED876E1F4286E6663FD7D54CDA" Guid="{A715AB26-63C0-4701-BB9E-A175154C8243}">
+                <File Id="filB1F22C93A956BB2A8EE2FD2B01308963" KeyPath="yes" Source="SourceDir\qml\QtQml\WorkerScript\qmldir" />
+            </Component>
+            <Component Id="cmp08D9C9933EFB91871225B53380202A87" Directory="dir92A51CED876E1F4286E6663FD7D54CDA" Guid="{25C02117-21FA-4FC2-A60E-B6B7F03ED4E5}">
+                <File Id="filAEEE5B727783E6621123551D42A7F31B" KeyPath="yes" Source="SourceDir\qml\QtQml\WorkerScript\workerscriptplugin.dll" />
+            </Component>
+            <Component Id="cmp30CA6BFC8F040C1F22D888FD79DC9B1B" Directory="dirD48123A7B6E2CFB660EC94F0CBE0276D" Guid="{78AAC9A0-AE99-4551-A4AB-9B367F2F8AE0}">
+                <File Id="fil49ED9A9A100F4B7848AF9A02B69490F3" KeyPath="yes" Source="SourceDir\qml\QtQml\XmlListModel\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp50357C4AABC622F51AD737AB1256FE31" Directory="dirD48123A7B6E2CFB660EC94F0CBE0276D" Guid="{CD9B6DCA-A162-4249-8298-A33A4FCEDB26}">
+                <File Id="filFBD23B1E55B401CA6C9778F361D5EC66" KeyPath="yes" Source="SourceDir\qml\QtQml\XmlListModel\qmldir" />
+            </Component>
+            <Component Id="cmp86A326EFCF0E1704C41D4A3BE3B2B50C" Directory="dirD48123A7B6E2CFB660EC94F0CBE0276D" Guid="{E022B1AC-7D09-4E7A-9EC2-23CAD69FFF41}">
+                <File Id="fil52EA3E4F82010DABF3A854BA9390EEAC" KeyPath="yes" Source="SourceDir\qml\QtQml\XmlListModel\qmlxmllistmodelplugin.dll" />
+            </Component>
+            <Component Id="cmpE4E1A33E38557FB60B3F45555FD20ECE" Directory="dir4B7D2A19DD0D33E235AA7BD43615A87E" Guid="{A5A470B8-018F-4306-90E3-FE22FFFCFA15}">
+                <File Id="fil1C0EEC7BC400302C41630BA92C1C307F" KeyPath="yes" Source="SourceDir\qml\QtQuick\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp1ED74D34F649FA02DC949AD36738A154" Directory="dir4B7D2A19DD0D33E235AA7BD43615A87E" Guid="{5C7B5693-E219-44EF-987F-1555955516F0}">
+                <File Id="fil7CD6FFB1D436DB58018DC2DB86CBA0D2" KeyPath="yes" Source="SourceDir\qml\QtQuick\qmldir" />
+            </Component>
+            <Component Id="cmp4DB15011F21E3B03E2E0F0F9D4BB573D" Directory="dir4B7D2A19DD0D33E235AA7BD43615A87E" Guid="{870676C3-5BAF-41D2-BE85-95D1E50EC3C2}">
+                <File Id="filBC92394F04FDB0CE1F9CF582A4F84C36" KeyPath="yes" Source="SourceDir\qml\QtQuick\qtquick2plugin.dll" />
+            </Component>
+            <Component Id="cmp4DCBBC4A26D0C4FB3623DD878E02CFCE" Directory="dir175FB1A22F883092D2FC39E138CDC5FC" Guid="{DE8E1B57-AAEC-430C-AF0B-84172D7083A7}">
+                <File Id="fil59DCF0C6D0B4BBBA10A36EB12B34D2A5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpE325B44584CD20BCE520071161F80F06" Directory="dir175FB1A22F883092D2FC39E138CDC5FC" Guid="{BB4A3956-9561-4061-9915-67A978C15071}">
+                <File Id="fil6115FDE68D8D8FF3AFCDD9E24BDB4562" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\qmldir" />
+            </Component>
+            <Component Id="cmp20C01608F59E2EEB9AB45A6904212E9A" Directory="dir175FB1A22F883092D2FC39E138CDC5FC" Guid="{944B25EC-B074-446D-9E08-CF6CE92907B2}">
+                <File Id="filEA309A6F3A745EE08925BE2C9654289D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\qtquickcontrols2plugin.dll" />
+            </Component>
+            <Component Id="cmp8C93A33CFCA426C5180507A0B98BEBF0" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{E85E69E3-B911-4019-ADDC-C99EEB279343}">
+                <File Id="filBC369AE402DD38E53E6923350FDF5653" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\AbstractButton.qml" />
+            </Component>
+            <Component Id="cmp8A493B71973648EC2B057CA3374E1EE7" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{1373656C-E772-4517-AC0A-8EE7B5C842FA}">
+                <File Id="filAFDBFFDF61F1880A7BBC4EF9A8831880" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Action.qml" />
+            </Component>
+            <Component Id="cmp18CDA922328CA34B0C584CBCA1FF9A07" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{DD875457-1EEB-4FE6-B09B-CD4BE798CCFB}">
+                <File Id="filF9E61D4DD55ADA9EF9D5FCF3C9742A6D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ActionGroup.qml" />
+            </Component>
+            <Component Id="cmp6A676B107BB40104A39E3A1FC9920DC5" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{5602BD8A-A3B8-4422-AF38-AE6FA5D51C86}">
+                <File Id="fil9ED46CC79E64F252F39C9F5D48BA97F2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmpC4574DD49EE73BBD7C9D46D19CABCE00" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{388391C1-6E4B-4D1A-9190-6774FD5B6791}">
+                <File Id="fil428F79F811C7EFFFF5EFD5D1C385E603" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\BusyIndicator.qml" />
+            </Component>
+            <Component Id="cmp5E103D9543FED51F3FEC441D27E30CFD" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{C154BB8A-1182-41F5-B2FA-2E693E4AD401}">
+                <File Id="filED6BC08EAA7BDC1922E5EF3FA89593F5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Button.qml" />
+            </Component>
+            <Component Id="cmp586B84D1D2ED76699E38F3C103B00E34" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{B42255D8-A6EE-4422-AC90-853FB295DE53}">
+                <File Id="fil63B2A244080B179569FD25B2AF2B8541" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ButtonGroup.qml" />
+            </Component>
+            <Component Id="cmpA98BD23094F94C354CF3233853D1FDEF" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{E68A7204-07CD-46D9-B047-9CFDE5CF405A}">
+                <File Id="filEE471E4AE1024C841C83A1939CBDA962" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Calendar.qml" />
+            </Component>
+            <Component Id="cmp6F97518E97DEEDE1E4CF4A892F43D8F3" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{34DA65DC-7492-4B8D-9882-9C97F96FD254}">
+                <File Id="filA9466F387D591B348EBE6EF066AD3CBF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\CalendarModel.qml" />
+            </Component>
+            <Component Id="cmp8D6C1B322664DB792ADAD70140F12A7B" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{5274CAF1-43E9-4B44-94D6-DBDE5C6F1A54}">
+                <File Id="fil86E157C28F2862D0ABF4871B3EA0E1F9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\CheckBox.qml" />
+            </Component>
+            <Component Id="cmpA42169CDD08727E08118CB82A57B045D" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{55D7C567-0D04-498B-A8C7-A179A76F261A}">
+                <File Id="fil8639CCFCC7900B80AAD254F318DE147B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\CheckDelegate.qml" />
+            </Component>
+            <Component Id="cmp939FB89E3AE89DB11ADDE1E4DF7ED3BD" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{28223D93-9CEE-4E71-9AB8-D6746E8A784C}">
+                <File Id="filFAC4CA844ADE51BD382B91DB80B0B1B5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ComboBox.qml" />
+            </Component>
+            <Component Id="cmpC1F61C19635F2E06D9A1CE39041C8532" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{A57E5AC3-6ABA-4A64-8D84-03BD407F5A56}">
+                <File Id="filDD413399B6656F652D996E2902E9EFC3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Container.qml" />
+            </Component>
+            <Component Id="cmp7884BA20ED773D52ECA78F771554B413" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{C4FC443B-C143-4690-B32C-5CFFD3DCF54F}">
+                <File Id="fil78BC91EA0996852B81A2216964214BC9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Control.qml" />
+            </Component>
+            <Component Id="cmpBAD59A78213E776132B2DCB9B916AB52" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{DE3F6C7F-944D-4507-ABDF-B1E872EB149F}">
+                <File Id="fil89ED420BA26EAEE076934735A91F5C7A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\DayOfWeekRow.qml" />
+            </Component>
+            <Component Id="cmp6043FBD10B6CD12C84234368659A1E5E" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{E6FD23C8-3D5B-4221-B86C-46E61CA9B5E2}">
+                <File Id="fil57496F5B57AE6062BBC6398A651E7B29" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\DelayButton.qml" />
+            </Component>
+            <Component Id="cmp39E2417580498483F716655E718C8AEF" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{4C1B5B02-7116-40FA-9F8E-1CF95EAFF4BB}">
+                <File Id="fil7B56B2CD7FAD68FC93729CB091088B95" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Dial.qml" />
+            </Component>
+            <Component Id="cmp8230F1ECB5AF410493C4F0F5DDCCFA6F" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{31BB72B7-DD10-4FFB-B041-F807456EA556}">
+                <File Id="fil95C9D3E07FA41700EE546DA8D0F58A34" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Dialog.qml" />
+            </Component>
+            <Component Id="cmp1B8E24691E5DE807D238D41F1383C726" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{BB5DCA3A-5D15-4305-8827-E5E980B5651F}">
+                <File Id="fil9461883C6FCD2F2B63E7DF897AB598F1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\DialogButtonBox.qml" />
+            </Component>
+            <Component Id="cmpAE8BB0CE07CF6799D67E194CE71F3B49" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{0F594C7C-C76B-4A12-A966-10DB5CF4DCF3}">
+                <File Id="filD9F7FECF0A814191B777A7DD1F8226CF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Drawer.qml" />
+            </Component>
+            <Component Id="cmp58BE65189C75757B4B1AE73EE8BB9ED4" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{151B6FEA-71E7-4E43-AE8D-665BC7AE8C72}">
+                <File Id="filCA18B4E8037B6D504FE019B1420FC262" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Frame.qml" />
+            </Component>
+            <Component Id="cmpAB65A995029614C0BEAE885EAC615969" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{474A0215-837B-426D-9AC8-5814CF28D2BC}">
+                <File Id="filCE960895937BB21868B32B21265D5549" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\GroupBox.qml" />
+            </Component>
+            <Component Id="cmp53BB29730C916C703FEB130DE6C869A3" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{76A762CF-0DB1-4CE1-AD73-4150DD1905AF}">
+                <File Id="fil6ADB14CFBBA72C53EBBCB499437D0768" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\HorizontalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp9F2A20383C3A799374E705196D5FC47B" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{85289423-4309-4B0C-A993-124F04427D0F}">
+                <File Id="fil1381FCD6D7340BA1D8F7061E24E9EE5F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ItemDelegate.qml" />
+            </Component>
+            <Component Id="cmpD26A15138E0B093D70CA9ED2F43A064C" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{83ED0FD5-7017-4061-A855-002845709E5F}">
+                <File Id="fil4BA7BF07A214D0A01C1F25AADCE273B5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Label.qml" />
+            </Component>
+            <Component Id="cmpBB6421C1FFC022560343F36A95041222" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{A55116DB-6224-4FF2-9EC0-70AB23E483E6}">
+                <File Id="filC3BFD0172E5E685EC33C8DB87BAD76F9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Menu.qml" />
+            </Component>
+            <Component Id="cmp4DDAC1E3DE44CC8CBCFBBA3D06CB210F" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{4618FD41-AB40-4728-BD99-219CD6C63F68}">
+                <File Id="fil7586BC2AE23CD3902193281F02C0248E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\MenuBar.qml" />
+            </Component>
+            <Component Id="cmpE04A95727C978383CBCB49D97A1EE446" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{F612D0EF-D45F-492E-BD52-3B7D6C7052DC}">
+                <File Id="fil09AEE5F0185B9FA68C9FE84BDF529F13" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\MenuBarItem.qml" />
+            </Component>
+            <Component Id="cmpED38B1F00D1225238E2214684DB828EA" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{5BB51E70-8B7F-4A27-8864-15FF4DAB25DF}">
+                <File Id="filD1F97BD06A655ED84342ABFF251174B7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\MenuItem.qml" />
+            </Component>
+            <Component Id="cmpC3A79272F4A6970E0BE4FC64790E430F" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{96EEB597-F1E9-4799-A45E-72BB58617BB7}">
+                <File Id="filD6552C22B9778DB2B92913DCBD9D9498" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmp5867F3DFD6DC4C9602E1434B07383698" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{21B57F82-C134-4724-89C5-6D7103875AF7}">
+                <File Id="filBF24CB2E7FA2AF9ED3022E6940B3E105" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\MonthGrid.qml" />
+            </Component>
+            <Component Id="cmp0B86A2BA0897185E793369896802E96E" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{9F9F88D1-9C12-4596-86BE-C5146B04D7B4}">
+                <File Id="fil8422F050DD9CDA42ECEBB1EE99208842" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Page.qml" />
+            </Component>
+            <Component Id="cmp30F08C16F7664A7FD8B800EA571EC600" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{966FF1B4-7530-4953-A6E8-B51303B57923}">
+                <File Id="fil3F4C9B4E6543791FEA602007B5F61BE1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\PageIndicator.qml" />
+            </Component>
+            <Component Id="cmp371D1295B34F7D85878351584703AAFD" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{ACA5087E-1862-477C-A431-B68417180EA2}">
+                <File Id="fil4D147319344129E9162821D4F667BCBE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Pane.qml" />
+            </Component>
+            <Component Id="cmpADED35243A1E1A4A4128ED7E402E6B2D" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{15496705-3885-4E26-989B-27D7AFED1AA7}">
+                <File Id="fil92A903FFCC15DB6E98E7B155939EAF13" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp85892AE6398EB099ACD7237FDDBC2663" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{CCE40DD5-5D07-4EA1-9F87-15E563832F0B}">
+                <File Id="fil75D7C7EFE499CAC1AF6DEFB6E0E0F660" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Popup.qml" />
+            </Component>
+            <Component Id="cmp41645B8851CC729CE8A2340B2D8EA8C9" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{46E2B398-7140-4EC1-8861-9AED1F88F4A5}">
+                <File Id="fil9208F77249384F46D7811C30F6DE22CC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmp83C3068A93D919B7751F6806A67BC010" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{63D1B7A5-9E4B-48B6-89A2-5EDA4884DB06}">
+                <File Id="fil858FBF534E1F3A1E9989698AF3D874E4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\qmldir" />
+            </Component>
+            <Component Id="cmp2972A3AC06DB36DE9B0A67D2D06F4C28" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{10A48A1A-F08C-40E1-8F9E-869032501E35}">
+                <File Id="fil8627AA72A823194DC9BC9F9DE84E4F99" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\qtquickcontrols2basicstyleplugin.dll" />
+            </Component>
+            <Component Id="cmpBA1D1B1B1B7C084AF7E1637AAC309AEC" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{31C7D90A-95C1-48E3-BED8-B32DCDCF0475}">
+                <File Id="fil4C6516E5D60D9651A2163C069495D184" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\RadioButton.qml" />
+            </Component>
+            <Component Id="cmp0E92111B9450F4A6D90ADE794FEA6196" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{EEEF141C-A8A4-46A5-B19A-1A93158527D7}">
+                <File Id="fil412584106AC4AE0333ABAF108A63EC27" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\RadioDelegate.qml" />
+            </Component>
+            <Component Id="cmpE47FFACC3F4516133E303BDD889C9D1F" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{FC8EE6CF-1075-42BE-A017-0528FF573162}">
+                <File Id="fil6BB3A845EF3A85E599DBC7B0E7FE0EE0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\RangeSlider.qml" />
+            </Component>
+            <Component Id="cmp91A119FC3A906D3D240EEECF8AE9D3D4" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{BE4969CD-60C6-426B-A458-48AD29EC4B5F}">
+                <File Id="filD9F784F19DF0CD98179945DEEBA488EE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\RoundButton.qml" />
+            </Component>
+            <Component Id="cmp7ABF5829E0A53A285828259AF55FBD93" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{3465DC88-63BA-4ED1-9D84-412DF78D71CF}">
+                <File Id="fil329C26DD4C9844E64AD2F57A60D15DBD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmp1F0564F62D095E1CBFEB84FC30825319" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{99D5DC5A-78FE-446C-9858-58BA1B388EC4}">
+                <File Id="fil1730A57B782FC190B6284D9F98E6C6E0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ScrollIndicator.qml" />
+            </Component>
+            <Component Id="cmp6803EDB4B203595638FEAEBC927CD68D" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{0596464B-FA2C-42E8-A376-DF87D0C3597F}">
+                <File Id="fil560A2E2DC775F39445BCAB545B6536D4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ScrollView.qml" />
+            </Component>
+            <Component Id="cmp134BAB74B710580F5A2FB00F8191EC6E" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{7CDAC9BA-8297-44B7-8B2C-8C121EE13242}">
+                <File Id="fil17A6F2623E603954DBADC1F1A006D289" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmp822617813D99C83FB8C867E6297D7F6A" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{22462D0C-D4F2-49A3-9249-90412F22D553}">
+                <File Id="fil5EC0830FAD7671CEA41A1F19D213AC89" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Slider.qml" />
+            </Component>
+            <Component Id="cmp2E7DE9133BD9260F859E61FFB2A3B393" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{DA381B86-02F5-475A-B163-1BA07836390C}">
+                <File Id="fil4173273958A65349F2C0391884F3355C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SpinBox.qml" />
+            </Component>
+            <Component Id="cmp9CE74A8AA17FD942ABD5D69C504AA378" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{C9DEF86C-DC75-4C80-BE28-C84897B5A3FA}">
+                <File Id="filAF339182D095266C9795FF9DB6C9E3DC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SplitView.qml" />
+            </Component>
+            <Component Id="cmp634D552A24343C8C14E67902DE8E523D" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{7CFBCB67-8F51-4619-916C-C45BDFF0AE18}">
+                <File Id="filDCC67DE7830A892538D3E3895EBEE12E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\StackView.qml" />
+            </Component>
+            <Component Id="cmp77FF8F56519CF853622F66C585276E22" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{D98BBC5B-2A5F-4FF6-A7BD-D3B54E993C9F}">
+                <File Id="fil10ED65E5FDA6E24A5D3208A737461CB5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SwipeDelegate.qml" />
+            </Component>
+            <Component Id="cmpFA4790A9B18722CD2DCBBF767423720D" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{AB4E18F7-3CC8-4E2E-A2E0-5D158D306E5D}">
+                <File Id="filBA3E75F9D796AA9CF10B12670E0B2EF2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SwipeView.qml" />
+            </Component>
+            <Component Id="cmp0289EFEC9DC411805D9B2A93ED64AD94" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{E5B2BC4F-544B-49C5-8277-F7D5295A7587}">
+                <File Id="fil439E587C6353C4C42DD76A6B3DAE462D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Switch.qml" />
+            </Component>
+            <Component Id="cmpD5C7BF54A25E68511DC6C3250DA7B211" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{0B602BA8-AB3E-4F2A-B7AC-93B6A4247859}">
+                <File Id="fil8BD9AE0ABF2958B6EC6D3810D7AF36E5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\SwitchDelegate.qml" />
+            </Component>
+            <Component Id="cmp90D750FC0F1A5C8EDF2B836213305DAD" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{FB7F4B26-0EC9-4341-9B63-B41545920CEA}">
+                <File Id="fil0DFFC68F79EC37C44365666166200602" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\TabBar.qml" />
+            </Component>
+            <Component Id="cmpA7AB4877150A0204566F9EEBF7C7B311" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{5315064C-FA7E-4E16-8DA5-6D0C4A50047C}">
+                <File Id="filB98379933BF7D2FDD7496601B4A293CE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\TabButton.qml" />
+            </Component>
+            <Component Id="cmpE22A10A2340956EA89848EED7EFC3017" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{32B001CA-C3CF-4145-8982-66503922D828}">
+                <File Id="fil484A760FEF941B3D581C380798DF6CA8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\TextArea.qml" />
+            </Component>
+            <Component Id="cmpFB2EA123216AE3660A5E7E0582BB08A3" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{3A7BEEC1-CF12-45C0-8D61-3BF2CBA3F572}">
+                <File Id="filCB1004A8018D061E4DB564F3732B229B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\TextField.qml" />
+            </Component>
+            <Component Id="cmp737444AC8A9F77EE007AB3754674F6BA" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{99182FEC-3A6B-484D-A195-1A07CCAC8A02}">
+                <File Id="fil16DE34E8016A06111873F00C8323EEBB" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ToolBar.qml" />
+            </Component>
+            <Component Id="cmp72FC9871D2AF77FA278880E76F3A90C8" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{5F7DD4FB-6293-48ED-8B9E-184F5B9407B5}">
+                <File Id="fil9023FEA2B9D5FD153A9510189AEA1451" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ToolButton.qml" />
+            </Component>
+            <Component Id="cmp3778759A7704F3FC7F8FA14C52E6B5BC" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{BBABEEE8-0288-400C-9F34-D543A5CB7719}">
+                <File Id="fil0EB13D05ACEC73289F34A64FDAB39CF3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ToolSeparator.qml" />
+            </Component>
+            <Component Id="cmpE0DE716FDEBC4752B543632377A2FDC6" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{E036624C-0F3B-4B49-AB31-8DB7DB546105}">
+                <File Id="filF0138391AA22E48C27E7C5916D2BD780" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\ToolTip.qml" />
+            </Component>
+            <Component Id="cmp1DE1FC193602D7441B20878A9C193CF8" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{E134EDE0-9B5D-4AA9-9494-6924F2D11EEA}">
+                <File Id="fil522BA53FA3FD00A7D7492FF5D686E026" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\TreeViewDelegate.qml" />
+            </Component>
+            <Component Id="cmpA57EEC60C15F1B7B2C0075C4D44977ED" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{E4A58F3D-4C42-41F6-94E0-9961AF61F086}">
+                <File Id="fil8E71D51A414BD3C3EDC95B87CAD123D5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\Tumbler.qml" />
+            </Component>
+            <Component Id="cmpF99329DD035D76C663CC51B0E752050B" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{2B4F610D-054F-4BDE-9BB9-9CBC0DFEE6F7}">
+                <File Id="filAE77B292C8754E49D307852AF1E7CC70" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\VerticalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp5E91A2344113309F2A9E9E8789C9B1CE" Directory="dir547CFC29D0E6DE0A73542C0E043048F6" Guid="{1AF1AB39-D7D8-4EE4-9C0C-E3119E3502B2}">
+                <File Id="fil8C4570AAF64E317A211B6CE6288A7083" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\WeekNumberColumn.qml" />
+            </Component>
+            <Component Id="cmp2A8B609424E7043FA8994B527D760FD2" Directory="dir78C3D911E47BC1C88AC3A9AC7D40EEC3" Guid="{B627BEA6-1B87-4EF9-8B55-634AE3F4959F}">
+                <File Id="fil00DBA91B02D5D1EEE5E70ECAEB18F097" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\impl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp42740A7DFD675250E1973458E1979562" Directory="dir78C3D911E47BC1C88AC3A9AC7D40EEC3" Guid="{FCD32C31-06CF-4279-A6DB-55FE79ADAAEE}">
+                <File Id="filB52B6F422718033D4700E70BA12B21B9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\impl\qmldir" />
+            </Component>
+            <Component Id="cmp191CA6324FBD06A3B02B1B7F18C11E96" Directory="dir78C3D911E47BC1C88AC3A9AC7D40EEC3" Guid="{51C2FBA6-A09F-4355-91A5-AA2CC6E95CBE}">
+                <File Id="fil94069F412C19B914E634255D82E89B35" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Basic\impl\qtquickcontrols2basicstyleimplplugin.dll" />
+            </Component>
+            <Component Id="cmp93BBA84C3F04B42711C94532FDCAA4F3" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{29689046-1BFA-46B4-9C2D-FAB075D62EDA}">
+                <File Id="fil123F8809BA31C59BD88BF52E11E576EF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmp63A8EEDAF640D7402B7313F0475DBD62" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{C48603C9-C6C3-4409-B08A-BC4F393FA891}">
+                <File Id="filECC91990D06DFF209ACEB521F687ED91" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\BusyIndicator.qml" />
+            </Component>
+            <Component Id="cmp957F39DE371F3BFC42D911ED82508CF5" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{522DCE50-4070-4730-B73B-8ECC0F64CA0E}">
+                <File Id="fil9AF4FD98954AFB8061E24AD7EE4E7666" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Button.qml" />
+            </Component>
+            <Component Id="cmpC645E66FCF36896B3810DFD86446472C" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{5FBE10B9-E6FC-42C3-806C-69B84C79229C}">
+                <File Id="fil02B5E701464A49D5F4EF3F2A82B9F4D5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\CheckBox.qml" />
+            </Component>
+            <Component Id="cmp85AFC164A579E3948ADF3B385C296C37" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{380C3636-7348-4147-9874-8C3CDF730B6C}">
+                <File Id="filAA59C5193AAAE12FBD0C57BD81BC47A0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\CheckDelegate.qml" />
+            </Component>
+            <Component Id="cmpD9E495923467D9338ADFA40E39588CF5" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{DAF109B1-7AAA-412E-ACC4-06FAD74D3B79}">
+                <File Id="filB2583FE5C7C545CB3CC2E15AB85BF2FF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ComboBox.qml" />
+            </Component>
+            <Component Id="cmp8A0F3B485218A94493D8E869CFF7287E" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{C4E4260C-A7FF-48E6-9496-70B30EA5A572}">
+                <File Id="fil56662563D7792ECB8A7B190285334F6F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\DelayButton.qml" />
+            </Component>
+            <Component Id="cmp7D57CA3D1A1FB615289A1F17C05DF5CE" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{4EB47DB2-601A-400A-AEC4-18550253405F}">
+                <File Id="filBA3E3EF2CAA08B7000373BDB72A26DA1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Dial.qml" />
+            </Component>
+            <Component Id="cmpA1042570ABF7ABC2AC03619A9B623C66" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{F2E66D1C-671C-4F3E-A315-856F65453BE5}">
+                <File Id="filB88E409B4C3630D5FF606919350D7219" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Dialog.qml" />
+            </Component>
+            <Component Id="cmp2F3BCEF2AD17BB0CF056453E44B1DA5D" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{1A2C81C3-DD1F-4946-A861-6CE76A0B0249}">
+                <File Id="fil68C900F0B3D2BF9559972322D2790CDC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\DialogButtonBox.qml" />
+            </Component>
+            <Component Id="cmpB4E47DE043DEAACAE753D8D57844C6C1" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{FA5EEC9C-773C-4F05-9F80-66405182AEA8}">
+                <File Id="fil2D0A397CE8545EC9F538A9D56CE27F95" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Drawer.qml" />
+            </Component>
+            <Component Id="cmp957C9CBB1420E1B0927FDA4A2795C569" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{03599738-25D9-4576-B3E0-3C952951D4C2}">
+                <File Id="fil6C2E832725A3055A70D4F75F006C864B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Frame.qml" />
+            </Component>
+            <Component Id="cmp0ABF4D43B8185E490D481E9AC8CA7891" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{385F0A0E-94E2-465F-A618-C0768739F148}">
+                <File Id="filBD63B5E121A287BAA4308A245BE0121D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\GroupBox.qml" />
+            </Component>
+            <Component Id="cmp6BD6F20FA4FA4D7D100E4F2B9D526B2E" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{D664437A-4767-466E-96C1-A6E41C86E2A4}">
+                <File Id="fil592D79350DC3B85519CF2F8F0447D2AF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\HorizontalHeaderView.qml" />
+            </Component>
+            <Component Id="cmpECE8405AEA1D11D330D925F66B0227EF" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{F9E3599E-54AB-4682-9381-66642B148176}">
+                <File Id="fil2035CE2CE76094FEB365CAB6AAE48C86" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ItemDelegate.qml" />
+            </Component>
+            <Component Id="cmp1F40775110A3758BD37D1603794F0C55" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{4EF54EFE-BD4E-4D0F-9ACB-743554CAD45B}">
+                <File Id="fil040E62548C74B08FC5BBF294DABF6EDE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Label.qml" />
+            </Component>
+            <Component Id="cmpC38E3633293A96FAA056E8B5F510E90D" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{15C8C08A-2379-44BE-8A57-1E55CFC60D27}">
+                <File Id="filF53AA72457D7ACC1A30AFD0B3341DC72" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Menu.qml" />
+            </Component>
+            <Component Id="cmp895C441C34D01DD38C4FA27C47B768CF" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{AA9B2DE1-C612-453A-9A3A-00049544EC54}">
+                <File Id="filBDEC65E4F2D0114683125F8534F7A8F6" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\MenuBar.qml" />
+            </Component>
+            <Component Id="cmpB9740012788E333FF69054EA6423DED5" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{316E558C-EE2C-4191-B838-1F32A6BFA7D0}">
+                <File Id="fil937C336532FDA482261C1DEA1DAB949D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\MenuBarItem.qml" />
+            </Component>
+            <Component Id="cmpA6DC7007132A6609116E60A89A0324CB" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{172F7276-8D2C-4862-9DAD-74E7C351D7EB}">
+                <File Id="filD93F11A74B5326175FEA10F9C9E66ADE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\MenuItem.qml" />
+            </Component>
+            <Component Id="cmp5B978B6F7BD2D11A34FA999077BF6A8F" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{C1C91208-10A8-4F0D-9263-D956C2E77D3F}">
+                <File Id="fil466EB4CACE10461C5B96ACCE96BAF396" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmp29B865CA91327F5D2E6473F3C1026D41" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{EC9E2315-40C5-46A3-9D18-F445E66B3872}">
+                <File Id="fil41BCC1BF081DFC0569CA520AACA4EDFC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Page.qml" />
+            </Component>
+            <Component Id="cmp1BCB2E932FE113B7305A0BD8C53E4202" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{9E42FF71-FE35-4B2C-AC6D-CA2FF5B46BA1}">
+                <File Id="filFFF887D3478E13F8116D350E78B2E27B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\PageIndicator.qml" />
+            </Component>
+            <Component Id="cmp65D24DF8622A7BC3ACD8701A0AD15EC2" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{B549A880-86E3-40C2-8C26-F9E8BA6C7312}">
+                <File Id="fil6574401211A0A823140A39DE924A9865" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Pane.qml" />
+            </Component>
+            <Component Id="cmp62BB96ABA056ABE59334D9EBDF8365C4" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{9FE50CF4-59F8-4773-A7DE-4D3A6B54373D}">
+                <File Id="fil61FD0EEA35A1607B76BEEC04B3591435" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp788A8E5607E57B678DBA1AC760058E03" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{0AB90761-0D94-43C9-88EC-190F0A733620}">
+                <File Id="fil8A374607A603219F95044EB9352E2694" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Popup.qml" />
+            </Component>
+            <Component Id="cmpE81BA304B75AC5646F26A5C27AE85FD3" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{E1BDCC16-FF68-4046-97BA-FCA92EE83E44}">
+                <File Id="fil598D164233B8457F51539BEAD532B5FD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmp04918B42D3A8771871BAA02F680DEAF7" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{37736DB0-97C6-45BC-9BC5-31038444A797}">
+                <File Id="filDEA0C64319CB4C98B1468FFFFC95FBEA" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\qmldir" />
+            </Component>
+            <Component Id="cmpA9CA24EFAEEAC38C1A3CF8B626D4CB53" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{1501E7FD-430C-457B-9EA2-392D6338F7EB}">
+                <File Id="fil7BFDDBFB1FE07B8E14E8E7E6516D5E35" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\qtquickcontrols2fusionstyleplugin.dll" />
+            </Component>
+            <Component Id="cmpB778EDCA12E3AF4E0A069AD702DED3C8" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{FA3CC813-E1B0-4D02-9398-873D37B46147}">
+                <File Id="fil15F0F77DD308E5C51C4073DB9FBFAC10" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\RadioButton.qml" />
+            </Component>
+            <Component Id="cmpDF999518BA27DAC2F266DAC0C7A2BB43" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{1C3C9062-4812-4A1C-8FFB-A56F60F78D9D}">
+                <File Id="fil04058A60CFC802EF1C27C700689E39AA" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\RadioDelegate.qml" />
+            </Component>
+            <Component Id="cmpAA4A6B9850EF1278FFF2F291071BCB2A" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{A7AD542C-5425-4B46-854C-8E4936410A2E}">
+                <File Id="filBEB790C48E1680FF411230854BFE2071" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\RangeSlider.qml" />
+            </Component>
+            <Component Id="cmp4EE7A42C854017155610E46E548CF266" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{D174A9A5-584F-477A-8D5E-8BE00656786B}">
+                <File Id="fil56C24855AEBCE47EBFDFC9159091FFD3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\RoundButton.qml" />
+            </Component>
+            <Component Id="cmpF7CA31D8018A5DB693939EE00E979876" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{E8E84CBE-716C-4BDB-A0FE-9173497596B1}">
+                <File Id="fil2D00D86916B210D4330C376EF72E5386" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmpB477A3A433A02C6DFEC309DD492687DB" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{8AF127BF-C646-4BAE-9873-FE24DBDC94FC}">
+                <File Id="fil9C56EDCF57BC93E1438CF96D36965311" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ScrollIndicator.qml" />
+            </Component>
+            <Component Id="cmpBBA3EDFEBF816D2097DB4C41F8635EA7" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{3DAA4505-5B96-43FE-8A6C-108393262C1E}">
+                <File Id="fil57C74AB1BDA9DDDFF66840A5697C689D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ScrollView.qml" />
+            </Component>
+            <Component Id="cmp5CE7C499E65206BD66363385164C9FA2" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{5EBCBA78-D252-42B1-A3FA-6B3F13355C65}">
+                <File Id="filB4403B5072467D366D172E078BC3AE35" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmpF8825C6CF454A874807D5853DAC96CA1" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{C7A2DE13-C07A-4616-8777-940C8CBD7601}">
+                <File Id="filBE6B7278916A7A8E6E1319CDE66FEE73" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Slider.qml" />
+            </Component>
+            <Component Id="cmpCB9E142AFC9D19FE67183D55865A3E0F" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{C261239F-142E-4074-B144-E911F612C820}">
+                <File Id="filB09B10505BABECAC307D1BE027CEABDF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\SpinBox.qml" />
+            </Component>
+            <Component Id="cmp19D2BC7A1742FE5CE2CE0CCE54535BA6" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{AC22C5FC-C853-4ADD-8450-CAB72D7532EA}">
+                <File Id="fil978756DA2C7C3E0E6711132F1C953B65" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\SplitView.qml" />
+            </Component>
+            <Component Id="cmp554C33E63FF2C81D7FBA5ABE29E383FA" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{805F9AB8-6F66-4BDB-B9D0-7AFAD67931B5}">
+                <File Id="fil229E136BCADEF09840D78F2426077261" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\SwipeDelegate.qml" />
+            </Component>
+            <Component Id="cmpD9377D7119EAD1A371E16D26AE8CD5F2" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{D28E3CCE-A5AD-4B5F-B4CA-FFD4B6E530AB}">
+                <File Id="fil034047174E9FB35E9E2E70606E534819" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Switch.qml" />
+            </Component>
+            <Component Id="cmpDB5E78C6CE44D55F53566D40D24BBE1D" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{4AEFBE9C-84EA-478F-8318-6B92F6627153}">
+                <File Id="filF2115CC5C5CFA29DB69A1AF7C0933FAF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\SwitchDelegate.qml" />
+            </Component>
+            <Component Id="cmpDE95A8CDA51D40841BFE42981D3A2F91" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{9A6C193A-5DFB-4E88-8755-578A700878A5}">
+                <File Id="filBB9A443F5B8EC06D82355789641B84C3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\TabBar.qml" />
+            </Component>
+            <Component Id="cmp38727874177B251967D83F117A3BEE70" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{1846637A-85E2-4591-9F74-AB0FD2C8803C}">
+                <File Id="filF520E186825210895A2EFF8C2EDC0352" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\TabButton.qml" />
+            </Component>
+            <Component Id="cmp1E8AD6A0BCBF8BD1A4460E16B1459646" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{49F73967-BEC3-4B86-975F-D6444E326BD3}">
+                <File Id="filF783B6FA414887CD8CC7F572A4922E24" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\TextArea.qml" />
+            </Component>
+            <Component Id="cmpFC095DCADEB5A89F06586707E07DB35A" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{F3AF4487-7632-4FF5-817A-31BA8FA66BE6}">
+                <File Id="fil4B4C38BC9AF9B5C1425906BE0652EEA7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\TextField.qml" />
+            </Component>
+            <Component Id="cmpBB7B13F1D78AE63A37ADA8B3FE512CB8" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{278B6D86-8530-4B51-891C-2F53CC8256D4}">
+                <File Id="filBFD211D2489FC6BD15F406F9D99D6E5C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ToolBar.qml" />
+            </Component>
+            <Component Id="cmpCFA92F895CDBBC5DBF724CDFEBE0765F" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{B08E10C3-7C0C-4632-9775-0048BF91C477}">
+                <File Id="filBCCF0BA09C035137942061AE11C65F4E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ToolButton.qml" />
+            </Component>
+            <Component Id="cmpAAE351D729AC09074760EE362CEAEDFC" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{EDE44E98-BB48-4091-A922-780AFBA5708D}">
+                <File Id="filFED9DC0564E217E5B70C40E88C150DAD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ToolSeparator.qml" />
+            </Component>
+            <Component Id="cmpCA85D444470DB90856D55C682B1B881A" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{BEFCFE71-D86C-4573-8850-A75BB07EA584}">
+                <File Id="fil94930E3044AC0BB7C0A1CC900C378C2C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\ToolTip.qml" />
+            </Component>
+            <Component Id="cmpE809A99452EB9066B44D80787ABDA5A7" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{DBAB7E1E-5A8D-4F5D-84A9-6F9D818C8908}">
+                <File Id="filF8C16A0A803A41821D8C78F410949C15" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\TreeViewDelegate.qml" />
+            </Component>
+            <Component Id="cmp1E706971AB2E6609A1BB91BD7754BE2C" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{B96B940B-588C-4B8B-9651-33DE339C251C}">
+                <File Id="fil71552616BD51D68C967A8108C3C34156" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\Tumbler.qml" />
+            </Component>
+            <Component Id="cmp71F7BFBAE8BCE4573D3708E017BA3876" Directory="dir1A86B48CD44E777E78B13993BE3DFC29" Guid="{A0B07D89-4F46-4856-ACA7-AFEBE534EE63}">
+                <File Id="filD7A0393F91A1D2039EEC353CAF154226" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\VerticalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp35F84E364132FF61C34E2B8A4085E59B" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="{EA835BA6-C3FF-4491-AE30-ABEB518C0849}">
+                <File Id="fil55C2B5E883CF11CA03AAB59564BCCF10" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\ButtonPanel.qml" />
+            </Component>
+            <Component Id="cmp7F2E51BEFFDCEE92C30ABC6172B62D21" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="{B302AFCD-DAC7-4EE1-956A-16361714153D}">
+                <File Id="filA17D372C53BE030209E1D95C71BDEC1D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\CheckIndicator.qml" />
+            </Component>
+            <Component Id="cmp24EE24A13287A5C8C0D65E0C2D4909B8" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="{90E0B233-57F9-46C9-ACF4-00D35B32A274}">
+                <File Id="fil9708C1A46FEDBA74EBFD3B0CA72CA6A9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp2392F6C066BC5837827B65D7B6A8F614" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="{C2613416-7C1F-4A4F-A773-3D7EE7F30989}">
+                <File Id="filAFDDCA55B6BE9082C0D7B9F47E4AADD3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\qmldir" />
+            </Component>
+            <Component Id="cmp08A8D7B767927E254C4B586CB5EB9F39" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="{426D73BE-9825-44C3-A5CF-2251D3B354B1}">
+                <File Id="fil3EE6699BC5D58405963695D2AC6E9FA8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\qtquickcontrols2fusionstyleimplplugin.dll" />
+            </Component>
+            <Component Id="cmp9105753D74B5DD54AA90A2AFB4426FD3" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="{71596E58-B4C6-46B3-B041-E9D891324E46}">
+                <File Id="fil40316BC51D62B7228F615E08D4CD68E2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\RadioIndicator.qml" />
+            </Component>
+            <Component Id="cmp31BD21BD04B2B8CA1EA22E2F26D5D0A5" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="{76562555-617F-4D13-9807-851A5D9DEA89}">
+                <File Id="fil03586879198A259A3854D5C0A515323E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\SliderGroove.qml" />
+            </Component>
+            <Component Id="cmp9BDE1F68055939DDBE5EE32AA53F5324" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="{6C299DEE-3564-4556-B8BE-F1112105BE5E}">
+                <File Id="fil412329F904651B548C74F4FE36C67763" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\SliderHandle.qml" />
+            </Component>
+            <Component Id="cmp61E7C47F93C8155B7A99A788B2152A49" Directory="dir1A6A7D662F671C349F73CA17E4F3DA62" Guid="{FC563160-A968-4A3B-B200-38D9B6D1E1E6}">
+                <File Id="fil47D1126A5491D649BB8E1EC6DDFD01A3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Fusion\impl\SwitchIndicator.qml" />
+            </Component>
+            <Component Id="cmpB90BC66FC30D3F0E07924B50F863255C" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{C7DE17B1-614B-4411-A303-DCEEC9E9F63A}">
+                <File Id="fil0A1DBE214C9CC60424A3DED42DCEDE67" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmp4EF1DC5A5072FB3F20AFD1AEB793F94F" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{30B6EE9E-BE07-473F-9C5D-230E28F0F643}">
+                <File Id="filC9F7BA6AB033842F916D7EEED1BB4EB6" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\BusyIndicator.qml" />
+            </Component>
+            <Component Id="cmp1388BC4D80F7D8EDB40817EDC492567C" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{6AA0D52C-BDD3-44FE-86CB-3A6774A406F0}">
+                <File Id="fil0021EEAEA9EFB1C60F51EB352EC1C26E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Button.qml" />
+            </Component>
+            <Component Id="cmpC3DE992FA17AF296C98959F021430280" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{BBFC30CC-22E1-4071-B5D6-FC98784D8C1B}">
+                <File Id="filD00EBDB1540428E75F22BFD9E4AC47C6" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\CheckBox.qml" />
+            </Component>
+            <Component Id="cmp48179523E340069145BBACC26A10AA26" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{71DD7744-9724-4377-8DBF-EB655CA690F7}">
+                <File Id="fil755F0ED01B35E28143D26D2FE7933E82" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\CheckDelegate.qml" />
+            </Component>
+            <Component Id="cmp11776F4851E1DD813385E8BC23FFE8C5" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{41318A55-670D-468D-B12D-D411DDC48B94}">
+                <File Id="fil3FB47A02BBB1F189AEA4CC08EB1A8B43" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ComboBox.qml" />
+            </Component>
+            <Component Id="cmp33DCD47F283028224453A973B4E2F271" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{FA7F9741-799C-4E12-A879-A2ADFA55DAA6}">
+                <File Id="fil4933A6622090D5CC8FEB52DBB946075F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\DelayButton.qml" />
+            </Component>
+            <Component Id="cmp81DA3D84F42F162D1646E837E76326A6" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{665247D2-81AE-4E5A-BFE7-874AE4948C0E}">
+                <File Id="fil0B027F6370F5FC42138BDDC3CB832CE5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Dial.qml" />
+            </Component>
+            <Component Id="cmpABD633E81E45041FFDDA75D788CC55DF" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{D858DD9D-86CE-43F7-8EDA-0F4AB73D6F70}">
+                <File Id="fil449068F2A33DADC4E4196B08F8CB3D5C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Dialog.qml" />
+            </Component>
+            <Component Id="cmpD63BB3D65AE4F43E1EB15B836291EB65" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{F0B7BC12-456E-4B39-9E48-E90AADFD59DA}">
+                <File Id="fil705B9A4FC934EA39803B96CD4B3C0637" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\DialogButtonBox.qml" />
+            </Component>
+            <Component Id="cmpB834EEB17E0CE0AEBEC6AAFB5F2BED19" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{D7B5459F-4C47-443C-8108-B57EBB34230F}">
+                <File Id="filAFCC88499681ACE1828E53C6EDDCEDD0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Drawer.qml" />
+            </Component>
+            <Component Id="cmp8938DC5D9FAD0154F3772DF16FA88014" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{6F485820-3E83-40E4-AB0F-918AA5AFD6E4}">
+                <File Id="filBB1F17C922B6DEB019DD4A3B26996E9A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Frame.qml" />
+            </Component>
+            <Component Id="cmp520B68523086C7771C12504E26FA66AE" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{EAC65DE1-DF00-4F17-BB4F-0C67A62A202A}">
+                <File Id="filF7EB8A18EEC601CC4FD8AE854149B0E0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\GroupBox.qml" />
+            </Component>
+            <Component Id="cmp0DAB42BA97E67306E77D1D2FE4D4A41A" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{7843F513-F3B5-4966-9335-A8F80466BD7D}">
+                <File Id="fil0CF735FAC3FC78D02B1F52F9602B92F2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\HorizontalHeaderView.qml" />
+            </Component>
+            <Component Id="cmpBC65480FDE7319E7F5681A481DE4FB61" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{6EFC8D5B-CF31-45C1-B2FE-B23B83A017F9}">
+                <File Id="fil1126D7F43ECDE5F5AD064B7ACCBAD5BF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ItemDelegate.qml" />
+            </Component>
+            <Component Id="cmp517FF3481F92797BC098C4AAF0CEEBDA" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{74A0E7BD-74AF-4A36-BFE0-4046097B90F8}">
+                <File Id="fil1A4553431D266FF13F4AD499D78A7609" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Label.qml" />
+            </Component>
+            <Component Id="cmpF7C94F112CAF3E64CA3DB5825ECABFC9" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{1D22C79D-3A39-4D8E-8828-807EEFF5C32A}">
+                <File Id="fil16CC0D612CC665C1D31AEDD9FA6D6496" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Menu.qml" />
+            </Component>
+            <Component Id="cmp7314B2EFCC5266DD5976DFC65011E160" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{1E7EBEB0-05D9-4DD7-8B7E-BA8D3366A8E8}">
+                <File Id="fil5218219AD0F35AF3687C64908160315C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\MenuItem.qml" />
+            </Component>
+            <Component Id="cmp9524323CAE6CA0D851C351DF07B54B17" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{739E1F64-667C-4A52-A69D-B13987A9C8B1}">
+                <File Id="fil6C6B5EF0866BEA8C9DA2C397B53D4C8E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmpAB852F814AE2A0119F099F324F1C20E8" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{A8788201-1B91-428C-8AAA-CE4C1531CD6E}">
+                <File Id="fil34BD1730047582434887F38591E8AC51" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Page.qml" />
+            </Component>
+            <Component Id="cmpC7D6CFA9C1E1EA0C6597CA0B871B074F" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{4BC5591C-C288-4A80-A749-3984990F82D7}">
+                <File Id="fil96D91956C1C9B495B7434B8521D09454" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\PageIndicator.qml" />
+            </Component>
+            <Component Id="cmpC15260C4AD42D1CEF736F1C136F163F5" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{A6DF9EBE-6E72-4647-9BBA-C48AA27E4076}">
+                <File Id="fil344DEC2D0972A1F9F5D10D3FAE36F6CB" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Pane.qml" />
+            </Component>
+            <Component Id="cmp5B291454617AF23B7D38C994AB449D3D" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{F2F4C4BC-66FC-451D-ABAC-10CA08F57023}">
+                <File Id="fil2E691ECCB2F7963410346B040B175081" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp45789E71EBA816DADA30D3BC7E74C1EB" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{4C15B7A8-9E81-4777-BF92-F5270996AD80}">
+                <File Id="fil1EF2A0CAC51274AC3E8D05B8484ED1CE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Popup.qml" />
+            </Component>
+            <Component Id="cmpB6BB779949BD09A6C7C117B35AAC3C11" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{7FFA2ED2-668F-46BA-8016-F56D00F41A9E}">
+                <File Id="fil2C18387AE5BF4F98F09799931104506A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmpCAC7687406AF3756D9B550A0782E82DB" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{575E4D23-3391-4EAB-8485-5F54E073F8EF}">
+                <File Id="fil210E2A6B681C2F759FC90F73935B62B5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\qmldir" />
+            </Component>
+            <Component Id="cmpBB8909842E7E5F3DD1F13B8B867612A5" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{9D932AB0-7548-4451-96C3-BF7B8568EB08}">
+                <File Id="fil7153E75CF6D908370B21B71A3EA5CC72" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\qtquickcontrols2imaginestyleplugin.dll" />
+            </Component>
+            <Component Id="cmp651517D86E914D35D824A7FFECD13B3D" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{03A81FF8-716D-47F9-8D22-31B34EF92BC3}">
+                <File Id="fil4C27BF6D20264936550C6FE256B9450D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\RadioButton.qml" />
+            </Component>
+            <Component Id="cmp43AB7C7959383750E6036AB5F1252794" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{805D9FBB-4E6B-45F7-BC9A-4937D7011A06}">
+                <File Id="fil8028A5B8B42C09E22E9A1AB464895E82" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\RadioDelegate.qml" />
+            </Component>
+            <Component Id="cmpF37B0ACA1EC94845C16F4C4AFCB5A2B2" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{18EE83EA-7763-426E-BDD1-A1B429407C25}">
+                <File Id="filD5CB57E6FB9A8E4700982D249284FBB1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\RangeSlider.qml" />
+            </Component>
+            <Component Id="cmp59E1C7C4E4607CC6DCE13C51269B732C" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{2EF581B5-DB53-4E6F-BA3E-1DFC35098DB0}">
+                <File Id="fil6F7C1353446DC84FE1E32EC995CA7432" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\RoundButton.qml" />
+            </Component>
+            <Component Id="cmpB744FD5D1538FF6BBF40E66A57806042" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{7F577EAD-D5EC-472E-8FDF-F23831175045}">
+                <File Id="filBEAC9A233159C63C1DCFED5DA94D05F1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmp28D6C075D8931D2EE68394BFCC7B9769" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{72C0E2B6-85BF-48F1-BAA7-E05A2E1B7A62}">
+                <File Id="filC1ACEAA61DED08BBC2585FF5680C4F30" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ScrollIndicator.qml" />
+            </Component>
+            <Component Id="cmpDB9D955B068BFB9543C7EA8C53CF4415" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{472A21D8-F82E-43CA-A665-53AFD7213E9C}">
+                <File Id="fil1FDB67DBB9BA5B1BF415431442D996C4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ScrollView.qml" />
+            </Component>
+            <Component Id="cmpE20D36DB8EA2F08C50E3A3D297C13511" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{EA2242E7-B52E-4ABC-9B40-2E22DE32A868}">
+                <File Id="fil51FC8E7FBCC491FA0221A44E277F8F46" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmpEE3458BDB70A8381167CD9334D836E21" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{F7D733EE-4B72-4C98-AF1E-B518029B966F}">
+                <File Id="filEB662C8D7D644D4BAE0BED4BA81BAB24" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Slider.qml" />
+            </Component>
+            <Component Id="cmp72E1DA0B7032E89A531FE9ECCC502479" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{02D374B5-C0C4-46E2-A54D-4C3996562406}">
+                <File Id="filC3083743295587902F4DFD54580585C8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SpinBox.qml" />
+            </Component>
+            <Component Id="cmpF68C8F37149A1B836FF56BA9AB021FB6" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{0BFD8326-F038-45A4-94F8-C55C31A00FCB}">
+                <File Id="fil49E9BF9639FDC3D443E71CAC43BBB93B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SplitView.qml" />
+            </Component>
+            <Component Id="cmp3D21995F3CB052BA85DB46E236E13F80" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{57C4BEF5-ECC9-4075-A8A2-92E3ACBF9DFC}">
+                <File Id="filF8009105B5CF6F20079F72CF3B503261" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\StackView.qml" />
+            </Component>
+            <Component Id="cmpDE32BE851FF654E13F8100FC908AE929" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{D5D7131E-B6E3-41C5-8C83-C13D5B2C54EF}">
+                <File Id="filF511A0B0142A4B8E84AB5AC79B23D664" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SwipeDelegate.qml" />
+            </Component>
+            <Component Id="cmp3798BA5F7F00735D5CD82831987CEA88" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{9E07D6A9-EDEA-4BA7-A5B1-E53247961D00}">
+                <File Id="fil10D0FBA433FD973A4809BDB8C39246FC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SwipeView.qml" />
+            </Component>
+            <Component Id="cmp65C62E5359CDF461ECD63741FE2281E7" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{16ACCF92-3704-4CFA-B5E6-BBE2D2C7933E}">
+                <File Id="fil5924DC22A75969FB264E08E704EF055E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Switch.qml" />
+            </Component>
+            <Component Id="cmp2B18A4940D87785365EDFE294DC1D4F1" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{59709E9D-CCE4-45C4-A3AC-65ED6BBA360D}">
+                <File Id="filCDA34143557E58582C9FB30A1BB5BBDF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\SwitchDelegate.qml" />
+            </Component>
+            <Component Id="cmpA85B65AFCC3DD70425F2B555CDEB9C5D" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{B6B10A16-4CDA-4B5B-B555-CF644A0AD8C2}">
+                <File Id="fil1436075F9B2C7C9EA6BDA5B931965B4E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\TabBar.qml" />
+            </Component>
+            <Component Id="cmpA234831EABA20D54678B4AF2AFB001D9" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{4F20491B-C3A9-4BF2-A8DA-61CB87DFAADE}">
+                <File Id="filE3FA6AD5AC0C59434804CB8F23241AEE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\TabButton.qml" />
+            </Component>
+            <Component Id="cmp85D3E99C234D5C73F5C8891E2E03CD4F" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{26F102C5-2B9A-451D-B145-9C828325AABA}">
+                <File Id="fil8371CD0F022F8EBA56526C6AB1BD5F55" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\TextArea.qml" />
+            </Component>
+            <Component Id="cmp9A7E2FE9312CD8830B3B6A104DBF9527" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{BE480E48-3A64-409E-BCF4-2D3F6083A742}">
+                <File Id="filF7491E17C5398B6321A5CA6C195A86B3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\TextField.qml" />
+            </Component>
+            <Component Id="cmp5C181DE0E73A872702E7A5BCBEA45966" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{5E34C9EE-2C92-432F-B3CE-D86920F1BC88}">
+                <File Id="filE8186132C708AE394DEB781D91051FE1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ToolBar.qml" />
+            </Component>
+            <Component Id="cmp8FCAA916FDD499CA06E61F6B4BAB58ED" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{F3FA6D64-D707-44C3-BC54-29B5A91597E4}">
+                <File Id="fil1F09EDC2E28D6848DD0F6B2F109F1B61" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ToolButton.qml" />
+            </Component>
+            <Component Id="cmp2BE8196732C5C2D101C8928A22CD6D15" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{C1A32134-2159-46E4-9786-5B109A61485C}">
+                <File Id="fil180E626065A3BAB06BE24B814A2A404C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ToolSeparator.qml" />
+            </Component>
+            <Component Id="cmp3BBC88103916373B38F08B749DA30956" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{784B66CF-D50A-4334-AE4F-EF6069ADD67E}">
+                <File Id="fil8527A8105CB7EC9CE3F7098C086C38B2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\ToolTip.qml" />
+            </Component>
+            <Component Id="cmpAFB2B07799CF545EFF0B66FDCFC4FF47" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{EC627F4B-FDC1-4DC1-AA54-2931969F12EB}">
+                <File Id="filCAB0EB96ABFFD0D5C3A967366423849E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\Tumbler.qml" />
+            </Component>
+            <Component Id="cmpC6138D71524F53A3542DBA7DD2177CE7" Directory="dir5C7B63749559BE5BFFCAD592A78D8BA7" Guid="{125F6E7E-B439-4590-805E-18884FBF06A0}">
+                <File Id="fil6F5C2672986ACD051C851B373F6527C9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\VerticalHeaderView.qml" />
+            </Component>
+            <Component Id="cmpFE7088B595171444EAD0A7EAD9833039" Directory="dir4E741D8C175690CD9096A89077530C9C" Guid="{5DC1F48C-4902-4A5E-AECE-2BC7D4A9FC70}">
+                <File Id="filFDC49910B20D3BEAC0AAD591C870845C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\impl\OpacityMask.qml" />
+            </Component>
+            <Component Id="cmpEEDFF0982D868E8C996B37C3E9592698" Directory="dir4E741D8C175690CD9096A89077530C9C" Guid="{A817A960-E889-4253-8E0D-0B9C39FE09B8}">
+                <File Id="fil1A4D9CB661D5F7AA80CA699FB6BB887E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\impl\qmldir" />
+            </Component>
+            <Component Id="cmpB7362BFEADB697DA93C3B0E9471FC933" Directory="dir4E741D8C175690CD9096A89077530C9C" Guid="{975DC03A-8740-47AD-9DAD-F90588A55336}">
+                <File Id="filC7915B57A913E31D3C4D427C6E2219F5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\impl\qtquickcontrols2imaginestyleimplplugin.dll" />
+            </Component>
+            <Component Id="cmp7477DF3B20A2314AB9D9D7C2B37516D1" Directory="dir4E741D8C175690CD9096A89077530C9C" Guid="{5408DE80-60C4-4816-B640-A55A96AB29F4}">
+                <File Id="filBCA1409917679CAA35888D9AE61821D2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Imagine\impl\qtquickcontrols2imaginestyleimplplugin.qmltypes" />
+            </Component>
+            <Component Id="cmp11D7D79C97B237EC913F8032C8F836E4" Directory="dir171007FEE30C4A50EC7ED2CB58E0B5E3" Guid="{04471F08-242C-4659-9F1D-BE21CCA14AD9}">
+                <File Id="fil8E3F37E1853A667FFA38E60B8B1D9B85" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\impl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp62D853DC85F6B1CDD4296C97C09EEC5E" Directory="dir171007FEE30C4A50EC7ED2CB58E0B5E3" Guid="{2EFAE591-A45A-4F86-A0B5-8657064BB949}">
+                <File Id="fil86621360A721274A27591FE2D5E332C8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\impl\qmldir" />
+            </Component>
+            <Component Id="cmp4AC6569F2C038B84A66F7107E43CE9A5" Directory="dir171007FEE30C4A50EC7ED2CB58E0B5E3" Guid="{3753F0D4-FD59-4E50-B63E-3B190CBFB255}">
+                <File Id="fil159FB9D3A93DD5E084022CE4FF2A6036" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\impl\qtquickcontrols2implplugin.dll" />
+            </Component>
+            <Component Id="cmpCF13BB5CB6845852387DB0E66E6FB9E0" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{5F9CFC0D-3280-403F-800F-BFCF1C007794}">
+                <File Id="filA3CCB43FBDE7526A171F1B23C7D68019" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmpEA8352233733F0363804856322EB0FE6" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{54A94EC7-F2F7-4B3A-A76F-D7742D895E2F}">
+                <File Id="filCFCAADBC9A7DF963E5D1F99E31F790F4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\BusyIndicator.qml" />
+            </Component>
+            <Component Id="cmpCB55784D3984D9F2DF6615E789C57FAF" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{4170FFC8-36B4-4D26-A518-101423CAC181}">
+                <File Id="filAC46DEAA1116ECA656B8A60FE478AFD0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Button.qml" />
+            </Component>
+            <Component Id="cmp01ECF58B0613C06B4BA4FE126C713F0E" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{B4FCD061-40C7-46A1-AD38-30C76E69AAC3}">
+                <File Id="filA63D753CD0F650D43D0A3385004927CE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\CheckBox.qml" />
+            </Component>
+            <Component Id="cmp005997DB9453F1DE5910014AB11D4C75" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{E5BE9CFB-1B4F-401C-A264-F8DED9C55402}">
+                <File Id="fil840D7F4F642145ADA3304B0943E075E7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\CheckDelegate.qml" />
+            </Component>
+            <Component Id="cmpD66560236CF24FF97AB65D56AA555530" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{80D9B265-FDF2-45D9-90F6-FE8C6B9E9E67}">
+                <File Id="fil5F51EFB1183F259CBB20408ED17D912A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ComboBox.qml" />
+            </Component>
+            <Component Id="cmp96F28C12C99816E6BF59536C553840AC" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{A7607E2E-6EA5-4183-8D5D-90FE2B87FA2A}">
+                <File Id="filDE90510FAD5DB3EC855DD77BE31557FE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\DelayButton.qml" />
+            </Component>
+            <Component Id="cmp154E9BD7A64B0DD68CD83B4F23511A21" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{F86A99A9-35C9-4FEE-A275-46102EC527B4}">
+                <File Id="filF96A517FD34F4B3451814DEB8A4AFE4D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Dial.qml" />
+            </Component>
+            <Component Id="cmp80E1A825825A58BA318AECDBFA24CF1B" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{573013B0-A9A0-407C-8205-91D2DFDB2CE2}">
+                <File Id="filECE3A53486E1A49EE2014DED52AD7B05" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Dialog.qml" />
+            </Component>
+            <Component Id="cmp888BEB5962E610A3947D2FF3387E6C37" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{0EDF783D-8C25-4C05-B744-D78AF728E1E4}">
+                <File Id="fil302D513E9406E43BDCC01FCDB1E5A4A9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\DialogButtonBox.qml" />
+            </Component>
+            <Component Id="cmpE557B10B81996EA9E0C090A014620ED1" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{E1287B39-DA4D-40FA-83F2-6B844336BCAF}">
+                <File Id="filC0FB78AC4AF34DB778DC9626289CAA0C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Drawer.qml" />
+            </Component>
+            <Component Id="cmp190AA6A126CE5FE0DBBDFB2E9E370822" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{6647E4D9-09A6-4D52-93B8-0E1E8EDA3889}">
+                <File Id="fil7D5D1415252A4B9A51401D53E8436427" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Frame.qml" />
+            </Component>
+            <Component Id="cmp1E05B467AF2E75161A28F5E277E0BC54" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{0A063CFC-A562-4EB3-8B9C-B8D20C25986F}">
+                <File Id="fil719B30F98FD710E5E1310DDA4B41C1DC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\GroupBox.qml" />
+            </Component>
+            <Component Id="cmpBD15B6ABA62E41DEE473C65FAC1CDC3E" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{8AD45546-F1E7-43C8-B055-ED9993ABF4AD}">
+                <File Id="fil450DEED653A70B5D0787578C3EFF67C0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\HorizontalHeaderView.qml" />
+            </Component>
+            <Component Id="cmpCE84DF6B3EFC1F4DA2580194322E6FF9" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{03099521-3EAF-4701-9147-385F00BB8C2B}">
+                <File Id="fil70892882732D9D6CA5DEBDBBE7A16C32" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ItemDelegate.qml" />
+            </Component>
+            <Component Id="cmp6B02AE0065D2F87E3E1EE0FD80C2F114" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{2350BB30-5473-4301-A96E-EB5B3C0242DA}">
+                <File Id="filA8167CF68C0141B8CF10930ABBBDFA24" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Label.qml" />
+            </Component>
+            <Component Id="cmpD5A3C65BA27F1F7D3C1C50FE455F2D2E" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{B262886B-C49A-4016-B318-904124CF32F1}">
+                <File Id="filB2549F45573FCC17A86310628BC6071E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Menu.qml" />
+            </Component>
+            <Component Id="cmpCF1B98B583FE40BEB19FFE3103A22E1B" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{56EE55B0-2B96-4964-82BE-1A24E39EBB41}">
+                <File Id="fil1396E4F58ADC81EDEFD42D691E754D74" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\MenuBar.qml" />
+            </Component>
+            <Component Id="cmp60BC16CE2C100CB3F06A7FB02995E94A" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{316A7684-D310-4192-A609-97EE5E3AF307}">
+                <File Id="fil74C563C8F2B8EB30CF1980ADAB1D40AD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\MenuBarItem.qml" />
+            </Component>
+            <Component Id="cmpB3740D2D255D2EE71B4E675920B5AFEB" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{B7B414FE-3141-4316-95D4-17CF64660E0B}">
+                <File Id="fil951CDA05D439F1C57B4515E40E866D79" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\MenuItem.qml" />
+            </Component>
+            <Component Id="cmp7B7C1CECA8E7E92CA81441EA981CA135" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{D9F49CD3-2CFA-4FD2-BCE9-034535E1390C}">
+                <File Id="fil56681E7D5B501EBB64DC7ACA76A9B2F5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmpF81A2DDE2AD5C942A50EC687D33B60E5" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{20BF2496-172E-49A8-912E-D717EB699369}">
+                <File Id="fil8D8880C57F5A507C6AB72CBF88D14171" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Page.qml" />
+            </Component>
+            <Component Id="cmp794134A6C8E5C4CA118CBC4FD67AE890" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{73C45D36-3AE6-43DF-B3BD-BE250A87C202}">
+                <File Id="filBDAABFC0E12EA846ED903A9377CCDF54" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\PageIndicator.qml" />
+            </Component>
+            <Component Id="cmpE794B530811F6258FCE9C834CB062851" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{7009ACEE-7D72-4861-A281-2366EE0F4B15}">
+                <File Id="fil0D80249F03C878DDBE677508709FD698" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Pane.qml" />
+            </Component>
+            <Component Id="cmpB04392B85D826BFDCC105DB22BDB2546" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{32DF7868-0DE9-42A6-97FC-77C0BC23BDBD}">
+                <File Id="filC3A2F8CDA348ACDB2AF26C9E6FF8FAA7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp0402A1984DEDA53F483897C1C679BF1F" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{FE372008-BF56-4B0B-9399-24CB754A52FD}">
+                <File Id="filD62B57B4E80B34E2BB014622F70DE27C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Popup.qml" />
+            </Component>
+            <Component Id="cmp918C71F55F3A45A8D28838ACFE6248C5" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{04BE9DB0-8A4C-470F-8378-9E4CB2141B22}">
+                <File Id="filF11F4684DFBFBF9F0FF4D9AD374F0387" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmpB6BEE21772567F79969561FF4BA72410" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{99730887-D16A-45DF-A19E-F861EA65176A}">
+                <File Id="fil053291A05BDF8AD3704981A7B0898F82" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\qmldir" />
+            </Component>
+            <Component Id="cmp1C864015DC7795834A0057A175C597E1" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{5E34839B-1428-4097-8BA8-2696BBBC15C9}">
+                <File Id="fil84A915D0D0D5112A88F2D9B399BDCB45" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\qtquickcontrols2materialstyleplugin.dll" />
+            </Component>
+            <Component Id="cmpC97800A6EBF13D95E9FAC300048C560D" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{187A96A4-FEB7-4226-87DB-666EC4974699}">
+                <File Id="fil5F5B5BA5159415A93B17246AAD0F1B2B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\RadioButton.qml" />
+            </Component>
+            <Component Id="cmp810E9C023C627F0FD312E6664F2A25D9" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{42A753ED-C830-4CC7-A19A-5B3E638454C2}">
+                <File Id="fil5C06D9354E96E77D1D3863FC94773599" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\RadioDelegate.qml" />
+            </Component>
+            <Component Id="cmp519F1EA5B1FD43D841FA07B7E430E445" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{1A7EBA0E-1AC8-4046-9216-5AD8CDA67C0F}">
+                <File Id="fil3C4C340CA6E8CEC7266592F48257C70E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\RangeSlider.qml" />
+            </Component>
+            <Component Id="cmpC36777B7421BD8565A73A1CECA4A6EE5" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{7F5F7D4D-7DA5-43ED-B9BF-3688046F5C94}">
+                <File Id="filCC10D65CA29863504C8CC74493BD6CA5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\RoundButton.qml" />
+            </Component>
+            <Component Id="cmp29EE21BDFB11914175077667D19EFB5B" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{62B3C915-F242-47A6-97CF-59B837F4A338}">
+                <File Id="fil94B9EC0027D8068E4F6709CA3E0C3360" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmp2141E33205B42038E203536124C78B26" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{FB7B617A-78DC-462A-915C-4ACA51488D9B}">
+                <File Id="filBAD56A1834C7FCE66FB34D6AEAA39A01" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ScrollIndicator.qml" />
+            </Component>
+            <Component Id="cmpF0443FA976B9A0C13FF2F410172218EF" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{C734623A-955D-4303-92B2-13308DB2F3FD}">
+                <File Id="filF9E938FE9C5D0D3449F5B405DB07D73E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ScrollView.qml" />
+            </Component>
+            <Component Id="cmp290D27CBECEDEAFCC6D399584107C8C0" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{009DC712-6A4A-44D2-B7C8-FE22F35B2F2C}">
+                <File Id="fil0B08D3BB58D6C6CA8328496E951B338A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmpECBA9A40396575E8805D705BA6DD87DB" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{FB8C0669-DF32-4168-B6F0-9B20E03474D6}">
+                <File Id="fil5A7B23D53F1D31AF38D118A0E6207972" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Slider.qml" />
+            </Component>
+            <Component Id="cmp9886A67A48D7C9D7684522160D081022" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{0E0E2CC0-3ED1-4C8A-A793-B79701BB1307}">
+                <File Id="fil460842FDA0D0EC28490A2B7FF0BC3205" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SpinBox.qml" />
+            </Component>
+            <Component Id="cmp985E022A5C9C43CCCB2BFA14A9108961" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{35573BEE-C561-48AD-BAF8-8E6591FA9630}">
+                <File Id="fil054E98ED8AF7502514CEC672E99F3F50" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SplitView.qml" />
+            </Component>
+            <Component Id="cmpAE2EEFF88C165D2E2D45EBE3A37D558E" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{6A15B0F0-DCA9-46DD-8005-91ACE503BF7D}">
+                <File Id="filD3BFACC2DA16F1BDEB5F32A9280F21B3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\StackView.qml" />
+            </Component>
+            <Component Id="cmp9EB09B3CCD9D6D7B9D65629B00942455" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{E431BBBE-28F3-4446-A245-DD95E9A0A96F}">
+                <File Id="fil9ADA5B74CCB596A8EAF3FA017EFB5260" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SwipeDelegate.qml" />
+            </Component>
+            <Component Id="cmpE36B9BE6B18A774CDB849B20C13BAB86" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{AD84244C-AA27-42A7-8247-0B5D75BD5355}">
+                <File Id="fil3C5CF2C209F301AD82A32857FE65AD48" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SwipeView.qml" />
+            </Component>
+            <Component Id="cmp9734CBE5CCB5307199376FFEB7E2A5A2" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{9056E5CF-F9F8-49DE-ADBA-69D4480FC116}">
+                <File Id="filAEF492CB3B3CD816DB0F56ACA80A50AE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Switch.qml" />
+            </Component>
+            <Component Id="cmp4BD0D0FEC80C2290B78A28E477536FD0" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{EFF56476-5487-4929-BD7D-6F3BEFB4E0A7}">
+                <File Id="filCCB96245DBD179FED5867875729BF838" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\SwitchDelegate.qml" />
+            </Component>
+            <Component Id="cmpA4621B705888F802DEFBFBBEE22375B1" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{B72C25B8-46FB-487F-B006-3C121C2F9609}">
+                <File Id="filE761FF0B176FD74798A2B0372140C031" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\TabBar.qml" />
+            </Component>
+            <Component Id="cmp0289C3F6859ADC6AF76A91C9F5484C31" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{44DB6D6B-9093-49AA-9B0E-594F300E46C8}">
+                <File Id="fil3877A40E3995B4EB525E3390B8862476" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\TabButton.qml" />
+            </Component>
+            <Component Id="cmpCB1CCFB21E1EB2298E29111F5579D022" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{F29928A4-6DB8-476C-B317-B875767DDA41}">
+                <File Id="fil9D73808A3A40379E7D8386F82262AB99" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\TextArea.qml" />
+            </Component>
+            <Component Id="cmp3A262ACD2D62E8DFF3DFC1ADC239B32A" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{C8A98A9F-D52F-4717-A83C-BEBDC7DEB4BE}">
+                <File Id="fil5D746CB66F1EA350A4ED1E78AAE1BE2D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\TextField.qml" />
+            </Component>
+            <Component Id="cmp8088ADB1622AEA4DB7C0B72A37F0EEBB" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{F290768E-8DF4-4ABC-9DD4-0C2565D9DE01}">
+                <File Id="fil32F2E0A71EAD04B891D2CBC968967C52" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ToolBar.qml" />
+            </Component>
+            <Component Id="cmp12029B0ACE2380A95ADAE86EB4BC13C0" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{9A467CF3-4844-474C-9CF8-03644EA57753}">
+                <File Id="fil1EE7E4109157ADD9974A013DE84F00E0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ToolButton.qml" />
+            </Component>
+            <Component Id="cmp4D6D0C748629626F327BC86EF1951208" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{0F5989CA-F60C-46CC-9D7C-2993A6EC87CA}">
+                <File Id="fil3AAEBEBB0EEF1D1A8023F8647A9D2908" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ToolSeparator.qml" />
+            </Component>
+            <Component Id="cmp2B3619B85F678EBF8B98B06E6ED1C7EF" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{1B4B529F-F038-4EA2-A9A1-1F118303123C}">
+                <File Id="filDB43C5904651368F7B695B52A34B2780" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\ToolTip.qml" />
+            </Component>
+            <Component Id="cmpDCB56F79763E84D43053248E9BAC844E" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{772473FE-535B-45D3-AD46-30E7BD8FDFA5}">
+                <File Id="fil3467664546CAB8F0F6410A5183CF489C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\TreeViewDelegate.qml" />
+            </Component>
+            <Component Id="cmp8F987B0D48E647C9F0BB5810CC50DEB9" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{39020040-5A88-4F2A-9569-8CB5DC54B818}">
+                <File Id="filE68A2F6161B427B4E53B59ADC42F5A8A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\Tumbler.qml" />
+            </Component>
+            <Component Id="cmp4E3A3D737E0A5ACC7A8D42D075170F2A" Directory="dir463F84444BF926F4998942302E2C59AF" Guid="{E4582867-29A3-41FC-83EA-A9A4EFFB4D40}">
+                <File Id="filB920B036CABD41BA576BDAEE5F50AE7C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\VerticalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp4974A3B78183896E1AED2DEE9BAE0251" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{4053A434-A838-4B7F-961C-CE0674A38372}">
+                <File Id="filEEB1FD3B3304738741C4BA6D7D64A00A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\BoxShadow.qml" />
+            </Component>
+            <Component Id="cmp6A8F7A322E2F2C6FD2BA43B8B451FFB8" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{B7891102-51C6-4598-86FE-7B0DF340C67F}">
+                <File Id="fil6589143E2BCB1391E81C601ECA3D476D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\CheckIndicator.qml" />
+            </Component>
+            <Component Id="cmpF62E2C4B7807E0679EE98B3175A6F50F" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{DBF50EDE-9177-4E4D-A651-CA34FDB70AE2}">
+                <File Id="fil89C00A9449C46E4D87A720B60C1F16B1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\CursorDelegate.qml" />
+            </Component>
+            <Component Id="cmp5ABFD007CB27FCCCC6C7F6BF2BF7F1C3" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{A9889C9C-7B1F-41B0-A64B-2DD55B70261A}">
+                <File Id="fil3A032480D0BC1C9FB9BDFAA859BBB944" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\ElevationEffect.qml" />
+            </Component>
+            <Component Id="cmpBACD13AA12B6FC7BF611DC56FC238999" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{D22D9132-5ED7-4EDE-89FA-A68AED478FE4}">
+                <File Id="filFDC96EA252569D84275658079E02E9C7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp37DFC6073765C4BA0142EAE156302405" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{CED68487-0B28-421C-84DE-1857BFA070CB}">
+                <File Id="filF63CEDA19E2B556BF56331E9847E297E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\qmldir" />
+            </Component>
+            <Component Id="cmp5E6437A6E1FEFDC5F3FACA13467786C6" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{3FE6BCA4-7793-4801-8628-D9018A0478ED}">
+                <File Id="fil553D014E7D65B2D6B1576172992F1B4F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\qtquickcontrols2materialstyleimplplugin.dll" />
+            </Component>
+            <Component Id="cmp2AE92027A7FCDEBF15F1A365C3786D3C" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{66E4B841-120A-4D97-A09C-0C818829B44B}">
+                <File Id="filB30B1757D9725E0AA4A920E66578281E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\RadioIndicator.qml" />
+            </Component>
+            <Component Id="cmp36E9DC5315FE3B8AA35070E2DA84DD48" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{64C8CCCE-A696-44D7-8DDC-613184C051EA}">
+                <File Id="fil3F0A3E1797F80CA73C3FA6B42F73C834" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\RectangularGlow.qml" />
+            </Component>
+            <Component Id="cmp754D99341A465EFD63ADC35046E16790" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{C925CA46-0C6E-4E42-A175-3C55960D10DE}">
+                <File Id="fil1174B4B89CC14686B2D704BEF48A73B4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\RoundedElevationEffect.qml" />
+            </Component>
+            <Component Id="cmp082A1EEBA23939825BF1B8C532D1919C" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{55C55D51-AB1D-48D1-A3DB-1FB8F9EA1821}">
+                <File Id="filB787BFACAE37AC95BC017EC2F3148E77" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\SliderHandle.qml" />
+            </Component>
+            <Component Id="cmp6A3F490C8D166289D4E591A3FB38B14A" Directory="dirA55BCC7BED6B60785CEB74AFBA374495" Guid="{54A0343C-5FEA-483E-9BFA-DBC323F0CB9C}">
+                <File Id="fil1CC04953B53C4A7D01CBA295531B02AF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Material\impl\SwitchIndicator.qml" />
+            </Component>
+            <Component Id="cmp51D4953927172FE66AB259A9E3465218" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{728A2703-A87C-449C-BF30-8AF6BB8CF6C7}">
+                <File Id="fil60EFB57B56EFABFC5D70302BA7B83C51" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmp50AE987125D48D86F78F809B6B326AF2" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{7D472D96-93C1-4A5C-B22F-362B2E863502}">
+                <File Id="filBC5F989227D32BE957078E299336A32B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\BusyIndicator.qml" />
+            </Component>
+            <Component Id="cmp9FB9149775FE4BFAA9B0238DD797CB99" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{CFAE9678-EDB1-47CD-B165-B369AF503CE5}">
+                <File Id="filFA519204CB8D32802BE976C955ADE249" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Button.qml" />
+            </Component>
+            <Component Id="cmpDC91AE6EF2520D4C8A306AD82BE8E963" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{BB99E73B-A2A1-4CE4-8856-750922BF3F05}">
+                <File Id="fil9DAEFC0BA4824FF863E0BC607BB3F726" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\CheckBox.qml" />
+            </Component>
+            <Component Id="cmp6354B1BE039A3A801525721FB8B6DA39" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{BB427DF6-5D63-4E9B-BB98-98480059B9A7}">
+                <File Id="filC688B6E0B44BE8B7183854F811FCA3CF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\CheckDelegate.qml" />
+            </Component>
+            <Component Id="cmp9D0B5875C4045E501F674FF488B8D6E8" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{DDF0F71B-66D4-4F70-BF74-D757F3AE2C26}">
+                <File Id="fil31451B6BEECF73BDBAE7AB4ED9B8A74C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ComboBox.qml" />
+            </Component>
+            <Component Id="cmp087C60BF1A5ACC5118DF8BF393AD237F" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{47B86663-0777-44DE-A552-496D5F4E1733}">
+                <File Id="filCB4C62BF8104E9DD140E2CDEBF1216D1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\DelayButton.qml" />
+            </Component>
+            <Component Id="cmp8C096DE3954F3C717FD6A15BFBB1DFA6" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{B4A04F43-6761-40FC-910B-11D616C9B62B}">
+                <File Id="fil621F0D1411277F3863B5713D6DA92E86" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Dial.qml" />
+            </Component>
+            <Component Id="cmp9A29685D109A5F605D3307C06A7E94DB" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{55B1241C-D0CF-43D7-AC73-7E22E4F7DFAE}">
+                <File Id="filB07409843046124F0BBCEA771662FE93" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Dialog.qml" />
+            </Component>
+            <Component Id="cmp910AA543273FFA2E55BED62330ED3FB2" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{27965755-AFA7-4F10-A841-347471243A58}">
+                <File Id="fil5C2DA019DD85B1AB18DEBB58FD7B17A8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\DialogButtonBox.qml" />
+            </Component>
+            <Component Id="cmpF8DABF55A2291D3AB2E08F172A4A06A2" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{C3B99B80-DA62-4BD0-BEC9-B633374C5A22}">
+                <File Id="fil3EB5FC177D81A00809E137EA010E4051" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Drawer.qml" />
+            </Component>
+            <Component Id="cmp96161F632A366081919D6B2A30255DC1" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{0C1F73BE-3C78-48E8-A5EA-85A61A456B63}">
+                <File Id="fil9BEE87A9A8B44D9B499D632FAB30219A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Frame.qml" />
+            </Component>
+            <Component Id="cmp4C17868CD5D85912A6F0BE644AB3095C" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{61E4424E-7314-4007-A90C-7B42C659DA6B}">
+                <File Id="fil4AC7BD9A0592C11527E2882BBF99DE25" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\GroupBox.qml" />
+            </Component>
+            <Component Id="cmpCB3A44ACDFD39C22F19F1A1C83E28EF1" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{0DDA1972-C333-481C-80EA-E7D8A282A383}">
+                <File Id="fil064D0FBC8E9088A72337290D87E14422" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\HorizontalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp9E02917D9B78D87D7ED5986CFCF0BC38" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{A2480A98-9E8F-4A07-A920-F5542045D72F}">
+                <File Id="fil6D16367AF986E353A82BEBF5FCB7CF9B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ItemDelegate.qml" />
+            </Component>
+            <Component Id="cmpA2D8FA34582A60D9034870387BD3D03E" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{AC1943AF-11FD-482C-B084-1560D0FC99BB}">
+                <File Id="filCCD76566485F6C9478A00AAB064E9C00" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Label.qml" />
+            </Component>
+            <Component Id="cmp728F2B606FDC43D7AEDE271F1E2A81A8" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{CACE6C5E-E32E-4E18-B967-6C56ED73627D}">
+                <File Id="fil42276E9C369EBEEB368374611883C535" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Menu.qml" />
+            </Component>
+            <Component Id="cmp6852AE6BDFAFAB326B6402ABE17B022D" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{A935632F-7CE9-4E9D-A1EC-2F18BCC93758}">
+                <File Id="fil03498BED0D38C2F221B4050185FF1698" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\MenuBar.qml" />
+            </Component>
+            <Component Id="cmp3DFC359CF7645BCB15E6A7555D8CE483" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{7421136D-FEB8-481B-A342-8C1FE0B9433E}">
+                <File Id="fil373F2E2ECEE795B06FDD75C967B8B4B9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\MenuBarItem.qml" />
+            </Component>
+            <Component Id="cmp4F17A89EEB5A0EADBFCF141685503D29" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{1CC9F5B0-36C1-4C63-B5E7-B1EECA05EF77}">
+                <File Id="filDD18D1C6CEECFD677C923B93AC3D0C76" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\MenuItem.qml" />
+            </Component>
+            <Component Id="cmpC44104343636AEE657FF14B1BF500629" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{D63F5105-A05A-4A80-B565-4903A65DB41C}">
+                <File Id="filDF2F69E5F74A66E0838D016BBBF102A7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmp9E3F847F66BAED7CAB8605D5F8042DCC" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{BB8CE079-DCDB-4091-9B6C-F10A842F791B}">
+                <File Id="filC2B089FE945EB06E7049D5033791F4D7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Page.qml" />
+            </Component>
+            <Component Id="cmp907EDD0EC968E3CB752741291EBA1AE5" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{6C90D862-12B3-462A-8B9C-B31E01BA94D5}">
+                <File Id="filF940E45C7ADC6D8891FACF4ADCA3060D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\PageIndicator.qml" />
+            </Component>
+            <Component Id="cmp1FEE0C7E5A8B3F19CF1A59898415A197" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{DC0BF97B-3854-4A7A-8787-4992CE769FAE}">
+                <File Id="fil9BB6E26F8F3C859253711636D08442AA" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Pane.qml" />
+            </Component>
+            <Component Id="cmp685E4176CF6CD43E3F2296FE3011A50C" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{C59EE695-9F52-4721-9870-33E37CC1F08A}">
+                <File Id="fil2BD53F267B47AABFE9880CC83450CED4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpAF3F2B21903C71CDC9EB17006779D60E" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{20D7D31B-1879-4756-B836-D9B4DA6516CC}">
+                <File Id="fil71562651D8D0F800311B61E5064E3CC2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Popup.qml" />
+            </Component>
+            <Component Id="cmpA9C2E39114BEEF09DE4535C28964189B" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{118C4AB6-23ED-4ACF-B452-014600F69B97}">
+                <File Id="fil11BC81473EBB26C6EBBA5170D5EBDE75" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmpDCB058CAC37112FA04AD8AAE585D409B" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{AD22E54A-A4BB-4283-ADF4-83F62E4E5C00}">
+                <File Id="fil59652335BE550A0250A3E167B1F40736" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\qmldir" />
+            </Component>
+            <Component Id="cmp33FB18F3293A67E3EBBB2358F64755E0" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{C510EED8-37D2-40F3-94EE-61F16B4402A5}">
+                <File Id="fil5FC12DA36BBB59BCA1862F9EDA004269" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\qtquickcontrols2universalstyleplugin.dll" />
+            </Component>
+            <Component Id="cmp5C4F612CE6E32CCFFA240B4A0B87918D" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{8EF7537D-17BA-4B21-9BE8-9BE303072E73}">
+                <File Id="fil01F4B5A3F96708A3194B368B53340FD4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\RadioButton.qml" />
+            </Component>
+            <Component Id="cmp1B849263C1213BAC8231151AEF54F69C" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{8870FC05-6175-47F3-9BFD-E2E7EBB46AF9}">
+                <File Id="fil432F670291BD6028E1969692246B13AA" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\RadioDelegate.qml" />
+            </Component>
+            <Component Id="cmp537E154F45B568C4A7F32AFFA81089E2" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{D49AF4E5-5315-4433-A789-A6F8681481F8}">
+                <File Id="fil4B711803D153FDFB6A4A7CF9E2600D80" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\RangeSlider.qml" />
+            </Component>
+            <Component Id="cmpB6C66482626F530FC2503D0F0E59D9BB" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{D5C0B1E5-DB43-4AC5-98E1-591C8FDDC102}">
+                <File Id="fil21A8ACDA637940AC4286A7AF53B8B17D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\RoundButton.qml" />
+            </Component>
+            <Component Id="cmp64A1F7B15057A2E1D19560ABDF9DAF80" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{4AA15098-BD14-4ECE-B41D-C4C81A392D17}">
+                <File Id="fil2FA3E488B17C5AB584728B88EB831B64" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmpCD57DD397AFFFE295449E80AFB9F4E5F" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{F956C62E-A82A-4F29-B14E-892333AFE760}">
+                <File Id="fil7B5AD4F110A9268DB1ED92413E6DEDE8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ScrollIndicator.qml" />
+            </Component>
+            <Component Id="cmp9524C10C5C6DA9FF8F8B6002740132CF" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{05F5C1BE-5D94-44AA-BA00-445DCEF37855}">
+                <File Id="fil0A2F722A7C60AA2DFAF098A341F5CFF2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ScrollView.qml" />
+            </Component>
+            <Component Id="cmp5A0AF016778EF8E4C1D06EA2E10A7EE3" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{D9CDA431-D381-4344-9736-FC78DED54626}">
+                <File Id="fil03ED63365C5C6CE2B52F3ECB5E0B4B39" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmp29C9420D05B976736364005D792E5DA6" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{EF142FEE-79DC-489D-9FF8-0DA79729796C}">
+                <File Id="fil0A418C1F9F31EA91E6F9C6D5059AA8BC" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Slider.qml" />
+            </Component>
+            <Component Id="cmp416E2860BEED8693C7D2E3FEAAC98CE6" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{09F33D0D-FEE7-4B91-ABF7-C36313434AEE}">
+                <File Id="fil4B32E68B346D12238475094812008A27" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\SpinBox.qml" />
+            </Component>
+            <Component Id="cmp9277891754C1B5136DD8569A94B4DF80" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{2962D89D-3343-4E2C-AB8D-BDCA39AE069E}">
+                <File Id="filB45FC92883BB1384879235F2FE3FD0F0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\SplitView.qml" />
+            </Component>
+            <Component Id="cmpF8D9196CFA84283BAA12A5A7C7C09537" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{1988980D-CD88-4A50-A403-BAB9EE505B34}">
+                <File Id="filF381B4329F33E2F4016938E283FDE306" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\StackView.qml" />
+            </Component>
+            <Component Id="cmp7A2E1B8C0BA43F0FCE98C773CC1B4EA5" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{C7FA22BC-D811-4FF8-9AC1-2C6E6685FC90}">
+                <File Id="filCC2E6195FB7078E99066C2C1AFC2E8EE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\SwipeDelegate.qml" />
+            </Component>
+            <Component Id="cmp4EAF90EE5A418B31FD73F5715EA24DE0" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{96BD4673-8AF6-4091-8F4D-17DB740F6153}">
+                <File Id="filF3694E8DA7CF82CD09FDEF364F0B670E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Switch.qml" />
+            </Component>
+            <Component Id="cmp12F9CC77E0C03C07FF302D5D7BA23AD5" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{71743AD0-4AC6-41BD-AC18-11C32B7214F8}">
+                <File Id="filB0E07FB192EF460722852FFE7407DF82" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\SwitchDelegate.qml" />
+            </Component>
+            <Component Id="cmpBDBF2411D4F7E49220B2BCF845E99AA5" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{36C0142C-8ADA-4952-8D76-A59FBC55EBE4}">
+                <File Id="fil3DB068D0F1619CC321E92AC31651AD51" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\TabBar.qml" />
+            </Component>
+            <Component Id="cmp1F9F5B5C7855DEDC823C34BC1A8CC6EC" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{59C0757D-C2D1-48E8-A059-335F381077DF}">
+                <File Id="fil55391B4C477CB9D40328A11E56930971" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\TabButton.qml" />
+            </Component>
+            <Component Id="cmpE9E2E248248D424BFB1DC6CA31BC27D0" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{24D267A4-83FD-4A51-81A3-C63D7FBDD5D3}">
+                <File Id="filFFC17C428A0FAED302DE985D00A846C1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\TextArea.qml" />
+            </Component>
+            <Component Id="cmp3FD867BC1A97A8D0378B3C8DE43B974F" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{11C01B9E-FFC1-48EA-ABD5-D13BC7A87F63}">
+                <File Id="fil0D775F39F24A63854675B5109686CD47" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\TextField.qml" />
+            </Component>
+            <Component Id="cmp6E54963F3B620EBC67E5F63FD718977D" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{BDB38F3C-9AAF-49D0-BFF0-51B0BF0D3EB6}">
+                <File Id="filA23504C4849E7A6ADE28FAD856C5F859" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ToolBar.qml" />
+            </Component>
+            <Component Id="cmp31447F7164FAAB2B0C95D9AECBEF31B0" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{28318173-6475-410C-AEAA-9A8C00E939AE}">
+                <File Id="fil3ED20D130D8B21E93E817825A0936FB9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ToolButton.qml" />
+            </Component>
+            <Component Id="cmp984F077A561593958287F1B765CFFE25" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{5C0D5082-E415-46E1-AF59-8F47B83134C6}">
+                <File Id="fil926D6452C56D308D9C80B02B00608E66" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ToolSeparator.qml" />
+            </Component>
+            <Component Id="cmpD76004E2F76A20F78C7300677C61D0FA" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{D630F414-AD57-40AA-931F-139D9E6BABA7}">
+                <File Id="filA3B34BD4D8D009E22A6B01C98AA9EF25" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\ToolTip.qml" />
+            </Component>
+            <Component Id="cmp644DC3C8747E6DB3DCDBA429C9571ACC" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{2F3DAD80-3231-4316-9641-85EB2A9A88B3}">
+                <File Id="fil8A4F194DE6EDCC255FF25B054DAEFF2D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\Tumbler.qml" />
+            </Component>
+            <Component Id="cmp2E6AE1BC29B54991354BB7D83580A121" Directory="dir2B55988B75845F194F607E4ED40BB41B" Guid="{6666EB71-7F13-4280-9967-2729581E8089}">
+                <File Id="filF9865D03CD55A2D3C9E1F7A645886DFD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\VerticalHeaderView.qml" />
+            </Component>
+            <Component Id="cmp2D4D3039A8F4786B29CBC59279E11CD6" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="{BEEBAA2E-2365-4AAC-86E4-B81B12832966}">
+                <File Id="filD93A1F8A66A03948D28EB10C08DBC075" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\CheckIndicator.qml" />
+            </Component>
+            <Component Id="cmp11DD90574BA55792E09E85BC875E7CC6" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="{E03E316C-5FC1-4DA9-8840-2E41D60F91A0}">
+                <File Id="filDCB41B677B2078FEF88ED999B8C5DE52" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp8B74A6DA01445238C3DB5F8302852C5B" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="{CB2EF16B-DA62-4C75-A15B-EF6022DD6204}">
+                <File Id="fil5F817B95E18EA3903E5E492507037529" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\qmldir" />
+            </Component>
+            <Component Id="cmpACCD6EDBF6360766EBEC3D0A26FF4485" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="{397E8627-68B3-4969-AA81-59D633C14184}">
+                <File Id="fil00B2852292FF266BFDC78E27246BDF0E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\qtquickcontrols2universalstyleimplplugin.dll" />
+            </Component>
+            <Component Id="cmpE2D975039020B0A28962D1B925A960E1" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="{EE5E5754-45D2-4681-94F5-72FDBE585988}">
+                <File Id="filCC47BCAA474BE0F9328DE3C9C733C42E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\RadioIndicator.qml" />
+            </Component>
+            <Component Id="cmpC300DE4CB804EF265FD4219B34BCA1C3" Directory="dirBF720220167083D5C665DD0D131194BE" Guid="{C76B72E1-0EAA-4465-A2AE-26F6B7015933}">
+                <File Id="filA4283A2AAA72A0BAD305878F149F8112" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Universal\impl\SwitchIndicator.qml" />
+            </Component>
+            <Component Id="cmpAAF28E9A2579CFC59E2032E5D93E1329" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{AC4BB879-D6FD-4045-9519-281BEF81A859}">
+                <File Id="fil882894436C42BA03390013190DA428DD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\ApplicationWindow.qml" />
+            </Component>
+            <Component Id="cmpC40578D06BD93FA899E895CEF21DBA18" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{8ADFB105-88FC-4007-B2A9-67DAA801A8DB}">
+                <File Id="fil2AA977FB2D945038560ED99BC2DBF037" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\Button.qml" />
+            </Component>
+            <Component Id="cmpE68756474722B3F21FC5532D1062FB1D" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{87B337BC-B684-4E21-A7DC-44654F236C27}">
+                <File Id="fil83C17F6A6B9AE52616089E09065D4409" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\CheckBox.qml" />
+            </Component>
+            <Component Id="cmp3296BF97A2E0EF541BB34170D2415F01" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{9CA20EB9-A718-4F5B-BD78-C942B464D874}">
+                <File Id="filBCD6667BB8BBFA9456E666B4E33AB0B1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\ComboBox.qml" />
+            </Component>
+            <Component Id="cmp8C1B3E8BDCD066E43D0D7711C6230BF0" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{609CC4D5-3B31-49F2-B9C4-64D66B4624C5}">
+                <File Id="fil743C0CB7BC821AB1DE5E10195D4DD7F9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\Frame.qml" />
+            </Component>
+            <Component Id="cmp7837CC5D7098081B4DF4FF165458173D" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{4260D2BD-A5B7-4A95-A241-516C8847C29B}">
+                <File Id="fil71972C76D5C2B98132AF5B318815F12E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\GroupBox.qml" />
+            </Component>
+            <Component Id="cmpEDDCDA25E43BFDE157B14A1304F2881F" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{8E82840D-4688-49B4-8D5D-6DD5B32D9290}">
+                <File Id="fil276159053BE57D0C24D41C849C2BAE91" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpD23B38A078CFEA9D72897D555FDE6F99" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{C7360252-1A47-4909-8983-FA5BBFBB3EEE}">
+                <File Id="fil2DE71BEB259AC59FA0C9A0D2284808B4" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\ProgressBar.qml" />
+            </Component>
+            <Component Id="cmp3C6D627ED97D630610BE717D30D0D763" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{CE220391-1E05-4F5E-9625-FF52569E61A7}">
+                <File Id="fil97BA32DCDFF1EDD5C462E0E34667C383" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\qmldir" />
+            </Component>
+            <Component Id="cmp8F960D4B5204B9EED233F6E3F8B827E2" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{F2F16A8A-2153-4952-9AC1-A9EB352C0DF0}">
+                <File Id="fil4B2F046B200837613E9C6D66BB296674" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\qtquickcontrols2windowsstyleplugin.dll" />
+            </Component>
+            <Component Id="cmpC77F62D1E6B5F70004BAB55B363F9B11" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{F5215D39-719F-4305-B165-ECB1704B9B25}">
+                <File Id="filAE22C9B5BA41390AC226359F794FC06C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\RadioButton.qml" />
+            </Component>
+            <Component Id="cmpEF137D0A83F3A18031FFD79BFE04B9F2" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{86874DED-ADB1-4AB8-9348-72A7FEA8E08E}">
+                <File Id="fil685FF36668F4A89EF52829644479A47D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\ScrollBar.qml" />
+            </Component>
+            <Component Id="cmpE22CC5E1ED8DF1A9868758098747EEFB" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{59BD9DE6-C820-4AB8-BE92-82F515C134B9}">
+                <File Id="fil95C622A6C8EB1C2113128E62F1806D13" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\ScrollView.qml" />
+            </Component>
+            <Component Id="cmp87AE5D4D01002471473080DA66B6CCC0" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{E868715A-FB85-4268-99D1-81FFD010FB21}">
+                <File Id="fil8844CE9FFA700D403EBE27FEB1225120" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\SelectionRectangle.qml" />
+            </Component>
+            <Component Id="cmp87BE1C51D8DE5CBD247E1A8892F70F82" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{45934537-3238-4E93-ACC4-3E5157EBBB8B}">
+                <File Id="fil9887F9433050ECE31852BEB260381494" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\Slider.qml" />
+            </Component>
+            <Component Id="cmpDC4F51A305FA129D657009601950B7A3" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{3FCC08CF-7ABD-4B1E-8711-76B69295AF17}">
+                <File Id="fil00A561FEF601BE682473E1310E50E3DB" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\SpinBox.qml" />
+            </Component>
+            <Component Id="cmpDF55645037117499DE465427DC8DF07D" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{2FC0A47A-0E3A-4109-A691-0135CF115119}">
+                <File Id="filF35FCE92DDE8D63D8CFC96EEDBF8F66C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\TextArea.qml" />
+            </Component>
+            <Component Id="cmp2203A2072E1F1CCF26557C70F67B6390" Directory="dirB030248D75EA99E75698A1E011FE9CC0" Guid="{C3DCDE95-125D-449B-AB6E-0A46F5184BA6}">
+                <File Id="filE88281D540F6A3D22F1B131E68180E08" KeyPath="yes" Source="SourceDir\qml\QtQuick\Controls\Windows\TextField.qml" />
+            </Component>
+            <Component Id="cmpC437AB325C956B39F47527216D28546B" Directory="dir3390D3B4341886F0DA85EE3BA92439FF" Guid="{423D4EAD-FCE4-4E50-875B-6EB326325B7A}">
+                <File Id="filFC0B5707CF45175E2548BA04998F9D14" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpFD0F8FF1618CF94939B70BB1D4874BD5" Directory="dir3390D3B4341886F0DA85EE3BA92439FF" Guid="{CD7FFA98-9FFC-4EF7-8DDE-D3CE291FDD65}">
+                <File Id="fil326E1B5A831353646A2BA9A334652C8E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\qmldir" />
+            </Component>
+            <Component Id="cmp19505C3234633AD6D7F2A9FF634021A4" Directory="dir3390D3B4341886F0DA85EE3BA92439FF" Guid="{BF7BA44A-5DC2-4FD5-8E4F-6EBBF2706809}">
+                <File Id="fil6CA3AB34BE81C0480807D319DA4D1B52" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\qtquickdialogsplugin.dll" />
+            </Component>
+            <Component Id="cmp26A12B2050D05AC837B4FE95D70CF62A" Directory="dir34D1D6742B95CCA4110D8968A4544048" Guid="{63AAB19B-A715-48A6-B172-DFB7BC3D99B8}">
+                <File Id="fil2EF39784619037E8CC60A1D82B089DB1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp247A562FE0339C0E33051AE46663C848" Directory="dir34D1D6742B95CCA4110D8968A4544048" Guid="{E2093578-5F74-40CA-996A-EAB881D7E4C5}">
+                <File Id="fil73A709C03D74D3472D379DD8D232B321" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qmldir" />
+            </Component>
+            <Component Id="cmpD771E4FD8559DD19611671F07F50AE86" Directory="dir34D1D6742B95CCA4110D8968A4544048" Guid="{28A55D2A-96F7-48B0-97FA-95150B57B184}">
+                <File Id="filF7DDABE25D647E77D24BC54ABB437481" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qtquickdialogs2quickimplplugin.dll" />
+            </Component>
+            <Component Id="cmp1F9B87E10A30CED8E63B585060A1FF8B" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{328E2F4C-9D60-42F2-8F2A-B703D89B2BDC}">
+                <File Id="filA0DF6F7D59B18B7A0A1AF7DA6AF2FF68" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmp5CC1A9822278627BA1C6F6C0810F05AF" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{3E34664B-49E3-4D8D-BA5F-8263B77D8EF0}">
+                <File Id="fil9352455FFAFA7537481F68705CDAB62A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\ColorInputs.qml" />
+            </Component>
+            <Component Id="cmpCD8E153BC6DC54FB225F0335FD2F1022" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{8C64BE40-2759-42C9-880A-5987F237B826}">
+                <File Id="fil93D2D8A8DBA12B61620EBAF03A9BE6D6" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FileDialog.qml" />
+            </Component>
+            <Component Id="cmpA43D16647F1AFE697DD7E77561269215" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{2D3AAD29-5156-4656-B22B-EA8978EB267E}">
+                <File Id="fil209068D9C33B43DA14DC86382904CBEE" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FileDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmpA5CBE15352FE008097BC876C40461A13" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{0BE9DEC1-610E-4064-9ED5-D0CF07BCA172}">
+                <File Id="fil4540A3A2DE480ED8F7FC352CFA54A0A3" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FileDialogDelegateLabel.qml" />
+            </Component>
+            <Component Id="cmpBA80190D493F665368F7F2D88009CA35" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{F4E3BD0D-669B-4C94-A67E-70932CA1C056}">
+                <File Id="filD4C93FE2E3ECA41F2B088C15EE8E7B44" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FolderBreadcrumbBar.qml" />
+            </Component>
+            <Component Id="cmp2B690A76E357815CD2278DADED472DCD" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{45BC2125-8EAF-4E5A-92B7-F180D1F61FD5}">
+                <File Id="fil0A290A43252CCACCDD06DC74AD3F0150" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FolderDialog.qml" />
+            </Component>
+            <Component Id="cmpD2D023E7908B3EF1FBC9EE76B53FF954" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{DA2D5115-A89A-49E2-9D8E-E0429CA7D1E4}">
+                <File Id="fil9048C8B68F95216847AB9C2BB586267B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FolderDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp3FD2DFA51DAE6BF8C71900CA48779DC8" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{677453BD-2E28-4147-8F6F-BA9C8695FAAD}">
+                <File Id="fil090B870ACCCB3E9AC35C9BE4A09AC383" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FolderDialogDelegateLabel.qml" />
+            </Component>
+            <Component Id="cmp89D2DEE36502469CA5E9442966437A3E" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{928E1916-1B33-484E-82D8-18E2B52D5EDD}">
+                <File Id="filB7007AC91E08C3AF1B5D1FB0BF4E0D70" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FontDialog.qml" />
+            </Component>
+            <Component Id="cmp23ABBE7C0C0AFCF242C44EDCA1D7A2B5" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{319256D6-A52A-482B-8209-0DF3C3DC72F6}">
+                <File Id="filBA3B6DBDD6BF053B67B4C49EEC242FCA" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\FontDialogContent.qml" />
+            </Component>
+            <Component Id="cmp0B0F513E9ED50F545C66F3A24CC92F08" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{577AF665-C115-49C7-916D-A4E8A73A3A84}">
+                <File Id="fil65FEDAF25F1E1AC39DC10AD70C88DAB8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\HueGradient.qml" />
+            </Component>
+            <Component Id="cmp41298595F02EF32949F1566F589B454C" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{9F15C0D0-2888-47CC-AB24-F11D538C2B78}">
+                <File Id="fil767F03E07565A9F69AF8C06A5EBADD4E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\MessageDialog.qml" />
+            </Component>
+            <Component Id="cmpF435AE4D59420FEEB320C8B81458D76C" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{F0988C83-68DF-45B3-AB03-0E17D6CD9D34}">
+                <File Id="fil85F4300A5F660E5B427FFD02F328EAF5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\PickerHandle.qml" />
+            </Component>
+            <Component Id="cmpC4C42A5D0D070431E5C9FEED80365921" Directory="dir755D907146AC4C2DC7E3BE88C0F1A70F" Guid="{669D74BA-A25E-45E3-99B9-9BCBD2CAD9F7}">
+                <File Id="fil67AAEC8993AE5CDEFC014103CC45F500" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\SaturationLightnessPicker.qml" />
+            </Component>
+            <Component Id="cmp69A36B9C73762AA365C1BC5F970FC8E5" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="{48AA48DA-1102-4ED3-8620-62CA6E74A5CE}">
+                <File Id="fil45A808D742A78F7A926AD621F7283728" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmp7BBF58B50638FEB770B04C7F73171AC6" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="{6FBAEF6C-ABAE-4E97-A57C-9D748B9F9C6E}">
+                <File Id="fil7153DDC4143EAC85FD70680E09CE21A0" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FileDialog.qml" />
+            </Component>
+            <Component Id="cmp773DC90FC00B0677B841101198BF6921" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="{1CC59C1A-29F1-490D-B1BA-1E0C4BC1DFE0}">
+                <File Id="fil87F4823E5A4E9CAD89C9F18EB7D940C2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FileDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp93F9CB69ACD380F7D59D08819255322B" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="{CC385A86-C88F-409C-ADE0-7BCF8DD46C78}">
+                <File Id="filA3DF900A1D2C6A4D414885D446B5C70C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FolderBreadcrumbBar.qml" />
+            </Component>
+            <Component Id="cmp29403D08CB444131141FF50C5977F801" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="{DC93624A-8F0D-47FA-BDB0-AF8A0781BBAC}">
+                <File Id="filEA8B9CAEBE85B9B32212D7D7AC61E3C8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FolderDialog.qml" />
+            </Component>
+            <Component Id="cmp0F3406384273F4D43A9ECCF138617EFD" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="{6ABEA4A4-8047-4A2E-BBDD-60F9284879C0}">
+                <File Id="filE73DB30D303712F991084FF0EEB941F5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FolderDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp1AD58D86AC860A01A93BF5C9248B5E0B" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="{94387038-5A74-48DA-8531-714A1BBFED7A}">
+                <File Id="fil36CA66A25F1D4A60DCA8851C04360F5A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\FontDialog.qml" />
+            </Component>
+            <Component Id="cmp2E10228540CA4421B670586FA6F602D4" Directory="dir929495D16D3C46BFD28C6AD595BCD701" Guid="{EAA4236A-7D79-420D-87D6-28ED23D6460A}">
+                <File Id="fil103C6B50716464DFCFF67BCDF19D268F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Fusion\MessageDialog.qml" />
+            </Component>
+            <Component Id="cmpB06D35D731CDFFF625B04E79D8E88BB4" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="{3E057F8C-DF84-4681-BA6A-9562C0790EB5}">
+                <File Id="fil75D5FBD826D73921FABD447701DE184A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmp53496C379F6D669E9DC8EE0F3AD3E17E" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="{3150C6F6-6842-4B88-8DB9-9668D9EE0678}">
+                <File Id="filD95B88F29ACA98EBCB5B86D8F5FA2505" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FileDialog.qml" />
+            </Component>
+            <Component Id="cmp94CCFDA09967CE02B01DF2FD85424408" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="{805E967C-B9F5-48B9-8C87-7AA0E168B837}">
+                <File Id="fil1939DC9854DE81CAD586E8EA8C9F11A5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FileDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp28FD3CFF63249E9A333566E0D4403C06" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="{CF289DEF-97C5-4276-A5AA-759B1C43A151}">
+                <File Id="fil540156639772798B8D6CF3832BA6CEFF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FolderBreadcrumbBar.qml" />
+            </Component>
+            <Component Id="cmp8FB0248D377F04FE065ED8D3F0956920" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="{ECA74581-7195-4E89-9793-E0543CB65F84}">
+                <File Id="fil1B2FC023BAB032527FD158332D7A31A1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FolderDialog.qml" />
+            </Component>
+            <Component Id="cmpEB30E27FA4A0E15D8110F522E6E24AB9" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="{3BE4CCBF-C42C-4BA3-A90C-5F9D7095D942}">
+                <File Id="fil4C6AFCFFF15FA8BF18BBF16D8D04263A" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FolderDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmpE831C08038ED1CE79CB86EA78782321B" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="{697E0CB1-6C86-46C1-9AF7-52E1A27E148D}">
+                <File Id="fil49DC57941DAE9D048E67CC9D4037E20F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\FontDialog.qml" />
+            </Component>
+            <Component Id="cmpC9A55BF0CB886578DBBE11A05E82098E" Directory="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Guid="{9F6224D1-093E-491F-9D48-2F4F8C48AB5E}">
+                <File Id="fil4366D0DAFCA17243777D0FC31A6D7CCF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Imagine\MessageDialog.qml" />
+            </Component>
+            <Component Id="cmp0ABC2788AE2935B5CB49F7CFC3447AE7" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="{24D9FA8D-6FE2-4501-AEE0-F3B9D673FA69}">
+                <File Id="filB61687597342BBBABAE1E94101F3EC6E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmp1B66918327259E9332768B2CEDF63F3F" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="{31BF28D2-0A26-4183-832C-3E04A9860196}">
+                <File Id="fil62168DF788419AC877C9C1D54BABA7E2" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FileDialog.qml" />
+            </Component>
+            <Component Id="cmp7D2F50F1ECA2513B10F2D9E268603803" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="{3D69E07B-58BE-4042-B7A2-EBA806B429D8}">
+                <File Id="fil5909CA77FA8E0522A5ABE19D201D339E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FileDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp54231F7DE8257F6F679C508C4A324029" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="{BFED6612-1F38-4694-84B1-4ABBE2D3EC74}">
+                <File Id="fil066B22B57C4829B076EF2637C8369609" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FolderBreadcrumbBar.qml" />
+            </Component>
+            <Component Id="cmpEBBD3D3A57C898928DA40D237B8191AE" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="{C1A5A501-5258-4007-8304-8EE952E74938}">
+                <File Id="fil9364C24A62A278E0DF4A32219457A0E7" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FolderDialog.qml" />
+            </Component>
+            <Component Id="cmp4CFF955FC6C7A59A9C3686F6EA07DEB2" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="{175127CE-5DA3-4EFF-BA4F-72B09E25F44D}">
+                <File Id="filFAB35638D14AAE200469B556D0704257" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FolderDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmpE18DF027A0BDBB96DACB63898F697766" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="{20DB0640-120F-4BA6-A307-9111CE086AF0}">
+                <File Id="filDA5275126CA3326219C5633BBAF43DA5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\FontDialog.qml" />
+            </Component>
+            <Component Id="cmpFE745526AAA642BA836DA29C9F77ED9B" Directory="dirC980C9942B69E6635E4CF7C57652D12B" Guid="{DE8767C0-F5D4-4FFE-992A-08F3E4B0107D}">
+                <File Id="fil54D49371701AEB3AD847CDD309D9B185" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Material\MessageDialog.qml" />
+            </Component>
+            <Component Id="cmpD4B5BFCAFB6DC32B78A6C9AC75762ACC" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="{972FC60C-F8D8-461B-BBFD-7DF6E7086DF2}">
+                <File Id="fil4D10401F2E4EA015EF5CE8BEA0AE7121" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmpC67BC79E14952488E29CF9E27BD202FB" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="{2E2F47EC-9897-4C8E-8B29-7BFE9712BCFD}">
+                <File Id="fil35DAD56258028832AFB445B0E0263B84" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FileDialog.qml" />
+            </Component>
+            <Component Id="cmp9F8F91188C1B368A7FE7E51517C5ACC0" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="{27628965-E859-46D5-B9D3-18142C281F2A}">
+                <File Id="filCCF6364980A7F563A9642915E70CB32B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FileDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmp24B89BE2A868B73CC480D3F15E9AA30B" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="{9C323147-D535-45F2-B677-56AA821A240F}">
+                <File Id="fil39DD65F761C1D67E92728B9250518B4D" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FolderBreadcrumbBar.qml" />
+            </Component>
+            <Component Id="cmp48D9FBFB9413B749E8E5568D27D9E7BA" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="{F1D9031B-220F-4833-8BB5-93B8463B2E8D}">
+                <File Id="fil620700542B96A19799BA7151FA3C37CF" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FolderDialog.qml" />
+            </Component>
+            <Component Id="cmpC5B074A0F5386F436C0457318AEBEAC8" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="{874D7476-7737-47B9-A494-A9EA4DBFED08}">
+                <File Id="fil26FDE36A980B8A98EB003E08B71AAAFD" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FolderDialogDelegate.qml" />
+            </Component>
+            <Component Id="cmpA671B669F6A248C1F3648B5295D227F2" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="{095D7121-4B02-4550-8D4C-E81FE6E6C015}">
+                <File Id="fil158685A04206B07FCFB74FDBDD3C4579" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\FontDialog.qml" />
+            </Component>
+            <Component Id="cmp388F3D53B11EE0449503AEBC67192D85" Directory="dir8C55A2520B05F605E480B8F2F2039F1C" Guid="{8F932E16-3B5C-4B90-8049-90498387825E}">
+                <File Id="filF4BAFBB95A2A38BD425F65092CB041B8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Dialogs\quickimpl\qml\+Universal\MessageDialog.qml" />
+            </Component>
+            <Component Id="cmpED7844544FD113D2024E6D3A0A781CAE" Directory="dir7397A50E4D7D313615C4CF7C94C61160" Guid="{3999B85C-6807-4735-92CF-D5976160ED5C}">
+                <File Id="filDB53B5FD7BD0B0F1464E81D6A058D610" KeyPath="yes" Source="SourceDir\qml\QtQuick\Effects\effectsplugin.dll" />
+            </Component>
+            <Component Id="cmp2564A9FB9971981965C804A9765373B9" Directory="dir7397A50E4D7D313615C4CF7C94C61160" Guid="{E315BDE1-9966-4BC1-8CF4-4AED0B2AB05B}">
+                <File Id="fil747AF0BECF09264818C8DF354CF6A40B" KeyPath="yes" Source="SourceDir\qml\QtQuick\Effects\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp6895F64D8B98A38B0AE2C8F5A310BE47" Directory="dir7397A50E4D7D313615C4CF7C94C61160" Guid="{304F1CB2-E273-4EB4-A885-B44C7191EBFD}">
+                <File Id="filF8BAD332FBA7B0262015FEF686BF25A5" KeyPath="yes" Source="SourceDir\qml\QtQuick\Effects\qmldir" />
+            </Component>
+            <Component Id="cmp690AC8265BC3CC9BA6C7C6DBD3E29F84" Directory="dir7EBA39754BD2CE291EC25825433A43D2" Guid="{2DDAF948-7474-4682-BD00-A3C78BC9C20C}">
+                <File Id="filAA4FB44B93EF08B6742B2481420E8468" KeyPath="yes" Source="SourceDir\qml\QtQuick\Layouts\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpC6E50C98991E0EAA0E3A4D95C7D4F3DB" Directory="dir7EBA39754BD2CE291EC25825433A43D2" Guid="{42A736A8-E59E-40A7-A35A-8BCB4AB7EC88}">
+                <File Id="filB30B70CC7D54752ADC0401EE99EBCA64" KeyPath="yes" Source="SourceDir\qml\QtQuick\Layouts\qmldir" />
+            </Component>
+            <Component Id="cmp4260C68CD0F932092808D3942F80E172" Directory="dir7EBA39754BD2CE291EC25825433A43D2" Guid="{6210CD29-AEE9-4A68-916C-B7B8A3EA3EBF}">
+                <File Id="fil11D74BAB954F6060FE25421FF4DF2D92" KeyPath="yes" Source="SourceDir\qml\QtQuick\Layouts\qquicklayoutsplugin.dll" />
+            </Component>
+            <Component Id="cmpD7C080A5C7175B15B438D5E4851B9267" Directory="dirA6F6BC40CF508C5F92BB49483928E0FC" Guid="{C3694BA2-F15C-4396-90D8-C384C67F5367}">
+                <File Id="filDE9CEDD02770FA8D5A6B443C92AF13E2" KeyPath="yes" Source="SourceDir\qml\QtQuick\LocalStorage\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp6F365EE63059CFBDA7386B8F1B4B7E73" Directory="dirA6F6BC40CF508C5F92BB49483928E0FC" Guid="{4CAC376D-A372-4AEB-83E1-903A9A015526}">
+                <File Id="fil19E5C5D82A58C106C10F4EF71FBDC561" KeyPath="yes" Source="SourceDir\qml\QtQuick\LocalStorage\qmldir" />
+            </Component>
+            <Component Id="cmp56020DD868F550FF4C2761D9FCAF2820" Directory="dirA6F6BC40CF508C5F92BB49483928E0FC" Guid="{50781D85-0D79-446C-ADF7-AEF6B6324E71}">
+                <File Id="fil373F860BFC86DCA9B91B06DE6B8312C0" KeyPath="yes" Source="SourceDir\qml\QtQuick\LocalStorage\qmllocalstorageplugin.dll" />
+            </Component>
+            <Component Id="cmpE6642776D930163E5B3F23D206815BB4" Directory="dirF7455B984D3CF2564DED2759788752A0" Guid="{C1F45943-5DF4-46B0-9C63-B2E15A012243}">
+                <File Id="filE1D8B4CEB7BAAD320C44F6CD0D7101AF" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpF080C580F3B4157111B93C9666E6557C" Directory="dirF7455B984D3CF2564DED2759788752A0" Guid="{5AD2FB33-4BB8-484E-886F-EEF41EB03E4A}">
+                <File Id="fil7693DA2416BEB023863E57694682496C" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\qmldir" />
+            </Component>
+            <Component Id="cmp1052E5F49C616A54DF531B8121F3D962" Directory="dirF7455B984D3CF2564DED2759788752A0" Guid="{7D231850-64B7-452D-AE5E-458968F5D6DE}">
+                <File Id="fil4B9E63D0E3544A93D3C93F417E63A2D7" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\qtquickcontrols2nativestyleplugin.dll" />
+            </Component>
+            <Component Id="cmpC9C0D310B3B9C47633E52904919A68A3" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{BFF61EA7-1CC6-4246-82E9-6D4E2E90FC5C}">
+                <File Id="fil4E74AF38A57B5DC51A0C8BAA7FA0248A" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultButton.qml" />
+            </Component>
+            <Component Id="cmp8A89A151469884206970043CC9CF4D86" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{F9E13138-F84A-4A0E-A101-1D1976C4142C}">
+                <File Id="fil8ABD292511F2D365661FA57136628A97" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultCheckBox.qml" />
+            </Component>
+            <Component Id="cmp998E8AF42C18DDD09BFF57E42546A8D5" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{77651B30-C65B-4363-82EF-A52A5F69FC3F}">
+                <File Id="filCAF57E8E6C8CC0D9A2780CDD88AD462B" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultComboBox.qml" />
+            </Component>
+            <Component Id="cmpD499A28F8E9F6FEE97F97777DA6CB3AE" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{BD1933FE-0A92-4AF8-99FC-4D2B2E89FF03}">
+                <File Id="fil8B58BB6E6F240EEB5AB919B5E8FF3FCC" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultDial.qml" />
+            </Component>
+            <Component Id="cmpDBDF7121809130D7416E032EA09C16B0" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{A1658F78-523C-48F9-9234-ECE710E1EDF2}">
+                <File Id="fil410E97B463184F448D3625740C057F2B" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultFrame.qml" />
+            </Component>
+            <Component Id="cmp2948DD4EC5076256EF0BA0FE75947821" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{1164C747-ED06-4891-B5B7-D2D2B57624B9}">
+                <File Id="fil514160DB4A8D6516426012B1E5DCA0C4" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultGroupBox.qml" />
+            </Component>
+            <Component Id="cmpBAD2FDC63EF91AF984D6977B2A9641A7" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{1DE5E90F-1543-44AE-AF50-710C30878686}">
+                <File Id="filC20F91CF453A8F4B5F75C95AADAFFDF4" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultProgressBar.qml" />
+            </Component>
+            <Component Id="cmp6504A26398241286FD87575D3DDDEE30" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{415D1FFC-2833-43B8-BDF7-B947F076AE0C}">
+                <File Id="fil82D83C5E4CA154FA270B4DA26EEB3BA6" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultRadioButton.qml" />
+            </Component>
+            <Component Id="cmpF53E23CCF650110414BF5121E32F5801" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{B6FF0EE3-BA67-45C0-8626-CC1B885ABC9D}">
+                <File Id="fil203E12EF4C14B14DFCDA757CA082B189" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultScrollBar.qml" />
+            </Component>
+            <Component Id="cmpC43D383FA22807D352ED92706A3B7BB5" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{D1FD9DD4-4BC3-43C3-8CD4-7885EAF5C33D}">
+                <File Id="fil336CE1259605D5F6DD75031302D1D172" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultSlider.qml" />
+            </Component>
+            <Component Id="cmpE1F0EBE68EDB43973B9E52D9C0F96504" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{0F889D31-D05F-4951-BADE-1855290932F4}">
+                <File Id="fil05CB3CD7C420EE0D632AB36B001DDC11" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultSpinBox.qml" />
+            </Component>
+            <Component Id="cmp8D7D9681C26F0A7B52E3C668EA4BF45B" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{475F3D95-92FE-4D21-A51C-CF14DED25C30}">
+                <File Id="fil6324820B417FACA7B03D7631F502BF76" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultTextArea.qml" />
+            </Component>
+            <Component Id="cmpF20525D1576510B72940A1B3A2B872E9" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{DA00649A-32EC-48E5-A2F0-4386BA48ACD1}">
+                <File Id="filE190EBA3C1D870E6CF7BCBFDC2EF4B9F" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultTextField.qml" />
+            </Component>
+            <Component Id="cmp5C615FF3D38B0DAEA6E54192A348A499" Directory="dir3A2097370D43A6BEED9BF1FBE00B9935" Guid="{2396D282-E796-44DF-ACE2-956B3A75F412}">
+                <File Id="filB2DF42D1E5EA322F47250AD21CF4E4F5" KeyPath="yes" Source="SourceDir\qml\QtQuick\NativeStyle\controls\DefaultTreeViewDelegate.qml" />
+            </Component>
+            <Component Id="cmp18CB7E4808B561BE6CFA935B7A701259" Directory="dir2105A1F7199CAE1171E138FDED1D8E49" Guid="{4D3B1A28-867A-4687-9521-A94B79D36EDB}">
+                <File Id="filAF591582B9EB7D4C0EF0E76AB1774FE1" KeyPath="yes" Source="SourceDir\qml\QtQuick\Particles\particlesplugin.dll" />
+            </Component>
+            <Component Id="cmp4F76529422F947A88380E6330781E9A8" Directory="dir2105A1F7199CAE1171E138FDED1D8E49" Guid="{2C798851-B87F-46AA-B907-61F2AFA60733}">
+                <File Id="fil12011A3B6C5641C4DBB765FE3972890F" KeyPath="yes" Source="SourceDir\qml\QtQuick\Particles\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp79A247854241165CFF0438015FC92CBB" Directory="dir2105A1F7199CAE1171E138FDED1D8E49" Guid="{FFEC7B0E-C5F3-4005-97E4-F01BDD023AD5}">
+                <File Id="fil5CFE0F9500667BFD3E26D27A494DA78C" KeyPath="yes" Source="SourceDir\qml\QtQuick\Particles\qmldir" />
+            </Component>
+            <Component Id="cmp451BE08037AE1075B3D0480F83957794" Directory="dir96881D4937C7E7E8DC6A06696EF52C96" Guid="{F218CBB6-2DD5-41A1-9F56-1826C80A2C4B}">
+                <File Id="filA5E21912CAC2144F8A23EB7C07A64E50" KeyPath="yes" Source="SourceDir\qml\QtQuick\Shapes\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp8068B6605CFFD06999D8AD47565E2117" Directory="dir96881D4937C7E7E8DC6A06696EF52C96" Guid="{8BBDE301-E82A-4CE8-8B9D-D1BA5AEAE153}">
+                <File Id="fil13979215E10E260593D3F3CC875D614E" KeyPath="yes" Source="SourceDir\qml\QtQuick\Shapes\qmldir" />
+            </Component>
+            <Component Id="cmpC06754C3C30F7E84E5F94A00CBC4BF8A" Directory="dir96881D4937C7E7E8DC6A06696EF52C96" Guid="{D02959D7-8ADC-40D6-A4E7-E893C12B28D3}">
+                <File Id="filC889B1860D61C95441338B48F27D42E8" KeyPath="yes" Source="SourceDir\qml\QtQuick\Shapes\qmlshapesplugin.dll" />
+            </Component>
+            <Component Id="cmp60DA059DBD878BB08B9E9EE991277BD1" Directory="dirA16BBE5DE11DF2FD79B07E2965737D3E" Guid="{21F373E7-09D8-484B-B4E0-9F7102E14EF5}">
+                <File Id="fil57F3B26780163C8F4D0ED41B413DC849" KeyPath="yes" Source="SourceDir\qml\QtQuick\Templates\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp68A90E74F6C13409CCC0C76BF2ECC1D0" Directory="dirA16BBE5DE11DF2FD79B07E2965737D3E" Guid="{A6AC0227-4E89-436B-9934-87FDAC87B27A}">
+                <File Id="filC14F61F821BEACCCD681664F76075405" KeyPath="yes" Source="SourceDir\qml\QtQuick\Templates\qmldir" />
+            </Component>
+            <Component Id="cmp6B89F1FC6840B89D467D2EBE8B043FE1" Directory="dirA16BBE5DE11DF2FD79B07E2965737D3E" Guid="{930C0CB5-3767-4247-A5D7-8A385FF0344E}">
+                <File Id="filED4C60CE9B81695ECE44D214DCBCCE02" KeyPath="yes" Source="SourceDir\qml\QtQuick\Templates\qtquicktemplates2plugin.dll" />
+            </Component>
+            <Component Id="cmp69BEF3BE02352CA6B785101AAED71318" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="{0EDF30C9-0CCB-480C-B957-604C01BA559B}">
+                <File Id="fil1760F73BD6E3E7286EB8F1D3A33C2073" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Component.qml" />
+            </Component>
+            <Component Id="cmp10D98830533C42F8706977851B39A3A0" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="{7AC6A3ED-EA58-47A6-8AB3-BD7FA42C145E}">
+                <File Id="filBF176D7C05C63830AFF6998ABFB38A7B" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Enum.qml" />
+            </Component>
+            <Component Id="cmp15249FC563E9C8918952F7D1A2F805C9" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="{4735B6BA-B771-4BAE-8C6C-C103E6B43A00}">
+                <File Id="fil2CAD05441DC803A5186E966A1DDA48C6" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Member.qml" />
+            </Component>
+            <Component Id="cmp4833C19054EF2E14E372AD2880495C73" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="{A478AE0F-CF66-48F9-834F-F2C3724B6871}">
+                <File Id="fil43B1B5A91FA45D5C106A45D805FEFC40" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Method.qml" />
+            </Component>
+            <Component Id="cmp33AC2E102B879ECCC6316EF7AEFDBB99" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="{29E6646A-D2DC-456F-B002-9874DAB30B8C}">
+                <File Id="fil95F6CB39A7A168DA09B6ABC8A860B095" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Module.qml" />
+            </Component>
+            <Component Id="cmpD956362F874FB96C3A0DFCADA1405E4B" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="{E6D431BC-F544-4043-9220-D72E1095259A}">
+                <File Id="fil12BB133642A6251AFE56340C20C38432" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Parameter.qml" />
+            </Component>
+            <Component Id="cmp05C5F4FC22BE05FF20AF6B70F2648E30" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="{21E2A211-764A-4061-8B3C-829FC6DA1E6D}">
+                <File Id="filC998D7BD8B71F6CD05288B0CF1CE0D63" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Property.qml" />
+            </Component>
+            <Component Id="cmpA5F05A0EC98A80D47E263C8226347F09" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="{73B849AB-C9FB-4C1C-8FBF-FB6F5419BFFB}">
+                <File Id="filB09D42B54721DA6F6C88B761F67D1CD3" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\qmldir" />
+            </Component>
+            <Component Id="cmp69C56DED012A8CF7146BB347775E1111" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="{C4C7A555-20F9-4CF0-BAAB-85323CF95C89}">
+                <File Id="fil2C39EC74EEBD3E3FB173F25F6474B71B" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\quicktooling.qmltypes" />
+            </Component>
+            <Component Id="cmp8CF4D885EDBD87E92F14AD9224A64585" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="{9EDAA25D-41BD-4154-9191-AD91477441BD}">
+                <File Id="fil619C9C3A532439F43969D239FC47574E" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\quicktoolingplugin.dll" />
+            </Component>
+            <Component Id="cmp40C935D75C83A908961342FA5F190AB4" Directory="dir2C126EEEB46329DB6052C65B78F1DA75" Guid="{2E64BF9B-123A-47A8-A4E3-93081043E53B}">
+                <File Id="fil9A187523ABDC241490F213412E806915" KeyPath="yes" Source="SourceDir\qml\QtQuick\tooling\Signal.qml" />
+            </Component>
+            <Component Id="cmp3E281B1E4C74C596886969A2CCFF6DB0" Directory="dir27DABF0F34E48010C38FDC22DEE2453A" Guid="{7AA23465-D8C4-45CE-851F-89DCD7337756}">
+                <File Id="fil620C826946D01508C458662B95E34005" KeyPath="yes" Source="SourceDir\qml\QtQuick\Window\qmldir" />
+            </Component>
+            <Component Id="cmp4C7449C9615F5B56C877F1DD796BCED9" Directory="dir27DABF0F34E48010C38FDC22DEE2453A" Guid="{B686AD4A-9F1E-4CEA-A2DC-3CFED9B358AA}">
+                <File Id="fil5C4310277F80C74CE8597AA07AADF489" KeyPath="yes" Source="SourceDir\qml\QtQuick\Window\quickwindow.qmltypes" />
+            </Component>
+            <Component Id="cmp8CAB2B6A300CCCBD332533B4BFBF9125" Directory="dir27DABF0F34E48010C38FDC22DEE2453A" Guid="{12FDD533-EDAC-4EBC-B3A0-5E5F592ACCD3}">
+                <File Id="fil8749130B368B4A583386390720832DC9" KeyPath="yes" Source="SourceDir\qml\QtQuick\Window\quickwindowplugin.dll" />
+            </Component>
+            <Component Id="cmpC680AB1168D76A008F6D481AF9C77950" Directory="dir30FC873BE970351C9459A1383DCBEA3D" Guid="{BE573507-98AA-4C17-84EC-BBFB9FB7F5D6}">
+                <File Id="fil64CB17C538790FF2F41160D864EE25E2" KeyPath="yes" Source="SourceDir\qml\QtWebChannel\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmp072B7F91297F86D3BDCFBD50CEADEC08" Directory="dir30FC873BE970351C9459A1383DCBEA3D" Guid="{E785466E-0037-4442-BB43-1AABD9C60E9A}">
+                <File Id="fil830FE0B6AE85ABAD33F965FA48D2A9D4" KeyPath="yes" Source="SourceDir\qml\QtWebChannel\qmldir" />
+            </Component>
+            <Component Id="cmpB9B4251EC70936E366F1ADEF4DBA9A35" Directory="dir30FC873BE970351C9459A1383DCBEA3D" Guid="{8E6F75FC-8222-46F4-92FC-8CCFE70EBE15}">
+                <File Id="fil2433013655774AC94A0236C340127486" KeyPath="yes" Source="SourceDir\qml\QtWebChannel\webchannelplugin.dll" />
+            </Component>
+            <Component Id="cmp136E2541355213ECB53F1748901B5921" Directory="dirD4BB3B3030E6C19000CA3A1BC69B6F39" Guid="{014C3BF0-EB02-4DD8-97F0-AD47E1AE7F7C}">
+                <File Id="fil6E5C9BBEEE5288F679B12AE6A49657B2" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpF56EE8D13DFE14559F6E7B9EF1D34569" Directory="dirD4BB3B3030E6C19000CA3A1BC69B6F39" Guid="{B8FA9CF5-ADE2-4426-95C1-AD1CA529E04F}">
+                <File Id="fil8097C7F5EBB660B90F65F96EC62F6342" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\qmldir" />
+            </Component>
+            <Component Id="cmp8E3363215DF077EB827751DB665D9DD1" Directory="dirD4BB3B3030E6C19000CA3A1BC69B6F39" Guid="{F4497801-71D9-4FAA-AD8C-246C60056E11}">
+                <File Id="fil2497BDEBFF9ED845670AED37393D4EDD" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\qtwebenginequickplugin.dll" />
+            </Component>
+            <Component Id="cmp7A8772E58D71483AB938D78C8B7D1B79" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{174CCED6-2707-455C-B609-3AC085FB5C0F}">
+                <File Id="filE1BB18B10EE0470145DDDA0C5E4A3B99" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\AlertDialog.qml" />
+            </Component>
+            <Component Id="cmpEE8B93301235F322A5BD23F0C50EED3F" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{D8884104-A700-4DE8-932C-DAC44019A5BA}">
+                <File Id="fil3D4C79F57FA6E2E2A3343868C653A6A5" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\AuthenticationDialog.qml" />
+            </Component>
+            <Component Id="cmp2C69E47276AE27A52BD269FB9BE30AAD" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{20862762-B83A-45A1-A54F-A473A6D2A0CE}">
+                <File Id="fil80A7B2A5FCF793D0801C69BCBD98F249" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\AutofillPopup.qml" />
+            </Component>
+            <Component Id="cmpA9795EC6D9B937B8A86B7B2846C91948" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{7A04DC39-26B9-4621-B5DF-0EE26A2002F3}">
+                <File Id="filA6C1CA6308618936A31EB03CEB74F27A" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\ColorDialog.qml" />
+            </Component>
+            <Component Id="cmp59759C9323D1558B224AADEA2AAF9C64" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{506D8589-0CF9-4B8B-9547-DD0DEB5A1D7B}">
+                <File Id="fil651A6A58BA66409A10279997FDF431A7" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\ConfirmDialog.qml" />
+            </Component>
+            <Component Id="cmp1B4AF93D806ADB8B732F2A27EB652B98" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{F5E54C76-0280-4DB1-9185-543360CE8900}">
+                <File Id="filF73B3538DC6FC52FF15D02C2C0E58D32" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\DirectoryPicker.qml" />
+            </Component>
+            <Component Id="cmpF2CCD1A91F993653DF09C0E15EC64A4B" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{04F36E55-26C2-459C-AD01-8E83D61B9D8C}">
+                <File Id="fil74DBD34E74C737746E6CAB216E06D48F" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\FilePicker.qml" />
+            </Component>
+            <Component Id="cmpA7D7583F9A23AD1D9C5E1762D7868578" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{1CE60DB8-9248-426A-84C8-4799ED25836A}">
+                <File Id="filD4288E12E49244FBBCD5FC5EFFE0298C" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\Menu.qml" />
+            </Component>
+            <Component Id="cmp7101449A820EA328AA1A4D1B5E999A0C" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{722B6A0B-6A25-4C96-96E4-ECA98AD77354}">
+                <File Id="filBD111FAA5EF2C87B70E794D43B659AF2" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\MenuItem.qml" />
+            </Component>
+            <Component Id="cmpAD3AF866463125CF8655A99CED3AA499" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{C163C7C3-A984-4066-9905-B95A02331E29}">
+                <File Id="fil98058066907D9EBDF12B81A4E59979BD" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\MenuSeparator.qml" />
+            </Component>
+            <Component Id="cmpE19B99C56B831180E601BB3589E3F3EA" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{AC95A951-DDB9-405A-BA24-D6ECC311B113}">
+                <File Id="filE382C586DDB2EA853C109D8235DF6C4F" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\PromptDialog.qml" />
+            </Component>
+            <Component Id="cmp4715158A523F0FB8C16A313FD382AFF9" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{9FA4AD8E-5518-41E7-B63D-81C9088F325E}">
+                <File Id="filA500F08171E7AD5EF0B2841D2A86F407" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\qmldir" />
+            </Component>
+            <Component Id="cmp2696C32671FC8FBEF137F3AC34DE43FD" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{A3F13B40-4EE5-44EF-ABE1-DDD47E80E9AF}">
+                <File Id="filE6605EF825D5763D112ABD3139E76B74" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\qtwebenginequickdelegatesplugin.dll" />
+            </Component>
+            <Component Id="cmp2E51274B580CEE17803AD1CE80B76947" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{683B1534-88D2-432A-8BD5-E2DDAC882438}">
+                <File Id="fil93F79CE6E26226D4196803E05887053C" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\ToolTip.qml" />
+            </Component>
+            <Component Id="cmp90F62D3657982F0E2E1182816125FED6" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{CBCCE1CB-34C9-4823-82DC-715280147B8A}">
+                <File Id="fil61B872246F6A108B9B7BE4F4F7E90DE8" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\TouchHandle.qml" />
+            </Component>
+            <Component Id="cmp14169E3CF5A8B218146130249FBB815D" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{B07EC381-AB58-4611-B885-80002E924066}">
+                <File Id="filFE30BDE0B2C918072AA010AE4AFF6C82" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\TouchSelectionMenu.qml" />
+            </Component>
+            <Component Id="cmpB07E9D9CFF105AE7053ACEA3668A2A8E" Directory="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Guid="{5B162E9C-AE2D-4BC8-8042-DA311B80E5C9}">
+                <File Id="filB51A511EB7DCD4E087D230FD943C82DB" KeyPath="yes" Source="SourceDir\qml\QtWebEngine\ControlsDelegates\WebEngineQuickDelegatesQml.qmltypes" />
+            </Component>
+            <Component Id="cmp740EB509039EF4F9005AA06519EACF79" Directory="dirAB78018B8375BAA818F1C40C8D16F821" Guid="{684794E3-E1E9-4F45-9071-566EA41167BA}">
+                <File Id="fil22604A1353A5B198D4846DC21D1783C9" KeyPath="yes" Source="SourceDir\qml\QtWebView\plugins.qmltypes" />
+            </Component>
+            <Component Id="cmpBBC59F3C27766AE0AB5515EFACEFAEC7" Directory="dirAB78018B8375BAA818F1C40C8D16F821" Guid="{5AC1C6A3-DCBA-41A0-8862-DBEFCCCEDE0F}">
+                <File Id="filD3823857AC018E3558C9BEC3252F47AB" KeyPath="yes" Source="SourceDir\qml\QtWebView\qmldir" />
+            </Component>
+            <Component Id="cmp40BFD4D4FCB9AB19F9943260306B2645" Directory="dirAB78018B8375BAA818F1C40C8D16F821" Guid="{B65F6D1C-EDBD-4BF0-BC8F-F1FD465F1172}">
+                <File Id="filFD81EBBF5AA65D21E5FC3AC0F9F27DD4" KeyPath="yes" Source="SourceDir\qml\QtWebView\qtwebviewquickplugin.dll" />
+            </Component>
+            <Component Id="cmp3C72ADF7447C02CBC442F4D498F97D6E" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="{33B44E03-270E-41E8-88E5-3FCA09403760}">
+                <File Id="fil1468B4258ED9D02B8E682200C411FD95" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_debugger.dll" />
+            </Component>
+            <Component Id="cmpD130E0EDF6AF950EA6D6A53A1751AA96" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="{059E0231-292B-41FE-8958-F7CCCD71482A}">
+                <File Id="filF63AD8464B4C5A27DCC098B1D1D42511" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_inspector.dll" />
+            </Component>
+            <Component Id="cmp5BDACEF05055E085579E7B097E890197" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="{2543FBAD-8F1E-4EAE-871E-760FA6E1EA64}">
+                <File Id="filB504D3FAAE942E0495C3640CFD5B377B" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_local.dll" />
+            </Component>
+            <Component Id="cmpBC2D56955BEA8DFE7E62B048E7F30EE8" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="{897C818C-DDAF-4A86-A7C8-4E999167C954}">
+                <File Id="fil26713E98F405141391C6BEA20282D73E" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_messages.dll" />
+            </Component>
+            <Component Id="cmpC9297AB1335961FB731B05361407FD09" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="{7D3D2565-3902-43C5-B03E-67E14EE8AE24}">
+                <File Id="fil7BAF56034C328F11684D5D91E5582BA6" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_native.dll" />
+            </Component>
+            <Component Id="cmp19E5AFEAFD4EE414B80BF9256489A85F" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="{C070071F-2298-4606-A25B-6A5D7829755C}">
+                <File Id="fil715013DA57F3A007F8DF4E53099D3C7A" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_nativedebugger.dll" />
+            </Component>
+            <Component Id="cmp57FF2A44B7244B35C17BB63FE8A1D97F" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="{305D972F-6917-4C3B-8B03-31953D49B965}">
+                <File Id="fil75DECF1849BEFF9DAC535DA290EA2440" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_preview.dll" />
+            </Component>
+            <Component Id="cmp7F8E313E83EEF3F0A5320389A0D409F0" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="{8C98FD2F-DFD0-4050-BB49-63CDF19E08C3}">
+                <File Id="filD2DF020A7341138A3174AA9926C94254" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_profiler.dll" />
+            </Component>
+            <Component Id="cmpA1FA5ACBABE6ABAF59E8A15E771A1020" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="{5CF27846-29A9-459F-B2D6-970AA89267F7}">
+                <File Id="fil2F21F43E56AEBBDE0C3002B3565087E0" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_quickprofiler.dll" />
+            </Component>
+            <Component Id="cmp3EA7E1216A1E529D7944144D42506A2C" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="{A41413ED-7A41-484E-B0DD-919DF20A5EF4}">
+                <File Id="filFABEED80D5D5652BC0BA2F689F6592E0" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_server.dll" />
+            </Component>
+            <Component Id="cmpCBA063886C021F79C883C543705AD5ED" Directory="dir9331125B8BF1F459D0AC9F91A6A2F779" Guid="{72954704-28D3-4571-8DA4-967AA7987E4B}">
+                <File Id="fil0839ED3FA6DF360E9FE61C24F7241731" KeyPath="yes" Source="SourceDir\qmltooling\qmldbg_tcp.dll" />
+            </Component>
+            <Component Id="cmp7939DBD081402A3D5A1E67BDD3FDC674" Directory="dir08941CEA6046320FCCF158F759AF80F2" Guid="{70690AC8-9EAC-4207-AEC3-2CC877115035}">
+                <File Id="fil51E0E82B321287CAC39351EA74609C90" KeyPath="yes" Source="SourceDir\resources\icudtl.dat" />
+            </Component>
+            <Component Id="cmpFCDF3702D9093A75BC161A3A1BB6BB86" Directory="dir08941CEA6046320FCCF158F759AF80F2" Guid="{7A483067-48E9-4830-9A70-46DF777DFF49}">
+                <File Id="fil5E3E79E2C8D58B41AC6EF5891C08A810" KeyPath="yes" Source="SourceDir\resources\qtwebengine_devtools_resources.pak" />
+            </Component>
+            <Component Id="cmp09CCD93B93EDDE01A6B28A19B724194A" Directory="dir08941CEA6046320FCCF158F759AF80F2" Guid="{28157F51-E8DF-4080-9275-9853C0E3D2B2}">
+                <File Id="fil08082A21F54192390BB8CCF88F7E6B9D" KeyPath="yes" Source="SourceDir\resources\qtwebengine_resources.pak" />
+            </Component>
+            <Component Id="cmp263082AF02EA4B9F434F2501E23AC972" Directory="dir08941CEA6046320FCCF158F759AF80F2" Guid="{EB5C7253-D603-4B31-95CF-7C7157A3E3F4}">
+                <File Id="fil24B7F212EFD5AB10B522561407BDDFC9" KeyPath="yes" Source="SourceDir\resources\qtwebengine_resources_100p.pak" />
+            </Component>
+            <Component Id="cmpC2267073DB55586843E13B823F6E0E8B" Directory="dir08941CEA6046320FCCF158F759AF80F2" Guid="{B7CB4FC1-BF14-47CF-AD7C-04B581B028F8}">
+                <File Id="filFDCD09910FB501B1A747E0A04B8D3E31" KeyPath="yes" Source="SourceDir\resources\qtwebengine_resources_200p.pak" />
+            </Component>
+            <Component Id="cmp5DAC8E92F0D1CE1028B76D02F12152DE" Directory="dir3BC41331752E78D0C2719C277915294F" Guid="{3773817D-66C1-48E3-A370-333237394858}">
+                <File Id="fil4F3C3348649DA965E980E99B9E82B7FF" KeyPath="yes" Source="SourceDir\styles\qwindowsvistastyle.dll" />
+            </Component>
+            <Component Id="cmpD57693E12A6CF9AFDC034864F2950D6F" Directory="dir509C75F94B9AF6C35CA00410005C14EE" Guid="{F49F8C94-0B60-4F26-B9B0-345928B1C3BA}">
+                <File Id="filA3197B081536A45213DBC7CD12A615C4" KeyPath="yes" Source="SourceDir\tls\qcertonlybackend.dll" />
+            </Component>
+            <Component Id="cmpC8A93EB5F0E2881753F8DCDA1B02129B" Directory="dir509C75F94B9AF6C35CA00410005C14EE" Guid="{89FB7332-0A1E-4DB8-937E-E293E2F90075}">
+                <File Id="fil63F4FB8EB696B5BA01AD976846F45663" KeyPath="yes" Source="SourceDir\tls\qschannelbackend.dll" />
+            </Component>
+            <Component Id="cmpE7627EECD23898BC1AEEEBA80CF4C18E" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{316228AE-54BF-4084-A220-5718FD5F2575}">
+                <File Id="fil841A28DBCBF2256981B583E30DC241AF" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\am.pak" />
+            </Component>
+            <Component Id="cmp4AA1EF8E6B3E8E5742AC3878237B15BD" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{2B7AE48B-46A6-4D7A-B9A0-DA8CDB51E47E}">
+                <File Id="fil7B1E977DD95738FCA43A5AF4E3DB820F" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ar.pak" />
+            </Component>
+            <Component Id="cmp489F46694306763E664E68F1A1D9398A" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{37E51441-D238-4C10-A4E7-99E6F40ABAA7}">
+                <File Id="filF1068C95C51734B905AD68641F8932DE" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\bg.pak" />
+            </Component>
+            <Component Id="cmpCB3806B057A11129336C03B60DB93368" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{CA89D25B-C4C3-4038-A33C-4786A0EABBC8}">
+                <File Id="fil5E98C112CCDD7A81098634A800367973" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\bn.pak" />
+            </Component>
+            <Component Id="cmp117BEDE3E308B6346D00371AE16FC727" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{108DBE8F-87E4-416F-A7ED-CBBBCE4BF903}">
+                <File Id="fil1F344AC58CF6783633B80F8532689D84" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ca.pak" />
+            </Component>
+            <Component Id="cmp1B4BD11EB6D78DE042388FC7B3B10979" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{56E502AD-1F48-4BC3-A8B8-2CCC94942864}">
+                <File Id="fil2DFDD403496C8DA8549416381A275F4B" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\cs.pak" />
+            </Component>
+            <Component Id="cmp8B5804D6EA4CE8594E25FA2EAA53B2CE" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{10031699-B994-41DF-AC30-9D1E3EF39BEA}">
+                <File Id="fil8998ADC69728900F32D27523BE7C0A7A" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\da.pak" />
+            </Component>
+            <Component Id="cmp14635E19526FBB5CC031D48A21B8DFE4" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{2147E310-8D3C-4E8A-A96A-A69FD8871F35}">
+                <File Id="filE4885D67548AFA65C78C08300C700419" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\de.pak" />
+            </Component>
+            <Component Id="cmp0DD34B1E211835A0825C4F37AEC7D421" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{4E40A463-2A73-488B-A0D6-9972A050CD9D}">
+                <File Id="fil544A6F5F8C37D55BB452375A1C857B0E" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\el.pak" />
+            </Component>
+            <Component Id="cmpBB56FFD9B4B03F1B7EFFE608596C1DF0" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{868AF772-3DDE-43A9-A878-59F4A8AC74F8}">
+                <File Id="filC138B25B3D7A4129D2F01586DDAB4929" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\en-GB.pak" />
+            </Component>
+            <Component Id="cmp7D1B7857542317367F521676B9C49306" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{EA66E25D-5305-46DC-8C66-2618D1C7B382}">
+                <File Id="fil1726E7077A64870D0E207D3578CBBDF4" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\en-US.pak" />
+            </Component>
+            <Component Id="cmpAC456BCAF3DCFA01198913534FCD2F3E" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{811D68EC-064D-4C0D-8707-346D564CCA9F}">
+                <File Id="fil2DFEE5CE81BFF7E138A6954DE8118BBF" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\es-419.pak" />
+            </Component>
+            <Component Id="cmp561D4111326710AC9AA46907E05AE2F8" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{BC07D34B-0DEE-422B-B8A6-6D22A039B010}">
+                <File Id="fil5149EA34D842EEA58E32E2FF3EA9FC74" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\es.pak" />
+            </Component>
+            <Component Id="cmp767F92E764F493FC663E591BBF691924" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{08879442-0461-4BD9-A3CA-05685EDC3464}">
+                <File Id="fil175C98AD989D0A19A6051CECB41755E7" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\et.pak" />
+            </Component>
+            <Component Id="cmp83CE7D31B03F0E640D486C8951C9F88A" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{C6C7DDC4-95C2-409F-B96D-1A967BBDA27E}">
+                <File Id="fil80A0D15334F4AE3C93E9AB996047F063" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\fa.pak" />
+            </Component>
+            <Component Id="cmpB556D1FABA724DC54358690B4A4F5528" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{94F25F58-8232-4978-B451-E21C0305AC3B}">
+                <File Id="fil9E20F6D165CA887227872340CF842DB7" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\fi.pak" />
+            </Component>
+            <Component Id="cmp2229E25AED1AE14594087A37BF635290" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{44E44245-0A1C-4257-8EA8-978A85CF8A6A}">
+                <File Id="filF183998CF77694A80E81B414ADAAD6F6" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\fil.pak" />
+            </Component>
+            <Component Id="cmp7E4EE520FFDF02CB789F8284CCF745D8" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{C01581FA-A19C-438B-AAB4-23BF0254167C}">
+                <File Id="fil2CEB374D9A3E9F8697A7245C40BAE974" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\fr.pak" />
+            </Component>
+            <Component Id="cmpB06A2E85562494B9B6F676A03E87D654" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{5D04712D-14A9-4707-BE57-AEA0D2EE9F8B}">
+                <File Id="filBC5A940DFAF4F4289C03179C3331CA32" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\gu.pak" />
+            </Component>
+            <Component Id="cmpCCDE1181B25851E3340339CF61382B80" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{050EA410-4B39-4C02-81AD-3FB847BF94AA}">
+                <File Id="fil96390711BE9B947D7B7FA5E0211E42E9" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\he.pak" />
+            </Component>
+            <Component Id="cmp25690B79DEAC48ED058A09275BBAAC7B" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{96CC859F-82BF-41BD-AB1F-D9F9F9A8097D}">
+                <File Id="fil85370AA4C63C2DECEC33E30E3DB303F9" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\hi.pak" />
+            </Component>
+            <Component Id="cmp6EE71F73860FDA82CE0BA04F55AB4E65" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{6FA8B74C-91C4-497A-8047-964F6B93A4EE}">
+                <File Id="fil8F215425A3BC657C3D4C352ECD1F7101" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\hr.pak" />
+            </Component>
+            <Component Id="cmpD21C6E91A9244444957B19A630773D29" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{2F7071AA-56BA-4FA2-ABE0-ECB0D5F4259F}">
+                <File Id="filE9F1E4FD2A7A617A4E4166414AB45691" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\hu.pak" />
+            </Component>
+            <Component Id="cmp38389365912F0AE1DFACDDC8479C7BF2" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{7C72D56E-D050-4BFF-B17D-09A32D8AF46E}">
+                <File Id="fil7D1F559C2C1110D18CA5764F1971BBA7" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\id.pak" />
+            </Component>
+            <Component Id="cmp9BCC30608F875FB2B6809ACBE266A0EB" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{F7AB65A5-FFAD-4A78-9DC6-A24F2B54BA79}">
+                <File Id="fil4CA2DD502CC46D09E241817165D07A69" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\it.pak" />
+            </Component>
+            <Component Id="cmp66B3A389FF5706DD47F6031C96870AC3" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{F4E2360B-4EB2-4E96-9F59-EFBAFA5ECCCF}">
+                <File Id="fil9A44734AE8E81C970FCD4CD1A15562EC" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ja.pak" />
+            </Component>
+            <Component Id="cmpC35D767A0329A304ED9409EE1758B26E" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{CCF07DE0-7975-4D9C-9728-FEE0C72C27AC}">
+                <File Id="filBEF8A5F318E161CAB9EB973C81EE8BAD" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\kn.pak" />
+            </Component>
+            <Component Id="cmp4FAE7008D38C2834FF608ECB10F422B3" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{8FE6E9B9-1BCB-467D-9345-0651E7D798BD}">
+                <File Id="fil0E05F04BA9603B8A46D202EE87E01968" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ko.pak" />
+            </Component>
+            <Component Id="cmp41E232D5090F2D0E0B1781C90734A625" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{334C395C-21E0-44D0-9570-4643A55F0660}">
+                <File Id="filB1E277BB14B69222B7C7DCC91A1E41D0" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\lt.pak" />
+            </Component>
+            <Component Id="cmp68559A181BEB78EFD5137F57CAD5FB00" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{3224E314-C635-4F09-8A28-EEB759E669D4}">
+                <File Id="fil75045C6FE752E32D2CA7E724F92A4C03" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\lv.pak" />
+            </Component>
+            <Component Id="cmp0F1F24602E525646795B185F7B39955A" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{D16B0186-716A-43FA-AF63-0FF5F1C019EC}">
+                <File Id="fil4EAF6A9B314FEFD4868F09695F2883EF" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ml.pak" />
+            </Component>
+            <Component Id="cmpAD12B9F7ABF28F48C3C94ADA02853AD5" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{31391CBB-65BE-4943-841D-2FDBE1471BB8}">
+                <File Id="filD4AE47CF3E0E2219C965A01B69346DC6" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\mr.pak" />
+            </Component>
+            <Component Id="cmpA6990FFF2D1086D28FBE7A600D52F67C" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{25E8974E-27FB-4A5F-9D6C-281519DAAD65}">
+                <File Id="fil8DA799DEB9484A9C1D8C766F558D928B" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ms.pak" />
+            </Component>
+            <Component Id="cmp74CC7AE3E2D73D6EB448973554564268" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{9E32E662-0FE7-4024-9293-A4CB5CC70EC9}">
+                <File Id="filD917C45F7D063800C10DFF694350E88F" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\nb.pak" />
+            </Component>
+            <Component Id="cmpCF99C5168606D7B5069B19761014FF91" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{7EE07CC1-BDAB-4CA7-B082-7ACBBCD867DA}">
+                <File Id="fil0BBBA2ED5EA505A0761814636A3E14CC" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\nl.pak" />
+            </Component>
+            <Component Id="cmpCF5AA8564BF2B7AECAAE7FFE84D8E4D9" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{50B28306-7296-452D-8855-84FBE0DEF3C6}">
+                <File Id="filBBE0AA8ADCAD675D5BE7906A7557CA33" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\pl.pak" />
+            </Component>
+            <Component Id="cmp1E4EF1965C96EAF1EDF0ED36C03EAE83" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{631D7CAE-7F80-49A8-871D-C97B28659180}">
+                <File Id="fil6A9727728F57E501FC885A71011F615B" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\pt-BR.pak" />
+            </Component>
+            <Component Id="cmp291C58BF5375B658AD36A7D92A0C8479" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{A16B236D-8BD1-48FD-889C-87E3E1A84FB5}">
+                <File Id="fil5444B8CC9431DD88F907C57EA9A90572" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\pt-PT.pak" />
+            </Component>
+            <Component Id="cmpAE702D6C4F5F596DB6B15FB36C6D9DAB" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{4195DB42-C6EE-4F23-B1F5-5F310A93E660}">
+                <File Id="filB15BEEE3DA276ADEA0E8A24C77D5A9C2" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ro.pak" />
+            </Component>
+            <Component Id="cmpDF7385B2F039DBE31A2545C254ABF0F0" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{23B8D5DF-092F-4F9F-A409-B10F7C3F43E3}">
+                <File Id="filBCB5F742C4B473A8C6C5CF4917E6144A" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ru.pak" />
+            </Component>
+            <Component Id="cmpDF8F1EB0E2A70BD5F396A75FECB92533" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{6C528DEF-26A6-41B1-890D-7C93C5C00CEF}">
+                <File Id="fil9769BAFE384BB1853A2C508DFA05229F" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\sk.pak" />
+            </Component>
+            <Component Id="cmpDB5BD4BF5A46761C6544B87CF7085EEB" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{3C78325E-A8C8-481D-8170-3FB1DB79399D}">
+                <File Id="filA84154373398754DDDB4E950A134C5F5" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\sl.pak" />
+            </Component>
+            <Component Id="cmp1C71511A57869D1F5DD784E9E4191BFD" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{C8E2297C-65D1-4781-B7B6-01EE34763396}">
+                <File Id="fil5F171B8A37BA8123184CABD8A9F23CFA" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\sr.pak" />
+            </Component>
+            <Component Id="cmpB922A5B2744CD81821403BFD931F8112" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{1602F9E2-E6D2-4232-83BC-9624941DDA8D}">
+                <File Id="fil20AA02ACBBE6A5F32DE18FF2F0A7AEFF" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\sv.pak" />
+            </Component>
+            <Component Id="cmp6DAB86BB3D2953D10957B0FAFF22E155" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{C2B6BA19-F347-4DC5-B7D2-D91A62E89EA9}">
+                <File Id="fil6199BA4B33801E936F1847E792F5CAAB" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\sw.pak" />
+            </Component>
+            <Component Id="cmpFC87AE461BD4B51F4A19E039A3C25CA1" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{C7484B41-0BC9-42B1-9FA6-7847B56C883B}">
+                <File Id="fil38C023D4016EF85C52C0B835FC94F0C7" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\ta.pak" />
+            </Component>
+            <Component Id="cmp58B01AD92CF1FEEF6EA0325F60D05AA6" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{DDB5E1A6-82AB-48F8-A46B-F3B05A6B2B8C}">
+                <File Id="fil09449582AB62FB08D7CD3574754B8C0C" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\te.pak" />
+            </Component>
+            <Component Id="cmpA3A2E454F58F8435858E88AC73495360" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{0277E03E-1470-4733-AF67-DB4166968375}">
+                <File Id="fil27FE0443556E7587E13EE46CFBF62A4F" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\th.pak" />
+            </Component>
+            <Component Id="cmp15CD85788F2B7BE37D3D33C7B69D5FF6" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{04AE80EA-26AC-4B7B-8A8E-7CB0CCB58B04}">
+                <File Id="fil55F7FECA91CEE1F865B75C6581438FFC" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\tr.pak" />
+            </Component>
+            <Component Id="cmp2D4EB727F69B23FADD8074CB77E51F36" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{5A9D30E7-6F12-4978-82A9-BF0CB15BA670}">
+                <File Id="filC97CBBD646D7E81182BFE9ECB61844CA" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\uk.pak" />
+            </Component>
+            <Component Id="cmp43AF26BE2B604BD873763D73460FB4B9" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{88D4C378-A57F-4F12-9CBD-ED7F62D7060A}">
+                <File Id="fil467C07F74FBFC01F0D823F0E76F07FB7" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\vi.pak" />
+            </Component>
+            <Component Id="cmp8CCECC99D6C73FD198EA365D4835864F" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{8B20312C-891D-48C7-9898-CDF4C2A84671}">
+                <File Id="filD0D4650DF850FAD942030BBC923D8D7A" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\zh-CN.pak" />
+            </Component>
+            <Component Id="cmp9F8E9C0770E5832169033250113D1A21" Directory="dirA016FBC978C7F28C3C5CDBC06002A5D6" Guid="{6FADF2B8-45CE-4E11-B85C-C800C3F90616}">
+                <File Id="fil2FAD3F60E739CE8D347E5ADE50EA653C" KeyPath="yes" Source="SourceDir\translations\qtwebengine_locales\zh-TW.pak" />
+            </Component>
+            <Component Id="cmp8F41C0E31AFDC29F8BDF013E907B2A5B" Directory="dir90EFFBA81E5867E3E251FBDCB82DBC0C" Guid="{6EBE96D6-0BC9-4EA2-9990-CF184EC5958E}">
+                <File Id="filCF87AAEC5D091D6D8852B61917DB3BBF" KeyPath="yes" Source="SourceDir\webview\qtwebview_webengine.dll" />
+            </Component>
+        </ComponentGroup>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir171007FEE30C4A50EC7ED2CB58E0B5E3" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir175FB1A22F883092D2FC39E138CDC5FC" Name="Controls" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir1A86B48CD44E777E78B13993BE3DFC29">
+            <Directory Id="dir1A6A7D662F671C349F73CA17E4F3DA62" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir1A86B48CD44E777E78B13993BE3DFC29" Name="Fusion" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir2105A1F7199CAE1171E138FDED1D8E49" Name="Particles" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir27DABF0F34E48010C38FDC22DEE2453A" Name="Window" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir2B55988B75845F194F607E4ED40BB41B" Name="Universal" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir2C126EEEB46329DB6052C65B78F1DA75" Name="tooling" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir93F3A6F9A0F15C62D9983B7251AEF092">
+            <Directory Id="dir2FA83874D84EB850CB9EC2829B8A3243" Name="GraphicalEffects" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dir30FC873BE970351C9459A1383DCBEA3D" Name="QtWebChannel" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir3390D3B4341886F0DA85EE3BA92439FF" Name="Dialogs" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir3390D3B4341886F0DA85EE3BA92439FF">
+            <Directory Id="dir34D1D6742B95CCA4110D8968A4544048" Name="quickimpl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirF7455B984D3CF2564DED2759788752A0">
+            <Directory Id="dir3A2097370D43A6BEED9BF1FBE00B9935" Name="controls" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir463F84444BF926F4998942302E2C59AF" Name="Material" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dir4B7D2A19DD0D33E235AA7BD43615A87E" Name="QtQuick" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir5C7B63749559BE5BFFCAD592A78D8BA7">
+            <Directory Id="dir4E741D8C175690CD9096A89077530C9C" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir547CFC29D0E6DE0A73542C0E043048F6" Name="Basic" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dir5C7B63749559BE5BFFCAD592A78D8BA7" Name="Imagine" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir7397A50E4D7D313615C4CF7C94C61160" Name="Effects" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir34D1D6742B95CCA4110D8968A4544048">
+            <Directory Id="dir755D907146AC4C2DC7E3BE88C0F1A70F" Name="qml" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir547CFC29D0E6DE0A73542C0E043048F6">
+            <Directory Id="dir78C3D911E47BC1C88AC3A9AC7D40EEC3" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir2FA83874D84EB850CB9EC2829B8A3243">
+            <Directory Id="dir7B12C3FC704EE1F6A1CBC6ECA590217F" Name="private" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir7EBA39754BD2CE291EC25825433A43D2" Name="Layouts" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir755D907146AC4C2DC7E3BE88C0F1A70F">
+            <Directory Id="dir8C28C0A2CF1DDABB1BF2872E8E6AE192" Name="+Imagine" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir755D907146AC4C2DC7E3BE88C0F1A70F">
+            <Directory Id="dir8C55A2520B05F605E480B8F2F2039F1C" Name="+Universal" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir755D907146AC4C2DC7E3BE88C0F1A70F">
+            <Directory Id="dir929495D16D3C46BFD28C6AD595BCD701" Name="+Fusion" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dir92990262DE0ED5508C44523003A4B360" Name="QtQml" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir92990262DE0ED5508C44523003A4B360">
+            <Directory Id="dir92A51CED876E1F4286E6663FD7D54CDA" Name="WorkerScript" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dir93F3A6F9A0F15C62D9983B7251AEF092" Name="Qt5Compat" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dir96881D4937C7E7E8DC6A06696EF52C96" Name="Shapes" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirD4BB3B3030E6C19000CA3A1BC69B6F39">
+            <Directory Id="dir9A8AE25EE73C6EF6DEA0B7B52AA34AE6" Name="ControlsDelegates" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir1F7AD7AA9B228AED2E74CDD7C3088B5C">
+            <Directory Id="dirA016FBC978C7F28C3C5CDBC06002A5D6" Name="qtwebengine_locales" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dirA16BBE5DE11DF2FD79B07E2965737D3E" Name="Templates" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir463F84444BF926F4998942302E2C59AF">
+            <Directory Id="dirA55BCC7BED6B60785CEB74AFBA374495" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dirA6F6BC40CF508C5F92BB49483928E0FC" Name="LocalStorage" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir92990262DE0ED5508C44523003A4B360">
+            <Directory Id="dirAA6B0932D46A7D6E35A8D1E06E10B2B7" Name="Models" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dirAB78018B8375BAA818F1C40C8D16F821" Name="QtWebView" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir175FB1A22F883092D2FC39E138CDC5FC">
+            <Directory Id="dirB030248D75EA99E75698A1E011FE9CC0" Name="Windows" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir2B55988B75845F194F607E4ED40BB41B">
+            <Directory Id="dirBF720220167083D5C665DD0D131194BE" Name="impl" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir755D907146AC4C2DC7E3BE88C0F1A70F">
+            <Directory Id="dirC980C9942B69E6635E4CF7C57652D12B" Name="+Material" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir92990262DE0ED5508C44523003A4B360">
+            <Directory Id="dirD3E9E07666E50DD0B119271684422637" Name="Base" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir92990262DE0ED5508C44523003A4B360">
+            <Directory Id="dirD48123A7B6E2CFB660EC94F0CBE0276D" Name="XmlListModel" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dirC5F8EA44B55B774D108D66814D18D4D1">
+            <Directory Id="dirD4BB3B3030E6C19000CA3A1BC69B6F39" Name="QtWebEngine" />
+        </DirectoryRef>
+    </Fragment>
+    <Fragment>
+        <DirectoryRef Id="dir4B7D2A19DD0D33E235AA7BD43615A87E">
+            <Directory Id="dirF7455B984D3CF2564DED2759788752A0" Name="NativeStyle" />
+        </DirectoryRef>
+    </Fragment>
+</Wix>
\ No newline at end of file