New upstream version 2.5.0+ds
authorIOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
Sat, 25 Jan 2025 14:50:56 +0000 (15:50 +0100)
committerIOhannes m zmölnig (Debian/GNU) <umlaeute@debian.org>
Sat, 25 Jan 2025 14:50:56 +0000 (15:50 +0100)
124 files changed:
.dockerignore
CMakeLists.txt
Dockerfile
build
build-aux/flatpak/org.jacktrip.JackTrip.json
docs/Build/Linux.md
docs/Build/Mac.md
docs/Build/Windows.md
docs/changelog.yml
externals/oscpp/CMakeLists.txt [new file with mode: 0644]
externals/oscpp/LICENSE [new file with mode: 0644]
externals/oscpp/Makefile [new file with mode: 0644]
externals/oscpp/README.md [new file with mode: 0644]
externals/oscpp/doxygen.cfg [new file with mode: 0644]
externals/oscpp/include/oscpp/client.hpp [new file with mode: 0644]
externals/oscpp/include/oscpp/detail/endian.hpp [new file with mode: 0644]
externals/oscpp/include/oscpp/detail/host.hpp [new file with mode: 0644]
externals/oscpp/include/oscpp/detail/stream.hpp [new file with mode: 0644]
externals/oscpp/include/oscpp/error.hpp [new file with mode: 0644]
externals/oscpp/include/oscpp/print.hpp [new file with mode: 0644]
externals/oscpp/include/oscpp/server.hpp [new file with mode: 0644]
externals/oscpp/include/oscpp/types.hpp [new file with mode: 0644]
externals/oscpp/include/oscpp/util.hpp [new file with mode: 0644]
externals/oscpp/test/CMakeLists.txt [new file with mode: 0644]
externals/oscpp/test/oscpp_autocheck.cpp [new file with mode: 0644]
externals/oscpp/tools/clang-format [new file with mode: 0755]
externals/oscpp/tools/mdcode.rb [new file with mode: 0755]
jacktrip.pro
linux/Dockerfile.build [new file with mode: 0644]
linux/README.md
macos/JackTrip.vst3_template/Contents/Info.plist [new file with mode: 0644]
macos/JackTrip.vst3_template/Contents/PkgInfo [new file with mode: 0644]
macos/assemble_app.sh
macos/package/JackTrip.pkgproj_template_with_vst3 [new file with mode: 0644]
macos/package/postinstall.sh
macos/sign-stuff.sh
meson.build
meson_options.txt
src/Analyzer.cpp
src/AudioInterface.cpp
src/AudioInterface.h
src/AudioSocket.cpp [new file with mode: 0644]
src/AudioSocket.h [new file with mode: 0644]
src/Compressor.cpp
src/JackTrip.cpp
src/JackTrip.h
src/JackTripWorker.h
src/JitterBuffer.h
src/Limiter.cpp
src/Meter.cpp
src/Monitor.cpp
src/OscServer.cpp [new file with mode: 0644]
src/OscServer.h [new file with mode: 0644]
src/ProcessPlugin.h
src/Regulator.cpp
src/Regulator.h
src/Reverb.cpp
src/RingBuffer.cpp
src/RingBuffer.h
src/RtAudioInterface.cpp
src/RtAudioInterface.h
src/SampleRateConverter.cpp [new file with mode: 0644]
src/SampleRateConverter.h [new file with mode: 0644]
src/Settings.cpp
src/SocketClient.cpp [new file with mode: 0644]
src/SocketClient.h [new file with mode: 0644]
src/SocketServer.cpp [new file with mode: 0644]
src/SocketServer.h [new file with mode: 0644]
src/StereoToMono.cpp
src/Tone.cpp
src/UdpHubListener.cpp
src/UdpHubListener.h
src/Volume.cpp
src/gui/about.cpp
src/gui/about.ui
src/gui/qjacktrip.cpp
src/gui/qjacktrip.ui
src/images/images.qrc
src/jacktrip_globals.h
src/vs/AudioSettings.qml
src/vs/ChangeDevices.qml
src/vs/FeedbackSurvey.qml
src/vs/Settings.qml
src/vs/WebEngine.qml
src/vs/balance.svg [new file with mode: 0644]
src/vs/virtualstudio.cpp
src/vs/virtualstudio.h
src/vs/vs.qrc
src/vs/vsApi.cpp
src/vs/vsApi.h
src/vs/vsAudio.cpp
src/vs/vsAudio.h
src/vs/vsAuth.cpp
src/vs/vsAuth.h
src/vs/vsDeeplink.cpp
src/vs/vsDeeplink.h
src/vs/vsDevice.cpp
src/vs/vsDevice.h
src/vs/vsMacPermissions.mm
src/vs/vsPinger.cpp
src/vs/vsPinger.h
src/vs/vsQuickView.cpp
src/vs/vsQuickView.h
src/vs/vsWebSocket.cpp
src/vs/vsWebSocket.h
src/vst3/JackTripVST.h [new file with mode: 0644]
src/vst3/JackTripVSTController.cpp [new file with mode: 0644]
src/vst3/JackTripVSTController.h [new file with mode: 0644]
src/vst3/JackTripVSTDataBlock.h [new file with mode: 0644]
src/vst3/JackTripVSTEntry.cpp [new file with mode: 0644]
src/vst3/JackTripVSTProcessor.cpp [new file with mode: 0644]
src/vst3/JackTripVSTProcessor.h [new file with mode: 0644]
src/vst3/resources/Dual_LED.png [new file with mode: 0644]
src/vst3/resources/JackTripEditor.uidesc [new file with mode: 0644]
src/vst3/resources/Sercan_Moog_Knob.png [new file with mode: 0644]
src/vst3/resources/background.png [new file with mode: 0644]
src/vst3/resources/background_2x.png [new file with mode: 0644]
src/vst3/resources/moduleinfo.json [new file with mode: 0644]
src/vst3/resources/win32resource.rc [new file with mode: 0644]
subprojects/libsamplerate.wrap [new file with mode: 0644]
tests/audio_socket_test.cpp [new file with mode: 0644]
win/build_installer.bat
win/jacktrip-vst3.wxs [new file with mode: 0644]
win/jacktrip.wxs

index b517c8e24d238e019e4a7ba13f8bcf79fe81ad3f..a2c033a4976a445ba1ec10706e9c78f3f0380b57 100644 (file)
@@ -2,7 +2,9 @@
 !docs
 !meson*
 !container
+!externals
 !src
 !subprojects
 !linux
-!win
\ No newline at end of file
+!win
+!tests
index e4db93db244657aa45b02a8a8fe84e59c83d80b2..442eaafbcd08d40740e38893d100a5621b458bd4 100644 (file)
@@ -1,5 +1,6 @@
 cmake_minimum_required(VERSION 3.12)
 set(CMAKE_OSX_DEPLOYMENT_TARGET 10.14)
+set(CMAKE_OSX_ARCHITECTURES arm64;x86_64)
 set(CMAKE_CXX_STANDARD 17)
 project(QJackTrip)
 
@@ -7,14 +8,16 @@ set(nogui FALSE)
 set(rtaudio TRUE)
 set(weakjack TRUE)
 set(novs FALSE)
+set(nooscpp FALSE)
 set(vsftux FALSE)
 set(noupdater FALSE)
 set(psi FALSE)
-set(QtVersion "5")
+set(QtVersion "6")
 
 if (${QtVersion} MATCHES "5")
   set(CMAKE_OSX_DEPLOYMENT_TARGET 10.13)
-  set(CMAKE_OSX_ARCHITECTURES arm64;x86_64)
+elseif (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
+  list(APPEND CMAKE_PREFIX_PATH "/opt/local/libexec/qt6/lib/cmake")
 endif ()
 
 message(STATUS "Hello Aaron! For anyone else, heed the following warning:")
@@ -55,9 +58,10 @@ if (psi)
   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}")
+  string(REPLACE "<file>icon_256.png" "<file alias=\"icon_256.png\">../gui/alt/icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  string(REPLACE "<file alias=\"icon_128@2x.png\">icon_256.png" "<file alias=\"icon_128@2x.png\">../gui/alt/icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  string(REPLACE "<file>icon_128.png" "<file alias=\"icon_128.png\">../gui/alt/icon_128.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  string(REPLACE "<file>icon_32.png" "<file alias=\"icon_32.png\">../gui/alt/icon_32.png" QRC_CONTENTS "${QRC_CONTENTS}")
   file(WRITE "${QRC_FILE}" "${QRC_CONTENTS}")
   string(TIMESTAMP BUILD_DATE "%Y%m%d")
   set(BUILD_NUMBER "00")
@@ -65,9 +69,10 @@ if (psi)
   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}")
+  string(REPLACE "<file alias=\"icon_256.png\">../gui/alt/icon_256.png" "<file>icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  string(REPLACE "<file alias=\"icon_128@2x.png\">../gui/alt/icon_256.png" "<file alias=\"icon_128@2x.png\">icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  string(REPLACE "<file alias=\"icon_128.png\">../gui/alt/icon_128.png" "<file>icon_128.png" QRC_CONTENTS "${QRC_CONTENTS}")
+  string(REPLACE "<file alias=\"icon_32.png\">../gui/alt/icon_32.png" "<file>icon_32.png" QRC_CONTENTS "${QRC_CONTENTS}")
   file(WRITE "${QRC_FILE}" "${QRC_CONTENTS}")
 endif ()
 
@@ -87,7 +92,7 @@ elseif (${CMAKE_SYSTEM_NAME} MATCHES "Windows")
   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}")
+  list(APPEND 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")
@@ -132,6 +137,8 @@ find_package(${QtVersion}Network CONFIG REQUIRED)
 set(qjacktrip_SRC
   src/main.cpp
   src/Settings.cpp
+  src/SocketClient.cpp
+  src/SocketServer.cpp
   src/jacktrip_globals.cpp
   src/JackTrip.cpp
   src/UdpHubListener.cpp
@@ -139,6 +146,7 @@ set(qjacktrip_SRC
   src/DataProtocol.cpp
   src/UdpDataProtocol.cpp
   src/AudioInterface.cpp
+  src/AudioSocket.cpp
   src/JackAudioInterface.cpp
   src/JMess.cpp
   src/LoopBack.cpp
@@ -146,6 +154,7 @@ set(qjacktrip_SRC
   src/RingBuffer.cpp
   src/JitterBuffer.cpp
   src/Regulator.cpp
+  src/SampleRateConverter.cpp
   src/Compressor.cpp
   src/Limiter.cpp
   src/Reverb.cpp
@@ -156,6 +165,16 @@ set(qjacktrip_SRC
   src/ProcessPlugin.cpp
 )
 
+if (nooscpp)
+  add_compile_definitions(NO_OSCPP)
+else ()
+  include_directories("externals/oscpp")
+  include_directories("externals/oscpp/include")
+  set (qjacktrip_SRC ${qjacktrip_SRC}
+    src/OscServer.cpp
+  )
+endif ()
+
 if (rtaudio)
   add_compile_definitions(RT_AUDIO)
   set (qjacktrip_SRC ${qjacktrip_SRC}
@@ -182,7 +201,7 @@ if (NOT nogui)
     src/Meter.cpp
     src/UserInterface.cpp
   )
-  
+
   if (NOT novs)
     set (qjacktrip_SRC ${qjacktrip_SRC}
       src/vs/virtualstudio.cpp
@@ -214,7 +233,7 @@ if (NOT nogui)
   else ()
     set (qjacktrip_SRC ${qjacktrip_SRC} src/images/images.qrc)
   endif ()
-  
+
   if (NOT noupdater)
     set (qjacktrip_SRC ${qjacktrip_SRC}
       src/dblsqd/feed.cpp
index 2fa1bc0f634b87332451b9b0f9128e905baccf3f..b5d513ad3c054fa4db4485fec5b3b2857980857d 100644 (file)
@@ -17,7 +17,7 @@ ARG JACK_VERSION=latest
 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
+RUN dnf install -y --nodocs cmake gcc gcc-c++ meson git python3-pyyaml python3-jinja2 glib2-devel jack-audio-connection-kit-devel dbus-devel
 
 ENV QT_VERSION=6.5.3
 RUN if [ "$(uname -m)" = "x86_64" ]; then export ARCH=amd64; else export ARCH=arm64; fi \
@@ -65,4 +65,4 @@ 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
+EXPOSE 61000-61100/udp
diff --git a/build b/build
index 351873ebb71d42b86534e3cf898a5d08a81dc4ca..57e24449c3d7e176ce4579a8b211a9c83cd7aa45 100755 (executable)
--- a/build
+++ b/build
@@ -20,6 +20,7 @@ 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
+nooscpp - build without OSC support\n
 vsftux - build with Virtual Studio first launch experience\n
 noupdater - build without auto-update support\n
 static - build with static libraries\n
@@ -32,25 +33,29 @@ while [[ "$#" -gt 0 ]]; do
       noclean) clean=0 ;;
       nojack)
       echo "Building without jack"
-      CONFIG="-config nojack $CONFIG" 
+      CONFIG="-config nojack $CONFIG"
       ;;
-      rtaudio) 
+      rtaudio)
       RTAUDIO=1
       ;;
-      no-system-rtaudio) 
+      no-system-rtaudio)
       NO_SYSTEM_RTAUDIO=1
       ;;
       nogui)
       echo "Building without the gui"
-      CONFIG="-config nogui $CONFIG" 
+      CONFIG="-config nogui $CONFIG"
       ;;
       novs)
       echo "Building without Virtual Studio support"
-      CONFIG="-config novs $CONFIG" 
+      CONFIG="-config novs $CONFIG"
+      ;;
+      nooscpp)
+      echo "Building without OSC support"
+      CONFIG="-config nooscpp $CONFIG"
       ;;
       vsftux)
       echo "Building with Virtual Studio first launch experience"
-      CONFIG="-config vsftux $CONFIG" 
+      CONFIG="-config vsftux $CONFIG"
       ;;
       noupdater)
       echo "Building without auto-update support"
@@ -58,7 +63,7 @@ while [[ "$#" -gt 0 ]]; do
       ;;
       static)
       echo "Building with static libraries"
-      CONFIG="-config static $CONFIG" 
+      CONFIG="-config static $CONFIG"
       ;;
       weakjack)
       echo "Building with weak linking of jack"
@@ -75,8 +80,8 @@ while [[ "$#" -gt 0 ]]; do
       fi
       echo "Will build using $jobs make jobs"
       ;;
-      -h|--help) 
-      echo -e $HELP_STR; exit 
+      -h|--help)
+      echo -e $HELP_STR; exit
       ;;
       *) UNKNOWN_OPTIONS+=("$1") ;;
     esac
index e4348c02b4224e3dff1a3984dbb30cb48096b938..49aec5f5c686325500f6a1c8d35f52e525048f02 100644 (file)
@@ -28,6 +28,7 @@
             "buildsystem": "meson",
             "config-opts": [
                 "-Dbuildtype=debugoptimized",
+                "-Dlibsamplerate=disabled",
                 "-Dpkg_config_path=/app/lib/x86_64-linux-gnu/pkgconfig"
             ],
             "sources": [
index 985228dbda53fe1ee95eba2498456eb10dff3194..8086733c7ac42439a1b5f5f1481ff1084ca974a5 100644 (file)
@@ -20,7 +20,7 @@ Optional:
 dnf install qt5-qtbase-devel qt5-qtnetworkauth-devel qt5-qtwebsockets-devel qt5-qtquickcontrols2-devel qt5-qtsvg-devel
 dnf groupinstall "C Development Tools and Libraries"
 dnf groupinstall "Development Tools"
-dnf install "pkgconfig(jack)" rtaudio-devel git help2man python3-jinja2
+dnf install "pkgconfig(jack)" rtaudio-devel git help2man python3-jinja2 dbus-devel
 ```
 
 ### Fedora (Qt6)
@@ -28,7 +28,7 @@ dnf install "pkgconfig(jack)" rtaudio-devel git help2man python3-jinja2
 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
+dnf install "pkgconfig(jack)" rtaudio-devel git help2man python3-jinja2 dbus-devel
 ```
 
 Clone the git repo with submodules and run `./build install` in the project
@@ -124,6 +124,57 @@ $ meson install -C builddir
 # enter your password when prompted
 ```
 
+### Building with Docker
+
+You can also build JackTrip using Docker, which especially makes it easier
+to build for alternative architectures. The following build arguments are
+available:
+
+* BUILD_CONTAINER - Debian based container image to build with
+* MESON_ARGS - arguments to build using meson
+* QT_DOWNLOAD_URL - path to qt6 download (optional)
+* VST3SDK_DOWNLOAD_URL - path to the VST3 SDK (optional)
+
+For example:
+
+amd64 dynamic
+```
+docker buildx build --target=artifact -f linux/Dockerfile.build --output type=local,dest=./ \
+  --platform linux/amd64 --build-arg BUILD_CONTAINER=ubuntu:22.04 \
+  --build-arg MESON_ARGS="-Ddefault_library=shared -Drtaudio=enabled -Drtaudio:jack=disabled -Drtaudio:default_library=static -Drtaudio:alsa=enabled -Drtaudio:pulse=enabled -Drtaudio:werror=false" .
+```
+
+amd64 static
+```
+docker buildx build --target=artifact -f linux/Dockerfile.build --output type=local,dest=./ \
+  --platform linux/amd64 --build-arg BUILD_CONTAINER=ubuntu:20.04 \
+  --build-arg MESON_ARGS="-Ddefault_library=static -Drtaudio=enabled -Drtaudio:jack=disabled -Drtaudio:default_library=static -Drtaudio:alsa=enabled -Drtaudio:pulse=disabled -Drtaudio:werror=false -Dnogui=true" \
+  --build-arg QT_DOWNLOAD_URL=https://files.jacktrip.org/contrib/qt/qt-6.5.3-static-linux-amd64.tar.gz .
+```
+
+arm64 dynamic
+```
+docker buildx build --target=artifact -f linux/Dockerfile.build --output type=local,dest=./ \
+  --platform linux/arm64 --build-arg BUILD_CONTAINER=ubuntu:22.04 \
+  --build-arg MESON_ARGS="-Ddefault_library=shared -Drtaudio=enabled -Drtaudio:jack=disabled -Drtaudio:default_library=static -Drtaudio:alsa=enabled -Drtaudio:pulse=enabled -Drtaudio:werror=false" .
+```
+
+arm64 static
+```
+docker buildx build --target=artifact -f linux/Dockerfile.build --output type=local,dest=./ \
+  --platform linux/arm64 --build-arg BUILD_CONTAINER=ubuntu:20.04 \
+  --build-arg MESON_ARGS="-Ddefault_library=static -Drtaudio=enabled -Drtaudio:jack=disabled -Drtaudio:default_library=static -Drtaudio:alsa=enabled -Drtaudio:pulse=disabled -Drtaudio:werror=false -Dnogui=true" \
+  --build-arg QT_DOWNLOAD_URL=https://files.jacktrip.org/contrib/qt/qt-6.5.3-static-linux-arm64.tar.gz .
+```
+
+arm32 static
+```
+docker buildx build --target=artifact -f linux/Dockerfile.build --output type=local,dest=./ \
+  --platform linux/arm/v7 --build-arg BUILD_CONTAINER=debian:buster \
+  --build-arg MESON_ARGS="-Ddefault_library=static -Drtaudio=enabled -Drtaudio:jack=disabled -Drtaudio:default_library=static -Drtaudio:alsa=enabled -Drtaudio:pulse=disabled -Drtaudio:werror=false -Dnogui=true -Dcpp_link_args='-no-pie'" \
+  --build-arg QT_DOWNLOAD_URL=https://files.jacktrip.org/contrib/qt/qt-5.15.13-static-linux-arm32.tar.gz .
+```
+
 ### Verification
 
 If you have installed jacktrip, from anywhere in the Terminal, type:
@@ -177,3 +228,35 @@ $ pwd
 ```
 
 The new version's directory structure might look like this: ``` jacktrip-1.x.x/builddir``` and the old version ``` jacktrip/builddir```.
+
+## Building VST3 SDK for Linux
+
+You may need a few extra development libraries to build the VST3 SDK:
+
+On Fedora:
+```
+sudo dnf install -y expat-devel freetype-devel pango-devel xcb-util-devel xcb-util-cursor-devel xcb-util-keysyms-devel libxkbcommon-x11-devel gtkmm3.0-devel libsqlite3x-devel
+```
+
+On Ubuntu and Debian/Raspbian:
+```
+sudo apt install -y libexpat-dev libxml2-dev libxcb-util-dev libxcb-cursor-dev libxcb-keysyms1-dev libxcb-xkb-dev libxkbcommon-dev libxkbcommon-x11-dev libgtkmm-3.0-dev libsqlite3-dev
+```
+
+To build and install the VST3 SDK:
+```
+git clone --recursive https://github.com/steinbergmedia/vst3sdk
+mkdir vst3sdk/build
+cd vst3sdk/build
+cmake -DCMAKE_BUILD_TYPE=Release ../
+cmake --build . --config Release
+sudo mkdir -p /opt/vst3sdk
+sudo cp -r lib/Release /opt/vst3sdk/lib
+sudo cp -r bin/Release /opt/vst3sdk/bin
+sudo cp -r ../base ../pluginterfaces ../public.sdk ../vstgui4 /opt/vst3sdk
+```
+
+When you run `meson setup` use `-Dvst-sdkdir=/path/to/vst3sdk`
+
+Please note that redistribution of JackTrip's VST3 plugin requires a
+[license from Steinberg](https://www.steinberg.net/developers/).
index 898361f2e1373d6e8185ed1f1bb273aa53ab708e..dba52b425e7cd0a57dc25708d652ee1c11bb3369 100644 (file)
@@ -112,3 +112,32 @@ If you see something like this, you have successfully installed Jacktrip:
 >     Copyright (c) 2008-2020 Juan-Pablo Caceres, Chris Chafe.
 >     SoundWIRE group at CCRMA, Stanford University
 
+## Building VST3 SDK for Mac
+
+```
+git clone --recursive https://github.com/steinbergmedia/vst3sdk
+mkdir vst3sdk/build
+cd vst3sdk/build
+cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" ../
+cmake --build . --config Release
+sudo mkdir -p /opt/vst3sdk
+sudo cp -r lib/Release /opt/vst3sdk/lib
+sudo cp -r bin/Release /opt/vst3sdk/bin
+sudo cp -r ../base ../pluginterfaces ../public.sdk ../vstgui4 /opt/vst3sdk
+```
+
+VST plugins are not allowed to have any shared library dependencies. If you
+are using a shared/dynamic version of the Qt libraries to build JackTrip,
+you may need to copy over a few static versions for a few of these so that
+the linker can find them:
+
+```
+sudo cp /opt/qt-6.2.6-static/lib/libQt6Core.a /opt/vst3sdk/lib
+sudo cp /opt/qt-6.2.6-static/lib/libQt6Network.a /opt/vst3sdk/lib
+sudo cp /opt/qt-6.2.6-static/lib/libQt6BundledPcre2.a /opt/vst3sdk/lib
+```
+
+When you run `meson setup` use `-Dvst-sdkdir=/path/to/vst3sdk`
+
+Please note that redistribution of JackTrip's VST3 plugin requires a
+[license from Steinberg](https://www.steinberg.net/developers/).
index bd7dc90bc25bedbddbf9f42d88abfa6cb222172b..2dc3fa93153b634808429cb860776b1fe1f439ab 100644 (file)
@@ -82,3 +82,29 @@ If you see something like this, you have successfully installed Jacktrip:
 >     Copyright (c) 2008-2020 Juan-Pablo Caceres, Chris Chafe.
 >     SoundWIRE group at CCRMA, Stanford University
 
+
+## Building VST3 SDK for Windows
+
+```
+git clone --recursive https://github.com/steinbergmedia/vst3sdk
+mkdir vst3sdk/build
+cd vst3sdk/build
+cmake -G "Visual Studio 17 2022" -A x64 -DSMTG_CREATE_PLUGIN_LINK=0 ../
+cmake --build . -DCMAKE_CXX_FLAGS="/MD" --config Release
+mkdir c:\vst3sdk
+xcopy /E lib\Release c:\vst3sdk\lib\
+xcopy /E bin\Release c:\vst3sdk\bin\
+xcopy /E ..\base c:\vst3sdk\base\
+xcopy /E ..\pluginterfaces c:\vst3sdk\pluginterfaces\
+xcopy /E ..\public.sdk c:\vst3sdk\public.sdk\
+xcopy /E ..\vstgui4 c:\vst3sdk\vstgui4\
+```
+
+VST plugins are not allowed to have any shared library dependencies. You
+can currently only build it when using a static build of Qt. Note that
+this also requires configuring Meson without support for the GUI.
+
+When you run `meson setup` use `-Dnogui=true -Dvst-sdkdir=c:\vst3sdk`
+
+Please note that redistribution of JackTrip's VST3 plugin requires a
+[license from Steinberg](https://www.steinberg.net/developers/).
index 811e2ec4ce79da1ace38ea27befc60bf4d9b336a..e44a0ec824b91bcb6017528bb988dfd12612be69 100644 (file)
@@ -1,3 +1,19 @@
+- Version: "2.5.0"
+  Date: 2025-01-21
+  Description:
+  - (added) New JackTrip Audio Bridge VST3 Plugin
+  - (added) Sample rate conversion for audio interfaces
+  - (added) Automated arm64 and arm32 builds for Linux
+  - (added) Dynamic adjustment of PLC queues using OSC messages
+  - (updated) VS Mode remote control for audio quality slider
+  - (updated) VS Mode switch to using cookies for authentication
+  - (updated) PLC mode improvements in auto headroom calculations
+  - (fixed) PLC audio corruption when buffer sizes differ
+  - (fixed) PLC broadcast queue length when buffer sizes differ
+  - (fixed) Support for multiple commas in --audiodevice parameter
+  - (fixed) VS Mode access token expires after running for a day
+  - (fixed) VS Mode session feedback dialog closes on navigation
+  - (fixed) VS Mode deeplinks broken for first run after install
 - Version: "2.4.1"
   Date: 2024-09-27
   Description:
diff --git a/externals/oscpp/CMakeLists.txt b/externals/oscpp/CMakeLists.txt
new file mode 100644 (file)
index 0000000..003b2e3
--- /dev/null
@@ -0,0 +1,14 @@
+cmake_minimum_required(VERSION 3.9)
+project(oscpp VERSION 0.3.0)
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
+    set(LINUX TRUE)
+endif ()
+
+set(CMAKE_CXX_STANDARD 11)
+set(CMAKE_C_STANDARD 99)
+
+enable_testing()
+
+add_subdirectory(test)
+
diff --git a/externals/oscpp/LICENSE b/externals/oscpp/LICENSE
new file mode 100644 (file)
index 0000000..7d3e03b
--- /dev/null
@@ -0,0 +1,23 @@
+oscpp library
+
+Copyright (c) 2004-2018 Stefan Kersten <sk@declaredvolatile.org>
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN 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/oscpp/Makefile b/externals/oscpp/Makefile
new file mode 100644 (file)
index 0000000..6ed9240
--- /dev/null
@@ -0,0 +1,28 @@
+.PHONY: build clean distclean test v verbose
+
+NINJA_FLAGS = $(args)
+
+ifeq (1,$(or $(verbose),$(v)))
+NINJA_FLAGS += -v
+endif
+
+build: build/CMakeCache.txt
+       cd build && ninja $(NINJA_FLAGS)
+
+v: verbose
+
+verbose: NINJA_FLAGS += -v
+verbose: build
+
+build/CMakeCache.txt:
+       mkdir -p build
+       cd build && cmake -G Ninja ..
+
+clean:
+       cd build && ninja clean
+
+distclean:
+       rm -rf build
+
+test: build
+       cd build && ctest -V
diff --git a/externals/oscpp/README.md b/externals/oscpp/README.md
new file mode 100644 (file)
index 0000000..4919817
--- /dev/null
@@ -0,0 +1,274 @@
+[![Build Status](https://img.shields.io/travis/kaoskorobase/oscpp.svg?style=flat)](https://travis-ci.org/kaoskorobase/oscpp)
+[![Build status](https://ci.appveyor.com/api/projects/status/b7qk7t9mmgnc1n1v?svg=true)](https://ci.appveyor.com/project/kaoskorobase/oscpp)
+
+**oscpp** is a header-only C++11 library for constructing and parsing
+[OpenSoundControl](http://opensoundcontrol.org) packets. Supported platforms
+are MacOS X, iOS, Linux, Android and Windows; the code should be easily
+portable to any platform with a C++11 compiler. **oscpp** intends to be a
+minimal, high-performance solution for working with OSC data. The library
+doesn't perform memory allocation (except when throwing exceptions) or other
+system calls and is suitable for use in realtime sensitive contexts such as
+audio driver callbacks.
+
+**oscpp** conforms to the [OpenSoundControl 1.0
+specification](http://opensoundcontrol.org/spec-1_0). Except for arrays,
+non-standard message argument types are currently not supported and there is no
+direct support for message address patterns or bundle scheduling; it is up to
+the user of the library to implement (a subset of) the semantics according to
+the spec.
+
+## Installation
+
+Since **oscpp** only consists of header files, the library doesn't need to be
+compiled or installed. Simply put the `include` directory into a location that
+is searched by your compiler and you're set.
+
+## Usage
+
+**oscpp** places everything in the `OSCPP` namespace, with the two most
+important subnamespaces `Client` for constructing packets and `Server` for
+parsing packets.
+
+First let's have a look at how to build OSC packets in memory: Assuming you
+have allocated a buffer you can construct a client packet on the stack and
+start filling the buffer with data. When all the data has been written, the
+`size` method returns the actual size in bytes of the resulting OSC packet.
+
+~~~~cpp
+#include <oscpp/client.hpp>
+
+size_t makePacket(void* buffer, size_t size)
+{
+    // Construct a packet
+    OSCPP::Client::Packet packet(buffer, size);
+    packet
+        // Open a bundle with a timetag
+        .openBundle(1234ULL)
+            // Add a message with two arguments and an array with 6 elements;
+            // for efficiency this needs to be known in advance.
+            .openMessage("/s_new", 2 + OSCPP::Tags::array(6))
+                // Write the arguments
+                .string("sinesweep")
+                .int32(2)
+                .openArray()
+                    .string("start-freq")
+                    .float32(330.0f)
+                    .string("end-freq")
+                    .float32(990.0f)
+                    .string("amp")
+                    .float32(0.4f)
+                .closeArray()
+            // Every `open` needs a corresponding `close`
+            .closeMessage()
+            // Add another message with one argument
+            .openMessage("/n_free", 1)
+                .int32(1)
+            .closeMessage()
+            // And nother one
+            .openMessage("/n_set", 3)
+                .int32(1)
+                .string("wobble")
+                // Numeric arguments are converted automatically
+                // (see below)
+                .int32(31)
+            .closeMessage()
+        .closeBundle();
+    return packet.size();
+}
+~~~~
+
+Now given a suitable packet transport (e.g. a UDP socket or an in-memory FIFO,
+see below for a dummy implementation), a packet can be constructed and sent as
+follows:
+
+~~~~cpp
+class Transport;
+
+size_t send(Transport* t, const void* buffer, size_t size);
+
+void sendPacket(Transport* t, void* buffer, size_t bufferSize)
+{
+    const size_t packetSize = makePacket(buffer, bufferSize);
+    send(t, buffer, packetSize);
+}
+~~~~
+
+When parsing data from OSC packets you have to handle the two distinct cases of bundles and messages:
+
+~~~~cpp
+#include <oscpp/server.hpp>
+#include <oscpp/print.hpp>
+#include <iostream>
+
+void handlePacket(const OSCPP::Server::Packet& packet)
+{
+    if (packet.isBundle()) {
+        // Convert to bundle
+        OSCPP::Server::Bundle bundle(packet);
+
+        // Print the time
+        std::cout << "#bundle " << bundle.time() << std::endl;
+
+        // Get packet stream
+        OSCPP::Server::PacketStream packets(bundle.packets());
+
+        // Iterate over all the packets and call handlePacket recursively.
+        // Cuidado: Might lead to stack overflow!
+        while (!packets.atEnd()) {
+            handlePacket(packets.next());
+        }
+    } else {
+        // Convert to message
+        OSCPP::Server::Message msg(packet);
+
+        // Get argument stream
+        OSCPP::Server::ArgStream args(msg.args());
+
+        // Directly compare message address to string with operator==.
+        // For handling larger address spaces you could use e.g. a
+        // dispatch table based on std::unordered_map.
+        if (msg == "/s_new") {
+            const char* name = args.string();
+            const int32_t id = args.int32();
+            std::cout << "/s_new" << " "
+                      << name << " "
+                      << id << " ";
+            // Get the params array as an ArgStream
+            OSCPP::Server::ArgStream params(args.array());
+            while (!params.atEnd()) {
+                const char* param = params.string();
+                const float value = params.float32();
+                std::cout << param << ":" << value << " ";
+            }
+            std::cout << std::endl;
+        } else if (msg == "/n_set") {
+            const int32_t id = args.int32();
+            const char* key = args.string();
+            // Numeric arguments are converted automatically
+            // to float32 (e.g. from int32).
+            const float value = args.float32();
+            std::cout << "/n_set" << " "
+                      << id << " "
+                      << key << " "
+                      << value << std::endl;
+        } else {
+            // Simply print unknown messages
+            std::cout << "Unknown message: " << msg << std::endl;
+        }
+    }
+}
+~~~~
+
+Now we can receive data from a message based transport and pass it to our
+packet handling function:
+
+~~~~cpp
+#include <array>
+
+const size_t kMaxPacketSize = 8192;
+
+size_t recv(Transport* t, void* buffer, size_t size);
+
+void recvPacket(Transport* t)
+{
+    std::array<char,kMaxPacketSize> buffer;
+    size_t size = recv(t, buffer.data(), buffer.size());
+    handlePacket(OSCPP::Server::Packet(buffer.data(), size));
+}
+~~~~
+
+Here's our code in an example main function:
+
+~~~~cpp
+#include <memory>
+#include <stdexcept>
+
+Transport* newTransport();
+
+int main(int, char**)
+{
+    std::unique_ptr<Transport> t(newTransport());
+    std::array<char,kMaxPacketSize> sendBuffer;
+    try {
+        sendPacket(t.get(), sendBuffer.data(), sendBuffer.size());
+        recvPacket(t.get());
+    } catch (std::exception& e) {
+        std::cerr << "Exception: " << e.what() << std::endl;
+    }
+    return 0;
+}
+~~~~
+
+Compiling and running the example produces the following output:
+
+~~~~
+#bundle 1234
+/s_new sinesweep 2 start-freq:330 end-freq:990 amp:0.4
+Unknown message: /n_free i:1
+/n_set 1 wobble 31
+~~~~
+
+## How to run the example
+
+You can build and run the example by executing
+
+~~~~
+make README
+~~~~
+
+You'll need to install the [Haskell Platform](http://www.haskell.org/platform/)
+and the [Pandoc](http://johnmacfarlane.net/pandoc/) library:
+
+~~~~
+cabal install pandoc
+~~~~
+
+## Appendix: Support code
+
+Here's the code for a trivial transport that has a single packet buffer:
+
+~~~~cpp
+#include <cstring>
+
+class Transport
+{
+public:
+    size_t send(const void* buffer, size_t size)
+    {
+        size_t n = std::min(m_buffer.size(), size);
+        std::memcpy(m_buffer.data(), buffer, n);
+        m_message = n;
+        return n;
+    }
+
+    size_t recv(void* buffer, size_t size)
+    {
+        if (m_message > 0) {
+            size_t n = std::min(m_message, size);
+            std::memcpy(buffer, m_buffer.data(), n);
+            m_message = 0;
+            return n;
+        }
+        return 0;
+    }
+
+private:
+    std::array<char,kMaxPacketSize> m_buffer;
+    size_t m_message;
+};
+
+Transport* newTransport()
+{
+    return new Transport;
+}
+
+size_t send(Transport* t, const void* buffer, size_t size)
+{
+    return t->send(buffer, size);
+}
+
+size_t recv(Transport* t, void* buffer, size_t size)
+{
+    return t->recv(buffer, size);
+}
+~~~~
diff --git a/externals/oscpp/doxygen.cfg b/externals/oscpp/doxygen.cfg
new file mode 100644 (file)
index 0000000..4ddfcd4
--- /dev/null
@@ -0,0 +1,938 @@
+# Doxyfile 1.2.15
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project
+#
+# All text after a 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 (" ")
+
+#---------------------------------------------------------------------------
+# General configuration options
+#---------------------------------------------------------------------------
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded 
+# by quotes) that should identify the project.
+
+PROJECT_NAME           = "OSC Template Library"
+
+# 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         = "$Id"
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute) 
+# base path where the generated documentation will be put. 
+# 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       = doc
+
+# 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. 
+# The default language is English, other supported languages are: 
+# Brazilian, Chinese, Croatian, Czech, Danish, Dutch, Finnish, French, 
+# German, Greek, Hungarian, Italian, Japanese, Korean, Norwegian, Polish, 
+# Portuguese, Romanian, Russian, Slovak, Slovene, Spanish and Swedish.
+
+OUTPUT_LANGUAGE        = English
+
+# 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 and EXTRACT_STATIC tags are set to YES
+
+EXTRACT_ALL            = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class 
+# will be included in the documentation.
+
+EXTRACT_PRIVATE        = NO
+
+# If the EXTRACT_STATIC tag is set to YES all static members of a file 
+# will be included in the documentation.
+
+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.
+
+EXTRACT_LOCAL_CLASSES  = YES
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all 
+# undocumented members of documented classes, files or namespaces. 
+# If set to NO (the default) 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.
+
+HIDE_UNDOC_MEMBERS     = YES
+
+# 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 (the default) these class will be included in the various 
+# overviews. This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_CLASSES     = YES
+
+# If the BRIEF_MEMBER_DESC tag is set to YES (the default) 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.
+
+BRIEF_MEMBER_DESC      = YES
+
+# If the REPEAT_BRIEF tag is set to YES (the default) 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.
+
+REPEAT_BRIEF           = YES
+
+# 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.
+
+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.
+
+INLINE_INHERITED_MEMB  = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then 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.
+
+FULL_PATH_NAMES        = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then 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. It is allowed to use relative paths in the argument list.
+
+STRIP_FROM_PATH        = 
+
+# The INTERNAL_DOCS tag determines if documentation 
+# that is typed after a \internal command is included. If the tag is set 
+# to NO (the default) then the documentation will be excluded. 
+# Set it to YES to include the internal documentation.
+
+INTERNAL_DOCS          = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct 
+# doxygen to hide any special comment blocks from generated source code 
+# fragments. Normal C and C++ comments will always remain visible.
+
+STRIP_CODE_COMMENTS    = YES
+
+# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate 
+# file names in lower case letters. If set to YES upper case letters are also 
+# allowed. This is useful if you have classes or files whose names only differ 
+# in case and if your file system supports case sensitive file names. Windows 
+# users are adviced to set this option to NO.
+
+CASE_SENSE_NAMES       = YES
+
+# 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.
+
+SHORT_NAMES            = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen 
+# will show members with their full class and namespace scopes in the 
+# documentation. If set to YES the scope will be hidden.
+
+HIDE_SCOPE_NAMES       = NO
+
+# If the VERBATIM_HEADERS tag is set to YES (the default) 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.
+
+VERBATIM_HEADERS       = YES
+
+# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen 
+# will put list of the files that are included by a file in the documentation 
+# of that file.
+
+SHOW_INCLUDE_FILES     = YES
+
+# 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 
+# comments  will behave just like the Qt-style comments (thus requiring an 
+# explict @brief command for a brief description.
+
+JAVADOC_AUTOBRIEF      = NO
+
+# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented 
+# member inherits the documentation from any documented member that it 
+# reimplements.
+
+INHERIT_DOCS           = YES
+
+# If the INLINE_INFO tag is set to YES (the default) then a tag [inline] 
+# is inserted in the documentation for inline members.
+
+INLINE_INFO            = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES (the default) 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.
+
+SORT_MEMBER_DOCS       = 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.
+
+DISTRIBUTE_GROUP_DOC   = 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.
+
+TAB_SIZE               = 8
+
+# 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.
+
+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.
+
+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.
+
+GENERATE_BUGLIST       = YES
+
+# This tag can be used to specify a number of aliases that acts 
+# 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.
+
+ALIASES                = 
+
+# The ENABLED_SECTIONS tag can be used to enable conditional 
+# documentation sections, marked by \if sectionname ... \endif.
+
+ENABLED_SECTIONS       = 
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines 
+# the initial value of a variable or define consist of 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 initializer of individual variables and defines in the 
+# documentation can be controlled using \showinitializer or \hideinitializer 
+# command in the documentation regardless of this setting.
+
+MAX_INITIALIZER_LINES  = 30
+
+# 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.
+
+OPTIMIZE_OUTPUT_FOR_C  = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java sources 
+# only. Doxygen will then generate output that is more tailored for Java. 
+# For instance namespaces will be presented as packages, qualified scopes 
+# will look different, etc.
+
+OPTIMIZE_OUTPUT_JAVA   = NO
+
+# 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.
+
+SHOW_USED_FILES        = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated 
+# by doxygen. Possible values are YES and NO. If left blank NO is used.
+
+QUIET                  = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are 
+# generated by doxygen. Possible values are YES and NO. If left blank 
+# NO is used.
+
+WARNINGS               = YES
+
+# If WARN_IF_UNDOCUMENTED 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.
+
+WARN_IF_UNDOCUMENTED   = YES
+
+# 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.
+
+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 stderr.
+
+WARN_LOGFILE           = 
+
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag can be 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.
+
+INPUT                  = .
+
+# If the value of the INPUT tag contains directories, you can use the 
+# FILE_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 the following patterns are tested: 
+# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx *.hpp 
+# *.h++ *.idl *.odl
+
+FILE_PATTERNS          = *.hh
+
+# The RECURSIVE tag can be used to turn specify whether or not subdirectories 
+# should be searched for input files as well. Possible values are YES and NO. 
+# If left blank NO is used.
+
+RECURSIVE              = NO
+
+# The EXCLUDE tag can be used to specify files and/or directories that should 
+# 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.
+
+EXCLUDE                = 
+
+# The EXCLUDE_SYMLINKS tag can be used select whether or not files or directories 
+# that are symbolic links (a Unix filesystem feature) are excluded from the input.
+
+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.
+
+EXCLUDE_PATTERNS       = 
+
+# 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. 
+# Possible values are YES and NO. If left blank NO is used.
+
+EXAMPLE_RECURSIVE      = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or 
+# directories that contain image that are included in the documentation (see 
+# the \image command).
+
+IMAGE_PATH             = 
+
+# 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.
+
+INPUT_FILTER           = 
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using 
+# INPUT_FILTER) will be used to filter the input files when producing source 
+# files to browse.
+
+FILTER_SOURCE_FILES    = NO
+
+#---------------------------------------------------------------------------
+# 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.
+
+SOURCE_BROWSER         = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body 
+# of functions and classes directly in the documentation.
+
+INLINE_SOURCES         = NO
+
+# If the REFERENCED_BY_RELATION tag is set to YES (the default) 
+# then for each documented function all documented 
+# functions referencing it will be listed.
+
+REFERENCED_BY_RELATION = YES
+
+# If the REFERENCES_RELATION tag is set to YES (the default) 
+# then for each documented function all documented entities 
+# called/used by that function will be listed.
+
+REFERENCES_RELATION    = YES
+
+#---------------------------------------------------------------------------
+# 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.
+
+ALPHABETICAL_INDEX     = NO
+
+# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then 
+# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns 
+# in which this list will be split (can be a number in the range [1..20])
+
+COLS_IN_ALPHA_INDEX    = 5
+
+# 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 one or more prefixes that 
+# should be ignored while generating the index headers.
+
+IGNORE_PREFIX          = 
+
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES (the default) Doxygen will 
+# generate HTML output.
+
+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. If left blank `html' will be used as the default path.
+
+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). If it is left blank 
+# doxygen will generate files with .html extension.
+
+HTML_FILE_EXTENSION    = .html
+
+# The HTML_HEADER tag can be used to specify a personal HTML header for 
+# each generated HTML page. If it is left blank doxygen will generate a 
+# standard header.
+
+HTML_HEADER            = 
+
+# The HTML_FOOTER tag can be used to specify a personal HTML footer for 
+# each generated HTML page. If it is left blank doxygen will generate a 
+# standard footer.
+
+HTML_FOOTER            = 
+
+# 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 the tag is left blank doxygen 
+# will generate a default style sheet
+
+HTML_STYLESHEET        = 
+
+# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes, 
+# files or namespaces will be aligned in HTML using tables. If set to 
+# NO a bullet list will be used.
+
+HTML_ALIGN_MEMBERS     = YES
+
+# If the GENERATE_HTMLHELP tag is set to YES, additional index files 
+# will be generated that can be used as input for tools like the 
+# Microsoft HTML help workshop to generate a compressed HTML help file (.chm) 
+# of the generated HTML documentation.
+
+GENERATE_HTMLHELP      = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag 
+# controls if a separate .chi index file is generated (YES) or that 
+# it should be included in the master .chm file (NO).
+
+GENERATE_CHI           = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, 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.
+
+BINARY_TOC             = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members 
+# to the contents of the Html help documentation and to the tree view.
+
+TOC_EXPAND             = NO
+
+# The DISABLE_INDEX tag can be used to turn on/off the condensed index at 
+# top of each HTML page. The value NO (the default) enables the index and 
+# the value YES disables it.
+
+DISABLE_INDEX          = NO
+
+# This tag can be used to set the number of enum values (range [1..20]) 
+# that doxygen will group on one line in the generated HTML documentation.
+
+ENUM_VALUES_PER_LINE   = 4
+
+# If the GENERATE_TREEVIEW tag 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 and frames is required (for instance Mozilla, Netscape 4.0+, 
+# or Internet explorer 4.0+). Note that for large projects the tree generation 
+# can take a very long time. In such cases it is better to disable this feature. 
+# Windows users are probably better off using the HTML help feature.
+
+GENERATE_TREEVIEW      = NO
+
+# 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.
+
+TREEVIEW_WIDTH         = 250
+
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will 
+# generate Latex output.
+
+GENERATE_LATEX         = YES
+
+# 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. If left blank `latex' will be used as the default path.
+
+LATEX_OUTPUT           = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be invoked. If left blank `latex' will be used as the default command name.
+
+LATEX_CMD_NAME         = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to 
+# generate index for LaTeX. If left blank `makeindex' will be used as the 
+# default command name.
+
+MAKEINDEX_CMD_NAME     = 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.
+
+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, a4wide, letter, legal and 
+# executive. If left blank a4wide will be used.
+
+PAPER_TYPE             = a4wide
+
+# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX 
+# packages that should be included in the LaTeX output.
+
+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. Notice: only use this tag if you know what you are doing!
+
+LATEX_HEADER           = 
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated 
+# is prepared for conversion to pdf (using ps2pdf). 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.
+
+PDF_HYPERLINKS         = NO
+
+# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of 
+# plain latex in the generated Makefile. Set this option to YES to get a 
+# higher quality PDF documentation.
+
+USE_PDFLATEX           = NO
+
+# 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.
+
+LATEX_BATCHMODE        = NO
+
+#---------------------------------------------------------------------------
+# 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 optimised for Word 97 and may not look very pretty with 
+# other RTF readers or editors.
+
+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. If left blank `rtf' will be used as the default path.
+
+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.
+
+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 other 
+# programs which support those fields. 
+# Note: wordpad (write) and others do not support links.
+
+RTF_HYPERLINKS         = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's 
+# config file, i.e. a series of assigments. You only have to provide 
+# replacements, missing definitions are set to their default value.
+
+RTF_STYLESHEET_FILE    = 
+
+# Set optional variables used in the generation of an rtf document. 
+# Syntax is similar to doxygen's config file.
+
+RTF_EXTENSIONS_FILE    = 
+
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES (the default) Doxygen will 
+# generate man pages
+
+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. If left blank `man' will be used as the default path.
+
+MAN_OUTPUT             = man
+
+# The MAN_EXTENSION tag determines the extension that is added to 
+# the generated man pages (default is the subroutine's section .3)
+
+MAN_EXTENSION          = .3
+
+# 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 is NO.
+
+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. Note that this 
+# feature is still experimental and incomplete at the 
+# moment.
+
+GENERATE_XML           = 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 autogen.sf.net) file 
+# that captures the structure of the code including all 
+# documentation. Note that this feature is still experimental 
+# and incomplete at the moment.
+
+GENERATE_AUTOGEN_DEF   = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor   
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will 
+# evaluate all C-preprocessor directives found in the sources and include 
+# files.
+
+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 (the default) only conditional 
+# compilation will be performed. Macro expansion can be done in a controlled 
+# way by setting EXPAND_ONLY_PREDEF 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_PREDEFINED tags.
+
+EXPAND_ONLY_PREDEF     = NO
+
+# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files 
+# in the INCLUDE_PATH (see below) will be search if a #include is found.
+
+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.
+
+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.
+
+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 
+# 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.
+
+PREDEFINED             = 
+
+# If the MACRO_EXPANSION and EXPAND_PREDEF_ONLY 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.
+
+EXPAND_AS_DEFINED      = 
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then 
+# doxygen's preprocessor will remove all function-like macros that are alone 
+# on a line 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.
+
+SKIP_FUNCTION_MACROS   = YES
+
+#---------------------------------------------------------------------------
+# Configuration::addtions related to external references   
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tagfiles.
+
+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.
+
+GENERATE_TAGFILE       = 
+
+# If the ALLEXTERNALS tag is set to YES all external classes will be listed 
+# in the class index. If set to NO only the inherited external classes 
+# will be listed.
+
+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.
+
+EXTERNAL_GROUPS        = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script 
+# interpreter (i.e. the result of `which perl').
+
+PERL_PATH              = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool   
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will 
+# generate a inheritance diagram (in Html, RTF and LaTeX) for classes with base or 
+# super classes. Setting the tag to NO turns the diagrams off. Note that this 
+# option is superceded by the HAVE_DOT option below. This is only a fallback. It is 
+# recommended to install and use dot, since it yield more powerful graphs.
+
+CLASS_DIAGRAMS         = 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, 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)
+
+HAVE_DOT               = NO
+
+# If the CLASS_GRAPH and HAVE_DOT tags are 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 
+# the CLASS_DIAGRAMS tag to NO.
+
+CLASS_GRAPH            = YES
+
+# If the COLLABORATION_GRAPH and HAVE_DOT tags are 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.
+
+COLLABORATION_GRAPH    = YES
+
+# If set to YES, the inheritance and collaboration graphs will show the 
+# relations between templates and their instances.
+
+TEMPLATE_RELATIONS     = YES
+
+# 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.
+
+HIDE_UNDOC_RELATIONS   = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT 
+# 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.
+
+INCLUDE_GRAPH          = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and 
+# HAVE_DOT tags are set to YES then doxygen will generate a graph for each 
+# documented header file showing the documented files that directly or 
+# indirectly include this file.
+
+INCLUDED_BY_GRAPH      = YES
+
+# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen 
+# will graphical hierarchy of all classes instead of a textual one.
+
+GRAPHICAL_HIERARCHY    = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images 
+# generated by dot. Possible values are png, jpg, or gif
+# If left blank png will be used.
+
+DOT_IMAGE_FORMAT       = png
+
+# The tag DOT_PATH 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 on the path.
+
+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).
+
+DOTFILE_DIRS           = 
+
+# The MAX_DOT_GRAPH_WIDTH tag can be used to set the maximum allowed width 
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than 
+# this value, doxygen will try to truncate the graph, so that it fits within 
+# the specified constraint. Beware that most browsers cannot cope with very 
+# large images.
+
+MAX_DOT_GRAPH_WIDTH    = 1024
+
+# The MAX_DOT_GRAPH_HEIGHT tag can be used to set the maximum allows height 
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than 
+# this value, doxygen will try to truncate the graph, so that it fits within 
+# the specified constraint. Beware that most browsers cannot cope with very 
+# large images.
+
+MAX_DOT_GRAPH_HEIGHT   = 1024
+
+# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will 
+# generate a legend page explaining the meaning of the various boxes and 
+# arrows in the dot generated graphs.
+
+GENERATE_LEGEND        = YES
+
+# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will 
+# remove the intermedate dot files that are used to generate 
+# the various graphs.
+
+DOT_CLEANUP            = YES
+
+#---------------------------------------------------------------------------
+# Configuration::addtions related to the search engine   
+#---------------------------------------------------------------------------
+
+# The SEARCHENGINE tag specifies whether or not a search engine should be 
+# used. If set to NO the values of all tags below this one will be ignored.
+
+SEARCHENGINE           = NO
+
+# The CGI_NAME tag should be the name of the CGI script that 
+# starts the search engine (doxysearch) with the correct parameters. 
+# A script with this name will be generated by doxygen.
+
+CGI_NAME               = search.cgi
+
+# The CGI_URL tag should be the absolute URL to the directory where the 
+# cgi binaries are located. See the documentation of your http daemon for 
+# details.
+
+CGI_URL                = 
+
+# The DOC_URL tag should be the absolute URL to the directory where the 
+# documentation is located. If left blank the absolute path to the 
+# documentation, with file:// prepended to it, will be used.
+
+DOC_URL                = 
+
+# The DOC_ABSPATH tag should be the absolute path to the directory where the 
+# documentation is located. If left blank the directory on the local machine 
+# will be used.
+
+DOC_ABSPATH            = 
+
+# The BIN_ABSPATH tag must point to the directory where the doxysearch binary 
+# is installed.
+
+BIN_ABSPATH            = /usr/local/bin/
+
+# The EXT_DOC_PATHS tag can be used to specify one or more paths to 
+# documentation generated for other projects. This allows doxysearch to search 
+# the documentation for these projects as well.
+
+EXT_DOC_PATHS          = 
diff --git a/externals/oscpp/include/oscpp/client.hpp b/externals/oscpp/include/oscpp/client.hpp
new file mode 100644 (file)
index 0000000..36cfd92
--- /dev/null
@@ -0,0 +1,368 @@
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_CLIENT_HPP_INCLUDED
+#define OSCPP_CLIENT_HPP_INCLUDED
+
+#include <oscpp/detail/host.hpp>
+#include <oscpp/detail/stream.hpp>
+#include <oscpp/util.hpp>
+
+#include <cstdint>
+#include <limits>
+#include <sstream>
+#include <stdexcept>
+#include <type_traits>
+
+namespace OSCPP { namespace Client {
+
+//! OSC packet construction.
+/*!
+ * Construct a valid OSC packet for transmitting over a transport
+ * medium.
+ */
+class Packet
+{
+    int32_t ptrDiff(const char* a, const char* b)
+    {
+        // Make sure pointer difference fits into int32_t
+        const intptr_t diff = a - b;
+        if (diff < std::numeric_limits<int32_t>::min() ||
+            diff > std::numeric_limits<int32_t>::max())
+        {
+            std::stringstream s;
+            s << "Pointer difference " << diff
+              << " can't be represented by int32_t";
+            throw std::logic_error(s.str());
+        }
+        return static_cast<int32_t>(diff);
+    }
+
+    int32_t calcSize(const char* begin, const char* end)
+    {
+        const int32_t size = ptrDiff(end, begin) - 4;
+        if (size < 0)
+        {
+            throw std::logic_error("Calculated size is negative");
+        }
+        return size;
+    }
+
+public:
+    //! Constructor.
+    /*!
+     */
+    Packet()
+    {
+        reset(0, 0);
+    }
+
+    //! Constructor.
+    /*!
+     */
+    Packet(void* buffer, size_t size)
+    {
+        reset(buffer, size);
+    }
+
+    //! Destructor.
+    virtual ~Packet()
+    {}
+
+    //! Get packet buffer address.
+    /*!
+     * Return the start address of the packet currently under
+     * construction.
+     */
+    void* data() const
+    {
+        return m_buffer;
+    }
+
+    size_t capacity() const
+    {
+        return m_capacity;
+    }
+
+    //! Get packet content size.
+    /*!
+     * Return the size of the packet currently under construction.
+     */
+    size_t size() const
+    {
+        return m_args.consumed();
+    }
+
+    //! Reset packet state.
+    void reset(void* buffer, size_t size)
+    {
+        checkAlignment(&m_buffer, kAlignment);
+        m_buffer = buffer;
+        m_capacity = size;
+        m_args = WriteStream(m_buffer, m_capacity);
+        m_sizePosM = m_sizePosB = nullptr;
+        m_inBundle = 0;
+    }
+
+    void reset()
+    {
+        reset(m_buffer, m_capacity);
+    }
+
+    Packet& openBundle(uint64_t time)
+    {
+        if (m_inBundle > 0)
+        {
+            assert(m_sizePosB != nullptr || m_inBundle == 1);
+            // Remember previous size pos offset
+            const int32_t offset =
+                m_sizePosB == nullptr ? 0 : ptrDiff(m_sizePosB, m_args.begin());
+            char* curPos = m_args.pos();
+            m_args.skip(4);
+            // Record size pos
+            std::memcpy(curPos, &offset, 4);
+            m_sizePosB = curPos;
+        }
+        else if (m_args.pos() != m_args.begin())
+        {
+            throw std::logic_error(
+                "Cannot open toplevel bundle in non-empty packet");
+        }
+
+        m_inBundle++;
+        m_args.putString("#bundle");
+        m_args.putUInt64(time);
+        return *this;
+    }
+
+    Packet& closeBundle()
+    {
+        if (m_inBundle > 0)
+        {
+            if (m_inBundle > 1)
+            {
+                // Get current stream pos
+                char* curPos = m_args.pos();
+
+                // Get previous bundle size stream pos
+                int32_t offset;
+                memcpy(&offset, m_sizePosB, 4);
+                // Get previous size pos
+                char* prevPos = m_args.begin() + offset;
+
+                const int32_t bundleSize = calcSize(m_sizePosB, curPos);
+                assert(bundleSize >= 0 &&
+                       (size_t)bundleSize >= Size::bundle(0));
+                // Write bundle size
+                m_args.setPos(m_sizePosB);
+                m_args.putInt32(bundleSize);
+                m_args.setPos(curPos);
+
+                // record outer bundle size pos
+                m_sizePosB = prevPos;
+            }
+            m_inBundle--;
+        }
+        else
+        {
+            throw std::logic_error(
+                "closeBundle() without matching openBundle()");
+        }
+        return *this;
+    }
+
+    Packet& openMessage(const char* addr, size_t numTags)
+    {
+        if (m_inBundle > 0)
+        {
+            // record message size pos
+            m_sizePosM = m_args.pos();
+            // advance arg stream
+            m_args.skip(4);
+        }
+        m_args.putString(addr);
+        size_t sigLen = numTags + 2;
+        m_tags = WriteStream(m_args, sigLen);
+        m_args.zero(align(sigLen));
+        m_tags.putChar(',');
+        return *this;
+    }
+
+    Packet& closeMessage()
+    {
+        if (m_inBundle > 0)
+        {
+            // Get current stream pos
+            char* curPos = m_args.pos();
+            // write message size
+            m_args.setPos(m_sizePosM);
+            m_args.putInt32(calcSize(m_sizePosM, curPos));
+            // restore stream pos
+            m_args.setPos(curPos);
+            // reset tag stream
+            m_tags = WriteStream();
+        }
+        return *this;
+    }
+
+    //! Write integer message argument.
+    /*!
+     * Write a 32 bit integer message argument.
+     *
+     * \param arg 32 bit integer argument.
+     *
+     * \pre openMessage must have been called before with no intervening
+     * closeMessage.
+     *
+     * \throw OSCPP::XRunError stream buffer xrun.
+     */
+    Packet& int32(int32_t arg)
+    {
+        m_tags.putChar('i');
+        m_args.putInt32(arg);
+        return *this;
+    }
+
+    Packet& float32(float arg)
+    {
+        m_tags.putChar('f');
+        m_args.putFloat32(arg);
+        return *this;
+    }
+
+    Packet& string(const char* arg)
+    {
+        m_tags.putChar('s');
+        m_args.putString(arg);
+        return *this;
+    }
+
+    // @throw std::invalid_argument if blob size is greater than
+    // std::numeric_limits<int32_t>::max()
+    Packet& blob(const Blob& arg)
+    {
+        if (arg.size() > (size_t)std::numeric_limits<int32_t>::max())
+        {
+            throw std::invalid_argument("Blob size greater than maximum "
+                                        "value representable by int32_t");
+        }
+        m_tags.putChar('b');
+        m_args.putInt32(static_cast<int32_t>(arg.size()));
+        m_args.putData(arg.data(), arg.size());
+        return *this;
+    }
+
+    Packet& openArray()
+    {
+        m_tags.putChar('[');
+        return *this;
+    }
+
+    Packet& closeArray()
+    {
+        m_tags.putChar(']');
+        return *this;
+    }
+
+    template <typename T> Packet& put(T)
+    {
+        T::OSC_Client_Packet_put_unimplemented;
+        return *this;
+    }
+
+    template <typename InputIterator>
+    Packet& put(InputIterator begin, InputIterator end)
+    {
+        for (auto it = begin; it != end; it++)
+        {
+            put(*it);
+        }
+        return *this;
+    }
+
+    template <typename InputIterator>
+    Packet& putArray(InputIterator begin, InputIterator end)
+    {
+        openArray();
+        put<InputIterator>(begin, end);
+        closeArray();
+        return *this;
+    }
+
+private:
+    void*       m_buffer;
+    size_t      m_capacity;
+    WriteStream m_args;     // packet stream
+    WriteStream m_tags;     // current tag stream
+    char*       m_sizePosM; // last message size position
+    char*       m_sizePosB; // last bundle size position
+    size_t      m_inBundle; // bundle nesting depth
+};
+
+template <> inline Packet& Packet::put<int32_t>(int32_t x)
+{
+    return int32(x);
+}
+template <> inline Packet& Packet::put<float>(float x)
+{
+    return float32(x);
+}
+template <> inline Packet& Packet::put<const char*>(const char* x)
+{
+    return string(x);
+}
+template <> inline Packet& Packet::put<Blob>(Blob x)
+{
+    return blob(x);
+}
+
+template <size_t buffer_size> class StaticPacket : public Packet
+{
+public:
+    StaticPacket()
+    : Packet(reinterpret_cast<char*>(&m_buffer), buffer_size)
+    {}
+
+private:
+    typedef typename std::aligned_storage<buffer_size, kAlignment>::type
+                  AlignedBuffer;
+    AlignedBuffer m_buffer;
+};
+
+class DynamicPacket : public Packet
+{
+public:
+    DynamicPacket(size_t buffer_size)
+    : Packet(static_cast<char*>(new char[buffer_size]), buffer_size)
+    {}
+
+    ~DynamicPacket()
+    {
+        delete[] static_cast<char*>(data());
+    }
+};
+
+}} // namespace OSCPP::Client
+
+#endif // OSCPP_CLIENT_HPP_INCLUDED
diff --git a/externals/oscpp/include/oscpp/detail/endian.hpp b/externals/oscpp/include/oscpp/detail/endian.hpp
new file mode 100644 (file)
index 0000000..f9a0c2e
--- /dev/null
@@ -0,0 +1,82 @@
+// Copyright 2005 Caleb Epstein
+// Copyright 2006 John Maddock
+// Copyright 2010 Rene Rivera
+// Distributed under the Boost Software License, Version 1.0. (See accompany-
+// ing file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+/*
+ * Copyright (c) 1997
+ * Silicon Graphics Computer Systems, Inc.
+ *
+ * Permission to use, copy, modify, distribute and sell this software
+ * and its documentation for any purpose is hereby granted without fee,
+ * provided that the above copyright notice appear in all copies and
+ * that both that copyright notice and this permission notice appear
+ * in supporting documentation.  Silicon Graphics makes no
+ * representations about the suitability of this software for any
+ * purpose.  It is provided "as is" without express or implied warranty.
+ */
+
+/*
+ * Copyright notice reproduced from <boost/detail/limits.hpp>, from
+ * which this code was originally taken.
+ *
+ * Modified by Caleb Epstein to use <endian.h> with GNU libc and to
+ * defined the BOOST_ENDIAN macro.
+ */
+
+/*
+ * Modifications for oscpp by Stefan Kersten
+ *  - Change prefix from BOOST to OSCPP
+ *  - Remove PDP endianness
+ *  - Add OSCPP_BYTE_ORDER_* macros
+ */
+
+#ifndef OSCPP_ENDIAN_HPP_INCLUDED
+#define OSCPP_ENDIAN_HPP_INCLUDED
+
+#define OSCPP_BYTE_ORDER_BIG_ENDIAN 4321
+#define OSCPP_BYTE_ORDER_LITTLE_ENDIAN 1234
+
+// GNU libc offers the helpful header <endian.h> which defines
+// __BYTE_ORDER
+
+#if defined(__GLIBC__) || defined(__ANDROID__)
+#    include <endian.h>
+#    if (__BYTE_ORDER == __LITTLE_ENDIAN)
+#        define OSCPP_LITTLE_ENDIAN
+#    elif (__BYTE_ORDER == __BIG_ENDIAN)
+#        define OSCPP_BIG_ENDIAN
+#    else
+#        error Unknown machine endianness detected.
+#    endif
+#    define OSCPP_BYTE_ORDER __BYTE_ORDER
+#elif defined(_BIG_ENDIAN) && !defined(_LITTLE_ENDIAN) ||     \
+    defined(__BIG_ENDIAN__) && !defined(__LITTLE_ENDIAN__) || \
+    defined(_STLP_BIG_ENDIAN) && !defined(_STLP_LITTLE_ENDIAN)
+#    define OSCPP_BIG_ENDIAN
+#    define OSCPP_BYTE_ORDER OSCPP_BYTE_ORDER_BIG_ENDIAN
+#elif defined(_LITTLE_ENDIAN) && !defined(_BIG_ENDIAN) ||     \
+    defined(__LITTLE_ENDIAN__) && !defined(__BIG_ENDIAN__) || \
+    defined(_STLP_LITTLE_ENDIAN) && !defined(_STLP_BIG_ENDIAN)
+#    define OSCPP_LITTLE_ENDIAN
+#    define OSCPP_BYTE_ORDER OSCPP_BYTE_ORDER_LITTLE_ENDIAN
+#elif defined(__sparc) || defined(__sparc__) || defined(_POWER) || \
+    defined(__powerpc__) || defined(__ppc__) || defined(__hpux) || \
+    defined(__hppa) || defined(_MIPSEB) || defined(_POWER) ||      \
+    defined(__s390__)
+#    define OSCPP_BIG_ENDIAN
+#    define OSCPP_BYTE_ORDER OSCPP_BYTE_ORDER_BIG_ENDIAN
+#elif defined(__i386__) || defined(__alpha__) || defined(__ia64) ||  \
+    defined(__ia64__) || defined(_M_IX86) || defined(_M_IA64) ||     \
+    defined(_M_ALPHA) || defined(__amd64) || defined(__amd64__) ||   \
+    defined(_M_AMD64) || defined(__x86_64) || defined(__x86_64__) || \
+    defined(_M_X64) || defined(__bfin__)
+
+#    define OSCPP_LITTLE_ENDIAN
+#    define OSCPP_BYTE_ORDER OSCPP_BYTE_ORDER_LITTLE_ENDIAN
+#else
+#    error The file oscpp/endian.hpp needs to be set up for your CPU type.
+#endif
+
+#endif // OSCPP_ENDIAN_HPP_INCLUDED
diff --git a/externals/oscpp/include/oscpp/detail/host.hpp b/externals/oscpp/include/oscpp/detail/host.hpp
new file mode 100644 (file)
index 0000000..3aea894
--- /dev/null
@@ -0,0 +1,118 @@
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_HOST_HPP_INCLUDED
+#define OSCPP_HOST_HPP_INCLUDED
+
+#include <oscpp/detail/endian.hpp>
+
+#include <cstdint>
+#include <stdexcept>
+
+namespace OSCPP {
+#if defined(__GNUC__)
+inline static uint32_t bswap32(uint32_t x)
+{
+    return __builtin_bswap32(x);
+}
+inline static uint64_t bswap64(uint64_t x)
+{
+    return __builtin_bswap64(x);
+}
+#elif defined(_WINDOWS_) || defined(_WIN32)
+#    include <stdlib.h>
+inline static uint32_t bswap32(uint32_t x)
+{
+    return _byteswap_ulong(x);
+}
+inline static uint64_t bswap64(uint64_t x)
+{
+    return _byteswap_uint64(x);
+}
+#else
+// Fallback implementation
+#    warning Using unoptimized byte swap functions
+
+inline static uint32_t bswap32(uint32_t x)
+{
+    const uint32_t b1 = x << 24;
+    const uint32_t b2 = (x & 0x0000FF00) << 8;
+    const uint32_t b3 = (x & 0x00FF0000) >> 8;
+    const uint32_t b4 = x >> 24;
+    return b1 | b2 | b3 | b4;
+}
+inline static uint64_t bswap64(int64_t x)
+{
+    const uint64_t w1 = oscpp_bswap(uint32_t(x & 0x00000000FFFFFFFF)) << 32;
+    const uint64_t w2 = oscpp_bswap(uint32_t(x >> 32));
+    return w1 | w2;
+}
+#endif
+
+enum ByteOrder
+{
+    NetworkByteOrder,
+    HostByteOrder
+};
+
+template <ByteOrder B> inline uint32_t convert32(uint32_t)
+{
+    throw std::logic_error("Invalid byte order");
+}
+
+template <> inline uint32_t convert32<NetworkByteOrder>(uint32_t x)
+{
+#if defined(OSCPP_LITTLE_ENDIAN)
+    return bswap32(x);
+#else
+    return x;
+#endif
+}
+
+template <> inline uint32_t convert32<HostByteOrder>(uint32_t x)
+{
+    return x;
+}
+
+template <ByteOrder B> inline uint64_t convert64(uint64_t)
+{
+    throw std::logic_error("Invalid byte order");
+}
+
+template <> inline uint64_t convert64<NetworkByteOrder>(uint64_t x)
+{
+#if defined(OSCPP_LITTLE_ENDIAN)
+    return bswap64(x);
+#else
+    return x;
+#endif
+}
+
+template <> inline uint64_t convert64<HostByteOrder>(uint64_t x)
+{
+    return x;
+}
+} // namespace OSCPP
+
+#endif // OSCPP_HOST_HPP_INCLUDED
diff --git a/externals/oscpp/include/oscpp/detail/stream.hpp b/externals/oscpp/include/oscpp/detail/stream.hpp
new file mode 100644 (file)
index 0000000..67b064a
--- /dev/null
@@ -0,0 +1,365 @@
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_STREAM_HPP_INCLUDED
+#define OSCPP_STREAM_HPP_INCLUDED
+
+#include <oscpp/detail/host.hpp>
+#include <oscpp/error.hpp>
+#include <oscpp/types.hpp>
+#include <oscpp/util.hpp>
+
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <cstring>
+
+namespace OSCPP {
+
+class Stream
+{
+public:
+    Stream()
+    {
+        m_begin = m_end = m_pos = 0;
+    }
+
+    Stream(void* data, size_t size)
+    {
+        m_begin = static_cast<char*>(data);
+        m_end = m_begin + size;
+        m_pos = m_begin;
+    }
+
+    Stream(const Stream& stream, size_t size)
+    {
+        m_begin = m_pos = stream.m_pos;
+        m_end = m_begin + size;
+        if (m_end > stream.m_end)
+            throw UnderrunError();
+    }
+
+    void reset()
+    {
+        m_pos = m_begin;
+    }
+
+    const char* begin() const
+    {
+        return m_begin;
+    }
+
+    char* begin()
+    {
+        return m_begin;
+    }
+
+    const char* end() const
+    {
+        return m_end;
+    }
+
+    size_t capacity() const
+    {
+        return end() - begin();
+    }
+
+    const char* pos() const
+    {
+        return m_pos;
+    }
+
+    char* pos()
+    {
+        return m_pos;
+    }
+
+    void setPos(char* pos)
+    {
+        assert((pos >= m_begin) && (pos <= m_end));
+        m_pos = pos;
+    }
+
+    void advance(size_t n)
+    {
+        m_pos += n;
+    }
+
+    bool atEnd() const
+    {
+        return pos() == end();
+    }
+
+    size_t consumed() const
+    {
+        return pos() - begin();
+    }
+
+    size_t consumable() const
+    {
+        return end() - pos();
+    }
+
+    inline void checkAlignment(size_t n) const
+    {
+        OSCPP::checkAlignment(pos(), n);
+    }
+
+protected:
+    char* m_begin;
+    char* m_end;
+    char* m_pos;
+};
+
+template <ByteOrder B> class BasicWriteStream : public Stream
+{
+public:
+    BasicWriteStream()
+    : Stream()
+    {}
+
+    BasicWriteStream(void* data, size_t size)
+    : Stream(data, size)
+    {}
+
+    BasicWriteStream(const BasicWriteStream& stream, size_t size)
+    : Stream(stream, size)
+    {}
+
+    // throw (OverflowError)
+    inline void checkWritable(size_t n) const
+    {
+        if (consumable() < n)
+            throw OverflowError(n - consumable());
+    }
+
+    void skip(size_t n)
+    {
+        checkWritable(n);
+        advance(n);
+    }
+
+    void zero(size_t n)
+    {
+        checkWritable(n);
+        std::memset(m_pos, 0, n);
+        advance(n);
+    }
+
+    void putChar(char c)
+    {
+        checkWritable(1);
+        *pos() = c;
+        advance(1);
+    }
+
+    void putInt32(int32_t x)
+    {
+        checkWritable(4);
+        checkAlignment(4);
+        uint32_t uh;
+        memcpy(&uh, &x, 4);
+        const uint32_t un = convert32<B>(uh);
+        std::memcpy(pos(), &un, 4);
+        advance(4);
+    }
+
+    void putUInt64(uint64_t x)
+    {
+        checkWritable(8);
+        const uint64_t un = convert64<B>(x);
+        std::memcpy(pos(), &un, 8);
+        advance(8);
+    }
+
+    void putFloat32(float f)
+    {
+        checkWritable(4);
+        checkAlignment(4);
+        uint32_t uh;
+        std::memcpy(&uh, &f, 4);
+        const uint32_t un = convert32<B>(uh);
+        std::memcpy(pos(), &un, 4);
+        advance(4);
+    }
+
+    void putFloat64(double f)
+    {
+        checkWritable(8);
+        checkAlignment(4);
+        uint64_t uh;
+        std::memcpy(&uh, &f, 8);
+        const uint64_t un = convert64<B>(uh);
+        std::memcpy(pos(), &un, 8);
+        advance(8);
+    }
+
+    void putData(const void* data, size_t size)
+    {
+        const size_t padding = OSCPP::padding(size);
+        const size_t n = size + padding;
+        checkWritable(n);
+        std::memcpy(pos(), data, size);
+        std::memset(pos() + size, 0, padding);
+        advance(n);
+    }
+
+    void putString(const char* s)
+    {
+        putData(s, strlen(s) + 1);
+    }
+};
+
+typedef BasicWriteStream<NetworkByteOrder> WriteStream;
+
+template <ByteOrder B> class BasicReadStream : public Stream
+{
+public:
+    BasicReadStream()
+    {}
+
+    BasicReadStream(const void* data, size_t size)
+    : Stream(const_cast<void*>(data), size)
+    {}
+
+    BasicReadStream(const BasicReadStream& stream, size_t size)
+    : Stream(stream, size)
+    {}
+
+    // throw (UnderrunError)
+    void checkReadable(size_t n) const
+    {
+        if (consumable() < n)
+            throw UnderrunError();
+    }
+
+    // throw (UnderrunError)
+    void skip(size_t n)
+    {
+        checkReadable(n);
+        advance(n);
+    }
+
+    // throw (UnderrunError)
+    inline char peekChar() const
+    {
+        checkReadable(1);
+        return *pos();
+    }
+
+    // throw (UnderrunError)
+    inline char getChar()
+    {
+        const char x = peekChar();
+        advance(1);
+        return x;
+    }
+
+    // throw (UnderrunError)
+    inline int32_t peekInt32() const
+    {
+        checkReadable(4);
+        checkAlignment(4);
+        uint32_t un;
+        std::memcpy(&un, pos(), 4);
+        const uint32_t uh = convert32<B>(un);
+        int32_t        x;
+        std::memcpy(&x, &uh, 4);
+        return x;
+    }
+
+    // throw (UnderrunError)
+    inline int32_t getInt32()
+    {
+        const int32_t x = peekInt32();
+        advance(4);
+        return x;
+    }
+
+    // throw (UnderrunError)
+    inline uint64_t getUInt64()
+    {
+        checkReadable(8);
+        uint64_t un;
+        std::memcpy(&un, pos(), 8);
+        advance(8);
+        return convert64<B>(un);
+    }
+
+    // throw (UnderrunError)
+    inline float getFloat32()
+    {
+        checkReadable(4);
+        checkAlignment(4);
+        uint32_t un;
+        std::memcpy(&un, pos(), 4);
+        advance(4);
+        const uint32_t uh = convert32<B>(un);
+        float          f;
+        std::memcpy(&f, &uh, 4);
+        return f;
+    }
+
+    // throw (UnderrunError)
+    inline double getFloat64()
+    {
+        checkReadable(8);
+        checkAlignment(4);
+        uint64_t un;
+        std::memcpy(&un, pos(), 8);
+        advance(8);
+        const uint64_t uh = convert64<B>(un);
+        double         f;
+        std::memcpy(&f, &uh, 8);
+        return f;
+    }
+
+    // throw (UnderrunError, ParseError)
+    const char* getString()
+    {
+        checkReadable(4); // min string length
+
+        const char* ptr = static_cast<const char*>(pos()) + 3;
+        const char* end = static_cast<const char*>(this->end());
+
+        while (true)
+        {
+            if (ptr >= end)
+                throw UnderrunError();
+            if (*ptr == '\0')
+                break;
+            ptr += 4;
+        }
+
+        const char* x = pos();
+        advance(ptr - pos() + 1);
+
+        return x;
+    }
+};
+
+typedef BasicReadStream<NetworkByteOrder> ReadStream;
+} // namespace OSCPP
+
+#endif // OSCPP_STREAM_HPP_INCLUDED
diff --git a/externals/oscpp/include/oscpp/error.hpp b/externals/oscpp/include/oscpp/error.hpp
new file mode 100644 (file)
index 0000000..600883a
--- /dev/null
@@ -0,0 +1,87 @@
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_ERROR_HPP_INCLUDED
+#define OSCPP_ERROR_HPP_INCLUDED
+
+#include <exception>
+#include <string>
+
+namespace OSCPP {
+
+class Error : public std::exception
+{
+public:
+    Error(const std::string& what)
+    : m_what(what)
+    {}
+
+    virtual ~Error() noexcept
+    {}
+
+    const char* what() const noexcept override
+    {
+        return m_what.c_str();
+    }
+
+private:
+    std::string m_what;
+};
+
+class UnderrunError : public Error
+{
+public:
+    UnderrunError()
+    : Error(std::string("Buffer underrun"))
+    {}
+};
+
+class OverflowError : public Error
+{
+public:
+    OverflowError(size_t bytes)
+    : Error(std::string("Buffer overflow"))
+    , m_bytes(bytes)
+    {}
+
+    size_t numBytes() const
+    {
+        return m_bytes;
+    }
+
+private:
+    size_t m_bytes;
+};
+
+class ParseError : public Error
+{
+public:
+    ParseError(const std::string& what = "Parse error")
+    : Error(what)
+    {}
+};
+
+} // namespace OSCPP
+
+#endif // OSCPP_ERROR_HPP_INCLUDED
diff --git a/externals/oscpp/include/oscpp/print.hpp b/externals/oscpp/include/oscpp/print.hpp
new file mode 100644 (file)
index 0000000..04f1579
--- /dev/null
@@ -0,0 +1,183 @@
+// OSCpp library
+//
+// Copyright (c) 2004-2011 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_PRINT_HPP_INCLUDED
+#define OSCPP_PRINT_HPP_INCLUDED
+
+#include <oscpp/client.hpp>
+#include <oscpp/server.hpp>
+
+#include <ostream>
+
+namespace OSCPP { namespace detail {
+
+const size_t kDefaultIndentWidth = 4;
+
+class Indent
+{
+public:
+    Indent(size_t w)
+    : m_width(w)
+    , m_indent(0)
+    {}
+    Indent(size_t w, size_t n)
+    : m_width(w)
+    , m_indent(n)
+    {}
+    Indent(const Indent&) = default;
+
+    operator size_t() const
+    {
+        return m_indent;
+    }
+    Indent inc() const
+    {
+        return Indent(m_width, m_indent + m_width);
+    }
+
+private:
+    size_t m_width;
+    size_t m_indent;
+};
+
+inline std::ostream& operator<<(std::ostream& out, const Indent& indent)
+{
+    size_t n = indent;
+    while (n-- > 0)
+        out << ' ';
+    return out;
+}
+
+inline void printArgs(std::ostream& out, Server::ArgStream args)
+{
+    while (!args.atEnd())
+    {
+        const char t = args.tag();
+        switch (t)
+        {
+            case 'i':
+                out << "i:" << args.int32();
+                break;
+            case 'f':
+                out << "f:" << args.float32();
+                break;
+            case 's':
+                out << "s:" << args.string();
+                break;
+            case 'b':
+                out << "b:" << args.blob().size();
+                break;
+            case '[':
+                out << "[ ";
+                printArgs(out, args.array());
+                out << " ]";
+                break;
+            default:
+                out << t << ":?";
+                args.drop();
+                break;
+        }
+        out << ' ';
+    }
+}
+
+inline void printMessage(std::ostream& out, const Server::Message& msg,
+                         const Indent& indent)
+{
+    out << indent << msg.address() << ' ';
+    printArgs(out, msg.args());
+}
+
+inline void printBundle(std::ostream& out, const Server::Bundle& bundle,
+                        const Indent& indent)
+{
+    out << indent << "# " << bundle.time() << " [" << std::endl;
+    Indent nextIndent = indent.inc();
+    auto   packets = bundle.packets();
+    while (!packets.atEnd())
+    {
+        auto packet = packets.next();
+        if (packet.isMessage())
+        {
+            printMessage(out, packet, nextIndent);
+        }
+        else
+        {
+            printBundle(out, packet, nextIndent);
+        }
+        out << std::endl;
+    }
+    out << indent << "]";
+}
+
+inline void printPacket(std::ostream& out, const Server::Packet& packet,
+                        const Indent& indent)
+{
+    if (packet.isMessage())
+    {
+        printMessage(out, packet, indent);
+    }
+    else
+    {
+        printBundle(out, packet, indent);
+    }
+}
+
+}} // namespace OSCPP::detail
+
+namespace OSCPP { namespace Server {
+
+inline std::ostream& operator<<(std::ostream& out, const Packet& packet)
+{
+    detail::printPacket(out, packet,
+                        detail::Indent(detail::kDefaultIndentWidth));
+    return out;
+}
+
+inline std::ostream& operator<<(std::ostream& out, const Bundle& packet)
+{
+    detail::printBundle(out, packet,
+                        detail::Indent(detail::kDefaultIndentWidth));
+    return out;
+}
+
+inline std::ostream& operator<<(std::ostream& out, const Message& packet)
+{
+    detail::printMessage(out, packet,
+                         detail::Indent(detail::kDefaultIndentWidth));
+    return out;
+}
+
+}} // namespace OSCPP::Server
+
+namespace OSCPP { namespace Client {
+
+inline std::ostream& operator<<(std::ostream& out, const Packet& packet)
+{
+    return out << Server::Packet(packet.data(), packet.size());
+}
+
+}} // namespace OSCPP::Client
+
+#endif // OSCPP_PRINT_HPP_INCLUDED
diff --git a/externals/oscpp/include/oscpp/server.hpp b/externals/oscpp/include/oscpp/server.hpp
new file mode 100644 (file)
index 0000000..a3933f7
--- /dev/null
@@ -0,0 +1,493 @@
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_SERVER_HPP_INCLUDED
+#define OSCPP_SERVER_HPP_INCLUDED
+
+#include <oscpp/detail/stream.hpp>
+#include <oscpp/util.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <tuple>
+
+namespace OSCPP { namespace Server {
+
+//! OSC Message Argument Iterator.
+/*!
+ * Retrieve typed arguments from an incoming message.
+ *
+ * Supported tags and their correspondong types are:
+ *
+ *  i       -- 32 bit signed integer number<br>
+ *  f       -- 32 bit floating point number<br>
+ *  s       -- NULL-terminated string padded to 4-byte boundary<br>
+ *  b       -- 32-bit integer size followed by 4-byte aligned data
+ *
+ * \sa getArgInt32
+ * \sa getArgFloat32
+ * \sa getArgString
+ */
+class ArgStream
+{
+public:
+    //* Empty argument stream.
+    ArgStream() = default;
+
+    //* Construct argument stream from tag and value streams.
+    ArgStream(const ReadStream& tags, const ReadStream& args)
+    : m_tags(tags)
+    , m_args(args)
+    {}
+
+    //! Constructor.
+    /*!
+     * Read arguments from stream, which has to point to the start of a
+     * message type signature.
+     *
+     * \throw OSCPP::UnderrunError stream buffer underrun.
+     * \throw OSCPP::ParseError error while parsing input stream.
+     */
+    ArgStream(const ReadStream& stream)
+    {
+        m_args = stream;
+        const char* tags = m_args.getString();
+        if (tags[0] != ',')
+            throw ParseError("Tag string doesn't start with ','");
+        m_tags = ReadStream(tags + 1, strlen(tags) - 1);
+    }
+
+    //* Return the number of arguments that can be read from the stream.
+    size_t size() const
+    {
+        return m_tags.capacity();
+    }
+
+    //* Return true if no more arguments can be read from the stream.
+    bool atEnd() const
+    {
+        return m_tags.atEnd();
+    }
+
+    //* Return tag and argument streams.
+    std::tuple<ReadStream, ReadStream> state() const
+    {
+        return std::make_tuple(m_tags, m_args);
+    }
+
+    //* Return the type tag corresponding to the next message argument.
+    char tag() const
+    {
+        return m_tags.peekChar();
+    }
+
+    //* Drop next argument.
+    void drop()
+    {
+        drop(m_tags.getChar());
+    }
+
+    //! Get next integer argument.
+    /*!
+     * Read next numerical argument from the input stream and convert it
+     * to an integer.
+     *
+     * \exception OSCPP::UnderrunError stream buffer underrun.
+     * \exception OSCPP::ParseError argument could not be converted.
+     */
+    int32_t int32()
+    {
+        const char t = m_tags.getChar();
+        if (t == 'i')
+            return m_args.getInt32();
+        if (t == 'f')
+            return (int32_t)m_args.getFloat32();
+        throw ParseError("Cannot convert argument to int");
+    }
+
+    //! Get next float argument.
+    /*!
+     * Read next numerical argument from the input stream and convert it
+     * to a float.
+     *
+     * \exception OSCPP::UnderrunError stream buffer underrun.
+     * \exception OSCPP::ParseError argument could not be converted.
+     */
+    float float32()
+    {
+        const char t = m_tags.getChar();
+        if (t == 'f')
+            return m_args.getFloat32();
+        if (t == 'i')
+            return (float)m_args.getInt32();
+        throw ParseError("Cannot convert argument to float");
+    }
+
+    //! Get next string argument.
+    /*!
+     * Read next string argument and return it as a NULL-terminated
+     * string.
+     *
+     * \exception OSCPP::UnderrunError stream buffer underrun.
+     * \exception OSCPP::ParseError argument could not be converted or
+     * is not a valid string.
+     */
+    const char* string()
+    {
+        if (m_tags.getChar() == 's')
+        {
+            return m_args.getString();
+        }
+        throw ParseError("Cannot convert argument to string");
+    }
+
+    //* Get next blob argument.
+    //
+    // @throw OSCPP::UnderrunError stream buffer underrun.
+    // @throw OSCPP::ParseError argument is not a valid blob
+    Blob blob()
+    {
+        if (m_tags.getChar() == 'b')
+        {
+            return parseBlob();
+        }
+        else
+        {
+            throw ParseError("Cannot convert argument to blob");
+        }
+    }
+
+    //* Return a stream corresponding to an array argument.
+    ArgStream array()
+    {
+        if (m_tags.getChar() == '[')
+        {
+            const char* tags = m_tags.pos();
+            const char* args = m_args.pos();
+            dropArray();
+            // m_tags.pos() points right after the closing ']'.
+            return ArgStream(ReadStream(tags, m_tags.pos() - tags - 1),
+                             ReadStream(args, m_args.pos() - args));
+        }
+        else
+        {
+            throw ParseError("Expected array");
+        }
+    }
+
+    template <typename T> T next()
+    {
+        return T::OSC_Server_ArgStream_next_unimplemented;
+    }
+
+private:
+    // Parse a blob (type tag already consumed).
+    Blob parseBlob()
+    {
+        int32_t size = m_args.getInt32();
+        if (size < 0)
+        {
+            throw ParseError("Invalid blob size is less than zero");
+        }
+        else
+        {
+            static_assert(
+                sizeof(size_t) >= sizeof(int32_t),
+                "Size of size_t must be greater than size of int32_t");
+            const void* data = m_args.pos();
+            m_args.skip(align(size));
+            return Blob(data, static_cast<size_t>(size));
+        }
+    }
+    // Drop an atomic value of type t (type tag already consumed).
+    void dropAtom(char t)
+    {
+        switch (t)
+        {
+            case 'i':
+                m_args.skip(4);
+                break;
+            case 'f':
+                m_args.skip(4);
+                break;
+            case 's':
+                m_args.getString();
+                break;
+            case 'b':
+                parseBlob();
+                break;
+        }
+    }
+    // Drop a possibly nested array.
+    void dropArray()
+    {
+        unsigned int level = 0;
+        for (;;)
+        {
+            char t = m_tags.getChar();
+            if (t == ']')
+            {
+                if (level == 0)
+                    break;
+                else
+                    level--;
+            }
+            else if (t == '[')
+            {
+                level++;
+            }
+            else
+            {
+                dropAtom(t);
+            }
+        }
+    }
+    // Drop the next argument of type t (type tag already consumed).
+    void drop(char t)
+    {
+        switch (t)
+        {
+            case '[':
+                dropArray();
+                break;
+            default:
+                dropAtom(t);
+        }
+    }
+
+private:
+    ReadStream m_tags;
+    ReadStream m_args;
+};
+
+class Message
+{
+public:
+    Message(const char* address, const ReadStream& stream)
+    : m_address(address)
+    , m_args(ArgStream(stream))
+    {}
+
+    const char* address() const
+    {
+        return m_address;
+    }
+
+    ArgStream args() const
+    {
+        return m_args;
+    }
+
+private:
+    const char* m_address;
+    ArgStream   m_args;
+};
+
+class PacketStream;
+
+class Bundle
+{
+public:
+    Bundle(uint64_t time, const ReadStream& stream)
+    : m_time(time)
+    , m_stream(stream)
+    {}
+
+    uint64_t time() const
+    {
+        return m_time;
+    }
+
+    inline PacketStream packets() const;
+
+private:
+    uint64_t   m_time;
+    ReadStream m_stream;
+};
+
+class Packet
+{
+public:
+    Packet()
+    : m_isBundle(false)
+    {}
+
+    Packet(const ReadStream& stream)
+    : m_stream(stream)
+    , m_isBundle(isBundle(stream))
+    {
+        // Skip over #bundle header
+        if (m_isBundle)
+            m_stream.skip(8);
+    }
+
+    Packet(const void* data, size_t size)
+    : Packet(ReadStream(data, size))
+    {}
+
+    const void* data() const
+    {
+        return m_stream.begin();
+    }
+
+    size_t size() const
+    {
+        return m_stream.capacity();
+    }
+
+    bool isBundle() const
+    {
+        return m_isBundle;
+    }
+
+    bool isMessage() const
+    {
+        return !isBundle();
+    }
+
+    operator Bundle() const
+    {
+        if (!isBundle())
+            throw ParseError("Packet is not a bundle");
+        ReadStream stream(m_stream);
+        uint64_t   time = stream.getUInt64();
+        return Bundle(time, std::move(stream));
+    }
+
+    operator Message() const
+    {
+        if (!isMessage())
+            throw ParseError("Packet is not a message");
+        ReadStream  stream(m_stream);
+        const char* address = stream.getString();
+        return Message(address, std::move(stream));
+    }
+
+    static bool isMessage(const void* data, size_t size)
+    {
+        return (size > 3) && (static_cast<const char*>(data)[0] != '#');
+    }
+
+    static bool isMessage(const ReadStream& stream)
+    {
+        return isMessage(stream.pos(), stream.consumable());
+    }
+
+    static bool isBundle(const void* data, size_t size)
+    {
+        return (size > 15) && (std::memcmp(data, "#bundle", 8) == 0);
+    }
+
+    static bool isBundle(const ReadStream& stream)
+    {
+        return isBundle(stream.pos(), stream.consumable());
+    }
+
+private:
+    ReadStream m_stream;
+    bool       m_isBundle;
+};
+
+class PacketStream
+{
+public:
+    PacketStream(const ReadStream& stream)
+    : m_stream(stream)
+    {}
+
+    bool atEnd() const
+    {
+        return m_stream.atEnd();
+    }
+
+    Packet next()
+    {
+        size_t     size = m_stream.getInt32();
+        ReadStream stream(m_stream, size);
+        m_stream.skip(size);
+        return Packet(stream);
+    }
+
+private:
+    ReadStream m_stream;
+};
+
+template <> inline int32_t ArgStream::next<int32_t>()
+{
+    return int32();
+}
+
+template <> inline float ArgStream::next<float>()
+{
+    return float32();
+}
+
+template <> inline const char* ArgStream::next<const char*>()
+{
+    return string();
+}
+
+template <> inline Blob ArgStream::next<Blob>()
+{
+    return blob();
+}
+
+template <> inline ArgStream ArgStream::next<ArgStream>()
+{
+    return array();
+}
+
+PacketStream Bundle::packets() const
+{
+    return PacketStream(m_stream);
+}
+
+}} // namespace OSCPP::Server
+
+static inline bool operator==(const OSCPP::Server::Message& msg,
+                              const char*                   str)
+{
+    return strcmp(msg.address(), str) == 0;
+}
+
+static inline bool operator==(const char*                   str,
+                              const OSCPP::Server::Message& msg)
+{
+    return msg == str;
+}
+
+static inline bool operator!=(const OSCPP::Server::Message& msg,
+                              const char*                   str)
+{
+    return !(msg == str);
+}
+
+static inline bool operator!=(const char*                   str,
+                              const OSCPP::Server::Message& msg)
+{
+    return msg != str;
+}
+
+#endif // OSCPP_SERVER_HPP_INCLUDED
diff --git a/externals/oscpp/include/oscpp/types.hpp b/externals/oscpp/include/oscpp/types.hpp
new file mode 100644 (file)
index 0000000..2afc68b
--- /dev/null
@@ -0,0 +1,59 @@
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_TYPES_HPP_INCLUDED
+#define OSCPP_TYPES_HPP_INCLUDED
+
+namespace OSCPP {
+
+class Blob
+{
+public:
+    Blob()
+    : m_size(0)
+    , m_data(nullptr)
+    {}
+    Blob(const void* data, size_t size)
+    : m_size(size)
+    , m_data(data)
+    {}
+    Blob(const Blob& other) = default;
+
+    size_t size() const
+    {
+        return m_size;
+    }
+    const void* data() const
+    {
+        return m_data;
+    }
+
+private:
+    size_t      m_size;
+    const void* m_data;
+};
+
+} // namespace OSCPP
+
+#endif // OSCPP_TYPES_HPP_INCLUDED
diff --git a/externals/oscpp/include/oscpp/util.hpp b/externals/oscpp/include/oscpp/util.hpp
new file mode 100644 (file)
index 0000000..5e3caa4
--- /dev/null
@@ -0,0 +1,159 @@
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_UTIL_HPP_INCLUDED
+#define OSCPP_UTIL_HPP_INCLUDED
+
+#include <cassert>
+#include <cstring>
+
+namespace OSCPP {
+
+static const size_t kAlignment = 4;
+
+inline bool isAligned(const void* ptr, size_t alignment)
+{
+    return (reinterpret_cast<uintptr_t>(ptr) & (alignment - 1)) == 0;
+}
+
+constexpr bool isAligned(size_t n)
+{
+    return (n & 3) == 0;
+}
+
+constexpr size_t align(size_t n)
+{
+    return (n + 3) & -4;
+}
+
+constexpr size_t padding(size_t n)
+{
+    return align(n) - n;
+}
+
+inline void checkAlignment(const void* ptr, size_t n)
+{
+    if (!isAligned(ptr, n))
+    {
+        throw std::runtime_error("Unaligned pointer");
+    }
+}
+
+namespace Tags {
+
+constexpr size_t int32()
+{
+    return 1;
+}
+constexpr size_t float32()
+{
+    return 1;
+}
+constexpr size_t string()
+{
+    return 1;
+}
+constexpr size_t blob()
+{
+    return 1;
+}
+constexpr size_t array(size_t numElems)
+{
+    return numElems + 2;
+}
+} // namespace Tags
+
+namespace Size {
+
+class String
+{
+public:
+    String(const char* x)
+    : m_value(x)
+    {}
+
+    operator const char*() const
+    {
+        return m_value;
+    }
+
+private:
+    const char* m_value;
+};
+
+inline size_t string(const String& x)
+{
+    return align(std::strlen(x) + 1);
+}
+
+template <size_t N> constexpr size_t string(char const (&)[N])
+{
+    return align(N);
+}
+
+constexpr size_t bundle(size_t numPackets)
+{
+    return 8 /* #bundle */ + 8 /* timestamp */ +
+           4 * numPackets /* size prefix */;
+}
+
+inline size_t message(const String& address, size_t numArgs)
+{
+    return string(address) + align(numArgs + 2);
+}
+
+template <size_t N>
+constexpr size_t message(char const (&address)[N], size_t numArgs)
+{
+    return string(address) + align(numArgs + 2);
+}
+
+constexpr size_t int32(size_t n = 1)
+{
+    return n * 4;
+}
+
+constexpr size_t float32(size_t n = 1)
+{
+    return n * 4;
+}
+
+constexpr size_t float64(size_t n = 1)
+{
+    return n * 8;
+}
+
+constexpr size_t string(size_t n)
+{
+    return align(n + 1);
+}
+
+constexpr size_t blob(size_t size)
+{
+    return 4 + align(size);
+}
+} // namespace Size
+} // namespace OSCPP
+
+#endif // OSCPP_UTIL_HPP_INCLUDED
diff --git a/externals/oscpp/test/CMakeLists.txt b/externals/oscpp/test/CMakeLists.txt
new file mode 100644 (file)
index 0000000..e5150f3
--- /dev/null
@@ -0,0 +1,42 @@
+set(warnings -Wall -Wextra -Wno-unused-parameter -Werror)
+
+add_compile_options(
+  "$<$<CXX_COMPILER_ID:Clang>:${warnings}>"
+  "$<$<CXX_COMPILER_ID:AppleClang>:${warnings}>"
+  "$<$<CXX_COMPILER_ID:GNU>:${warnings}>"
+)
+
+# =============================================================================
+# autocheck tests
+
+add_executable(oscpp_autocheck
+    oscpp_autocheck.cpp
+)
+
+target_include_directories(oscpp_autocheck PRIVATE
+    ../include
+    autocheck/include
+)
+
+add_test(oscpp_autocheck oscpp_autocheck)
+
+add_custom_command(
+    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/README.cpp
+    COMMAND ruby ${CMAKE_CURRENT_SOURCE_DIR}/../tools/mdcode.rb
+                 ${CMAKE_CURRENT_SOURCE_DIR}/../README.md
+                 ${CMAKE_CURRENT_BINARY_DIR}/README.cpp
+    MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/../README.md
+)
+
+# =============================================================================
+# README code
+
+add_executable(oscpp_readme
+  ${CMAKE_CURRENT_BINARY_DIR}/README.cpp
+)
+
+target_include_directories(oscpp_readme PRIVATE
+    ../include
+)
+
+add_test(oscpp_readme oscpp_readme)
diff --git a/externals/oscpp/test/oscpp_autocheck.cpp b/externals/oscpp/test/oscpp_autocheck.cpp
new file mode 100644 (file)
index 0000000..fdff9a3
--- /dev/null
@@ -0,0 +1,657 @@
+#include <oscpp/client.hpp>
+#include <oscpp/print.hpp>
+#include <oscpp/server.hpp>
+
+#include <autocheck/autocheck.hpp>
+#include <cstdint>
+#include <list>
+#include <memory>
+#include <string>
+
+namespace OSCPP { namespace AST {
+class Value
+{
+public:
+    virtual ~Value()
+    {}
+    virtual void print(std::ostream& out) const = 0;
+    virtual void put(OSCPP::Client::Packet& packet) const = 0;
+};
+
+template <class T> using List = std::list<std::shared_ptr<T>>;
+
+template <class T> bool equalList(const List<T>& list1, const List<T>& list2)
+{
+    if (list1.size() != list2.size())
+        return false;
+    auto it1 = list1.begin();
+    auto it2 = list2.begin();
+    while ((it1 != list1.end()) && (it2 != list2.end()))
+    {
+        if (**it1 == **it2)
+        {
+            it1++;
+            it2++;
+        }
+        else
+        {
+            return false;
+        }
+    }
+    return true;
+}
+
+template <class T> void printList(std::ostream& out, const List<T>& list)
+{
+    const size_t n = list.size();
+    size_t       i = 1;
+    out << '[';
+    for (auto x : list)
+    {
+        x->print(out);
+        if (i != n)
+        {
+            out << ',';
+        }
+        i++;
+    }
+    out << ']';
+}
+
+class Argument : public Value
+{
+public:
+    enum Type
+    {
+        kInt32,
+        kFloat32,
+        kString,
+        kBlob,
+        kArray,
+    };
+    static constexpr size_t kNumTypes = kArray + 1;
+
+    Argument(Type type)
+    : m_type(type)
+    {}
+
+    Type type() const
+    {
+        return m_type;
+    }
+
+    virtual size_t numTags() const
+    {
+        return 1;
+    }
+
+    static size_t numTags(const List<Argument>& args)
+    {
+        size_t n = 0;
+        for (auto x : args)
+            n += x->numTags();
+        return n;
+    }
+
+    bool operator==(const Argument& other)
+    {
+        return (type() == other.type()) && equals(other);
+    }
+
+    virtual size_t size() const = 0;
+
+protected:
+    virtual bool equals(const Argument& other) const = 0;
+
+private:
+    Type m_type;
+};
+
+class Bundle;
+class Message;
+
+class Packet : public Value
+{
+public:
+    enum Type
+    {
+        kMessage,
+        kBundle
+    };
+
+    Packet(Type type)
+    : m_type(type)
+    {}
+
+    Type type() const
+    {
+        return m_type;
+    }
+
+    virtual size_t size() const = 0;
+
+    static std::shared_ptr<Packet> parse(const OSCPP::Server::Packet& packet)
+    {
+        return packet.isBundle() ? parseBundle(packet) : parseMessage(packet);
+    }
+
+    bool operator==(const Packet& other) const
+    {
+        return type() == other.type() && equals(other);
+    }
+
+protected:
+    virtual bool equals(const Packet& other) const = 0;
+
+private:
+    static std::shared_ptr<Packet>
+                parseBundle(const OSCPP::Server::Bundle& bdl);
+    static void parseArgs(OSCPP::Server::ArgStream& inArgs,
+                          List<Argument>&           outArgs);
+    static std::shared_ptr<Packet>
+    parseMessage(const OSCPP::Server::Message& msg);
+
+    Type m_type;
+};
+
+class Bundle : public Packet
+{
+public:
+    Bundle(uint64_t time, List<Packet> packets)
+    : Packet(kBundle)
+    , m_time(time)
+    , m_packets(packets)
+    {
+        // assert(packets.size() > 0);
+    }
+
+    void print(std::ostream& out) const override
+    {
+        out << "Bundle(" << m_time << ", ";
+        printList(out, m_packets);
+        out << ')';
+    }
+
+    void put(OSCPP::Client::Packet& packet) const override
+    {
+        packet.openBundle(m_time);
+        for (auto p : m_packets)
+            p->put(packet);
+        packet.closeBundle();
+    }
+
+    size_t size() const override
+    {
+        size_t payload = 0;
+        for (auto x : m_packets)
+            payload += x->size();
+        assert(OSCPP::isAligned(payload));
+        return OSCPP::Size::bundle(m_packets.size()) + payload;
+    }
+
+protected:
+    bool equals(const Packet& other) const override
+    {
+        const auto& otherBundle = dynamic_cast<const Bundle&>(other);
+        return m_time == otherBundle.m_time &&
+               equalList(m_packets, otherBundle.m_packets);
+    }
+
+private:
+    uint64_t     m_time;
+    List<Packet> m_packets;
+};
+
+class Message : public Packet
+{
+public:
+    Message(std::string address, List<Argument> args)
+    : Packet(kMessage)
+    , m_address(address)
+    , m_args(args)
+    {}
+
+    void print(std::ostream& out) const override
+    {
+        out << "Message(" << m_address << ", ";
+        printList(out, m_args);
+        out << ')';
+    }
+
+    void put(OSCPP::Client::Packet& packet) const override
+    {
+        packet.openMessage(m_address.c_str(), Argument::numTags(m_args));
+        for (auto x : m_args)
+            x->put(packet);
+        packet.closeMessage();
+    }
+
+    size_t size() const override
+    {
+        size_t payload = 0;
+        for (auto x : m_args)
+            payload += x->size();
+        assert(OSCPP::isAligned(payload));
+        return OSCPP::Size::message(OSCPP::Size::String(m_address.c_str()),
+                                    Argument::numTags(m_args)) +
+               payload;
+    }
+
+protected:
+    bool equals(const Packet& other) const override
+    {
+        const auto& otherMsg = dynamic_cast<const Message&>(other);
+        return m_address == otherMsg.m_address &&
+               equalList(m_args, otherMsg.m_args);
+    }
+
+private:
+    std::string    m_address;
+    List<Argument> m_args;
+};
+
+class Int32 : public Argument
+{
+public:
+    Int32(int32_t value)
+    : Argument(kInt32)
+    , m_value(value)
+    {}
+
+    void print(std::ostream& out) const override
+    {
+        out << "i:" << m_value;
+    }
+
+    void put(OSCPP::Client::Packet& packet) const override
+    {
+        packet.put(m_value);
+    }
+
+    size_t size() const override
+    {
+        return OSCPP::Size::int32();
+    }
+
+protected:
+    bool equals(const Argument& other) const override
+    {
+        return dynamic_cast<const Int32&>(other).m_value == m_value;
+    }
+
+private:
+    int32_t m_value;
+};
+
+class Float32 : public Argument
+{
+public:
+    Float32(float value)
+    : Argument(kFloat32)
+    , m_value(value)
+    {}
+
+    void print(std::ostream& out) const override
+    {
+        out << "f:" << m_value;
+    }
+
+    void put(OSCPP::Client::Packet& packet) const override
+    {
+        packet.put(m_value);
+    }
+
+    size_t size() const override
+    {
+        return OSCPP::Size::float32();
+    }
+
+protected:
+    bool equals(const Argument& other) const override
+    {
+        return dynamic_cast<const Float32&>(other).m_value == m_value;
+    }
+
+private:
+    float m_value;
+};
+
+class String : public Argument
+{
+public:
+    String(std::string value)
+    : Argument(kString)
+    , m_value(value)
+    {}
+
+    void print(std::ostream& out) const override
+    {
+        out << "s:" << m_value;
+    }
+
+    void put(OSCPP::Client::Packet& packet) const override
+    {
+        packet.put(m_value.c_str());
+    }
+
+    size_t size() const override
+    {
+        return OSCPP::Size::string(OSCPP::Size::String(m_value.c_str()));
+    }
+
+protected:
+    bool equals(const Argument& other) const override
+    {
+        return dynamic_cast<const String&>(other).m_value == m_value;
+    }
+
+private:
+    std::string m_value;
+};
+
+class Blob : public Argument
+{
+public:
+    Blob(int32_t size, const void* data = nullptr)
+    : Argument(kBlob)
+    , m_size(std::max(0, size))
+    , m_data(nullptr)
+    {
+        if (m_size > 0)
+        {
+            m_data = new char[m_size];
+            if (data != nullptr)
+                std::memcpy(m_data, data, m_size);
+        }
+    }
+
+    Blob(OSCPP::Blob b)
+    : Blob(static_cast<int32_t>(b.size()), b.data())
+    {}
+
+    ~Blob()
+    {
+        delete[] m_data;
+    }
+
+    void print(std::ostream& out) const override
+    {
+        out << "b:" << m_size;
+    }
+
+    void put(OSCPP::Client::Packet& packet) const override
+    {
+        packet.put(OSCPP::Blob(m_data, m_size));
+    }
+
+    size_t size() const override
+    {
+        return OSCPP::Size::blob(m_size);
+    }
+
+protected:
+    bool equals(const Argument& other) const override
+    {
+        const Blob& otherBlob = dynamic_cast<const Blob&>(other);
+        return otherBlob.m_size == m_size &&
+               memcmp(m_data, otherBlob.m_data, m_size) == 0;
+    }
+
+private:
+    size_t m_size;
+    char*  m_data;
+};
+
+class Array : public Argument
+{
+public:
+    Array(List<Argument> elems = List<Argument>())
+    : Argument(kArray)
+    , m_elems(elems)
+    {}
+
+    void print(std::ostream& out) const override
+    {
+        printList(out, m_elems);
+    }
+
+    void put(OSCPP::Client::Packet& packet) const override
+    {
+        packet.openArray();
+        for (auto x : m_elems)
+            x->put(packet);
+        packet.closeArray();
+    }
+
+    size_t size() const override
+    {
+        size_t payload = 0;
+        for (auto x : m_elems)
+            payload += x->size();
+        assert(OSCPP::isAligned(payload));
+        return payload;
+    }
+
+    size_t numTags() const override
+    {
+        return OSCPP::Tags::array(Argument::numTags(m_elems));
+    }
+
+protected:
+    bool equals(const Argument& other) const override
+    {
+        return equalList(m_elems, dynamic_cast<const Array&>(other).m_elems);
+    }
+
+private:
+    List<Argument> m_elems;
+};
+
+std::shared_ptr<Packet> Packet::parseBundle(const OSCPP::Server::Bundle& bdl)
+{
+    List<Packet>                outPackets;
+    OSCPP::Server::PacketStream inPackets(bdl.packets());
+    while (!inPackets.atEnd())
+    {
+        outPackets.push_back(parse(inPackets.next()));
+    }
+    return std::make_shared<Bundle>(bdl.time(), std::move(outPackets));
+}
+
+void Packet::parseArgs(OSCPP::Server::ArgStream& inArgs,
+                       List<Argument>&           outArgs)
+{
+    while (!inArgs.atEnd())
+    {
+        switch (inArgs.tag())
+        {
+            case 'i':
+                outArgs.push_back(std::make_shared<Int32>(inArgs.int32()));
+                break;
+            case 'f':
+                outArgs.push_back(std::make_shared<Float32>(inArgs.float32()));
+                break;
+            case 's':
+                outArgs.push_back(std::make_shared<String>(inArgs.string()));
+                break;
+            case 'b':
+                outArgs.push_back(std::make_shared<Blob>(inArgs.blob()));
+                break;
+            case '[':
+            {
+                OSCPP::Server::ArgStream inElems(inArgs.array());
+                List<Argument>           outElems;
+                parseArgs(inElems, outElems);
+                outArgs.push_back(std::make_shared<Array>(outElems));
+            }
+            break;
+        }
+    }
+}
+
+std::shared_ptr<Packet> Packet::parseMessage(const OSCPP::Server::Message& msg)
+{
+    OSCPP::Server::ArgStream inArgs(msg.args());
+    List<Argument>           outArgs;
+    parseArgs(inArgs, outArgs);
+    return std::make_shared<Message>(msg.address(), outArgs);
+}
+
+std::ostream& operator<<(std::ostream& out, const Packet& packet)
+{
+    packet.print(out);
+    return out;
+}
+
+std::ostream& operator<<(std::ostream&                  out,
+                         const std::shared_ptr<Packet>& packet)
+{
+    packet->print(out);
+    return out;
+}
+}} // namespace OSCPP::AST
+
+namespace ac = autocheck;
+
+namespace OSCPP { namespace AutoCheck {
+
+struct MessageArgListGen
+{
+    typedef AST::List<AST::Argument> result_type;
+    result_type                      operator()(size_t size) const;
+};
+
+struct MessageArgGen
+{
+    typedef std::shared_ptr<AST::Argument> result_type;
+    result_type                            operator()(size_t size) const
+    {
+        AST::Argument::Type argType = static_cast<AST::Argument::Type>(
+            ac::generator<size_t>()(AST::Argument::kNumTypes - 1));
+        switch (argType)
+        {
+            case AST::Argument::kInt32:
+                return std::make_shared<AST::Int32>(
+                    ac::generator<int32_t>()(size));
+            case AST::Argument::kFloat32:
+                return std::make_shared<AST::Float32>(
+                    ac::generator<float>()(size));
+            case AST::Argument::kString:
+                return std::make_shared<AST::String>(
+                    ac::string<ac::ccPrintable>()(std::max<size_t>(1, size)));
+            case AST::Argument::kBlob:
+                return std::make_shared<AST::Blob>(
+                    ac::generator<int32_t>()(size));
+            case AST::Argument::kArray:
+                // Exponential size backoff
+                return std::make_shared<AST::Array>(
+                    MessageArgListGen()(size / 2));
+            default:
+                throw std::logic_error("Invalid AST::Argument::Type value");
+        }
+        const bool InvalidArgumentType = false;
+        assert(InvalidArgumentType);
+    }
+};
+
+MessageArgListGen::result_type MessageArgListGen::operator()(size_t size) const
+{
+    const auto& elems = ac::list_of(MessageArgGen())(size);
+    return AST::List<AST::Argument>(elems.begin(), elems.end());
+}
+
+struct PacketGen
+{
+    // ac::generator<std::shared_ptr<oscpp::AST::Packet>> source;
+    typedef std::shared_ptr<AST::Packet> result_type;
+    result_type                          operator()(size_t size) const
+    {
+        return ac::generator<bool>()(size) ? gen_bundle(size)
+                                           : gen_message(size);
+    }
+
+    result_type gen_bundle(size_t size) const
+    {
+        const auto& packets = ac::list_of(PacketGen())(size / 2);
+        return std::make_shared<AST::Bundle>(
+            ac::generator<uint64_t>()(size),
+            AST::List<AST::Packet>(packets.begin(), packets.end()));
+    }
+
+    std::string gen_message_address(size_t size) const
+    {
+        std::string result(
+            ac::string<ac::ccAlphaNumeric>()(std::max<size_t>(2, size)));
+        if (result[0] != '/')
+            result[0] = '/';
+        return result;
+    }
+
+    result_type gen_message(size_t size) const
+    {
+        return std::make_shared<AST::Message>(gen_message_address(size),
+                                              MessageArgListGen()(size));
+    }
+};
+}} // namespace OSCPP::AutoCheck
+
+bool prop_identity(const std::shared_ptr<OSCPP::AST::Packet>& packet1)
+{
+    // packet1->print(std::cerr); std::cerr << "\n";
+    const size_t            size = packet1->size();
+    std::unique_ptr<char[]> data(new char[size]);
+    OSCPP::Client::Packet   clientPacket(data.get(), size);
+    packet1->put(clientPacket);
+    OSCPP::Server::Packet serverPacket(clientPacket.data(),
+                                       clientPacket.size());
+    auto                  packet2 = OSCPP::AST::Packet::parse(serverPacket);
+    using namespace OSCPP::AST;
+    if (!(*packet1 == *packet2))
+    {
+        std::cerr << packet1 << std::endl;
+        std::cerr << packet2 << std::endl;
+        return false;
+    }
+    return true;
+}
+
+bool prop_overflow(const std::shared_ptr<OSCPP::AST::Packet>& packet,
+                   size_t                                     inBufferSize)
+{
+    const size_t packetSize = packet->size();
+    const size_t bufferSize =
+        inBufferSize == 0
+            ? 1
+            : (inBufferSize < packetSize ? inBufferSize : packetSize - 1);
+    std::cerr << "bufferSize " << bufferSize << std::endl;
+    std::unique_ptr<char[]> data(new char[bufferSize]);
+    OSCPP::Client::Packet   clientPacket(data.get(), bufferSize);
+    bool                    result = false;
+    try
+    {
+        packet->put(clientPacket);
+    }
+    catch (OSCPP::OverflowError&)
+    {
+        result = true;
+    }
+    catch (std::exception& e)
+    {
+        std::cerr << "Exception: " << e.what() << std::endl;
+    }
+    return result;
+}
+
+int main(int argc, char** argv)
+{
+    using namespace OSCPP::AST;
+    using namespace OSCPP::AutoCheck;
+    ac::check<std::shared_ptr<Packet>>(prop_identity, 150,
+                                       ac::make_arbitrary(PacketGen()));
+    // ac::check<std::shared_ptr<Packet>,size_t>(
+    //     prop_overflow,
+    //     150,
+    //     ac::make_arbitrary(PacketGen(), ac::generator<size_t>())
+    // );
+    return 0;
+}
diff --git a/externals/oscpp/tools/clang-format b/externals/oscpp/tools/clang-format
new file mode 100755 (executable)
index 0000000..543e35e
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/sh
+# clang-format all C/C++/ObjC source files under source control
+
+function source_files()
+{
+    git ls-tree -r HEAD --name-only | grep -E '\.(h|hpp|cpp|m|M)$'
+}
+
+function clang_format()
+{
+    xargs -n1 clang-format -style=file "$@"
+}
+
+case "$1" in
+    check)
+        source_files | clang_format -output-replacements-xml
+        ;;
+    *)
+        source_files | clang_format -i
+        ;;
+esac
+
diff --git a/externals/oscpp/tools/mdcode.rb b/externals/oscpp/tools/mdcode.rb
new file mode 100755 (executable)
index 0000000..40dfdce
--- /dev/null
@@ -0,0 +1,24 @@
+#!/usr/bin/env ruby
+
+if ARGV.size != 2
+  puts "Usage: #{File.basename($0)} INFILE OUTFILE"
+  exit 1
+end
+
+cpp = false
+
+File.open(ARGV[1], "w") do |out|
+  File.open(ARGV[0]).each do |line|
+    if cpp
+      if /^~~~~$/ =~ line
+        cpp = false
+      else
+        out.write(line)
+      end
+    else
+      if /^~~~~cpp$/ =~ line
+        cpp = true
+      end
+    end
+  end
+end
index 60c219847325b66f750e7d6f203512643741086f..b987594580e699bee2dfc24a01af5bc73475f4cb 100644 (file)
@@ -232,9 +232,12 @@ HEADERS += src/DataProtocol.h \
            src/RingBuffer.h \
            src/RingBufferWavetable.h \
            src/Settings.h \
+           src/SocketClient.h \
+           src/SocketServer.h \
            src/UdpDataProtocol.h \
            src/UdpHubListener.h \
            src/AudioInterface.h \
+           src/AudioSocket.h \
            src/compressordsp.h \
            src/limiterdsp.h \
            src/freeverbdsp.h \
@@ -305,10 +308,14 @@ SOURCES += src/DataProtocol.cpp \
            src/LoopBack.cpp \
            src/PacketHeader.cpp \
            src/RingBuffer.cpp \
+           src/SampleRateConverter.cpp \
            src/Settings.cpp \
+           src/SocketClient.cpp \
+           src/SocketServer.cpp \
            src/UdpDataProtocol.cpp \
            src/UdpHubListener.cpp \
            src/AudioInterface.cpp \
+           src/AudioSocket.cpp \
            src/main.cpp \
            src/SslServer.cpp \
            src/Auth.cpp
@@ -375,6 +382,14 @@ rtaudio|bundled_rtaudio {
   SOURCES += src/RtAudioInterface.cpp
 }
 
+nooscpp {
+  DEFINES += NO_OSCPP
+} else {
+  INCLUDEPATH += externals/oscpp externals/oscpp/include
+  HEADERS += src/OscServer.h
+  SOURCES += src/OscServer.cpp
+}
+
 weakjack {
   SOURCES += externals/weakjack/weak_libjack.c
 }
diff --git a/linux/Dockerfile.build b/linux/Dockerfile.build
new file mode 100644 (file)
index 0000000..4af7d42
--- /dev/null
@@ -0,0 +1,79 @@
+# JackTrip build container for Linux
+#
+# this Dockerfile is used by GitHub CI to create linux builds
+# it requires these environment variables:
+#
+# BUILD_CONTAINER - Debian based container image to build with
+# MESON_ARGS      - arguments to build using meson
+# QT_DOWNLOAD_URL - path to qt download (optional)
+
+# container image versions
+ARG BUILD_CONTAINER=ubuntu:20.04
+
+FROM ${BUILD_CONTAINER} AS builder
+
+# install required packages
+ENV DEBIAN_FRONTEND=noninteractive
+RUN apt-get update \
+  && apt-get install -yq --no-install-recommends curl python3-pip build-essential git libclang-dev libdbus-1-dev cmake ninja-build libjack-dev \
+  && apt-get install -yq --no-install-recommends 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-get install -yq --no-install-recommends libasound2-dev libpulse-dev \
+  && apt-get install -yq --no-install-recommends help2man clang-tidy desktop-file-utils
+RUN python3 -m pip install --upgrade pip \
+  && python3 -m pip install --upgrade certifi \
+  && python3 -m pip install meson pyyaml Jinja2
+
+WORKDIR /opt/jacktrip
+
+# install qt
+ARG QT_DOWNLOAD_URL=""
+ENV QT_DOWNLOAD_URL=$QT_DOWNLOAD_URL
+ENV QT_INSTALL_PATH="/opt"
+RUN if [ -n "$QT_DOWNLOAD_URL" ]; then \
+  mkdir -p $QT_INSTALL_PATH; \
+  chmod a+rwx $QT_INSTALL_PATH; \
+  curl -k -L $QT_DOWNLOAD_URL -o qt.tar.gz; \
+  tar -C $QT_INSTALL_PATH -xzf qt.tar.gz; \
+  rm qt.tar.gz; \
+  else \
+  add-apt-repository universe; \
+  apt-get update; \
+  apt-get install -yq --no-install-recommends 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 libgl1-mesa-dev libqt6core5compat6-dev libqt6shadertools6-dev; \
+  qtchooser -install qt6 $(which qmake6); \
+  fi
+
+# install vst3sdk
+ARG VST3SDK_DOWNLOAD_URL=""
+ENV VST3SDK_DOWNLOAD_URL=$VST3SDK_DOWNLOAD_URL
+ENV VST3SDK_INSTALL_PATH="/opt"
+RUN if [ -n "$VST3SDK_DOWNLOAD_URL" ]; then \
+  mkdir -p $VST3SDK_INSTALL_PATH; \
+  chmod a+rwx $VST3SDK_INSTALL_PATH; \
+  curl -k -L $VST3SDK_DOWNLOAD_URL -o vst3sdk.tar.gz; \
+  tar -C $VST3SDK_INSTALL_PATH -xzf vst3sdk.tar.gz; \
+  rm vst3sdk.tar.gz; \
+  apt-get install -yq --no-install-recommends libexpat-dev libxml2-dev libxcb-util-dev libxcb-cursor-dev libxcb-keysyms1-dev libxcb-xkb-dev libxkbcommon-dev libxkbcommon-x11-dev libgtkmm-3.0-dev libsqlite3-dev; \
+  fi
+
+# build jacktrip using meson
+COPY . ./
+ARG MESON_ARGS=""
+ENV MESON_ARGS=$MESON_ARGS
+ENV BUILD_PATH="/opt/jacktrip/builddir"
+RUN if [ -n "$QT_DOWNLOAD_URL" ]; then \
+  export QT_PATH="/opt/$(echo $QT_DOWNLOAD_URL | sed -e 's,.*/qt/\(qt-[.0-9]*\-[a-z]*\).*,\1,')"; \
+  export PATH="$PATH:$QT_PATH/bin"; \
+  export PKG_CONFIG_PATH="$QT_PATH/lib/pkgconfig"; \
+  export CMAKE_PREFIX_PATH="$QT_PATH"; fi \
+  && if [ -n "$VST3SDK_DOWNLOAD_URL" ]; then \
+  export MESON_ARGS="-Dvst-sdkdir=${VST3SDK_INSTALL_PATH}/vst3sdk $MESON_ARGS"; fi \
+  && export SSL_CERT_FILE=$(python3 -m certifi) \
+  && meson setup --buildtype release $MESON_ARGS $BUILD_PATH \
+  && meson compile -C $BUILD_PATH -v \
+  && strip $BUILD_PATH/jacktrip \
+  && if [ -n "$VST3SDK_DOWNLOAD_URL" ]; then \
+  strip $BUILD_PATH/JackTrip.vst3; fi
+
+FROM scratch AS artifact
+
+COPY --from=builder /opt/jacktrip/builddir/jacktrip /opt/jacktrip/builddir/JackTrip.vs[t]3 /opt/jacktrip/builddir/linux/org.jacktrip.JackTrip.desktop /
index fb8f2671ddc2cde7ba26a716721c50fab6d871dd..3d46779af5bf7b53b9f9cd5f85ffc3e73a6ebe00 100644 (file)
@@ -7,13 +7,13 @@ 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
+dnf install -y qt6-qtbase qt6-qtbase-common qt6-qtbase-gui qt6-qtsvg qt6-qtwebsockets qt6-qtwebengine qt6-qtwebchannel qt6-qt5compat rtaudio-devel
 ```
 
 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
+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 libjack-jackd2-0 librtaudio6 libgtkmm-3.0-1t64 libxcb-cursor0
 ```
 
 To install JackTrip as a Linux desktop application:
@@ -27,6 +27,13 @@ desktop-file-install --dir=$HOME/.local/share/applications org.jacktrip.JackTrip
 update-desktop-database $HOME/.local/share/applications
 ```
 
+To install the JackTrip Audio Bridge VST3 plugin:
+
+```
+mkdir -p $HOME/.vst3
+cp -r JackTrip.vst3 $HOME/.vst3
+```
+
 To install the manual page for JackTrip:
 
 ```
diff --git a/macos/JackTrip.vst3_template/Contents/Info.plist b/macos/JackTrip.vst3_template/Contents/Info.plist
new file mode 100644 (file)
index 0000000..5cb1588
--- /dev/null
@@ -0,0 +1,36 @@
+<?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>English</string>
+       <key>CFBundleExecutable</key>
+       <string>JackTrip.vst3</string>
+       <key>CFBundleGetInfoString</key>
+       <string>JackTrip Audio Bridge</string>
+       <key>CFBundleIconFile</key>
+       <string>jacktrip</string>
+       <key>CFBundleIdentifier</key>
+       <string>%BUNDLEID%</string>
+       <key>CFBundleInfoDictionaryVersion</key>
+       <string>6.0</string>
+       <key>CFBundleLongVersionString</key>
+       <string></string>
+       <key>CFBundleName</key>
+       <string>%BUNDLENAME%</string>
+       <key>CFBundlePackageType</key>
+       <string>BNDL</string>
+       <key>CFBundleShortVersionString</key>
+       <string></string>
+       <key>CFBundleSignature</key>
+       <string>????</string>
+       <key>CFBundleVersion</key>
+       <string>%VERSION%</string>
+       <key>CSResourcesFileMapped</key>
+       <true/>
+       <key>NSHumanReadableCopyright</key>
+       <string>Copyright © 2024-2025 JackTrip Labs, Inc.</string>
+</dict>
+</plist>
diff --git a/macos/JackTrip.vst3_template/Contents/PkgInfo b/macos/JackTrip.vst3_template/Contents/PkgInfo
new file mode 100644 (file)
index 0000000..19a9cf6
--- /dev/null
@@ -0,0 +1 @@
+BNDL????
\ No newline at end of file
index 22475f344304056de64a6a4e59bf68b9c903ff36..fdc07faeb4b6a094e088f462a7d637509f5e7616 100755 (executable)
@@ -17,6 +17,7 @@ KEY_STORE="AC_PASSWORD"
 TEMP_KEYCHAIN=""
 USE_DEFAULT_KEYCHAIN=false
 BINARY="../builddir/jacktrip"
+VST_BINARY="../builddir/$APPNAME.vst3"
 PSI=false
 
 OPTIND=1
@@ -140,7 +141,7 @@ 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"
+    echo "Detected a dynamic $QT_VERSION binary"
     DEPLOY_CMD="$(which macdeployqt)"
     if [ -z "$DEPLOY_CMD" ]; then
         # Attempt to find macdeployqt. Try macports location first, then brew.
@@ -171,6 +172,24 @@ if [ -n "$DYNAMIC_QT" ]; then
     fi
 fi
 
+if [ -f "$VST_BINARY" ]; then
+    echo "Building bundle $APPNAME.vst3 (id: $BUNDLE_ID.vst3)"
+    rm -rf "$APPNAME.vst3"
+    [ ! -d "JackTrip.vst3_template/Contents/MacOS" ] && mkdir JackTrip.vst3_template/Contents/MacOS
+    [ ! -d "JackTrip.vst3_template/Contents/Resources" ] && mkdir JackTrip.vst3_template/Contents/Resources
+    [ ! -d "JackTrip.app_template/Contents/Resources" ] && mkdir JackTrip.vst3_template/Contents/Resources
+    cp -a JackTrip.vst3_template "$APPNAME.vst3"
+    cp -f $VST_BINARY "$APPNAME.vst3/Contents/MacOS/"
+    # copy licenses
+    cp -f ../LICENSE.md "$APPNAME.vst3/Contents/Resources/"
+    cp -Rf ../LICENSES "$APPNAME.vst3/Contents/Resources/"
+    cp ../src/vst3/resources/* "$APPNAME.vst3/Contents/Resources/"
+    sed -i '' "s/%VERSION%/$VERSION/" "$APPNAME.vst3/Contents/Resources/moduleinfo.json"
+    sed -i '' "s/%VERSION%/$VERSION/" "$APPNAME.vst3/Contents/Info.plist"
+    sed -i '' "s/%BUNDLENAME%/$APPNAME.vst3/" "$APPNAME.vst3/Contents/Info.plist"
+    sed -i '' "s/%BUNDLEID%/$BUNDLE_ID.vst3/" "$APPNAME.vst3/Contents/Info.plist"
+fi
+
 [ $BUILD_INSTALLER = true ] || exit 0
 
 # If you have Packages installed, you can build an installer for the newly created app bundle.
@@ -185,6 +204,10 @@ fi
 if [ -n "$CERTIFICATE" ]; then
     echo "Signing $APPNAME.app"
     codesign -f -s "$CERTIFICATE" --timestamp --entitlements entitlements.plist --options "runtime" "$APPNAME.app"
+    if [ -f "$VST_BINARY" ]; then
+      echo "Signing $APPNAME.vst3"
+      codesign -f -s "$CERTIFICATE" --timestamp --entitlements entitlements.plist --options "runtime" "$APPNAME.vst3"
+    fi
 fi
 
 # prepare license
@@ -206,7 +229,11 @@ 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
+if [ -f "$VST_BINARY" ]; then
+  cp package/JackTrip.pkgproj_template_with_vst3 package/JackTrip.pkgproj
+else
+  cp package/JackTrip.pkgproj_template package/JackTrip.pkgproj
+fi
 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
diff --git a/macos/package/JackTrip.pkgproj_template_with_vst3 b/macos/package/JackTrip.pkgproj_template_with_vst3
new file mode 100644 (file)
index 0000000..d0be9eb
--- /dev/null
@@ -0,0 +1,1740 @@
+<?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>
+               <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/>
+                                                       <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>
+                                                               <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>
+                                                                               <dict>
+                                                                                       <key>CHILDREN</key>
+                                                                                       <array>
+                                                                                               <dict>
+                                                                                                       <key>CHILDREN</key>
+                                                                                                       <array>
+                                                                                                               <dict>
+                                                                                                                       <key>BUNDLE_CAN_DOWNGRADE</key>
+                                                                                                                       <false/>
+                                                                                                                       <key>CHILDREN</key>
+                                                                                                                       <array/>
+                                                                                                                       <key>GID</key>
+                                                                                                                       <integer>0</integer>
+                                                                                                                       <key>PATH</key>
+                                                                                                                       <string>../%BUNDLENAME%.vst3</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>
+                                                                                                       </array>
+                                                                                                       <key>GID</key>
+                                                                                                       <integer>0</integer>
+                                                                                                       <key>PATH</key>
+                                                                                                       <string>VST3</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>
+                                                                                       </array>
+                                                                                       <key>GID</key>
+                                                                                       <integer>0</integer>
+                                                                                       <key>PATH</key>
+                                                                                       <string>Plug-Ins</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>
+                                                                       </array>
+                                                                       <key>GID</key>
+                                                                       <integer>0</integer>
+                                                                       <key>PATH</key>
+                                                                       <string>Audio</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>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>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>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>Keyboard Layouts</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>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>
+                                       </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>
+                               <false/>
+                               <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%.vst3</string>
+                               <key>LOCATION</key>
+                               <integer>0</integer>
+                               <key>NAME</key>
+                               <string>%BUNDLENAME%.vst3</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>8CAD404A-E34F-469F-93DD-D2D2125F35F5</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>
index a668d3c7c1db70e28cdd8ce1987a34574eb245ad..b443867d55db426a8d8c57cbc7b45bb4fbeb3e4e 100755 (executable)
@@ -6,5 +6,5 @@ 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
+sudo -u $USER open -a /Applications/JackTrip.app
 exit 0
index ed020aa2584b5caa1f86e90abfba826bbfcb4dd1..fe92b5c579eb6dfc074cac2312ba3bd52dc44ac1 100755 (executable)
@@ -5,6 +5,7 @@ CERTIFICATE=""
 PACKAGE_CERT=""
 USERNAME=""
 PASSWORD=""
+# Only needed if you belong to more than one dev team
 TEAM_ID=""
 
 if [ -z $1 ]; then
index 0685415966f45cbb8ede11666b349d89d25d64d3..2bba8cbd520240fddf1d707a7d967f4815ee85e0 100644 (file)
@@ -31,7 +31,10 @@ defines = ['-DWAIRTOHUB']
 c_defines = []
 incdirs = []
 
-if get_option('debug') == false
+if get_option('debug') == true
+       defines += ['-D_DEBUG']
+       c_defines += ['-D_DEBUG']
+else
        defines += ['-DNDEBUG', '-DQT_NO_DEBUG']
        c_defines += ['-DNDEBUG', '-DQT_NO_DEBUG']
 endif
@@ -55,6 +58,7 @@ endif
 src = [        'src/DataProtocol.cpp',
        'src/JackTrip.cpp',
        'src/ProcessPlugin.cpp',
+       'src/AudioSocket.cpp',
        'src/AudioTester.cpp',
        'src/jacktrip_globals.cpp',
        'src/JackTripWorker.cpp',
@@ -63,7 +67,10 @@ src = [      'src/DataProtocol.cpp',
        'src/RingBuffer.cpp',
        'src/JitterBuffer.cpp',
        'src/Regulator.cpp',
+       'src/SampleRateConverter.cpp',
        'src/Settings.cpp',
+       'src/SocketClient.cpp',
+       'src/SocketServer.cpp',
        'src/UdpDataProtocol.cpp',
        'src/UdpHubListener.cpp',
        'src/AudioInterface.cpp',
@@ -82,6 +89,7 @@ src = [       'src/DataProtocol.cpp',
 moc_h = ['src/DataProtocol.h',
        'src/JackTrip.h',
        'src/ProcessPlugin.h',
+       'src/AudioSocket.h',
        'src/Meter.h',
        'src/Monitor.h',
        'src/StereoToMono.h',
@@ -91,11 +99,23 @@ moc_h = ['src/DataProtocol.h',
        'src/PacketHeader.h',
        'src/Regulator.h',
        'src/Settings.h',
+       'src/SocketClient.h',
+       'src/SocketServer.h',
        'src/UdpDataProtocol.h',
        'src/UdpHubListener.h',
        'src/Auth.h',
        'src/SslServer.h']
 
+if get_option('nooscpp') == true
+       defines += '-DNO_OSCPP'
+       c_defines += '-DNO_OSCPP'
+else
+       incdirs += include_directories('externals/oscpp', is_system: true)
+       incdirs += include_directories('externals/oscpp/include', is_system: true)
+       src += ['src/OscServer.cpp']
+       moc_h += ['src/OscServer.h']
+endif
+
 ui_h = []
 qres = []
 deps = [dependency('threads')]
@@ -117,7 +137,7 @@ else
                'src/Patcher.cpp']
        moc_h += ['src/Patcher.h']
        if get_option('weakjack') == true
-               incdirs += include_directories('externals/weakjack')
+               incdirs += include_directories('externals/weakjack', is_system: true)
                src += 'externals/weakjack/weak_libjack.c'
                defines += '-DUSE_WEAK_JACK'
                c_defines += '-DUSE_WEAK_JACK'
@@ -128,11 +148,16 @@ else
        endif
 endif
 
+qmake = ''
+qt_core_deps = []
 if qt_version == '5'
-       deps += dependency('qt5', modules: ['Core', 'Network'], include_type: 'system')
+       qmake = find_program('qmake', required: true)
+       qt_core_deps = dependency('qt5', modules: ['Core', 'Network'], include_type: 'system')
 else
-       deps += dependency('qt6', modules: ['Core', 'Network'], include_type: 'system')
+       qmake = find_program('qmake6', required: true)
+       qt_core_deps = dependency('qt6', modules: ['Core', 'Network'], include_type: 'system')
 endif
+deps += qt_core_deps
 
 if get_option('nogui') == true or (get_option('noclassic') == true and get_option('novs') == true)
        # command line only
@@ -246,52 +271,58 @@ else
        endif
 endif
 
+static_deps = []
+static_src = []
+static_link_args = []
 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')
+               static_deps += dependency('qt6', modules: ['DBus', 'BundledLibpng', 'BundledPcre2', 'BundledHarfbuzz', 'BundledZLIB'], include_type: 'system')
        else
-               deps += compiler.find_library('qtpcre2', required : true, dirs : [qt_libdir])
+               static_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)
+               static_deps += compiler.find_library('ssl', required : true, dirs : [qt_libdir])
+               static_deps += compiler.find_library('crypto', required : true, dirs : [qt_libdir])
+               static_deps += compiler.find_library('dl', required : true)
+               static_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']
+                       static_deps += compiler.find_library('dbus-1', required : true)
+                       static_deps += compiler.find_library('qopensslbackend', required : true, dirs : [qt_plugindir+'/tls'])
+                       static_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)
+                       static_deps += compiler.find_library('bcrypt', required : true)
+                       static_deps += compiler.find_library('winmm', required : true)
+                       static_deps += compiler.find_library('Crypt32', required : true)
                        if qt_version == '6'
-                               deps += compiler.find_library('Authz', required : true)
+                               static_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)
+                       #static_deps += dependency('CoreServices', required : true)
+                       static_link_args += ['-framework', 'CoreServices']
+                       static_link_args += ['-framework', 'CFNetwork']
+                       static_link_args += ['-framework', 'AppKit']
+                       static_link_args += ['-framework', 'IOKit']
+                       static_link_args += ['-framework', 'Security']
+                       static_link_args += ['-framework', 'GSS']
+                       static_link_args += ['-framework', 'SystemConfiguration']
+                       static_deps += dependency('zlib', required : true)
                endif
        endif
+       deps += static_deps
+       src += static_src
+       link_args += static_link_args
 endif
 
 # QT_OPENSOURCE should only be defined for open source Qt distribution
@@ -313,6 +344,28 @@ if rtaudio_dep.found() == false and jack_dep.found() == false
        configure.''')
 endif
 
+libsamplerate_dep = []
+found_libsamplerate = false
+if get_option('libsamplerate').allowed()
+       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
+       opt_var.add_cmake_defines({'CMAKE_POSITION_INDEPENDENT_CODE': 'ON'})
+       libsamplerate_subproject = cmake.subproject('libsamplerate', options: opt_var)
+       libsamplerate_dep = libsamplerate_subproject.dependency('samplerate')
+       found_libsamplerate = libsamplerate_dep.found()
+       if not found_libsamplerate and not get_option('libsamplerate').auto()
+               error('failed to configure libsamplerate')
+       endif
+       if found_libsamplerate
+               defines += '-DHAVE_LIBSAMPLERATE'
+               deps += libsamplerate_dep
+       endif
+endif
+
 if host_machine.system() == 'darwin'
        src += ['src/NoNap.mm']
        # Adding CoreAudio here is a workaround and should be removed
@@ -342,6 +395,124 @@ 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 )
 
+vst_sdkdir = get_option('vst-sdkdir')
+if vst_sdkdir != ''
+       # adapted from https://github.com/centricular/gstreamer-vst3
+       vst_includedir = '@0@/public.sdk/source'.format(vst_sdkdir)
+       vst_pluginterfaces_includedir = '@0@'.format(vst_sdkdir)
+       vst_incdirs = []
+       vst_incdirs += include_directories('@0@'.format(vst_includedir), is_system: true)
+       vst_incdirs += include_directories('@0@'.format(vst_pluginterfaces_includedir), is_system: true)
+       vst_incdirs += include_directories('@0@/vstgui4'.format(vst_pluginterfaces_includedir), is_system: true)
+
+       vst_libdir = get_option('vst-libdir')
+       if vst_libdir == ''
+               vst_libdir = vst_sdkdir + '/lib'
+       endif
+       libbase_dep = compiler.find_library('base', required : true, dirs : [vst_libdir])
+       libsdk_dep = compiler.find_library('sdk', required : true, dirs : [vst_libdir])
+       libsdk_common_dep = compiler.find_library('sdk_common', required : true, dirs : [vst_libdir])
+       libvstgui_dep = compiler.find_library('vstgui', required : true, dirs : [vst_libdir])
+       libvstgui_support_dep = compiler.find_library('vstgui_support', required : true, dirs : [vst_libdir])
+       libvstgui_uidescription_dep = compiler.find_library('vstgui_uidescription', required : true, dirs : [vst_libdir])
+       libpluginterfaces_dep = compiler.find_library('pluginterfaces', required : true, dirs : [vst_libdir])
+       vst_deps = [libbase_dep, libsdk_dep, libsdk_common_dep, libvstgui_dep, libvstgui_uidescription_dep, libvstgui_support_dep, libpluginterfaces_dep]
+       vst_deps += qt_core_deps
+
+       vst_sources = ['src/vst3/JackTripVSTController.cpp', 'src/vst3/JackTripVSTEntry.cpp', 'src/vst3/JackTripVSTProcessor.cpp']
+
+       # uncomment for live editor
+       # vst_sources += ['@0@/vstgui4/vstgui/vstgui_uidescription.cpp'.format(vst_sdkdir), '@0@/vstgui4/vstgui/plugin-bindings/vst3editor.cpp'.format(vst_sdkdir)]
+       # defines += ['-DVSTGUI_LIVE_EDITING=1']
+
+       vst_link_args = []
+       if (host_machine.system() == 'linux')
+               vst_sources += '@0@/main/linuxmain.cpp'.format(vst_includedir)
+               vst_deps += static_deps
+               vst_sources += static_src
+               vst_link_args += static_link_args
+               vst_deps += compiler.find_library('xcb-util', required : true)
+               vst_deps += compiler.find_library('xcb-cursor', required : true)
+               vst_deps += compiler.find_library('xkbcommon-x11', required : true)
+               vst_deps += compiler.find_library('xml2', required : true)
+               vst_deps += compiler.find_library('cairo', required : true)
+               vst_deps += compiler.find_library('pango-1.0', required : true)
+               vst_deps += compiler.find_library('pangocairo-1.0', required : true)
+               vst_deps += compiler.find_library('expat', required : true)
+               vst_deps += compiler.find_library('fontconfig', required : true)
+       elif (host_machine.system() == 'darwin')
+               vst_sources += '@0@/main/macmain.cpp'.format(vst_includedir)
+               vst_link_args += ['-framework', 'CoreServices']
+               vst_link_args += ['-framework', 'CFNetwork']
+               vst_link_args += ['-framework', 'AppKit']
+               vst_link_args += ['-framework', 'IOKit']
+               vst_link_args += ['-framework', 'Security']
+               vst_link_args += ['-framework', 'GSS']
+               vst_link_args += ['-framework', 'SystemConfiguration']
+               vst_deps += dependency('zlib', required : true)
+               if (qt_version == '5')
+                       vst_link_args += '@0@/libQt5Core.a'.format(vst_libdir)
+                       vst_link_args += '@0@/libQt5Network.a'.format(vst_libdir)
+               else
+                       vst_link_args += '@0@/libQt6Core.a'.format(vst_libdir)
+                       vst_link_args += '@0@/libQt6Network.a'.format(vst_libdir)
+                       vst_link_args += '@0@/libQt6BundledPcre2.a'.format(vst_libdir)
+               endif
+       elif (host_machine.system() == 'windows')
+               vst_sources += '@0@/main/dllmain.cpp'.format(vst_includedir)
+               vst_deps += static_deps
+               vst_sources += static_src
+               vst_link_args += static_link_args
+               vst_deps += compiler.find_library('bcrypt', required : true)
+               vst_deps += compiler.find_library('winmm', required : true)
+               vst_deps += compiler.find_library('Crypt32', required : true)
+               vst_deps += compiler.find_library('ws2_32', required: true)
+               vst_link_args += 'userenv.lib'
+               vst_link_args += 'Synchronization.lib'
+               vst_link_args += 'Netapi32.lib'
+               vst_link_args += 'Version.lib'
+               vst_link_args += 'Dwrite.lib'
+               vst_link_args += 'Iphlpapi.lib'
+               vst_link_args += 'Secur32.lib'
+               vst_link_args += 'Winhttp.lib'
+               vst_link_args += 'Dnsapi.lib'
+               vst_link_args += 'Iphlpapi.lib'
+       else
+               error('Unsupported platform: ' + host_machine.system())
+       endif
+
+       if found_libsamplerate
+               vst_deps += libsamplerate_dep
+       endif
+
+       audio_socket_moc_h = ['src/AudioSocket.h', 'src/SocketClient.h', 'src/ProcessPlugin.h']
+       audio_socket_sources = qt.compile_moc(headers: audio_socket_moc_h, extra_args: defines)
+       audio_socket_sources += [
+               'src/AudioSocket.cpp',
+               'src/SocketClient.cpp',
+               'src/ProcessPlugin.cpp',
+               'src/jacktrip_globals.cpp'
+       ]
+       audio_socket_test = executable('audio_socket_tests',
+               ['tests/audio_socket_test.cpp'] + audio_socket_sources,
+               cpp_args : defines,
+               dependencies : vst_deps,
+               include_directories: vst_incdirs + include_directories('src/'),
+               link_args: vst_link_args
+       )
+
+       vst3 = shared_module('JackTrip',
+               vst_sources, audio_socket_sources,
+               name_prefix: '',
+               name_suffix: 'vst3',
+               cpp_args : defines,
+               dependencies : vst_deps,
+               include_directories: vst_incdirs,
+               link_args: vst_link_args,
+               cpp_args: defines
+       )
+endif
+
 help2man = find_program('help2man', required: false)
 if (host_machine.system() == 'linux')
        if help2man.found()
@@ -373,4 +544,5 @@ summary({'JACK': jack_dep.found(),
 summary({'Application ID': application_id,
        'GUI': not get_option('nogui'),
        'WAIR': get_option('wair'),
+       'Sample rate conversions': found_libsamplerate,
        'Manpage': help2man.found()}, bool_yn: true, section: 'Configuration')
index ec665356b698c9bcf58ed6883f6ac3fb495fe31f..a1850ae43692c3384c727cd8a5729b72b92b5b9a 100644 (file)
@@ -2,8 +2,10 @@ 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('libsamplerate', type : 'feature', value : 'auto', description: 'Support for sample rate conversions')
 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('nooscpp', type : 'boolean', value : false, description: 'Build without OSC support')
 option('noclassic', type : 'boolean', value : false, description: 'Build without classic mode support')
 option('vsftux', type : 'boolean', value : false, description: 'Build with Virtual Studio first launch experience')
 option('noupdater', type : 'boolean', value : false, description: 'Build without auto-update support')
@@ -11,4 +13,8 @@ option('nofeedback', type : 'boolean', value : false, description: 'Build withou
 option('profile', type: 'combo', choices: ['default', 'development'], value: 'default', description: 'Choose build profile / Sets desktop id accordingly')
 option('qtversion', type : 'combo', choices: ['', '5', '6'], description: 'Choose to build with either Qt5 or Qt6')
 option('qtedition', type : 'combo', choices: ['opensource', 'commercial'], description: 'Choose license edition for Qt')
-option('buildinfo', type : 'string', value : '', yield : true, description: 'Additional info used to describe the build')
\ No newline at end of file
+option('buildinfo', type : 'string', value : '', yield : true, description: 'Additional info used to describe the build')
+option('vst-libdir', type : 'string', value : '', yield : true,
+  description : 'Directory with VST SDK3 libraries (e.g. libsdk.a, libbase.a)')
+option('vst-sdkdir', type : 'string', value : '', yield : true,
+  description : 'Directory with VST SDK3 headers (e.g. public.sdk/source/vst/hosting/module.h)')
\ No newline at end of file
index 824eba6b2e20089df3a884ae91b9f407e787750d..9ef9e3d63a43062149cec03ae8a9687d62d6b1bf 100644 (file)
@@ -83,7 +83,7 @@ Analyzer::~Analyzer()
 void Analyzer::init(int samplingRate, int bufferSize)
 {
     ProcessPlugin::init(samplingRate, bufferSize);
-    fs = float(fSamplingFreq);
+    fs = float(mSampleRate);
 
     mPushBuffer.resize(mBufferSize);
     mCircularBufferPtr = new WaitFreeFrameBuffer<4096>(mBufferSize * sizeof(float));
index 97cb28d70806a77551295fcb5215672302d792aa..7450912403e2f5c08bac7c1643c6f956016a3219 100644 (file)
@@ -41,7 +41,9 @@
 #include <cmath>
 #include <iostream>
 
+#include "AudioSocket.h"
 #include "JackTrip.h"
+#include "ProcessPlugin.h"
 
 using std::cout;
 using std::endl;
@@ -95,18 +97,10 @@ AudioInterface::~AudioInterface()
         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;
-    }
+    mProcessPluginsFromNetwork.clear();
+    mProcessPluginsToNetwork.clear();
+    mProcessPluginsToMonitor.clear();
+    mAudioSockets.clear();
 }
 
 //*******************************************************************************
@@ -195,6 +189,10 @@ void AudioInterface::audioInputCallback(QVarLengthArray<sample_t*>& in_buffer,
     }
 
 #ifndef WAIR
+    for (auto& s : qAsConst(mAudioSockets)) {
+        s->getFromAudioSocketPlugin()->compute(n_frames, in_buffer.data(),
+                                               in_buffer.data());
+    }
     if (mMonitorQueuePtr != nullptr && mProcessPluginsToMonitor.size() > 0) {
         // copy audio input to monitor queue
         for (int i = 0; i < mInputChans.size(); i++) {
@@ -206,7 +204,7 @@ void AudioInterface::audioInputCallback(QVarLengthArray<sample_t*>& in_buffer,
 #endif  // not WAIR
 
     // process incoming signal from audio interface using process plugins
-    for (auto* p : std::as_const(mProcessPluginsToNetwork)) {
+    for (auto& p : qAsConst(mProcessPluginsToNetwork)) {
         if (p->getInited()) {
             p->compute(n_frames, in_buffer.data(), in_buffer.data());
         }
@@ -268,7 +266,7 @@ void AudioInterface::audioOutputCallback(QVarLengthArray<sample_t*>& out_buffer,
     /// 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)) {
+    for (auto& p : qAsConst(mProcessPluginsFromNetwork)) {
         if (p->getInited()) {
             p->compute(n_frames, out_buffer.data(), out_buffer.data());
         }
@@ -302,7 +300,7 @@ void AudioInterface::audioOutputCallback(QVarLengthArray<sample_t*>& out_buffer,
             std::memcpy(mOutProcessBuffer[i], sample_ptr, sizeof(sample_t) * n_frames);
         }
         for (int i = 0; i < mProcessPluginsToMonitor.size(); i++) {
-            ProcessPlugin* p = mProcessPluginsToMonitor[i];
+            ProcessPlugin* p = mProcessPluginsToMonitor[i].get();
             if (p->getInited()) {
                 // note: for monitor plugins, the output is out_buffer (to the speakers)
                 p->compute(n_frames, mOutProcessBuffer.data(), out_buffer.data());
@@ -310,6 +308,11 @@ void AudioInterface::audioOutputCallback(QVarLengthArray<sample_t*>& out_buffer,
         }
     }
 
+    for (auto& s : qAsConst(mAudioSockets)) {
+        s->getToAudioSocketPlugin()->compute(n_frames, out_buffer.data(),
+                                             out_buffer.data());
+    }
+
 #else  // WAIR:
     // nib16 result now in mNetInBuffer
     int nChansIn  = mInputChans.size();
@@ -642,9 +645,9 @@ void AudioInterface::setPipewireLatency(unsigned int bufferSize, unsigned int sa
 }
 
 //*******************************************************************************
-void AudioInterface::appendProcessPluginToNetwork(ProcessPlugin* plugin)
+void AudioInterface::appendProcessPluginToNetwork(QSharedPointer<ProcessPlugin>& plugin)
 {
-    if (!plugin) {
+    if (plugin.isNull()) {
         return;
     }
 
@@ -663,9 +666,9 @@ void AudioInterface::appendProcessPluginToNetwork(ProcessPlugin* plugin)
     mProcessPluginsToNetwork.append(plugin);
 }
 
-void AudioInterface::appendProcessPluginFromNetwork(ProcessPlugin* plugin)
+void AudioInterface::appendProcessPluginFromNetwork(QSharedPointer<ProcessPlugin>& plugin)
 {
-    if (!plugin) {
+    if (plugin.isNull()) {
         return;
     }
 
@@ -684,9 +687,9 @@ void AudioInterface::appendProcessPluginFromNetwork(ProcessPlugin* plugin)
     mProcessPluginsFromNetwork.append(plugin);
 }
 
-void AudioInterface::appendProcessPluginToMonitor(ProcessPlugin* plugin)
+void AudioInterface::appendProcessPluginToMonitor(QSharedPointer<ProcessPlugin>& plugin)
 {
-    if (!plugin) {
+    if (plugin.isNull()) {
         return;
     }
 
@@ -715,34 +718,53 @@ void AudioInterface::appendProcessPluginToMonitor(ProcessPlugin* plugin)
     mProcessPluginsToMonitor.append(plugin);
 }
 
+void AudioInterface::appendAudioSocket(QSharedPointer<AudioSocket>& s)
+{
+    if (s.isNull())
+        return;
+    static_cast<FromAudioSocketPlugin*>(s->getFromAudioSocketPlugin().data())
+        ->setPassthrough(true);
+    mAudioSockets.append(s);
+}
+
 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();
+                   + mProcessPluginsToMonitor.size() + (mAudioSockets.size() * 2);
     if (nPlugins > 0) {
         if (verbose) {
             std::cout << "Initializing Faust plugins (have " << nPlugins
                       << ") at sampling rate " << mSampleRate << "\n";
         }
 
-        for (ProcessPlugin* plugin : std::as_const(mProcessPluginsFromNetwork)) {
+        for (auto& plugin : qAsConst(mProcessPluginsFromNetwork)) {
             plugin->setOutgoingToNetwork(false);
             plugin->updateNumChannels(nChansIn, nChansOut);
             plugin->init(mSampleRate, mBufferSizeInSamples);
         }
-        for (ProcessPlugin* plugin : std::as_const(mProcessPluginsToNetwork)) {
+        for (auto& plugin : qAsConst(mProcessPluginsToNetwork)) {
             plugin->setOutgoingToNetwork(true);
             plugin->updateNumChannels(nChansIn, nChansOut);
             plugin->init(mSampleRate, mBufferSizeInSamples);
         }
-        for (ProcessPlugin* plugin : std::as_const(mProcessPluginsToMonitor)) {
+        for (auto& plugin : qAsConst(mProcessPluginsToMonitor)) {
             plugin->setOutgoingToNetwork(false);
             plugin->updateNumChannels(nChansMon, nChansMon);
             plugin->init(mSampleRate, mBufferSizeInSamples);
         }
+        for (auto& s : qAsConst(mAudioSockets)) {
+            auto* plugin = s->getFromAudioSocketPlugin().get();
+            plugin->setOutgoingToNetwork(true);
+            plugin->updateNumChannels(nChansIn, nChansOut);
+            plugin->init(mSampleRate, mBufferSizeInSamples);
+            plugin = s->getToAudioSocketPlugin().get();
+            plugin->setOutgoingToNetwork(false);
+            plugin->updateNumChannels(nChansIn, nChansOut);
+            plugin->init(mSampleRate, mBufferSizeInSamples);
+        }
     }
 }
 
index d752222d2ba15a3c2f1fec18214ff22106b65b07..3de02af2af8859294fb5c84029628d60b2eeb8e2 100644 (file)
 #ifndef __AUDIOINTERFACE_H__
 #define __AUDIOINTERFACE_H__
 
+#include <QSharedPointer>
 #include <QVarLengthArray>
 #include <QVector>
 #include <functional>
 
 #include "AudioTester.h"
-#include "ProcessPlugin.h"
 #include "WaitFreeFrameBuffer.h"
 #include "jacktrip_types.h"
 
 // Forward declarations
+class AudioSocket;
 class JackTrip;
+class ProcessPlugin;
 
 // using namespace JackTripNamespace;
 
@@ -190,7 +192,7 @@ class AudioInterface
      * using something like:\n
      * <tt>std::tr1::shared_ptr<ProcessPluginName> loopback(new ProcessPluginName);</tt>
      */
-    virtual void appendProcessPluginToNetwork(ProcessPlugin* plugin);
+    virtual void appendProcessPluginToNetwork(QSharedPointer<ProcessPlugin>& plugin);
 
     /** \brief appendProcessPluginFromNetwork():
      * Same as appendProcessPluginToNetwork() except that these plugins operate
@@ -200,12 +202,17 @@ class AudioInterface
      *               -> remote JackTrip server
      *               -> JackTrip client -> processPlugin from network -> JACK -> audio
      */
-    virtual void appendProcessPluginFromNetwork(ProcessPlugin* plugin);
+    virtual void appendProcessPluginFromNetwork(QSharedPointer<ProcessPlugin>& plugin);
 
     /** \brief appendProcessPluginToMonitor():
      * Appends plugins used for local monitoring
      */
-    virtual void appendProcessPluginToMonitor(ProcessPlugin* plugin);
+    virtual void appendProcessPluginToMonitor(QSharedPointer<ProcessPlugin>& plugin);
+
+    /** \brief appendAudioSocket():
+     * Appends audio socket connections
+     */
+    virtual void appendAudioSocket(QSharedPointer<AudioSocket>& s);
 
     /** \brief initPlugins():
      * Initialize all ProcessPlugin modules.
@@ -337,12 +344,14 @@ class AudioInterface
     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*>
+    QVector<QSharedPointer<ProcessPlugin> >
         mProcessPluginsFromNetwork;  ///< Vector of ProcessPlugin<EM>s</EM>
-    QVector<ProcessPlugin*>
+    QVector<QSharedPointer<ProcessPlugin> >
         mProcessPluginsToNetwork;  ///< Vector of ProcessPlugin<EM>s</EM>
-    QVector<ProcessPlugin*>
+    QVector<QSharedPointer<ProcessPlugin> >
         mProcessPluginsToMonitor;  ///< Vector of ProcessPlugin<EM>s</EM>
+    QVector<QSharedPointer<AudioSocket> >
+        mAudioSockets;  ///< Vector of AudioSocket<EM>s</EM>
     QVarLengthArray<sample_t*>
         mInProcessBuffer;  ///< Vector of Input buffers/channel for ProcessPlugin
     QVarLengthArray<sample_t*>
diff --git a/src/AudioSocket.cpp b/src/AudioSocket.cpp
new file mode 100644 (file)
index 0000000..298f74d
--- /dev/null
@@ -0,0 +1,738 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024-2025 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 AudioSocket.cpp
+ * \author Mike Dickey
+ * \date December 2024
+ * \license MIT
+ */
+
+#include "AudioSocket.h"
+
+#include <QEventLoop>
+#include <iostream>
+
+#include "SocketClient.h"
+#include "jacktrip_globals.h"
+
+using namespace std;
+
+constexpr int BytesPerSample     = sizeof(float);
+constexpr int BytesForFullSample = BytesPerSample * AudioSocketNumChannels;
+
+//*******************************************************************************
+ToAudioSocketPlugin::ToAudioSocketPlugin(AudioSocketQueueT& sendQueue,
+                                         AudioSocketQueueT& receiveQueue)
+    : mSendQueue(sendQueue), mReceiveQueue(receiveQueue)
+{
+    mSendBuffer.resize(AudioSocketMaxSamplesPerBlock * BytesForFullSample
+                       + BytesPerSample);
+}
+
+//*******************************************************************************
+ToAudioSocketPlugin::~ToAudioSocketPlugin() {}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::init(int samplingRate, int bufferSize)
+{
+    if (bufferSize < 8) {
+        cerr << "*** ToAudioSocketPlugin " << this << ": bufferSize (" << bufferSize
+             << ") < 8! Setting to 8." << endl;
+        bufferSize = 8;
+    }
+    ProcessPlugin::init(samplingRate, bufferSize);
+    inited = true;
+}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::compute(int nframes, float** inputs,
+                                  [[maybe_unused]] float** outputs)
+{
+    if (!inited) {
+        cerr << "*** ToAudioSocketPlugin " << this << ": init never called! Doing it now."
+             << endl;
+        init(0, 0);
+    }
+
+    if (!mIsConnected) {
+        return;
+    }
+
+    if (!mSentAudioHeader) {
+        // send audio socket header
+        emit signalSendAudioHeader(getSampleRate(), getBufferSize());
+        mSentAudioHeader = true;
+        return;
+    }
+
+    if (!mRemoteIsReady) {
+        // waiting to receive audio header
+        return;
+    }
+
+    if (nframes > AudioSocketMaxSamplesPerBlock) {
+        // just a sanity check; shouldn't happen
+        nframes = AudioSocketMaxSamplesPerBlock;
+    }
+
+    // interleave samples into send buffer
+    float* framePtr = reinterpret_cast<float*>(mSendBuffer.data());
+    *(framePtr++)   = nframes;  // first value represents number of samples
+    for (int nextSample = 0; nextSample < nframes; ++nextSample) {
+        for (int i = 0; i < AudioSocketNumChannels; i++) {
+            int chan      = i < getNumInputs() ? i : 0;  // mono => dual mono
+            *(framePtr++) = inputs[chan][nextSample];
+        }
+    }
+
+    // send the samples to queue
+    mSendQueue.push(reinterpret_cast<int8_t*>(mSendBuffer.data()));
+    emit signalSendAudio();
+
+    // note: outputs are ignored
+}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::updateNumChannels(int nChansIn, int nChansOut)
+{
+    if (outgoingPluginToNetwork) {
+        mNumChannels = nChansIn;
+    } else {
+        mNumChannels = nChansOut;
+    }
+}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::remoteIsReady()
+{
+    mRemoteIsReady = true;
+}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::gotConnection()
+{
+    mSentAudioHeader = false;
+    mRemoteIsReady   = false;
+    mIsConnected     = true;
+}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::lostConnection()
+{
+    mIsConnected = false;
+}
+
+//*******************************************************************************
+FromAudioSocketPlugin::FromAudioSocketPlugin(AudioSocketQueueT& sendQueue,
+                                             AudioSocketQueueT& receiveQueue,
+                                             bool passthrough)
+    : mSendQueue(sendQueue), mReceiveQueue(receiveQueue), mPassthrough(passthrough)
+{
+    mRecvBuffer.resize(AudioSocketMaxSamplesPerBlock * BytesForFullSample
+                       + BytesPerSample);
+    mExtraSamples = new float*[AudioSocketNumChannels];
+    for (int i = 0; i < AudioSocketNumChannels; i++) {
+        mExtraSamples[i] = new float[AudioSocketMaxSamplesPerBlock];
+    }
+}
+
+//*******************************************************************************
+FromAudioSocketPlugin::~FromAudioSocketPlugin()
+{
+    for (int i = 0; i < AudioSocketNumChannels; i++) {
+        delete[] mExtraSamples[i];
+    }
+    delete[] mExtraSamples;
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::init(int samplingRate, int bufferSize)
+{
+    if (bufferSize < 8) {
+        cerr << "*** FromAudioSocketPlugin " << this << ": bufferSize (" << bufferSize
+             << ") < 8! Setting to 8." << endl;
+        bufferSize = 8;
+    }
+    ProcessPlugin::init(samplingRate, bufferSize);
+    inited = true;
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::compute(int nframes, [[maybe_unused]] float** inputs,
+                                    float** outputs)
+{
+    if (!inited) {
+        cerr << "*** FromAudioSocketPlugin " << this
+             << ": init never called! Doing it now." << endl;
+        init(0, 0);
+    }
+
+    // copy inputs to outputs
+    const int bytesPerChannel = nframes * BytesPerSample;
+    for (int i = 0; i < getNumOutputs(); i++) {
+        if (mPassthrough) {
+            memcpy(outputs[i], inputs[i], bytesPerChannel);
+        } else {
+            memset(outputs[i], 0, bytesPerChannel);
+        }
+    }
+
+    if (!mIsConnected) {
+        return;
+    }
+
+    if (!mRemoteIsReady) {
+        // waiting to receive audio header
+        return;
+    }
+
+    int nextSample = 0;
+    while (true) {
+        // use extra samples first
+        while (mNextExtraSample != mLastExtraSample && nextSample < nframes) {
+            for (int i = 0; i < AudioSocketNumChannels; i++) {
+                int chan = i < getNumOutputs() ? i : 0;  // mix to mono
+                outputs[chan][nextSample] += mExtraSamples[i][mNextExtraSample];
+            }
+            if (++mNextExtraSample >= AudioSocketMaxSamplesPerBlock) {
+                mNextExtraSample = 0;
+            }
+            ++nextSample;
+        }
+
+        if (nextSample >= nframes) {
+            break;
+        }
+
+        // get bytes from next packet
+        int8_t* recvPtr = reinterpret_cast<int8_t*>(mRecvBuffer.data());
+        if (!mReceiveQueue.pop(recvPtr)) {
+            // qDebug() << "Audio socket glitch: receive queue empty";
+            break;
+        }
+
+        // copy bytes from packet to extras
+        float* framePtr = reinterpret_cast<float*>(mRecvBuffer.data());
+        int newSamples  = static_cast<int>(*(framePtr++));
+        for (int j = 0; j < newSamples; j++) {
+            for (int i = 0; i < AudioSocketNumChannels; i++) {
+                mExtraSamples[i][mLastExtraSample] = *(framePtr++);
+            }
+            if (++mLastExtraSample >= AudioSocketMaxSamplesPerBlock) {
+                mLastExtraSample = 0;
+            }
+        }
+    }
+
+    updateQueueStats(nframes);
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::updateNumChannels(int nChansIn, int nChansOut)
+{
+    if (outgoingPluginToNetwork) {
+        mNumChannels = nChansIn;
+    } else {
+        mNumChannels = nChansOut;
+    }
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::remoteIsReady()
+{
+    mNextExtraSample = 0;
+    mLastExtraSample = 0;
+    mQueueCheckSec   = 2;
+    mRemoteIsReady   = true;
+    resetQueueStats();
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::gotConnection()
+{
+    mRemoteIsReady = false;
+    mIsConnected   = true;
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::lostConnection()
+{
+    mIsConnected = false;
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::updateQueueStats(int nframes)
+{
+    // update receive queue stats
+    int remainingPackets = static_cast<int>(mReceiveQueue.size());
+    if (remainingPackets < mMinQueuePackets) {
+        mMinQueuePackets = remainingPackets;
+    }
+    if (remainingPackets > mMaxQueuePackets) {
+        mMaxQueuePackets = remainingPackets;
+    }
+    if (mNextQueueCheck > static_cast<uint32_t>(nframes)) {
+        mNextQueueCheck -= nframes;
+        return;
+    }
+
+    // qDebug() << "Audio socket receive queue: min =" << mMinQueuePackets
+    //          << ", max =" << mMaxQueuePackets;
+
+    if (mMinQueuePackets > 0) {
+        // drain the queue to minimize latency
+        // qDebug() << "Audio socket draining" << mMinQueuePackets
+        //          << "packets from receive queue";
+        int8_t* recvPtr = reinterpret_cast<int8_t*>(mRecvBuffer.data());
+        do {
+            mReceiveQueue.pop(recvPtr);
+        } while (--mMinQueuePackets > 0);
+    }
+
+    resetQueueStats();
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::resetQueueStats()
+{
+    mMinQueuePackets = AudioSocketMaxQueueSize;
+    mMaxQueuePackets = 0;
+    mNextQueueCheck  = getSampleRate() * mQueueCheckSec;
+    if (mQueueCheckSec < 512)  // max interval of about 8.5 minutes
+        mQueueCheckSec *= 2;
+}
+
+//*******************************************************************************
+AudioSocketWorker::AudioSocketWorker(AudioSocketQueueT& sendQueue,
+                                     AudioSocketQueueT& receiveQueue,
+                                     QSharedPointer<QLocalSocket>& s)
+    : mSendQueue(sendQueue), mReceiveQueue(receiveQueue), mSocketPtr(s)
+{
+    mSendBuffer.resize(AudioSocketMaxSamplesPerBlock * BytesForFullSample
+                       + BytesPerSample);
+    mRecvBuffer.resize(AudioSocketMaxSamplesPerBlock * BytesForFullSample
+                       + BytesPerSample);
+    mPopBuffer.resize(AudioSocketMaxSamplesPerBlock * BytesForFullSample
+                      + BytesPerSample);
+}
+
+//*******************************************************************************
+AudioSocketWorker::~AudioSocketWorker()
+{
+#ifdef HAVE_LIBSAMPLERATE
+    if (mSrcStatePtr != nullptr) {
+        src_delete(mSrcStatePtr);
+    }
+    delete[] mSrcInDataPtr;
+#endif
+}
+
+//****************************************************************************
+void AudioSocketWorker::start()
+{
+    setRealtimeProcessPriority();
+}
+
+//****************************************************************************
+void AudioSocketWorker::connect()
+{
+    if (isConnected()) {
+        return;
+    }
+
+    SocketClient c(mSocketPtr);
+
+    if (!c.connect()) {
+        emit signalConnectionFailed();
+        return;
+    }
+
+    if (!c.sendHeader("audio")) {
+        mSocketPtr->close();
+        emit signalConnectionFailed();
+        return;
+    }
+
+    cout << "Established audio socket connection" << endl;
+    emit signalConnectionEstablished();
+}
+
+//*******************************************************************************
+void AudioSocketWorker::close()
+{
+    if (mSocketPtr->state() == QLocalSocket::UnconnectedState
+        || mSocketPtr->state() == QLocalSocket::ClosingState) {
+        return;
+    }
+    mSocketPtr->close();
+    mSocketPtr->disconnect();
+}
+
+//*******************************************************************************
+void AudioSocketWorker::sendAudioHeader(uint32_t sampleRate, uint16_t bufferSize)
+{
+    mLocalSampleRate = sampleRate;
+
+    // send audio socket header
+    QByteArray headerBuffer;
+    headerBuffer.resize(AudioSocketHeaderSize);
+    char* headPtr = headerBuffer.data();
+    memcpy(headPtr, &sampleRate, sizeof(uint32_t));
+    headPtr += 4;
+    memcpy(headPtr, &bufferSize, sizeof(uint16_t));
+    mSocketPtr->write(headerBuffer);
+    mSocketPtr->waitForBytesWritten(-1);
+
+    // read audio header from remote to get settings
+    emit signalReadAudioHeader();
+}
+
+//*******************************************************************************
+void AudioSocketWorker::readAudioHeader()
+{
+    if (!mSocketPtr->waitForReadyRead(100)) {
+        // check if connection was lost
+        if (isConnected()) {
+            // schedule another attempt
+            emit signalReadAudioHeader();
+        } else {
+            // lost audio socket connection
+            cout << "Lost audio socket connection" << endl;
+            mSocketPtr->disconnect();
+            emit signalLostConnection();
+        }
+        return;
+    }
+
+    uint32_t headSampleRate;
+    uint16_t headBufferSize;
+    QByteArray headerBuffer;
+    headerBuffer.resize(AudioSocketHeaderSize);
+    memset(headerBuffer.data(), 0, AudioSocketHeaderSize);
+    mSocketPtr->read(headerBuffer.data(), AudioSocketHeaderSize);
+    char* headPtr = headerBuffer.data();
+    memcpy(&headSampleRate, headPtr, sizeof(uint32_t));
+    headPtr += 4;
+    memcpy(&headBufferSize, headPtr, sizeof(uint16_t));
+
+    // sanity checks (should never happen)
+    if (headSampleRate != 44100 && headSampleRate != 48000 && headSampleRate != 96000) {
+        cerr << "Audio socket received invalid sample rate = " << headSampleRate << endl;
+        mSocketPtr->close();
+        return;
+    }
+    if (headBufferSize < 2) {
+        cerr << "Audio socket received invalid buffer size = " << headBufferSize << endl;
+        mSocketPtr->close();
+        return;
+    }
+
+    cout << "Received audio socket header: sample rate = " << headSampleRate
+         << ", buffer size = " << headBufferSize << endl;
+
+    mRemoteSampleRate = headSampleRate;
+
+#ifdef HAVE_LIBSAMPLERATE
+    if (mRemoteSampleRate != mLocalSampleRate) {
+        if (mSrcStatePtr == nullptr) {
+            int srcErr;
+            mSrcStatePtr = src_new(SRC_SINC_BEST_QUALITY, 2, &srcErr);
+            if (mSrcStatePtr == nullptr) {
+                cerr << "Failed to prepare sample rate converter: "
+                     << src_strerror(srcErr) << endl;
+                mSocketPtr->close();
+                return;
+            }
+            if (mSrcInDataPtr == nullptr) {
+                mSrcInDataPtr =
+                    new float[AudioSocketMaxSamplesPerBlock * BytesForFullSample];
+            }
+            mSrcData.data_in = mSrcInDataPtr;
+            mSrcData.data_out =
+                reinterpret_cast<float*>(mRecvBuffer.data() + BytesPerSample);
+            mSrcData.output_frames = AudioSocketMaxSamplesPerBlock;
+        } else {
+            src_reset(mSrcStatePtr);
+        }
+        mSrcData.src_ratio    = static_cast<double>(mLocalSampleRate) / mRemoteSampleRate;
+        mSrcData.end_of_input = 0;
+        mSrcInSamples         = 0;
+    }
+#else
+    if (mRemoteSampleRate != mLocalSampleRate) {
+        cerr << "Audio socket sample rate conversion not supported: " << mRemoteSampleRate
+             << " != " << mLocalSampleRate << endl;
+        mSocketPtr->close();
+        return;
+    }
+#endif
+
+    QObject::connect(mSocketPtr.data(), &QLocalSocket::readyRead, this,
+                     &AudioSocketWorker::receiveAudio, Qt::QueuedConnection);
+    emit signalRemoteIsReady();
+}
+
+//*******************************************************************************
+void AudioSocketWorker::sendAudio()
+{
+    if (!mSocketPtr->isValid() || mSocketPtr->state() != QLocalSocket::ConnectedState) {
+        // lost audio socket connection
+        cout << "Lost audio socket connection" << endl;
+        mSocketPtr->disconnect();
+        emit signalLostConnection();
+        return;
+    }
+
+    if (mSendQueue.empty()) {
+        return;
+    }
+
+    // send local audio packets to remote
+    int8_t* popPtr = reinterpret_cast<int8_t*>(mPopBuffer.data());
+    while (mSendQueue.pop(popPtr)) {
+        float* framePtr = reinterpret_cast<float*>(mPopBuffer.data());
+        int bytesToSend = *(framePtr++) * BytesForFullSample;
+        mSendBuffer.resize(bytesToSend);
+        memcpy(mSendBuffer.data(), framePtr, bytesToSend);
+        mSocketPtr->write(mSendBuffer);
+    }
+    mSocketPtr->waitForBytesWritten(-1);
+}
+
+//*******************************************************************************
+void AudioSocketWorker::receiveAudio()
+{
+    while (mSocketPtr->bytesAvailable() > BytesForFullSample) {
+        qint64 bytesToRead = mSocketPtr->bytesAvailable();
+        if (bytesToRead + BytesPerSample > mRecvBuffer.size())
+            bytesToRead = mRecvBuffer.size() - BytesPerSample;
+        if (bytesToRead % BytesForFullSample > 0)
+            bytesToRead -= (bytesToRead % BytesForFullSample);
+        int newSamples = bytesToRead / BytesForFullSample;
+
+#ifdef HAVE_LIBSAMPLERATE
+        if (mRemoteSampleRate == mLocalSampleRate) {
+            mSocketPtr->read(mRecvBuffer.data() + BytesPerSample, bytesToRead);
+        } else {
+            // convert remote to local sample rate
+            mSrcData.input_frames = newSamples + mSrcInSamples;
+            mSocketPtr->read(reinterpret_cast<char*>(mSrcInDataPtr)
+                                 + (mSrcInSamples * BytesForFullSample),
+                             bytesToRead);
+            int srcErr = src_process(mSrcStatePtr, &mSrcData);
+            if (srcErr != 0) {
+                cerr << "Sample rate conversion failure: " << src_strerror(srcErr)
+                     << endl;
+                mSocketPtr->close();
+                return;
+            }
+            mSrcInSamples = mSrcData.input_frames - mSrcData.input_frames_used;
+            if (mSrcInSamples > 0) {
+                // save remaining input frames for later
+                if (mSrcData.input_frames_used > 0) {
+                    // shift samples in memory buffer
+                    char* nextFramePtr =
+                        reinterpret_cast<char*>(mSrcInDataPtr)
+                        + (mSrcData.input_frames_used * BytesForFullSample);
+                    memmove(mSrcInDataPtr, nextFramePtr,
+                            mSrcInSamples * BytesForFullSample);
+                }
+            }
+            newSamples = mSrcData.output_frames_gen;
+        }
+#else
+        mSocketPtr->read(mRecvBuffer.data() + BytesPerSample, bytesToRead);
+#endif
+
+        if (newSamples > 0) {
+            // first value represents number of samples
+            float* framePtr = reinterpret_cast<float*>(mRecvBuffer.data());
+            *framePtr       = newSamples;
+            mReceiveQueue.push(reinterpret_cast<int8_t*>(mRecvBuffer.data()));
+        }
+    }
+}
+
+//*******************************************************************************
+void AudioSocketWorker::scheduleReconnect()
+{
+    if (mRetryConnection) {
+        qDebug() << "Attempting to reconnect audio socket";
+        if (mTimerPtr.isNull()) {
+            mTimerPtr.reset(new QTimer);
+            QObject::connect(mTimerPtr.data(), &QTimer::timeout, this,
+                             &AudioSocketWorker::connect);
+        }
+        mTimerPtr->start(1000);  // try reconnecting in 1 second
+    }
+}
+
+//*******************************************************************************
+AudioSocket::AudioSocket(bool retryConnection)
+    : mThread()
+    , mSendQueue(AudioSocketMaxSamplesPerBlock * BytesForFullSample + BytesPerSample)
+    , mReceiveQueue(AudioSocketMaxSamplesPerBlock * BytesForFullSample + BytesPerSample)
+    , mToAudioSocketPluginPtr(new ToAudioSocketPlugin(mSendQueue, mReceiveQueue))
+    , mFromAudioSocketPluginPtr(new FromAudioSocketPlugin(mSendQueue, mReceiveQueue))
+{
+    mThread.setObjectName("AudioSocket");
+    mThread.start();
+
+    QSharedPointer<QLocalSocket> s(new QLocalSocket);
+    s->moveToThread(&mThread);
+
+    mWorkerPtr.reset(new AudioSocketWorker(mSendQueue, mReceiveQueue, s));
+    mWorkerPtr->moveToThread(&mThread);
+    mWorkerPtr->setRetryConnection(retryConnection);
+
+    initWorker();
+}
+
+//*******************************************************************************
+AudioSocket::AudioSocket(QSharedPointer<QLocalSocket>& s)
+    : mThread()
+    , mSendQueue(AudioSocketMaxSamplesPerBlock * BytesForFullSample + BytesPerSample)
+    , mReceiveQueue(AudioSocketMaxSamplesPerBlock * BytesForFullSample + BytesPerSample)
+    , mToAudioSocketPluginPtr(new ToAudioSocketPlugin(mSendQueue, mReceiveQueue))
+    , mFromAudioSocketPluginPtr(new FromAudioSocketPlugin(mSendQueue, mReceiveQueue))
+    , mWorkerPtr(new AudioSocketWorker(mSendQueue, mReceiveQueue, s))
+{
+    mThread.setObjectName("AudioSocket");
+    mThread.start();
+
+    s->moveToThread(&mThread);
+    mWorkerPtr->moveToThread(&mThread);
+
+    initWorker();
+}
+
+//*******************************************************************************
+void AudioSocket::initWorker()
+{
+    auto* toPluginPtr = static_cast<ToAudioSocketPlugin*>(mToAudioSocketPluginPtr.get());
+    auto* fromPluginPtr =
+        static_cast<FromAudioSocketPlugin*>(mFromAudioSocketPluginPtr.get());
+
+    QObject::connect(this, &AudioSocket::signalConnect, mWorkerPtr.data(),
+                     &AudioSocketWorker::connect, Qt::QueuedConnection);
+    QObject::connect(this, &AudioSocket::signalClose, mWorkerPtr.data(),
+                     &AudioSocketWorker::close, Qt::QueuedConnection);
+    QObject::connect(this, &AudioSocket::signalStartWorker, mWorkerPtr.data(),
+                     &AudioSocketWorker::start, Qt::QueuedConnection);
+    QObject::connect(toPluginPtr, &ToAudioSocketPlugin::signalSendAudioHeader,
+                     mWorkerPtr.data(), &AudioSocketWorker::sendAudioHeader,
+                     Qt::QueuedConnection);
+    QObject::connect(toPluginPtr, &ToAudioSocketPlugin::signalSendAudio,
+                     mWorkerPtr.data(), &AudioSocketWorker::sendAudio,
+                     Qt::QueuedConnection);
+    QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalRemoteIsReady,
+                     toPluginPtr, &ToAudioSocketPlugin::remoteIsReady,
+                     Qt::DirectConnection);
+    QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalRemoteIsReady,
+                     fromPluginPtr, &FromAudioSocketPlugin::remoteIsReady,
+                     Qt::DirectConnection);
+    QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalConnectionEstablished,
+                     toPluginPtr, &ToAudioSocketPlugin::gotConnection,
+                     Qt::DirectConnection);
+    QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalConnectionEstablished,
+                     fromPluginPtr, &FromAudioSocketPlugin::gotConnection,
+                     Qt::DirectConnection);
+    QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalLostConnection,
+                     toPluginPtr, &ToAudioSocketPlugin::lostConnection,
+                     Qt::DirectConnection);
+    QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalLostConnection,
+                     fromPluginPtr, &FromAudioSocketPlugin::lostConnection,
+                     Qt::DirectConnection);
+    QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalLostConnection,
+                     mWorkerPtr.data(), &AudioSocketWorker::scheduleReconnect,
+                     Qt::DirectConnection);
+    QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalConnectionFailed,
+                     mWorkerPtr.data(), &AudioSocketWorker::scheduleReconnect,
+                     Qt::DirectConnection);
+    QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalReadAudioHeader,
+                     mWorkerPtr.data(), &AudioSocketWorker::readAudioHeader,
+                     Qt::QueuedConnection);
+
+    if (isConnected()) {
+        toPluginPtr->gotConnection();
+        fromPluginPtr->gotConnection();
+    }
+
+    emit signalStartWorker();
+}
+
+//*******************************************************************************
+AudioSocket::~AudioSocket()
+{
+    mThread.quit();
+    mThread.wait();
+    mWorkerPtr.reset();
+}
+
+//*******************************************************************************
+bool AudioSocket::connect(int samplingRate, int bufferSize)
+{
+    if (mWorkerPtr->isConnected()) {
+        return true;
+    }
+
+    mFromAudioSocketPluginPtr->init(samplingRate, bufferSize);
+    mToAudioSocketPluginPtr->init(samplingRate, bufferSize);
+    emit signalConnect();
+
+    QTimer timer;
+    timer.setTimerType(Qt::CoarseTimer);
+    timer.setSingleShot(true);
+
+    QEventLoop loop;
+    QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalConnectionEstablished,
+                     &loop, &QEventLoop::quit);
+    QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalConnectionFailed, &loop,
+                     &QEventLoop::quit);
+    QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+    timer.start(1000);
+    loop.exec();
+
+    return mWorkerPtr->isConnected();
+}
+
+//*******************************************************************************
+void AudioSocket::compute(int nframes, float** inputs, float** outputs)
+{
+    mToAudioSocketPluginPtr->compute(nframes, inputs, outputs);
+    mFromAudioSocketPluginPtr->compute(nframes, inputs, outputs);
+}
+
+//*******************************************************************************
+void AudioSocket::close()
+{
+    emit signalClose();
+}
diff --git a/src/AudioSocket.h b/src/AudioSocket.h
new file mode 100644 (file)
index 0000000..3d1be40
--- /dev/null
@@ -0,0 +1,280 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024-2025 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 AudioSocket.h
+ * \author Mike Dickey
+ * \date December 2024
+ * \license MIT
+ */
+
+#ifndef __AUDIOSOCKET_H__
+#define __AUDIOSOCKET_H__
+
+#include <QLocalSocket>
+#include <QObject>
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <QThread>
+#include <QTimer>
+
+#include "ProcessPlugin.h"
+#include "WaitFreeFrameBuffer.h"
+
+#ifdef HAVE_LIBSAMPLERATE
+#include "samplerate.h"
+#endif
+
+// assume stereo audio for this implementation
+constexpr int AudioSocketNumChannels = 2;
+
+// assume max buffer size of 8192 samples
+constexpr int AudioSocketMaxSamplesPerBlock = 8192;
+
+// allow up to 1024 frames
+constexpr int AudioSocketMaxQueueSize = 1024;
+
+// audio header is 4 bytes for the number of samples + 2 bytes for the buffer size
+constexpr int AudioSocketHeaderSize = 4 + 2;
+
+// data type for audio socket circular buffer
+typedef WaitFreeFrameBuffer<AudioSocketMaxQueueSize> AudioSocketQueueT;
+
+/** \brief ToAudioSocketPlugin is used to send audio from a signal chain to an audio
+ * socket
+ */
+class ToAudioSocketPlugin : public ProcessPlugin
+{
+    Q_OBJECT;
+
+   public:
+    ToAudioSocketPlugin(AudioSocketQueueT& sendQueue, AudioSocketQueueT& receiveQueue);
+    virtual ~ToAudioSocketPlugin();
+
+    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 "ToAudioSocket"; };
+    void updateNumChannels(int nChansIn, int nChansOut) override;
+
+   signals:
+    void signalSendAudioHeader(uint32_t sampleRate, uint16_t bufferSize);
+    void signalSendAudio();
+
+   public slots:
+    void remoteIsReady();
+    void gotConnection();
+    void lostConnection();
+
+   private:
+    AudioSocketQueueT& mSendQueue;
+    AudioSocketQueueT& mReceiveQueue;
+    QByteArray mSendBuffer;
+    int mNumChannels      = AudioSocketNumChannels;
+    bool mSentAudioHeader = false;
+    bool mRemoteIsReady   = false;
+    bool mIsConnected     = false;
+};
+
+/** \brief FromAudioSocketPlugin is used mix audio from an audio socket into a signal
+ * chain
+ */
+class FromAudioSocketPlugin : public ProcessPlugin
+{
+    Q_OBJECT;
+
+   public:
+    FromAudioSocketPlugin(AudioSocketQueueT& sendQueue, AudioSocketQueueT& receiveQueue,
+                          bool passthrough = false);
+    virtual ~FromAudioSocketPlugin();
+
+    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 "FromAudioSocket"; };
+    void updateNumChannels(int nChansIn, int nChansOut) override;
+    void setPassthrough(bool b) { mPassthrough = b; }
+
+   public slots:
+    void remoteIsReady();
+    void gotConnection();
+    void lostConnection();
+
+   protected:
+    void updateQueueStats(int nframes);
+    void resetQueueStats();
+
+   private:
+    AudioSocketQueueT& mSendQueue;
+    AudioSocketQueueT& mReceiveQueue;
+    QByteArray mRecvBuffer;
+    float** mExtraSamples    = nullptr;
+    int mNumChannels         = AudioSocketNumChannels;
+    int mNextExtraSample     = 0;
+    int mLastExtraSample     = 0;
+    int mMinQueuePackets     = 0;
+    int mMaxQueuePackets     = 0;
+    int mQueueCheckSec       = 0;
+    uint32_t mNextQueueCheck = 0;
+    bool mRemoteIsReady      = false;
+    bool mIsConnected        = false;
+    bool mPassthrough        = false;
+};
+
+/** \brief AudioSocketWorker is used to perform socket operations in a separate thread
+ */
+class AudioSocketWorker : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    AudioSocketWorker(AudioSocketQueueT& sendQueue, AudioSocketQueueT& receiveQueue,
+                      QSharedPointer<QLocalSocket>& s);
+    virtual ~AudioSocketWorker();
+
+    inline void setRetryConnection(bool retry) { mRetryConnection = retry; }
+    inline bool isConnected()
+    {
+        return mSocketPtr->state() == QLocalSocket::ConnectedState;
+    }
+    inline QLocalSocket& getSocket() { return *mSocketPtr; }
+
+   signals:
+    void signalReadAudioHeader();
+    void signalConnectionEstablished();
+    void signalConnectionFailed();
+    void signalLostConnection();
+    void signalRemoteIsReady();
+
+   public slots:
+    // sets a few things up at startup
+    void start();
+
+    // attempts to connect to remote instance's socket server
+    // returns true if connection was successfully established
+    // returns false and schedules retry if connection failed
+    void connect();
+
+    /// \brief closes the connection to remote instance's socket server
+    void close();
+
+    /// \brief send audio header to remote instance
+    void sendAudioHeader(uint32_t sampleRate, uint16_t bufferSize);
+
+    /// \brief read audio header from remote instance
+    void readAudioHeader();
+
+    /// \brief sends audio packets to remote instance
+    void sendAudio();
+
+    /// \brief receives audio bytes from remote instance
+    void receiveAudio();
+
+    /// \brief schedules a reconnect attempt
+    void scheduleReconnect();
+
+   private:
+    AudioSocketQueueT& mSendQueue;
+    AudioSocketQueueT& mReceiveQueue;
+    QScopedPointer<QTimer> mTimerPtr;
+    QSharedPointer<QLocalSocket> mSocketPtr;
+    QByteArray mSendBuffer;
+    QByteArray mRecvBuffer;
+    QByteArray mPopBuffer;
+    bool mRetryConnection = false;
+    int mLocalSampleRate  = 0;
+    int mRemoteSampleRate = 0;
+#ifdef HAVE_LIBSAMPLERATE
+    SRC_DATA mSrcData;
+    SRC_STATE* mSrcStatePtr = nullptr;
+    float* mSrcInDataPtr    = nullptr;
+    int mSrcInSamples       = 0;
+#endif
+};
+
+/** \brief An AudioSocket is used to exchange audio with another processes via a local
+ * socket
+ */
+class AudioSocket : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    AudioSocket(bool retryConnection = false);
+    AudioSocket(QSharedPointer<QLocalSocket>& s);
+    virtual ~AudioSocket();
+
+    inline bool isConnected() { return mWorkerPtr->isConnected(); }
+    inline QLocalSocket& getSocket() { return mWorkerPtr->getSocket(); }
+    inline int getSampleRate() const { return mToAudioSocketPluginPtr->getSampleRate(); }
+    inline int getBufferSize() const { return mToAudioSocketPluginPtr->getBufferSize(); }
+    inline QSharedPointer<ProcessPlugin>& getToAudioSocketPlugin()
+    {
+        return mToAudioSocketPluginPtr;
+    }
+    inline QSharedPointer<ProcessPlugin>& getFromAudioSocketPlugin()
+    {
+        return mFromAudioSocketPluginPtr;
+    }
+    inline void setRetryConnection(bool retry) { mWorkerPtr->setRetryConnection(retry); }
+
+    // attempts to connect to remote instance's socket server
+    // returns true if connection was successfully established
+    // returns false and schedules retry if connection failed
+    bool connect(int samplingRate, int bufferSize);
+
+    /// \brief audio callback for duplex processing
+    void compute(int nframes, float** inputs, float** outputs);
+
+    /// \brief closes the connection to remote instance's socket server
+    void close();
+
+   signals:
+    void signalStartWorker();
+    void signalConnect();
+    void signalClose();
+
+   private:
+    /// \brief initializes worker and worker thread
+    void initWorker();
+
+    QThread mThread;
+    AudioSocketQueueT mSendQueue;
+    AudioSocketQueueT mReceiveQueue;
+    QSharedPointer<ProcessPlugin> mToAudioSocketPluginPtr;
+    QSharedPointer<ProcessPlugin> mFromAudioSocketPluginPtr;
+    QScopedPointer<AudioSocketWorker> mWorkerPtr;
+
+    friend class AudioSocketWorker;
+};
+
+#endif
\ No newline at end of file
index 04311fe79488fec738c1767deaf5091b8f30909c..a58bbd79b66f61c6bab429e7d3f036cdab39f253 100644 (file)
@@ -95,7 +95,7 @@ void Compressor::setParamAllChannels(const char pName[], float p)
 void Compressor::init(int samplingRate, int bufferSize)
 {
     ProcessPlugin::init(samplingRate, bufferSize);
-    fs = float(fSamplingFreq);
+    fs = float(mSampleRate);
     for (int i = 0; i < mNumChannels; i++) {
         static_cast<compressordsp*>(compressorP[i])
             ->init(fs);  // compression filter parameters depend on sampling rate
index ad5bd5b4a055b71453c3625d0e7c4ce4c0b3d8cb..2813da17e3489469916c2db2bb46ae37d85ccb05 100644 (file)
@@ -460,27 +460,27 @@ void JackTrip::setPeerAddress(const QString& PeerHostOrIP)
 }
 
 //*******************************************************************************
-void JackTrip::appendProcessPluginToNetwork(ProcessPlugin* plugin)
+void JackTrip::appendProcessPluginToNetwork(QSharedPointer<ProcessPlugin>& plugin)
 {
-    if (plugin) {
+    if (!plugin.isNull()) {
         mProcessPluginsToNetwork.append(plugin);  // ownership transferred
         // mAudioInterface->appendProcessPluginToNetwork(plugin);
     }
 }
 
 //*******************************************************************************
-void JackTrip::appendProcessPluginFromNetwork(ProcessPlugin* plugin)
+void JackTrip::appendProcessPluginFromNetwork(QSharedPointer<ProcessPlugin>& plugin)
 {
-    if (plugin) {
+    if (!plugin.isNull()) {
         mProcessPluginsFromNetwork.append(plugin);  // ownership transferred
         // mAudioInterface->appendProcessPluginFromNetwork(plugin);
     }
 }
 
 //*******************************************************************************
-void JackTrip::appendProcessPluginToMonitor(ProcessPlugin* plugin)
+void JackTrip::appendProcessPluginToMonitor(QSharedPointer<ProcessPlugin>& plugin)
 {
-    if (plugin) {
+    if (!plugin.isNull()) {
         mProcessPluginsToMonitor.append(plugin);  // ownership transferred
         // mAudioInterface->appendProcessPluginFromNetwork(plugin);
     }
@@ -769,7 +769,9 @@ void JackTrip::onStatTimer()
             //                     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_corr
+            << " \t latency (max): " << setw(5) << std::setprecision(3)
+            << mReceiveRingBuffer->getLatency() << " \t dsp (max): " << setw(5)
             << INVFLOATFACTOR * recv_io_stat.autoq_rate
             //                     << " sync: " << recv_io_stat.level << "/"
             //                     << recv_io_stat.buf_inc_underrun << "/"
index 166106b327722d312fddb944c43a6373fefc33f7..00b9cce1b640c9f9e5db4e4ff1b07cb6fed27428 100644 (file)
@@ -173,9 +173,9 @@ class JackTrip : public QObject
      * \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);
+    virtual void appendProcessPluginToNetwork(QSharedPointer<ProcessPlugin>& plugin);
+    virtual void appendProcessPluginFromNetwork(QSharedPointer<ProcessPlugin>& plugin);
+    virtual void appendProcessPluginToMonitor(QSharedPointer<ProcessPlugin>& plugin);
 
     /// \brief Start the processing threads
     virtual void startProcess(
@@ -219,9 +219,18 @@ class JackTrip : public QObject
         createHeader(mPacketHeaderType);
     }
     /// \brief Sets (override) Buffer Queue Length Mode after construction
-    virtual void setBufferQueueLength(int BufferQueueLength)
+    virtual void setBufferQueueLength(int queueBuffer)
     {
-        mBufferQueueLength = BufferQueueLength;
+        if (mBufferQueueLength == queueBuffer) {
+            return;
+        }
+        mBufferQueueLength = queueBuffer;
+        if (mReceiveRingBuffer != nullptr
+            && (mBufferStrategy == 3 || mBufferStrategy == 4)) {
+            // mReceiveRingBuffer should be an instance of Regulator when mBufferStrategy
+            // is 3 or 4
+            mReceiveRingBuffer->setQueueBufferLength(mBufferQueueLength);
+        }
     }
     virtual void setBufferStrategy(int BufferStrategy)
     {
@@ -542,6 +551,10 @@ class JackTrip : public QObject
         return (mAudioInterface == nullptr) ? false
                                             : mAudioInterface->getHighLatencyFlag();
     }
+    double getLatency() const
+    {
+        return mReceiveRingBuffer == nullptr ? -1 : mReceiveRingBuffer->getLatency();
+    }
     //@}
     //------------------------------------------------------------------------------------
 
@@ -705,11 +718,11 @@ class JackTrip : public QObject
     JackTrip::hubConnectionModeT
         mHubConnectionModeT;  ///< Hub Server Jack Audio Patch Connection Mode
 
-    QVector<ProcessPlugin*>
+    QVector<QSharedPointer<ProcessPlugin> >
         mProcessPluginsFromNetwork;  ///< Vector of ProcessPlugin<EM>s</EM>
-    QVector<ProcessPlugin*>
+    QVector<QSharedPointer<ProcessPlugin> >
         mProcessPluginsToNetwork;  ///< Vector of ProcessPlugin<EM>s</EM>
-    QVector<ProcessPlugin*>
+    QVector<QSharedPointer<ProcessPlugin> >
         mProcessPluginsToMonitor;  ///< Vector of ProcessPlugin<EM>s</EM>
     QTimer mTimeoutTimer;
     QTimer mRetryTimer;
index dc31a1581c28c8b864db3d5bc0930baae734f6cf..091f9836de53de1a359008d6ceba3bb20ef288ac 100644 (file)
@@ -112,6 +112,15 @@ class JackTripWorker : public QObject
     }
     void setBroadcast(int broadcast_queue) { mBroadcastQueue = broadcast_queue; }
     void setUseRtUdpPriority(bool use) { mUseRtUdpPriority = use; }
+    void setBufferQueueLength(int queueBufferLength)
+    {
+        QMutexLocker lock(&mMutex);
+        if (mJackTrip.isNull() || mBufferQueueLength == queueBufferLength) {
+            return;
+        }
+        mBufferQueueLength = queueBufferLength;
+        mJackTrip->setBufferQueueLength(mBufferQueueLength);
+    }
 
     void setIOStatTimeout(int timeout) { mIOStatTimeout = timeout; }
     void setIOStatStream(QSharedPointer<std::ostream> statStream)
index a4ca931d1c80e9e82bbaa35cf7bcc32b2d280c5e..e94ecc748889a22c1309f7ca7b434c5e7295a2b7 100644 (file)
@@ -55,6 +55,9 @@ class JitterBuffer : public RingBuffer
 
     virtual bool getStats(IOStat* stat, bool reset);
 
+    /// @brief returns max latency during previous interval, in milliseconds
+    virtual double getLatency() const { return mMaxLatency; }
+
     void setJackTrip(JackTrip* jackTrip) { mJackTrip = jackTrip; }
 
    protected:
index 8796075df64a3bbb142d7a4ae00a08f88f905e39..d80326451d9e53460048cd14a7163601ef768c5f 100644 (file)
@@ -85,7 +85,7 @@ Limiter::~Limiter()
 void Limiter::init(int samplingRate, int bufferSize)
 {
     ProcessPlugin::init(samplingRate, bufferSize);
-    fs = float(fSamplingFreq);
+    fs = float(mSampleRate);
     for (int i = 0; i < mNumChannels; i++) {
         static_cast<limiterdsp*>(limiterP[i])
             ->init(fs);  // compression filter parameters depend on sampling rate
index a641e8bd632a1452e69fbf3d3f0a7ba0c2c3e1ee..f77336b6c1b508b4824a53189ec7fa1839abed82 100644 (file)
@@ -76,7 +76,7 @@ void Meter::init(int samplingRate, int bufferSize)
 {
     ProcessPlugin::init(samplingRate, bufferSize);
 
-    fs = float(fSamplingFreq);
+    fs = float(mSampleRate);
     for (int i = 0; i < mNumChannels; i++) {
         static_cast<meterdsp*>(meterP[i])->init(fs);
     }
index 2f9a7d26e400af6e6bd21cf062c6bb13573568d7..4ebf0d14795eae1be918770603c665b106b25b30 100644 (file)
@@ -70,7 +70,7 @@ Monitor::~Monitor()
 void Monitor::init(int samplingRate, int bufferSize)
 {
     ProcessPlugin::init(samplingRate, bufferSize);
-    fs = float(fSamplingFreq);
+    fs = float(mSampleRate);
 
     for (int i = 0; i < mNumChannels; i++) {
         static_cast<monitordsp*>(monitorP[i])
diff --git a/src/OscServer.cpp b/src/OscServer.cpp
new file mode 100644 (file)
index 0000000..5c3b61b
--- /dev/null
@@ -0,0 +1,143 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024 JackTrip Labs, Inc.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file OscServer.cpp
+ * \author Nelson Wang
+ * \date November 2024
+ */
+
+#include "OscServer.h"
+
+#include <iostream>
+
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+OscServer::OscServer(quint16 port, QObject* parent) : QObject(parent), mPort(port) {}
+
+//*******************************************************************************
+OscServer::~OscServer()
+{
+    stop();
+}
+
+//*******************************************************************************
+void OscServer::stop()
+{
+    closeSocket();
+}
+
+//*******************************************************************************
+void OscServer::closeSocket()
+{
+    if (!mOscServerSocket.isNull()) {
+        mOscServerSocket->close();
+        mOscServerSocket.reset();
+    }
+}
+
+//*******************************************************************************
+void OscServer::start()
+{
+    mOscServerSocket.reset(new QUdpSocket(this));
+    qDebug() << "Binding OSC server socket to UDP port " << mPort;
+    if (!mOscServerSocket->bind(QHostAddress::LocalHost, mPort)) {
+        qDebug() << "Error binding OSC server socket";
+        return;
+    }
+
+    connect(mOscServerSocket.get(), &QUdpSocket::readyRead, this,
+            &OscServer::readPendingDatagrams);
+    qDebug() << "OSC server started on UDP port " << mPort;
+}
+
+//*******************************************************************************
+void OscServer::readPendingDatagrams()
+{
+    while (mOscServerSocket->hasPendingDatagrams()) {
+        QByteArray datagram;
+        datagram.resize(mOscServerSocket->pendingDatagramSize());
+        QHostAddress sender;
+        quint16 senderPort;
+
+        mOscServerSocket->readDatagram(datagram.data(), datagram.size(), &sender,
+                                       &senderPort);
+        qDebug() << "Received datagram from" << sender << ":" << senderPort;
+        qDebug() << "  - Data:" << datagram;
+#ifndef NO_OSCPP
+        handlePacket(OSCPP::Server::Packet(datagram.data(), datagram.size()));
+#endif  // NO_OSCPP
+        // Send a reply back to the client
+        // QByteArray replyData("Reply from server");
+        // socket->writeDatagram(replyData, sender, senderPort);
+    }
+}
+
+//*******************************************************************************
+#ifndef NO_OSCPP
+void OscServer::handlePacket(const OSCPP::Server::Packet& packet)
+{
+    try {
+        if (packet.isBundle()) {
+            // Convert to bundle
+            OSCPP::Server::Bundle bundle(packet);
+            // Get packet stream
+            OSCPP::Server::PacketStream packets(bundle.packets());
+
+            // Iterate over all the packets and call handlePacket recursively.
+            while (!packets.atEnd()) {
+                handlePacket(packets.next());
+            }
+        } else {
+            // Convert to message
+            OSCPP::Server::Message msg(packet);
+            // Get argument stream
+            OSCPP::Server::ArgStream args(msg.args());
+
+            if (msg == "/config") {
+                const char* key   = args.string();
+                const float value = args.float32();
+                cout << "Config received - key (" << key << ") value (" << value << ")"
+                     << endl;
+                if (strcmp("queueBuffer", key) == 0) {
+                    emit signalQueueBufferChanged(static_cast<int>(value));
+                }
+            } else {
+                // Simply print unknown messages
+                cout << "Unknown message:" << msg.address() << endl;
+            }
+        }
+    } catch (std::exception& e) {
+        cout << "Exception:" << e.what() << endl;
+    }
+}
+#endif  // NO_OSCPP
diff --git a/src/OscServer.h b/src/OscServer.h
new file mode 100644 (file)
index 0000000..5b2b55c
--- /dev/null
@@ -0,0 +1,100 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024 JackTrip Labs, Inc.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file OscServer.h
+ * \author Nelson Wang
+ * \date November 2024
+ */
+
+#ifndef __OSCSERVER_H__
+#define __OSCSERVER_H__
+
+#include <QObject>
+#include <QUdpSocket>
+#include <QtCore>
+
+#ifndef NO_OSCPP
+#include "oscpp/client.hpp"
+#include "oscpp/server.hpp"
+#endif  // NO_OSCPP
+
+class OscServer : public QObject
+{
+    Q_OBJECT;
+
+   public:
+    OscServer(quint16 port, QObject* parent = nullptr);
+
+    /// \brief The class destructor
+    virtual ~OscServer();
+    void start();
+    void stop();
+
+    static size_t makeConfigPacket(void* buffer, size_t size, const char* key,
+                                   float value)
+    {
+#ifndef NO_OSCPP
+        // Construct a packet
+        OSCPP::Client::Packet packet(buffer, size);
+        packet
+            // Open a bundle with a timetag
+            .openBundle(1234ULL)
+            // Add a message with two arguments
+            // for efficiency this needs to be known in advance.
+            .openMessage("/config", 2)
+            // Write the arguments
+            .string(key)
+            .float32(value)
+            // Every `open` needs a corresponding `close`
+            .closeMessage()
+            .closeBundle();
+        return packet.size();
+#else
+        return 0;
+#endif  // NO_OSCPP
+    }
+   signals:
+    void signalQueueBufferChanged(int queueBufferSize);
+
+   private slots:
+    void readPendingDatagrams();
+
+   private:
+    void closeSocket();
+#ifndef NO_OSCPP
+    void handlePacket(const OSCPP::Server::Packet& packet);
+#endif  // NO_OSCPP
+
+    QSharedPointer<QUdpSocket> mOscServerSocket;
+    quint16 mPort;
+};
+
+#endif
index e4af0edf914e7b7baec595934fcf5b36498ee463..f8fb64af996c23f4aae48c2464c728efdc8b59c3 100644 (file)
@@ -39,7 +39,6 @@
 #define __PROCESSPLUGIN_H__
 
 #include <QObject>
-#include <QThread>
 
 /** \brief Interface for the process plugins to add to the JACK callback process in
  * JackAudioInterface
@@ -56,14 +55,22 @@ class ProcessPlugin : public QObject
    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;
 
+    /// \brief Return local audio sample rate
+    int getSampleRate() const { return mSampleRate; }
+
+    /// \brief Return local audio buffer size
+    int getBufferSize() const { return mBufferSize; }
+
     // virtual void buildUserInterface(UI* interface) = 0;
 
     virtual const char* getName() const = 0;  // get name of DERIVED class
@@ -83,8 +90,8 @@ class ProcessPlugin : public QObject
             bufferSize = 128;
             printf("%s: *** HAD TO GUESS the buffer size (chose 128) ***\n", getName());
         }
-        fSamplingFreq = samplingRate;
-        mBufferSize   = bufferSize;
+        mSampleRate = samplingRate;
+        mBufferSize = bufferSize;
         if (verbose) {
             printf("%s: init(%d, %d)\n", getName(), samplingRate, bufferSize);
         }
@@ -109,8 +116,8 @@ class ProcessPlugin : public QObject
     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
+    int mSampleRate;  //< local audio 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
index cbf06d733456665cc97205729ea4e95d60203c76..faffb2cf458b2b3a96cfa27e03d60d8192eadf52 100644 (file)
@@ -104,7 +104,7 @@ constexpr double AutoInitValFactor =
 // tweak
 constexpr int WindowDivisor = 8;  // for faster auto tracking
 constexpr double AutoHeadroomGlitchTolerance =
-    0.01;  // Acceptable rate of skips before auto headroom is increased (1.0%)
+    0.01;  // Acceptable rate of glitches before auto headroom is increased (1.0%)
 constexpr double AutoHistoryWindow =
     60;  // rolling window of time (in seconds) over which auto tolerance roughly adjusts
 constexpr double AutoSmoothingFactor =
@@ -372,6 +372,9 @@ void Regulator::setFPPratio(int len)
     //////////////////////////////////////
 
     if (mPeerFPP != mLocalFPP) {
+        // adjust broadcast queue length so that time duration matches across all clients
+        if (m_b_BroadcastQueueLength)
+            m_b_BroadcastQueueLength = (m_b_BroadcastQueueLength * mLocalFPP) / mPeerFPP;
         if (mPeerFPP > mLocalFPP)
             mFPPratioDenominator = mPeerFPP / mLocalFPP;
         else
@@ -479,29 +482,30 @@ void Regulator::updateTolerance(int glitches, int skipped)
         // only increase headroom if doing so would have reduced the number of
         // glitches that occured over the past second by 1% or more.
         // prevent headroom from growing beyond rolling average of max.
-        int skipsAllowed;
+        const int maxHeadroom = pushStat->longTermMax + 1;
+        int glitchesAllowed;
         if (mMsecTolerance >= (mPeerFPPdurMsec * 2)) {
-            // calculate skips allowed if tolerance if above or equal to duration of two
-            // packets
-            skipsAllowed =
+            // calculate glitches allowed if tolerance if above or equal to duration of
+            // two packets
+            glitchesAllowed =
                 static_cast<int>(AutoHeadroomGlitchTolerance * mSampleRate / mPeerFPP);
         } else {
-            // zero skips allowed if tolerance is below duration of two packets
-            skipsAllowed = 0;
+            // zero glitches allowed if tolerance is below duration of two packets
+            glitchesAllowed = 0;
             // also don't require two intervals in a row (override)
             mSkipAutoHeadroom = false;
         }
-        if (glitches > 0 && skipped > skipsAllowed
-            && mCurrentHeadroom + 1 <= pushStat->longTermMax) {
+        if (skipped > 0 && glitches > glitchesAllowed
+            && mCurrentHeadroom + 1 <= maxHeadroom) {
             if (mSkipAutoHeadroom) {
                 mSkipAutoHeadroom = false;
             } else {
                 // don't increase headroom two intervals in a row
                 mSkipAutoHeadroom = true;
                 ++mCurrentHeadroom;
-                cout << "PLC glitches=" << glitches << " skipped=" << skipped << ">"
-                     << skipsAllowed << ", increasing headroom to " << mCurrentHeadroom
-                     << " (max=" << pushStat->longTermMax << ")" << endl;
+                cout << "PLC skipped=" << skipped << " glitches=" << glitches << ">"
+                     << glitchesAllowed << ", increasing headroom to " << mCurrentHeadroom
+                     << " (max=" << maxHeadroom << ")" << endl;
             }
         } else {
             // thresholds not met: require 2 intervals in a row
@@ -549,9 +553,11 @@ void Regulator::updatePushStats(int seq_num)
         const int newSkipped    = totalSkipped - mLastSkipped;
         mLastGlitches           = totalGlitches;
         mLastSkipped            = totalSkipped;
+        mLastMaxLatency         = mStatsMaxLatency;
+        mStatsMaxLatency        = 0;
         if (mAuto && pushStat->lastTime > AutoInitDur) {
             // after AutoInitDur: update auto tolerance once per second
-            if (pushStat->lastTime <= (AutoInitDur + 3000)) {
+            if (pushStat->lastTime <= mAutoHeadroomStartTime) {
                 // Ignore glitches and skips for the first 3 seconds after
                 // we have switched from using the startup tolerance to
                 // a calculated tolerance. Otherwise, the switch can
@@ -565,6 +571,29 @@ void Regulator::updatePushStats(int seq_num)
     }
 }
 
+//*******************************************************************************
+void Regulator::setQueueBufferLength(int queueBuffer)
+{
+    if (queueBuffer > 0) {
+        // update to a fixed tolerance
+        mAuto            = false;
+        mCurrentHeadroom = 0;
+        mMsecTolerance   = queueBuffer;
+        return;
+    }
+    // update auto headroom for auto tolerance
+    mAuto = true;
+    if (queueBuffer == -500.0) {
+        mAutoHeadroom          = -1;
+        mCurrentHeadroom       = 0;
+        mSkipAutoHeadroom      = true;
+        mAutoHeadroomStartTime = pushStat ? (pushStat->lastTime + 3000.0) : 3000.0;
+    } else {
+        mAutoHeadroom    = std::abs(queueBuffer);
+        mCurrentHeadroom = mAutoHeadroom;
+    }
+}
+
 //*******************************************************************************
 void Regulator::pushPacket(const int8_t* buf, int seq_num)
 {
@@ -621,6 +650,10 @@ bool Regulator::pullPacket()
                 // next is the best candidate
                 memcpy(mXfrBuffer, mSlots[next], mPeerBytes);
                 mLastSeqNumOut = next;
+                double latency = (now - mIncomingTiming[mLastSeqNumOut]);
+                if (latency > mStatsMaxLatency) {
+                    mStatsMaxLatency = latency;
+                }
                 goto PACKETOK;
             }
             // track how many good packets we skipped due to tolerance < 1ms
@@ -822,11 +855,16 @@ bool StdDev::tick(double prevTime, double curTime)
         return false;
 
     data[ctr] = msElapsed;
-    if (msElapsed < min)
+    acc += msElapsed;
+    if (ctr == 0) {
         min = msElapsed;
-    else if (msElapsed > max)
         max = msElapsed;
-    acc += msElapsed;
+    } else {
+        if (msElapsed < min)
+            min = msElapsed;
+        if (msElapsed > max)
+            max = msElapsed;
+    }
     if (ctr == 0 && longTermCnt % WindowDivisor == 0) {
         lastMin = msElapsed;
         lastMax = msElapsed;
@@ -1098,7 +1136,8 @@ bool Regulator::getStats(RingBuffer::IOStat* stat, bool reset)
     }
 
     // hijack  of  struct IOStat {
-    stat->underruns = mLastGlitches - mStatsGlitches;
+    const int lastGlitches = mLastGlitches;
+    stat->underruns        = lastGlitches - mStatsGlitches;
 #define FLOATFACTOR 1000.0
     stat->overflows = FLOATFACTOR * pushStat->longTermStdDev;
     stat->skew      = FLOATFACTOR * pushStat->lastMean;
@@ -1114,7 +1153,7 @@ bool Regulator::getStats(RingBuffer::IOStat* stat, bool reset)
     stat->broadcast_delta    = FLOATFACTOR * pullStat->lastStdDev;
     stat->autoq_rate         = FLOATFACTOR * mStatsMaxPLCdspElapsed;
     // reset a few stats for next time
-    mStatsGlitches         = mLastGlitches;
+    mStatsGlitches         = lastGlitches;
     mStatsMaxPLCdspElapsed = 0.0;
     // none are unused
     return true;
index c65c3d2c71b6915ee39b74c9f3f93d620ea97be3..2be916f6f9bde817ad7b68dc4085fe933b21e462 100644 (file)
@@ -225,9 +225,15 @@ class Regulator : public RingBuffer
     /// @brief returns true if worker thread & queue is enabled
     inline bool isWorkerEnabled() const { return mWorkerEnabled; }
 
-    //    virtual QString getStats(uint32_t statCount, uint32_t lostCount);
+    /// @brief returns statistics for -I command line option
     virtual bool getStats(IOStat* stat, bool reset);
 
+    /// @brief sets length of queue buffer
+    virtual void setQueueBufferLength([[maybe_unused]] int queueBuffer);
+
+    /// @brief returns max latency during previous interval, in milliseconds
+    virtual double getLatency() const { return mLastMaxLatency; }
+
    private:
     void pushPacket(const int8_t* buf, int seq_num);
     void updatePushStats(int seq_num);
@@ -294,8 +300,11 @@ class Regulator : public RingBuffer
     int mLastSkipped              = 0;
     int mLastGlitches             = 0;
     int mStatsGlitches            = 0;
+    double mLastMaxLatency        = 0;
+    double mStatsMaxLatency       = 0;
     double mStatsMaxPLCdspElapsed = 0;
     double mCurrentHeadroom       = 0;
+    double mAutoHeadroomStartTime = 6000.0;
     double mAutoHeadroom          = -1;
     Time* mTime                   = nullptr;
 
index c8f3b45f9bf636ba458b2a39f89c7b7d451de2d2..d3ccdd41fecb0126984ec4e09ef3edbf863afac3 100644 (file)
@@ -106,7 +106,7 @@ Reverb::~Reverb()
 void Reverb::init(int samplingRate, int bufferSize)
 {
     ProcessPlugin::init(samplingRate, bufferSize);
-    fs = float(fSamplingFreq);
+    fs = float(mSampleRate);
     if (mReverbLevel <= 1.0) {  // freeverb:
         static_cast<freeverbdsp*>(freeverbStereoP)
             ->init(fs);  // compression filter parameters depend on sampling rate
index 370355ed9736d800dd12fc8c079e7177d9265f31..ea8243c0881c3f6e100e895515583cd2a3b422f5 100644 (file)
@@ -224,6 +224,13 @@ void RingBuffer::readBroadcastSlot(int8_t* ptrToReadSlot)
     std::memset(ptrToReadSlot, 0, mSlotSize);
 }
 
+//*******************************************************************************
+// Not supported in RingBuffer
+void RingBuffer::setQueueBufferLength([[maybe_unused]] int queueBuffer)
+{
+    return;
+}
+
 //*******************************************************************************
 void RingBuffer::setUnderrunReadSlot(int8_t* ptrToReadSlot)
 {
@@ -319,3 +326,10 @@ void RingBuffer::updateReadStats()
     mUnderrunsNew = 0;
     mLevel        = std::ceil(mLevelCur);
 }
+
+//*******************************************************************************
+// Not supported in RingBuffer
+double RingBuffer::getLatency() const
+{
+    return -1;
+}
index a35f0b28ebbf18f225bf88aab4b26f715f6f97e8..9cc07ad2c5c6dd64fc1a88de0dea931dff3794c7 100644 (file)
@@ -101,6 +101,9 @@ class RingBuffer
     virtual void readSlotNonBlocking(int8_t* ptrToReadSlot);
     virtual void readBroadcastSlot(int8_t* ptrToReadSlot);
 
+    /// @brief sets length of queue buffer
+    virtual void setQueueBufferLength([[maybe_unused]] int queueBuffer);
+
     struct IOStat {
         uint32_t underruns;
         uint32_t overflows;
@@ -117,8 +120,13 @@ class RingBuffer
         int32_t autoq_corr;
         int32_t autoq_rate;
     };
+
+    /// @brief returns statistics for -I command line option
     virtual bool getStats(IOStat* stat, bool reset);
 
+    /// @brief returns max latency during previous interval, in milliseconds
+    virtual double getLatency() const;
+
    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.
index 3fa35cda5772376b76e7005f474767d604aff052..8ff1599db70e5dced81de6be4b890bccaa1258dd 100644 (file)
@@ -42,6 +42,7 @@
 #include <cstdlib>
 
 #include "JackTrip.h"
+#include "SampleRateConverter.h"
 #include "StereoToMono.h"
 #include "jacktrip_globals.h"
 
@@ -83,6 +84,13 @@ void RtAudioDevice::printVerbose() const
 #endif
 }
 
+//*******************************************************************************
+bool RtAudioDevice::isAirpods() const
+{
+    return name.substr(0, 11) == "Apple Inc.:"
+           && name.find("AirPods") != std::string::npos;
+}
+
 //*******************************************************************************
 bool RtAudioDevice::checkSampleRate(unsigned int srate) const
 {
@@ -93,6 +101,22 @@ bool RtAudioDevice::checkSampleRate(unsigned int srate) const
     return false;
 }
 
+//*******************************************************************************
+unsigned int RtAudioDevice::getClosestSampleRate(unsigned int srate) const
+{
+    unsigned int bestResult        = 0;
+    const unsigned int doubleSrate = srate * 2;
+    for (unsigned int i = 0; i < this->sampleRates.size(); i++) {
+        if (this->sampleRates[i] == srate)
+            return srate;
+        // pick the next highest rate available, or 2x if available
+        if (this->sampleRates[i] == doubleSrate
+            || (bestResult<srate&& this->sampleRates[i]> bestResult))
+            bestResult = this->sampleRates[i];
+    }
+    return bestResult;
+}
+
 //*******************************************************************************
 RtAudioDevice& RtAudioDevice::operator=(const RtAudio::DeviceInfo& info)
 {
@@ -243,6 +267,31 @@ void RtAudioInterface::setup(bool verbose)
         }
     }
 
+    mInSampleRate = mOutSampleRate = getSampleRate();
+#ifdef HAVE_LIBSAMPLERATE
+    if (!in_device.checkSampleRate(getSampleRate())) {
+        mInSampleRate = in_device.getClosestSampleRate(getSampleRate());
+        mInSrcPtr.reset(new SampleRateConverter(mInSampleRate, getSampleRate(),
+                                                in_chans_num, getBufferSizeInSamples()));
+        cout << "Converting input sample rate from " << mInSampleRate << " to "
+             << getSampleRate() << endl;
+    }
+    // special hack for apple's airpods. these work at 48khz ONLY for output.
+    // they will only run at 24k for input, and if input is active, they will
+    // also run output at 24k, despite claiming to be working at 48k.
+    if (in_device.isAirpods() && out_device.isAirpods()) {
+        mOutSampleRate = 24000;
+    } else if (!out_device.checkSampleRate(getSampleRate())) {
+        mOutSampleRate = out_device.getClosestSampleRate(getSampleRate());
+    }
+    if (mOutSampleRate != getSampleRate()) {
+        mOutSrcPtr.reset(new SampleRateConverter(
+            getSampleRate(), mOutSampleRate, out_chans_num, getBufferSizeInSamples()));
+        mOutTmpPtr.reset(new float[getBufferSizeInSamples() * out_chans_num]);
+        cout << "Converting output sample rate from " << mOutSampleRate << " to "
+             << getSampleRate() << endl;
+    }
+#else
     if (!in_device.checkSampleRate(getSampleRate())) {
         QString errorMsg;
         QTextStream(&errorMsg) << "Input device \"" << QString::fromStdString(in_name)
@@ -257,6 +306,7 @@ void RtAudioInterface::setup(bool verbose)
                                << getSampleRate();
         throw std::runtime_error(errorMsg.toStdString());
     }
+#endif
 
     // provide warnings for common known failure cases
     const QString out_device_lower_name =
@@ -325,8 +375,6 @@ void RtAudioInterface::setup(bool verbose)
     // 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)
@@ -348,15 +396,15 @@ void RtAudioInterface::setup(bool verbose)
     try {
         if (mDuplexMode) {
             mRtAudioInput->openStream(
-                &out_params, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                &out_params, &in_params, RTAUDIO_FLOAT32, mInSampleRate, &bufferFrames,
                 &RtAudioInterface::wrapperRtAudioCallback, this, &options, errorFunc);
         } else {
             mRtAudioInput->openStream(
-                nullptr, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                nullptr, &in_params, RTAUDIO_FLOAT32, mInSampleRate, &bufferFrames,
                 &RtAudioInterface::wrapperRtAudioCallback, this, &options, errorFunc);
             const unsigned int inputBufferFrames = bufferFrames;
             mRtAudioOutput->openStream(
-                &out_params, nullptr, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                &out_params, nullptr, RTAUDIO_FLOAT32, mOutSampleRate, &bufferFrames,
                 &RtAudioInterface::wrapperRtAudioCallback, this, &options, errorFunc);
             if (inputBufferFrames != bufferFrames) {
                 // output device doesn't support the same buffer size
@@ -364,7 +412,7 @@ void RtAudioInterface::setup(bool verbose)
                 const unsigned int outputBufferFrames = bufferFrames;
                 mRtAudioInput->closeStream();
                 mRtAudioInput->openStream(
-                    nullptr, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                    nullptr, &in_params, RTAUDIO_FLOAT32, mInSampleRate, &bufferFrames,
                     &RtAudioInterface::wrapperRtAudioCallback, this, &options, errorFunc);
                 if (outputBufferFrames != bufferFrames) {
                     // just give up if this still doesn't work
@@ -385,7 +433,7 @@ void RtAudioInterface::setup(bool verbose)
     if (mDuplexMode) {
         if (RTAUDIO_NO_ERROR
             != mRtAudioInput->openStream(
-                &out_params, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                &out_params, &in_params, RTAUDIO_FLOAT32, mInSampleRate, &bufferFrames,
                 &RtAudioInterface::wrapperRtAudioCallback, this, &options)) {
             errorText = mRtAudioInput->getErrorText();
         }
@@ -393,14 +441,14 @@ void RtAudioInterface::setup(bool verbose)
         mRtAudioOutput->setErrorCallback(errorFunc);
         if (RTAUDIO_NO_ERROR
             != mRtAudioInput->openStream(
-                nullptr, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
+                nullptr, &in_params, RTAUDIO_FLOAT32, mInSampleRate, &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,
+                    &out_params, nullptr, RTAUDIO_FLOAT32, mOutSampleRate, &bufferFrames,
                     &RtAudioInterface::wrapperRtAudioCallback, this, &options)) {
                 errorText = mRtAudioOutput->getErrorText();
             } else if (inputBufferFrames != bufferFrames) {
@@ -410,8 +458,9 @@ void RtAudioInterface::setup(bool verbose)
                 mRtAudioInput->closeStream();
                 if (RTAUDIO_NO_ERROR
                     != mRtAudioInput->openStream(
-                        nullptr, &in_params, RTAUDIO_FLOAT32, sampleRate, &bufferFrames,
-                        &RtAudioInterface::wrapperRtAudioCallback, this, &options)) {
+                        nullptr, &in_params, RTAUDIO_FLOAT32, mInSampleRate,
+                        &bufferFrames, &RtAudioInterface::wrapperRtAudioCallback, this,
+                        &options)) {
                     errorText = mRtAudioInput->getErrorText();
                 } else if (outputBufferFrames != bufferFrames) {
                     // just give up if this still doesn't work
@@ -450,7 +499,7 @@ void RtAudioInterface::setup(bool verbose)
     // Setup StereoToMonoMixer
     // This MUST be after RtAudio::openSteram in case bufferFrames changes
     mStereoToMonoMixerPtr.reset(new StereoToMono());
-    mStereoToMonoMixerPtr->init(sampleRate, bufferFrames);
+    mStereoToMonoMixerPtr->init(getSampleRate(), bufferFrames);
 }
 
 //*******************************************************************************
@@ -550,47 +599,85 @@ long RtAudioInterface::getDefaultDeviceForLinuxPulseAudio(bool isInput)
 
 //*******************************************************************************
 int RtAudioInterface::RtAudioCallback(void* outputBuffer, void* inputBuffer,
-                                      unsigned int nFrames, double /*streamTime*/,
+                                      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) {
+        if (inputBuffer == NULL || outputBuffer == NULL) {
             return 0;
         }
-    } else if (inputBuffer_sample == NULL && outputBuffer_sample == NULL) {
+    } else if (inputBuffer == NULL && outputBuffer == 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);
+#ifdef HAVE_LIBSAMPLERATE
+    uint32_t bufferSize = getBufferSizeInSamples();
+    if (inputBuffer != NULL && getSampleRate() != mInSampleRate) {
+        int framesAvailable = mInSrcPtr->push(inputBuffer, nframes);
+        if (framesAvailable < 0) {
+            std::cerr << "RtAudioInterface sample rate conversion error" << std::endl;
+            return -1;
         }
-        if (in_chans_num == 2 && mInBuffer.size() == in_chans_num
-            && mInputMixMode == AudioInterface::MIXTOMONO) {
-            mStereoToMonoMixerPtr->compute(nFrames, mInBuffer.data(), mInBuffer.data());
+        while (static_cast<uint32_t>(framesAvailable) >= bufferSize) {
+            prepareInputBuffer(mInSrcPtr->pop(bufferSize), bufferSize);
+            AudioInterface::audioInputCallback(mInBuffer, bufferSize);
+            framesAvailable -= bufferSize;
         }
-        AudioInterface::audioInputCallback(mInBuffer, nFrames);
+        inputBuffer = NULL;
+    }
+    if (outputBuffer != NULL && getSampleRate() != mOutSampleRate) {
+        prepareOutputBuffer(mOutTmpPtr.get(), bufferSize);
+        int framesAvailable = mOutSrcPtr->getFramesAvailable();
+        while (static_cast<unsigned int>(framesAvailable) < nframes) {
+            AudioInterface::audioOutputCallback(mOutBuffer, bufferSize);
+            framesAvailable = mOutSrcPtr->push(mOutTmpPtr.get(), bufferSize);
+            if (framesAvailable < 0) {
+                std::cerr << "RtAudioInterface sample rate conversion error" << std::endl;
+                return -1;
+            }
+        }
+        memcpy(outputBuffer, mOutSrcPtr->pop(nframes),
+               nframes * mOutBuffer.size() * sizeof(float));
+        outputBuffer = NULL;
     }
+#endif
 
-    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);
+    // process input before output to minimize monitor latency on duplex devices
+    if (inputBuffer != NULL) {
+        prepareInputBuffer(static_cast<sample_t*>(inputBuffer), nframes);
+        AudioInterface::audioInputCallback(mInBuffer, nframes);
+    }
+
+    if (outputBuffer != NULL) {
+        prepareOutputBuffer(static_cast<sample_t*>(outputBuffer), nframes);
+        AudioInterface::audioOutputCallback(mOutBuffer, nframes);
     }
 
     return 0;
 }
 
+//*******************************************************************************
+void RtAudioInterface::prepareInputBuffer(sample_t* ptr, unsigned int nframes)
+{
+    for (int i = 0; i < mInBuffer.size(); i++) {
+        // Input Ports are READ ONLY
+        mInBuffer[i] = ptr + (nframes * i);
+    }
+    if (getNumInputChannels() == 2 && mInBuffer.size() == getNumInputChannels()
+        && mInputMixMode == AudioInterface::MIXTOMONO) {
+        mStereoToMonoMixerPtr->compute(nframes, mInBuffer.data(), mInBuffer.data());
+    }
+}
+
+//*******************************************************************************
+void RtAudioInterface::prepareOutputBuffer(sample_t* ptr, unsigned int nframes)
+{
+    for (int i = 0; i < mOutBuffer.size(); i++) {
+        // Output Ports are WRITABLE
+        mOutBuffer[i] = ptr + (nframes * i);
+    }
+}
+
 //*******************************************************************************
 int RtAudioInterface::wrapperRtAudioCallback(void* outputBuffer, void* inputBuffer,
                                              unsigned int nFrames, double streamTime,
index b58125c1c7858018501e74265639925dfeac502d..1207eb66609413449fe821fd9126835fabd3a77e 100644 (file)
 #include "AudioInterface.h"
 #include "StereoToMono.h"
 #include "jacktrip_globals.h"
-class JackTrip;  // Forward declaration
+
+// Forward declarations
+class JackTrip;
+class SampleRateConverter;
 
 /// \brief Simple Class that represents an audio interface available via RtAudio
 class RtAudioDevice : public RtAudio::DeviceInfo
@@ -64,7 +67,9 @@ class RtAudioDevice : public RtAudio::DeviceInfo
     RtAudio::Api api;
     void print() const;
     void printVerbose() const;
+    bool isAirpods() const;
     bool checkSampleRate(unsigned int srate) const;
+    unsigned int getClosestSampleRate(unsigned int srate) const;
     RtAudioDevice& operator=(const RtAudio::DeviceInfo& info);
 };
 
@@ -125,6 +130,8 @@ class RtAudioInterface : public AudioInterface
    private:
     int RtAudioCallback(void* outputBuffer, void* inputBuffer, unsigned int nFrames,
                         double streamTime, RtAudioStreamStatus status);
+    void prepareInputBuffer(sample_t* ptr, unsigned int nframes);
+    void prepareOutputBuffer(sample_t* ptr, unsigned int nframes);
     static int wrapperRtAudioCallback(void* outputBuffer, void* inputBuffer,
                                       unsigned int nFrames, double streamTime,
                                       RtAudioStreamStatus status, void* userData);
@@ -155,6 +162,11 @@ class RtAudioInterface : public AudioInterface
     bool mDuplexMode;  ///< true if using duplex stream mode (input device == output
                        ///< device)
     QScopedPointer<StereoToMono> mStereoToMonoMixerPtr;
+    QScopedPointer<SampleRateConverter> mInSrcPtr;
+    QScopedPointer<SampleRateConverter> mOutSrcPtr;
+    QScopedPointer<float> mOutTmpPtr;
+    uint32_t mInSampleRate;   ///< Actual sampling rate for input device
+    uint32_t mOutSampleRate;  ///< Actual sampling rate for output device
 };
 
 #endif  // __RTAUDIOINTERFACE_H__
diff --git a/src/SampleRateConverter.cpp b/src/SampleRateConverter.cpp
new file mode 100644 (file)
index 0000000..f784c74
--- /dev/null
@@ -0,0 +1,141 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2025 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 SampleRateConverter.cpp
+ * \author Mike Dickey
+ * \date January 2025
+ */
+
+#include "SampleRateConverter.h"
+
+#include <cstring>
+#include <stdexcept>
+#include <string>
+
+using namespace std;
+
+constexpr int BufferSizeMultiple = 3;
+constexpr int BytesPerSample     = sizeof(float);
+
+//*******************************************************************************
+SampleRateConverter::SampleRateConverter(unsigned int inRate, unsigned int outRate,
+                                         unsigned int numChans, unsigned int bufferSize)
+    : mBytesPerFrame(numChans * BytesPerSample)
+    , mNumChannels(numChans)
+    , mBufferSize(bufferSize)
+{
+#ifdef HAVE_LIBSAMPLERATE
+    int srcErr;
+    mStatePtr = src_new(SRC_SINC_BEST_QUALITY, numChans, &srcErr);
+    if (mStatePtr == nullptr) {
+        string errorMsg("Failed to prepare sample rate converter: ");
+        errorMsg += src_strerror(srcErr);
+        throw runtime_error(errorMsg);
+    }
+    mInDataPtr          = new char[BufferSizeMultiple * mBufferSize * mBytesPerFrame];
+    mOutDataPtr         = new char[BufferSizeMultiple * mBufferSize * mBytesPerFrame];
+    mOutPopPtr          = new float[mBufferSize * mNumChannels];
+    mData.data_in       = reinterpret_cast<float*>(mInDataPtr);
+    mData.src_ratio     = static_cast<double>(outRate) / inRate;
+    mData.end_of_input  = 0;
+    mInFramesLeftover   = 0;
+    mOutFramesAvailable = 0;
+#else
+    throw runtime_error("JackTrip was not built with support for sample rate conversion");
+#endif
+}
+
+//*******************************************************************************
+SampleRateConverter::~SampleRateConverter()
+{
+#ifdef HAVE_LIBSAMPLERATE
+    if (mStatePtr != nullptr) {
+        src_delete(mStatePtr);
+    }
+    delete[] mInDataPtr;
+    delete[] mOutDataPtr;
+    delete[] mOutPopPtr;
+#endif
+}
+
+//*******************************************************************************
+int SampleRateConverter::push(void* inPtr, unsigned int nframes)
+{
+#ifdef HAVE_LIBSAMPLERATE
+    char* framePtr      = mOutDataPtr + (mOutFramesAvailable * mBytesPerFrame);
+    mData.data_out      = reinterpret_cast<float*>(framePtr);
+    mData.output_frames = (BufferSizeMultiple * mBufferSize) - mOutFramesAvailable;
+    mData.input_frames  = nframes + mInFramesLeftover;
+    // interleave input
+    float* fromPtr = reinterpret_cast<float*>(inPtr);
+    float* toPtr =
+        reinterpret_cast<float*>(mInDataPtr + (mInFramesLeftover * mBytesPerFrame));
+    for (unsigned int i = 0; i < nframes; ++i) {
+        for (unsigned int c = 0; c < mNumChannels; ++c) {
+            *(toPtr++) = fromPtr[i + (c * nframes)];
+        }
+    }
+    if (src_process(mStatePtr, &mData) != 0)
+        return -1;
+    mInFramesLeftover = mData.input_frames - mData.input_frames_used;
+    if (mInFramesLeftover > 0 && mData.input_frames_used > 0) {
+        memmove(mInDataPtr, mInDataPtr + (mData.input_frames_used * mBytesPerFrame),
+                mInFramesLeftover * mBytesPerFrame);
+    }
+    mOutFramesAvailable += mData.output_frames_gen;
+#endif
+    return mOutFramesAvailable;
+}
+
+//*******************************************************************************
+float* SampleRateConverter::pop(unsigned int nframes)
+{
+#ifdef HAVE_LIBSAMPLERATE
+    // de-interleave output
+    float* fromPtr = reinterpret_cast<float*>(mOutDataPtr);
+    float* toPtr   = mOutPopPtr;
+    for (unsigned int c = 0; c < mNumChannels; ++c) {
+        for (unsigned int i = 0; i < nframes; ++i) {
+            *(toPtr++) = fromPtr[c + (i * mNumChannels)];
+        }
+    }
+    // pop frames
+    const unsigned int remainingFrames = mOutFramesAvailable - nframes;
+    if (remainingFrames > 0) {
+        memmove(mOutDataPtr, mOutDataPtr + (nframes * mBytesPerFrame),
+                (remainingFrames * mBytesPerFrame));
+    }
+    mOutFramesAvailable = remainingFrames;
+    return mOutPopPtr;
+#else
+    return nullptr;
+#endif
+}
diff --git a/src/SampleRateConverter.h b/src/SampleRateConverter.h
new file mode 100644 (file)
index 0000000..8f1c1d7
--- /dev/null
@@ -0,0 +1,78 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2025 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 SampleRateConverter.h
+ * \author Mike Dickey
+ * \date January 2025
+ */
+
+#ifndef __SAMPLERATECONVERTER_H__
+#define __SAMPLERATECONVERTER_H__
+
+#ifdef HAVE_LIBSAMPLERATE
+#include "samplerate.h"
+#endif
+
+/// \brief Trivial wrapper for sample rate conversion
+class SampleRateConverter
+{
+   public:
+    SampleRateConverter(unsigned int inRate, unsigned int outRate, unsigned int numChans,
+                        unsigned int bufferSize);
+    ~SampleRateConverter();
+
+    /// processes nframes non-interleaved samples from inPtr and returns
+    /// the number of output samples available, or -1 if there is an error
+    int push(void* inPtr, unsigned int nframes);
+
+    /// pops next block of nframes and returns a pointer to
+    /// non-interleaved buffer
+    float* pop(unsigned int nframes);
+
+    /// \brief returns the number of converted samples that are ready
+    inline int getFramesAvailable() const { return mOutFramesAvailable; }
+
+#ifdef HAVE_LIBSAMPLERATE
+   private:
+    SRC_DATA mData;
+    SRC_STATE* mStatePtr = nullptr;
+    char* mInDataPtr     = nullptr;
+    char* mOutDataPtr    = nullptr;
+    float* mOutPopPtr    = nullptr;
+#endif
+    unsigned int mInFramesLeftover   = 0;
+    unsigned int mOutFramesAvailable = 0;
+    unsigned int mBytesPerFrame      = 0;
+    unsigned int mNumChannels        = 0;
+    unsigned int mBufferSize         = 0;
+};
+
+#endif  // __SAMPLERATECONVERTER_H__
index 28ffb1364490d6b0fa3f11748bf51be977028842..9ee0a4332b5bf1e82e24d3ab977211845305bb23 100644 (file)
@@ -981,19 +981,56 @@ void Settings::setDevicesByString(std::string nameArg)
 {
     size_t commaPos;
     char delim = ',';
-    if (std::count(nameArg.begin(), nameArg.end(), delim) > 1) {
+
+    // Some audio device names contain commas. Allow these to be escaped with a backslash.
+    int delimCount      = std::count(nameArg.begin(), nameArg.end(), delim);
+    std::string escaped = "\\,";
+    std::vector<size_t> escapedPositions;
+
+    size_t position = nameArg.find(escaped, 0);
+    while (position != std::string::npos) {
+        // Store our comma locations for future reference.
+        escapedPositions.push_back(position + 1);
+        cout << position + 1 << endl;
+        position = nameArg.find(escaped, position + escaped.length());
+    }
+
+    if (delimCount - escapedPositions.size() > 1) {
         throw std::invalid_argument(
-            "Found multiple commas in the --audiodevice argument, cannot parse "
+            "Found multiple unescaped commas in the --audiodevice argument, cannot parse "
             "reliably.");
     }
-    commaPos = nameArg.rfind(delim);
+    int index = escapedPositions.size() - 1;
+    commaPos  = nameArg.rfind(delim);
+    while (commaPos > 0 && index >= 0 && commaPos == escapedPositions.at(index)) {
+        commaPos = nameArg.rfind(delim, commaPos - 1);
+        index--;
+    }
+
     if (commaPos || nameArg[0] == delim) {
         mInputDeviceName  = nameArg.substr(0, commaPos);
         mOutputDeviceName = nameArg.substr(commaPos + 1);
     } else {
         mInputDeviceName = mOutputDeviceName = nameArg;
     }
+
+    if (escapedPositions.size() > 0) {
+        // We have to get rid of instances of our escape character.
+        position = 0;
+        while ((position = mInputDeviceName.find(escaped, position))
+               != std::string::npos) {
+            mInputDeviceName.replace(position, escaped.length(), ",");
+            position++;
+        }
+        position = 0;
+        while ((position = mOutputDeviceName.find(escaped, position))
+               != std::string::npos) {
+            mOutputDeviceName.replace(position, escaped.length(), ",");
+            position++;
+        }
+    }
 }
+
 #endif
 
 //*******************************************************************************
@@ -1226,13 +1263,15 @@ JackTrip* Settings::getConfiguredJackTrip()
     std::vector<ProcessPlugin*> outgoingEffects =
         mEffects.allocateOutgoingEffects(mNumAudioInputChans - nReservedChans);
     for (auto p : outgoingEffects) {
-        jackTrip->appendProcessPluginToNetwork(p);
+        QSharedPointer<ProcessPlugin> pShared(p);
+        jackTrip->appendProcessPluginToNetwork(pShared);
     }
 
     std::vector<ProcessPlugin*> incomingEffects =
         mEffects.allocateIncomingEffects(mNumAudioOutputChans - nReservedChans);
     for (auto p : incomingEffects) {
-        jackTrip->appendProcessPluginFromNetwork(p);
+        QSharedPointer<ProcessPlugin> pShared(p);
+        jackTrip->appendProcessPluginFromNetwork(pShared);
     }
 
 #ifdef WAIR  // WAIR
diff --git a/src/SocketClient.cpp b/src/SocketClient.cpp
new file mode 100644 (file)
index 0000000..a9ef002
--- /dev/null
@@ -0,0 +1,89 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2022-2025 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 SocketClient.cpp
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date December 2024
+ */
+
+#include "SocketClient.h"
+
+#include <QDebug>
+
+SocketClient::SocketClient(QObject* parent)
+    : QObject(parent), m_socket(new QLocalSocket(this)), m_owns_socket(true)
+{
+}
+
+SocketClient::SocketClient(QSharedPointer<QLocalSocket>& s, QObject* parent)
+    : QObject(parent), m_socket(s), m_owns_socket(false)
+{
+}
+
+SocketClient::~SocketClient()
+{
+    if (isConnected() && m_owns_socket) {
+        m_socket->close();
+        m_socket->waitForDisconnected(1000);  // wait for up to 1 second
+    }
+}
+
+bool SocketClient::connect()
+{
+    if (isConnected()) {
+        return true;
+    }
+    m_socket->connectToServer(JACKTRIP_SOCKET_NAME);
+    return m_socket->waitForConnected(1000);  // wait for up to 1 second
+}
+
+void SocketClient::close()
+{
+    if (isConnected()) {
+        m_socket->close();
+        m_socket->waitForDisconnected(1000);  // wait for up to 1 second
+    }
+}
+
+bool SocketClient::sendHeader(const QString& handler)
+{
+    // sanity check
+    if (!isConnected()) {
+        return false;
+    }
+    QString headerStr = "JackTrip/1.0 ";
+    headerStr += handler;
+    headerStr += "\n";
+    QByteArray headerBytes = headerStr.toLocal8Bit();
+    qint64 writeBytes      = m_socket->write(headerBytes);
+    m_socket->waitForBytesWritten(-1);
+    return writeBytes > 0;
+}
diff --git a/src/SocketClient.h b/src/SocketClient.h
new file mode 100644 (file)
index 0000000..281d029
--- /dev/null
@@ -0,0 +1,89 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2022-2025 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 SocketClient.h
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date December 2024
+ */
+
+#ifndef __SocketClient_H__
+#define __SocketClient_H__
+
+#include <QLocalSocket>
+#include <QSharedPointer>
+
+// name of the local socket used by JackTrip
+constexpr const char* JACKTRIP_SOCKET_NAME = "JackTrip";
+
+// SocketClient lists for local socket connections from remote JackTrip processes
+class SocketClient : public QObject
+{
+    Q_OBJECT
+
+   public:
+    // default constructor
+    SocketClient(QObject* parent = nullptr);
+
+    // construct with an existing socket
+    SocketClient(QSharedPointer<QLocalSocket>& s, QObject* parent = nullptr);
+
+    // virtual destructor since it inherits from QObject
+    virtual ~SocketClient();
+
+    // return local socket connection
+    inline bool isConnected()
+    {
+        return m_socket->state() == QLocalSocket::ConnectedState;
+    }
+
+    // return local socket connection
+    inline QLocalSocket& getSocket() { return *m_socket; }
+
+    // attempts to connect to remote instance's socket server
+    // returns true if connection was successfully established
+    // returns false if connection failed
+    bool connect();
+
+    // closes the connection to remote instance's socket server
+    void close();
+
+    // send connection header with name of handler to use
+    bool sendHeader(const QString& handler);
+
+   private:
+    // used to check if there is another server already running
+    QSharedPointer<QLocalSocket> m_socket;
+
+    // true if a this owns the socket and should close on destruction
+    bool m_owns_socket = false;
+};
+
+#endif  // __SocketClient_H__
diff --git a/src/SocketServer.cpp b/src/SocketServer.cpp
new file mode 100644 (file)
index 0000000..ed36602
--- /dev/null
@@ -0,0 +1,140 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2022-2025 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 SocketServer.cpp
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date December 2024
+ */
+
+#include "SocketServer.h"
+
+#include <QDebug>
+#include <iostream>
+
+#include "SocketClient.h"
+
+using namespace std;
+
+bool SocketServer::start()
+{
+    // sanity check for repeated calls
+    if (!m_instanceServer.isNull()) {
+        m_serverStarted = true;
+        return m_serverStarted;
+    }
+
+    // attempt local socket connection to check for an existing instance
+    SocketClient c;
+    bool established = c.connect();
+
+    if (established) {
+        c.close();
+        m_serverStarted = false;
+    } else {
+        // confirmed that no other jacktrip instance is running
+        m_instanceServer.reset(new QLocalServer());
+        m_instanceServer->setSocketOptions(QLocalServer::WorldAccessOption);
+        QObject::connect(m_instanceServer.data(), &QLocalServer::newConnection, this,
+                         &SocketServer::handlePendingConnections, Qt::QueuedConnection);
+        QString serverName(JACKTRIP_SOCKET_NAME);
+        QLocalServer::removeServer(serverName);
+        m_serverStarted = m_instanceServer->listen(serverName);
+        if (m_serverStarted) {
+            cout << "Listening for local connections: "
+                 << m_instanceServer->fullServerName().toStdString() << endl;
+        } else {
+            cerr << "Error listening for local connections: "
+                 << m_instanceServer->errorString().toStdString() << endl;
+        }
+    }
+
+    // return true if a local socket server was started
+    return m_serverStarted;
+}
+
+void SocketServer::handlePendingConnections()
+{
+    while (m_instanceServer->hasPendingConnections()) {
+        QLocalSocket* connectedSocket = m_instanceServer->nextPendingConnection();
+
+        if (connectedSocket == nullptr || !connectedSocket->waitForConnected()) {
+            qDebug() << "Socket server: never received connection";
+            continue;
+        }
+
+        if (!connectedSocket->waitForReadyRead()
+            && connectedSocket->bytesAvailable() <= 0) {
+            qDebug() << "Socket server: not ready and no bytes available: "
+                     << connectedSocket->errorString();
+            continue;
+        }
+
+        if (connectedSocket->bytesAvailable() < (int)sizeof(quint16)) {
+            qDebug() << "Socket server: ready but no bytes available";
+            continue;
+        }
+
+        // first line should be in the format "JackTrip/1.0 HandlerName"
+        // where HandlerName indicates which handler should be used
+        QByteArray in(connectedSocket->readLine());
+        QString header(in);
+        if (!header.startsWith("JackTrip/1.0 ")) {
+            if (header.startsWith("JackTrip/")) {
+                cerr << "Socket server: unknown version: " << header.toStdString()
+                     << endl;
+            } else {
+                cerr << "Socket server: invalid header: " << header.toStdString() << endl;
+            }
+            continue;
+        }
+        QString handlerName(header);
+        handlerName.replace("JackTrip/1.0 ", "");
+        handlerName.replace("\n", "");
+
+        cout << "Socket server: received connection for " << handlerName.toStdString()
+             << endl;
+        connectedSocket->setParent(nullptr);
+        QSharedPointer<QLocalSocket> sharedSocket(connectedSocket);
+        handleConnection(handlerName, sharedSocket);
+    }
+}
+
+void SocketServer::handleConnection(const QString& name,
+                                    QSharedPointer<QLocalSocket>& socket)
+{
+    auto it = m_handlers.find(name);
+    if (it == m_handlers.end()) {
+        cerr << "Socket server: request for unknown handler: " << name.toStdString()
+             << endl;
+        return;
+    }
+    it.value()(socket);
+}
\ No newline at end of file
diff --git a/src/SocketServer.h b/src/SocketServer.h
new file mode 100644 (file)
index 0000000..840dafe
--- /dev/null
@@ -0,0 +1,89 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2022-2025 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 SocketServer.h
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date December 2024
+ */
+
+#ifndef __SocketServer_H__
+#define __SocketServer_H__
+
+#include <QHash>
+#include <QLocalServer>
+#include <QLocalSocket>
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <functional>
+
+// SocketHandler is used to handle a new socket connection
+typedef std::function<void(QSharedPointer<QLocalSocket>&)> SocketHandler;
+
+// SocketServer lists for local socket connections from remote JackTrip processes
+class SocketServer : public QObject
+{
+    Q_OBJECT
+
+   public:
+    // default constructor
+    SocketServer() {}
+
+    // virtual destructor since it inherits from QObject
+    virtual ~SocketServer() {}
+
+    // sets handler for local socket connections
+    void addHandler(QString name, SocketHandler f) { m_handlers[name] = f; }
+
+    // attempts to start the local socket server
+    // returns true if it started successfully
+    // returns false if already running in another JackTrip process
+    bool start();
+
+   private slots:
+
+    // called by local socket server to handle requests
+    void handlePendingConnections();
+
+   private:
+    // called by local socket server to handle requests
+    void handleConnection(const QString& name, QSharedPointer<QLocalSocket>& socket);
+
+    // used to listen for requests via local socket connections
+    QScopedPointer<QLocalServer> m_instanceServer;
+
+    // used to handle requests
+    QHash<QString, SocketHandler> m_handlers;
+
+    // true if a local socket server was started, false if remote was detected
+    bool m_serverStarted = false;
+};
+
+#endif  // __SocketServer_H__
index fa99c333ec866a7c737e479970225eb09af068f2..861ed4bc00f56ab8195da47d1b6159295b9a3605 100644 (file)
@@ -60,7 +60,7 @@ void StereoToMono::init(int samplingRate, int bufferSize)
 {
     ProcessPlugin::init(samplingRate, bufferSize);
 
-    fs = float(fSamplingFreq);
+    fs = float(mSampleRate);
     static_cast<stereotomonodsp*>(stereoToMonoP)->init(fs);
 
     inited = true;
index b003ebeff1f5554b6d64e2c8814bc843453a96bf..fa6af19231f0e6ea3537d1f971ce98ea2b437266 100644 (file)
@@ -71,7 +71,7 @@ Tone::~Tone()
 void Tone::init(int samplingRate, int bufferSize)
 {
     ProcessPlugin::init(samplingRate, bufferSize);
-    fs = float(fSamplingFreq);
+    fs = float(mSampleRate);
 
     for (int i = 0; i < mNumChannels; i++) {
         static_cast<tonedsp*>(toneP[i])->init(
index ecd588fbde15daa393e148e79103e5a91ed938bf..1d602dc9fda1f79bc2b1bca2a43214a72c723f68 100644 (file)
@@ -220,6 +220,8 @@ void UdpHubListener::start()
         mAuth.reset(new Auth(mCredsFile, true));
     }
 
+    startOscServer();
+
     cout << "JackTrip HUB SERVER: Waiting for client connections..." << endl;
     cout << "JackTrip HUB SERVER: Hub auto audio patch setting = " << mHubPatch << " ("
          << mHubPatchDescriptions.at(mHubPatch).toStdString() << ")" << endl;
@@ -364,6 +366,19 @@ void UdpHubListener::stopCheck()
     }
 }
 
+void UdpHubListener::queueBufferChanged(int queueBufferSize)
+{
+    cout << "Updating queueBuffer to " << queueBufferSize << endl;
+    QMutexLocker lock(&mMutex);
+    mBufferQueueLength = queueBufferSize;
+    // Now that we have our actual port, remove any duplicate workers.
+    for (int i = 0; i < gMaxThreads; i++) {
+        if (mJTWorkers->at(i) != nullptr) {
+            mJTWorkers->at(i)->setBufferQueueLength(mBufferQueueLength);
+        }
+    }
+}
+
 //*******************************************************************************
 // Returns 0 on error
 int UdpHubListener::readClientUdpPort(QSslSocket* clientConnection, QString& clientName)
index a810025615c09cd39bdd924c89212adf3f620198..1570180f8007691c7026196e37d14789e1cba85f 100644 (file)
@@ -54,6 +54,7 @@
 #include "Patcher.h"
 #endif
 #include "Auth.h"
+#include "OscServer.h"
 #include "SslServer.h"
 
 class JackTripWorker;  // forward declaration
@@ -109,6 +110,7 @@ class UdpHubListener : public QObject
     }
     void receivedNewConnection();
     void stopCheck();
+    void queueBufferChanged(int queueBufferSize);
 
    signals:
     void signalStarted();
@@ -129,6 +131,16 @@ class UdpHubListener : public QObject
     int checkAuthAndReadPort(QSslSocket* clientConnection, QString& clientName);
     int sendUdpPort(QSslSocket* clientConnection, qint32 udp_port);
 
+    void startOscServer()
+    {
+        // start osc server to listen to config updates
+        mOscServer = new OscServer(mServerPort, this);
+        mOscServer->start();
+
+        QObject::connect(mOscServer, &OscServer::signalQueueBufferChanged, this,
+                         &UdpHubListener::queueBufferChanged, Qt::QueuedConnection);
+    };
+
     /**
      * \brief Send the JackTripWorker to the thread pool. This will run
      * until it's done. We still have control over the prototype class.
@@ -156,6 +168,8 @@ class UdpHubListener : public QObject
 
     // JackTripWorker* mJTWorker; ///< Class that will be used as prototype
     QVector<JackTripWorker*>* mJTWorkers;  ///< Vector of JackTripWorkers
+    // Pointer to OscServer
+    OscServer* mOscServer;
 
     SslServer mTcpServer;
     int mServerPort;     //< Server known port number
index 1690b079af78014cf288027897f5d5a67d148e40..7707bc1450a7d2abcfc5488d054eb5960d6c47d9 100644 (file)
@@ -70,7 +70,7 @@ Volume::~Volume()
 void Volume::init(int samplingRate, int bufferSize)
 {
     ProcessPlugin::init(samplingRate, bufferSize);
-    fs = float(fSamplingFreq);
+    fs = float(mSampleRate);
 
     for (int i = 0; i < mNumChannels; i++) {
         static_cast<volumedsp*>(volumeP[i])
index bfe7afe523dcb54125024aa644245e337c3cc9ae..c2357793468ddab7374354c70e2d09e433a733e5 100644 (file)
@@ -87,7 +87,7 @@ About::About(QWidget* parent) : QDialog(parent), m_ui(new Ui::About)
         m_ui->aboutLabel->text().replace(QLatin1String("%BUILD%"), buildString));
 
 #ifdef __APPLE__
-    m_ui->aboutImage->setPixmap(QPixmap(":/images/icon_256.png"));
+    m_ui->aboutImage->setPixmap(QPixmap(":/images/icon_128@2x.png"));
 #endif
 
     aboutText.setHtml(m_ui->aboutLabel->text());
index e30f1a628b5f385c8255376ae2859f886713a8e2..fa992b29773744cf6be0358675789d8dc52cccd1 100644 (file)
@@ -148,7 +148,7 @@ border: 4px solid black; </string>
   </layout>
  </widget>
  <resources>
-  <include location="qjacktrip.qrc"/>
+  <include location="../images/images.qrc"/>
  </resources>
  <connections/>
 </ui>
index f7a91ecd7e73d95185d615e1c50f7130f079ff5b..aec05463dca6efa81a082da2511c155c5f53ef99 100644 (file)
@@ -497,7 +497,11 @@ void QJackTrip::processFinished()
 
     if (m_ui->disconnectScriptCheckBox->isChecked()) {
         QStringList arguments = m_ui->disconnectScriptEdit->text().split(
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
             QStringLiteral(" "), Qt::SkipEmptyParts);
+#else
+            QStringLiteral(" "), QString::SkipEmptyParts);
+#endif
         if (!arguments.isEmpty()) {
             QProcess disconnectScript;
             disconnectScript.setProgram(arguments.takeFirst());
@@ -547,7 +551,12 @@ void QJackTrip::receivedConnectionFromPeer()
     m_assignedClientName = m_jackTrip->getAssignedClientName();
     if (m_ui->connectScriptCheckBox->isChecked()) {
         QStringList arguments = m_ui->connectScriptEdit->text().split(QStringLiteral(" "),
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
                                                                       Qt::SkipEmptyParts);
+#else
+                                                                      QString::
+                                                                          SkipEmptyParts);
+#endif
         if (!arguments.isEmpty()) {
             QProcess connectScript;
             connectScript.setProgram(arguments.takeFirst());
@@ -1631,53 +1640,61 @@ void QJackTrip::appendPlugins(JackTrip* jackTrip, int numSendChannels,
     // 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(
+        QSharedPointer<ProcessPlugin> pluginPtr(
             new Compressor(numSendChannels, false, CompressorPresets::voice));
+        jackTrip->appendProcessPluginToNetwork(pluginPtr);
     }
     if (m_ui->inCompressorCheckBox->isChecked()) {
-        jackTrip->appendProcessPluginFromNetwork(
+        QSharedPointer<ProcessPlugin> pluginPtr(
             new Compressor(numRecvChannels, false, CompressorPresets::voice));
+        jackTrip->appendProcessPluginFromNetwork(pluginPtr);
     }
 
     if (m_ui->outZitarevCheckBox->isChecked()) {
         qreal wetness = m_ui->outZitarevWetnessSlider->value() / 100.0;
-        jackTrip->appendProcessPluginToNetwork(
+        QSharedPointer<ProcessPlugin> pluginPtr(
             new Reverb(numSendChannels, numSendChannels, 1.0 + wetness));
+        jackTrip->appendProcessPluginToNetwork(pluginPtr);
     }
     if (m_ui->inZitarevCheckBox->isChecked()) {
         qreal wetness = m_ui->inZitarevWetnessSlider->value() / 100.0;
-        jackTrip->appendProcessPluginFromNetwork(
+        QSharedPointer<ProcessPlugin> pluginPtr(
             new Reverb(numRecvChannels, numRecvChannels, 1.0 + wetness));
+        jackTrip->appendProcessPluginFromNetwork(pluginPtr);
     }
 
     if (m_ui->outFreeverbCheckBox->isChecked()) {
         qreal wetness = m_ui->outFreeverbWetnessSlider->value() / 100.0;
-        jackTrip->appendProcessPluginToNetwork(
+        QSharedPointer<ProcessPlugin> pluginPtr(
             new Reverb(numSendChannels, numSendChannels, wetness));
+        jackTrip->appendProcessPluginToNetwork(pluginPtr);
     }
     if (m_ui->inFreeverbCheckBox->isChecked()) {
         qreal wetness = m_ui->inFreeverbWetnessSlider->value() / 100.0;
-        jackTrip->appendProcessPluginFromNetwork(
+        QSharedPointer<ProcessPlugin> pluginPtr(
             new Reverb(numRecvChannels, numRecvChannels, wetness));
+        jackTrip->appendProcessPluginFromNetwork(pluginPtr);
     }
 
     // Limiters go last in the plugin sequence.
     if (m_ui->outLimiterCheckBox->isChecked()) {
-        jackTrip->appendProcessPluginToNetwork(
+        QSharedPointer<ProcessPlugin> pluginPtr(
             new Limiter(numSendChannels, m_ui->outClientsSpinBox->value()));
+        jackTrip->appendProcessPluginToNetwork(pluginPtr);
     }
     if (m_ui->inLimiterCheckBox->isChecked()) {
-        jackTrip->appendProcessPluginFromNetwork(new Limiter(numRecvChannels, 1));
+        QSharedPointer<ProcessPlugin> pluginPtr(new Limiter(numRecvChannels, 1));
+        jackTrip->appendProcessPluginFromNetwork(pluginPtr);
     }
 }
 
 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);
+    QSharedPointer<ProcessPlugin> inputMeterPtr(new Meter(inputChannels));
+    QSharedPointer<ProcessPlugin> outputMeterPtr(new Meter(outputChannels));
+    m_jackTrip->appendProcessPluginToNetwork(inputMeterPtr);
+    m_jackTrip->appendProcessPluginFromNetwork(outputMeterPtr);
 
     // Create our widgets.
     for (quint32 i = 0; i < inputChannels; i++) {
@@ -1703,9 +1720,11 @@ void QJackTrip::createMeters(quint32 inputChannels, quint32 outputChannels)
     }
     m_outputLayout->setRowStretch(outputChannels, 100);
 
-    QObject::connect(inputMeter, &Meter::onComputedVolumeMeasurements, this,
+    QObject::connect(static_cast<Meter*>(inputMeterPtr.get()),
+                     &Meter::onComputedVolumeMeasurements, this,
                      &QJackTrip::updatedInputMeasurements);
-    QObject::connect(outputMeter, &Meter::onComputedVolumeMeasurements, this,
+    QObject::connect(static_cast<Meter*>(outputMeterPtr.get()),
+                     &Meter::onComputedVolumeMeasurements, this,
                      &QJackTrip::updatedOutputMeasurements);
 }
 
@@ -1956,8 +1975,12 @@ QString QJackTrip::commandLineFromCurrentOptions()
         if (m_ui->outputDeviceComboBox->currentIndex() > 0) {
             outDevice = m_ui->outputDeviceComboBox->currentText();
         }
-        commandLine.append(
-            QStringLiteral(" --audiodevice \"%1\",\"%2\"").arg(inDevice, outDevice));
+        QString inDeviceEscaped =
+            QString(inDevice).replace(QStringLiteral(","), QStringLiteral("\\,"));
+        QString outDeviceEscaped =
+            QString(outDevice).replace(QStringLiteral(","), QStringLiteral("\\,"));
+        commandLine.append(QStringLiteral(" --audiodevice \"%1\",\"%2\"")
+                               .arg(inDeviceEscaped, outDeviceEscaped));
     }
 #endif
 
index 0c537cc4318671bd2f82b2e03fee96aee3f43888..995d80970af9aeee04dac8f5823a84e2dd49790f 100644 (file)
@@ -2000,7 +2000,7 @@ To connect to a hub server you need to run as a hub client.</string>
   <tabstop>disconnectScriptBrowse</tabstop>
  </tabstops>
  <resources>
-  <include location="qjacktrip.qrc"/>
+  <include location="../images/images.qrc"/>
  </resources>
  <connections/>
 </ui>
index 12f71298dd6ec48e26f61d0744d9eb0d41df55aa..90fc3dc298728419fdab72ec81646f7baca2e163 100644 (file)
@@ -1,6 +1,7 @@
 <RCC>
   <qresource prefix="images">
     <file>icon_256.png</file>
+    <file alias="icon_128@2x.png">icon_256.png</file>
     <file>icon_128.png</file>
     <file>icon_32.png</file>
   </qresource>
index daeed1637344806c9e6c8cd20f5f8b5edd53cb05..c2ed2335dff96d35f81c13697301a8e12f41e9c5 100644 (file)
@@ -38,9 +38,9 @@
 #ifndef __JACKTRIP_GLOBALS_H__
 #define __JACKTRIP_GLOBALS_H__
 
-#include "AudioInterface.h"
+#include "jacktrip_types.h"
 
-constexpr const char* const gVersion = "2.4.1";  ///< JackTrip version
+constexpr const char* const gVersion = "2.5.0";  ///< JackTrip version
 
 //*******************************************************************************
 /// \name Default Values
@@ -75,10 +75,6 @@ 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;
@@ -87,7 +83,7 @@ constexpr uint32_t gDefaultBufferSizeInSamples = 128;
 constexpr const char* gDefaultLocalAddress     = "";
 constexpr int gDefaultRedundancy               = 1;
 constexpr int gTimeOutMultiThreadedServer      = 10000;  // seconds
-constexpr int gUdpWaitTimeout                  = 50;     // milliseconds
+constexpr int gUdpWaitTimeout                  = 512;    // milliseconds
 //@}
 
 //*******************************************************************************
index 0b56065f996f982e3de4de59065ecb0a5aa54209..a02a8de3f393dce9e2e1885aab8f855069a2b0f0 100644 (file)
@@ -793,11 +793,39 @@ Rectangle {
                 id: scanningDevicesLabel
                 x: 0; y: 0
                 width: parent.width - (16 * virtualstudio.uiScale)
-                text: "Scanning audio devices..."
+                text: (Qt.platform.os == "osx" && permissions.micPermission != "granted") ? "Microphone permissions not permitted" : "Scanning audio devices..."
                 font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
                 wrapMode: Text.WordWrap
                 color: textColour
             }
+
+            Button {
+                id: openSettingsButton
+                visible: Qt.platform.os == "osx" && permissions.micPermission != "granted"
+                background: Rectangle {
+                    radius: 6 * virtualstudio.uiScale
+                    color: openSettingsButton.down ? buttonPressedColour : buttonColour
+                    border.width: 1
+                    border.color: openSettingsButton.down || openSettingsButton.hovered ? buttonPressedStroke : buttonStroke
+                    layer.enabled: openSettingsButton.hovered && !openSettingsButton.down
+                }
+                onClicked: {
+                    permissions.openSystemPrivacy();
+                }
+                anchors.top: scanningDevicesLabel.bottom
+                anchors.topMargin: 16 * virtualstudio.uiScale
+                anchors.horizontalCenter: parent.horizontalCenter
+                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: textColour
+                    anchors.horizontalCenter: parent.horizontalCenter
+                    anchors.verticalCenter: parent.verticalCenter
+                }
+            }
         }
     }
 }
index 1a8dcfedf7b2ecffd6364bdd651b7aa6d4912059..dcff43487be15f032013540b97d2f76f1b1f8f7f 100644 (file)
@@ -25,6 +25,12 @@ Rectangle {
     property string shadowColour: virtualstudio.darkMode ? "#40000000" : "#80A1A1A1"
     property string toolTipBackgroundColour: virtualstudio.darkMode ? "#323232" : "#F3F3F3"
     property string toolTipTextColour: textColour
+    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 checkboxStroke: "#0062cc"
+    property string checkboxPressedStroke: "#007AFF"
 
     property string browserButtonColour: virtualstudio.darkMode ? "#494646" : "#EAECEC"
     property string browserButtonHoverColour: virtualstudio.darkMode ? "#5B5858" : "#D3D4D4"
@@ -35,6 +41,13 @@ Rectangle {
 
     property string linkText: virtualstudio.darkMode ? "#8B8D8D" : "#272525"
 
+    function getQueueBufferString () {
+        if (virtualstudio.queueBuffer == 0) {
+            return "auto";
+        }
+        return virtualstudio.queueBuffer + " ms";
+    }
+
     MouseArea {
         anchors.fill: parent
         propagateComposedEvents: false
@@ -43,7 +56,6 @@ Rectangle {
     Rectangle {
         id: audioSettingsView
         width: parent.width;
-        height: parent.height;
         color: backgroundColour
         radius: 6 * virtualstudio.uiScale
 
@@ -78,6 +90,149 @@ Rectangle {
             anchors.top: refreshButton.bottom;
             anchors.topMargin: 16 * virtualstudio.uiScale;
         }
+
+        Rectangle {
+            id: latencyDivider
+            anchors.top: audioSettings.bottom
+            anchors.topMargin: 54 * virtualstudio.uiScale
+            x: 24 * virtualstudio.uiScale
+            width: parent.width - x - (24 * virtualstudio.uiScale);
+            height: 2 * virtualstudio.uiScale
+            color: "#E0E0E0"
+        }
+
+        Text {
+            id: queueBufferLabel
+            anchors.top: latencyDivider.bottom
+            anchors.topMargin: 16 * virtualstudio.uiScale
+            x: 24 * virtualstudio.uiScale
+            text: "Audio Quality"
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        InfoTooltip {
+            id: queueBufferTooltip
+            content: "JackTrip analyzes your Internet connection to find the best balance between audio latency and quality. Add additional latency to further improve quality."
+            size: 16 * virtualstudio.uiScale
+            anchors.left: queueBufferLabel.right
+            anchors.bottom: queueBufferLabel.top
+            anchors.bottomMargin: -8 * virtualstudio.uiScale
+        }
+
+        AppIcon {
+            id: balanceIcon
+            anchors.left: queueBufferLabel.left
+            anchors.top: queueBufferLabel.bottom
+            width: 32 * virtualstudio.uiScale
+            height: 32 * virtualstudio.uiScale
+            icon.source: "balance.svg"
+        }
+
+        CheckBox {
+            id: useStudioQueueBuffer
+            checked: virtualstudio.useStudioQueueBuffer
+            text: qsTr("Use Studio settings")
+            anchors.top: latencyDivider.bottom
+            anchors.topMargin: 16 * virtualstudio.uiScale
+            x: 168 * virtualstudio.uiScale;
+            onClicked: { virtualstudio.useStudioQueueBuffer = useStudioQueueBuffer.checkState == Qt.Checked; }
+            indicator: Rectangle {
+                implicitWidth: 16 * virtualstudio.uiScale
+                implicitHeight: 16 * virtualstudio.uiScale
+                x: useStudioQueueBuffer.leftPadding
+                y: parent.height / 2 - height / 2
+                radius: 3 * virtualstudio.uiScale
+                border.color: useStudioQueueBuffer.down || useStudioQueueBuffer.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: useStudioQueueBuffer.down || useStudioQueueBuffer.hovered ? checkboxPressedStroke : checkboxStroke
+                    visible: useStudioQueueBuffer.checked
+                }
+            }
+            contentItem: Text {
+                text: useStudioQueueBuffer.text
+                font.family: "Poppins"
+                font.pixelSize: 10 * virtualstudio.fontScale * virtualstudio.uiScale
+                anchors.horizontalCenter: parent.horizontalCenter
+                anchors.verticalCenter: parent.verticalCenter
+                leftPadding: useStudioQueueBuffer.indicator.width + useStudioQueueBuffer.spacing
+                color: textColour
+            }
+        }
+
+        Text {
+            id: currentLatency
+            anchors.top: latencyDivider.bottom
+            anchors.topMargin: 16 * virtualstudio.uiScale
+            anchors.right: parent.right
+            anchors.rightMargin: 24 * virtualstudio.uiScale
+            text: "Buffer Latency: " + Math.round(virtualstudio.networkStats.recvLatency) + " ms"
+            font { family: "Poppins"; pixelSize: fontSmall * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+        }
+
+        Slider {
+            id: queueBufferSlider
+            value: virtualstudio.queueBuffer
+            onMoved: {
+                virtualstudio.queueBuffer = value;
+            }
+            from: 0
+            to: 250
+            stepSize: 1
+            padding: 0
+            visible: useStudioQueueBuffer.checkState != Qt.Checked
+
+            anchors.top: useStudioQueueBuffer.bottom
+            anchors.topMargin: 16 * virtualstudio.uiScale
+            x: queueBufferText.x + queueBufferText.width
+            width: parent.width - x - (16 * virtualstudio.uiScale) - queueBufferText.width;
+
+            background: Rectangle {
+                x: queueBufferSlider.leftPadding
+                y: queueBufferSlider.topPadding + queueBufferSlider.availableHeight / 2 - height / 2
+                implicitWidth: parent.width
+                implicitHeight: 6
+                width: queueBufferSlider.availableWidth
+                height: implicitHeight
+                radius: 4
+                color: sliderTrackColour
+
+                Rectangle {
+                    width: queueBufferSlider.visualPosition * parent.width
+                    height: parent.height
+                    color: sliderActiveTrackColour
+                    radius: 4
+                }
+            }
+
+            handle: Rectangle {
+                x: queueBufferSlider.leftPadding + queueBufferSlider.visualPosition * (queueBufferSlider.availableWidth - width)
+                y: queueBufferSlider.topPadding + queueBufferSlider.availableHeight / 2 - height / 2
+                implicitWidth: 26 * virtualstudio.uiScale
+                implicitHeight: 26 * virtualstudio.uiScale
+                radius: 13 * virtualstudio.uiScale
+                color: queueBufferSlider.pressed ? sliderPressedColour : sliderColour
+                border.color: buttonStroke
+            }
+        }
+
+        Text {
+            id: queueBufferText
+            width: (64 * virtualstudio.uiScale)
+            anchors.left: useStudioQueueBuffer.left
+            anchors.verticalCenter: queueBufferSlider.verticalCenter
+            text: getQueueBufferString()
+            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
+            color: textColour
+            visible: useStudioQueueBuffer.checkState != Qt.Checked
+        }
     }
 
     Button {
index 05731f396b28bf3dba348184070bfc6f65fa9134..bab87a39a9ef4a5b0936c4a71bd597f92e7ac849 100644 (file)
@@ -417,5 +417,9 @@ Item {
         userFeedbackSurvey.serverId = serverId;
         userFeedbackModal.open();
       }
+
+      function onCloseFeedbackSurveyModal() {
+        userFeedbackModal.close();
+      }
     }
 }
index e969637d4db3a07f4eae739c322dd62caba3f89f..cd589e2aab929769e75b3aa584ea783ab5ca1c88 100644 (file)
@@ -59,13 +59,6 @@ Item {
         return idx;
     }
 
-    function getQueueBufferString () {
-        if (audio.queueBuffer == 0) {
-            return "auto";
-        }
-        return audio.queueBuffer + " ms";
-    }
-
     Rectangle {
         id: audioSettingsView
         width: 0.8 * parent.width
@@ -480,83 +473,12 @@ Item {
             color: textColour
         }
 
-        Slider {
-            id: queueBufferSlider
-            value: audio.queueBuffer
-            onMoved: {
-                audio.queueBuffer = value;
-            }
-            from: 0
-            to: 128
-            stepSize: 1
-            padding: 0
-            x: updateChannelCombo.x + queueBufferText.width
-            y: bufferCombo.y + (54 * virtualstudio.uiScale)
-            width: updateChannelCombo.width - queueBufferText.width
-
-            background: Rectangle {
-                x: queueBufferSlider.leftPadding
-                y: queueBufferSlider.topPadding + queueBufferSlider.availableHeight / 2 - height / 2
-                implicitWidth: parent.width
-                implicitHeight: 6
-                width: queueBufferSlider.availableWidth
-                height: implicitHeight
-                radius: 4
-                color: sliderTrackColour
-
-                Rectangle {
-                    width: queueBufferSlider.visualPosition * parent.width
-                    height: parent.height
-                    color: sliderActiveTrackColour
-                    radius: 4
-                }
-            }
-
-            handle: Rectangle {
-                x: queueBufferSlider.leftPadding + queueBufferSlider.visualPosition * (queueBufferSlider.availableWidth - width)
-                y: queueBufferSlider.topPadding + queueBufferSlider.availableHeight / 2 - height / 2
-                implicitWidth: 26 * virtualstudio.uiScale
-                implicitHeight: 26 * virtualstudio.uiScale
-                radius: 13 * virtualstudio.uiScale
-                color: queueBufferSlider.pressed ? sliderPressedColour : sliderColour
-                border.color: buttonStroke
-            }
-        }
-
-        Text {
-            id: queueBufferText
-            width: (64 * virtualstudio.uiScale)
-            x: updateChannelCombo.x;
-            anchors.verticalCenter: queueBufferSlider.verticalCenter
-            text: getQueueBufferString()
-            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
-            color: textColour
-        }
-
-        Text {
-            id: queueBufferLabel
-            anchors.verticalCenter: queueBufferSlider.verticalCenter
-            x: leftMargin * virtualstudio.uiScale
-            text: "Adjust Latency"
-            font { family: "Poppins"; pixelSize: fontMedium * virtualstudio.fontScale * virtualstudio.uiScale }
-            color: textColour
-        }
-
-        InfoTooltip {
-            id: tooltip
-            content: "JackTrip analyzes your Internet connection to find the best balance between audio latency and quality. Add additional latency to further improve quality."
-            size: 16
-            anchors.left: queueBufferLabel.right
-            anchors.leftMargin: 2 * virtualstudio.uiScale
-            anchors.verticalCenter: queueBufferSlider.verticalCenter
-        }
-
         CheckBox {
             id: feedbackDetection
             checked: audio.feedbackDetectionEnabled
             text: qsTr("Automatically mute when feedback is detected")
             x: updateChannelCombo.x;
-            y: queueBufferSlider.y + (48 * virtualstudio.uiScale)
+            y: bufferCombo.y + (48 * virtualstudio.uiScale)
             onClicked: { audio.feedbackDetectionEnabled = feedbackDetection.checkState == Qt.Checked; }
             indicator: Rectangle {
                 implicitWidth: 16 * virtualstudio.uiScale
index a45b459b9ea1c53408a385777473561f70a8778d..2f157641ccd8a1e4c8252142f45af7b8980ba942 100644 (file)
@@ -62,7 +62,7 @@ Item {
             settings.javascriptCanPaste: true
             settings.screenCaptureEnabled: true
             profile.httpUserAgent: `JackTrip/${virtualstudio.versionString}`
-            url: `https://${virtualstudio.apiHost}/studios/${studioId}/live?accessToken=${accessToken}`
+            url: `https://${virtualstudio.apiHost}/studios/${studioId}/live`
 
             // useful for debugging
             // onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceID) {
diff --git a/src/vs/balance.svg b/src/vs/balance.svg
new file mode 100644 (file)
index 0000000..c6ab21c
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="iso-8859-1"?>\r
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\r
+<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" \r
+        viewBox="0 0 462.587 462.587" xml:space="preserve">\r
+<g id="XMLID_12_">\r
+       <g>\r
+               <path d="M457.012,206.595c-1.509-1.656-3.647-2.608-5.888-2.608h-15.69c0,0-54.307-103.321-57.115-108.681\r
+                       c-3.012-5.747-7.57-10.778-14.387-10.778c-33.177,0-64.138-7.079-87.179-19.935c-11.496-6.414-20.164-13.917-25.46-21.803V20\r
+                       c0-11.046-8.954-20-20-20c-11.046,0-20,8.954-20,20v22.79c-5.296,7.886-13.964,15.389-25.46,21.803\r
+                       c-23.042,12.855-54.003,19.935-87.18,19.935c-6.817,0-10.923,4.181-14.387,10.778c-8.099,15.424-57.114,108.681-57.114,108.681\r
+                       H11.463c-2.242,0-4.381,0.945-5.891,2.602c-1.51,1.658-2.25,3.874-2.045,6.107c4.394,47.706,44.521,85.063,93.374,85.063\r
+                       s88.98-37.357,93.374-85.063c0.205-2.231-0.539-4.445-2.048-6.101c-1.509-1.656-3.647-2.608-5.888-2.608h-15.69l-47.389-90.282\r
+                       c36.922-2.961,69.151-13.791,92.034-29.701v328.583h-90.649c-13.807,0-25,11.193-25,25s11.193,25,25,25h217.453\r
+                       c13.807,0,25-11.193,25-25s-11.193-25-25-25h-86.804V84.004c22.883,15.91,55.112,26.741,92.033,29.701l-47.389,90.282h-15.69\r
+                       c-2.242,0-4.381,0.945-5.891,2.602c-1.51,1.658-2.25,3.874-2.044,6.107c4.393,47.706,44.521,85.063,93.374,85.063\r
+                       c48.853,0,88.98-37.357,93.374-85.063C459.266,210.465,458.521,208.252,457.012,206.595z M132.767,203.987H61.034l35.866-68.33\r
+                       L132.767,203.987z M329.819,203.987l35.867-68.331l35.867,68.331H329.819z"/>\r
+       </g>\r
+</g>\r
+</svg>
\ No newline at end of file
index 6f27a67f351a157182bd65a1f954df9018dde009..15d2e8a508f6b1b2fa83036f7ac007523c306814 100644 (file)
 // https://bugreports.qt.io/browse/QTBUG-55199
 #include <QSvgGenerator>
 
+#include "../AudioSocket.h"
 #include "../JackTrip.h"
 #include "../Settings.h"
+#include "../SocketClient.h"
+#include "../SocketServer.h"
 #include "../jacktrip_globals.h"
 #include "JTApplication.h"
 #include "WebSocketTransport.h"
@@ -130,6 +133,8 @@ VirtualStudio::VirtualStudio(UserInterface& parent)
     m_auth.reset(new VsAuth(m_networkAccessManagerPtr, m_api.data()));
     connect(m_auth.data(), &VsAuth::authSucceeded, this,
             &VirtualStudio::slotAuthSucceeded);
+    connect(m_auth.data(), &VsAuth::updatedAccessToken, this,
+            &VirtualStudio::slotAccessTokenUpdated);
     connect(m_auth.data(), &VsAuth::refreshTokenFailed, this, [=]() {
         m_auth->authenticate(QStringLiteral(""));  // retry without using refresh token
     });
@@ -174,6 +179,26 @@ VirtualStudio::VirtualStudio(UserInterface& parent)
     // Register clipboard Qml type
     qmlRegisterType<VsQmlClipboard>("VS", 1, 0, "Clipboard");
 
+    // on window focus, attempt to refresh the access token if the token is more than 1
+    // hour old
+    connect(m_view.data(), &VsQuickView::focusGained, this, [=]() {
+        QString refreshToken = m_auth->refreshToken();
+        if (refreshToken.isEmpty()) {
+            return;
+        }
+
+        qint64 maxElapsedTimeInMs      = 1000 * 60 * 60;  // 1 hour
+        QDateTime accessTokenTimestamp = m_auth->accessTokenTimestamp();
+        // only refresh after auth process completed the first time
+        if (accessTokenTimestamp.toMSecsSinceEpoch() > 0) {
+            QDateTime accessTokenDeadline = QDateTime::fromMSecsSinceEpoch(
+                accessTokenTimestamp.toMSecsSinceEpoch() + maxElapsedTimeInMs);
+            if (QDateTime::currentDateTime() > accessTokenDeadline) {
+                m_auth->refreshAccessToken(refreshToken);
+            }
+        }
+    });
+
     // setup QML view
     m_view->engine()->rootContext()->setContextProperty(QStringLiteral("virtualstudio"),
                                                         this);
@@ -213,14 +238,6 @@ VirtualStudio::VirtualStudio(UserInterface& parent)
     connect(m_view.get(), &VsQuickView::windowClose, this, &VirtualStudio::exit,
             Qt::QueuedConnection);
 
-    // prepare handler for deeplinks jacktrip://join/<StudioID>
-    m_deepLinkPtr.reset(new VsDeeplink(m_interface.getSettings().getDeeplink()));
-    if (!m_deepLinkPtr->getDeeplink().isEmpty()) {
-        bool readyForExit = m_deepLinkPtr->waitForReady();
-        if (readyForExit)
-            std::exit(0);
-    }
-
     // Log to file
     QString logPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
     QDir logDir;
@@ -236,10 +253,53 @@ VirtualStudio::VirtualStudio(UserInterface& parent)
     ts = new QTextStream(&outFile);
     qInstallMessageHandler(qtMessageHandler);
 
-    // Get ready for deep link signals
-    QObject::connect(m_deepLinkPtr.get(), &VsDeeplink::signalDeeplink, this,
+    // check if started with a deep link to handle jacktrip://join/<StudioID>
+    QString deepLinkStr = m_interface.getSettings().getDeeplink();
+    if (!deepLinkStr.isEmpty()) {
+        // started with a deep link; check if another instance is already running
+        SocketClient c;
+        if (c.connect()) {
+            // existing instance found; send deeplink to it and exit
+            if (!c.sendHeader("deeplink")) {
+                c.close();
+                std::cerr << "Failed to send deeplink header" << std::endl;
+                std::exit(1);
+            }
+            QLocalSocket& s          = c.getSocket();
+            QByteArray deepLinkBytes = deepLinkStr.toLocal8Bit();
+            qint64 bytesWritten      = s.write(deepLinkBytes);
+            s.flush();
+            s.waitForBytesWritten(1000);
+            if (bytesWritten != deepLinkBytes.size()) {
+                std::cerr << "Failed to send deeplink" << std::endl;
+                std::exit(1);
+            }
+            std::cout << "sent deeplink: " << deepLinkStr.toStdString() << std::endl;
+            std::exit(0);
+        }
+    }
+
+    // prepare handler for deep link requests
+    m_deepLinkPtr.reset(new VsDeeplink());
+    QObject::connect(m_deepLinkPtr.get(), &VsDeeplink::signalVsDeeplink, this,
                      &VirtualStudio::handleDeeplinkRequest, Qt::QueuedConnection);
-    m_deepLinkPtr->readyForSignals();
+    if (!deepLinkStr.isEmpty()) {
+        QUrl deepLinkUrl(deepLinkStr);
+        m_deepLinkPtr->handleUrl(deepLinkUrl);
+    }
+
+    // prepare handler for local socket connections
+    m_socketServerPtr.reset(new SocketServer());
+    m_socketServerPtr->addHandler("deeplink", [=](QSharedPointer<QLocalSocket>& socket) {
+        m_deepLinkPtr->handleVsDeeplinkRequest(socket);
+    });
+    m_socketServerPtr->addHandler("audio", [=](QSharedPointer<QLocalSocket>& socket) {
+        this->handleAudioSocketRequest(socket);
+    });
+    m_socketServerPtr->start();
+
+    // initialize default QtWebEngineProfile
+    m_qwebEngineProfile = QWebEngineProfile::defaultProfile();
 }
 
 void VirtualStudio::show()
@@ -308,11 +368,6 @@ int VirtualStudio::webChannelPort()
     return m_webChannelPort;
 }
 
-bool VirtualStudio::hasRefreshToken()
-{
-    return !m_refreshToken.isEmpty();
-}
-
 QString VirtualStudio::versionString()
 {
     return QLatin1String(gVersion);
@@ -396,6 +451,53 @@ bool VirtualStudio::networkOutage()
     return m_devicePtr.isNull() ? false : m_devicePtr->getNetworkOutage();
 }
 
+int VirtualStudio::getQueueBuffer() const
+{
+    return m_queueBuffer;
+}
+
+void VirtualStudio::setQueueBuffer(int queueBuffer)
+{
+    if (m_queueBuffer == queueBuffer)
+        return;
+
+    m_queueBuffer = queueBuffer;
+    emit queueBufferChanged(queueBuffer);
+    if (!m_useStudioQueueBuffer && !m_devicePtr.isNull())
+        m_devicePtr->setQueueBuffer(queueBuffer);
+
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("QueueBuffer"), m_queueBuffer);
+    settings.endGroup();
+}
+
+bool VirtualStudio::useStudioQueueBuffer()
+{
+    return m_useStudioQueueBuffer;
+}
+
+void VirtualStudio::setUseStudioQueueBuffer(bool b)
+{
+    if (m_useStudioQueueBuffer == b)
+        return;
+
+    m_useStudioQueueBuffer = b;
+    emit useStudioQueueBufferChanged(b);
+    if (!m_devicePtr.isNull()) {
+        if (!m_useStudioQueueBuffer) {
+            m_devicePtr->setQueueBuffer(m_queueBuffer);
+        } else if (m_currentStudio.id() != "") {
+            m_devicePtr->setQueueBuffer(m_currentStudio.queueBuffer());
+        }
+    }
+
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("UseStudioQueueBuffer"), m_useStudioQueueBuffer);
+    settings.endGroup();
+}
+
 QJsonObject VirtualStudio::regions()
 {
     return m_regions;
@@ -495,6 +597,9 @@ void VirtualStudio::setWindowState(QString state)
         // just to reduce risk of running into a deadlock
         emit scheduleStudioRefresh(-1, false);
     }
+    if (m_windowState != "browse") {
+        emit closeFeedbackSurveyModal();
+    }
     emit windowStateUpdated();
 }
 
@@ -537,7 +642,19 @@ void VirtualStudio::setRefreshInProgress(bool b)
 void VirtualStudio::collectFeedbackSurvey(QString serverId, int rating, QString message)
 {
     QJsonObject feedback;
+    feedback.insert(QStringLiteral("appVersion"), versionString());
 
+#if defined(Q_OS_WIN)
+    feedback.insert(QStringLiteral("platform"), "windows");
+#endif
+#if defined(Q_OS_MACOS)
+    feedback.insert(QStringLiteral("platform"), "macos");
+#endif
+#if defined(Q_OS_LINUX)
+    feedback.insert(QStringLiteral("platform"), "linux");
+#endif
+
+    feedback.insert(QStringLiteral("osVersion"), QSysInfo::prettyProductName());
     QString sysInfo = QString("[platform=%1").arg(QSysInfo::prettyProductName());
 #ifdef RT_AUDIO
     QString inputDevice =
@@ -560,6 +677,19 @@ void VirtualStudio::collectFeedbackSurvey(QString serverId, int rating, QString
         feedback.insert(QStringLiteral("message"), message + " " + sysInfo);
     }
 
+    QString deviceIssues  = "";
+    QString deviceError   = m_audioConfigPtr->getDevicesErrorMsg();
+    QString deviceWarning = m_audioConfigPtr->getDevicesWarningMsg();
+    if (!deviceError.isEmpty()) {
+        deviceIssues.append(deviceError);
+    } else if (!deviceWarning.isEmpty()) {
+        deviceIssues.append(deviceWarning);
+    }
+    if (!deviceIssues.isEmpty()) {
+        feedback.insert(QStringLiteral("deviceIssues"), deviceIssues);
+        message.append(" (deviceIssues=" + deviceIssues + ")");
+    }
+
     QJsonDocument data = QJsonDocument(feedback);
     m_api->submitServerFeedback(serverId, data.toJson());
     return;
@@ -845,7 +975,6 @@ void VirtualStudio::logout()
     m_refreshToken.clear();
     m_userMetadata = QJsonObject();
     m_userId.clear();
-    emit hasRefreshTokenChanged();
 
     // reset window state
     setWindowState(QStringLiteral("login"));
@@ -864,6 +993,9 @@ void VirtualStudio::loadSettings()
     m_testMode       = settings.value(QStringLiteral("TestMode"), false).toBool();
     m_showInactive   = settings.value(QStringLiteral("ShowInactive"), true).toBool();
     m_showSelfHosted = settings.value(QStringLiteral("ShowSelfHosted"), true).toBool();
+    m_useStudioQueueBuffer =
+        settings.value(QStringLiteral("UseStudioQueueBuffer"), true).toBool();
+    m_queueBuffer = settings.value(QStringLiteral("QueueBuffer"), 0).toInt();
 
     // use setters to emit signals for these if they change; otherwise, the
     // user interface will not revert back after cancelling settings changes
@@ -897,10 +1029,10 @@ void VirtualStudio::connectToStudio()
 
     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()));
+    m_studioSocketPtr.reset(
+        new VsWebSocket(QUrl(QStringLiteral("wss://%1/api/servers/%2")
+                                 .arg(m_api->getApiHost(), m_currentStudio.id())),
+                        m_auth->accessToken(), QString(), QString()));
     connect(m_studioSocketPtr.get(), &VsWebSocket::textMessageReceived, this,
             &VirtualStudio::handleWebsocketMessage);
     connect(m_studioSocketPtr.get(), &VsWebSocket::disconnected, this,
@@ -970,18 +1102,16 @@ void VirtualStudio::completeConnection()
         }
 #endif
 
-        // adjust queueBuffer config setting to map to auto headroom
-        int queue_buffer = m_audioConfigPtr->getQueueBuffer();
-        if (queue_buffer <= 0) {
-            queue_buffer = -500;
-        } else {
-            queue_buffer *= -1;
-        }
+        // check if we should use studio or local queueBuffer setting
+        int queueBuffer = m_queueBuffer;
+        if (m_useStudioQueueBuffer)
+            queueBuffer = m_currentStudio.queueBuffer();
+        m_devicePtr->setQueueBuffer(queueBuffer);
 
         // create a new JackTrip instance
         JackTrip* jackTrip = m_devicePtr->initJackTrip(
             useRtAudio, input, output, baseInputChannel, numInputChannels,
-            baseOutputChannel, numOutputChannels, inputMixMode, buffer_size, queue_buffer,
+            baseOutputChannel, numOutputChannels, inputMixMode, buffer_size,
             &m_currentStudio);
         if (jackTrip == 0) {
             processError("Could not bind port");
@@ -1091,6 +1221,9 @@ void VirtualStudio::disconnect()
     // reset network statistics
     m_networkStats = QJsonObject();
 
+    // force audio sockets to reconnect, since audio has stopped
+    m_audioConfigPtr->clearAudioSockets();
+
     if (!m_currentStudio.id().isEmpty()) {
         emit openFeedbackSurveyModal(m_currentStudio.id());
         m_currentStudio.setId("");
@@ -1182,6 +1315,13 @@ void VirtualStudio::handleDeeplinkRequest(const QUrl& link)
     // otherwise, assume we are on setup screens and can let the normal flow handle it
 }
 
+void VirtualStudio::handleAudioSocketRequest(QSharedPointer<QLocalSocket>& socket)
+{
+    QSharedPointer<AudioSocket> audioSocketPtr(new AudioSocket(socket));
+    m_audioConfigPtr->registerAudioSocket(audioSocketPtr);
+    triggerReconnect(true);
+}
+
 void VirtualStudio::exit()
 {
     // if multiple close events are received, emit the signalExit to force-quit the app
@@ -1218,18 +1358,10 @@ void VirtualStudio::slotAuthSucceeded()
     }
     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
+    if (!m_devicePtr.isNull()) {
+        m_devicePtr->disconnect();
+    }
     m_devicePtr.reset(new VsDevice(m_auth, m_api, m_audioConfigPtr));
     connect(m_devicePtr.get(), &VsDevice::updateNetworkStats, this,
             &VirtualStudio::updatedStats);
@@ -1256,6 +1388,38 @@ void VirtualStudio::slotAuthSucceeded()
     getUserMetadata();  // calls refreshStudios(-1, false)
 }
 
+void VirtualStudio::slotAccessTokenUpdated(QString accessToken)
+{
+    // set cookie
+    QWebEngineCookieStore* cookieStore = m_qwebEngineProfile->cookieStore();
+    QNetworkCookie authCookie =
+        QNetworkCookie(QByteArray("auth_code"), accessToken.toUtf8());
+
+    QUrl url1 = QUrl(QStringLiteral("https://www.jacktrip.com"));
+    QUrl url2 = QUrl(QStringLiteral("https://app.jacktrip.com"));
+    QUrl url3 = QUrl(QStringLiteral("http://localhost:3000"));
+    if (testMode()) {
+        url1 = QUrl(QStringLiteral("https://next-test.jacktrip.com"));
+        url2 = QUrl(QStringLiteral("https://test.jacktrip.com"));
+    }
+
+    cookieStore->setCookie(authCookie, url1);
+    cookieStore->setCookie(authCookie, url2);
+    if (testMode()) {
+        cookieStore->setCookie(authCookie, url3);
+    }
+
+    // Get refresh token and userId
+    m_refreshToken = m_auth->refreshToken();
+    m_userId       = m_auth->userId();
+
+    QSettings settings;
+    settings.beginGroup(QStringLiteral("VirtualStudio"));
+    settings.setValue(QStringLiteral("RefreshToken"), m_refreshToken);
+    settings.setValue(QStringLiteral("UserId"), m_userId);
+    settings.endGroup();
+}
+
 void VirtualStudio::connectionFinished()
 {
     if (!m_devicePtr.isNull()
@@ -1341,6 +1505,7 @@ void VirtualStudio::handleWebsocketMessage(const QString& msg)
     QString serverStatus    = serverState[QStringLiteral("status")].toString();
     bool serverEnabled      = serverState[QStringLiteral("enabled")].toBool();
     QString serverCloudId   = serverState[QStringLiteral("cloudId")].toString();
+    int queueBuffer         = serverState[QStringLiteral("queueBuffer")].toInt();
 
     // server notifications are also transmitted along this websocket, so ignore data if
     // it contains "message"
@@ -1354,6 +1519,7 @@ void VirtualStudio::handleWebsocketMessage(const QString& msg)
     m_currentStudio.setStatus(serverStatus);
     m_currentStudio.setEnabled(serverEnabled);
     m_currentStudio.setCloudId(serverCloudId);
+    m_currentStudio.setQueueBuffer(queueBuffer);
     if (!m_jackTripRunning) {
         if (serverStatus == QLatin1String("Ready") && m_onConnectedScreen) {
             m_currentStudio.setHost(serverState[QStringLiteral("serverHost")].toString());
@@ -1362,6 +1528,8 @@ void VirtualStudio::handleWebsocketMessage(const QString& msg)
                 serverState[QStringLiteral("sessionId")].toString());
             completeConnection();
         }
+    } else if (m_useStudioQueueBuffer && !m_devicePtr.isNull()) {
+        m_devicePtr->setQueueBuffer(m_currentStudio.queueBuffer());
     }
 
     emit currentStudioChanged();
@@ -1689,9 +1857,15 @@ VirtualStudio::~VirtualStudio()
     // close the window
     m_view.reset();
     // stop the audio worker thread before destructing other things
-    m_audioConfigPtr.reset();
+    if (!m_audioConfigPtr.isNull()) {
+        m_audioConfigPtr->disconnect();
+        m_audioConfigPtr.reset();
+    }
     // stop device and corresponding threads
-    m_devicePtr.reset();
+    if (!m_devicePtr.isNull()) {
+        m_devicePtr->disconnect();
+        m_devicePtr.reset();
+    }
 }
 
 QApplication* VirtualStudio::createApplication(int& argc, char* argv[])
index 8861af536c0be2d2e420a9155be9fdc4a8546ed3..35987fff584da45d19622672eddb97f53d053b24 100644 (file)
@@ -41,6 +41,7 @@
 #include <QMap>
 #include <QMutex>
 #include <QNetworkAccessManager>
+#include <QNetworkCookie>
 #include <QObject>
 #include <QScopedPointer>
 #include <QSharedPointer>
@@ -50,6 +51,8 @@
 #include <QUrl>
 #include <QVector>
 #include <QWebChannel>
+#include <QWebEngineCookieStore>
+#include <QWebEngineProfile>
 #include <QWebSocketServer>
 
 #include "../Settings.h"
@@ -59,6 +62,8 @@
 #include "vsServerInfo.h"
 
 class JackTrip;
+class QLocalSocket;
+class SocketServer;
 class VsAudio;
 class VsApi;
 class VsAuth;
@@ -72,7 +77,6 @@ class VirtualStudio : public QObject
 {
     Q_OBJECT
     Q_PROPERTY(int webChannelPort READ webChannelPort NOTIFY webChannelPortChanged)
-    Q_PROPERTY(bool hasRefreshToken READ hasRefreshToken NOTIFY hasRefreshTokenChanged)
     Q_PROPERTY(QString versionString READ versionString CONSTANT)
     Q_PROPERTY(QString buildString READ buildString CONSTANT)
     Q_PROPERTY(QString copyrightString READ copyrightString CONSTANT)
@@ -94,6 +98,10 @@ class VirtualStudio : public QObject
     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(int queueBuffer READ getQueueBuffer WRITE setQueueBuffer NOTIFY
+                   queueBufferChanged)
+    Q_PROPERTY(bool useStudioQueueBuffer READ useStudioQueueBuffer WRITE
+                   setUseStudioQueueBuffer NOTIFY useStudioQueueBufferChanged)
 
     Q_PROPERTY(QString updateChannel READ updateChannel WRITE setUpdateChannel NOTIFY
                    updateChannelChanged)
@@ -130,7 +138,6 @@ class VirtualStudio : public QObject
     void raiseToTop();
 
     int webChannelPort();
-    bool hasRefreshToken();
     QString versionString();
     QString buildString();
     QString copyrightString();
@@ -168,6 +175,10 @@ class VirtualStudio : public QObject
     bool psiBuild();
     QString failedMessage();
     bool networkOutage();
+    int getQueueBuffer() const;
+    void setQueueBuffer(int queueBuffer);
+    bool useStudioQueueBuffer();
+    void setUseStudioQueueBuffer(bool b);
     bool backendAvailable();
     QString windowState();
     QString apiHost();
@@ -198,6 +209,7 @@ class VirtualStudio : public QObject
     void showAbout();
     void openLink(const QString& url);
     void handleDeeplinkRequest(const QUrl& url);
+    void handleAudioSocketRequest(QSharedPointer<QLocalSocket>& socket);
     void udpWaitingTooLong();
     void setWindowState(QString state);
     void joinStudio();
@@ -210,7 +222,6 @@ class VirtualStudio : public QObject
     void disconnected();
     void refreshFinished(int index);
     void webChannelPortChanged(int webChannelPort);
-    void hasRefreshTokenChanged();
     void logoSectionChanged();
     void connectedErrorMsgChanged();
     void serverModelChanged();
@@ -233,6 +244,8 @@ class VirtualStudio : public QObject
     void failedMessageChanged();
     void studioToJoinChanged();
     void updatedNetworkOutage(bool outage);
+    void queueBufferChanged(int queueBuffer);
+    void useStudioQueueBufferChanged(bool b);
     void windowStateUpdated();
     void isExitingChanged();
     void scheduleStudioRefresh(int index, bool signalRefresh);
@@ -240,10 +253,12 @@ class VirtualStudio : public QObject
     void apiHostChanged();
     void feedbackDetected();
     void openFeedbackSurveyModal(QString serverId);
+    void closeFeedbackSurveyModal();
     void openAboutWindow();
 
    private slots:
     void slotAuthSucceeded();
+    void slotAccessTokenUpdated(QString accessToken);
     void receivedConnectionFromPeer();
     void handleWebsocketMessage(const QString& msg);
     void restartStudioSocket();
@@ -274,6 +289,8 @@ class VirtualStudio : public QObject
     UserInterface& m_interface;
     VsServerInfo m_currentStudio;
     QNetworkAccessManager* m_networkAccessManagerPtr;
+    QWebEngineProfile* m_qwebEngineProfile;
+    QSharedPointer<SocketServer> m_socketServerPtr;
     QScopedPointer<VsQuickView> m_view;
     QSharedPointer<VsDeeplink> m_deepLinkPtr;
     QSharedPointer<VsAuth> m_auth;
@@ -314,6 +331,8 @@ class VirtualStudio : public QObject
     bool m_collapseDeviceControls = false;
     bool m_testMode               = false;
     bool m_authenticated          = false;
+    bool m_useStudioQueueBuffer   = true;
+    int m_queueBuffer             = 0;
     float m_fontScale             = 1;
     float m_uiScale               = 1;
     uint32_t m_webChannelPort     = 1;
index 0b847fe8dc07f5232b3672ccb3e1f58f1862063b..945ce72552b92dfd0ac35a8e8d5b2c3e705c22c6 100644 (file)
@@ -50,6 +50,7 @@
     <file>language.svg</file>
     <file>micoff.svg</file>
     <file>help.svg</file>
+    <file>balance.svg</file>
     <file>quiet.svg</file>
     <file>loud.svg</file>
     <file>refresh.svg</file>
index 513390bc7c5005a6feb9ba7bb5c20fff49ba4196..74fdaff97a502083799015303ccefcba0a983811 100644 (file)
@@ -45,7 +45,17 @@ VsApi::VsApi(QNetworkAccessManager* networkAccessManager)
 
 QNetworkReply* VsApi::getAuth0UserInfo()
 {
-    return get(QUrl("https://auth.jacktrip.org/userinfo"));
+    // this function operates a little differently because it is made to
+    // Auth0 directly rather than our own API server, which requires a specific
+    // an Authorization header rather than a cookie
+    QUrl url                = QUrl("https://auth.jacktrip.org/userinfo");
+    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::getUser(const QString& userId)
@@ -115,9 +125,12 @@ 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());
 
+    QList<QNetworkCookie> cookies;
+    QNetworkCookie authCookie =
+        QNetworkCookie(QByteArray("auth_code"), m_accessToken.toUtf8());
+    cookies.append(authCookie);
+    request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
     QNetworkReply* reply = m_networkAccessManager->get(request);
     return reply;
 }
@@ -127,11 +140,14 @@ 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());
 
+    QList<QNetworkCookie> cookies;
+    QNetworkCookie authCookie =
+        QNetworkCookie(QByteArray("auth_code"), m_accessToken.toUtf8());
+    cookies.append(authCookie);
+    request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
     QNetworkReply* reply = m_networkAccessManager->post(request, data);
     return reply;
 }
@@ -141,10 +157,14 @@ 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());
+
+    QList<QNetworkCookie> cookies;
+    QNetworkCookie authCookie =
+        QNetworkCookie(QByteArray("auth_code"), m_accessToken.toUtf8());
+    cookies.append(authCookie);
+    request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
     QNetworkReply* reply = m_networkAccessManager->put(request, data);
     return reply;
 }
@@ -154,9 +174,12 @@ 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());
 
+    QList<QNetworkCookie> cookies;
+    QNetworkCookie authCookie =
+        QNetworkCookie(QByteArray("auth_code"), m_accessToken.toUtf8());
+    cookies.append(authCookie);
+    request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
     QNetworkReply* reply = m_networkAccessManager->deleteResource(request);
     return reply;
 }
\ No newline at end of file
index 9b1d71350aaf8f2cc60240b30565ad8613074f71..d66cab8a555930e844bda2b6e76a9157cdf5ddc9 100644 (file)
 
 #include <QEventLoop>
 #include <QJsonParseError>
+#include <QList>
 #include <QMap>
 #include <QNetworkAccessManager>
+#include <QNetworkCookie>
 #include <QNetworkReply>
 #include <QNetworkRequest>
 #include <QString>
index 6964c7754781a18884b722a4938e09b95e1159ee..d244238447bead17e2e5548dccef9284d3f33908 100644 (file)
@@ -60,6 +60,7 @@
 #include "../Analyzer.h"
 #endif
 
+#include "../AudioSocket.h"
 #include "../JackTrip.h"
 #include "../Meter.h"
 #include "../Monitor.h"
@@ -96,11 +97,6 @@ VsAudio::VsAudio(QObject* parent)
     , 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();
@@ -176,6 +172,7 @@ VsAudio::VsAudio(QObject* parent)
 #else
     m_permissionsPtr.reset(new VsPermissions());
 #endif
+    qDebug() << "Microphone permissions: " << m_permissionsPtr->micPermission();
 }
 
 VsAudio::~VsAudio()
@@ -279,16 +276,6 @@ void VsAudio::setBufferSize(int bufSize)
     emit bufferSizeChanged();
 }
 
-void VsAudio::setQueueBuffer(int queueBuffer)
-{
-    if (m_queueBuffer == queueBuffer)
-        return;
-    if (queueBuffer < 0)
-        queueBuffer = 0;
-    m_queueBuffer = queueBuffer;
-    emit queueBufferChanged();
-}
-
 void VsAudio::setNumInputChannels(int numChannels)
 {
     if (numChannels == m_numInputChannels)
@@ -442,6 +429,7 @@ void VsAudio::stopAudio(bool block)
     if (!getAudioReady())
         return;
     emit signalStopAudio();
+    clearAudioSockets();  // force audio sockets to reconnect
     if (!block)
         return;
     WaitForSignal(this, &VsAudio::signalAudioIsNotReady);
@@ -542,7 +530,6 @@ void VsAudio::loadSettings()
     }
 
     setBufferSize(settings.value(QStringLiteral("BufferSize"), 128).toInt());
-    setQueueBuffer(settings.value(QStringLiteral("QueueBuffer"), 0).toInt());
     setFeedbackDetectionEnabled(
         settings.value(QStringLiteral("FeedbackDetectionEnabled"), true).toBool());
     settings.endGroup();
@@ -565,7 +552,6 @@ void VsAudio::saveSettings()
     settings.setValue(QStringLiteral("BaseOutputChannel"), m_baseOutputChannel);
     settings.setValue(QStringLiteral("NumOutputChannels"), m_numOutputChannels);
     settings.setValue(QStringLiteral("BufferSize"), m_audioBufferSize);
-    settings.setValue(QStringLiteral("QueueBuffer"), m_queueBuffer);
     settings.setValue(QStringLiteral("FeedbackDetectionEnabled"),
                       m_feedbackDetectionEnabled);
     settings.endGroup();
@@ -666,74 +652,106 @@ void VsAudio::appendProcessPlugins(AudioInterface& audioInterface, bool forJackT
     setInputMuted(false);
 
     // Create plugins
-    m_inputMeterPluginPtr   = new Meter(numInputChannels);
-    m_outputMeterPluginPtr  = new Meter(numOutputChannels);
-    m_inputVolumePluginPtr  = new Volume(numInputChannels);
-    m_outputVolumePluginPtr = new Volume(numOutputChannels);
+    Meter* inputMeterPluginPtr    = new Meter(numInputChannels);
+    Meter* outputMeterPluginPtr   = new Meter(numOutputChannels);
+    Volume* inputVolumePluginPtr  = new Volume(numInputChannels);
+    Volume* outputVolumePluginPtr = new Volume(numOutputChannels);
+    QSharedPointer<ProcessPlugin> inputVolumePluginSharedPtr(inputVolumePluginPtr);
+    QSharedPointer<ProcessPlugin> outputVolumePluginSharedPtr(outputVolumePluginPtr);
+    QSharedPointer<ProcessPlugin> inputMeterPluginSharedPtr(inputMeterPluginPtr);
+    QSharedPointer<ProcessPlugin> outputMeterPluginSharedPtr(outputMeterPluginPtr);
 
     // initialize input and output volumes
-    m_outputVolumePluginPtr->volumeUpdated(m_outMultiplier);
-    m_inputVolumePluginPtr->volumeUpdated(m_inMultiplier);
-    m_inputVolumePluginPtr->muteUpdated(m_inMuted);
+    outputVolumePluginPtr->volumeUpdated(m_outMultiplier);
+    inputVolumePluginPtr->volumeUpdated(m_inMultiplier);
+    inputVolumePluginPtr->muteUpdated(m_inMuted);
 
     // Connect plugins for communication with UI
-    connect(m_inputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
+    connect(inputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
             &VsAudio::updatedInputVuMeasurements);
-    connect(m_outputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
+    connect(outputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
             &VsAudio::updatedOutputVuMeasurements);
-    connect(this, &VsAudio::updatedInputVolume, m_inputVolumePluginPtr,
+    connect(this, &VsAudio::updatedInputVolume, inputVolumePluginPtr,
             &Volume::volumeUpdated);
-    connect(this, &VsAudio::updatedOutputVolume, m_outputVolumePluginPtr,
+    connect(this, &VsAudio::updatedOutputVolume, outputVolumePluginPtr,
             &Volume::volumeUpdated);
-    connect(this, &VsAudio::updatedInputMuted, m_inputVolumePluginPtr,
+    connect(this, &VsAudio::updatedInputMuted, 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);
+    audioInterface.appendProcessPluginToNetwork(inputVolumePluginSharedPtr);
+    audioInterface.appendProcessPluginToNetwork(inputMeterPluginSharedPtr);
 
     if (forJackTrip) {
         // plugins for stream going to audio interface
-        audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr);
+        audioInterface.appendProcessPluginFromNetwork(outputVolumePluginSharedPtr);
 
         // 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* monitorPluginPtr =
+            new Monitor(std::max(numInputChannels, numOutputChannels));
+        monitorPluginPtr->volumeUpdated(m_monMultiplier);
+        connect(this, &VsAudio::updatedMonitorVolume, monitorPluginPtr,
                 &Monitor::volumeUpdated);
+        QSharedPointer<ProcessPlugin> monitorPluginSharedPtr(monitorPluginPtr);
+        audioInterface.appendProcessPluginToMonitor(monitorPluginSharedPtr);
 
 #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,
+            Analyzer* outputAnalyzerPluginPtr = new Analyzer(numOutputChannels);
+            outputAnalyzerPluginPtr->setIsMonitoringAnalyzer(true);
+            connect(outputAnalyzerPluginPtr, &Analyzer::signalFeedbackDetected, this,
                     &VsAudio::detectedFeedbackLoop);
+            QSharedPointer<ProcessPlugin> outputAnalyzerPluginSharedPtr(
+                outputAnalyzerPluginPtr);
+            audioInterface.appendProcessPluginToMonitor(outputAnalyzerPluginSharedPtr);
         }
 #endif
 
         // Setup output meter
         // Note: Add this to monitor process to include self-volume
-        m_outputMeterPluginPtr->setIsMonitoringMeter(true);
-        audioInterface.appendProcessPluginToMonitor(m_outputMeterPluginPtr);
+        outputMeterPluginPtr->setIsMonitoringMeter(true);
+        audioInterface.appendProcessPluginToMonitor(outputMeterPluginSharedPtr);
 
     } else {
         // tone plugin is used to test audio output
         Tone* outputTonePluginPtr = new Tone(getNumOutputChannels());
         connect(this, &VsAudio::signalPlayOutputAudio, outputTonePluginPtr,
                 &Tone::triggerPlayback);
-        audioInterface.appendProcessPluginFromNetwork(outputTonePluginPtr);
+        QSharedPointer<ProcessPlugin> outputTonePluginSharedPtr(outputTonePluginPtr);
+        audioInterface.appendProcessPluginFromNetwork(outputTonePluginSharedPtr);
 
         // plugins for stream going to audio interface
-        audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr);
-        audioInterface.appendProcessPluginFromNetwork(m_outputMeterPluginPtr);
+        audioInterface.appendProcessPluginFromNetwork(outputVolumePluginSharedPtr);
+        audioInterface.appendProcessPluginFromNetwork(outputMeterPluginSharedPtr);
+    }
+
+    // clear out any audio sockets that have disconnected
+    QMutexLocker locker(&m_audioSocketMutex);
+    for (auto i = m_audioSockets.begin(); i != m_audioSockets.end();) {
+        if ((*i)->isConnected()) {
+            audioInterface.appendAudioSocket(*i);
+            ++i;
+        } else {
+            i = m_audioSockets.erase(i);
+        }
     }
 }
 
+void VsAudio::registerAudioSocket(QSharedPointer<AudioSocket>& s)
+{
+    QMutexLocker locker(&m_audioSocketMutex);
+    m_audioSockets.push_back(s);
+}
+
+void VsAudio::clearAudioSockets()
+{
+    QMutexLocker locker(&m_audioSocketMutex);
+    m_audioSockets.clear();
+}
+
 void VsAudio::setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel)
 {
     m_inputComboModel  = inputComboModel;
index c75b9fc78eb9d7f4ef57a4c0442ba0279048a34d..3521d51ff992c07e6e7c6f875cbbd00e951c7761 100644 (file)
@@ -39,6 +39,7 @@
 
 #include <QJsonArray>
 #include <QList>
+#include <QMutex>
 #include <QObject>
 #include <QSharedPointer>
 #include <QString>
@@ -54,6 +55,7 @@
 #endif
 
 class Analyzer;
+class AudioSocket;
 class JackTrip;
 class Meter;
 class Monitor;
@@ -81,8 +83,6 @@ class VsAudio : public QObject
         int sampleRate READ getSampleRate WRITE setSampleRate NOTIFY sampleRateChanged)
     Q_PROPERTY(
         int bufferSize READ getBufferSize WRITE setBufferSize NOTIFY bufferSizeChanged)
-    Q_PROPERTY(int queueBuffer READ getQueueBuffer WRITE setQueueBuffer NOTIFY
-                   queueBufferChanged)
     Q_PROPERTY(int numInputChannels READ getNumInputChannels WRITE setNumInputChannels
                    NOTIFY numInputChannelsChanged)
     Q_PROPERTY(int numOutputChannels READ getNumOutputChannels WRITE setNumOutputChannels
@@ -168,7 +168,6 @@ class VsAudio : public QObject
     }
     int getSampleRate() const { return m_audioSampleRate; }
     int getBufferSize() const { return m_audioBufferSize; }
-    int getQueueBuffer() const { return m_queueBuffer; }
     int getNumInputChannels() const { return getUseRtAudio() ? m_numInputChannels : 2; }
     int getNumOutputChannels() const { return getUseRtAudio() ? m_numOutputChannels : 2; }
     int getBaseInputChannel() const { return getUseRtAudio() ? m_baseInputChannel : 0; }
@@ -208,6 +207,11 @@ class VsAudio : public QObject
     const QString& getDevicesWarningHelpUrl() const { return m_devicesWarningHelpUrl; }
     const QString& getDevicesErrorHelpUrl() const { return m_devicesErrorHelpUrl; }
     bool getHighLatencyFlag() const { return m_highLatencyFlag; }
+
+    // called by local socket server to process audio requests
+    void registerAudioSocket(QSharedPointer<AudioSocket>& s);
+    void clearAudioSockets();
+
    public slots:
 
     // setters for state shared with QML
@@ -215,7 +219,6 @@ class VsAudio : public QObject
     void setAudioBackend(const QString& backend);
     void setSampleRate(int sampleRate);
     void setBufferSize(int bufSize);
-    void setQueueBuffer(int queueBuffer);
     void setNumInputChannels(int numChannels);
     void setNumOutputChannels(int numChannels);
     void setBaseInputChannel(int baseChannel);
@@ -252,7 +255,6 @@ class VsAudio : public QObject
     void audioBackendChanged(bool useRtAudio);
     void sampleRateChanged();
     void bufferSizeChanged();
-    void queueBufferChanged();
     void numInputChannelsChanged(int numChannels);
     void numOutputChannelsChanged(int numChannels);
     void baseInputChannelChanged(int baseChannel);
@@ -326,7 +328,6 @@ class VsAudio : public QObject
     int m_audioSampleRate           = gDefaultSampleRate;
     int m_audioBufferSize =
         gDefaultBufferSizeInSamples;  ///< Audio buffer size to process on each callback
-    int m_queueBuffer       = 0;
     int m_numInputChannels  = gDefaultNumInChannels;
     int m_numOutputChannels = gDefaultNumOutChannels;
     int m_baseInputChannel  = 0;
@@ -357,20 +358,13 @@ class VsAudio : public QObject
     // other state not shared with QML
     QSharedPointer<VsPermissions> m_permissionsPtr;
     QScopedPointer<VsAudioWorker> m_audioWorkerPtr;
+    QVector<QSharedPointer<AudioSocket>> m_audioSockets;
+    QMutex m_audioSocketMutex;
     QThread* m_workerThreadPtr;
     QTimer m_inputClipTimer;
     QTimer m_outputClipTimer;
-    Meter* m_inputMeterPluginPtr;
-    Meter* m_outputMeterPluginPtr;
-    Volume* m_inputVolumePluginPtr;
-    Volume* m_outputVolumePluginPtr;
-    Monitor* m_monitorPluginPtr;
     bool mHasErrors;  ///< true if one or more error callbacks have been triggered
 
-#ifndef NO_FEEDBACK
-    Analyzer* m_outputAnalyzerPluginPtr;
-#endif
-
     QStringList m_audioBackendComboModel = {"JACK", "RtAudio"};
     QStringList m_bufferSizeComboModel = {"16", "32", "64", "128", "256", "512", "1024"};
 
index 25d84279ecf6d3f79bb4265e39cc32fef7d56258..0b558a5ba017e80de9faae146191b69b11932f4c 100644 (file)
 VsAuth::VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api)
     : m_clientId(AUTH_CLIENT_ID), m_authorizationServerHost(AUTH_SERVER_HOST)
 {
+    qint64 refreshIntervalInMs =
+        1000 * 60 * 60 * 3;  // automatic access token refresh every 3 hours
+    m_refreshTimer.reset(new QTimer());
+    m_refreshTimer->setInterval(refreshIntervalInMs);
+    m_refreshTimer->setSingleShot(false);
+
     m_networkAccessManager = networkAccessManager;
     m_api                  = api;
     m_deviceCodeFlow.reset(new VsDeviceCodeFlow(networkAccessManager));
@@ -53,6 +59,7 @@ VsAuth::VsAuth(QNetworkAccessManager* networkAccessManager, VsApi* api)
             &VsAuth::codeFlowCompleted);
     connect(m_deviceCodeFlow.data(), &VsDeviceCodeFlow::deviceCodeFlowTimedOut, this,
             &VsAuth::codeExpired);
+    connect(m_refreshTimer.data(), &QTimer::timeout, this, &VsAuth::refreshTimerTimedOut);
 
     m_verificationUrl = QStringLiteral("https://auth.jacktrip.org/activate");
 }
@@ -190,6 +197,13 @@ void VsAuth::codeExpired()
     emit deviceCodeExpired();
 }
 
+void VsAuth::refreshTimerTimedOut()
+{
+    if (m_refreshToken != "") {
+        refreshAccessToken(m_refreshToken);
+    }
+}
+
 void VsAuth::handleRefreshSucceeded(QString accessToken)
 {
     qDebug() << "Successfully refreshed access token";
@@ -198,11 +212,21 @@ void VsAuth::handleRefreshSucceeded(QString accessToken)
     m_authenticationStage    = QStringLiteral("success");
     m_errorMessage           = QStringLiteral("");
     m_attemptingRefreshToken = false;
+    m_accessTokenTimestamp   = QDateTime::currentDateTime();
+
+    m_refreshTimer->start();
 
     emit updatedAuthenticationStage(m_authenticationStage);
     emit updatedErrorMessage(m_errorMessage);
     emit updatedVerificationCode(m_verificationCode);
     emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
+    emit updatedAccessToken(m_accessToken);
+    emit updatedAccessTokenTimestamp(m_accessTokenTimestamp);
+}
+
+void VsAuth::handleRefreshFailed()
+{
+    m_refreshTimer->stop();
 }
 
 void VsAuth::handleAuthSucceeded(QString userId, QString accessToken)
@@ -225,6 +249,9 @@ void VsAuth::handleAuthSucceeded(QString userId, QString accessToken)
     m_errorMessage           = QStringLiteral("");
     m_attemptingRefreshToken = false;
     m_isAuthenticated        = true;
+    m_accessTokenTimestamp   = QDateTime::currentDateTime();
+
+    m_refreshTimer->start();
 
     emit updatedUserId(m_userId);
     emit updatedAuthenticationStage(m_authenticationStage);
@@ -233,6 +260,8 @@ void VsAuth::handleAuthSucceeded(QString userId, QString accessToken)
     emit updatedIsAuthenticated(m_isAuthenticated);
     emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
     emit updatedAuthenticationMethod(m_authenticationMethod);
+    emit updatedAccessToken(m_accessToken);
+    emit updatedAccessTokenTimestamp(m_accessTokenTimestamp);
 
     // notify UI and virtual studio class of success
     emit authSucceeded();
@@ -253,6 +282,7 @@ void VsAuth::handleAuthFailed(QString errorMessage)
     m_authenticationMethod   = QStringLiteral("");
     m_attemptingRefreshToken = false;
     m_isAuthenticated        = false;
+    m_accessTokenTimestamp   = QDateTime::fromMSecsSinceEpoch(0);
 
     emit updatedUserId(m_userId);
     emit updatedAuthenticationStage(m_authenticationStage);
@@ -261,6 +291,8 @@ void VsAuth::handleAuthFailed(QString errorMessage)
     emit updatedIsAuthenticated(m_isAuthenticated);
     emit updatedAttemptingRefreshToken(m_attemptingRefreshToken);
     emit updatedAuthenticationMethod(m_authenticationMethod);
+    emit updatedAccessToken(m_accessToken);
+    emit updatedAccessTokenTimestamp(m_accessTokenTimestamp);
 
     // notify UI and virtual studio class of failure
     emit authFailed();
@@ -271,18 +303,21 @@ void VsAuth::cancelAuthenticationFlow()
     qDebug() << "Canceling authentication flow";
     m_deviceCodeFlow->cancelCodeFlow();
 
-    m_userId              = QStringLiteral("");
-    m_verificationCode    = QStringLiteral("");
-    m_accessToken         = QStringLiteral("");
-    m_authenticationStage = QStringLiteral("unauthenticated");
-    m_errorMessage        = QStringLiteral("cancelled");
-    m_isAuthenticated     = false;
+    m_userId               = QStringLiteral("");
+    m_verificationCode     = QStringLiteral("");
+    m_accessToken          = QStringLiteral("");
+    m_accessTokenTimestamp = QDateTime::fromMSecsSinceEpoch(0);
+    m_authenticationStage  = QStringLiteral("unauthenticated");
+    m_errorMessage         = QStringLiteral("cancelled");
+    m_isAuthenticated      = false;
 
     emit updatedUserId(m_userId);
     emit updatedAuthenticationStage(m_authenticationStage);
     emit updatedErrorMessage(m_errorMessage);
     emit updatedVerificationCode(m_verificationCode);
     emit updatedIsAuthenticated(m_isAuthenticated);
+    emit updatedAccessToken(m_accessToken);
+    emit updatedAccessTokenTimestamp(m_accessTokenTimestamp);
 }
 
 void VsAuth::logout()
@@ -292,13 +327,18 @@ void VsAuth::logout()
     }
     qDebug() << "Logging out";
 
+    // stop timer to refresh token
+    m_refreshTimer->stop();
+
     // reset auth state
-    m_userId              = QStringLiteral("");
-    m_verificationCode    = QStringLiteral("");
-    m_accessToken         = QStringLiteral("");
-    m_authenticationStage = QStringLiteral("unauthenticated");
-    m_errorMessage        = QStringLiteral("");
-    m_isAuthenticated     = false;
+    m_userId               = QStringLiteral("");
+    m_verificationCode     = QStringLiteral("");
+    m_accessToken          = QStringLiteral("");
+    m_refreshToken         = QStringLiteral("");
+    m_authenticationStage  = QStringLiteral("unauthenticated");
+    m_errorMessage         = QStringLiteral("");
+    m_isAuthenticated      = false;
+    m_accessTokenTimestamp = QDateTime::fromMSecsSinceEpoch(0);
 
     emit updatedUserId(m_userId);
     emit updatedAuthenticationStage(m_authenticationStage);
index e79116a4eae8e177926ad44a317fa8b6e208ee77..e075478bd88afa3c2986f6cc7922ecd0c1c1f713 100644 (file)
 #ifndef VSAUTH_H
 #define VSAUTH_H
 
+#include <QDateTime>
 #include <QNetworkAccessManager>
 #include <QQmlContext>
 #include <QQmlEngine>
 #include <QString>
+#include <QTimer>
 #include <iostream>
 
 #include "vsApi.h"
@@ -86,6 +88,7 @@ class VsAuth : public QObject
     QString refreshToken() { return m_refreshToken; };
     QString authenticationMethod() { return m_authenticationMethod; }
     bool attemptingRefreshToken() { return m_attemptingRefreshToken; }
+    QDateTime accessTokenTimestamp() { return m_accessTokenTimestamp; }
 
    signals:
     void updatedAuthenticationStage(QString authenticationStage);
@@ -96,6 +99,8 @@ class VsAuth : public QObject
     void updatedUserId(QString userId);
     void updatedAuthenticationMethod(QString grant);
     void updatedAttemptingRefreshToken(bool attemptingRefreshToken);
+    void updatedAccessToken(QString accessToken);
+    void updatedAccessTokenTimestamp(QDateTime accessTokenTimestamp);
     void authSucceeded();
     void authFailed();
     void refreshTokenFailed();
@@ -104,11 +109,13 @@ class VsAuth : public QObject
 
    private slots:
     void handleRefreshSucceeded(QString accessToken);
+    void handleRefreshFailed();
     void handleAuthSucceeded(QString userId, QString accessToken);
     void handleAuthFailed(QString errorMessage);
     void initializedCodeFlow(QString code, QString verificationUrl);
     void codeFlowCompleted(QString accessToken, QString refreshToken);
     void codeExpired();
+    void refreshTimerTimedOut();
 
    private:
     void fetchUserInfo(QString accessToken);
@@ -127,10 +134,12 @@ class VsAuth : public QObject
     QString m_userId;
     QString m_accessToken;
     QString m_refreshToken;
+    QDateTime m_accessTokenTimestamp = QDateTime::fromMSecsSinceEpoch(0);
 
     QNetworkAccessManager* m_networkAccessManager;
     VsApi* m_api;
     QScopedPointer<VsDeviceCodeFlow> m_deviceCodeFlow;
+    QScopedPointer<QTimer> m_refreshTimer;
 };
 
 #endif
\ No newline at end of file
index a602abae321f094b1226d91ed26fd88029267be4..f1448a4d5f3d0f16fae16ad22a4d4ce50b086002 100644 (file)
@@ -29,9 +29,9 @@
 //*****************************************************************
 
 /**
- * \file vsDeeplink.cpp
- * \author Aaron Wyatt, based on code by Matt Horton
- * \date February 2023
+ * \file VsDeeplink.cpp
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date December 2024
  */
 
 #include "vsDeeplink.h"
 #include <QDesktopServices>
 #include <QDir>
 #include <QEventLoop>
+#include <QLocalSocket>
 #include <QMutexLocker>
 #include <QSettings>
 #include <QTimer>
 
-VsDeeplink::VsDeeplink(const QString& deeplink) : m_deeplink(deeplink)
+VsDeeplink::VsDeeplink()
 {
     setUrlScheme();
-    checkForInstance();
     QDesktopServices::setUrlHandler(QStringLiteral("jacktrip"), this, "handleUrl");
 }
 
@@ -57,140 +57,30 @@ 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");
+    emit signalVsDeeplink(url);
 }
 
-void VsDeeplink::connectionReceived()
+void VsDeeplink::handleVsDeeplinkRequest(QSharedPointer<QLocalSocket>& socket)
 {
-    // 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;
+    if (!socket->waitForReadyRead() && socket->bytesAvailable() <= 0) {
+        qDebug() << "VsDeeplink socket: not ready and no bytes available: "
+                 << socket->errorString();
+        socket->close();
+        return;
     }
 
-    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();
+    if (socket->bytesAvailable() < (int)sizeof(quint16)) {
+        qDebug() << "VsDeeplink socket: ready but no bytes available";
+        socket->close();
+        return;
     }
 
-    // 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);
-    }
+    QByteArray in(socket->readAll());
+    socket->close();
+    QString urlString(in);
+    handleUrl(urlString);
 }
 
 void VsDeeplink::setUrlScheme()
index 464073bb4c231fa61207d5e548c8ea33c7c943dd..faa01922014eb1973544d90d1feb1addea70e8be 100644 (file)
 //*****************************************************************
 
 /**
- * \file vsDeeplink.h
+ * \file VsDeeplink.h
  * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
- * \date August 2023
+ * \date December 2024
  */
 
 #ifndef __VSDEEPLINK_H__
 #define __VSDEEPLINK_H__
 
-#include <QLocalServer>
-#include <QLocalSocket>
-#include <QScopedPointer>
+#include <QObject>
+#include <QSharedPointer>
 #include <QString>
 #include <QUrl>
 
+class QLocalSocket;
+
 class VsDeeplink : public QObject
 {
     Q_OBJECT
 
    public:
     // construct with an instance of the application, to parse command line args
-    VsDeeplink(const QString& deeplink);
+    VsDeeplink();
 
     // 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
+   public slots:
+    // handleUrl is called to trigger processing of a VsDeeplink
     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 by local socket server to process VsDeeplink requests
+    void handleVsDeeplinkRequest(QSharedPointer<QLocalSocket>& socket);
 
-    // 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();
+   signals:
+    // signalVsDeeplink is emitted when we want the local instance to process a VsDeeplink
+    void signalVsDeeplink(const QUrl& url);
 
    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__
index 4f036df3a38cf876d8f1bf4539620b42ea96996f..8e49fd6179844b8de1e73cb9d9e86662b4b5c658 100644 (file)
@@ -189,6 +189,8 @@ void VsDevice::sendHeartbeat()
         json.insert(QLatin1String("high_latency"),
                     m_audioConfigPtr->getHighLatencyFlag());
         json.insert(QLatin1String("network_outage"), m_networkOutage);
+        json.insert(QLatin1String("recv_latency"),
+                    m_jackTrip.isNull() ? -1 : m_jackTrip->getLatency());
 
         // For the internal application UI, ms will suffice. No conversion needed
         QJsonObject pingStats = {};
@@ -201,6 +203,8 @@ void VsDevice::sendHeartbeat()
                          ((int)(10 * stats.stdDevRtt)) / 10.0);
         pingStats.insert(QLatin1String("highLatency"),
                          m_audioConfigPtr->getHighLatencyFlag());
+        pingStats.insert(QLatin1String("recvLatency"),
+                         m_jackTrip.isNull() ? -1 : m_jackTrip->getLatency());
         emit updateNetworkStats(pingStats);
     }
 
@@ -277,8 +281,7 @@ JackTrip* VsDevice::initJackTrip(
     [[maybe_unused]] std::string output, [[maybe_unused]] int baseInputChannel,
     [[maybe_unused]] int numChannelsIn, [[maybe_unused]] int baseOutputChannel,
     [[maybe_unused]] int numChannelsOut, [[maybe_unused]] int inputMixMode,
-    [[maybe_unused]] int bufferSize, [[maybe_unused]] int queueBuffer,
-    VsServerInfo* studioInfo)
+    [[maybe_unused]] int bufferSize, VsServerInfo* studioInfo)
 {
     m_jackTrip.reset(
         new JackTrip(JackTrip::CLIENTTOPINGSERVER, JackTrip::UDP, baseInputChannel,
@@ -305,7 +308,16 @@ JackTrip* VsDevice::initJackTrip(
     m_jackTrip->setBindPorts(bindPort);
     m_jackTrip->setRemoteClientName(m_appID);
     m_jackTrip->setBufferStrategy(3);  // PLC
+
+    // adjust queueBuffer config setting to map to auto headroom
+    int queueBuffer = m_queueBuffer;
+    if (queueBuffer <= 0) {
+        queueBuffer = -500;
+    } else {
+        queueBuffer *= -1;
+    }
     m_jackTrip->setBufferQueueLength(queueBuffer);
+
     m_jackTrip->setUseRtUdpPriority(
         true);  // rt udp priority reduces glitches on desktops
     m_jackTrip->setPeerAddress(studioInfo->host());
@@ -412,6 +424,24 @@ void VsDevice::syncDeviceSettings()
     m_sendVolumeTimer.start(100);
 }
 
+// setQueueBuffer updates balance between latency and quality for audio received
+void VsDevice::setQueueBuffer(int queueBuffer)
+{
+    if (m_queueBuffer == queueBuffer)
+        return;
+    if (queueBuffer < 0)
+        queueBuffer = 0;
+    m_queueBuffer = queueBuffer;
+    if (!m_jackTrip.isNull()) {
+        if (queueBuffer <= 0) {
+            queueBuffer = -500;
+        } else {
+            queueBuffer *= -1;
+        }
+        m_jackTrip->setBufferQueueLength(queueBuffer);
+    }
+}
+
 // handleJackTripError is a slot intended to be triggered on jacktrip process signals
 void VsDevice::handleJackTripError()
 {
index 2ddb7f962ba7e9def267ca9090ee3436baee5aec..64878ba62f819b91013a203f4a3a583bd383720d 100644 (file)
@@ -72,7 +72,7 @@ class VsDevice : public QObject
     JackTrip* initJackTrip(bool useRtAudio, std::string input, std::string output,
                            int baseInputChannel, int numChannelsIn, int baseOutputChannel,
                            int numChannelsOut, int inputMixMode, int bufferSize,
-                           int queueBuffer, VsServerInfo* studioInfo);
+                           VsServerInfo* studioInfo);
     void startJackTrip(const VsServerInfo& studioInfo);
     void stopJackTrip(bool isReconnecting = false);
     void reconcileAgentConfig(QJsonDocument newState);
@@ -84,6 +84,7 @@ class VsDevice : public QObject
 
    public slots:
     void syncDeviceSettings();
+    void setQueueBuffer(int queueBuffer);
 
    private slots:
     void handleJackTripError();
@@ -114,6 +115,7 @@ class VsDevice : public QObject
     QScopedPointer<JackTrip> m_jackTrip;
     QRandomGenerator m_randomizer;
     QTimer m_sendVolumeTimer;
+    int m_queueBuffer    = 0;
     bool m_networkOutage = false;
     bool m_stopping      = false;
 };
index cca13002a590e84ba325d1b3cbd8d12159c89dbf..46e3f361787e6cc98609172385e164cfccc98d69 100644 (file)
@@ -85,11 +85,13 @@ void VsMacPermissions::getMicPermission()
             {
                 // The user has previously denied access.
                 setMicPermission(QStringLiteral("denied"));
+                break;
             }
             case AVAuthorizationStatusRestricted:
             {
                 // The user can't grant access due to restrictions.
                 setMicPermission(QStringLiteral("denied"));
+                break;
             }
         }
     } else {
index 1034af03f6d18c33e99df56e883417654731c007..beae7fbfdc17840b16bf20310798d0c3a56b64af 100644 (file)
@@ -79,13 +79,10 @@ void VsPinger::start()
 
     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;
index 3319d14354779415df9759fb4c2252234c71870b..a093a5323721ef7ba66f19900842a0829c4382cc 100644 (file)
@@ -39,6 +39,8 @@
 
 #include <QAbstractSocket>
 #include <QDateTime>
+#include <QList>
+#include <QNetworkRequest>
 #include <QObject>
 #include <QTimer>
 #include <QUrl>
index 6143cbd0f9f37d39b9a168a5781502f3c50ea174..1c7dbc45c86b7e9a15bdf0375aa41ed6b2bf0d72 100644 (file)
@@ -54,7 +54,11 @@ VsQuickView::VsQuickView(QWindow* parent) : QQuickView(parent)
 
 bool VsQuickView::event(QEvent* event)
 {
-    if (event->type() == QEvent::Close || event->type() == QEvent::Quit) {
+    if (event->type() == QEvent::FocusIn) {
+        emit focusGained();
+    } else if (event->type() == QEvent::FocusOut) {
+        emit focusLost();
+    } else if (event->type() == QEvent::Close || event->type() == QEvent::Quit) {
         emit windowClose();
         event->ignore();
     }
index db7810d3e1c5bac6b7919d5b4d5f1e43848de9b8..eef2d0fc95fe8e186a40c4e48fea17ebd4439f32 100644 (file)
@@ -55,6 +55,8 @@ class VsQuickView : public QQuickView
 
    signals:
     void windowClose();
+    void focusGained();
+    void focusLost();
 
    private slots:
     void closeWindow();
index 62946a085f7ca63226d09891598a6f2f330328ef..39cd62521d5e2985d00b9e621c5dd745b5f2b03d 100644 (file)
@@ -85,15 +85,17 @@ void VsWebSocket::openSocket()
     }
 
     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());
 
+    QList<QNetworkCookie> cookies;
+    QNetworkCookie authCookie = QNetworkCookie(QByteArray("auth_code"), m_token.toUtf8());
+    cookies.append(authCookie);
+    req.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
+
     if (!m_webSocket.isNull()) {
         m_webSocket->open(req);
         qDebug() << "Opened websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery);
@@ -114,7 +116,7 @@ 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;
+        qDebug() << "Websocket error: " << m_url << " " << error;
     }
     if (!m_webSocket.isNull()) {
         m_webSocket->abort();
index 34da9b46701ff1c82eb4e6091e487fb8257f7080..b5e4c47a0f074c33e284a7b50f9a68a04b3662c0 100644 (file)
@@ -38,6 +38,7 @@
 #define VSWEBSOCKET_H
 
 #include <QList>
+#include <QNetworkRequest>
 #include <QObject>
 #include <QScopedPointer>
 #include <QSslError>
diff --git a/src/vst3/JackTripVST.h b/src/vst3/JackTripVST.h
new file mode 100644 (file)
index 0000000..b7ad310
--- /dev/null
@@ -0,0 +1,59 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024-2025 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.
+*/
+//*****************************************************************
+
+// Based on the Hello World VST 3 example from Steinberg
+// https://github.com/steinbergmedia/vst3_example_plugin_hello_world
+
+#pragma once
+
+#include "pluginterfaces/base/funknown.h"
+#include "pluginterfaces/vst/vsttypes.h"
+
+#define JackTripVSTVST3Category "Fx"
+#define stringOriginalFilename  "JackTrip.vst3"
+#define stringFileDescription   "JackTrip VST3"
+#define stringCompanyName       "JackTrip Labs\0"
+#define stringLegalCopyright    "Copyright (c) 2024-2025 JackTrip Labs, Inc."
+#define stringLegalTrademarks   "VST is a trademark of Steinberg Media Technologies GmbH"
+
+//------------------------------------------------------------------------
+enum JackTripVSTParams : Steinberg::Vst::ParamID {
+    kParamGainSendId   = 100,
+    kParamMixOutputId  = 101,
+    kParamGainOutputId = 102,
+    kParamConnectedId  = 200,
+    kBypassId          = 1000
+};
+
+//------------------------------------------------------------------------
+static const Steinberg::FUID kJackTripVSTProcessorUID(0x176F9AF4, 0xA56041A1, 0x890DD021,
+                                                      0x765ABCF0);
+static const Steinberg::FUID kJackTripVSTControllerUID(0x075C3106, 0xBC524686, 0xB63544CC,
+                                                       0xF88423FF);
diff --git a/src/vst3/JackTripVSTController.cpp b/src/vst3/JackTripVSTController.cpp
new file mode 100644 (file)
index 0000000..75ce3cd
--- /dev/null
@@ -0,0 +1,235 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024-2025 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.
+*/
+//*****************************************************************
+
+// Based on the Hello World VST 3 example from Steinberg
+// https://github.com/steinbergmedia/vst3_example_plugin_hello_world
+
+#include "JackTripVSTController.h"
+
+#include "JackTripVST.h"
+#include "JackTripVSTDataBlock.h"
+#include "base/source/fstreamer.h"
+#include "pluginterfaces/base/ibstream.h"
+#include "vstgui/plugin-bindings/vst3editor.h"
+
+using namespace Steinberg;
+
+// the number of parameters used by the plugin
+constexpr int32 JackTripVSTNumParameters = 5;
+
+//------------------------------------------------------------------------
+// JackTripVSTController Implementation
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTController::initialize(FUnknown* context)
+{
+    // Here the Plug-in will be instantiated
+
+    //---do not forget to call parent ------
+    tresult result = EditControllerEx1::initialize(context);
+    if (result != kResultOk) {
+        return result;
+    }
+
+    // Here you could register some parameters
+    if (result == kResultTrue) {
+        //---Create Parameters------------
+        parameters.addParameter(STR16("Send Gain"), STR16("dB"), 199, 1.,
+                                Vst::ParameterInfo::kCanAutomate,
+                                JackTripVSTParams::kParamGainSendId, 0, STR16("Send"));
+
+        parameters.addParameter(STR16("Output Mix"), STR16("dB"), 199, 0,
+                                Vst::ParameterInfo::kCanAutomate,
+                                JackTripVSTParams::kParamMixOutputId, 0, STR16("Mix"));
+
+        parameters.addParameter(STR16("Output Gain"), STR16("dB"), 199, 1.,
+                                Vst::ParameterInfo::kCanAutomate,
+                                JackTripVSTParams::kParamGainOutputId, 0, STR16("Gain"));
+
+        parameters.addParameter(
+            STR16("Connected"), STR16("On/Off"), 1, 0, Vst::ParameterInfo::kIsReadOnly,
+            JackTripVSTParams::kParamConnectedId, 0, STR16("Connected"));
+
+        parameters.addParameter(
+            STR16("Bypass"), nullptr, 1, 0,
+            Vst::ParameterInfo::kCanAutomate | Vst::ParameterInfo::kIsBypass,
+            JackTripVSTParams::kBypassId);
+    }
+
+    return result;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTController::terminate()
+{
+    // Here the Plug-in will be de-instantiated, last possibility to remove some memory!
+
+    //---do not forget to call parent ------
+    return EditControllerEx1::terminate();
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTController::setComponentState(IBStream* state)
+{
+    // Here you get the state of the component (Processor part)
+    if (!state)
+        return kResultFalse;
+
+    IBStreamer streamer(state, kLittleEndian);
+
+    float sendGain = 1.f;
+    if (streamer.readFloat(sendGain) == false)
+        return kResultFalse;
+    setParamNormalized(JackTripVSTParams::kParamGainSendId, sendGain);
+
+    float outputMix = 1.f;
+    if (streamer.readFloat(outputMix) == false)
+        return kResultFalse;
+    setParamNormalized(JackTripVSTParams::kParamMixOutputId, outputMix);
+
+    float outputGain = 1.f;
+    if (streamer.readFloat(outputGain) == false)
+        return kResultFalse;
+    setParamNormalized(JackTripVSTParams::kParamGainOutputId, outputGain);
+
+    int8 connectedState = 0;
+    if (streamer.readInt8(connectedState) == false)
+        return kResultFalse;
+    setParamNormalized(JackTripVSTParams::kParamConnectedId, connectedState);
+
+    int32 bypassState;
+    if (streamer.readInt32(bypassState) == false)
+        return kResultFalse;
+    setParamNormalized(kBypassId, bypassState ? 1 : 0);
+
+    return kResultOk;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTController::setState([[maybe_unused]] IBStream* state)
+{
+    // Here you get the state of the controller
+
+    return kResultTrue;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTController::getState([[maybe_unused]] IBStream* state)
+{
+    // Here you are asked to deliver the state of the controller (if needed)
+    // Note: the real state of your plug-in is saved in the processor
+
+    return kResultTrue;
+}
+
+//------------------------------------------------------------------------
+int32 PLUGIN_API JackTripVSTController::getParameterCount()
+{
+    return JackTripVSTNumParameters;
+}
+
+//------------------------------------------------------------------------
+IPlugView* PLUGIN_API JackTripVSTController::createView(FIDString name)
+{
+    // Here the Host wants to open your editor (if you have one)
+    if (FIDStringsEqual(name, Vst::ViewType::kEditor)) {
+        // create your editor here and return a IPlugView ptr of it
+        auto* view = new VSTGUI::VST3Editor(this, "view", "JackTripEditor.uidesc");
+        return view;
+    }
+    return nullptr;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTController::setParamNormalized(Vst::ParamID tag,
+                                                             Vst::ParamValue value)
+{
+    // called by host to update your parameters
+    tresult result = EditControllerEx1::setParamNormalized(tag, value);
+    return result;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTController::getParamStringByValue(
+    Vst::ParamID tag, Vst::ParamValue valueNormalized, Vst::String128 string)
+{
+    // called by host to get a string for given normalized value of a specific parameter
+    // (without having to set the value!)
+    return EditControllerEx1::getParamStringByValue(tag, valueNormalized, string);
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTController::getParamValueByString(
+    Vst::ParamID tag, Vst::TChar* string, Vst::ParamValue& valueNormalized)
+{
+    // called by host to get a normalized value from a string representation of a specific
+    // parameter (without having to set the value!)
+    return EditControllerEx1::getParamValueByString(tag, string, valueNormalized);
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTController::notify(Vst::IMessage* message)
+{
+    if (mDataExchangeHandler.onMessage(message))
+        return kResultTrue;
+    return EditControllerEx1::notify(message);
+}
+
+//------------------------------------------------------------------------
+void PLUGIN_API JackTripVSTController::queueOpened(
+    [[maybe_unused]] Vst::DataExchangeUserContextID userContextID,
+    [[maybe_unused]] uint32 blockSize, [[maybe_unused]] TBool& dispatchOnBackgroundThread)
+{
+    // qDebug() << "Data Exchange Queue opened.\n";
+}
+
+//------------------------------------------------------------------------
+void PLUGIN_API JackTripVSTController::queueClosed(
+    [[maybe_unused]] Vst::DataExchangeUserContextID userContextID)
+{
+    // qDebug() << "Data Exchange Queue closed.\n";
+}
+
+//------------------------------------------------------------------------
+void PLUGIN_API JackTripVSTController::onDataExchangeBlocksReceived(
+    [[maybe_unused]] Vst::DataExchangeUserContextID userContextID, uint32 numBlocks,
+    Vst::DataExchangeBlock* blocks, [[maybe_unused]] TBool onBackgroundThread)
+{
+    for (auto index = 0u; index < numBlocks; ++index) {
+        auto dataBlock = toDataBlock(blocks[index]);
+        beginEdit(JackTripVSTParams::kParamConnectedId);
+        Vst::ParamValue connectedState = dataBlock->connectedState ? 1 : 0;
+        if (setParamNormalized(JackTripVSTParams::kParamConnectedId, connectedState)
+            == kResultOk) {
+            performEdit(JackTripVSTParams::kParamConnectedId,
+                        getParamNormalized(JackTripVSTParams::kParamConnectedId));
+        }
+        endEdit(JackTripVSTParams::kParamConnectedId);
+    }
+}
diff --git a/src/vst3/JackTripVSTController.h b/src/vst3/JackTripVSTController.h
new file mode 100644 (file)
index 0000000..9ce544f
--- /dev/null
@@ -0,0 +1,97 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024-2025 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.
+*/
+//*****************************************************************
+
+// Based on the Hello World VST 3 example from Steinberg
+// https://github.com/steinbergmedia/vst3_example_plugin_hello_world
+
+#pragma once
+
+#include "public.sdk/source/vst/utility/dataexchange.h"
+#include "public.sdk/source/vst/vsteditcontroller.h"
+
+//------------------------------------------------------------------------
+//  JackTripVSTController
+//------------------------------------------------------------------------
+class JackTripVSTController
+    : public Steinberg::Vst::EditControllerEx1
+    , public Steinberg::Vst::IDataExchangeReceiver
+{
+   public:
+    //------------------------------------------------------------------------
+    JackTripVSTController()                = default;
+    ~JackTripVSTController() SMTG_OVERRIDE = default;
+
+    // Create function
+    static Steinberg::FUnknown* createInstance(void* /*context*/)
+    {
+        return (Steinberg::Vst::IEditController*)new JackTripVSTController;
+    }
+
+    // IPluginBase
+    Steinberg::tresult PLUGIN_API initialize(Steinberg::FUnknown* context) SMTG_OVERRIDE;
+    Steinberg::tresult PLUGIN_API terminate() SMTG_OVERRIDE;
+
+    // EditController
+    Steinberg::tresult PLUGIN_API setComponentState(Steinberg::IBStream* state)
+        SMTG_OVERRIDE;
+    Steinberg::IPlugView* PLUGIN_API createView(Steinberg::FIDString name) SMTG_OVERRIDE;
+    Steinberg::tresult PLUGIN_API setState(Steinberg::IBStream* state) SMTG_OVERRIDE;
+    Steinberg::tresult PLUGIN_API getState(Steinberg::IBStream* state) SMTG_OVERRIDE;
+    Steinberg::int32 PLUGIN_API getParameterCount() SMTG_OVERRIDE;
+    Steinberg::tresult PLUGIN_API setParamNormalized(
+        Steinberg::Vst::ParamID tag, Steinberg::Vst::ParamValue value) SMTG_OVERRIDE;
+    Steinberg::tresult PLUGIN_API getParamStringByValue(
+        Steinberg::Vst::ParamID tag, Steinberg::Vst::ParamValue valueNormalized,
+        Steinberg::Vst::String128 string) SMTG_OVERRIDE;
+    Steinberg::tresult PLUGIN_API
+    getParamValueByString(Steinberg::Vst::ParamID tag, Steinberg::Vst::TChar* string,
+                          Steinberg::Vst::ParamValue& valueNormalized) SMTG_OVERRIDE;
+
+    // IDataExchangeReceiver
+    Steinberg::tresult PLUGIN_API notify(Steinberg::Vst::IMessage* message) override;
+    void PLUGIN_API queueOpened(Steinberg::Vst::DataExchangeUserContextID userContextID,
+                                Steinberg::uint32 blockSize,
+                                Steinberg::TBool& dispatchOnBackgroundThread) override;
+    void PLUGIN_API
+    queueClosed(Steinberg::Vst::DataExchangeUserContextID userContextID) override;
+    void PLUGIN_API onDataExchangeBlocksReceived(
+        Steinberg::Vst::DataExchangeUserContextID userContextID,
+        Steinberg::uint32 numBlocks, Steinberg::Vst::DataExchangeBlock* blocks,
+        Steinberg::TBool onBackgroundThread) override;
+
+    //---Interface---------
+    DEFINE_INTERFACES
+    DEF_INTERFACE(Steinberg::Vst::IDataExchangeReceiver)
+    END_DEFINE_INTERFACES(EditController)
+    DELEGATE_REFCOUNT(EditController)
+
+   private:
+    Steinberg::Vst::DataExchangeReceiverHandler mDataExchangeHandler{this};
+};
diff --git a/src/vst3/JackTripVSTDataBlock.h b/src/vst3/JackTripVSTDataBlock.h
new file mode 100644 (file)
index 0000000..a4d32ce
--- /dev/null
@@ -0,0 +1,51 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2025 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.
+*/
+//*****************************************************************
+
+// Based on the VST3 SDK Data Exchange tutorial at
+// https://steinbergmedia.github.io/vst3_dev_portal/pages/Tutorials/Data+Exchange.html
+
+#pragma once
+
+#include <cstdint>
+
+#include "public.sdk/source/vst/utility/dataexchange.h"
+
+// this is currently overkill for a bool, but we can use it for other things
+// such as volume meters in the future
+struct DataBlock {
+    bool connectedState;
+};
+
+inline DataBlock* toDataBlock(const Steinberg::Vst::DataExchangeBlock& block)
+{
+    if (block.blockID != Steinberg::Vst::InvalidDataExchangeBlockID)
+        return reinterpret_cast<DataBlock*>(block.data);
+    return nullptr;
+}
diff --git a/src/vst3/JackTripVSTEntry.cpp b/src/vst3/JackTripVSTEntry.cpp
new file mode 100644 (file)
index 0000000..bbff0c4
--- /dev/null
@@ -0,0 +1,87 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024-2025 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.
+*/
+//*****************************************************************
+
+// Based on the Hello World VST 3 example from Steinberg
+// https://github.com/steinbergmedia/vst3_example_plugin_hello_world
+
+#include "../jacktrip_globals.h"
+#include "JackTripVST.h"
+#include "JackTripVSTController.h"
+#include "JackTripVSTProcessor.h"
+#include "public.sdk/source/main/pluginfactory.h"
+
+#define stringPluginName "JackTrip Audio Bridge"
+
+using namespace Steinberg::Vst;
+using namespace Steinberg;
+
+//------------------------------------------------------------------------
+//  VST Plug-in Entry
+//------------------------------------------------------------------------
+// Windows: do not forget to include a .def file in your project to export
+// GetPluginFactory function!
+//------------------------------------------------------------------------
+
+BEGIN_FACTORY_DEF("JackTrip Labs", "https://www.jacktrip.com",
+                  "mailto:support@jacktrip.com")
+
+//---First Plug-in included in this factory-------
+// its kVstAudioEffectClass component
+DEF_CLASS2(INLINE_UID_FROM_FUID(kJackTripVSTProcessorUID),
+           PClassInfo::kManyInstances,  // cardinality
+           kVstAudioEffectClass,        // the component category (do not changed this)
+           stringPluginName,            // here the Plug-in name (to be changed)
+           Vst::kDistributable,         // means that component and controller could be
+                                        // distributed on different computers
+           JackTripVSTVST3Category,     // Subcategory for this Plug-in (to be changed)
+           gVersion,                    // Plug-in version (to be changed)
+           kVstVersionString,  // the VST 3 SDK version (do not changed this, use always
+                               // this define)
+           JackTripVSTProcessor::createInstance)  // function pointer called when this
+                                                  // component should be instantiated
+
+// its kVstComponentControllerClass component
+DEF_CLASS2(INLINE_UID_FROM_FUID(kJackTripVSTControllerUID),
+           PClassInfo::kManyInstances,    // cardinality
+           kVstComponentControllerClass,  // the Controller category (do not changed this)
+           stringPluginName
+           "",                 // controller name (could be the same than component name)
+           0,                  // not used here
+           "",                 // not used here
+           gVersion,           // Plug-in version (to be changed)
+           kVstVersionString,  // the VST 3 SDK version (do not changed this, use always
+                               // this define)
+           JackTripVSTController::createInstance)  // function pointer called when this
+                                                   // component should be instantiated
+
+//----for others Plug-ins contained in this factory, put like for the first Plug-in
+// different DEF_CLASS2---
+
+END_FACTORY
diff --git a/src/vst3/JackTripVSTProcessor.cpp b/src/vst3/JackTripVSTProcessor.cpp
new file mode 100644 (file)
index 0000000..5659b28
--- /dev/null
@@ -0,0 +1,593 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024-2025 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.
+*/
+//*****************************************************************
+
+// Based on the Hello World VST 3 example from Steinberg
+// https://github.com/steinbergmedia/vst3_example_plugin_hello_world
+
+#include "JackTripVSTProcessor.h"
+
+#include "../AudioSocket.h"
+#include "JackTripVST.h"
+#include "JackTripVSTDataBlock.h"
+#include "base/source/fstreamer.h"
+#include "pluginterfaces/vst/ivstparameterchanges.h"
+
+using namespace std;
+using namespace Steinberg;
+
+// uncomment to generate log file, for debugging purposes
+// #define JACKTRIP_VST_LOG
+
+#ifdef JACKTRIP_VST_LOG
+#if defined(_WIN32)
+#define JACKTRIP_VST_LOG_PATH "c:/JackTripTemp"
+#define JACKTRIP_VST_LOG_FILE "c:/JackTripTemp/vst.log"
+#else
+#define JACKTRIP_VST_LOG_PATH "/tmp/jacktrip"
+#define JACKTRIP_VST_LOG_FILE "/tmp/jacktrip/vst.log"
+#endif
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+
+static ofstream kLogFile;
+
+void qtMessageHandler([[maybe_unused]] QtMsgType type,
+                      [[maybe_unused]] const QMessageLogContext& context,
+                      const QString& msg)
+{
+    kLogFile << msg.toStdString() << endl;
+}
+#endif
+
+// any multiplier less than this is considered to be silent
+constexpr double kSilentMul = 0.0000001;
+
+static QCoreApplication* sQtAppPtr = nullptr;
+
+static QCoreApplication* getQtAppPtr()
+{
+    if (sQtAppPtr == nullptr) {
+        sQtAppPtr = QCoreApplication::instance();
+        if (sQtAppPtr == nullptr) {
+            int argc  = 0;
+            sQtAppPtr = new QCoreApplication(argc, nullptr);
+            sQtAppPtr->setAttribute(Qt::AA_NativeWindows);
+        }
+    }
+    return sQtAppPtr;
+}
+
+//------------------------------------------------------------------------
+// JackTripVSTProcessor
+//------------------------------------------------------------------------
+JackTripVSTProcessor::JackTripVSTProcessor()
+{
+    //--- set the wanted controller for our processor
+    setControllerClass(kJackTripVSTControllerUID);
+    mCurrentExchangeBlock =
+        Vst::DataExchangeBlock{nullptr, 0, Vst::InvalidDataExchangeBlockID};
+}
+
+//------------------------------------------------------------------------
+JackTripVSTProcessor::~JackTripVSTProcessor() {}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::initialize(FUnknown* context)
+{
+    // Here the Plug-in will be instantiated
+
+    //---always initialize the parent-------
+    tresult result = AudioEffect::initialize(context);
+    // if everything Ok, continue
+    if (result != kResultOk) {
+        return result;
+    }
+
+    //--- create Audio IO ------
+    addAudioInput(STR16("Stereo In"), Vst::SpeakerArr::kStereo);
+    addAudioOutput(STR16("Stereo Out"), Vst::SpeakerArr::kStereo);
+
+    getQtAppPtr();
+
+    mInputBuffer  = new float*[AudioSocketNumChannels];
+    mOutputBuffer = new float*[AudioSocketNumChannels];
+    for (int i = 0; i < AudioSocketNumChannels; i++) {
+        mInputBuffer[i]  = new float[AudioSocketMaxSamplesPerBlock];
+        mOutputBuffer[i] = new float[AudioSocketMaxSamplesPerBlock];
+    }
+
+#ifdef JACKTRIP_VST_LOG
+    if (!filesystem::is_directory(JACKTRIP_VST_LOG_PATH)) {
+        if (!filesystem::create_directory(JACKTRIP_VST_LOG_PATH)) {
+            qDebug() << "Failed to create VST log directory: " << JACKTRIP_VST_LOG_PATH;
+        }
+    }
+    kLogFile.open(JACKTRIP_VST_LOG_FILE, ios::app);
+    if (kLogFile.is_open()) {
+        kLogFile << "JackTrip VST initialized" << endl;
+        kLogFile.flush();
+        cout.rdbuf(kLogFile.rdbuf());
+        cerr.rdbuf(kLogFile.rdbuf());
+    } else {
+        qDebug() << "Failed to open VST log file: " << JACKTRIP_VST_LOG_FILE;
+    }
+    qInstallMessageHandler(qtMessageHandler);
+#endif
+
+    qDebug() << "JackTrip VST initialized";
+
+    return kResultOk;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::terminate()
+{
+    mSocketPtr.reset();
+
+    for (int i = 0; i < AudioSocketNumChannels; i++) {
+        delete[] mInputBuffer[i];
+        delete[] mOutputBuffer[i];
+    }
+    delete[] mInputBuffer;
+    delete[] mOutputBuffer;
+
+    qDebug() << "JackTrip VST terminated";
+
+#ifdef JACKTRIP_VST_LOG
+    kLogFile.close();
+#endif
+
+    //---do not forget to call parent ------
+    return AudioEffect::terminate();
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::connect(Vst::IConnectionPoint* other)
+{
+    auto result = Vst::AudioEffect::connect(other);
+    if (result == kResultTrue) {
+        auto configCallback = [](Vst::DataExchangeHandler::Config& config,
+                                 [[maybe_unused]] const Vst::ProcessSetup& setup) {
+            config.blockSize     = sizeof(DataBlock);
+            config.numBlocks     = 2;  // max number of pending blocks allowed
+            config.alignment     = 32;
+            config.userContextID = 0;
+            return true;
+        };
+        mDataExchangePtr.reset(new Vst::DataExchangeHandler(this, configCallback));
+        mDataExchangePtr->onConnect(other, getHostContext());
+    }
+    return result;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::disconnect(Vst::IConnectionPoint* other)
+{
+    if (!mDataExchangePtr.isNull()) {
+        mDataExchangePtr->onDisconnect(other);
+        mDataExchangePtr.reset();
+    }
+    return AudioEffect::disconnect(other);
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API
+JackTripVSTProcessor::setBusArrangements(Vst::SpeakerArrangement* inputs, int32 numIns,
+                                         Vst::SpeakerArrangement* outputs, int32 numOuts)
+{
+    // based on again example from sdk (support 1->1 or 2->2)
+    if (numIns == 1 && numOuts == 1) {
+        // the host wants Mono => Mono (or 1 channel -> 1 channel)
+        if (Vst::SpeakerArr::getChannelCount(inputs[0]) == 1
+            && Vst::SpeakerArr::getChannelCount(outputs[0]) == 1) {
+            auto* bus = FCast<Steinberg::Vst::AudioBus>(audioInputs.at(0));
+            if (bus) {
+                // check if we are Mono => Mono, if not we need to recreate the busses
+                if (bus->getArrangement() != inputs[0]) {
+                    getAudioInput(0)->setArrangement(inputs[0]);
+                    getAudioInput(0)->setName(STR16("Mono In"));
+                    getAudioOutput(0)->setArrangement(outputs[0]);
+                    getAudioOutput(0)->setName(STR16("Mono Out"));
+                }
+                return kResultOk;
+            }
+        } else {
+            // the host wants something else than Mono => Mono,
+            // in this case we are always Stereo => Stereo
+            auto* bus = FCast<Steinberg::Vst::AudioBus>(audioInputs.at(0));
+            if (bus) {
+                tresult result = kResultFalse;
+                // the host wants 2->2 (could be LsRs -> LsRs)
+                if (Vst::SpeakerArr::getChannelCount(inputs[0]) == 2
+                    && Vst::SpeakerArr::getChannelCount(outputs[0]) == 2) {
+                    getAudioInput(0)->setArrangement(inputs[0]);
+                    getAudioInput(0)->setName(STR16("Stereo In"));
+                    getAudioOutput(0)->setArrangement(outputs[0]);
+                    getAudioOutput(0)->setName(STR16("Stereo Out"));
+                    result = kResultTrue;
+                } else if (bus->getArrangement() != Vst::SpeakerArr::kStereo) {
+                    // the host want something different than 1->1 or 2->2 : in this case
+                    // we want stereo
+                    getAudioInput(0)->setArrangement(Vst::SpeakerArr::kStereo);
+                    getAudioInput(0)->setName(STR16("Stereo In"));
+                    getAudioOutput(0)->setArrangement(Vst::SpeakerArr::kStereo);
+                    getAudioOutput(0)->setName(STR16("Stereo Out"));
+                    result = kResultFalse;
+                }
+                return result;
+            }
+        }
+    }
+    return kResultFalse;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::setActive(TBool state)
+{
+    if (state) {
+        // sanity check to ensure these were initialized by setupProcessing()
+        if (mSampleRate == 0 || mBufferSize == 0) {
+            return kResultFalse;
+        }
+        // create a audio new socket
+        if (mSocketPtr.isNull()) {
+            // not yet initialized
+            mSocketPtr.reset(new AudioSocket(true));
+            // automatically retry to establish connection
+            mSocketPtr->setRetryConnection(true);
+            mSocketPtr->connect(mSampleRate, mBufferSize);
+        }
+        // activate data exchange API
+        if (!mDataExchangePtr.isNull()) {
+            mDataExchangePtr->onActivate(processSetup);
+        }
+    } else {
+        // disconnect from remote when inactive
+        mSocketPtr.reset();
+        // deactivate data exchange API
+        if (!mDataExchangePtr.isNull()) {
+            mDataExchangePtr->onDeactivate();
+        }
+    }
+
+    qDebug() << "JackTrip VST setActive(" << int(state) << ")";
+
+    //--- called when the Plug-in is enable/disable (On/Off) -----
+    return AudioEffect::setActive(state);
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::setProcessing(TBool state)
+{
+    qDebug() << "JackTrip VST setProcessing(" << int(state) << ")";
+    return AudioEffect::setProcessing(state);
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::process(Vst::ProcessData& data)
+{
+    // sanity check; should never happen
+    if (mSocketPtr.isNull())
+        return kResultFalse;
+
+    //--- Read inputs parameter changes-----------
+    if (data.inputParameterChanges) {
+        int32 numParamsChanged = data.inputParameterChanges->getParameterCount();
+        for (int32 index = 0; index < numParamsChanged; index++) {
+            Vst::IParamValueQueue* paramQueue =
+                data.inputParameterChanges->getParameterData(index);
+            if (paramQueue) {
+                Vst::ParamValue value;
+                int32 sampleOffset;
+                int32 numPoints = paramQueue->getPointCount();
+                switch (paramQueue->getParameterId()) {
+                case JackTripVSTParams::kParamGainSendId:
+                    if (paramQueue->getPoint(numPoints - 1, sampleOffset, value)
+                        == kResultTrue)
+                        mSendGain = value;
+                    break;
+                case JackTripVSTParams::kParamMixOutputId:
+                    if (paramQueue->getPoint(numPoints - 1, sampleOffset, value)
+                        == kResultTrue)
+                        mOutputMix = value;
+                    break;
+                case JackTripVSTParams::kParamGainOutputId:
+                    if (paramQueue->getPoint(numPoints - 1, sampleOffset, value)
+                        == kResultTrue)
+                        mOutputGain = value;
+                    break;
+                case JackTripVSTParams::kParamConnectedId:
+                    if (paramQueue->getPoint(numPoints - 1, sampleOffset, value)
+                        == kResultTrue)
+                        mConnected = value > 0;
+                    break;
+                case JackTripVSTParams::kBypassId:
+                    if (paramQueue->getPoint(numPoints - 1, sampleOffset, value)
+                        == kResultTrue)
+                        mBypass = value > 0;
+                    break;
+                }
+            }
+        }
+        if (numParamsChanged > 0)
+            updateVolumeMultipliers();
+    }
+
+#if 0
+    if (mLogFile.is_open()) {
+        mLogFile << "JackTrip VST process: inputs=" << data.numInputs
+                 << ", outputs=" << data.numOutputs
+                 << ", samples=" << data.numSamples
+                 << endl;
+    }
+#endif
+
+    // handle connection state change
+    if (mConnected != mSocketPtr->isConnected()) {
+        // try both methods because some hosts only support one or the other.
+        // first try to use data output parameters, if available.
+        bool updatedConnectedState = false;
+        if (data.outputParameterChanges) {
+            int32 index = 0;
+            Steinberg::Vst::IParamValueQueue* paramQueue =
+                data.outputParameterChanges->addParameterData(kParamConnectedId, index);
+            if (paramQueue) {
+                int8 connectedState = mSocketPtr->isConnected() ? 1 : 0;
+                int32 index2        = 0;
+                if (paramQueue->addPoint(0, connectedState, index2) == kResultOk) {
+                    updatedConnectedState = true;
+                }
+            }
+        }
+        // if unsuccessful, try to use data exchange API
+        if (!updatedConnectedState && !mDataExchangePtr.isNull()) {
+            if (mCurrentExchangeBlock.blockID == Vst::InvalidDataExchangeBlockID) {
+                acquireNewExchangeBlock();
+            }
+            if (auto block = toDataBlock(mCurrentExchangeBlock)) {
+                block->connectedState = mSocketPtr->isConnected();
+                if (mDataExchangePtr->sendCurrentBlock()) {
+                    updatedConnectedState = true;
+                }
+                // we need to acquire a new block before the current one will be sent
+                acquireNewExchangeBlock();
+            }
+        }
+        if (updatedConnectedState) {
+            // we can update our state after successfully deliver the change
+            mConnected = mSocketPtr->isConnected();
+        }
+    }
+
+    //--- Process Audio---------------------
+    //--- ----------------------------------
+    if (data.numInputs == 0 || data.numOutputs == 0) {
+        // nothing to do
+        return kResultOk;
+    }
+
+    if (data.numSamples <= 0) {
+        // nothing to do
+        return kResultOk;
+    }
+
+    if (data.numSamples > AudioSocketMaxSamplesPerBlock) {
+        // just a sanity check; shouldn't happen
+        data.numSamples = AudioSocketMaxSamplesPerBlock;
+    }
+
+    if (mBypass) {
+        // copy input to output
+        for (int i = 0; i < data.inputs[0].numChannels && i < data.outputs[0].numChannels;
+             i++) {
+            memcpy(data.outputs[0].channelBuffers32[i],
+                   data.inputs[0].channelBuffers32[i],
+                   data.numSamples * sizeof(Vst::Sample32));
+        }
+        data.outputs[0].silenceFlags = data.inputs[0].silenceFlags;
+        return kResultOk;
+    }
+
+    // clear buffers
+    for (int i = 0; i < AudioSocketNumChannels; i++) {
+        memset(mInputBuffer[i], 0, data.numSamples * sizeof(float));
+        memset(mOutputBuffer[i], 0, data.numSamples * sizeof(float));
+    }
+
+    // copy input to buffer
+    if (mSendMul >= kSilentMul) {
+        uint64 isSilentFlag = 1;
+        int channelsIn      = min(data.inputs[0].numChannels, AudioSocketNumChannels);
+        for (int i = 0; i < channelsIn; i++) {
+            bool isSilent = isSilentFlag & data.inputs[0].silenceFlags;
+            isSilentFlag <<= 1;
+            if (isSilent)
+                continue;
+            Vst::Sample32* inBuffer = data.inputs[0].channelBuffers32[i];
+            for (int j = 0; j < data.numSamples; j++) {
+                mInputBuffer[i][j] = inBuffer[j] * mSendMul;
+            }
+        }
+    }
+
+    // send to audio socket
+    mSocketPtr->compute(data.numSamples, mInputBuffer, mOutputBuffer);
+
+    // copy buffer to output
+    for (int i = 0; i < data.outputs[0].numChannels; i++) {
+        bool silent = true;
+        memset(data.outputs[0].channelBuffers32[i], 0,
+               data.numSamples * sizeof(Vst::Sample32));
+        if (mPassMul >= kSilentMul || mRecvMul >= kSilentMul) {
+            Vst::Sample32* outBuffer = data.outputs[0].channelBuffers32[i];
+            for (int j = 0; j < data.numSamples; j++) {
+                if (i < AudioSocketNumChannels && mRecvMul >= kSilentMul) {
+                    outBuffer[j] = mOutputBuffer[i][j] * mRecvMul;
+                }
+                if (i < data.inputs[0].numChannels && mPassMul >= kSilentMul) {
+                    outBuffer[j] += data.inputs[0].channelBuffers32[i][j] * mPassMul;
+                }
+                if (silent && outBuffer[j] != 0) {
+                    silent = false;
+                }
+            }
+        }
+        if (silent) {
+            data.outputs[0].silenceFlags |= static_cast<Steinberg::uint64>(1) << i;
+        }
+    }
+
+    return kResultOk;
+}
+
+//------------------------------------------------------------------------
+float JackTripVSTProcessor::gainToVol(double gain)
+{
+    // handle min and max
+    if (gain < kSilentMul)
+        return 0;
+    if (gain > 0.9999999)
+        return 1.0;
+    // simple logarithmic conversion
+    return exp(log(1000) * gain) / 1000.0;
+}
+
+//------------------------------------------------------------------------
+void JackTripVSTProcessor::updateVolumeMultipliers()
+{
+    // convert [0-1.0] gain (dB) values into [0-1.0] volume multiplers
+    float outMul = gainToVol(mOutputGain);
+    mSendMul     = gainToVol(mSendGain);
+    mRecvMul     = mOutputMix * outMul;
+    mPassMul     = (1.0f - mOutputMix) * outMul;
+
+    qDebug() << "JackTrip VST send =" << mSendMul << "(" << mSendGain
+             << "), out =" << outMul << "(" << mOutputGain << "), mix =" << mOutputMix
+             << ", recv =" << mRecvMul << ", pass =" << mPassMul;
+}
+
+//------------------------------------------------------------------------
+void JackTripVSTProcessor::acquireNewExchangeBlock()
+{
+    mCurrentExchangeBlock = mDataExchangePtr->getCurrentOrNewBlock();
+    if (auto block = toDataBlock(mCurrentExchangeBlock)) {
+        block->connectedState = false;  // default
+    }
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::setupProcessing(Vst::ProcessSetup& newSetup)
+{
+    mSampleRate = newSetup.sampleRate;
+    mBufferSize = static_cast<int>(newSetup.maxSamplesPerBlock);
+
+    qDebug() << "JackTrip VST setupProcessing: mSampleRate=" << mSampleRate
+             << ", mbufferSize=" << mBufferSize;
+
+    //--- called before any processing ----
+    return AudioEffect::setupProcessing(newSetup);
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::canProcessSampleSize(int32 symbolicSampleSize)
+{
+    // by default kSample32 is supported
+    if (symbolicSampleSize == Vst::kSample32)
+        return kResultTrue;
+
+    // disable the following comment if your processing support kSample64
+    /* if (symbolicSampleSize == Vst::kSample64)
+        return kResultTrue; */
+
+    return kResultFalse;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::setState(IBStream* state)
+{
+    if (!state)
+        return kResultFalse;
+
+    // called when we load a preset or project, the model has to be reloaded
+
+    IBStreamer streamer(state, kLittleEndian);
+
+    float sendGain = 1.f;
+    if (streamer.readFloat(sendGain) == false)
+        return kResultFalse;
+
+    float outputMix = 1.f;
+    if (streamer.readFloat(outputMix) == false)
+        return kResultFalse;
+
+    float outputGain = 1.f;
+    if (streamer.readFloat(outputGain) == false)
+        return kResultFalse;
+
+    int8 connectedState = 0;
+    if (streamer.readInt8(connectedState) == false)
+        return kResultFalse;
+
+    int32 bypassState = 0;
+    if (streamer.readInt32(bypassState) == false)
+        return kResultFalse;
+
+    mSendGain   = sendGain;
+    mOutputMix  = outputMix;
+    mOutputGain = outputGain;
+    mConnected  = connectedState > 0;
+    mBypass     = bypassState > 0;
+
+    updateVolumeMultipliers();
+
+    return kResultOk;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::getState(IBStream* state)
+{
+    // here we need to save the model (preset or project)
+
+    float sendGain      = mSendGain;
+    float outputMix     = mOutputMix;
+    float outputGain    = mOutputGain;
+    int8 connectedState = mConnected ? 1 : 0;
+    int32 bypassState   = mBypass ? 1 : 0;
+
+    IBStreamer streamer(state, kLittleEndian);
+    streamer.writeFloat(sendGain);
+    streamer.writeFloat(outputMix);
+    streamer.writeFloat(outputGain);
+    streamer.writeInt8(connectedState);
+    streamer.writeInt32(bypassState);
+
+    return kResultOk;
+}
diff --git a/src/vst3/JackTripVSTProcessor.h b/src/vst3/JackTripVSTProcessor.h
new file mode 100644 (file)
index 0000000..a61bf2c
--- /dev/null
@@ -0,0 +1,128 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024-2025 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.
+*/
+//*****************************************************************
+
+// Based on the Hello World VST 3 example from Steinberg
+// https://github.com/steinbergmedia/vst3_example_plugin_hello_world
+
+#pragma once
+
+#include <QCoreApplication>
+#include <QScopedPointer>
+#include <QThread>
+
+#include "public.sdk/source/vst/utility/dataexchange.h"
+#include "public.sdk/source/vst/vstaudioeffect.h"
+
+class AudioSocket;
+
+//------------------------------------------------------------------------
+//  JackTripVSTProcessor
+//------------------------------------------------------------------------
+class JackTripVSTProcessor : public Steinberg::Vst::AudioEffect
+{
+   public:
+    JackTripVSTProcessor();
+    ~JackTripVSTProcessor() SMTG_OVERRIDE;
+
+    // Create function
+    static Steinberg::FUnknown* createInstance(void* /*context*/)
+    {
+        return (Steinberg::Vst::IAudioProcessor*)new JackTripVSTProcessor;
+    }
+
+    //--- ---------------------------------------------------------------------
+    // AudioEffect overrides:
+    //--- ---------------------------------------------------------------------
+    /** Called at first after constructor */
+    Steinberg::tresult PLUGIN_API initialize(Steinberg::FUnknown* context) SMTG_OVERRIDE;
+
+    /** Called at the end before destructor */
+    Steinberg::tresult PLUGIN_API terminate() SMTG_OVERRIDE;
+
+    /** Called to connect data exchange API */
+    Steinberg::tresult PLUGIN_API
+    connect(Steinberg::Vst::IConnectionPoint* other) override;
+
+    /** Called to disconnect data exchange API */
+    Steinberg::tresult PLUGIN_API
+    disconnect(Steinberg::Vst::IConnectionPoint* other) override;
+
+    /** Called to set bus arrangements */
+    Steinberg::tresult PLUGIN_API setBusArrangements(
+        Steinberg::Vst::SpeakerArrangement* inputs, Steinberg::int32 numIns,
+        Steinberg::Vst::SpeakerArrangement* outputs,
+        Steinberg::int32 numOuts) SMTG_OVERRIDE;
+
+    /** Switch the Plug-in on/off */
+    Steinberg::tresult PLUGIN_API setActive(Steinberg::TBool state) SMTG_OVERRIDE;
+
+    /** Called by audio thread immediately before processing starts, and after it ends */
+    Steinberg::tresult PLUGIN_API setProcessing(Steinberg::TBool state) SMTG_OVERRIDE;
+
+    /** Will be called before any process call */
+    Steinberg::tresult PLUGIN_API setupProcessing(Steinberg::Vst::ProcessSetup& newSetup)
+        SMTG_OVERRIDE;
+
+    /** Asks if a given sample size is supported see SymbolicSampleSizes. */
+    Steinberg::tresult PLUGIN_API
+    canProcessSampleSize(Steinberg::int32 symbolicSampleSize) SMTG_OVERRIDE;
+
+    /** Here we go...the process call */
+    Steinberg::tresult PLUGIN_API process(Steinberg::Vst::ProcessData& data)
+        SMTG_OVERRIDE;
+
+    /** For persistence */
+    Steinberg::tresult PLUGIN_API setState(Steinberg::IBStream* state) SMTG_OVERRIDE;
+    Steinberg::tresult PLUGIN_API getState(Steinberg::IBStream* state) SMTG_OVERRIDE;
+
+    //------------------------------------------------------------------------
+   protected:
+    static float gainToVol(double gain);
+    void updateVolumeMultipliers();
+    void acquireNewExchangeBlock();
+
+    Steinberg::Vst::ParamValue mSendGain   = 1.f;
+    Steinberg::Vst::ParamValue mOutputMix  = 0;
+    Steinberg::Vst::ParamValue mOutputGain = 1.f;
+    float mSendMul                         = 1.f;
+    float mRecvMul                         = 0;
+    float mPassMul                         = 1.f;
+    bool mConnected                        = false;
+    bool mBypass                           = false;
+
+   private:
+    QScopedPointer<AudioSocket> mSocketPtr;
+    QScopedPointer<Steinberg::Vst::DataExchangeHandler> mDataExchangePtr;
+    Steinberg::Vst::DataExchangeBlock mCurrentExchangeBlock;
+    float** mInputBuffer;
+    float** mOutputBuffer;
+    Steinberg::Vst::SampleRate mSampleRate = 0;
+    int mBufferSize                        = 0;
+};
diff --git a/src/vst3/resources/Dual_LED.png b/src/vst3/resources/Dual_LED.png
new file mode 100644 (file)
index 0000000..45458f2
Binary files /dev/null and b/src/vst3/resources/Dual_LED.png differ
diff --git a/src/vst3/resources/JackTripEditor.uidesc b/src/vst3/resources/JackTripEditor.uidesc
new file mode 100644 (file)
index 0000000..a1e5a3a
--- /dev/null
@@ -0,0 +1,373 @@
+{
+       "vstgui-ui-description": {
+               "version": "1",
+               "bitmaps": {
+                       "background": {
+                               "path": "background.png"
+                       },
+                       "background_2x": {
+                               "path": "background_2x.png",
+                               "scale-factor": "2"
+                       },
+                       "Dual_LED": {
+                               "path": "Dual_LED.png"
+                       },
+                       "Sercan_Moog_Knob": {
+                               "path": "Sercan_Moog_Knob.png"
+                       }
+               },
+               "fonts": {},
+               "colors": {},
+               "gradients": {},
+               "control-tags": {
+                       "Bypass": "1000",
+                       "Connected": "200",
+                       "Output Gain": "102",
+                       "Output Mix": "101",
+                       "Send": "100"
+               },
+               "custom": {
+                       "FocusDrawing": {},
+                       "VST3Editor": {
+                               "Path": "JackTripEditor.uidesc"
+                       },
+                       "UIGridController": {
+                               "Grids": "1x 1,5x 5,10x 10,12x 12,15x 15",
+                               "Size": "10, 10"
+                       },
+                       "UITemplateController": {
+                               "SelectedTemplate": "view"
+                       },
+                       "UIEditController": {
+                               "EditViewScale": "1",
+                               "EditorSize": "0, 0, 1391, 803",
+                               "SplitViewSize_0_0": "0.5894465894465894528764238202711567282677",
+                               "SplitViewSize_0_1": "0.3848133848133848400330236927402438595891",
+                               "SplitViewSize_1_0": "0.5109395109395109546568392033805139362812",
+                               "SplitViewSize_1_1": "0.4851994851994851920551354851340875029564",
+                               "SplitViewSize_2_0": "0.7239396117900790406096689366677310317755",
+                               "SplitViewSize_2_1": "0.2724658519051042504521831233432749286294",
+                               "TabSwitchValue": "0",
+                               "Version": "1"
+                       },
+                       "UIAttributesController": {},
+                       "UIViewCreatorDataSource": {
+                               "SelectedRow": "9"
+                       },
+                       "UIBitmapsDataSource": {
+                               "SelectedRow": "3"
+                       },
+                       "UITagsDataSource": {
+                               "SelectedRow": "1"
+                       },
+                       "UIFontsDataSource": {
+                               "SelectedRow": "-1"
+                       },
+                       "UIGradientsDataSource": {
+                               "SelectedRow": "-1"
+                       },
+                       "UIColorsDataSource": {
+                               "SelectedRow": "-1"
+                       }
+               },
+               "templates": {
+                       "view": {
+                               "attributes": {
+                                       "background-color": "~ BlackCColor",
+                                       "background-color-draw-style": "filled and stroked",
+                                       "bitmap": "background",
+                                       "class": "CViewContainer",
+                                       "mouse-enabled": "true",
+                                       "opacity": "1",
+                                       "origin": "0, 0",
+                                       "size": "400, 200",
+                                       "transparent": "false",
+                                       "wants-focus": "false"
+                               },
+                               "children": {
+                                       "COnOffButton": {
+                                               "attributes": {
+                                                       "bitmap": "Dual_LED",
+                                                       "class": "COnOffButton",
+                                                       "control-tag": "Connected",
+                                                       "opacity": "1",
+                                                       "origin": "335, 8",
+                                                       "size": "62, 62",
+                                                       "tooltip": "Green when connected to JackTrip",
+                                                       "transparent": "false",
+                                                       "uidesc-label": "Connected",
+                                                       "wants-focus": "false",
+                                                       "wheel-inc-value": "0.1"
+                                               }
+                                       },
+                                       "CViewContainer": {
+                                               "attributes": {
+                                                       "background-color": "~ BlackCColor",
+                                                       "background-color-draw-style": "filled and stroked",
+                                                       "class": "CViewContainer",
+                                                       "mouse-enabled": "true",
+                                                       "opacity": "1",
+                                                       "origin": "70, 80",
+                                                       "size": "72, 112",
+                                                       "transparent": "true",
+                                                       "uidesc-label": "Send",
+                                                       "wants-focus": "false"
+                                               },
+                                               "children": {
+                                                       "CAnimKnob": {
+                                                               "attributes": {
+                                                                       "angle-range": "270",
+                                                                       "angle-start": "135",
+                                                                       "bitmap": "Sercan_Moog_Knob",
+                                                                       "class": "CAnimKnob",
+                                                                       "control-tag": "Send",
+                                                                       "height-of-one-image": "72",
+                                                                       "inverse-bitmap": "false",
+                                                                       "knob-range": "200",
+                                                                       "opacity": "1",
+                                                                       "origin": "0, 20",
+                                                                       "size": "72, 72",
+                                                                       "sub-pixmaps": "120",
+                                                                       "tooltip": "Gain applied to audio sent to JackTrip",
+                                                                       "transparent": "false",
+                                                                       "uidesc-label": "Send Knob",
+                                                                       "value-inset": "0",
+                                                                       "wants-focus": "false",
+                                                                       "wheel-inc-value": "0.1",
+                                                                       "zoom-factor": "1.5"
+                                                               }
+                                                       },
+                                                       "CTextLabel": {
+                                                               "attributes": {
+                                                                       "back-color": "~ BlackCColor",
+                                                                       "background-offset": "0, 0",
+                                                                       "class": "CTextLabel",
+                                                                       "default-value": "0.5",
+                                                                       "font": "~ NormalFontBig",
+                                                                       "font-antialias": "true",
+                                                                       "font-color": "~ WhiteCColor",
+                                                                       "frame-color": "~ BlackCColor",
+                                                                       "frame-width": "1",
+                                                                       "max-value": "1",
+                                                                       "min-value": "0",
+                                                                       "mouse-enabled": "false",
+                                                                       "opacity": "1",
+                                                                       "origin": "5, 0",
+                                                                       "round-rect-radius": "6",
+                                                                       "shadow-color": "~ RedCColor",
+                                                                       "size": "60, 20",
+                                                                       "style-3D-in": "false",
+                                                                       "style-3D-out": "false",
+                                                                       "style-no-draw": "false",
+                                                                       "style-no-frame": "false",
+                                                                       "style-no-text": "false",
+                                                                       "style-round-rect": "false",
+                                                                       "style-shadow-text": "false",
+                                                                       "text-alignment": "center",
+                                                                       "text-inset": "0, 0",
+                                                                       "text-rotation": "0",
+                                                                       "text-shadow-offset": "1, 1",
+                                                                       "title": "Send",
+                                                                       "transparent": "true",
+                                                                       "uidesc-label": "Send Label",
+                                                                       "value-precision": "2",
+                                                                       "wants-focus": "false",
+                                                                       "wheel-inc-value": "0.1"
+                                                               }
+                                                       },
+                                                       "CTextLabel": {
+                                                               "attributes": {
+                                                                       "back-color": "~ BlackCColor",
+                                                                       "background-offset": "0, 0",
+                                                                       "class": "CTextLabel",
+                                                                       "default-value": "0.5",
+                                                                       "font": "~ NormalFontSmaller",
+                                                                       "font-antialias": "true",
+                                                                       "font-color": "~ WhiteCColor",
+                                                                       "frame-width": "1",
+                                                                       "max-value": "1",
+                                                                       "min-value": "0",
+                                                                       "mouse-enabled": "false",
+                                                                       "opacity": "1",
+                                                                       "origin": "-10, 80",
+                                                                       "round-rect-radius": "6",
+                                                                       "size": "90, 30",
+                                                                       "style-3D-in": "false",
+                                                                       "style-3D-out": "false",
+                                                                       "style-no-draw": "false",
+                                                                       "style-no-frame": "false",
+                                                                       "style-no-text": "false",
+                                                                       "style-round-rect": "false",
+                                                                       "style-shadow-text": "false",
+                                                                       "text-alignment": "center",
+                                                                       "text-inset": "0, 0",
+                                                                       "text-rotation": "0",
+                                                                       "text-shadow-offset": "1, 1",
+                                                                       "title": "To JackTrip",
+                                                                       "transparent": "true",
+                                                                       "uidesc-label": "To JackTrip Label",
+                                                                       "value-precision": "2",
+                                                                       "wants-focus": "false",
+                                                                       "wheel-inc-value": "0.1"
+                                                               }
+                                                       }
+                                               }
+                                       },
+                                       "CViewContainer": {
+                                               "attributes": {
+                                                       "background-color": "~ BlackCColor",
+                                                       "background-color-draw-style": "filled and stroked",
+                                                       "class": "CViewContainer",
+                                                       "mouse-enabled": "true",
+                                                       "opacity": "1",
+                                                       "origin": "200, 80",
+                                                       "size": "172, 112",
+                                                       "transparent": "true",
+                                                       "uidesc-label": "Output Mix",
+                                                       "wants-focus": "false"
+                                               },
+                                               "children": {
+                                                       "CAnimKnob": {
+                                                               "attributes": {
+                                                                       "angle-range": "270",
+                                                                       "angle-start": "135",
+                                                                       "bitmap": "Sercan_Moog_Knob",
+                                                                       "class": "CAnimKnob",
+                                                                       "control-tag": "Output Mix",
+                                                                       "height-of-one-image": "72",
+                                                                       "inverse-bitmap": "false",
+                                                                       "knob-range": "200",
+                                                                       "opacity": "1",
+                                                                       "origin": "50, 20",
+                                                                       "size": "72, 72",
+                                                                       "sub-pixmaps": "120",
+                                                                       "tooltip": "Mix output between source and JackTrip",
+                                                                       "transparent": "false",
+                                                                       "uidesc-label": "Output Mix Knob",
+                                                                       "value-inset": "0",
+                                                                       "wants-focus": "false",
+                                                                       "wheel-inc-value": "0.1",
+                                                                       "zoom-factor": "1.5"
+                                                               }
+                                                       },
+                                                       "CTextLabel": {
+                                                               "attributes": {
+                                                                       "back-color": "~ BlackCColor",
+                                                                       "background-offset": "0, 0",
+                                                                       "class": "CTextLabel",
+                                                                       "default-value": "0.5",
+                                                                       "font": "~ NormalFontBig",
+                                                                       "font-antialias": "true",
+                                                                       "font-color": "~ WhiteCColor",
+                                                                       "frame-color": "~ BlackCColor",
+                                                                       "frame-width": "1",
+                                                                       "max-value": "1",
+                                                                       "min-value": "0",
+                                                                       "mouse-enabled": "false",
+                                                                       "opacity": "1",
+                                                                       "origin": "35, 0",
+                                                                       "round-rect-radius": "6",
+                                                                       "shadow-color": "~ RedCColor",
+                                                                       "size": "100, 20",
+                                                                       "style-3D-in": "false",
+                                                                       "style-3D-out": "false",
+                                                                       "style-no-draw": "false",
+                                                                       "style-no-frame": "false",
+                                                                       "style-no-text": "false",
+                                                                       "style-round-rect": "false",
+                                                                       "style-shadow-text": "false",
+                                                                       "text-alignment": "center",
+                                                                       "text-inset": "0, 0",
+                                                                       "text-rotation": "0",
+                                                                       "text-shadow-offset": "1, 1",
+                                                                       "title": "Output Mix",
+                                                                       "transparent": "true",
+                                                                       "uidesc-label": "Output Mix Label",
+                                                                       "value-precision": "2",
+                                                                       "wants-focus": "false",
+                                                                       "wheel-inc-value": "0.1"
+                                                               }
+                                                       },
+                                                       "CTextLabel": {
+                                                               "attributes": {
+                                                                       "back-color": "~ BlackCColor",
+                                                                       "background-offset": "0, 0",
+                                                                       "class": "CTextLabel",
+                                                                       "default-value": "0.5",
+                                                                       "font": "~ NormalFontSmaller",
+                                                                       "font-antialias": "true",
+                                                                       "font-color": "~ WhiteCColor",
+                                                                       "frame-color": "~ BlackCColor",
+                                                                       "frame-width": "1",
+                                                                       "max-value": "1",
+                                                                       "min-value": "0",
+                                                                       "mouse-enabled": "false",
+                                                                       "opacity": "1",
+                                                                       "origin": "90, 80",
+                                                                       "round-rect-radius": "6",
+                                                                       "shadow-color": "~ RedCColor",
+                                                                       "size": "70, 30",
+                                                                       "style-3D-in": "false",
+                                                                       "style-3D-out": "false",
+                                                                       "style-no-draw": "false",
+                                                                       "style-no-frame": "false",
+                                                                       "style-no-text": "false",
+                                                                       "style-round-rect": "false",
+                                                                       "style-shadow-text": "false",
+                                                                       "text-alignment": "center",
+                                                                       "text-inset": "0, 0",
+                                                                       "text-rotation": "0",
+                                                                       "text-shadow-offset": "1, 1",
+                                                                       "title": "From JackTrip",
+                                                                       "transparent": "true",
+                                                                       "uidesc-label": "Mix JackTrip Label",
+                                                                       "value-precision": "2",
+                                                                       "wants-focus": "false",
+                                                                       "wheel-inc-value": "0.1"
+                                                               }
+                                                       },
+                                                       "CTextLabel": {
+                                                               "attributes": {
+                                                                       "back-color": "~ BlackCColor",
+                                                                       "background-offset": "0, 0",
+                                                                       "class": "CTextLabel",
+                                                                       "default-value": "0.5",
+                                                                       "font": "~ NormalFontSmaller",
+                                                                       "font-antialias": "true",
+                                                                       "font-color": "~ WhiteCColor",
+                                                                       "frame-color": "~ BlackCColor",
+                                                                       "frame-width": "1",
+                                                                       "max-value": "1",
+                                                                       "min-value": "0",
+                                                                       "mouse-enabled": "false",
+                                                                       "opacity": "1",
+                                                                       "origin": "10, 80",
+                                                                       "round-rect-radius": "6",
+                                                                       "shadow-color": "~ RedCColor",
+                                                                       "size": "70, 30",
+                                                                       "style-3D-in": "false",
+                                                                       "style-3D-out": "false",
+                                                                       "style-no-draw": "false",
+                                                                       "style-no-frame": "false",
+                                                                       "style-no-text": "false",
+                                                                       "style-round-rect": "false",
+                                                                       "style-shadow-text": "false",
+                                                                       "text-alignment": "center",
+                                                                       "text-inset": "0, 0",
+                                                                       "text-rotation": "0",
+                                                                       "text-shadow-offset": "1, 1",
+                                                                       "title": "Pass-Through",
+                                                                       "transparent": "true",
+                                                                       "uidesc-label": "Mix Passthrough Label",
+                                                                       "value-precision": "2",
+                                                                       "wants-focus": "false",
+                                                                       "wheel-inc-value": "0.1"
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+}
\ No newline at end of file
diff --git a/src/vst3/resources/Sercan_Moog_Knob.png b/src/vst3/resources/Sercan_Moog_Knob.png
new file mode 100644 (file)
index 0000000..fdab8b5
Binary files /dev/null and b/src/vst3/resources/Sercan_Moog_Knob.png differ
diff --git a/src/vst3/resources/background.png b/src/vst3/resources/background.png
new file mode 100644 (file)
index 0000000..2c7effa
Binary files /dev/null and b/src/vst3/resources/background.png differ
diff --git a/src/vst3/resources/background_2x.png b/src/vst3/resources/background_2x.png
new file mode 100644 (file)
index 0000000..7e1312d
Binary files /dev/null and b/src/vst3/resources/background_2x.png differ
diff --git a/src/vst3/resources/moduleinfo.json b/src/vst3/resources/moduleinfo.json
new file mode 100644 (file)
index 0000000..2843a8c
--- /dev/null
@@ -0,0 +1,43 @@
+{
+  "Name": "JackTrip",
+  "Version": "%VERSION%",
+  "Factory Info": {
+    "Vendor": "JackTrip Labs",
+    "URL": "https://www.jacktrip.com",
+    "E-Mail": "mailto:support@jacktrip.com",
+    "Flags": {
+      "Unicode": true,
+      "Classes Discardable": false,
+      "Component Non Discardable": false,
+    },
+  },
+  "Classes": [
+    {
+      "CID": "176F9AF4A56041A1890DD021765ABCF0",
+      "Category": "Audio Module Class",
+      "Name": "JackTrip Audio Bridge",
+      "Vendor": "JackTrip Labs",
+      "Version": "%VERSION%",
+      "SDKVersion": "VST 3.7.12",
+      "Sub Categories": [
+        "Fx",
+      ],
+      "Class Flags": 1,
+      "Cardinality": 2147483647,
+      "Snapshots": [
+      ],
+    },
+    {
+      "CID": "075C3106BC524686B63544CCF88423FF",
+      "Category": "Component Controller Class",
+      "Name": "JackTrip Audio Bridge",
+      "Vendor": "JackTrip Labs",
+      "Version": "%VERSION%",
+      "SDKVersion": "VST 3.7.12",
+      "Class Flags": 0,
+      "Cardinality": 2147483647,
+      "Snapshots": [
+      ],
+    },
+  ],
+}
\ No newline at end of file
diff --git a/src/vst3/resources/win32resource.rc b/src/vst3/resources/win32resource.rc
new file mode 100644 (file)
index 0000000..3294677
--- /dev/null
@@ -0,0 +1,44 @@
+#include <windows.h>
+#include "../source/version.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// Version
+/////////////////////////////////////////////////////////////////////////////
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION MAJOR_VERSION_INT,SUB_VERSION_INT,RELEASE_NUMBER_INT,BUILD_NUMBER_INT
+ PRODUCTVERSION MAJOR_VERSION_INT,SUB_VERSION_INT,RELEASE_NUMBER_INT,BUILD_NUMBER_INT
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS 0x40004L
+ FILETYPE 0x1L
+ FILESUBTYPE 0x0L
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "040004e4"
+        BEGIN
+            VALUE "FileVersion",      FULL_VERSION_STR"\0"
+            VALUE "ProductVersion",   FULL_VERSION_STR"\0"
+            VALUE "OriginalFilename", stringOriginalFilename"\0"
+            VALUE "FileDescription",  stringFileDescription"\0"
+            VALUE "InternalName",     stringFileDescription"\0"
+            VALUE "ProductName",      stringFileDescription"\0"
+            VALUE "CompanyName",      stringCompanyName"\0"
+            VALUE "LegalCopyright",   stringLegalCopyright"\0"
+            VALUE "LegalTrademarks",  stringLegalTrademarks"\0"
+            //VALUE "PrivateBuild",     " \0"
+            //VALUE "SpecialBuild",     " \0"
+            //VALUE "Comments",         " \0"
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x400, 1252
+    END
+END
diff --git a/subprojects/libsamplerate.wrap b/subprojects/libsamplerate.wrap
new file mode 100644 (file)
index 0000000..4f72f59
--- /dev/null
@@ -0,0 +1,8 @@
+[wrap-file]
+directory = libsamplerate-0.2.2
+source_url = https://github.com/libsndfile/libsamplerate/archive/refs/tags/0.2.2.tar.gz
+source_filename = libsamplerate-0.2.2.tar.gz
+source_hash = 16e881487f184250deb4fcb60432d7556ab12cb58caea71ef23960aec6c0405a
+
+[provide]
+dependency_names = libsamplerate
diff --git a/tests/audio_socket_test.cpp b/tests/audio_socket_test.cpp
new file mode 100644 (file)
index 0000000..57e7776
--- /dev/null
@@ -0,0 +1,98 @@
+//*****************************************************************
+/*
+  JackTrip: A System for High-Quality Audio Network Performance
+  over the Internet
+
+  Copyright (c) 2024 JackTrip Labs, Inc.
+
+  Permission is hereby granted, free of charge, to any person
+  obtaining a copy of this software and associated documentation
+  files (the "Software"), to deal in the Software without
+  restriction, including without limitation the rights to use,
+  copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the
+  Software is furnished to do so, subject to the following
+  conditions:
+
+  The above copyright notice and this permission notice shall be
+  included in all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+  OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+  WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+  OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file audio_socket_tests.cpp
+ * \author Mike Dickey
+ * \date December 2024
+ * \license MIT
+ */
+
+#include <iostream>
+#include <QCoreApplication>
+
+#include "AudioSocket.h"
+#include "jacktrip_globals.h"
+
+using std::cout;
+using std::cerr;
+using std::endl;
+
+const int SAMPLE_RATE = 48000;
+const int BUFFER_SIZE = 128;
+const int NUM_CHANNELS = 2;
+
+class MyThread : public QThread
+{
+public:
+    MyThread(AudioSocket& socket) : s(socket) {}
+    virtual ~MyThread() {}
+    void run() override {
+        float **inputs = new float*[NUM_CHANNELS];
+        float **outputs = new float*[NUM_CHANNELS];
+        for (int i = 0; i < NUM_CHANNELS; i++) {
+            inputs[i] = new float[BUFFER_SIZE];
+            outputs[i] = new float[BUFFER_SIZE];
+            for (int j = 0; j < BUFFER_SIZE; j++) {
+                inputs[i][j] = j;
+            }
+        }
+
+        setRealtimeProcessPriority();
+
+        do {
+            s.compute(BUFFER_SIZE, inputs, outputs);
+            QThread::usleep(BUFFER_SIZE * 1000000 / SAMPLE_RATE);
+        } while (isRunning());
+
+        cout << "Exiting" << endl;
+    }
+
+private:
+    AudioSocket& s;
+};
+
+int main(int argc, char** argv)
+{
+    QCoreApplication app(argc, argv);
+
+    AudioSocket s;
+    if (!s.connect(SAMPLE_RATE, BUFFER_SIZE)) {
+        cerr << "Failed to connect: " << s.getSocket().errorString().toStdString() << endl;
+        return -1;
+    }
+    s.setRetryConnection(true);
+
+    MyThread thread(s);
+    QObject::connect(&thread, &QThread::finished, &app, &QCoreApplication::quit);
+    thread.start();
+
+    return app.exec();
+}
index a21fc6162693638fb4932cee50eff3b0d91df9ff..f70c847695f81c3a94e4ba394a04e90fadf2774a 100755 (executable)
@@ -38,6 +38,16 @@ if "%~1"=="/q" (
 )\r
 if exist ..\builddir\release\jacktrip.exe (set JACKTRIP=..\builddir\release\jacktrip.exe) else (set JACKTRIP=..\builddir\jacktrip.exe)\r
 copy %JACKTRIP% deploy\\r
+if exist ..\builddir\JackTrip.vst3 (\r
+    echo Including JackTrip.vst3\r
+       mkdir deploy\JackTrip.vst3\r
+       mkdir deploy\JackTrip.vst3\Contents\r
+       xcopy /E ..\src\vst3\resources deploy\JackTrip.vst3\Contents\Resources\\r
+       copy ..\LICENSE.md deploy\JackTrip.vst3\Contents\Resources\LICENSE.md\r
+       xcopy /E ..\LICENSES deploy\JackTrip.vst3\Contents\Resources\LICENSES\\r
+       mkdir deploy\JackTrip.vst3\Contents\x86_64-win\r
+       copy ..\builddir\JackTrip.vst3 deploy\JackTrip.vst3\Contents\x86_64-win\JackTrip.vst3\r
+)\r
 cd deploy\r
 \r
 set "WIXDEFINES="\r
@@ -65,17 +75,23 @@ if defined DYNAMIC_QT (
        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
+\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
+\r
+if exist JackTrip.vst3 (\r
+       powershell -Command "(gc JackTrip.vst3\Contents\Resources\moduleinfo.json) -replace '%%VERSION%%', '%VERSION%' | Out-File -encoding ASCII JackTrip.vst3\Contents\Resources\moduleinfo.json"\r
+       candle.exe -arch x64 -ext WixUIExtension -ext WixUtilExtension -dvst=true -dVersion=%VERSION%%WIXDEFINES% ..\jacktrip.wxs ..\jacktrip-vst3.wxs ..\qt%QTVERSION%.wxs\r
+       light.exe -ext WixUIExtension -ext WixUtilExtension -o JackTrip.msi jacktrip.wixobj jacktrip-vst3.wixobj qt%QTVERSION%.wixobj\r
+) else (\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
+)\r
 endlocal\r
diff --git a/win/jacktrip-vst3.wxs b/win/jacktrip-vst3.wxs
new file mode 100644 (file)
index 0000000..8c017ed
--- /dev/null
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>\r
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">\r
+    <Fragment>\r
+        <DirectoryRef Id="VST3DIR">\r
+            <Directory Id="dirC529895EE77450F3C72B644D3B50C911" Name="JackTrip.vst3" />\r
+        </DirectoryRef>\r
+    </Fragment>\r
+    <Fragment>\r
+        <ComponentGroup Id="jacktripvst3">\r
+            <Component Id="cmpA89E2B3B233F97AB53B4FE9FA684CAB2" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{E1C88C7E-8D90-434C-940C-2A867C70CFDE}">\r
+                <File Id="fil0E885921A1CB51256D60C9E0024B88D2" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\background.png" />\r
+            </Component>\r
+            <Component Id="cmp3A3529091478AB40CC0D6F40EC0F7DB1" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{09CF4480-E124-45E7-BBF9-878C695E4A40}">\r
+                <File Id="filB09E0C2581F99AFB520F0ADBA4A2DFB2" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\background_2x.png" />\r
+            </Component>\r
+            <Component Id="cmp542902F0C5B22695EE2E3E0A78590723" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{353BA4A9-64F7-4609-B1B7-81EDDDECEC78}">\r
+                <File Id="filD75564B094910F7A9BC39FBC5B9DE70F" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\Dual_LED.png" />\r
+            </Component>\r
+            <Component Id="cmp811349B294D2CD0BB3FC552CCE42DD6B" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{D7364DBB-E501-4B99-B7D1-58ED45702A5A}">\r
+                <File Id="filBC885E8370B8BD97431A54039D53EDED" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\JackTripEditor.uidesc" />\r
+            </Component>\r
+            <Component Id="cmp9F06646184EC3EA2FAF8242332D67338" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{D061BD66-EC3F-4F6E-BEF9-8282D24FD046}">\r
+                <File Id="filD2134FEF8A44C8959D9A355ECB8D93A5" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\LICENSE.md" />\r
+            </Component>\r
+            <Component Id="cmpB981C2A08F9547568395028EB4709838" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{366731F7-5AE7-4DA9-B3E7-73F0662AB52E}">\r
+                <File Id="fil710883FC5749D9E8A486068DAADEF594" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\moduleinfo.json" />\r
+            </Component>\r
+            <Component Id="cmpC5B85F125D058623BA6601590DCF5D45" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{C336EFB0-3773-4F2C-82C2-3F6FA19E27F4}">\r
+                <File Id="filDCBF690BADDC59592CB00A70CB303639" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\Sercan_Moog_Knob.png" />\r
+            </Component>\r
+            <Component Id="cmp91C15F6937472EDADF18A3DF2850B68B" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{F340688E-5925-4D32-9039-730CFFBA31F5}">\r
+                <File Id="filAFDCAB68DCCBE776AA6F238B56AA907F" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\win32resource.rc" />\r
+            </Component>\r
+            <Component Id="cmp695B40111D773D55335931EEDF9B3871" Directory="dir3E32C5B4E1995E12C8A983E30AC42ED0" Guid="{A385F0E4-A33B-4B1D-9701-E50A96933115}">\r
+                <File Id="fil8849A500FEBD18AD3169AF868F318EBF" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\LICENSES\AVC.txt" />\r
+            </Component>\r
+            <Component Id="cmp30746D59FCB8AB350A1516C27BC53EAC" Directory="dir3E32C5B4E1995E12C8A983E30AC42ED0" Guid="{E39D3840-A8C5-46DA-B312-598B24D7526B}">\r
+                <File Id="fil19A081EB090F775872B98B344AF8D03F" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\LICENSES\GPL-3.0.txt" />\r
+            </Component>\r
+            <Component Id="cmp2425D35CB0206AC69A746C931011450C" Directory="dir3E32C5B4E1995E12C8A983E30AC42ED0" Guid="{EF7ED563-EBE0-4301-947B-1CE45AC69F8C}">\r
+                <File Id="fil971B4767893F685F1E7D68C66CD9710E" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\LICENSES\LGPL-3.0-only.txt" />\r
+            </Component>\r
+            <Component Id="cmpF438EE21A995DA392086A47877FAEC24" Directory="dir3E32C5B4E1995E12C8A983E30AC42ED0" Guid="{60E31672-8BAC-4BC0-9221-B3AB9949B5AC}">\r
+                <File Id="fil56278695D7A872F1D231A820110CC704" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\LICENSES\MIT.txt" />\r
+            </Component>\r
+            <Component Id="cmpC4A0D6F318491F474FFDF80A46C53B24" Directory="dir7528754B446ECAFE2C3463C0916A775D" Guid="{BB89EFC4-CD0B-417B-9AC2-56444262C0ED}">\r
+                <File Id="fil5F012D81F27C3F619CE5A017C47B3727" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\x86_64-win\JackTrip.vst3" />\r
+            </Component>\r
+        </ComponentGroup>\r
+    </Fragment>\r
+    <Fragment>\r
+        <DirectoryRef Id="dirC529895EE77450F3C72B644D3B50C911">\r
+            <Directory Id="dir8DFAD377CE63CA353FD57A8086B458C5" Name="Contents" />\r
+        </DirectoryRef>\r
+    </Fragment>\r
+    <Fragment>\r
+        <DirectoryRef Id="dir8DFAD377CE63CA353FD57A8086B458C5">\r
+            <Directory Id="dir7528754B446ECAFE2C3463C0916A775D" Name="x86_64-win" />\r
+        </DirectoryRef>\r
+    </Fragment>\r
+    <Fragment>\r
+        <DirectoryRef Id="dir8DFAD377CE63CA353FD57A8086B458C5">\r
+            <Directory Id="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Name="Resources" />\r
+        </DirectoryRef>\r
+    </Fragment>\r
+    <Fragment>\r
+        <DirectoryRef Id="dirD7DDD8DA90BC893A3523C3F478EDBFA5">\r
+            <Directory Id="dir3E32C5B4E1995E12C8A983E30AC42ED0" Name="LICENSES" />\r
+        </DirectoryRef>\r
+    </Fragment>\r
+</Wix>
\ No newline at end of file
index 565f1ac9b9d67781f45d5769564ad15d2c8901a8..e48cdf3db5d20e01febf8553d614173993f20149 100644 (file)
                 <Directory Id='INSTALLDIR' Name='JackTrip'>\r
                 </Directory>\r
             </Directory>\r
+            <?ifdef vst?>\r
+            <Directory Id="CommonFiles64Folder" Name="CFiles">\r
+                <Directory Id='VST3DIR' Name='VST3'>\r
+                </Directory>\r
+            </Directory>\r
+            <?endif?>\r
             <Directory Id='ProgramMenuFolder' Name='Programs'>\r
                 <Directory Id='ProgramMenuDir' Name='JackTrip'>\r
                     <Component Id="ProgramMenuDir" Guid="2f25e96a-47a5-4eca-b367-a78ce536d9b9">\r
             <ComponentRef Id='ProgramMenuDir' />\r
         </Feature>\r
 \r
+        <?ifdef vst?>\r
+        <Feature Id='VST3Install' Title='JackTrip VST3 Plugin' Description='VST3 Plugin' Level='1'>\r
+            <ComponentGroupRef Id='jacktripvst3' />\r
+        </Feature>\r
+        <?endif?>\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