From: IOhannes m zmölnig (Debian/GNU) Date: Sat, 25 Jan 2025 14:50:56 +0000 (+0100) Subject: New upstream version 2.5.0+ds X-Git-Tag: archive/raspbian/2.5.1+ds-1+rpi1^2~7^2~1 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=bd16c0e315d4408b35715164f5ec8ea7ef5f2a2e;p=jacktrip.git New upstream version 2.5.0+ds --- diff --git a/.dockerignore b/.dockerignore index b517c8e..a2c033a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,7 +2,9 @@ !docs !meson* !container +!externals !src !subprojects !linux -!win \ No newline at end of file +!win +!tests diff --git a/CMakeLists.txt b/CMakeLists.txt index e4db93d..442eaaf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 "about@2x.png" "alt/about@2x.png" QRC_CONTENTS "${QRC_CONTENTS}") - string(REPLACE "about.png" "alt/about.png" QRC_CONTENTS "${QRC_CONTENTS}") - string(REPLACE "icon.png" "alt/icon.png" QRC_CONTENTS "${QRC_CONTENTS}") + string(REPLACE "icon_256.png" "../gui/alt/icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}") + string(REPLACE "icon_256.png" "../gui/alt/icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}") + string(REPLACE "icon_128.png" "../gui/alt/icon_128.png" QRC_CONTENTS "${QRC_CONTENTS}") + string(REPLACE "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 "alt/about@2x.png" "about@2x.png" QRC_CONTENTS "${QRC_CONTENTS}") - string(REPLACE "alt/about.png" "about.png" QRC_CONTENTS "${QRC_CONTENTS}") - string(REPLACE "alt/icon.png" "icon.png" QRC_CONTENTS "${QRC_CONTENTS}") + string(REPLACE "../gui/alt/icon_256.png" "icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}") + string(REPLACE "../gui/alt/icon_256.png" "icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}") + string(REPLACE "../gui/alt/icon_128.png" "icon_128.png" QRC_CONTENTS "${QRC_CONTENTS}") + string(REPLACE "../gui/alt/icon_32.png" "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 diff --git a/Dockerfile b/Dockerfile index 2fa1bc0..b5d513a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 351873e..57e2444 100755 --- 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 diff --git a/build-aux/flatpak/org.jacktrip.JackTrip.json b/build-aux/flatpak/org.jacktrip.JackTrip.json index e4348c0..49aec5f 100644 --- a/build-aux/flatpak/org.jacktrip.JackTrip.json +++ b/build-aux/flatpak/org.jacktrip.JackTrip.json @@ -28,6 +28,7 @@ "buildsystem": "meson", "config-opts": [ "-Dbuildtype=debugoptimized", + "-Dlibsamplerate=disabled", "-Dpkg_config_path=/app/lib/x86_64-linux-gnu/pkgconfig" ], "sources": [ diff --git a/docs/Build/Linux.md b/docs/Build/Linux.md index 985228d..8086733 100644 --- a/docs/Build/Linux.md +++ b/docs/Build/Linux.md @@ -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/). diff --git a/docs/Build/Mac.md b/docs/Build/Mac.md index 898361f..dba52b4 100644 --- a/docs/Build/Mac.md +++ b/docs/Build/Mac.md @@ -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/). diff --git a/docs/Build/Windows.md b/docs/Build/Windows.md index bd7dc90..2dc3fa9 100644 --- a/docs/Build/Windows.md +++ b/docs/Build/Windows.md @@ -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/). diff --git a/docs/changelog.yml b/docs/changelog.yml index 811e2ec..e44a0ec 100644 --- a/docs/changelog.yml +++ b/docs/changelog.yml @@ -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 index 0000000..003b2e3 --- /dev/null +++ b/externals/oscpp/CMakeLists.txt @@ -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 index 0000000..7d3e03b --- /dev/null +++ b/externals/oscpp/LICENSE @@ -0,0 +1,23 @@ +oscpp library + +Copyright (c) 2004-2018 Stefan Kersten + +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 index 0000000..6ed9240 --- /dev/null +++ b/externals/oscpp/Makefile @@ -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 index 0000000..4919817 --- /dev/null +++ b/externals/oscpp/README.md @@ -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 + +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 +#include +#include + +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 + +const size_t kMaxPacketSize = 8192; + +size_t recv(Transport* t, void* buffer, size_t size); + +void recvPacket(Transport* t) +{ + std::array 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 +#include + +Transport* newTransport(); + +int main(int, char**) +{ + std::unique_ptr t(newTransport()); + std::array 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 + +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 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 index 0000000..4ddfcd4 --- /dev/null +++ b/externals/oscpp/doxygen.cfg @@ -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 , where +# is the value of the INPUT_FILTER tag, and 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 index 0000000..36cfd92 --- /dev/null +++ b/externals/oscpp/include/oscpp/client.hpp @@ -0,0 +1,368 @@ +// oscpp library +// +// Copyright (c) 2004-2013 Stefan Kersten +// +// 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 +#include +#include + +#include +#include +#include +#include +#include + +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::min() || + diff > std::numeric_limits::max()) + { + std::stringstream s; + s << "Pointer difference " << diff + << " can't be represented by int32_t"; + throw std::logic_error(s.str()); + } + return static_cast(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::max() + Packet& blob(const Blob& arg) + { + if (arg.size() > (size_t)std::numeric_limits::max()) + { + throw std::invalid_argument("Blob size greater than maximum " + "value representable by int32_t"); + } + m_tags.putChar('b'); + m_args.putInt32(static_cast(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 Packet& put(T) + { + T::OSC_Client_Packet_put_unimplemented; + return *this; + } + + template + Packet& put(InputIterator begin, InputIterator end) + { + for (auto it = begin; it != end; it++) + { + put(*it); + } + return *this; + } + + template + Packet& putArray(InputIterator begin, InputIterator end) + { + openArray(); + put(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 x) +{ + return int32(x); +} +template <> inline Packet& Packet::put(float x) +{ + return float32(x); +} +template <> inline Packet& Packet::put(const char* x) +{ + return string(x); +} +template <> inline Packet& Packet::put(Blob x) +{ + return blob(x); +} + +template class StaticPacket : public Packet +{ +public: + StaticPacket() + : Packet(reinterpret_cast(&m_buffer), buffer_size) + {} + +private: + typedef typename std::aligned_storage::type + AlignedBuffer; + AlignedBuffer m_buffer; +}; + +class DynamicPacket : public Packet +{ +public: + DynamicPacket(size_t buffer_size) + : Packet(static_cast(new char[buffer_size]), buffer_size) + {} + + ~DynamicPacket() + { + delete[] static_cast(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 index 0000000..f9a0c2e --- /dev/null +++ b/externals/oscpp/include/oscpp/detail/endian.hpp @@ -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 , from + * which this code was originally taken. + * + * Modified by Caleb Epstein to use 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 which defines +// __BYTE_ORDER + +#if defined(__GLIBC__) || defined(__ANDROID__) +# include +# 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 index 0000000..3aea894 --- /dev/null +++ b/externals/oscpp/include/oscpp/detail/host.hpp @@ -0,0 +1,118 @@ +// oscpp library +// +// Copyright (c) 2004-2013 Stefan Kersten +// +// 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 + +#include +#include + +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 +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 inline uint32_t convert32(uint32_t) +{ + throw std::logic_error("Invalid byte order"); +} + +template <> inline uint32_t convert32(uint32_t x) +{ +#if defined(OSCPP_LITTLE_ENDIAN) + return bswap32(x); +#else + return x; +#endif +} + +template <> inline uint32_t convert32(uint32_t x) +{ + return x; +} + +template inline uint64_t convert64(uint64_t) +{ + throw std::logic_error("Invalid byte order"); +} + +template <> inline uint64_t convert64(uint64_t x) +{ +#if defined(OSCPP_LITTLE_ENDIAN) + return bswap64(x); +#else + return x; +#endif +} + +template <> inline uint64_t convert64(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 index 0000000..67b064a --- /dev/null +++ b/externals/oscpp/include/oscpp/detail/stream.hpp @@ -0,0 +1,365 @@ +// oscpp library +// +// Copyright (c) 2004-2013 Stefan Kersten +// +// 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 +#include +#include +#include + +#include +#include +#include +#include + +namespace OSCPP { + +class Stream +{ +public: + Stream() + { + m_begin = m_end = m_pos = 0; + } + + Stream(void* data, size_t size) + { + m_begin = static_cast(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 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(uh); + std::memcpy(pos(), &un, 4); + advance(4); + } + + void putUInt64(uint64_t x) + { + checkWritable(8); + const uint64_t un = convert64(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(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(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 WriteStream; + +template class BasicReadStream : public Stream +{ +public: + BasicReadStream() + {} + + BasicReadStream(const void* data, size_t size) + : Stream(const_cast(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(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(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(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(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(pos()) + 3; + const char* end = static_cast(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 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 index 0000000..600883a --- /dev/null +++ b/externals/oscpp/include/oscpp/error.hpp @@ -0,0 +1,87 @@ +// oscpp library +// +// Copyright (c) 2004-2013 Stefan Kersten +// +// 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 +#include + +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 index 0000000..04f1579 --- /dev/null +++ b/externals/oscpp/include/oscpp/print.hpp @@ -0,0 +1,183 @@ +// OSCpp library +// +// Copyright (c) 2004-2011 Stefan Kersten +// +// 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 +#include + +#include + +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 index 0000000..a3933f7 --- /dev/null +++ b/externals/oscpp/include/oscpp/server.hpp @@ -0,0 +1,493 @@ +// oscpp library +// +// Copyright (c) 2004-2013 Stefan Kersten +// +// 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 +#include + +#include +#include +#include +#include + +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
+ * f -- 32 bit floating point number
+ * s -- NULL-terminated string padded to 4-byte boundary
+ * 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 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 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)); + } + } + // 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(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() +{ + return int32(); +} + +template <> inline float ArgStream::next() +{ + return float32(); +} + +template <> inline const char* ArgStream::next() +{ + return string(); +} + +template <> inline Blob ArgStream::next() +{ + return blob(); +} + +template <> inline ArgStream ArgStream::next() +{ + 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 index 0000000..2afc68b --- /dev/null +++ b/externals/oscpp/include/oscpp/types.hpp @@ -0,0 +1,59 @@ +// oscpp library +// +// Copyright (c) 2004-2013 Stefan Kersten +// +// 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 index 0000000..5e3caa4 --- /dev/null +++ b/externals/oscpp/include/oscpp/util.hpp @@ -0,0 +1,159 @@ +// oscpp library +// +// Copyright (c) 2004-2013 Stefan Kersten +// +// 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 +#include + +namespace OSCPP { + +static const size_t kAlignment = 4; + +inline bool isAligned(const void* ptr, size_t alignment) +{ + return (reinterpret_cast(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 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 +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 index 0000000..e5150f3 --- /dev/null +++ b/externals/oscpp/test/CMakeLists.txt @@ -0,0 +1,42 @@ +set(warnings -Wall -Wextra -Wno-unused-parameter -Werror) + +add_compile_options( + "$<$:${warnings}>" + "$<$:${warnings}>" + "$<$:${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 index 0000000..fdff9a3 --- /dev/null +++ b/externals/oscpp/test/oscpp_autocheck.cpp @@ -0,0 +1,657 @@ +#include +#include +#include + +#include +#include +#include +#include +#include + +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 using List = std::list>; + +template bool equalList(const List& list1, const List& 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 void printList(std::ostream& out, const List& 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& 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 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 + parseBundle(const OSCPP::Server::Bundle& bdl); + static void parseArgs(OSCPP::Server::ArgStream& inArgs, + List& outArgs); + static std::shared_ptr + parseMessage(const OSCPP::Server::Message& msg); + + Type m_type; +}; + +class Bundle : public Packet +{ +public: + Bundle(uint64_t time, List 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(other); + return m_time == otherBundle.m_time && + equalList(m_packets, otherBundle.m_packets); + } + +private: + uint64_t m_time; + List m_packets; +}; + +class Message : public Packet +{ +public: + Message(std::string address, List 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(other); + return m_address == otherMsg.m_address && + equalList(m_args, otherMsg.m_args); + } + +private: + std::string m_address; + List 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(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(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(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(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(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 elems = List()) + : 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(other).m_elems); + } + +private: + List m_elems; +}; + +std::shared_ptr Packet::parseBundle(const OSCPP::Server::Bundle& bdl) +{ + List outPackets; + OSCPP::Server::PacketStream inPackets(bdl.packets()); + while (!inPackets.atEnd()) + { + outPackets.push_back(parse(inPackets.next())); + } + return std::make_shared(bdl.time(), std::move(outPackets)); +} + +void Packet::parseArgs(OSCPP::Server::ArgStream& inArgs, + List& outArgs) +{ + while (!inArgs.atEnd()) + { + switch (inArgs.tag()) + { + case 'i': + outArgs.push_back(std::make_shared(inArgs.int32())); + break; + case 'f': + outArgs.push_back(std::make_shared(inArgs.float32())); + break; + case 's': + outArgs.push_back(std::make_shared(inArgs.string())); + break; + case 'b': + outArgs.push_back(std::make_shared(inArgs.blob())); + break; + case '[': + { + OSCPP::Server::ArgStream inElems(inArgs.array()); + List outElems; + parseArgs(inElems, outElems); + outArgs.push_back(std::make_shared(outElems)); + } + break; + } + } +} + +std::shared_ptr Packet::parseMessage(const OSCPP::Server::Message& msg) +{ + OSCPP::Server::ArgStream inArgs(msg.args()); + List outArgs; + parseArgs(inArgs, outArgs); + return std::make_shared(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->print(out); + return out; +} +}} // namespace OSCPP::AST + +namespace ac = autocheck; + +namespace OSCPP { namespace AutoCheck { + +struct MessageArgListGen +{ + typedef AST::List result_type; + result_type operator()(size_t size) const; +}; + +struct MessageArgGen +{ + typedef std::shared_ptr result_type; + result_type operator()(size_t size) const + { + AST::Argument::Type argType = static_cast( + ac::generator()(AST::Argument::kNumTypes - 1)); + switch (argType) + { + case AST::Argument::kInt32: + return std::make_shared( + ac::generator()(size)); + case AST::Argument::kFloat32: + return std::make_shared( + ac::generator()(size)); + case AST::Argument::kString: + return std::make_shared( + ac::string()(std::max(1, size))); + case AST::Argument::kBlob: + return std::make_shared( + ac::generator()(size)); + case AST::Argument::kArray: + // Exponential size backoff + return std::make_shared( + 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(elems.begin(), elems.end()); +} + +struct PacketGen +{ + // ac::generator> source; + typedef std::shared_ptr result_type; + result_type operator()(size_t size) const + { + return ac::generator()(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( + ac::generator()(size), + AST::List(packets.begin(), packets.end())); + } + + std::string gen_message_address(size_t size) const + { + std::string result( + ac::string()(std::max(2, size))); + if (result[0] != '/') + result[0] = '/'; + return result; + } + + result_type gen_message(size_t size) const + { + return std::make_shared(gen_message_address(size), + MessageArgListGen()(size)); + } +}; +}} // namespace OSCPP::AutoCheck + +bool prop_identity(const std::shared_ptr& packet1) +{ + // packet1->print(std::cerr); std::cerr << "\n"; + const size_t size = packet1->size(); + std::unique_ptr 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& 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 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>(prop_identity, 150, + ac::make_arbitrary(PacketGen())); + // ac::check,size_t>( + // prop_overflow, + // 150, + // ac::make_arbitrary(PacketGen(), ac::generator()) + // ); + return 0; +} diff --git a/externals/oscpp/tools/clang-format b/externals/oscpp/tools/clang-format new file mode 100755 index 0000000..543e35e --- /dev/null +++ b/externals/oscpp/tools/clang-format @@ -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 index 0000000..40dfdce --- /dev/null +++ b/externals/oscpp/tools/mdcode.rb @@ -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 diff --git a/jacktrip.pro b/jacktrip.pro index 60c2198..b987594 100644 --- a/jacktrip.pro +++ b/jacktrip.pro @@ -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 index 0000000..4af7d42 --- /dev/null +++ b/linux/Dockerfile.build @@ -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 / diff --git a/linux/README.md b/linux/README.md index fb8f267..3d46779 100644 --- a/linux/README.md +++ b/linux/README.md @@ -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 index 0000000..5cb1588 --- /dev/null +++ b/macos/JackTrip.vst3_template/Contents/Info.plist @@ -0,0 +1,36 @@ + + + + + BuildMachineOSBuild + 19E287 + CFBundleDevelopmentRegion + English + CFBundleExecutable + JackTrip.vst3 + CFBundleGetInfoString + JackTrip Audio Bridge + CFBundleIconFile + jacktrip + CFBundleIdentifier + %BUNDLEID% + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + + CFBundleName + %BUNDLENAME% + CFBundlePackageType + BNDL + CFBundleShortVersionString + + CFBundleSignature + ???? + CFBundleVersion + %VERSION% + CSResourcesFileMapped + + NSHumanReadableCopyright + Copyright © 2024-2025 JackTrip Labs, Inc. + + diff --git a/macos/JackTrip.vst3_template/Contents/PkgInfo b/macos/JackTrip.vst3_template/Contents/PkgInfo new file mode 100644 index 0000000..19a9cf6 --- /dev/null +++ b/macos/JackTrip.vst3_template/Contents/PkgInfo @@ -0,0 +1 @@ +BNDL???? \ No newline at end of file diff --git a/macos/assemble_app.sh b/macos/assemble_app.sh index 22475f3..fdc07fa 100755 --- a/macos/assemble_app.sh +++ b/macos/assemble_app.sh @@ -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 index 0000000..d0be9eb --- /dev/null +++ b/macos/package/JackTrip.pkgproj_template_with_vst3 @@ -0,0 +1,1740 @@ + + + + + PACKAGES + + + MUST-CLOSE-APPLICATION-ITEMS + + MUST-CLOSE-APPLICATIONS + + PACKAGE_FILES + + DEFAULT_INSTALL_LOCATION + / + HIERARCHY + + CHILDREN + + + CHILDREN + + + CHILDREN + + GID + 80 + PATH + Jack + PATH_TYPE + 2 + PERMISSIONS + 509 + TYPE + 2 + UID + 0 + + + BUNDLE_CAN_DOWNGRADE + + BUNDLE_POSTINSTALL_PATH + + PATH + postinstall.sh + PATH_TYPE + 1 + + BUNDLE_PREINSTALL_PATH + + PATH_TYPE + 0 + + CHILDREN + + GID + 80 + PATH + ../%BUNDLENAME%.app + PATH_TYPE + 1 + PERMISSIONS + 493 + TYPE + 3 + UID + 0 + + + CHILDREN + + GID + 80 + PATH + Utilities + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + GID + 80 + PATH + Applications + PATH_TYPE + 0 + PERMISSIONS + 509 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + bin + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + CHILDREN + + + CHILDREN + + GID + 80 + PATH + Application Support + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Audio + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Automator + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + ColorPickers + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Documentation + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Extensions + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Filesystems + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 80 + PATH + Fonts + PATH_TYPE + 0 + PERMISSIONS + 1021 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Frameworks + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Input Methods + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Internet Plug-Ins + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + LaunchAgents + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + LaunchDaemons + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + PreferencePanes + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Preferences + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 80 + PATH + Printers + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + PrivilegedHelperTools + PATH_TYPE + 0 + PERMISSIONS + 1005 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + QuickLook + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + QuickTime + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Screen Savers + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Scripts + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Services + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Widgets + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 0 + PATH + Library + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + + CHILDREN + + GID + 0 + PATH + etc + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + var + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + GID + 0 + PATH + private + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + sbin + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + CHILDREN + + + CHILDREN + + + CHILDREN + + GID + 0 + PATH + Extensions + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 0 + PATH + Library + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 0 + PATH + System + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + + CHILDREN + + GID + 0 + PATH + Shared + PATH_TYPE + 0 + PERMISSIONS + 1023 + TYPE + 1 + UID + 0 + + + GID + 80 + PATH + Users + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + + CHILDREN + + GID + 0 + PATH + bin + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + include + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + lib + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + CHILDREN + + + CHILDREN + + GID + 0 + PATH + bin + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + GID + 0 + PATH + local + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + sbin + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + share + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + GID + 0 + PATH + usr + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + -1 + UID + 0 + + + GID + 0 + PATH + / + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + PAYLOAD_TYPE + 0 + PRESERVE_EXTENDED_ATTRIBUTES + + SHOW_INVISIBLE + + SPLIT_FORKS + + TREAT_MISSING_FILES_AS_WARNING + + VERSION + 5 + + PACKAGE_SCRIPTS + + POSTINSTALL_PATH + + PATH_TYPE + 0 + + PREINSTALL_PATH + + PATH_TYPE + 0 + + RESOURCES + + + PACKAGE_SETTINGS + + AUTHENTICATION + 1 + CONCLUSION_ACTION + 0 + FOLLOW_SYMBOLIC_LINKS + + IDENTIFIER + %BUNDLEID% + LOCATION + 0 + NAME + %BUNDLENAME% + OVERWRITE_PERMISSIONS + + PAYLOAD_SIZE + -1 + REFERENCE_PATH + + RELOCATABLE + + USE_HFS+_COMPRESSION + + VERSION + %VERSION% + + TYPE + 0 + UUID + 10E1CE8D-C84E-45FC-81DA-B174548AE779 + + + MUST-CLOSE-APPLICATION-ITEMS + + MUST-CLOSE-APPLICATIONS + + PACKAGE_FILES + + DEFAULT_INSTALL_LOCATION + / + HIERARCHY + + CHILDREN + + + CHILDREN + + GID + 80 + PATH + Applications + PATH_TYPE + 0 + PERMISSIONS + 509 + TYPE + 1 + UID + 0 + + + CHILDREN + + + CHILDREN + + GID + 80 + PATH + Application Support + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + + CHILDREN + + + CHILDREN + + + BUNDLE_CAN_DOWNGRADE + + CHILDREN + + GID + 0 + PATH + ../%BUNDLENAME%.vst3 + PATH_TYPE + 1 + PERMISSIONS + 493 + TYPE + 3 + UID + 0 + + + GID + 0 + PATH + VST3 + PATH_TYPE + 2 + PERMISSIONS + 509 + TYPE + 2 + UID + 0 + + + GID + 0 + PATH + Plug-Ins + PATH_TYPE + 2 + PERMISSIONS + 509 + TYPE + 2 + UID + 0 + + + GID + 0 + PATH + Audio + PATH_TYPE + 2 + PERMISSIONS + 509 + TYPE + 2 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Automator + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Documentation + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Extensions + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Filesystems + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Frameworks + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Input Methods + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Internet Plug-Ins + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Keyboard Layouts + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + LaunchAgents + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + LaunchDaemons + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + PreferencePanes + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Preferences + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 80 + PATH + Printers + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + PrivilegedHelperTools + PATH_TYPE + 0 + PERMISSIONS + 1005 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + QuickLook + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + QuickTime + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Screen Savers + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Scripts + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Services + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Widgets + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 0 + PATH + Library + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + + CHILDREN + + GID + 0 + PATH + Shared + PATH_TYPE + 0 + PERMISSIONS + 1023 + TYPE + 1 + UID + 0 + + + GID + 80 + PATH + Users + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 0 + PATH + / + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + PAYLOAD_TYPE + 0 + PRESERVE_EXTENDED_ATTRIBUTES + + SHOW_INVISIBLE + + SPLIT_FORKS + + TREAT_MISSING_FILES_AS_WARNING + + VERSION + 5 + + PACKAGE_SCRIPTS + + POSTINSTALL_PATH + + PATH_TYPE + 0 + + PREINSTALL_PATH + + PATH_TYPE + 0 + + RESOURCES + + + PACKAGE_SETTINGS + + AUTHENTICATION + 1 + CONCLUSION_ACTION + 0 + FOLLOW_SYMBOLIC_LINKS + + IDENTIFIER + %BUNDLEID%.vst3 + LOCATION + 0 + NAME + %BUNDLENAME%.vst3 + OVERWRITE_PERMISSIONS + + PAYLOAD_SIZE + -1 + REFERENCE_PATH + + RELOCATABLE + + USE_HFS+_COMPRESSION + + VERSION + %VERSION% + + TYPE + 0 + UUID + 8CAD404A-E34F-469F-93DD-D2D2125F35F5 + + + PROJECT + + PROJECT_COMMENTS + + NOTES + + + + PROJECT_PRESENTATION + + BACKGROUND + + APPAREANCES + + DARK_AQUA + + LIGHT_AQUA + + + SHARED_SETTINGS_FOR_ALL_APPAREANCES + + + INSTALLATION_STEPS + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewIntroductionController + INSTALLER_PLUGIN + Introduction + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewReadMeController + INSTALLER_PLUGIN + ReadMe + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewLicenseController + INSTALLER_PLUGIN + License + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewDestinationSelectController + INSTALLER_PLUGIN + TargetSelect + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewInstallationTypeController + INSTALLER_PLUGIN + PackageSelection + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewInstallationController + INSTALLER_PLUGIN + Install + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewSummaryController + INSTALLER_PLUGIN + Summary + LIST_TITLE_KEY + InstallerSectionTitle + + + INTRODUCTION + + LOCALIZATIONS + + + LICENSE + + LOCALIZATIONS + + + LANGUAGE + English + VALUE + + PATH + license.txt + PATH_TYPE + 3 + + + + MODE + 0 + + README + + LOCALIZATIONS + + + LANGUAGE + English + VALUE + + PATH + readme.txt + PATH_TYPE + 3 + + + + + TITLE + + LOCALIZATIONS + + + + PROJECT_REQUIREMENTS + + LIST + + RESOURCES + + ROOT_VOLUME_ONLY + + + PROJECT_SETTINGS + + BUILD_FORMAT + 0 + BUILD_PATH + + PATH + build + PATH_TYPE + 1 + + EXCLUDED_FILES + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + .DS_Store + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Remove .DS_Store files + PROXY_TOOLTIP + Remove ".DS_Store" files created by the Finder. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + .pbdevelopment + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Remove .pbdevelopment files + PROXY_TOOLTIP + Remove ".pbdevelopment" files created by ProjectBuilder or Xcode. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + CVS + TYPE + 1 + + + REGULAR_EXPRESSION + + STRING + .cvsignore + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + .cvspass + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + .svn + TYPE + 1 + + + REGULAR_EXPRESSION + + STRING + .git + TYPE + 1 + + + REGULAR_EXPRESSION + + STRING + .gitignore + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Remove SCM metadata + PROXY_TOOLTIP + Remove helper files and folders used by the CVS, SVN or Git Source Code Management systems. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + classes.nib + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + designable.db + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + info.nib + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Optimize nib files + PROXY_TOOLTIP + Remove "classes.nib", "info.nib" and "designable.nib" files within .nib bundles. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + Resources Disabled + TYPE + 1 + + + PROTECTED + + PROXY_NAME + Remove Resources Disabled folders + PROXY_TOOLTIP + Remove "Resources Disabled" folders. + STATE + + + + SEPARATOR + + + + NAME + %BUNDLENAME% + PAYLOAD_ONLY + + TREAT_MISSING_PRESENTATION_DOCUMENTS_AS_WARNING + + + + TYPE + 0 + VERSION + 2 + + diff --git a/macos/package/postinstall.sh b/macos/package/postinstall.sh index a668d3c..b443867 100755 --- a/macos/package/postinstall.sh +++ b/macos/package/postinstall.sh @@ -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 diff --git a/macos/sign-stuff.sh b/macos/sign-stuff.sh index ed020aa..fe92b5c 100755 --- a/macos/sign-stuff.sh +++ b/macos/sign-stuff.sh @@ -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 diff --git a/meson.build b/meson.build index 0685415..2bba8cb 100644 --- a/meson.build +++ b/meson.build @@ -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') diff --git a/meson_options.txt b/meson_options.txt index ec66535..a1850ae 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -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 diff --git a/src/Analyzer.cpp b/src/Analyzer.cpp index 824eba6..9ef9e3d 100644 --- a/src/Analyzer.cpp +++ b/src/Analyzer.cpp @@ -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)); diff --git a/src/AudioInterface.cpp b/src/AudioInterface.cpp index 97cb28d..7450912 100644 --- a/src/AudioInterface.cpp +++ b/src/AudioInterface.cpp @@ -41,7 +41,9 @@ #include #include +#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& 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& 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& 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& 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& 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& 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& 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& plugin) { - if (!plugin) { + if (plugin.isNull()) { return; } @@ -715,34 +718,53 @@ void AudioInterface::appendProcessPluginToMonitor(ProcessPlugin* plugin) mProcessPluginsToMonitor.append(plugin); } +void AudioInterface::appendAudioSocket(QSharedPointer& s) +{ + if (s.isNull()) + return; + static_cast(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); + } } } diff --git a/src/AudioInterface.h b/src/AudioInterface.h index d752222..3de02af 100644 --- a/src/AudioInterface.h +++ b/src/AudioInterface.h @@ -38,17 +38,19 @@ #ifndef __AUDIOINTERFACE_H__ #define __AUDIOINTERFACE_H__ +#include #include #include #include #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 * std::tr1::shared_ptr loopback(new ProcessPluginName); */ - virtual void appendProcessPluginToNetwork(ProcessPlugin* plugin); + virtual void appendProcessPluginToNetwork(QSharedPointer& 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& plugin); /** \brief appendProcessPluginToMonitor(): * Appends plugins used for local monitoring */ - virtual void appendProcessPluginToMonitor(ProcessPlugin* plugin); + virtual void appendProcessPluginToMonitor(QSharedPointer& plugin); + + /** \brief appendAudioSocket(): + * Appends audio socket connections + */ + virtual void appendAudioSocket(QSharedPointer& 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 + QVector > mProcessPluginsFromNetwork; ///< Vector of ProcessPlugins - QVector + QVector > mProcessPluginsToNetwork; ///< Vector of ProcessPlugins - QVector + QVector > mProcessPluginsToMonitor; ///< Vector of ProcessPlugins + QVector > + mAudioSockets; ///< Vector of AudioSockets QVarLengthArray mInProcessBuffer; ///< Vector of Input buffers/channel for ProcessPlugin QVarLengthArray diff --git a/src/AudioSocket.cpp b/src/AudioSocket.cpp new file mode 100644 index 0000000..298f74d --- /dev/null +++ b/src/AudioSocket.cpp @@ -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 +#include + +#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(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(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(mRecvBuffer.data()); + if (!mReceiveQueue.pop(recvPtr)) { + // qDebug() << "Audio socket glitch: receive queue empty"; + break; + } + + // copy bytes from packet to extras + float* framePtr = reinterpret_cast(mRecvBuffer.data()); + int newSamples = static_cast(*(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(mReceiveQueue.size()); + if (remainingPackets < mMinQueuePackets) { + mMinQueuePackets = remainingPackets; + } + if (remainingPackets > mMaxQueuePackets) { + mMaxQueuePackets = remainingPackets; + } + if (mNextQueueCheck > static_cast(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(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& 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(mRecvBuffer.data() + BytesPerSample); + mSrcData.output_frames = AudioSocketMaxSamplesPerBlock; + } else { + src_reset(mSrcStatePtr); + } + mSrcData.src_ratio = static_cast(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(mPopBuffer.data()); + while (mSendQueue.pop(popPtr)) { + float* framePtr = reinterpret_cast(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(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(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(mRecvBuffer.data()); + *framePtr = newSamples; + mReceiveQueue.push(reinterpret_cast(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 s(new QLocalSocket); + s->moveToThread(&mThread); + + mWorkerPtr.reset(new AudioSocketWorker(mSendQueue, mReceiveQueue, s)); + mWorkerPtr->moveToThread(&mThread); + mWorkerPtr->setRetryConnection(retryConnection); + + initWorker(); +} + +//******************************************************************************* +AudioSocket::AudioSocket(QSharedPointer& 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(mToAudioSocketPluginPtr.get()); + auto* fromPluginPtr = + static_cast(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 index 0000000..3d1be40 --- /dev/null +++ b/src/AudioSocket.h @@ -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 +#include +#include +#include +#include +#include + +#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 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& 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 mTimerPtr; + QSharedPointer 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& 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& getToAudioSocketPlugin() + { + return mToAudioSocketPluginPtr; + } + inline QSharedPointer& 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 mToAudioSocketPluginPtr; + QSharedPointer mFromAudioSocketPluginPtr; + QScopedPointer mWorkerPtr; + + friend class AudioSocketWorker; +}; + +#endif \ No newline at end of file diff --git a/src/Compressor.cpp b/src/Compressor.cpp index 04311fe..a58bbd7 100644 --- a/src/Compressor.cpp +++ b/src/Compressor.cpp @@ -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(compressorP[i]) ->init(fs); // compression filter parameters depend on sampling rate diff --git a/src/JackTrip.cpp b/src/JackTrip.cpp index ad5bd5b..2813da1 100644 --- a/src/JackTrip.cpp +++ b/src/JackTrip.cpp @@ -460,27 +460,27 @@ void JackTrip::setPeerAddress(const QString& PeerHostOrIP) } //******************************************************************************* -void JackTrip::appendProcessPluginToNetwork(ProcessPlugin* plugin) +void JackTrip::appendProcessPluginToNetwork(QSharedPointer& plugin) { - if (plugin) { + if (!plugin.isNull()) { mProcessPluginsToNetwork.append(plugin); // ownership transferred // mAudioInterface->appendProcessPluginToNetwork(plugin); } } //******************************************************************************* -void JackTrip::appendProcessPluginFromNetwork(ProcessPlugin* plugin) +void JackTrip::appendProcessPluginFromNetwork(QSharedPointer& plugin) { - if (plugin) { + if (!plugin.isNull()) { mProcessPluginsFromNetwork.append(plugin); // ownership transferred // mAudioInterface->appendProcessPluginFromNetwork(plugin); } } //******************************************************************************* -void JackTrip::appendProcessPluginToMonitor(ProcessPlugin* plugin) +void JackTrip::appendProcessPluginToMonitor(QSharedPointer& 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 << "/" diff --git a/src/JackTrip.h b/src/JackTrip.h index 166106b..00b9cce 100644 --- a/src/JackTrip.h +++ b/src/JackTrip.h @@ -173,9 +173,9 @@ class JackTrip : public QObject * \param plugin Pointer to ProcessPlugin Class */ // void appendProcessPlugin(const std::tr1::shared_ptr plugin); - virtual void appendProcessPluginToNetwork(ProcessPlugin* plugin); - virtual void appendProcessPluginFromNetwork(ProcessPlugin* plugin); - virtual void appendProcessPluginToMonitor(ProcessPlugin* plugin); + virtual void appendProcessPluginToNetwork(QSharedPointer& plugin); + virtual void appendProcessPluginFromNetwork(QSharedPointer& plugin); + virtual void appendProcessPluginToMonitor(QSharedPointer& 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 + QVector > mProcessPluginsFromNetwork; ///< Vector of ProcessPlugins - QVector + QVector > mProcessPluginsToNetwork; ///< Vector of ProcessPlugins - QVector + QVector > mProcessPluginsToMonitor; ///< Vector of ProcessPlugins QTimer mTimeoutTimer; QTimer mRetryTimer; diff --git a/src/JackTripWorker.h b/src/JackTripWorker.h index dc31a15..091f983 100644 --- a/src/JackTripWorker.h +++ b/src/JackTripWorker.h @@ -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 statStream) diff --git a/src/JitterBuffer.h b/src/JitterBuffer.h index a4ca931..e94ecc7 100644 --- a/src/JitterBuffer.h +++ b/src/JitterBuffer.h @@ -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: diff --git a/src/Limiter.cpp b/src/Limiter.cpp index 8796075..d803264 100644 --- a/src/Limiter.cpp +++ b/src/Limiter.cpp @@ -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(limiterP[i]) ->init(fs); // compression filter parameters depend on sampling rate diff --git a/src/Meter.cpp b/src/Meter.cpp index a641e8b..f77336b 100644 --- a/src/Meter.cpp +++ b/src/Meter.cpp @@ -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(meterP[i])->init(fs); } diff --git a/src/Monitor.cpp b/src/Monitor.cpp index 2f9a7d2..4ebf0d1 100644 --- a/src/Monitor.cpp +++ b/src/Monitor.cpp @@ -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(monitorP[i]) diff --git a/src/OscServer.cpp b/src/OscServer.cpp new file mode 100644 index 0000000..5c3b61b --- /dev/null +++ b/src/OscServer.cpp @@ -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 + +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(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 index 0000000..5b2b55c --- /dev/null +++ b/src/OscServer.h @@ -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 +#include +#include + +#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 mOscServerSocket; + quint16 mPort; +}; + +#endif diff --git a/src/ProcessPlugin.h b/src/ProcessPlugin.h index e4af0ed..f8fb64a 100644 --- a/src/ProcessPlugin.h +++ b/src/ProcessPlugin.h @@ -39,7 +39,6 @@ #define __PROCESSPLUGIN_H__ #include -#include /** \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 diff --git a/src/Regulator.cpp b/src/Regulator.cpp index cbf06d7..faffb2c 100644 --- a/src/Regulator.cpp +++ b/src/Regulator.cpp @@ -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(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; diff --git a/src/Regulator.h b/src/Regulator.h index c65c3d2..2be916f 100644 --- a/src/Regulator.h +++ b/src/Regulator.h @@ -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; diff --git a/src/Reverb.cpp b/src/Reverb.cpp index c8f3b45..d3ccdd4 100644 --- a/src/Reverb.cpp +++ b/src/Reverb.cpp @@ -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(freeverbStereoP) ->init(fs); // compression filter parameters depend on sampling rate diff --git a/src/RingBuffer.cpp b/src/RingBuffer.cpp index 370355e..ea8243c 100644 --- a/src/RingBuffer.cpp +++ b/src/RingBuffer.cpp @@ -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; +} diff --git a/src/RingBuffer.h b/src/RingBuffer.h index a35f0b2..9cc07ad 100644 --- a/src/RingBuffer.h +++ b/src/RingBuffer.h @@ -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. diff --git a/src/RtAudioInterface.cpp b/src/RtAudioInterface.cpp index 3fa35cd..8ff1599 100644 --- a/src/RtAudioInterface.cpp +++ b/src/RtAudioInterface.cpp @@ -42,6 +42,7 @@ #include #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 + || (bestResultsampleRates[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(inputBuffer); - sample_t* outputBuffer_sample = static_cast(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(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(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(inputBuffer), nframes); + AudioInterface::audioInputCallback(mInBuffer, nframes); + } + + if (outputBuffer != NULL) { + prepareOutputBuffer(static_cast(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, diff --git a/src/RtAudioInterface.h b/src/RtAudioInterface.h index b58125c..1207eb6 100644 --- a/src/RtAudioInterface.h +++ b/src/RtAudioInterface.h @@ -52,7 +52,10 @@ #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 mStereoToMonoMixerPtr; + QScopedPointer mInSrcPtr; + QScopedPointer mOutSrcPtr; + QScopedPointer 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 index 0000000..f784c74 --- /dev/null +++ b/src/SampleRateConverter.cpp @@ -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 +#include +#include + +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(mInDataPtr); + mData.src_ratio = static_cast(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(framePtr); + mData.output_frames = (BufferSizeMultiple * mBufferSize) - mOutFramesAvailable; + mData.input_frames = nframes + mInFramesLeftover; + // interleave input + float* fromPtr = reinterpret_cast(inPtr); + float* toPtr = + reinterpret_cast(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(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 index 0000000..8f1c1d7 --- /dev/null +++ b/src/SampleRateConverter.h @@ -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__ diff --git a/src/Settings.cpp b/src/Settings.cpp index 28ffb13..9ee0a43 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -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 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 outgoingEffects = mEffects.allocateOutgoingEffects(mNumAudioInputChans - nReservedChans); for (auto p : outgoingEffects) { - jackTrip->appendProcessPluginToNetwork(p); + QSharedPointer pShared(p); + jackTrip->appendProcessPluginToNetwork(pShared); } std::vector incomingEffects = mEffects.allocateIncomingEffects(mNumAudioOutputChans - nReservedChans); for (auto p : incomingEffects) { - jackTrip->appendProcessPluginFromNetwork(p); + QSharedPointer pShared(p); + jackTrip->appendProcessPluginFromNetwork(pShared); } #ifdef WAIR // WAIR diff --git a/src/SocketClient.cpp b/src/SocketClient.cpp new file mode 100644 index 0000000..a9ef002 --- /dev/null +++ b/src/SocketClient.cpp @@ -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 + +SocketClient::SocketClient(QObject* parent) + : QObject(parent), m_socket(new QLocalSocket(this)), m_owns_socket(true) +{ +} + +SocketClient::SocketClient(QSharedPointer& 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 index 0000000..281d029 --- /dev/null +++ b/src/SocketClient.h @@ -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 +#include + +// 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& 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 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 index 0000000..ed36602 --- /dev/null +++ b/src/SocketServer.cpp @@ -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 +#include + +#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 sharedSocket(connectedSocket); + handleConnection(handlerName, sharedSocket); + } +} + +void SocketServer::handleConnection(const QString& name, + QSharedPointer& 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 index 0000000..840dafe --- /dev/null +++ b/src/SocketServer.h @@ -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 +#include +#include +#include +#include +#include + +// SocketHandler is used to handle a new socket connection +typedef std::function&)> 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& socket); + + // used to listen for requests via local socket connections + QScopedPointer m_instanceServer; + + // used to handle requests + QHash m_handlers; + + // true if a local socket server was started, false if remote was detected + bool m_serverStarted = false; +}; + +#endif // __SocketServer_H__ diff --git a/src/StereoToMono.cpp b/src/StereoToMono.cpp index fa99c33..861ed4b 100644 --- a/src/StereoToMono.cpp +++ b/src/StereoToMono.cpp @@ -60,7 +60,7 @@ void StereoToMono::init(int samplingRate, int bufferSize) { ProcessPlugin::init(samplingRate, bufferSize); - fs = float(fSamplingFreq); + fs = float(mSampleRate); static_cast(stereoToMonoP)->init(fs); inited = true; diff --git a/src/Tone.cpp b/src/Tone.cpp index b003ebe..fa6af19 100644 --- a/src/Tone.cpp +++ b/src/Tone.cpp @@ -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(toneP[i])->init( diff --git a/src/UdpHubListener.cpp b/src/UdpHubListener.cpp index ecd588f..1d602dc 100644 --- a/src/UdpHubListener.cpp +++ b/src/UdpHubListener.cpp @@ -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) diff --git a/src/UdpHubListener.h b/src/UdpHubListener.h index a810025..1570180 100644 --- a/src/UdpHubListener.h +++ b/src/UdpHubListener.h @@ -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* mJTWorkers; ///< Vector of JackTripWorkers + // Pointer to OscServer + OscServer* mOscServer; SslServer mTcpServer; int mServerPort; //< Server known port number diff --git a/src/Volume.cpp b/src/Volume.cpp index 1690b07..7707bc1 100644 --- a/src/Volume.cpp +++ b/src/Volume.cpp @@ -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(volumeP[i]) diff --git a/src/gui/about.cpp b/src/gui/about.cpp index bfe7afe..c235779 100644 --- a/src/gui/about.cpp +++ b/src/gui/about.cpp @@ -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()); diff --git a/src/gui/about.ui b/src/gui/about.ui index e30f1a6..fa992b2 100644 --- a/src/gui/about.ui +++ b/src/gui/about.ui @@ -148,7 +148,7 @@ border: 4px solid black; - + diff --git a/src/gui/qjacktrip.cpp b/src/gui/qjacktrip.cpp index f7a91ec..aec0546 100644 --- a/src/gui/qjacktrip.cpp +++ b/src/gui/qjacktrip.cpp @@ -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 pluginPtr( new Compressor(numSendChannels, false, CompressorPresets::voice)); + jackTrip->appendProcessPluginToNetwork(pluginPtr); } if (m_ui->inCompressorCheckBox->isChecked()) { - jackTrip->appendProcessPluginFromNetwork( + QSharedPointer 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 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 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 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 pluginPtr( new Reverb(numRecvChannels, numRecvChannels, wetness)); + jackTrip->appendProcessPluginFromNetwork(pluginPtr); } // Limiters go last in the plugin sequence. if (m_ui->outLimiterCheckBox->isChecked()) { - jackTrip->appendProcessPluginToNetwork( + QSharedPointer pluginPtr( new Limiter(numSendChannels, m_ui->outClientsSpinBox->value())); + jackTrip->appendProcessPluginToNetwork(pluginPtr); } if (m_ui->inLimiterCheckBox->isChecked()) { - jackTrip->appendProcessPluginFromNetwork(new Limiter(numRecvChannels, 1)); + QSharedPointer 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 inputMeterPtr(new Meter(inputChannels)); + QSharedPointer 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(inputMeterPtr.get()), + &Meter::onComputedVolumeMeasurements, this, &QJackTrip::updatedInputMeasurements); - QObject::connect(outputMeter, &Meter::onComputedVolumeMeasurements, this, + QObject::connect(static_cast(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 diff --git a/src/gui/qjacktrip.ui b/src/gui/qjacktrip.ui index 0c537cc..995d809 100644 --- a/src/gui/qjacktrip.ui +++ b/src/gui/qjacktrip.ui @@ -2000,7 +2000,7 @@ To connect to a hub server you need to run as a hub client. disconnectScriptBrowse - + diff --git a/src/images/images.qrc b/src/images/images.qrc index 12f7129..90fc3dc 100644 --- a/src/images/images.qrc +++ b/src/images/images.qrc @@ -1,6 +1,7 @@ icon_256.png + icon_256.png icon_128.png icon_32.png diff --git a/src/jacktrip_globals.h b/src/jacktrip_globals.h index daeed16..c2ed233 100644 --- a/src/jacktrip_globals.h +++ b/src/jacktrip_globals.h @@ -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 //@} //******************************************************************************* diff --git a/src/vs/AudioSettings.qml b/src/vs/AudioSettings.qml index 0b56065..a02a8de 100644 --- a/src/vs/AudioSettings.qml +++ b/src/vs/AudioSettings.qml @@ -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 + } + } } } } diff --git a/src/vs/ChangeDevices.qml b/src/vs/ChangeDevices.qml index 1a8dcfe..dcff434 100644 --- a/src/vs/ChangeDevices.qml +++ b/src/vs/ChangeDevices.qml @@ -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 { diff --git a/src/vs/FeedbackSurvey.qml b/src/vs/FeedbackSurvey.qml index 05731f3..bab87a3 100644 --- a/src/vs/FeedbackSurvey.qml +++ b/src/vs/FeedbackSurvey.qml @@ -417,5 +417,9 @@ Item { userFeedbackSurvey.serverId = serverId; userFeedbackModal.open(); } + + function onCloseFeedbackSurveyModal() { + userFeedbackModal.close(); + } } } diff --git a/src/vs/Settings.qml b/src/vs/Settings.qml index e969637..cd589e2 100644 --- a/src/vs/Settings.qml +++ b/src/vs/Settings.qml @@ -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 diff --git a/src/vs/WebEngine.qml b/src/vs/WebEngine.qml index a45b459..2f15764 100644 --- a/src/vs/WebEngine.qml +++ b/src/vs/WebEngine.qml @@ -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 index 0000000..c6ab21c --- /dev/null +++ b/src/vs/balance.svg @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/vs/virtualstudio.cpp b/src/vs/virtualstudio.cpp index 6f27a67..15d2e8a 100644 --- a/src/vs/virtualstudio.cpp +++ b/src/vs/virtualstudio.cpp @@ -62,8 +62,11 @@ // https://bugreports.qt.io/browse/QTBUG-55199 #include +#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("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/ - 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/ + 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& socket) { + m_deepLinkPtr->handleVsDeeplinkRequest(socket); + }); + m_socketServerPtr->addHandler("audio", [=](QSharedPointer& 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& socket) +{ + QSharedPointer 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[]) diff --git a/src/vs/virtualstudio.h b/src/vs/virtualstudio.h index 8861af5..35987ff 100644 --- a/src/vs/virtualstudio.h +++ b/src/vs/virtualstudio.h @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,8 @@ #include #include #include +#include +#include #include #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& 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 m_socketServerPtr; QScopedPointer m_view; QSharedPointer m_deepLinkPtr; QSharedPointer 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; diff --git a/src/vs/vs.qrc b/src/vs/vs.qrc index 0b847fe..945ce72 100644 --- a/src/vs/vs.qrc +++ b/src/vs/vs.qrc @@ -50,6 +50,7 @@ language.svg micoff.svg help.svg + balance.svg quiet.svg loud.svg refresh.svg diff --git a/src/vs/vsApi.cpp b/src/vs/vsApi.cpp index 513390b..74fdaff 100644 --- a/src/vs/vsApi.cpp +++ b/src/vs/vsApi.cpp @@ -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 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 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 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 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 diff --git a/src/vs/vsApi.h b/src/vs/vsApi.h index 9b1d713..d66cab8 100644 --- a/src/vs/vsApi.h +++ b/src/vs/vsApi.h @@ -39,8 +39,10 @@ #include #include +#include #include #include +#include #include #include #include diff --git a/src/vs/vsAudio.cpp b/src/vs/vsAudio.cpp index 6964c77..d244238 100644 --- a/src/vs/vsAudio.cpp +++ b/src/vs/vsAudio.cpp @@ -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 inputVolumePluginSharedPtr(inputVolumePluginPtr); + QSharedPointer outputVolumePluginSharedPtr(outputVolumePluginPtr); + QSharedPointer inputMeterPluginSharedPtr(inputMeterPluginPtr); + QSharedPointer 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 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 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 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& 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; diff --git a/src/vs/vsAudio.h b/src/vs/vsAudio.h index c75b9fc..3521d51 100644 --- a/src/vs/vsAudio.h +++ b/src/vs/vsAudio.h @@ -39,6 +39,7 @@ #include #include +#include #include #include #include @@ -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& 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 m_permissionsPtr; QScopedPointer m_audioWorkerPtr; + QVector> 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"}; diff --git a/src/vs/vsAuth.cpp b/src/vs/vsAuth.cpp index 25d8427..0b558a5 100644 --- a/src/vs/vsAuth.cpp +++ b/src/vs/vsAuth.cpp @@ -41,6 +41,12 @@ 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); diff --git a/src/vs/vsAuth.h b/src/vs/vsAuth.h index e79116a..e075478 100644 --- a/src/vs/vsAuth.h +++ b/src/vs/vsAuth.h @@ -37,10 +37,12 @@ #ifndef VSAUTH_H #define VSAUTH_H +#include #include #include #include #include +#include #include #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 m_deviceCodeFlow; + QScopedPointer m_refreshTimer; }; #endif \ No newline at end of file diff --git a/src/vs/vsDeeplink.cpp b/src/vs/vsDeeplink.cpp index a602aba..f1448a4 100644 --- a/src/vs/vsDeeplink.cpp +++ b/src/vs/vsDeeplink.cpp @@ -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" @@ -41,14 +41,14 @@ #include #include #include +#include #include #include #include -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& 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() diff --git a/src/vs/vsDeeplink.h b/src/vs/vsDeeplink.h index 464073b..faa0192 100644 --- a/src/vs/vsDeeplink.h +++ b/src/vs/vsDeeplink.h @@ -29,84 +29,47 @@ //***************************************************************** /** - * \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 -#include -#include +#include +#include #include #include +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& 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 m_instanceCheckSocket; - - // used to listen for deeplink requests via local socket connections - QScopedPointer m_instanceServer; - - // used to synchronize with main thread at startup - bool m_isReady = false; - bool m_readyForSignals = false; - bool m_readyToExit = false; - QUrl m_deeplink; }; #endif // __VSDEEPLINK_H__ diff --git a/src/vs/vsDevice.cpp b/src/vs/vsDevice.cpp index 4f036df..8e49fd6 100644 --- a/src/vs/vsDevice.cpp +++ b/src/vs/vsDevice.cpp @@ -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() { diff --git a/src/vs/vsDevice.h b/src/vs/vsDevice.h index 2ddb7f9..64878ba 100644 --- a/src/vs/vsDevice.h +++ b/src/vs/vsDevice.h @@ -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 m_jackTrip; QRandomGenerator m_randomizer; QTimer m_sendVolumeTimer; + int m_queueBuffer = 0; bool m_networkOutage = false; bool m_stopping = false; }; diff --git a/src/vs/vsMacPermissions.mm b/src/vs/vsMacPermissions.mm index cca1300..46e3f36 100644 --- a/src/vs/vsMacPermissions.mm +++ b/src/vs/vsMacPermissions.mm @@ -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 { diff --git a/src/vs/vsPinger.cpp b/src/vs/vsPinger.cpp index 1034af0..beae7fb 100644 --- a/src/vs/vsPinger.cpp +++ b/src/vs/vsPinger.cpp @@ -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; diff --git a/src/vs/vsPinger.h b/src/vs/vsPinger.h index 3319d14..a093a53 100644 --- a/src/vs/vsPinger.h +++ b/src/vs/vsPinger.h @@ -39,6 +39,8 @@ #include #include +#include +#include #include #include #include diff --git a/src/vs/vsQuickView.cpp b/src/vs/vsQuickView.cpp index 6143cbd..1c7dbc4 100644 --- a/src/vs/vsQuickView.cpp +++ b/src/vs/vsQuickView.cpp @@ -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(); } diff --git a/src/vs/vsQuickView.h b/src/vs/vsQuickView.h index db7810d..eef2d0f 100644 --- a/src/vs/vsQuickView.h +++ b/src/vs/vsQuickView.h @@ -55,6 +55,8 @@ class VsQuickView : public QQuickView signals: void windowClose(); + void focusGained(); + void focusLost(); private slots: void closeWindow(); diff --git a/src/vs/vsWebSocket.cpp b/src/vs/vsWebSocket.cpp index 62946a0..39cd625 100644 --- a/src/vs/vsWebSocket.cpp +++ b/src/vs/vsWebSocket.cpp @@ -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 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(); diff --git a/src/vs/vsWebSocket.h b/src/vs/vsWebSocket.h index 34da9b4..b5e4c47 100644 --- a/src/vs/vsWebSocket.h +++ b/src/vs/vsWebSocket.h @@ -38,6 +38,7 @@ #define VSWEBSOCKET_H #include +#include #include #include #include diff --git a/src/vst3/JackTripVST.h b/src/vst3/JackTripVST.h new file mode 100644 index 0000000..b7ad310 --- /dev/null +++ b/src/vst3/JackTripVST.h @@ -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 index 0000000..75ce3cd --- /dev/null +++ b/src/vst3/JackTripVSTController.cpp @@ -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 index 0000000..9ce544f --- /dev/null +++ b/src/vst3/JackTripVSTController.h @@ -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 index 0000000..a4d32ce --- /dev/null +++ b/src/vst3/JackTripVSTDataBlock.h @@ -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 + +#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(block.data); + return nullptr; +} diff --git a/src/vst3/JackTripVSTEntry.cpp b/src/vst3/JackTripVSTEntry.cpp new file mode 100644 index 0000000..bbff0c4 --- /dev/null +++ b/src/vst3/JackTripVSTEntry.cpp @@ -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 index 0000000..5659b28 --- /dev/null +++ b/src/vst3/JackTripVSTProcessor.cpp @@ -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 +#include +#include + +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(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(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(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(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 index 0000000..a61bf2c --- /dev/null +++ b/src/vst3/JackTripVSTProcessor.h @@ -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 +#include +#include + +#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 mSocketPtr; + QScopedPointer 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 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 index 0000000..a1e5a3a --- /dev/null +++ b/src/vst3/resources/JackTripEditor.uidesc @@ -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 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 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 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 index 0000000..2843a8c --- /dev/null +++ b/src/vst3/resources/moduleinfo.json @@ -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 index 0000000..3294677 --- /dev/null +++ b/src/vst3/resources/win32resource.rc @@ -0,0 +1,44 @@ +#include +#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 index 0000000..4f72f59 --- /dev/null +++ b/subprojects/libsamplerate.wrap @@ -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 index 0000000..57e7776 --- /dev/null +++ b/tests/audio_socket_test.cpp @@ -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 +#include + +#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(); +} diff --git a/win/build_installer.bat b/win/build_installer.bat index a21fc61..f70c847 100755 --- a/win/build_installer.bat +++ b/win/build_installer.bat @@ -38,6 +38,16 @@ if "%~1"=="/q" ( ) if exist ..\builddir\release\jacktrip.exe (set JACKTRIP=..\builddir\release\jacktrip.exe) else (set JACKTRIP=..\builddir\jacktrip.exe) copy %JACKTRIP% deploy\ +if exist ..\builddir\JackTrip.vst3 ( + echo Including JackTrip.vst3 + mkdir deploy\JackTrip.vst3 + mkdir deploy\JackTrip.vst3\Contents + xcopy /E ..\src\vst3\resources deploy\JackTrip.vst3\Contents\Resources\ + copy ..\LICENSE.md deploy\JackTrip.vst3\Contents\Resources\LICENSE.md + xcopy /E ..\LICENSES deploy\JackTrip.vst3\Contents\Resources\LICENSES\ + mkdir deploy\JackTrip.vst3\Contents\x86_64-win + copy ..\builddir\JackTrip.vst3 deploy\JackTrip.vst3\Contents\x86_64-win\JackTrip.vst3 +) cd deploy set "WIXDEFINES=" @@ -65,17 +75,23 @@ if defined DYNAMIC_QT ( set WIXDEFINES=!WIXDEFINES! -ddynamic -dqt%QTVERSION% ) -copy ..\jacktrip.wxs .\ -copy ..\qt%QTVERSION%.wxs .\ .\jacktrip --test-gui if %ERRORLEVEL% NEQ 0 ( echo You need to build jacktrip with gui support to build the installer. exit /b 1 ) + rem Get our version number for /f "tokens=*" %%a in ('.\jacktrip -v ^| findstr VERSION') do for %%b in (%%~a) do set VERSION=%%b for /f "tokens=1 delims=-" %%a in ("%VERSION%") do set VERSION=%%a echo Version=%VERSION% -candle.exe -arch x64 -ext WixUIExtension -ext WixUtilExtension -dVersion=%VERSION%%WIXDEFINES% ..\jacktrip.wxs ..\qt%QTVERSION%.wxs -light.exe -ext WixUIExtension -ext WixUtilExtension -o JackTrip.msi jacktrip.wixobj qt%QTVERSION%.wixobj + +if exist JackTrip.vst3 ( + powershell -Command "(gc JackTrip.vst3\Contents\Resources\moduleinfo.json) -replace '%%VERSION%%', '%VERSION%' | Out-File -encoding ASCII JackTrip.vst3\Contents\Resources\moduleinfo.json" + candle.exe -arch x64 -ext WixUIExtension -ext WixUtilExtension -dvst=true -dVersion=%VERSION%%WIXDEFINES% ..\jacktrip.wxs ..\jacktrip-vst3.wxs ..\qt%QTVERSION%.wxs + light.exe -ext WixUIExtension -ext WixUtilExtension -o JackTrip.msi jacktrip.wixobj jacktrip-vst3.wixobj qt%QTVERSION%.wixobj +) else ( + candle.exe -arch x64 -ext WixUIExtension -ext WixUtilExtension -dVersion=%VERSION%%WIXDEFINES% ..\jacktrip.wxs ..\qt%QTVERSION%.wxs + light.exe -ext WixUIExtension -ext WixUtilExtension -o JackTrip.msi jacktrip.wixobj qt%QTVERSION%.wixobj +) endlocal diff --git a/win/jacktrip-vst3.wxs b/win/jacktrip-vst3.wxs new file mode 100644 index 0000000..8c017ed --- /dev/null +++ b/win/jacktrip-vst3.wxs @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/win/jacktrip.wxs b/win/jacktrip.wxs index 565f1ac..e48cdf3 100644 --- a/win/jacktrip.wxs +++ b/win/jacktrip.wxs @@ -18,6 +18,12 @@ + + + + + + @@ -34,6 +40,12 @@ + + + + + +