!docs
!meson*
!container
+!externals
!src
!subprojects
!linux
-!win
\ No newline at end of file
+!win
+!tests
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)
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:")
endif ()
file(READ "${QRC_FILE}" QRC_CONTENTS)
- string(REPLACE "<file>about@2x.png" "<file alias=\"about@2x.png\">alt/about@2x.png" QRC_CONTENTS "${QRC_CONTENTS}")
- string(REPLACE "<file>about.png" "<file alias=\"about.png\">alt/about.png" QRC_CONTENTS "${QRC_CONTENTS}")
- string(REPLACE "<file>icon.png" "<file alias=\"icon.png\">alt/icon.png" QRC_CONTENTS "${QRC_CONTENTS}")
+ string(REPLACE "<file>icon_256.png" "<file alias=\"icon_256.png\">../gui/alt/icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}")
+ string(REPLACE "<file alias=\"icon_128@2x.png\">icon_256.png" "<file alias=\"icon_128@2x.png\">../gui/alt/icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}")
+ string(REPLACE "<file>icon_128.png" "<file alias=\"icon_128.png\">../gui/alt/icon_128.png" QRC_CONTENTS "${QRC_CONTENTS}")
+ string(REPLACE "<file>icon_32.png" "<file alias=\"icon_32.png\">../gui/alt/icon_32.png" QRC_CONTENTS "${QRC_CONTENTS}")
file(WRITE "${QRC_FILE}" "${QRC_CONTENTS}")
string(TIMESTAMP BUILD_DATE "%Y%m%d")
set(BUILD_NUMBER "00")
add_compile_definitions(NDEBUG)
else ()
file(READ "${QRC_FILE}" QRC_CONTENTS)
- string(REPLACE "<file alias=\"about@2x.png\">alt/about@2x.png" "<file>about@2x.png" QRC_CONTENTS "${QRC_CONTENTS}")
- string(REPLACE "<file alias=\"about.png\">alt/about.png" "<file>about.png" QRC_CONTENTS "${QRC_CONTENTS}")
- string(REPLACE "<file alias=\"icon.png\">alt/icon.png" "<file>icon.png" QRC_CONTENTS "${QRC_CONTENTS}")
+ string(REPLACE "<file alias=\"icon_256.png\">../gui/alt/icon_256.png" "<file>icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}")
+ string(REPLACE "<file alias=\"icon_128@2x.png\">../gui/alt/icon_256.png" "<file alias=\"icon_128@2x.png\">icon_256.png" QRC_CONTENTS "${QRC_CONTENTS}")
+ string(REPLACE "<file alias=\"icon_128.png\">../gui/alt/icon_128.png" "<file>icon_128.png" QRC_CONTENTS "${QRC_CONTENTS}")
+ string(REPLACE "<file alias=\"icon_32.png\">../gui/alt/icon_32.png" "<file>icon_32.png" QRC_CONTENTS "${QRC_CONTENTS}")
file(WRITE "${QRC_FILE}" "${QRC_CONTENTS}")
endif ()
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")
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
src/DataProtocol.cpp
src/UdpDataProtocol.cpp
src/AudioInterface.cpp
+ src/AudioSocket.cpp
src/JackAudioInterface.cpp
src/JMess.cpp
src/LoopBack.cpp
src/RingBuffer.cpp
src/JitterBuffer.cpp
src/Regulator.cpp
+ src/SampleRateConverter.cpp
src/Compressor.cpp
src/Limiter.cpp
src/Reverb.cpp
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}
src/Meter.cpp
src/UserInterface.cpp
)
-
+
if (NOT novs)
set (qjacktrip_SRC ${qjacktrip_SRC}
src/vs/virtualstudio.cpp
else ()
set (qjacktrip_SRC ${qjacktrip_SRC} src/images/images.qrc)
endif ()
-
+
if (NOT noupdater)
set (qjacktrip_SRC ${qjacktrip_SRC}
src/dblsqd/feed.cpp
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 \
# 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
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
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"
;;
static)
echo "Building with static libraries"
- CONFIG="-config static $CONFIG"
+ CONFIG="-config static $CONFIG"
;;
weakjack)
echo "Building with weak linking of jack"
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
"buildsystem": "meson",
"config-opts": [
"-Dbuildtype=debugoptimized",
+ "-Dlibsamplerate=disabled",
"-Dpkg_config_path=/app/lib/x86_64-linux-gnu/pkgconfig"
],
"sources": [
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)
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
# 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:
```
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/).
> 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/).
> 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/).
+- 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:
--- /dev/null
+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)
+
--- /dev/null
+oscpp library
+
+Copyright (c) 2004-2018 Stefan Kersten <sk@declaredvolatile.org>
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
--- /dev/null
+.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
--- /dev/null
+[](https://travis-ci.org/kaoskorobase/oscpp)
+[](https://ci.appveyor.com/project/kaoskorobase/oscpp)
+
+**oscpp** is a header-only C++11 library for constructing and parsing
+[OpenSoundControl](http://opensoundcontrol.org) packets. Supported platforms
+are MacOS X, iOS, Linux, Android and Windows; the code should be easily
+portable to any platform with a C++11 compiler. **oscpp** intends to be a
+minimal, high-performance solution for working with OSC data. The library
+doesn't perform memory allocation (except when throwing exceptions) or other
+system calls and is suitable for use in realtime sensitive contexts such as
+audio driver callbacks.
+
+**oscpp** conforms to the [OpenSoundControl 1.0
+specification](http://opensoundcontrol.org/spec-1_0). Except for arrays,
+non-standard message argument types are currently not supported and there is no
+direct support for message address patterns or bundle scheduling; it is up to
+the user of the library to implement (a subset of) the semantics according to
+the spec.
+
+## Installation
+
+Since **oscpp** only consists of header files, the library doesn't need to be
+compiled or installed. Simply put the `include` directory into a location that
+is searched by your compiler and you're set.
+
+## Usage
+
+**oscpp** places everything in the `OSCPP` namespace, with the two most
+important subnamespaces `Client` for constructing packets and `Server` for
+parsing packets.
+
+First let's have a look at how to build OSC packets in memory: Assuming you
+have allocated a buffer you can construct a client packet on the stack and
+start filling the buffer with data. When all the data has been written, the
+`size` method returns the actual size in bytes of the resulting OSC packet.
+
+~~~~cpp
+#include <oscpp/client.hpp>
+
+size_t makePacket(void* buffer, size_t size)
+{
+ // Construct a packet
+ OSCPP::Client::Packet packet(buffer, size);
+ packet
+ // Open a bundle with a timetag
+ .openBundle(1234ULL)
+ // Add a message with two arguments and an array with 6 elements;
+ // for efficiency this needs to be known in advance.
+ .openMessage("/s_new", 2 + OSCPP::Tags::array(6))
+ // Write the arguments
+ .string("sinesweep")
+ .int32(2)
+ .openArray()
+ .string("start-freq")
+ .float32(330.0f)
+ .string("end-freq")
+ .float32(990.0f)
+ .string("amp")
+ .float32(0.4f)
+ .closeArray()
+ // Every `open` needs a corresponding `close`
+ .closeMessage()
+ // Add another message with one argument
+ .openMessage("/n_free", 1)
+ .int32(1)
+ .closeMessage()
+ // And nother one
+ .openMessage("/n_set", 3)
+ .int32(1)
+ .string("wobble")
+ // Numeric arguments are converted automatically
+ // (see below)
+ .int32(31)
+ .closeMessage()
+ .closeBundle();
+ return packet.size();
+}
+~~~~
+
+Now given a suitable packet transport (e.g. a UDP socket or an in-memory FIFO,
+see below for a dummy implementation), a packet can be constructed and sent as
+follows:
+
+~~~~cpp
+class Transport;
+
+size_t send(Transport* t, const void* buffer, size_t size);
+
+void sendPacket(Transport* t, void* buffer, size_t bufferSize)
+{
+ const size_t packetSize = makePacket(buffer, bufferSize);
+ send(t, buffer, packetSize);
+}
+~~~~
+
+When parsing data from OSC packets you have to handle the two distinct cases of bundles and messages:
+
+~~~~cpp
+#include <oscpp/server.hpp>
+#include <oscpp/print.hpp>
+#include <iostream>
+
+void handlePacket(const OSCPP::Server::Packet& packet)
+{
+ if (packet.isBundle()) {
+ // Convert to bundle
+ OSCPP::Server::Bundle bundle(packet);
+
+ // Print the time
+ std::cout << "#bundle " << bundle.time() << std::endl;
+
+ // Get packet stream
+ OSCPP::Server::PacketStream packets(bundle.packets());
+
+ // Iterate over all the packets and call handlePacket recursively.
+ // Cuidado: Might lead to stack overflow!
+ while (!packets.atEnd()) {
+ handlePacket(packets.next());
+ }
+ } else {
+ // Convert to message
+ OSCPP::Server::Message msg(packet);
+
+ // Get argument stream
+ OSCPP::Server::ArgStream args(msg.args());
+
+ // Directly compare message address to string with operator==.
+ // For handling larger address spaces you could use e.g. a
+ // dispatch table based on std::unordered_map.
+ if (msg == "/s_new") {
+ const char* name = args.string();
+ const int32_t id = args.int32();
+ std::cout << "/s_new" << " "
+ << name << " "
+ << id << " ";
+ // Get the params array as an ArgStream
+ OSCPP::Server::ArgStream params(args.array());
+ while (!params.atEnd()) {
+ const char* param = params.string();
+ const float value = params.float32();
+ std::cout << param << ":" << value << " ";
+ }
+ std::cout << std::endl;
+ } else if (msg == "/n_set") {
+ const int32_t id = args.int32();
+ const char* key = args.string();
+ // Numeric arguments are converted automatically
+ // to float32 (e.g. from int32).
+ const float value = args.float32();
+ std::cout << "/n_set" << " "
+ << id << " "
+ << key << " "
+ << value << std::endl;
+ } else {
+ // Simply print unknown messages
+ std::cout << "Unknown message: " << msg << std::endl;
+ }
+ }
+}
+~~~~
+
+Now we can receive data from a message based transport and pass it to our
+packet handling function:
+
+~~~~cpp
+#include <array>
+
+const size_t kMaxPacketSize = 8192;
+
+size_t recv(Transport* t, void* buffer, size_t size);
+
+void recvPacket(Transport* t)
+{
+ std::array<char,kMaxPacketSize> buffer;
+ size_t size = recv(t, buffer.data(), buffer.size());
+ handlePacket(OSCPP::Server::Packet(buffer.data(), size));
+}
+~~~~
+
+Here's our code in an example main function:
+
+~~~~cpp
+#include <memory>
+#include <stdexcept>
+
+Transport* newTransport();
+
+int main(int, char**)
+{
+ std::unique_ptr<Transport> t(newTransport());
+ std::array<char,kMaxPacketSize> sendBuffer;
+ try {
+ sendPacket(t.get(), sendBuffer.data(), sendBuffer.size());
+ recvPacket(t.get());
+ } catch (std::exception& e) {
+ std::cerr << "Exception: " << e.what() << std::endl;
+ }
+ return 0;
+}
+~~~~
+
+Compiling and running the example produces the following output:
+
+~~~~
+#bundle 1234
+/s_new sinesweep 2 start-freq:330 end-freq:990 amp:0.4
+Unknown message: /n_free i:1
+/n_set 1 wobble 31
+~~~~
+
+## How to run the example
+
+You can build and run the example by executing
+
+~~~~
+make README
+~~~~
+
+You'll need to install the [Haskell Platform](http://www.haskell.org/platform/)
+and the [Pandoc](http://johnmacfarlane.net/pandoc/) library:
+
+~~~~
+cabal install pandoc
+~~~~
+
+## Appendix: Support code
+
+Here's the code for a trivial transport that has a single packet buffer:
+
+~~~~cpp
+#include <cstring>
+
+class Transport
+{
+public:
+ size_t send(const void* buffer, size_t size)
+ {
+ size_t n = std::min(m_buffer.size(), size);
+ std::memcpy(m_buffer.data(), buffer, n);
+ m_message = n;
+ return n;
+ }
+
+ size_t recv(void* buffer, size_t size)
+ {
+ if (m_message > 0) {
+ size_t n = std::min(m_message, size);
+ std::memcpy(buffer, m_buffer.data(), n);
+ m_message = 0;
+ return n;
+ }
+ return 0;
+ }
+
+private:
+ std::array<char,kMaxPacketSize> m_buffer;
+ size_t m_message;
+};
+
+Transport* newTransport()
+{
+ return new Transport;
+}
+
+size_t send(Transport* t, const void* buffer, size_t size)
+{
+ return t->send(buffer, size);
+}
+
+size_t recv(Transport* t, void* buffer, size_t size)
+{
+ return t->recv(buffer, size);
+}
+~~~~
--- /dev/null
+# Doxyfile 1.2.15
+
+# This file describes the settings to be used by the documentation system
+# doxygen (www.doxygen.org) for a project
+#
+# All text after a hash (#) is considered a comment and will be ignored
+# The format is:
+# TAG = value [value, ...]
+# For lists items can also be appended using:
+# TAG += value [value, ...]
+# Values that contain spaces should be placed between quotes (" ")
+
+#---------------------------------------------------------------------------
+# General configuration options
+#---------------------------------------------------------------------------
+
+# The PROJECT_NAME tag is a single word (or a sequence of words surrounded
+# by quotes) that should identify the project.
+
+PROJECT_NAME = "OSC Template Library"
+
+# The PROJECT_NUMBER tag can be used to enter a project or revision number.
+# This could be handy for archiving the generated documentation or
+# if some version control system is used.
+
+PROJECT_NUMBER = "$Id"
+
+# The OUTPUT_DIRECTORY tag is used to specify the (relative or absolute)
+# base path where the generated documentation will be put.
+# If a relative path is entered, it will be relative to the location
+# where doxygen was started. If left blank the current directory will be used.
+
+OUTPUT_DIRECTORY = doc
+
+# The OUTPUT_LANGUAGE tag is used to specify the language in which all
+# documentation generated by doxygen is written. Doxygen will use this
+# information to generate all constant output in the proper language.
+# The default language is English, other supported languages are:
+# Brazilian, Chinese, Croatian, Czech, Danish, Dutch, Finnish, French,
+# German, Greek, Hungarian, Italian, Japanese, Korean, Norwegian, Polish,
+# Portuguese, Romanian, Russian, Slovak, Slovene, Spanish and Swedish.
+
+OUTPUT_LANGUAGE = English
+
+# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in
+# documentation are documented, even if no documentation was available.
+# Private class members and static file members will be hidden unless
+# the EXTRACT_PRIVATE and EXTRACT_STATIC tags are set to YES
+
+EXTRACT_ALL = NO
+
+# If the EXTRACT_PRIVATE tag is set to YES all private members of a class
+# will be included in the documentation.
+
+EXTRACT_PRIVATE = NO
+
+# If the EXTRACT_STATIC tag is set to YES all static members of a file
+# will be included in the documentation.
+
+EXTRACT_STATIC = NO
+
+# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs)
+# defined locally in source files will be included in the documentation.
+# If set to NO only classes defined in header files are included.
+
+EXTRACT_LOCAL_CLASSES = YES
+
+# If the HIDE_UNDOC_MEMBERS tag is set to YES, Doxygen will hide all
+# undocumented members of documented classes, files or namespaces.
+# If set to NO (the default) these members will be included in the
+# various overviews, but no documentation section is generated.
+# This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_MEMBERS = YES
+
+# If the HIDE_UNDOC_CLASSES tag is set to YES, Doxygen will hide all
+# undocumented classes that are normally visible in the class hierarchy.
+# If set to NO (the default) these class will be included in the various
+# overviews. This option has no effect if EXTRACT_ALL is enabled.
+
+HIDE_UNDOC_CLASSES = YES
+
+# If the BRIEF_MEMBER_DESC tag is set to YES (the default) Doxygen will
+# include brief member descriptions after the members that are listed in
+# the file and class documentation (similar to JavaDoc).
+# Set to NO to disable this.
+
+BRIEF_MEMBER_DESC = YES
+
+# If the REPEAT_BRIEF tag is set to YES (the default) Doxygen will prepend
+# the brief description of a member or function before the detailed description.
+# Note: if both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the
+# brief descriptions will be completely suppressed.
+
+REPEAT_BRIEF = YES
+
+# If the ALWAYS_DETAILED_SEC and REPEAT_BRIEF tags are both set to YES then
+# Doxygen will generate a detailed section even if there is only a brief
+# description.
+
+ALWAYS_DETAILED_SEC = NO
+
+# If the INLINE_INHERITED_MEMB tag is set to YES, doxygen will show all inherited
+# members of a class in the documentation of that class as if those members were
+# ordinary class members. Constructors, destructors and assignment operators of
+# the base classes will not be shown.
+
+INLINE_INHERITED_MEMB = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then Doxygen will prepend the full
+# path before files name in the file list and in the header files. If set
+# to NO the shortest path that makes the file name unique will be used.
+
+FULL_PATH_NAMES = NO
+
+# If the FULL_PATH_NAMES tag is set to YES then the STRIP_FROM_PATH tag
+# can be used to strip a user defined part of the path. Stripping is
+# only done if one of the specified strings matches the left-hand part of
+# the path. It is allowed to use relative paths in the argument list.
+
+STRIP_FROM_PATH =
+
+# The INTERNAL_DOCS tag determines if documentation
+# that is typed after a \internal command is included. If the tag is set
+# to NO (the default) then the documentation will be excluded.
+# Set it to YES to include the internal documentation.
+
+INTERNAL_DOCS = NO
+
+# Setting the STRIP_CODE_COMMENTS tag to YES (the default) will instruct
+# doxygen to hide any special comment blocks from generated source code
+# fragments. Normal C and C++ comments will always remain visible.
+
+STRIP_CODE_COMMENTS = YES
+
+# If the CASE_SENSE_NAMES tag is set to NO then Doxygen will only generate
+# file names in lower case letters. If set to YES upper case letters are also
+# allowed. This is useful if you have classes or files whose names only differ
+# in case and if your file system supports case sensitive file names. Windows
+# users are adviced to set this option to NO.
+
+CASE_SENSE_NAMES = YES
+
+# If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter
+# (but less readable) file names. This can be useful is your file systems
+# doesn't support long names like on DOS, Mac, or CD-ROM.
+
+SHORT_NAMES = NO
+
+# If the HIDE_SCOPE_NAMES tag is set to NO (the default) then Doxygen
+# will show members with their full class and namespace scopes in the
+# documentation. If set to YES the scope will be hidden.
+
+HIDE_SCOPE_NAMES = NO
+
+# If the VERBATIM_HEADERS tag is set to YES (the default) then Doxygen
+# will generate a verbatim copy of the header file for each class for
+# which an include is specified. Set to NO to disable this.
+
+VERBATIM_HEADERS = YES
+
+# If the SHOW_INCLUDE_FILES tag is set to YES (the default) then Doxygen
+# will put list of the files that are included by a file in the documentation
+# of that file.
+
+SHOW_INCLUDE_FILES = YES
+
+# If the JAVADOC_AUTOBRIEF tag is set to YES then Doxygen
+# will interpret the first line (until the first dot) of a JavaDoc-style
+# comment as the brief description. If set to NO, the JavaDoc
+# comments will behave just like the Qt-style comments (thus requiring an
+# explict @brief command for a brief description.
+
+JAVADOC_AUTOBRIEF = NO
+
+# If the INHERIT_DOCS tag is set to YES (the default) then an undocumented
+# member inherits the documentation from any documented member that it
+# reimplements.
+
+INHERIT_DOCS = YES
+
+# If the INLINE_INFO tag is set to YES (the default) then a tag [inline]
+# is inserted in the documentation for inline members.
+
+INLINE_INFO = YES
+
+# If the SORT_MEMBER_DOCS tag is set to YES (the default) then doxygen
+# will sort the (detailed) documentation of file and class members
+# alphabetically by member name. If set to NO the members will appear in
+# declaration order.
+
+SORT_MEMBER_DOCS = YES
+
+# If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC
+# tag is set to YES, then doxygen will reuse the documentation of the first
+# member in the group (if any) for the other members of the group. By default
+# all members of a group must be documented explicitly.
+
+DISTRIBUTE_GROUP_DOC = NO
+
+# The TAB_SIZE tag can be used to set the number of spaces in a tab.
+# Doxygen uses this value to replace tabs by spaces in code fragments.
+
+TAB_SIZE = 8
+
+# The GENERATE_TODOLIST tag can be used to enable (YES) or
+# disable (NO) the todo list. This list is created by putting \todo
+# commands in the documentation.
+
+GENERATE_TODOLIST = YES
+
+# The GENERATE_TESTLIST tag can be used to enable (YES) or
+# disable (NO) the test list. This list is created by putting \test
+# commands in the documentation.
+
+GENERATE_TESTLIST = YES
+
+# The GENERATE_BUGLIST tag can be used to enable (YES) or
+# disable (NO) the bug list. This list is created by putting \bug
+# commands in the documentation.
+
+GENERATE_BUGLIST = YES
+
+# This tag can be used to specify a number of aliases that acts
+# as commands in the documentation. An alias has the form "name=value".
+# For example adding "sideeffect=\par Side Effects:\n" will allow you to
+# put the command \sideeffect (or @sideeffect) in the documentation, which
+# will result in a user defined paragraph with heading "Side Effects:".
+# You can put \n's in the value part of an alias to insert newlines.
+
+ALIASES =
+
+# The ENABLED_SECTIONS tag can be used to enable conditional
+# documentation sections, marked by \if sectionname ... \endif.
+
+ENABLED_SECTIONS =
+
+# The MAX_INITIALIZER_LINES tag determines the maximum number of lines
+# the initial value of a variable or define consist of for it to appear in
+# the documentation. If the initializer consists of more lines than specified
+# here it will be hidden. Use a value of 0 to hide initializers completely.
+# The appearance of the initializer of individual variables and defines in the
+# documentation can be controlled using \showinitializer or \hideinitializer
+# command in the documentation regardless of this setting.
+
+MAX_INITIALIZER_LINES = 30
+
+# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
+# only. Doxygen will then generate output that is more tailored for C.
+# For instance some of the names that are used will be different. The list
+# of all members will be omitted, etc.
+
+OPTIMIZE_OUTPUT_FOR_C = NO
+
+# Set the OPTIMIZE_OUTPUT_JAVA tag to YES if your project consists of Java sources
+# only. Doxygen will then generate output that is more tailored for Java.
+# For instance namespaces will be presented as packages, qualified scopes
+# will look different, etc.
+
+OPTIMIZE_OUTPUT_JAVA = NO
+
+# Set the SHOW_USED_FILES tag to NO to disable the list of files generated
+# at the bottom of the documentation of classes and structs. If set to YES the
+# list will mention the files that were used to generate the documentation.
+
+SHOW_USED_FILES = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to warning and progress messages
+#---------------------------------------------------------------------------
+
+# The QUIET tag can be used to turn on/off the messages that are generated
+# by doxygen. Possible values are YES and NO. If left blank NO is used.
+
+QUIET = NO
+
+# The WARNINGS tag can be used to turn on/off the warning messages that are
+# generated by doxygen. Possible values are YES and NO. If left blank
+# NO is used.
+
+WARNINGS = YES
+
+# If WARN_IF_UNDOCUMENTED is set to YES, then doxygen will generate warnings
+# for undocumented members. If EXTRACT_ALL is set to YES then this flag will
+# automatically be disabled.
+
+WARN_IF_UNDOCUMENTED = YES
+
+# The WARN_FORMAT tag determines the format of the warning messages that
+# doxygen can produce. The string should contain the $file, $line, and $text
+# tags, which will be replaced by the file and line number from which the
+# warning originated and the warning text.
+
+WARN_FORMAT = "$file:$line: $text"
+
+# The WARN_LOGFILE tag can be used to specify a file to which warning
+# and error messages should be written. If left blank the output is written
+# to stderr.
+
+WARN_LOGFILE =
+
+#---------------------------------------------------------------------------
+# configuration options related to the input files
+#---------------------------------------------------------------------------
+
+# The INPUT tag can be used to specify the files and/or directories that contain
+# documented source files. You may enter file names like "myfile.cpp" or
+# directories like "/usr/src/myproject". Separate the files or directories
+# with spaces.
+
+INPUT = .
+
+# If the value of the INPUT tag contains directories, you can use the
+# FILE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank the following patterns are tested:
+# *.c *.cc *.cxx *.cpp *.c++ *.java *.ii *.ixx *.ipp *.i++ *.inl *.h *.hh *.hxx *.hpp
+# *.h++ *.idl *.odl
+
+FILE_PATTERNS = *.hh
+
+# The RECURSIVE tag can be used to turn specify whether or not subdirectories
+# should be searched for input files as well. Possible values are YES and NO.
+# If left blank NO is used.
+
+RECURSIVE = NO
+
+# The EXCLUDE tag can be used to specify files and/or directories that should
+# excluded from the INPUT source files. This way you can easily exclude a
+# subdirectory from a directory tree whose root is specified with the INPUT tag.
+
+EXCLUDE =
+
+# The EXCLUDE_SYMLINKS tag can be used select whether or not files or directories
+# that are symbolic links (a Unix filesystem feature) are excluded from the input.
+
+EXCLUDE_SYMLINKS = NO
+
+# If the value of the INPUT tag contains directories, you can use the
+# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude
+# certain files from those directories.
+
+EXCLUDE_PATTERNS =
+
+# The EXAMPLE_PATH tag can be used to specify one or more files or
+# directories that contain example code fragments that are included (see
+# the \include command).
+
+EXAMPLE_PATH =
+
+# If the value of the EXAMPLE_PATH tag contains directories, you can use the
+# EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp
+# and *.h) to filter out the source-files in the directories. If left
+# blank all files are included.
+
+EXAMPLE_PATTERNS =
+
+# If the EXAMPLE_RECURSIVE tag is set to YES then subdirectories will be
+# searched for input files to be used with the \include or \dontinclude
+# commands irrespective of the value of the RECURSIVE tag.
+# Possible values are YES and NO. If left blank NO is used.
+
+EXAMPLE_RECURSIVE = NO
+
+# The IMAGE_PATH tag can be used to specify one or more files or
+# directories that contain image that are included in the documentation (see
+# the \image command).
+
+IMAGE_PATH =
+
+# The INPUT_FILTER tag can be used to specify a program that doxygen should
+# invoke to filter for each input file. Doxygen will invoke the filter program
+# by executing (via popen()) the command <filter> <input-file>, where <filter>
+# is the value of the INPUT_FILTER tag, and <input-file> is the name of an
+# input file. Doxygen will then use the output that the filter program writes
+# to standard output.
+
+INPUT_FILTER =
+
+# If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using
+# INPUT_FILTER) will be used to filter the input files when producing source
+# files to browse.
+
+FILTER_SOURCE_FILES = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to source browsing
+#---------------------------------------------------------------------------
+
+# If the SOURCE_BROWSER tag is set to YES then a list of source files will
+# be generated. Documented entities will be cross-referenced with these sources.
+
+SOURCE_BROWSER = NO
+
+# Setting the INLINE_SOURCES tag to YES will include the body
+# of functions and classes directly in the documentation.
+
+INLINE_SOURCES = NO
+
+# If the REFERENCED_BY_RELATION tag is set to YES (the default)
+# then for each documented function all documented
+# functions referencing it will be listed.
+
+REFERENCED_BY_RELATION = YES
+
+# If the REFERENCES_RELATION tag is set to YES (the default)
+# then for each documented function all documented entities
+# called/used by that function will be listed.
+
+REFERENCES_RELATION = YES
+
+#---------------------------------------------------------------------------
+# configuration options related to the alphabetical class index
+#---------------------------------------------------------------------------
+
+# If the ALPHABETICAL_INDEX tag is set to YES, an alphabetical index
+# of all compounds will be generated. Enable this if the project
+# contains a lot of classes, structs, unions or interfaces.
+
+ALPHABETICAL_INDEX = NO
+
+# If the alphabetical index is enabled (see ALPHABETICAL_INDEX) then
+# the COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns
+# in which this list will be split (can be a number in the range [1..20])
+
+COLS_IN_ALPHA_INDEX = 5
+
+# In case all classes in a project start with a common prefix, all
+# classes will be put under the same header in the alphabetical index.
+# The IGNORE_PREFIX tag can be used to specify one or more prefixes that
+# should be ignored while generating the index headers.
+
+IGNORE_PREFIX =
+
+#---------------------------------------------------------------------------
+# configuration options related to the HTML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_HTML tag is set to YES (the default) Doxygen will
+# generate HTML output.
+
+GENERATE_HTML = YES
+
+# The HTML_OUTPUT tag is used to specify where the HTML docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `html' will be used as the default path.
+
+HTML_OUTPUT = html
+
+# The HTML_FILE_EXTENSION tag can be used to specify the file extension for
+# each generated HTML page (for example: .htm,.php,.asp). If it is left blank
+# doxygen will generate files with .html extension.
+
+HTML_FILE_EXTENSION = .html
+
+# The HTML_HEADER tag can be used to specify a personal HTML header for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard header.
+
+HTML_HEADER =
+
+# The HTML_FOOTER tag can be used to specify a personal HTML footer for
+# each generated HTML page. If it is left blank doxygen will generate a
+# standard footer.
+
+HTML_FOOTER =
+
+# The HTML_STYLESHEET tag can be used to specify a user defined cascading
+# style sheet that is used by each HTML page. It can be used to
+# fine-tune the look of the HTML output. If the tag is left blank doxygen
+# will generate a default style sheet
+
+HTML_STYLESHEET =
+
+# If the HTML_ALIGN_MEMBERS tag is set to YES, the members of classes,
+# files or namespaces will be aligned in HTML using tables. If set to
+# NO a bullet list will be used.
+
+HTML_ALIGN_MEMBERS = YES
+
+# If the GENERATE_HTMLHELP tag is set to YES, additional index files
+# will be generated that can be used as input for tools like the
+# Microsoft HTML help workshop to generate a compressed HTML help file (.chm)
+# of the generated HTML documentation.
+
+GENERATE_HTMLHELP = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the GENERATE_CHI flag
+# controls if a separate .chi index file is generated (YES) or that
+# it should be included in the master .chm file (NO).
+
+GENERATE_CHI = NO
+
+# If the GENERATE_HTMLHELP tag is set to YES, the BINARY_TOC flag
+# controls whether a binary table of contents is generated (YES) or a
+# normal table of contents (NO) in the .chm file.
+
+BINARY_TOC = NO
+
+# The TOC_EXPAND flag can be set to YES to add extra items for group members
+# to the contents of the Html help documentation and to the tree view.
+
+TOC_EXPAND = NO
+
+# The DISABLE_INDEX tag can be used to turn on/off the condensed index at
+# top of each HTML page. The value NO (the default) enables the index and
+# the value YES disables it.
+
+DISABLE_INDEX = NO
+
+# This tag can be used to set the number of enum values (range [1..20])
+# that doxygen will group on one line in the generated HTML documentation.
+
+ENUM_VALUES_PER_LINE = 4
+
+# If the GENERATE_TREEVIEW tag is set to YES, a side panel will be
+# generated containing a tree-like index structure (just like the one that
+# is generated for HTML Help). For this to work a browser that supports
+# JavaScript and frames is required (for instance Mozilla, Netscape 4.0+,
+# or Internet explorer 4.0+). Note that for large projects the tree generation
+# can take a very long time. In such cases it is better to disable this feature.
+# Windows users are probably better off using the HTML help feature.
+
+GENERATE_TREEVIEW = NO
+
+# If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be
+# used to set the initial width (in pixels) of the frame in which the tree
+# is shown.
+
+TREEVIEW_WIDTH = 250
+
+#---------------------------------------------------------------------------
+# configuration options related to the LaTeX output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_LATEX tag is set to YES (the default) Doxygen will
+# generate Latex output.
+
+GENERATE_LATEX = YES
+
+# The LATEX_OUTPUT tag is used to specify where the LaTeX docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `latex' will be used as the default path.
+
+LATEX_OUTPUT = latex
+
+# The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be invoked. If left blank `latex' will be used as the default command name.
+
+LATEX_CMD_NAME = latex
+
+# The MAKEINDEX_CMD_NAME tag can be used to specify the command name to
+# generate index for LaTeX. If left blank `makeindex' will be used as the
+# default command name.
+
+MAKEINDEX_CMD_NAME = makeindex
+
+# If the COMPACT_LATEX tag is set to YES Doxygen generates more compact
+# LaTeX documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_LATEX = NO
+
+# The PAPER_TYPE tag can be used to set the paper type that is used
+# by the printer. Possible values are: a4, a4wide, letter, legal and
+# executive. If left blank a4wide will be used.
+
+PAPER_TYPE = a4wide
+
+# The EXTRA_PACKAGES tag can be to specify one or more names of LaTeX
+# packages that should be included in the LaTeX output.
+
+EXTRA_PACKAGES =
+
+# The LATEX_HEADER tag can be used to specify a personal LaTeX header for
+# the generated latex document. The header should contain everything until
+# the first chapter. If it is left blank doxygen will generate a
+# standard header. Notice: only use this tag if you know what you are doing!
+
+LATEX_HEADER =
+
+# If the PDF_HYPERLINKS tag is set to YES, the LaTeX that is generated
+# is prepared for conversion to pdf (using ps2pdf). The pdf file will
+# contain links (just like the HTML output) instead of page references
+# This makes the output suitable for online browsing using a pdf viewer.
+
+PDF_HYPERLINKS = NO
+
+# If the USE_PDFLATEX tag is set to YES, pdflatex will be used instead of
+# plain latex in the generated Makefile. Set this option to YES to get a
+# higher quality PDF documentation.
+
+USE_PDFLATEX = NO
+
+# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \\batchmode.
+# command to the generated LaTeX files. This will instruct LaTeX to keep
+# running if errors occur, instead of asking the user for help.
+# This option is also used when generating formulas in HTML.
+
+LATEX_BATCHMODE = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the RTF output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_RTF tag is set to YES Doxygen will generate RTF output
+# The RTF output is optimised for Word 97 and may not look very pretty with
+# other RTF readers or editors.
+
+GENERATE_RTF = NO
+
+# The RTF_OUTPUT tag is used to specify where the RTF docs will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `rtf' will be used as the default path.
+
+RTF_OUTPUT = rtf
+
+# If the COMPACT_RTF tag is set to YES Doxygen generates more compact
+# RTF documents. This may be useful for small projects and may help to
+# save some trees in general.
+
+COMPACT_RTF = NO
+
+# If the RTF_HYPERLINKS tag is set to YES, the RTF that is generated
+# will contain hyperlink fields. The RTF file will
+# contain links (just like the HTML output) instead of page references.
+# This makes the output suitable for online browsing using WORD or other
+# programs which support those fields.
+# Note: wordpad (write) and others do not support links.
+
+RTF_HYPERLINKS = NO
+
+# Load stylesheet definitions from file. Syntax is similar to doxygen's
+# config file, i.e. a series of assigments. You only have to provide
+# replacements, missing definitions are set to their default value.
+
+RTF_STYLESHEET_FILE =
+
+# Set optional variables used in the generation of an rtf document.
+# Syntax is similar to doxygen's config file.
+
+RTF_EXTENSIONS_FILE =
+
+#---------------------------------------------------------------------------
+# configuration options related to the man page output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_MAN tag is set to YES (the default) Doxygen will
+# generate man pages
+
+GENERATE_MAN = NO
+
+# The MAN_OUTPUT tag is used to specify where the man pages will be put.
+# If a relative path is entered the value of OUTPUT_DIRECTORY will be
+# put in front of it. If left blank `man' will be used as the default path.
+
+MAN_OUTPUT = man
+
+# The MAN_EXTENSION tag determines the extension that is added to
+# the generated man pages (default is the subroutine's section .3)
+
+MAN_EXTENSION = .3
+
+# If the MAN_LINKS tag is set to YES and Doxygen generates man output,
+# then it will generate one additional man file for each entity
+# documented in the real man page(s). These additional files
+# only source the real man page, but without them the man command
+# would be unable to find the correct page. The default is NO.
+
+MAN_LINKS = NO
+
+#---------------------------------------------------------------------------
+# configuration options related to the XML output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_XML tag is set to YES Doxygen will
+# generate an XML file that captures the structure of
+# the code including all documentation. Note that this
+# feature is still experimental and incomplete at the
+# moment.
+
+GENERATE_XML = NO
+
+#---------------------------------------------------------------------------
+# configuration options for the AutoGen Definitions output
+#---------------------------------------------------------------------------
+
+# If the GENERATE_AUTOGEN_DEF tag is set to YES Doxygen will
+# generate an AutoGen Definitions (see autogen.sf.net) file
+# that captures the structure of the code including all
+# documentation. Note that this feature is still experimental
+# and incomplete at the moment.
+
+GENERATE_AUTOGEN_DEF = NO
+
+#---------------------------------------------------------------------------
+# Configuration options related to the preprocessor
+#---------------------------------------------------------------------------
+
+# If the ENABLE_PREPROCESSING tag is set to YES (the default) Doxygen will
+# evaluate all C-preprocessor directives found in the sources and include
+# files.
+
+ENABLE_PREPROCESSING = YES
+
+# If the MACRO_EXPANSION tag is set to YES Doxygen will expand all macro
+# names in the source code. If set to NO (the default) only conditional
+# compilation will be performed. Macro expansion can be done in a controlled
+# way by setting EXPAND_ONLY_PREDEF to YES.
+
+MACRO_EXPANSION = NO
+
+# If the EXPAND_ONLY_PREDEF and MACRO_EXPANSION tags are both set to YES
+# then the macro expansion is limited to the macros specified with the
+# PREDEFINED and EXPAND_AS_PREDEFINED tags.
+
+EXPAND_ONLY_PREDEF = NO
+
+# If the SEARCH_INCLUDES tag is set to YES (the default) the includes files
+# in the INCLUDE_PATH (see below) will be search if a #include is found.
+
+SEARCH_INCLUDES = YES
+
+# The INCLUDE_PATH tag can be used to specify one or more directories that
+# contain include files that are not input files but should be processed by
+# the preprocessor.
+
+INCLUDE_PATH =
+
+# You can use the INCLUDE_FILE_PATTERNS tag to specify one or more wildcard
+# patterns (like *.h and *.hpp) to filter out the header-files in the
+# directories. If left blank, the patterns specified with FILE_PATTERNS will
+# be used.
+
+INCLUDE_FILE_PATTERNS =
+
+# The PREDEFINED tag can be used to specify one or more macro names that
+# are defined before the preprocessor is started (similar to the -D option of
+# gcc). The argument of the tag is a list of macros of the form: name
+# or name=definition (no spaces). If the definition and the = are
+# omitted =1 is assumed.
+
+PREDEFINED =
+
+# If the MACRO_EXPANSION and EXPAND_PREDEF_ONLY tags are set to YES then
+# this tag can be used to specify a list of macro names that should be expanded.
+# The macro definition that is found in the sources will be used.
+# Use the PREDEFINED tag if you want to use a different macro definition.
+
+EXPAND_AS_DEFINED =
+
+# If the SKIP_FUNCTION_MACROS tag is set to YES (the default) then
+# doxygen's preprocessor will remove all function-like macros that are alone
+# on a line and do not end with a semicolon. Such function macros are typically
+# used for boiler-plate code, and will confuse the parser if not removed.
+
+SKIP_FUNCTION_MACROS = YES
+
+#---------------------------------------------------------------------------
+# Configuration::addtions related to external references
+#---------------------------------------------------------------------------
+
+# The TAGFILES tag can be used to specify one or more tagfiles.
+
+TAGFILES =
+
+# When a file name is specified after GENERATE_TAGFILE, doxygen will create
+# a tag file that is based on the input files it reads.
+
+GENERATE_TAGFILE =
+
+# If the ALLEXTERNALS tag is set to YES all external classes will be listed
+# in the class index. If set to NO only the inherited external classes
+# will be listed.
+
+ALLEXTERNALS = NO
+
+# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed
+# in the modules index. If set to NO, only the current project's groups will
+# be listed.
+
+EXTERNAL_GROUPS = YES
+
+# The PERL_PATH should be the absolute path and name of the perl script
+# interpreter (i.e. the result of `which perl').
+
+PERL_PATH = /usr/bin/perl
+
+#---------------------------------------------------------------------------
+# Configuration options related to the dot tool
+#---------------------------------------------------------------------------
+
+# If the CLASS_DIAGRAMS tag is set to YES (the default) Doxygen will
+# generate a inheritance diagram (in Html, RTF and LaTeX) for classes with base or
+# super classes. Setting the tag to NO turns the diagrams off. Note that this
+# option is superceded by the HAVE_DOT option below. This is only a fallback. It is
+# recommended to install and use dot, since it yield more powerful graphs.
+
+CLASS_DIAGRAMS = YES
+
+# If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is
+# available from the path. This tool is part of Graphviz, a graph visualization
+# toolkit from AT&T and Lucent Bell Labs. The other options in this section
+# have no effect if this option is set to NO (the default)
+
+HAVE_DOT = NO
+
+# If the CLASS_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect inheritance relations. Setting this tag to YES will force the
+# the CLASS_DIAGRAMS tag to NO.
+
+CLASS_GRAPH = YES
+
+# If the COLLABORATION_GRAPH and HAVE_DOT tags are set to YES then doxygen
+# will generate a graph for each documented class showing the direct and
+# indirect implementation dependencies (inheritance, containment, and
+# class references variables) of the class with other documented classes.
+
+COLLABORATION_GRAPH = YES
+
+# If set to YES, the inheritance and collaboration graphs will show the
+# relations between templates and their instances.
+
+TEMPLATE_RELATIONS = YES
+
+# If set to YES, the inheritance and collaboration graphs will hide
+# inheritance and usage relations if the target is undocumented
+# or is not a class.
+
+HIDE_UNDOC_RELATIONS = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDE_GRAPH, and HAVE_DOT
+# tags are set to YES then doxygen will generate a graph for each documented
+# file showing the direct and indirect include dependencies of the file with
+# other documented files.
+
+INCLUDE_GRAPH = YES
+
+# If the ENABLE_PREPROCESSING, SEARCH_INCLUDES, INCLUDED_BY_GRAPH, and
+# HAVE_DOT tags are set to YES then doxygen will generate a graph for each
+# documented header file showing the documented files that directly or
+# indirectly include this file.
+
+INCLUDED_BY_GRAPH = YES
+
+# If the GRAPHICAL_HIERARCHY and HAVE_DOT tags are set to YES then doxygen
+# will graphical hierarchy of all classes instead of a textual one.
+
+GRAPHICAL_HIERARCHY = YES
+
+# The DOT_IMAGE_FORMAT tag can be used to set the image format of the images
+# generated by dot. Possible values are png, jpg, or gif
+# If left blank png will be used.
+
+DOT_IMAGE_FORMAT = png
+
+# The tag DOT_PATH can be used to specify the path where the dot tool can be
+# found. If left blank, it is assumed the dot tool can be found on the path.
+
+DOT_PATH =
+
+# The DOTFILE_DIRS tag can be used to specify one or more directories that
+# contain dot files that are included in the documentation (see the
+# \dotfile command).
+
+DOTFILE_DIRS =
+
+# The MAX_DOT_GRAPH_WIDTH tag can be used to set the maximum allowed width
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than
+# this value, doxygen will try to truncate the graph, so that it fits within
+# the specified constraint. Beware that most browsers cannot cope with very
+# large images.
+
+MAX_DOT_GRAPH_WIDTH = 1024
+
+# The MAX_DOT_GRAPH_HEIGHT tag can be used to set the maximum allows height
+# (in pixels) of the graphs generated by dot. If a graph becomes larger than
+# this value, doxygen will try to truncate the graph, so that it fits within
+# the specified constraint. Beware that most browsers cannot cope with very
+# large images.
+
+MAX_DOT_GRAPH_HEIGHT = 1024
+
+# If the GENERATE_LEGEND tag is set to YES (the default) Doxygen will
+# generate a legend page explaining the meaning of the various boxes and
+# arrows in the dot generated graphs.
+
+GENERATE_LEGEND = YES
+
+# If the DOT_CLEANUP tag is set to YES (the default) Doxygen will
+# remove the intermedate dot files that are used to generate
+# the various graphs.
+
+DOT_CLEANUP = YES
+
+#---------------------------------------------------------------------------
+# Configuration::addtions related to the search engine
+#---------------------------------------------------------------------------
+
+# The SEARCHENGINE tag specifies whether or not a search engine should be
+# used. If set to NO the values of all tags below this one will be ignored.
+
+SEARCHENGINE = NO
+
+# The CGI_NAME tag should be the name of the CGI script that
+# starts the search engine (doxysearch) with the correct parameters.
+# A script with this name will be generated by doxygen.
+
+CGI_NAME = search.cgi
+
+# The CGI_URL tag should be the absolute URL to the directory where the
+# cgi binaries are located. See the documentation of your http daemon for
+# details.
+
+CGI_URL =
+
+# The DOC_URL tag should be the absolute URL to the directory where the
+# documentation is located. If left blank the absolute path to the
+# documentation, with file:// prepended to it, will be used.
+
+DOC_URL =
+
+# The DOC_ABSPATH tag should be the absolute path to the directory where the
+# documentation is located. If left blank the directory on the local machine
+# will be used.
+
+DOC_ABSPATH =
+
+# The BIN_ABSPATH tag must point to the directory where the doxysearch binary
+# is installed.
+
+BIN_ABSPATH = /usr/local/bin/
+
+# The EXT_DOC_PATHS tag can be used to specify one or more paths to
+# documentation generated for other projects. This allows doxysearch to search
+# the documentation for these projects as well.
+
+EXT_DOC_PATHS =
--- /dev/null
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_CLIENT_HPP_INCLUDED
+#define OSCPP_CLIENT_HPP_INCLUDED
+
+#include <oscpp/detail/host.hpp>
+#include <oscpp/detail/stream.hpp>
+#include <oscpp/util.hpp>
+
+#include <cstdint>
+#include <limits>
+#include <sstream>
+#include <stdexcept>
+#include <type_traits>
+
+namespace OSCPP { namespace Client {
+
+//! OSC packet construction.
+/*!
+ * Construct a valid OSC packet for transmitting over a transport
+ * medium.
+ */
+class Packet
+{
+ int32_t ptrDiff(const char* a, const char* b)
+ {
+ // Make sure pointer difference fits into int32_t
+ const intptr_t diff = a - b;
+ if (diff < std::numeric_limits<int32_t>::min() ||
+ diff > std::numeric_limits<int32_t>::max())
+ {
+ std::stringstream s;
+ s << "Pointer difference " << diff
+ << " can't be represented by int32_t";
+ throw std::logic_error(s.str());
+ }
+ return static_cast<int32_t>(diff);
+ }
+
+ int32_t calcSize(const char* begin, const char* end)
+ {
+ const int32_t size = ptrDiff(end, begin) - 4;
+ if (size < 0)
+ {
+ throw std::logic_error("Calculated size is negative");
+ }
+ return size;
+ }
+
+public:
+ //! Constructor.
+ /*!
+ */
+ Packet()
+ {
+ reset(0, 0);
+ }
+
+ //! Constructor.
+ /*!
+ */
+ Packet(void* buffer, size_t size)
+ {
+ reset(buffer, size);
+ }
+
+ //! Destructor.
+ virtual ~Packet()
+ {}
+
+ //! Get packet buffer address.
+ /*!
+ * Return the start address of the packet currently under
+ * construction.
+ */
+ void* data() const
+ {
+ return m_buffer;
+ }
+
+ size_t capacity() const
+ {
+ return m_capacity;
+ }
+
+ //! Get packet content size.
+ /*!
+ * Return the size of the packet currently under construction.
+ */
+ size_t size() const
+ {
+ return m_args.consumed();
+ }
+
+ //! Reset packet state.
+ void reset(void* buffer, size_t size)
+ {
+ checkAlignment(&m_buffer, kAlignment);
+ m_buffer = buffer;
+ m_capacity = size;
+ m_args = WriteStream(m_buffer, m_capacity);
+ m_sizePosM = m_sizePosB = nullptr;
+ m_inBundle = 0;
+ }
+
+ void reset()
+ {
+ reset(m_buffer, m_capacity);
+ }
+
+ Packet& openBundle(uint64_t time)
+ {
+ if (m_inBundle > 0)
+ {
+ assert(m_sizePosB != nullptr || m_inBundle == 1);
+ // Remember previous size pos offset
+ const int32_t offset =
+ m_sizePosB == nullptr ? 0 : ptrDiff(m_sizePosB, m_args.begin());
+ char* curPos = m_args.pos();
+ m_args.skip(4);
+ // Record size pos
+ std::memcpy(curPos, &offset, 4);
+ m_sizePosB = curPos;
+ }
+ else if (m_args.pos() != m_args.begin())
+ {
+ throw std::logic_error(
+ "Cannot open toplevel bundle in non-empty packet");
+ }
+
+ m_inBundle++;
+ m_args.putString("#bundle");
+ m_args.putUInt64(time);
+ return *this;
+ }
+
+ Packet& closeBundle()
+ {
+ if (m_inBundle > 0)
+ {
+ if (m_inBundle > 1)
+ {
+ // Get current stream pos
+ char* curPos = m_args.pos();
+
+ // Get previous bundle size stream pos
+ int32_t offset;
+ memcpy(&offset, m_sizePosB, 4);
+ // Get previous size pos
+ char* prevPos = m_args.begin() + offset;
+
+ const int32_t bundleSize = calcSize(m_sizePosB, curPos);
+ assert(bundleSize >= 0 &&
+ (size_t)bundleSize >= Size::bundle(0));
+ // Write bundle size
+ m_args.setPos(m_sizePosB);
+ m_args.putInt32(bundleSize);
+ m_args.setPos(curPos);
+
+ // record outer bundle size pos
+ m_sizePosB = prevPos;
+ }
+ m_inBundle--;
+ }
+ else
+ {
+ throw std::logic_error(
+ "closeBundle() without matching openBundle()");
+ }
+ return *this;
+ }
+
+ Packet& openMessage(const char* addr, size_t numTags)
+ {
+ if (m_inBundle > 0)
+ {
+ // record message size pos
+ m_sizePosM = m_args.pos();
+ // advance arg stream
+ m_args.skip(4);
+ }
+ m_args.putString(addr);
+ size_t sigLen = numTags + 2;
+ m_tags = WriteStream(m_args, sigLen);
+ m_args.zero(align(sigLen));
+ m_tags.putChar(',');
+ return *this;
+ }
+
+ Packet& closeMessage()
+ {
+ if (m_inBundle > 0)
+ {
+ // Get current stream pos
+ char* curPos = m_args.pos();
+ // write message size
+ m_args.setPos(m_sizePosM);
+ m_args.putInt32(calcSize(m_sizePosM, curPos));
+ // restore stream pos
+ m_args.setPos(curPos);
+ // reset tag stream
+ m_tags = WriteStream();
+ }
+ return *this;
+ }
+
+ //! Write integer message argument.
+ /*!
+ * Write a 32 bit integer message argument.
+ *
+ * \param arg 32 bit integer argument.
+ *
+ * \pre openMessage must have been called before with no intervening
+ * closeMessage.
+ *
+ * \throw OSCPP::XRunError stream buffer xrun.
+ */
+ Packet& int32(int32_t arg)
+ {
+ m_tags.putChar('i');
+ m_args.putInt32(arg);
+ return *this;
+ }
+
+ Packet& float32(float arg)
+ {
+ m_tags.putChar('f');
+ m_args.putFloat32(arg);
+ return *this;
+ }
+
+ Packet& string(const char* arg)
+ {
+ m_tags.putChar('s');
+ m_args.putString(arg);
+ return *this;
+ }
+
+ // @throw std::invalid_argument if blob size is greater than
+ // std::numeric_limits<int32_t>::max()
+ Packet& blob(const Blob& arg)
+ {
+ if (arg.size() > (size_t)std::numeric_limits<int32_t>::max())
+ {
+ throw std::invalid_argument("Blob size greater than maximum "
+ "value representable by int32_t");
+ }
+ m_tags.putChar('b');
+ m_args.putInt32(static_cast<int32_t>(arg.size()));
+ m_args.putData(arg.data(), arg.size());
+ return *this;
+ }
+
+ Packet& openArray()
+ {
+ m_tags.putChar('[');
+ return *this;
+ }
+
+ Packet& closeArray()
+ {
+ m_tags.putChar(']');
+ return *this;
+ }
+
+ template <typename T> Packet& put(T)
+ {
+ T::OSC_Client_Packet_put_unimplemented;
+ return *this;
+ }
+
+ template <typename InputIterator>
+ Packet& put(InputIterator begin, InputIterator end)
+ {
+ for (auto it = begin; it != end; it++)
+ {
+ put(*it);
+ }
+ return *this;
+ }
+
+ template <typename InputIterator>
+ Packet& putArray(InputIterator begin, InputIterator end)
+ {
+ openArray();
+ put<InputIterator>(begin, end);
+ closeArray();
+ return *this;
+ }
+
+private:
+ void* m_buffer;
+ size_t m_capacity;
+ WriteStream m_args; // packet stream
+ WriteStream m_tags; // current tag stream
+ char* m_sizePosM; // last message size position
+ char* m_sizePosB; // last bundle size position
+ size_t m_inBundle; // bundle nesting depth
+};
+
+template <> inline Packet& Packet::put<int32_t>(int32_t x)
+{
+ return int32(x);
+}
+template <> inline Packet& Packet::put<float>(float x)
+{
+ return float32(x);
+}
+template <> inline Packet& Packet::put<const char*>(const char* x)
+{
+ return string(x);
+}
+template <> inline Packet& Packet::put<Blob>(Blob x)
+{
+ return blob(x);
+}
+
+template <size_t buffer_size> class StaticPacket : public Packet
+{
+public:
+ StaticPacket()
+ : Packet(reinterpret_cast<char*>(&m_buffer), buffer_size)
+ {}
+
+private:
+ typedef typename std::aligned_storage<buffer_size, kAlignment>::type
+ AlignedBuffer;
+ AlignedBuffer m_buffer;
+};
+
+class DynamicPacket : public Packet
+{
+public:
+ DynamicPacket(size_t buffer_size)
+ : Packet(static_cast<char*>(new char[buffer_size]), buffer_size)
+ {}
+
+ ~DynamicPacket()
+ {
+ delete[] static_cast<char*>(data());
+ }
+};
+
+}} // namespace OSCPP::Client
+
+#endif // OSCPP_CLIENT_HPP_INCLUDED
--- /dev/null
+// Copyright 2005 Caleb Epstein
+// Copyright 2006 John Maddock
+// Copyright 2010 Rene Rivera
+// Distributed under the Boost Software License, Version 1.0. (See accompany-
+// ing file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+
+/*
+ * Copyright (c) 1997
+ * Silicon Graphics Computer Systems, Inc.
+ *
+ * Permission to use, copy, modify, distribute and sell this software
+ * and its documentation for any purpose is hereby granted without fee,
+ * provided that the above copyright notice appear in all copies and
+ * that both that copyright notice and this permission notice appear
+ * in supporting documentation. Silicon Graphics makes no
+ * representations about the suitability of this software for any
+ * purpose. It is provided "as is" without express or implied warranty.
+ */
+
+/*
+ * Copyright notice reproduced from <boost/detail/limits.hpp>, from
+ * which this code was originally taken.
+ *
+ * Modified by Caleb Epstein to use <endian.h> with GNU libc and to
+ * defined the BOOST_ENDIAN macro.
+ */
+
+/*
+ * Modifications for oscpp by Stefan Kersten
+ * - Change prefix from BOOST to OSCPP
+ * - Remove PDP endianness
+ * - Add OSCPP_BYTE_ORDER_* macros
+ */
+
+#ifndef OSCPP_ENDIAN_HPP_INCLUDED
+#define OSCPP_ENDIAN_HPP_INCLUDED
+
+#define OSCPP_BYTE_ORDER_BIG_ENDIAN 4321
+#define OSCPP_BYTE_ORDER_LITTLE_ENDIAN 1234
+
+// GNU libc offers the helpful header <endian.h> which defines
+// __BYTE_ORDER
+
+#if defined(__GLIBC__) || defined(__ANDROID__)
+# include <endian.h>
+# if (__BYTE_ORDER == __LITTLE_ENDIAN)
+# define OSCPP_LITTLE_ENDIAN
+# elif (__BYTE_ORDER == __BIG_ENDIAN)
+# define OSCPP_BIG_ENDIAN
+# else
+# error Unknown machine endianness detected.
+# endif
+# define OSCPP_BYTE_ORDER __BYTE_ORDER
+#elif defined(_BIG_ENDIAN) && !defined(_LITTLE_ENDIAN) || \
+ defined(__BIG_ENDIAN__) && !defined(__LITTLE_ENDIAN__) || \
+ defined(_STLP_BIG_ENDIAN) && !defined(_STLP_LITTLE_ENDIAN)
+# define OSCPP_BIG_ENDIAN
+# define OSCPP_BYTE_ORDER OSCPP_BYTE_ORDER_BIG_ENDIAN
+#elif defined(_LITTLE_ENDIAN) && !defined(_BIG_ENDIAN) || \
+ defined(__LITTLE_ENDIAN__) && !defined(__BIG_ENDIAN__) || \
+ defined(_STLP_LITTLE_ENDIAN) && !defined(_STLP_BIG_ENDIAN)
+# define OSCPP_LITTLE_ENDIAN
+# define OSCPP_BYTE_ORDER OSCPP_BYTE_ORDER_LITTLE_ENDIAN
+#elif defined(__sparc) || defined(__sparc__) || defined(_POWER) || \
+ defined(__powerpc__) || defined(__ppc__) || defined(__hpux) || \
+ defined(__hppa) || defined(_MIPSEB) || defined(_POWER) || \
+ defined(__s390__)
+# define OSCPP_BIG_ENDIAN
+# define OSCPP_BYTE_ORDER OSCPP_BYTE_ORDER_BIG_ENDIAN
+#elif defined(__i386__) || defined(__alpha__) || defined(__ia64) || \
+ defined(__ia64__) || defined(_M_IX86) || defined(_M_IA64) || \
+ defined(_M_ALPHA) || defined(__amd64) || defined(__amd64__) || \
+ defined(_M_AMD64) || defined(__x86_64) || defined(__x86_64__) || \
+ defined(_M_X64) || defined(__bfin__)
+
+# define OSCPP_LITTLE_ENDIAN
+# define OSCPP_BYTE_ORDER OSCPP_BYTE_ORDER_LITTLE_ENDIAN
+#else
+# error The file oscpp/endian.hpp needs to be set up for your CPU type.
+#endif
+
+#endif // OSCPP_ENDIAN_HPP_INCLUDED
--- /dev/null
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_HOST_HPP_INCLUDED
+#define OSCPP_HOST_HPP_INCLUDED
+
+#include <oscpp/detail/endian.hpp>
+
+#include <cstdint>
+#include <stdexcept>
+
+namespace OSCPP {
+#if defined(__GNUC__)
+inline static uint32_t bswap32(uint32_t x)
+{
+ return __builtin_bswap32(x);
+}
+inline static uint64_t bswap64(uint64_t x)
+{
+ return __builtin_bswap64(x);
+}
+#elif defined(_WINDOWS_) || defined(_WIN32)
+# include <stdlib.h>
+inline static uint32_t bswap32(uint32_t x)
+{
+ return _byteswap_ulong(x);
+}
+inline static uint64_t bswap64(uint64_t x)
+{
+ return _byteswap_uint64(x);
+}
+#else
+// Fallback implementation
+# warning Using unoptimized byte swap functions
+
+inline static uint32_t bswap32(uint32_t x)
+{
+ const uint32_t b1 = x << 24;
+ const uint32_t b2 = (x & 0x0000FF00) << 8;
+ const uint32_t b3 = (x & 0x00FF0000) >> 8;
+ const uint32_t b4 = x >> 24;
+ return b1 | b2 | b3 | b4;
+}
+inline static uint64_t bswap64(int64_t x)
+{
+ const uint64_t w1 = oscpp_bswap(uint32_t(x & 0x00000000FFFFFFFF)) << 32;
+ const uint64_t w2 = oscpp_bswap(uint32_t(x >> 32));
+ return w1 | w2;
+}
+#endif
+
+enum ByteOrder
+{
+ NetworkByteOrder,
+ HostByteOrder
+};
+
+template <ByteOrder B> inline uint32_t convert32(uint32_t)
+{
+ throw std::logic_error("Invalid byte order");
+}
+
+template <> inline uint32_t convert32<NetworkByteOrder>(uint32_t x)
+{
+#if defined(OSCPP_LITTLE_ENDIAN)
+ return bswap32(x);
+#else
+ return x;
+#endif
+}
+
+template <> inline uint32_t convert32<HostByteOrder>(uint32_t x)
+{
+ return x;
+}
+
+template <ByteOrder B> inline uint64_t convert64(uint64_t)
+{
+ throw std::logic_error("Invalid byte order");
+}
+
+template <> inline uint64_t convert64<NetworkByteOrder>(uint64_t x)
+{
+#if defined(OSCPP_LITTLE_ENDIAN)
+ return bswap64(x);
+#else
+ return x;
+#endif
+}
+
+template <> inline uint64_t convert64<HostByteOrder>(uint64_t x)
+{
+ return x;
+}
+} // namespace OSCPP
+
+#endif // OSCPP_HOST_HPP_INCLUDED
--- /dev/null
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_STREAM_HPP_INCLUDED
+#define OSCPP_STREAM_HPP_INCLUDED
+
+#include <oscpp/detail/host.hpp>
+#include <oscpp/error.hpp>
+#include <oscpp/types.hpp>
+#include <oscpp/util.hpp>
+
+#include <algorithm>
+#include <cassert>
+#include <cstdint>
+#include <cstring>
+
+namespace OSCPP {
+
+class Stream
+{
+public:
+ Stream()
+ {
+ m_begin = m_end = m_pos = 0;
+ }
+
+ Stream(void* data, size_t size)
+ {
+ m_begin = static_cast<char*>(data);
+ m_end = m_begin + size;
+ m_pos = m_begin;
+ }
+
+ Stream(const Stream& stream, size_t size)
+ {
+ m_begin = m_pos = stream.m_pos;
+ m_end = m_begin + size;
+ if (m_end > stream.m_end)
+ throw UnderrunError();
+ }
+
+ void reset()
+ {
+ m_pos = m_begin;
+ }
+
+ const char* begin() const
+ {
+ return m_begin;
+ }
+
+ char* begin()
+ {
+ return m_begin;
+ }
+
+ const char* end() const
+ {
+ return m_end;
+ }
+
+ size_t capacity() const
+ {
+ return end() - begin();
+ }
+
+ const char* pos() const
+ {
+ return m_pos;
+ }
+
+ char* pos()
+ {
+ return m_pos;
+ }
+
+ void setPos(char* pos)
+ {
+ assert((pos >= m_begin) && (pos <= m_end));
+ m_pos = pos;
+ }
+
+ void advance(size_t n)
+ {
+ m_pos += n;
+ }
+
+ bool atEnd() const
+ {
+ return pos() == end();
+ }
+
+ size_t consumed() const
+ {
+ return pos() - begin();
+ }
+
+ size_t consumable() const
+ {
+ return end() - pos();
+ }
+
+ inline void checkAlignment(size_t n) const
+ {
+ OSCPP::checkAlignment(pos(), n);
+ }
+
+protected:
+ char* m_begin;
+ char* m_end;
+ char* m_pos;
+};
+
+template <ByteOrder B> class BasicWriteStream : public Stream
+{
+public:
+ BasicWriteStream()
+ : Stream()
+ {}
+
+ BasicWriteStream(void* data, size_t size)
+ : Stream(data, size)
+ {}
+
+ BasicWriteStream(const BasicWriteStream& stream, size_t size)
+ : Stream(stream, size)
+ {}
+
+ // throw (OverflowError)
+ inline void checkWritable(size_t n) const
+ {
+ if (consumable() < n)
+ throw OverflowError(n - consumable());
+ }
+
+ void skip(size_t n)
+ {
+ checkWritable(n);
+ advance(n);
+ }
+
+ void zero(size_t n)
+ {
+ checkWritable(n);
+ std::memset(m_pos, 0, n);
+ advance(n);
+ }
+
+ void putChar(char c)
+ {
+ checkWritable(1);
+ *pos() = c;
+ advance(1);
+ }
+
+ void putInt32(int32_t x)
+ {
+ checkWritable(4);
+ checkAlignment(4);
+ uint32_t uh;
+ memcpy(&uh, &x, 4);
+ const uint32_t un = convert32<B>(uh);
+ std::memcpy(pos(), &un, 4);
+ advance(4);
+ }
+
+ void putUInt64(uint64_t x)
+ {
+ checkWritable(8);
+ const uint64_t un = convert64<B>(x);
+ std::memcpy(pos(), &un, 8);
+ advance(8);
+ }
+
+ void putFloat32(float f)
+ {
+ checkWritable(4);
+ checkAlignment(4);
+ uint32_t uh;
+ std::memcpy(&uh, &f, 4);
+ const uint32_t un = convert32<B>(uh);
+ std::memcpy(pos(), &un, 4);
+ advance(4);
+ }
+
+ void putFloat64(double f)
+ {
+ checkWritable(8);
+ checkAlignment(4);
+ uint64_t uh;
+ std::memcpy(&uh, &f, 8);
+ const uint64_t un = convert64<B>(uh);
+ std::memcpy(pos(), &un, 8);
+ advance(8);
+ }
+
+ void putData(const void* data, size_t size)
+ {
+ const size_t padding = OSCPP::padding(size);
+ const size_t n = size + padding;
+ checkWritable(n);
+ std::memcpy(pos(), data, size);
+ std::memset(pos() + size, 0, padding);
+ advance(n);
+ }
+
+ void putString(const char* s)
+ {
+ putData(s, strlen(s) + 1);
+ }
+};
+
+typedef BasicWriteStream<NetworkByteOrder> WriteStream;
+
+template <ByteOrder B> class BasicReadStream : public Stream
+{
+public:
+ BasicReadStream()
+ {}
+
+ BasicReadStream(const void* data, size_t size)
+ : Stream(const_cast<void*>(data), size)
+ {}
+
+ BasicReadStream(const BasicReadStream& stream, size_t size)
+ : Stream(stream, size)
+ {}
+
+ // throw (UnderrunError)
+ void checkReadable(size_t n) const
+ {
+ if (consumable() < n)
+ throw UnderrunError();
+ }
+
+ // throw (UnderrunError)
+ void skip(size_t n)
+ {
+ checkReadable(n);
+ advance(n);
+ }
+
+ // throw (UnderrunError)
+ inline char peekChar() const
+ {
+ checkReadable(1);
+ return *pos();
+ }
+
+ // throw (UnderrunError)
+ inline char getChar()
+ {
+ const char x = peekChar();
+ advance(1);
+ return x;
+ }
+
+ // throw (UnderrunError)
+ inline int32_t peekInt32() const
+ {
+ checkReadable(4);
+ checkAlignment(4);
+ uint32_t un;
+ std::memcpy(&un, pos(), 4);
+ const uint32_t uh = convert32<B>(un);
+ int32_t x;
+ std::memcpy(&x, &uh, 4);
+ return x;
+ }
+
+ // throw (UnderrunError)
+ inline int32_t getInt32()
+ {
+ const int32_t x = peekInt32();
+ advance(4);
+ return x;
+ }
+
+ // throw (UnderrunError)
+ inline uint64_t getUInt64()
+ {
+ checkReadable(8);
+ uint64_t un;
+ std::memcpy(&un, pos(), 8);
+ advance(8);
+ return convert64<B>(un);
+ }
+
+ // throw (UnderrunError)
+ inline float getFloat32()
+ {
+ checkReadable(4);
+ checkAlignment(4);
+ uint32_t un;
+ std::memcpy(&un, pos(), 4);
+ advance(4);
+ const uint32_t uh = convert32<B>(un);
+ float f;
+ std::memcpy(&f, &uh, 4);
+ return f;
+ }
+
+ // throw (UnderrunError)
+ inline double getFloat64()
+ {
+ checkReadable(8);
+ checkAlignment(4);
+ uint64_t un;
+ std::memcpy(&un, pos(), 8);
+ advance(8);
+ const uint64_t uh = convert64<B>(un);
+ double f;
+ std::memcpy(&f, &uh, 8);
+ return f;
+ }
+
+ // throw (UnderrunError, ParseError)
+ const char* getString()
+ {
+ checkReadable(4); // min string length
+
+ const char* ptr = static_cast<const char*>(pos()) + 3;
+ const char* end = static_cast<const char*>(this->end());
+
+ while (true)
+ {
+ if (ptr >= end)
+ throw UnderrunError();
+ if (*ptr == '\0')
+ break;
+ ptr += 4;
+ }
+
+ const char* x = pos();
+ advance(ptr - pos() + 1);
+
+ return x;
+ }
+};
+
+typedef BasicReadStream<NetworkByteOrder> ReadStream;
+} // namespace OSCPP
+
+#endif // OSCPP_STREAM_HPP_INCLUDED
--- /dev/null
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_ERROR_HPP_INCLUDED
+#define OSCPP_ERROR_HPP_INCLUDED
+
+#include <exception>
+#include <string>
+
+namespace OSCPP {
+
+class Error : public std::exception
+{
+public:
+ Error(const std::string& what)
+ : m_what(what)
+ {}
+
+ virtual ~Error() noexcept
+ {}
+
+ const char* what() const noexcept override
+ {
+ return m_what.c_str();
+ }
+
+private:
+ std::string m_what;
+};
+
+class UnderrunError : public Error
+{
+public:
+ UnderrunError()
+ : Error(std::string("Buffer underrun"))
+ {}
+};
+
+class OverflowError : public Error
+{
+public:
+ OverflowError(size_t bytes)
+ : Error(std::string("Buffer overflow"))
+ , m_bytes(bytes)
+ {}
+
+ size_t numBytes() const
+ {
+ return m_bytes;
+ }
+
+private:
+ size_t m_bytes;
+};
+
+class ParseError : public Error
+{
+public:
+ ParseError(const std::string& what = "Parse error")
+ : Error(what)
+ {}
+};
+
+} // namespace OSCPP
+
+#endif // OSCPP_ERROR_HPP_INCLUDED
--- /dev/null
+// OSCpp library
+//
+// Copyright (c) 2004-2011 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_PRINT_HPP_INCLUDED
+#define OSCPP_PRINT_HPP_INCLUDED
+
+#include <oscpp/client.hpp>
+#include <oscpp/server.hpp>
+
+#include <ostream>
+
+namespace OSCPP { namespace detail {
+
+const size_t kDefaultIndentWidth = 4;
+
+class Indent
+{
+public:
+ Indent(size_t w)
+ : m_width(w)
+ , m_indent(0)
+ {}
+ Indent(size_t w, size_t n)
+ : m_width(w)
+ , m_indent(n)
+ {}
+ Indent(const Indent&) = default;
+
+ operator size_t() const
+ {
+ return m_indent;
+ }
+ Indent inc() const
+ {
+ return Indent(m_width, m_indent + m_width);
+ }
+
+private:
+ size_t m_width;
+ size_t m_indent;
+};
+
+inline std::ostream& operator<<(std::ostream& out, const Indent& indent)
+{
+ size_t n = indent;
+ while (n-- > 0)
+ out << ' ';
+ return out;
+}
+
+inline void printArgs(std::ostream& out, Server::ArgStream args)
+{
+ while (!args.atEnd())
+ {
+ const char t = args.tag();
+ switch (t)
+ {
+ case 'i':
+ out << "i:" << args.int32();
+ break;
+ case 'f':
+ out << "f:" << args.float32();
+ break;
+ case 's':
+ out << "s:" << args.string();
+ break;
+ case 'b':
+ out << "b:" << args.blob().size();
+ break;
+ case '[':
+ out << "[ ";
+ printArgs(out, args.array());
+ out << " ]";
+ break;
+ default:
+ out << t << ":?";
+ args.drop();
+ break;
+ }
+ out << ' ';
+ }
+}
+
+inline void printMessage(std::ostream& out, const Server::Message& msg,
+ const Indent& indent)
+{
+ out << indent << msg.address() << ' ';
+ printArgs(out, msg.args());
+}
+
+inline void printBundle(std::ostream& out, const Server::Bundle& bundle,
+ const Indent& indent)
+{
+ out << indent << "# " << bundle.time() << " [" << std::endl;
+ Indent nextIndent = indent.inc();
+ auto packets = bundle.packets();
+ while (!packets.atEnd())
+ {
+ auto packet = packets.next();
+ if (packet.isMessage())
+ {
+ printMessage(out, packet, nextIndent);
+ }
+ else
+ {
+ printBundle(out, packet, nextIndent);
+ }
+ out << std::endl;
+ }
+ out << indent << "]";
+}
+
+inline void printPacket(std::ostream& out, const Server::Packet& packet,
+ const Indent& indent)
+{
+ if (packet.isMessage())
+ {
+ printMessage(out, packet, indent);
+ }
+ else
+ {
+ printBundle(out, packet, indent);
+ }
+}
+
+}} // namespace OSCPP::detail
+
+namespace OSCPP { namespace Server {
+
+inline std::ostream& operator<<(std::ostream& out, const Packet& packet)
+{
+ detail::printPacket(out, packet,
+ detail::Indent(detail::kDefaultIndentWidth));
+ return out;
+}
+
+inline std::ostream& operator<<(std::ostream& out, const Bundle& packet)
+{
+ detail::printBundle(out, packet,
+ detail::Indent(detail::kDefaultIndentWidth));
+ return out;
+}
+
+inline std::ostream& operator<<(std::ostream& out, const Message& packet)
+{
+ detail::printMessage(out, packet,
+ detail::Indent(detail::kDefaultIndentWidth));
+ return out;
+}
+
+}} // namespace OSCPP::Server
+
+namespace OSCPP { namespace Client {
+
+inline std::ostream& operator<<(std::ostream& out, const Packet& packet)
+{
+ return out << Server::Packet(packet.data(), packet.size());
+}
+
+}} // namespace OSCPP::Client
+
+#endif // OSCPP_PRINT_HPP_INCLUDED
--- /dev/null
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_SERVER_HPP_INCLUDED
+#define OSCPP_SERVER_HPP_INCLUDED
+
+#include <oscpp/detail/stream.hpp>
+#include <oscpp/util.hpp>
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <tuple>
+
+namespace OSCPP { namespace Server {
+
+//! OSC Message Argument Iterator.
+/*!
+ * Retrieve typed arguments from an incoming message.
+ *
+ * Supported tags and their correspondong types are:
+ *
+ * i -- 32 bit signed integer number<br>
+ * f -- 32 bit floating point number<br>
+ * s -- NULL-terminated string padded to 4-byte boundary<br>
+ * b -- 32-bit integer size followed by 4-byte aligned data
+ *
+ * \sa getArgInt32
+ * \sa getArgFloat32
+ * \sa getArgString
+ */
+class ArgStream
+{
+public:
+ //* Empty argument stream.
+ ArgStream() = default;
+
+ //* Construct argument stream from tag and value streams.
+ ArgStream(const ReadStream& tags, const ReadStream& args)
+ : m_tags(tags)
+ , m_args(args)
+ {}
+
+ //! Constructor.
+ /*!
+ * Read arguments from stream, which has to point to the start of a
+ * message type signature.
+ *
+ * \throw OSCPP::UnderrunError stream buffer underrun.
+ * \throw OSCPP::ParseError error while parsing input stream.
+ */
+ ArgStream(const ReadStream& stream)
+ {
+ m_args = stream;
+ const char* tags = m_args.getString();
+ if (tags[0] != ',')
+ throw ParseError("Tag string doesn't start with ','");
+ m_tags = ReadStream(tags + 1, strlen(tags) - 1);
+ }
+
+ //* Return the number of arguments that can be read from the stream.
+ size_t size() const
+ {
+ return m_tags.capacity();
+ }
+
+ //* Return true if no more arguments can be read from the stream.
+ bool atEnd() const
+ {
+ return m_tags.atEnd();
+ }
+
+ //* Return tag and argument streams.
+ std::tuple<ReadStream, ReadStream> state() const
+ {
+ return std::make_tuple(m_tags, m_args);
+ }
+
+ //* Return the type tag corresponding to the next message argument.
+ char tag() const
+ {
+ return m_tags.peekChar();
+ }
+
+ //* Drop next argument.
+ void drop()
+ {
+ drop(m_tags.getChar());
+ }
+
+ //! Get next integer argument.
+ /*!
+ * Read next numerical argument from the input stream and convert it
+ * to an integer.
+ *
+ * \exception OSCPP::UnderrunError stream buffer underrun.
+ * \exception OSCPP::ParseError argument could not be converted.
+ */
+ int32_t int32()
+ {
+ const char t = m_tags.getChar();
+ if (t == 'i')
+ return m_args.getInt32();
+ if (t == 'f')
+ return (int32_t)m_args.getFloat32();
+ throw ParseError("Cannot convert argument to int");
+ }
+
+ //! Get next float argument.
+ /*!
+ * Read next numerical argument from the input stream and convert it
+ * to a float.
+ *
+ * \exception OSCPP::UnderrunError stream buffer underrun.
+ * \exception OSCPP::ParseError argument could not be converted.
+ */
+ float float32()
+ {
+ const char t = m_tags.getChar();
+ if (t == 'f')
+ return m_args.getFloat32();
+ if (t == 'i')
+ return (float)m_args.getInt32();
+ throw ParseError("Cannot convert argument to float");
+ }
+
+ //! Get next string argument.
+ /*!
+ * Read next string argument and return it as a NULL-terminated
+ * string.
+ *
+ * \exception OSCPP::UnderrunError stream buffer underrun.
+ * \exception OSCPP::ParseError argument could not be converted or
+ * is not a valid string.
+ */
+ const char* string()
+ {
+ if (m_tags.getChar() == 's')
+ {
+ return m_args.getString();
+ }
+ throw ParseError("Cannot convert argument to string");
+ }
+
+ //* Get next blob argument.
+ //
+ // @throw OSCPP::UnderrunError stream buffer underrun.
+ // @throw OSCPP::ParseError argument is not a valid blob
+ Blob blob()
+ {
+ if (m_tags.getChar() == 'b')
+ {
+ return parseBlob();
+ }
+ else
+ {
+ throw ParseError("Cannot convert argument to blob");
+ }
+ }
+
+ //* Return a stream corresponding to an array argument.
+ ArgStream array()
+ {
+ if (m_tags.getChar() == '[')
+ {
+ const char* tags = m_tags.pos();
+ const char* args = m_args.pos();
+ dropArray();
+ // m_tags.pos() points right after the closing ']'.
+ return ArgStream(ReadStream(tags, m_tags.pos() - tags - 1),
+ ReadStream(args, m_args.pos() - args));
+ }
+ else
+ {
+ throw ParseError("Expected array");
+ }
+ }
+
+ template <typename T> T next()
+ {
+ return T::OSC_Server_ArgStream_next_unimplemented;
+ }
+
+private:
+ // Parse a blob (type tag already consumed).
+ Blob parseBlob()
+ {
+ int32_t size = m_args.getInt32();
+ if (size < 0)
+ {
+ throw ParseError("Invalid blob size is less than zero");
+ }
+ else
+ {
+ static_assert(
+ sizeof(size_t) >= sizeof(int32_t),
+ "Size of size_t must be greater than size of int32_t");
+ const void* data = m_args.pos();
+ m_args.skip(align(size));
+ return Blob(data, static_cast<size_t>(size));
+ }
+ }
+ // Drop an atomic value of type t (type tag already consumed).
+ void dropAtom(char t)
+ {
+ switch (t)
+ {
+ case 'i':
+ m_args.skip(4);
+ break;
+ case 'f':
+ m_args.skip(4);
+ break;
+ case 's':
+ m_args.getString();
+ break;
+ case 'b':
+ parseBlob();
+ break;
+ }
+ }
+ // Drop a possibly nested array.
+ void dropArray()
+ {
+ unsigned int level = 0;
+ for (;;)
+ {
+ char t = m_tags.getChar();
+ if (t == ']')
+ {
+ if (level == 0)
+ break;
+ else
+ level--;
+ }
+ else if (t == '[')
+ {
+ level++;
+ }
+ else
+ {
+ dropAtom(t);
+ }
+ }
+ }
+ // Drop the next argument of type t (type tag already consumed).
+ void drop(char t)
+ {
+ switch (t)
+ {
+ case '[':
+ dropArray();
+ break;
+ default:
+ dropAtom(t);
+ }
+ }
+
+private:
+ ReadStream m_tags;
+ ReadStream m_args;
+};
+
+class Message
+{
+public:
+ Message(const char* address, const ReadStream& stream)
+ : m_address(address)
+ , m_args(ArgStream(stream))
+ {}
+
+ const char* address() const
+ {
+ return m_address;
+ }
+
+ ArgStream args() const
+ {
+ return m_args;
+ }
+
+private:
+ const char* m_address;
+ ArgStream m_args;
+};
+
+class PacketStream;
+
+class Bundle
+{
+public:
+ Bundle(uint64_t time, const ReadStream& stream)
+ : m_time(time)
+ , m_stream(stream)
+ {}
+
+ uint64_t time() const
+ {
+ return m_time;
+ }
+
+ inline PacketStream packets() const;
+
+private:
+ uint64_t m_time;
+ ReadStream m_stream;
+};
+
+class Packet
+{
+public:
+ Packet()
+ : m_isBundle(false)
+ {}
+
+ Packet(const ReadStream& stream)
+ : m_stream(stream)
+ , m_isBundle(isBundle(stream))
+ {
+ // Skip over #bundle header
+ if (m_isBundle)
+ m_stream.skip(8);
+ }
+
+ Packet(const void* data, size_t size)
+ : Packet(ReadStream(data, size))
+ {}
+
+ const void* data() const
+ {
+ return m_stream.begin();
+ }
+
+ size_t size() const
+ {
+ return m_stream.capacity();
+ }
+
+ bool isBundle() const
+ {
+ return m_isBundle;
+ }
+
+ bool isMessage() const
+ {
+ return !isBundle();
+ }
+
+ operator Bundle() const
+ {
+ if (!isBundle())
+ throw ParseError("Packet is not a bundle");
+ ReadStream stream(m_stream);
+ uint64_t time = stream.getUInt64();
+ return Bundle(time, std::move(stream));
+ }
+
+ operator Message() const
+ {
+ if (!isMessage())
+ throw ParseError("Packet is not a message");
+ ReadStream stream(m_stream);
+ const char* address = stream.getString();
+ return Message(address, std::move(stream));
+ }
+
+ static bool isMessage(const void* data, size_t size)
+ {
+ return (size > 3) && (static_cast<const char*>(data)[0] != '#');
+ }
+
+ static bool isMessage(const ReadStream& stream)
+ {
+ return isMessage(stream.pos(), stream.consumable());
+ }
+
+ static bool isBundle(const void* data, size_t size)
+ {
+ return (size > 15) && (std::memcmp(data, "#bundle", 8) == 0);
+ }
+
+ static bool isBundle(const ReadStream& stream)
+ {
+ return isBundle(stream.pos(), stream.consumable());
+ }
+
+private:
+ ReadStream m_stream;
+ bool m_isBundle;
+};
+
+class PacketStream
+{
+public:
+ PacketStream(const ReadStream& stream)
+ : m_stream(stream)
+ {}
+
+ bool atEnd() const
+ {
+ return m_stream.atEnd();
+ }
+
+ Packet next()
+ {
+ size_t size = m_stream.getInt32();
+ ReadStream stream(m_stream, size);
+ m_stream.skip(size);
+ return Packet(stream);
+ }
+
+private:
+ ReadStream m_stream;
+};
+
+template <> inline int32_t ArgStream::next<int32_t>()
+{
+ return int32();
+}
+
+template <> inline float ArgStream::next<float>()
+{
+ return float32();
+}
+
+template <> inline const char* ArgStream::next<const char*>()
+{
+ return string();
+}
+
+template <> inline Blob ArgStream::next<Blob>()
+{
+ return blob();
+}
+
+template <> inline ArgStream ArgStream::next<ArgStream>()
+{
+ return array();
+}
+
+PacketStream Bundle::packets() const
+{
+ return PacketStream(m_stream);
+}
+
+}} // namespace OSCPP::Server
+
+static inline bool operator==(const OSCPP::Server::Message& msg,
+ const char* str)
+{
+ return strcmp(msg.address(), str) == 0;
+}
+
+static inline bool operator==(const char* str,
+ const OSCPP::Server::Message& msg)
+{
+ return msg == str;
+}
+
+static inline bool operator!=(const OSCPP::Server::Message& msg,
+ const char* str)
+{
+ return !(msg == str);
+}
+
+static inline bool operator!=(const char* str,
+ const OSCPP::Server::Message& msg)
+{
+ return msg != str;
+}
+
+#endif // OSCPP_SERVER_HPP_INCLUDED
--- /dev/null
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_TYPES_HPP_INCLUDED
+#define OSCPP_TYPES_HPP_INCLUDED
+
+namespace OSCPP {
+
+class Blob
+{
+public:
+ Blob()
+ : m_size(0)
+ , m_data(nullptr)
+ {}
+ Blob(const void* data, size_t size)
+ : m_size(size)
+ , m_data(data)
+ {}
+ Blob(const Blob& other) = default;
+
+ size_t size() const
+ {
+ return m_size;
+ }
+ const void* data() const
+ {
+ return m_data;
+ }
+
+private:
+ size_t m_size;
+ const void* m_data;
+};
+
+} // namespace OSCPP
+
+#endif // OSCPP_TYPES_HPP_INCLUDED
--- /dev/null
+// oscpp library
+//
+// Copyright (c) 2004-2013 Stefan Kersten <sk@k-hornz.de>
+//
+// Permission is hereby granted, free of charge, to any person or organization
+// obtaining a copy of the software and accompanying documentation covered by
+// this license (the "Software") to use, reproduce, display, distribute,
+// execute, and transmit the Software, and to prepare derivative works of the
+// Software, and to permit third-parties to whom the Software is furnished to
+// do so, all subject to the following:
+//
+// The copyright notices in the Software and this entire statement, including
+// the above license grant, this restriction and the following disclaimer,
+// must be included in all copies of the Software, in whole or in part, and
+// all derivative works of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+// SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+// FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+#ifndef OSCPP_UTIL_HPP_INCLUDED
+#define OSCPP_UTIL_HPP_INCLUDED
+
+#include <cassert>
+#include <cstring>
+
+namespace OSCPP {
+
+static const size_t kAlignment = 4;
+
+inline bool isAligned(const void* ptr, size_t alignment)
+{
+ return (reinterpret_cast<uintptr_t>(ptr) & (alignment - 1)) == 0;
+}
+
+constexpr bool isAligned(size_t n)
+{
+ return (n & 3) == 0;
+}
+
+constexpr size_t align(size_t n)
+{
+ return (n + 3) & -4;
+}
+
+constexpr size_t padding(size_t n)
+{
+ return align(n) - n;
+}
+
+inline void checkAlignment(const void* ptr, size_t n)
+{
+ if (!isAligned(ptr, n))
+ {
+ throw std::runtime_error("Unaligned pointer");
+ }
+}
+
+namespace Tags {
+
+constexpr size_t int32()
+{
+ return 1;
+}
+constexpr size_t float32()
+{
+ return 1;
+}
+constexpr size_t string()
+{
+ return 1;
+}
+constexpr size_t blob()
+{
+ return 1;
+}
+constexpr size_t array(size_t numElems)
+{
+ return numElems + 2;
+}
+} // namespace Tags
+
+namespace Size {
+
+class String
+{
+public:
+ String(const char* x)
+ : m_value(x)
+ {}
+
+ operator const char*() const
+ {
+ return m_value;
+ }
+
+private:
+ const char* m_value;
+};
+
+inline size_t string(const String& x)
+{
+ return align(std::strlen(x) + 1);
+}
+
+template <size_t N> constexpr size_t string(char const (&)[N])
+{
+ return align(N);
+}
+
+constexpr size_t bundle(size_t numPackets)
+{
+ return 8 /* #bundle */ + 8 /* timestamp */ +
+ 4 * numPackets /* size prefix */;
+}
+
+inline size_t message(const String& address, size_t numArgs)
+{
+ return string(address) + align(numArgs + 2);
+}
+
+template <size_t N>
+constexpr size_t message(char const (&address)[N], size_t numArgs)
+{
+ return string(address) + align(numArgs + 2);
+}
+
+constexpr size_t int32(size_t n = 1)
+{
+ return n * 4;
+}
+
+constexpr size_t float32(size_t n = 1)
+{
+ return n * 4;
+}
+
+constexpr size_t float64(size_t n = 1)
+{
+ return n * 8;
+}
+
+constexpr size_t string(size_t n)
+{
+ return align(n + 1);
+}
+
+constexpr size_t blob(size_t size)
+{
+ return 4 + align(size);
+}
+} // namespace Size
+} // namespace OSCPP
+
+#endif // OSCPP_UTIL_HPP_INCLUDED
--- /dev/null
+set(warnings -Wall -Wextra -Wno-unused-parameter -Werror)
+
+add_compile_options(
+ "$<$<CXX_COMPILER_ID:Clang>:${warnings}>"
+ "$<$<CXX_COMPILER_ID:AppleClang>:${warnings}>"
+ "$<$<CXX_COMPILER_ID:GNU>:${warnings}>"
+)
+
+# =============================================================================
+# autocheck tests
+
+add_executable(oscpp_autocheck
+ oscpp_autocheck.cpp
+)
+
+target_include_directories(oscpp_autocheck PRIVATE
+ ../include
+ autocheck/include
+)
+
+add_test(oscpp_autocheck oscpp_autocheck)
+
+add_custom_command(
+ OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/README.cpp
+ COMMAND ruby ${CMAKE_CURRENT_SOURCE_DIR}/../tools/mdcode.rb
+ ${CMAKE_CURRENT_SOURCE_DIR}/../README.md
+ ${CMAKE_CURRENT_BINARY_DIR}/README.cpp
+ MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/../README.md
+)
+
+# =============================================================================
+# README code
+
+add_executable(oscpp_readme
+ ${CMAKE_CURRENT_BINARY_DIR}/README.cpp
+)
+
+target_include_directories(oscpp_readme PRIVATE
+ ../include
+)
+
+add_test(oscpp_readme oscpp_readme)
--- /dev/null
+#include <oscpp/client.hpp>
+#include <oscpp/print.hpp>
+#include <oscpp/server.hpp>
+
+#include <autocheck/autocheck.hpp>
+#include <cstdint>
+#include <list>
+#include <memory>
+#include <string>
+
+namespace OSCPP { namespace AST {
+class Value
+{
+public:
+ virtual ~Value()
+ {}
+ virtual void print(std::ostream& out) const = 0;
+ virtual void put(OSCPP::Client::Packet& packet) const = 0;
+};
+
+template <class T> using List = std::list<std::shared_ptr<T>>;
+
+template <class T> bool equalList(const List<T>& list1, const List<T>& list2)
+{
+ if (list1.size() != list2.size())
+ return false;
+ auto it1 = list1.begin();
+ auto it2 = list2.begin();
+ while ((it1 != list1.end()) && (it2 != list2.end()))
+ {
+ if (**it1 == **it2)
+ {
+ it1++;
+ it2++;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+template <class T> void printList(std::ostream& out, const List<T>& list)
+{
+ const size_t n = list.size();
+ size_t i = 1;
+ out << '[';
+ for (auto x : list)
+ {
+ x->print(out);
+ if (i != n)
+ {
+ out << ',';
+ }
+ i++;
+ }
+ out << ']';
+}
+
+class Argument : public Value
+{
+public:
+ enum Type
+ {
+ kInt32,
+ kFloat32,
+ kString,
+ kBlob,
+ kArray,
+ };
+ static constexpr size_t kNumTypes = kArray + 1;
+
+ Argument(Type type)
+ : m_type(type)
+ {}
+
+ Type type() const
+ {
+ return m_type;
+ }
+
+ virtual size_t numTags() const
+ {
+ return 1;
+ }
+
+ static size_t numTags(const List<Argument>& args)
+ {
+ size_t n = 0;
+ for (auto x : args)
+ n += x->numTags();
+ return n;
+ }
+
+ bool operator==(const Argument& other)
+ {
+ return (type() == other.type()) && equals(other);
+ }
+
+ virtual size_t size() const = 0;
+
+protected:
+ virtual bool equals(const Argument& other) const = 0;
+
+private:
+ Type m_type;
+};
+
+class Bundle;
+class Message;
+
+class Packet : public Value
+{
+public:
+ enum Type
+ {
+ kMessage,
+ kBundle
+ };
+
+ Packet(Type type)
+ : m_type(type)
+ {}
+
+ Type type() const
+ {
+ return m_type;
+ }
+
+ virtual size_t size() const = 0;
+
+ static std::shared_ptr<Packet> parse(const OSCPP::Server::Packet& packet)
+ {
+ return packet.isBundle() ? parseBundle(packet) : parseMessage(packet);
+ }
+
+ bool operator==(const Packet& other) const
+ {
+ return type() == other.type() && equals(other);
+ }
+
+protected:
+ virtual bool equals(const Packet& other) const = 0;
+
+private:
+ static std::shared_ptr<Packet>
+ parseBundle(const OSCPP::Server::Bundle& bdl);
+ static void parseArgs(OSCPP::Server::ArgStream& inArgs,
+ List<Argument>& outArgs);
+ static std::shared_ptr<Packet>
+ parseMessage(const OSCPP::Server::Message& msg);
+
+ Type m_type;
+};
+
+class Bundle : public Packet
+{
+public:
+ Bundle(uint64_t time, List<Packet> packets)
+ : Packet(kBundle)
+ , m_time(time)
+ , m_packets(packets)
+ {
+ // assert(packets.size() > 0);
+ }
+
+ void print(std::ostream& out) const override
+ {
+ out << "Bundle(" << m_time << ", ";
+ printList(out, m_packets);
+ out << ')';
+ }
+
+ void put(OSCPP::Client::Packet& packet) const override
+ {
+ packet.openBundle(m_time);
+ for (auto p : m_packets)
+ p->put(packet);
+ packet.closeBundle();
+ }
+
+ size_t size() const override
+ {
+ size_t payload = 0;
+ for (auto x : m_packets)
+ payload += x->size();
+ assert(OSCPP::isAligned(payload));
+ return OSCPP::Size::bundle(m_packets.size()) + payload;
+ }
+
+protected:
+ bool equals(const Packet& other) const override
+ {
+ const auto& otherBundle = dynamic_cast<const Bundle&>(other);
+ return m_time == otherBundle.m_time &&
+ equalList(m_packets, otherBundle.m_packets);
+ }
+
+private:
+ uint64_t m_time;
+ List<Packet> m_packets;
+};
+
+class Message : public Packet
+{
+public:
+ Message(std::string address, List<Argument> args)
+ : Packet(kMessage)
+ , m_address(address)
+ , m_args(args)
+ {}
+
+ void print(std::ostream& out) const override
+ {
+ out << "Message(" << m_address << ", ";
+ printList(out, m_args);
+ out << ')';
+ }
+
+ void put(OSCPP::Client::Packet& packet) const override
+ {
+ packet.openMessage(m_address.c_str(), Argument::numTags(m_args));
+ for (auto x : m_args)
+ x->put(packet);
+ packet.closeMessage();
+ }
+
+ size_t size() const override
+ {
+ size_t payload = 0;
+ for (auto x : m_args)
+ payload += x->size();
+ assert(OSCPP::isAligned(payload));
+ return OSCPP::Size::message(OSCPP::Size::String(m_address.c_str()),
+ Argument::numTags(m_args)) +
+ payload;
+ }
+
+protected:
+ bool equals(const Packet& other) const override
+ {
+ const auto& otherMsg = dynamic_cast<const Message&>(other);
+ return m_address == otherMsg.m_address &&
+ equalList(m_args, otherMsg.m_args);
+ }
+
+private:
+ std::string m_address;
+ List<Argument> m_args;
+};
+
+class Int32 : public Argument
+{
+public:
+ Int32(int32_t value)
+ : Argument(kInt32)
+ , m_value(value)
+ {}
+
+ void print(std::ostream& out) const override
+ {
+ out << "i:" << m_value;
+ }
+
+ void put(OSCPP::Client::Packet& packet) const override
+ {
+ packet.put(m_value);
+ }
+
+ size_t size() const override
+ {
+ return OSCPP::Size::int32();
+ }
+
+protected:
+ bool equals(const Argument& other) const override
+ {
+ return dynamic_cast<const Int32&>(other).m_value == m_value;
+ }
+
+private:
+ int32_t m_value;
+};
+
+class Float32 : public Argument
+{
+public:
+ Float32(float value)
+ : Argument(kFloat32)
+ , m_value(value)
+ {}
+
+ void print(std::ostream& out) const override
+ {
+ out << "f:" << m_value;
+ }
+
+ void put(OSCPP::Client::Packet& packet) const override
+ {
+ packet.put(m_value);
+ }
+
+ size_t size() const override
+ {
+ return OSCPP::Size::float32();
+ }
+
+protected:
+ bool equals(const Argument& other) const override
+ {
+ return dynamic_cast<const Float32&>(other).m_value == m_value;
+ }
+
+private:
+ float m_value;
+};
+
+class String : public Argument
+{
+public:
+ String(std::string value)
+ : Argument(kString)
+ , m_value(value)
+ {}
+
+ void print(std::ostream& out) const override
+ {
+ out << "s:" << m_value;
+ }
+
+ void put(OSCPP::Client::Packet& packet) const override
+ {
+ packet.put(m_value.c_str());
+ }
+
+ size_t size() const override
+ {
+ return OSCPP::Size::string(OSCPP::Size::String(m_value.c_str()));
+ }
+
+protected:
+ bool equals(const Argument& other) const override
+ {
+ return dynamic_cast<const String&>(other).m_value == m_value;
+ }
+
+private:
+ std::string m_value;
+};
+
+class Blob : public Argument
+{
+public:
+ Blob(int32_t size, const void* data = nullptr)
+ : Argument(kBlob)
+ , m_size(std::max(0, size))
+ , m_data(nullptr)
+ {
+ if (m_size > 0)
+ {
+ m_data = new char[m_size];
+ if (data != nullptr)
+ std::memcpy(m_data, data, m_size);
+ }
+ }
+
+ Blob(OSCPP::Blob b)
+ : Blob(static_cast<int32_t>(b.size()), b.data())
+ {}
+
+ ~Blob()
+ {
+ delete[] m_data;
+ }
+
+ void print(std::ostream& out) const override
+ {
+ out << "b:" << m_size;
+ }
+
+ void put(OSCPP::Client::Packet& packet) const override
+ {
+ packet.put(OSCPP::Blob(m_data, m_size));
+ }
+
+ size_t size() const override
+ {
+ return OSCPP::Size::blob(m_size);
+ }
+
+protected:
+ bool equals(const Argument& other) const override
+ {
+ const Blob& otherBlob = dynamic_cast<const Blob&>(other);
+ return otherBlob.m_size == m_size &&
+ memcmp(m_data, otherBlob.m_data, m_size) == 0;
+ }
+
+private:
+ size_t m_size;
+ char* m_data;
+};
+
+class Array : public Argument
+{
+public:
+ Array(List<Argument> elems = List<Argument>())
+ : Argument(kArray)
+ , m_elems(elems)
+ {}
+
+ void print(std::ostream& out) const override
+ {
+ printList(out, m_elems);
+ }
+
+ void put(OSCPP::Client::Packet& packet) const override
+ {
+ packet.openArray();
+ for (auto x : m_elems)
+ x->put(packet);
+ packet.closeArray();
+ }
+
+ size_t size() const override
+ {
+ size_t payload = 0;
+ for (auto x : m_elems)
+ payload += x->size();
+ assert(OSCPP::isAligned(payload));
+ return payload;
+ }
+
+ size_t numTags() const override
+ {
+ return OSCPP::Tags::array(Argument::numTags(m_elems));
+ }
+
+protected:
+ bool equals(const Argument& other) const override
+ {
+ return equalList(m_elems, dynamic_cast<const Array&>(other).m_elems);
+ }
+
+private:
+ List<Argument> m_elems;
+};
+
+std::shared_ptr<Packet> Packet::parseBundle(const OSCPP::Server::Bundle& bdl)
+{
+ List<Packet> outPackets;
+ OSCPP::Server::PacketStream inPackets(bdl.packets());
+ while (!inPackets.atEnd())
+ {
+ outPackets.push_back(parse(inPackets.next()));
+ }
+ return std::make_shared<Bundle>(bdl.time(), std::move(outPackets));
+}
+
+void Packet::parseArgs(OSCPP::Server::ArgStream& inArgs,
+ List<Argument>& outArgs)
+{
+ while (!inArgs.atEnd())
+ {
+ switch (inArgs.tag())
+ {
+ case 'i':
+ outArgs.push_back(std::make_shared<Int32>(inArgs.int32()));
+ break;
+ case 'f':
+ outArgs.push_back(std::make_shared<Float32>(inArgs.float32()));
+ break;
+ case 's':
+ outArgs.push_back(std::make_shared<String>(inArgs.string()));
+ break;
+ case 'b':
+ outArgs.push_back(std::make_shared<Blob>(inArgs.blob()));
+ break;
+ case '[':
+ {
+ OSCPP::Server::ArgStream inElems(inArgs.array());
+ List<Argument> outElems;
+ parseArgs(inElems, outElems);
+ outArgs.push_back(std::make_shared<Array>(outElems));
+ }
+ break;
+ }
+ }
+}
+
+std::shared_ptr<Packet> Packet::parseMessage(const OSCPP::Server::Message& msg)
+{
+ OSCPP::Server::ArgStream inArgs(msg.args());
+ List<Argument> outArgs;
+ parseArgs(inArgs, outArgs);
+ return std::make_shared<Message>(msg.address(), outArgs);
+}
+
+std::ostream& operator<<(std::ostream& out, const Packet& packet)
+{
+ packet.print(out);
+ return out;
+}
+
+std::ostream& operator<<(std::ostream& out,
+ const std::shared_ptr<Packet>& packet)
+{
+ packet->print(out);
+ return out;
+}
+}} // namespace OSCPP::AST
+
+namespace ac = autocheck;
+
+namespace OSCPP { namespace AutoCheck {
+
+struct MessageArgListGen
+{
+ typedef AST::List<AST::Argument> result_type;
+ result_type operator()(size_t size) const;
+};
+
+struct MessageArgGen
+{
+ typedef std::shared_ptr<AST::Argument> result_type;
+ result_type operator()(size_t size) const
+ {
+ AST::Argument::Type argType = static_cast<AST::Argument::Type>(
+ ac::generator<size_t>()(AST::Argument::kNumTypes - 1));
+ switch (argType)
+ {
+ case AST::Argument::kInt32:
+ return std::make_shared<AST::Int32>(
+ ac::generator<int32_t>()(size));
+ case AST::Argument::kFloat32:
+ return std::make_shared<AST::Float32>(
+ ac::generator<float>()(size));
+ case AST::Argument::kString:
+ return std::make_shared<AST::String>(
+ ac::string<ac::ccPrintable>()(std::max<size_t>(1, size)));
+ case AST::Argument::kBlob:
+ return std::make_shared<AST::Blob>(
+ ac::generator<int32_t>()(size));
+ case AST::Argument::kArray:
+ // Exponential size backoff
+ return std::make_shared<AST::Array>(
+ MessageArgListGen()(size / 2));
+ default:
+ throw std::logic_error("Invalid AST::Argument::Type value");
+ }
+ const bool InvalidArgumentType = false;
+ assert(InvalidArgumentType);
+ }
+};
+
+MessageArgListGen::result_type MessageArgListGen::operator()(size_t size) const
+{
+ const auto& elems = ac::list_of(MessageArgGen())(size);
+ return AST::List<AST::Argument>(elems.begin(), elems.end());
+}
+
+struct PacketGen
+{
+ // ac::generator<std::shared_ptr<oscpp::AST::Packet>> source;
+ typedef std::shared_ptr<AST::Packet> result_type;
+ result_type operator()(size_t size) const
+ {
+ return ac::generator<bool>()(size) ? gen_bundle(size)
+ : gen_message(size);
+ }
+
+ result_type gen_bundle(size_t size) const
+ {
+ const auto& packets = ac::list_of(PacketGen())(size / 2);
+ return std::make_shared<AST::Bundle>(
+ ac::generator<uint64_t>()(size),
+ AST::List<AST::Packet>(packets.begin(), packets.end()));
+ }
+
+ std::string gen_message_address(size_t size) const
+ {
+ std::string result(
+ ac::string<ac::ccAlphaNumeric>()(std::max<size_t>(2, size)));
+ if (result[0] != '/')
+ result[0] = '/';
+ return result;
+ }
+
+ result_type gen_message(size_t size) const
+ {
+ return std::make_shared<AST::Message>(gen_message_address(size),
+ MessageArgListGen()(size));
+ }
+};
+}} // namespace OSCPP::AutoCheck
+
+bool prop_identity(const std::shared_ptr<OSCPP::AST::Packet>& packet1)
+{
+ // packet1->print(std::cerr); std::cerr << "\n";
+ const size_t size = packet1->size();
+ std::unique_ptr<char[]> data(new char[size]);
+ OSCPP::Client::Packet clientPacket(data.get(), size);
+ packet1->put(clientPacket);
+ OSCPP::Server::Packet serverPacket(clientPacket.data(),
+ clientPacket.size());
+ auto packet2 = OSCPP::AST::Packet::parse(serverPacket);
+ using namespace OSCPP::AST;
+ if (!(*packet1 == *packet2))
+ {
+ std::cerr << packet1 << std::endl;
+ std::cerr << packet2 << std::endl;
+ return false;
+ }
+ return true;
+}
+
+bool prop_overflow(const std::shared_ptr<OSCPP::AST::Packet>& packet,
+ size_t inBufferSize)
+{
+ const size_t packetSize = packet->size();
+ const size_t bufferSize =
+ inBufferSize == 0
+ ? 1
+ : (inBufferSize < packetSize ? inBufferSize : packetSize - 1);
+ std::cerr << "bufferSize " << bufferSize << std::endl;
+ std::unique_ptr<char[]> data(new char[bufferSize]);
+ OSCPP::Client::Packet clientPacket(data.get(), bufferSize);
+ bool result = false;
+ try
+ {
+ packet->put(clientPacket);
+ }
+ catch (OSCPP::OverflowError&)
+ {
+ result = true;
+ }
+ catch (std::exception& e)
+ {
+ std::cerr << "Exception: " << e.what() << std::endl;
+ }
+ return result;
+}
+
+int main(int argc, char** argv)
+{
+ using namespace OSCPP::AST;
+ using namespace OSCPP::AutoCheck;
+ ac::check<std::shared_ptr<Packet>>(prop_identity, 150,
+ ac::make_arbitrary(PacketGen()));
+ // ac::check<std::shared_ptr<Packet>,size_t>(
+ // prop_overflow,
+ // 150,
+ // ac::make_arbitrary(PacketGen(), ac::generator<size_t>())
+ // );
+ return 0;
+}
--- /dev/null
+#!/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
+
--- /dev/null
+#!/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
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 \
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
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
}
--- /dev/null
+# 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 /
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:
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:
```
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>BuildMachineOSBuild</key>
+ <string>19E287</string>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleExecutable</key>
+ <string>JackTrip.vst3</string>
+ <key>CFBundleGetInfoString</key>
+ <string>JackTrip Audio Bridge</string>
+ <key>CFBundleIconFile</key>
+ <string>jacktrip</string>
+ <key>CFBundleIdentifier</key>
+ <string>%BUNDLEID%</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleLongVersionString</key>
+ <string></string>
+ <key>CFBundleName</key>
+ <string>%BUNDLENAME%</string>
+ <key>CFBundlePackageType</key>
+ <string>BNDL</string>
+ <key>CFBundleShortVersionString</key>
+ <string></string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>%VERSION%</string>
+ <key>CSResourcesFileMapped</key>
+ <true/>
+ <key>NSHumanReadableCopyright</key>
+ <string>Copyright © 2024-2025 JackTrip Labs, Inc.</string>
+</dict>
+</plist>
--- /dev/null
+BNDL????
\ No newline at end of file
TEMP_KEYCHAIN=""
USE_DEFAULT_KEYCHAIN=false
BINARY="../builddir/jacktrip"
+VST_BINARY="../builddir/$APPNAME.vst3"
PSI=false
OPTIND=1
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.
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.
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
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
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>PACKAGES</key>
+ <array>
+ <dict>
+ <key>MUST-CLOSE-APPLICATION-ITEMS</key>
+ <array/>
+ <key>MUST-CLOSE-APPLICATIONS</key>
+ <false/>
+ <key>PACKAGE_FILES</key>
+ <dict>
+ <key>DEFAULT_INSTALL_LOCATION</key>
+ <string>/</string>
+ <key>HIERARCHY</key>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Jack</string>
+ <key>PATH_TYPE</key>
+ <integer>2</integer>
+ <key>PERMISSIONS</key>
+ <integer>509</integer>
+ <key>TYPE</key>
+ <integer>2</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>BUNDLE_CAN_DOWNGRADE</key>
+ <false/>
+ <key>BUNDLE_POSTINSTALL_PATH</key>
+ <dict>
+ <key>PATH</key>
+ <string>postinstall.sh</string>
+ <key>PATH_TYPE</key>
+ <integer>1</integer>
+ </dict>
+ <key>BUNDLE_PREINSTALL_PATH</key>
+ <dict>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>../%BUNDLENAME%.app</string>
+ <key>PATH_TYPE</key>
+ <integer>1</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>3</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Utilities</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Applications</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>509</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>bin</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Application Support</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Audio</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Automator</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>ColorPickers</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Documentation</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Extensions</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Filesystems</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Fonts</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>1021</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Frameworks</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Input Methods</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Internet Plug-Ins</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>LaunchAgents</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>LaunchDaemons</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>PreferencePanes</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Preferences</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Printers</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>PrivilegedHelperTools</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>1005</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>QuickLook</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>QuickTime</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Screen Savers</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Scripts</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Services</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Widgets</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Library</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>etc</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>var</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>private</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>sbin</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Extensions</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Library</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>System</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Shared</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>1023</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Users</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>bin</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>include</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>lib</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>bin</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>local</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>sbin</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>share</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>usr</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>-1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>/</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <key>PAYLOAD_TYPE</key>
+ <integer>0</integer>
+ <key>PRESERVE_EXTENDED_ATTRIBUTES</key>
+ <false/>
+ <key>SHOW_INVISIBLE</key>
+ <true/>
+ <key>SPLIT_FORKS</key>
+ <true/>
+ <key>TREAT_MISSING_FILES_AS_WARNING</key>
+ <false/>
+ <key>VERSION</key>
+ <integer>5</integer>
+ </dict>
+ <key>PACKAGE_SCRIPTS</key>
+ <dict>
+ <key>POSTINSTALL_PATH</key>
+ <dict>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <key>PREINSTALL_PATH</key>
+ <dict>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <key>RESOURCES</key>
+ <array/>
+ </dict>
+ <key>PACKAGE_SETTINGS</key>
+ <dict>
+ <key>AUTHENTICATION</key>
+ <integer>1</integer>
+ <key>CONCLUSION_ACTION</key>
+ <integer>0</integer>
+ <key>FOLLOW_SYMBOLIC_LINKS</key>
+ <false/>
+ <key>IDENTIFIER</key>
+ <string>%BUNDLEID%</string>
+ <key>LOCATION</key>
+ <integer>0</integer>
+ <key>NAME</key>
+ <string>%BUNDLENAME%</string>
+ <key>OVERWRITE_PERMISSIONS</key>
+ <true/>
+ <key>PAYLOAD_SIZE</key>
+ <integer>-1</integer>
+ <key>REFERENCE_PATH</key>
+ <string></string>
+ <key>RELOCATABLE</key>
+ <false/>
+ <key>USE_HFS+_COMPRESSION</key>
+ <false/>
+ <key>VERSION</key>
+ <string>%VERSION%</string>
+ </dict>
+ <key>TYPE</key>
+ <integer>0</integer>
+ <key>UUID</key>
+ <string>10E1CE8D-C84E-45FC-81DA-B174548AE779</string>
+ </dict>
+ <dict>
+ <key>MUST-CLOSE-APPLICATION-ITEMS</key>
+ <array/>
+ <key>MUST-CLOSE-APPLICATIONS</key>
+ <false/>
+ <key>PACKAGE_FILES</key>
+ <dict>
+ <key>DEFAULT_INSTALL_LOCATION</key>
+ <string>/</string>
+ <key>HIERARCHY</key>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Applications</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>509</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Application Support</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>BUNDLE_CAN_DOWNGRADE</key>
+ <false/>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>../%BUNDLENAME%.vst3</string>
+ <key>PATH_TYPE</key>
+ <integer>1</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>3</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>VST3</string>
+ <key>PATH_TYPE</key>
+ <integer>2</integer>
+ <key>PERMISSIONS</key>
+ <integer>509</integer>
+ <key>TYPE</key>
+ <integer>2</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Plug-Ins</string>
+ <key>PATH_TYPE</key>
+ <integer>2</integer>
+ <key>PERMISSIONS</key>
+ <integer>509</integer>
+ <key>TYPE</key>
+ <integer>2</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Audio</string>
+ <key>PATH_TYPE</key>
+ <integer>2</integer>
+ <key>PERMISSIONS</key>
+ <integer>509</integer>
+ <key>TYPE</key>
+ <integer>2</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Automator</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Documentation</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Extensions</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Filesystems</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Frameworks</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Input Methods</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Internet Plug-Ins</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Keyboard Layouts</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>LaunchAgents</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>LaunchDaemons</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>PreferencePanes</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Preferences</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Printers</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>PrivilegedHelperTools</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>1005</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>QuickLook</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>QuickTime</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Screen Savers</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Scripts</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Services</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Widgets</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Library</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>CHILDREN</key>
+ <array>
+ <dict>
+ <key>CHILDREN</key>
+ <array/>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>Shared</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>1023</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>80</integer>
+ <key>PATH</key>
+ <string>Users</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>GID</key>
+ <integer>0</integer>
+ <key>PATH</key>
+ <string>/</string>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ <key>PERMISSIONS</key>
+ <integer>493</integer>
+ <key>TYPE</key>
+ <integer>1</integer>
+ <key>UID</key>
+ <integer>0</integer>
+ </dict>
+ <key>PAYLOAD_TYPE</key>
+ <integer>0</integer>
+ <key>PRESERVE_EXTENDED_ATTRIBUTES</key>
+ <false/>
+ <key>SHOW_INVISIBLE</key>
+ <false/>
+ <key>SPLIT_FORKS</key>
+ <true/>
+ <key>TREAT_MISSING_FILES_AS_WARNING</key>
+ <false/>
+ <key>VERSION</key>
+ <integer>5</integer>
+ </dict>
+ <key>PACKAGE_SCRIPTS</key>
+ <dict>
+ <key>POSTINSTALL_PATH</key>
+ <dict>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <key>PREINSTALL_PATH</key>
+ <dict>
+ <key>PATH_TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <key>RESOURCES</key>
+ <array/>
+ </dict>
+ <key>PACKAGE_SETTINGS</key>
+ <dict>
+ <key>AUTHENTICATION</key>
+ <integer>1</integer>
+ <key>CONCLUSION_ACTION</key>
+ <integer>0</integer>
+ <key>FOLLOW_SYMBOLIC_LINKS</key>
+ <false/>
+ <key>IDENTIFIER</key>
+ <string>%BUNDLEID%.vst3</string>
+ <key>LOCATION</key>
+ <integer>0</integer>
+ <key>NAME</key>
+ <string>%BUNDLENAME%.vst3</string>
+ <key>OVERWRITE_PERMISSIONS</key>
+ <true/>
+ <key>PAYLOAD_SIZE</key>
+ <integer>-1</integer>
+ <key>REFERENCE_PATH</key>
+ <string></string>
+ <key>RELOCATABLE</key>
+ <false/>
+ <key>USE_HFS+_COMPRESSION</key>
+ <false/>
+ <key>VERSION</key>
+ <string>%VERSION%</string>
+ </dict>
+ <key>TYPE</key>
+ <integer>0</integer>
+ <key>UUID</key>
+ <string>8CAD404A-E34F-469F-93DD-D2D2125F35F5</string>
+ </dict>
+ </array>
+ <key>PROJECT</key>
+ <dict>
+ <key>PROJECT_COMMENTS</key>
+ <dict>
+ <key>NOTES</key>
+ <data>
+ </data>
+ </dict>
+ <key>PROJECT_PRESENTATION</key>
+ <dict>
+ <key>BACKGROUND</key>
+ <dict>
+ <key>APPAREANCES</key>
+ <dict>
+ <key>DARK_AQUA</key>
+ <dict/>
+ <key>LIGHT_AQUA</key>
+ <dict/>
+ </dict>
+ <key>SHARED_SETTINGS_FOR_ALL_APPAREANCES</key>
+ <true/>
+ </dict>
+ <key>INSTALLATION_STEPS</key>
+ <array>
+ <dict>
+ <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+ <string>ICPresentationViewIntroductionController</string>
+ <key>INSTALLER_PLUGIN</key>
+ <string>Introduction</string>
+ <key>LIST_TITLE_KEY</key>
+ <string>InstallerSectionTitle</string>
+ </dict>
+ <dict>
+ <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+ <string>ICPresentationViewReadMeController</string>
+ <key>INSTALLER_PLUGIN</key>
+ <string>ReadMe</string>
+ <key>LIST_TITLE_KEY</key>
+ <string>InstallerSectionTitle</string>
+ </dict>
+ <dict>
+ <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+ <string>ICPresentationViewLicenseController</string>
+ <key>INSTALLER_PLUGIN</key>
+ <string>License</string>
+ <key>LIST_TITLE_KEY</key>
+ <string>InstallerSectionTitle</string>
+ </dict>
+ <dict>
+ <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+ <string>ICPresentationViewDestinationSelectController</string>
+ <key>INSTALLER_PLUGIN</key>
+ <string>TargetSelect</string>
+ <key>LIST_TITLE_KEY</key>
+ <string>InstallerSectionTitle</string>
+ </dict>
+ <dict>
+ <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+ <string>ICPresentationViewInstallationTypeController</string>
+ <key>INSTALLER_PLUGIN</key>
+ <string>PackageSelection</string>
+ <key>LIST_TITLE_KEY</key>
+ <string>InstallerSectionTitle</string>
+ </dict>
+ <dict>
+ <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+ <string>ICPresentationViewInstallationController</string>
+ <key>INSTALLER_PLUGIN</key>
+ <string>Install</string>
+ <key>LIST_TITLE_KEY</key>
+ <string>InstallerSectionTitle</string>
+ </dict>
+ <dict>
+ <key>ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS</key>
+ <string>ICPresentationViewSummaryController</string>
+ <key>INSTALLER_PLUGIN</key>
+ <string>Summary</string>
+ <key>LIST_TITLE_KEY</key>
+ <string>InstallerSectionTitle</string>
+ </dict>
+ </array>
+ <key>INTRODUCTION</key>
+ <dict>
+ <key>LOCALIZATIONS</key>
+ <array/>
+ </dict>
+ <key>LICENSE</key>
+ <dict>
+ <key>LOCALIZATIONS</key>
+ <array>
+ <dict>
+ <key>LANGUAGE</key>
+ <string>English</string>
+ <key>VALUE</key>
+ <dict>
+ <key>PATH</key>
+ <string>license.txt</string>
+ <key>PATH_TYPE</key>
+ <integer>3</integer>
+ </dict>
+ </dict>
+ </array>
+ <key>MODE</key>
+ <integer>0</integer>
+ </dict>
+ <key>README</key>
+ <dict>
+ <key>LOCALIZATIONS</key>
+ <array>
+ <dict>
+ <key>LANGUAGE</key>
+ <string>English</string>
+ <key>VALUE</key>
+ <dict>
+ <key>PATH</key>
+ <string>readme.txt</string>
+ <key>PATH_TYPE</key>
+ <integer>3</integer>
+ </dict>
+ </dict>
+ </array>
+ </dict>
+ <key>TITLE</key>
+ <dict>
+ <key>LOCALIZATIONS</key>
+ <array/>
+ </dict>
+ </dict>
+ <key>PROJECT_REQUIREMENTS</key>
+ <dict>
+ <key>LIST</key>
+ <array/>
+ <key>RESOURCES</key>
+ <array/>
+ <key>ROOT_VOLUME_ONLY</key>
+ <false/>
+ </dict>
+ <key>PROJECT_SETTINGS</key>
+ <dict>
+ <key>BUILD_FORMAT</key>
+ <integer>0</integer>
+ <key>BUILD_PATH</key>
+ <dict>
+ <key>PATH</key>
+ <string>build</string>
+ <key>PATH_TYPE</key>
+ <integer>1</integer>
+ </dict>
+ <key>EXCLUDED_FILES</key>
+ <array>
+ <dict>
+ <key>PATTERNS_ARRAY</key>
+ <array>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.DS_Store</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>PROTECTED</key>
+ <true/>
+ <key>PROXY_NAME</key>
+ <string>Remove .DS_Store files</string>
+ <key>PROXY_TOOLTIP</key>
+ <string>Remove ".DS_Store" files created by the Finder.</string>
+ <key>STATE</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>PATTERNS_ARRAY</key>
+ <array>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.pbdevelopment</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>PROTECTED</key>
+ <true/>
+ <key>PROXY_NAME</key>
+ <string>Remove .pbdevelopment files</string>
+ <key>PROXY_TOOLTIP</key>
+ <string>Remove ".pbdevelopment" files created by ProjectBuilder or Xcode.</string>
+ <key>STATE</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>PATTERNS_ARRAY</key>
+ <array>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>CVS</string>
+ <key>TYPE</key>
+ <integer>1</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.cvsignore</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.cvspass</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.svn</string>
+ <key>TYPE</key>
+ <integer>1</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.git</string>
+ <key>TYPE</key>
+ <integer>1</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>.gitignore</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>PROTECTED</key>
+ <true/>
+ <key>PROXY_NAME</key>
+ <string>Remove SCM metadata</string>
+ <key>PROXY_TOOLTIP</key>
+ <string>Remove helper files and folders used by the CVS, SVN or Git Source Code Management systems.</string>
+ <key>STATE</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>PATTERNS_ARRAY</key>
+ <array>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>classes.nib</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>designable.db</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>info.nib</string>
+ <key>TYPE</key>
+ <integer>0</integer>
+ </dict>
+ </array>
+ <key>PROTECTED</key>
+ <true/>
+ <key>PROXY_NAME</key>
+ <string>Optimize nib files</string>
+ <key>PROXY_TOOLTIP</key>
+ <string>Remove "classes.nib", "info.nib" and "designable.nib" files within .nib bundles.</string>
+ <key>STATE</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>PATTERNS_ARRAY</key>
+ <array>
+ <dict>
+ <key>REGULAR_EXPRESSION</key>
+ <false/>
+ <key>STRING</key>
+ <string>Resources Disabled</string>
+ <key>TYPE</key>
+ <integer>1</integer>
+ </dict>
+ </array>
+ <key>PROTECTED</key>
+ <true/>
+ <key>PROXY_NAME</key>
+ <string>Remove Resources Disabled folders</string>
+ <key>PROXY_TOOLTIP</key>
+ <string>Remove "Resources Disabled" folders.</string>
+ <key>STATE</key>
+ <true/>
+ </dict>
+ <dict>
+ <key>SEPARATOR</key>
+ <true/>
+ </dict>
+ </array>
+ <key>NAME</key>
+ <string>%BUNDLENAME%</string>
+ <key>PAYLOAD_ONLY</key>
+ <false/>
+ <key>TREAT_MISSING_PRESENTATION_DOCUMENTS_AS_WARNING</key>
+ <false/>
+ </dict>
+ </dict>
+ <key>TYPE</key>
+ <integer>0</integer>
+ <key>VERSION</key>
+ <integer>2</integer>
+</dict>
+</plist>
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
PACKAGE_CERT=""
USERNAME=""
PASSWORD=""
+# Only needed if you belong to more than one dev team
TEAM_ID=""
if [ -z $1 ]; then
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
src = [ 'src/DataProtocol.cpp',
'src/JackTrip.cpp',
'src/ProcessPlugin.cpp',
+ 'src/AudioSocket.cpp',
'src/AudioTester.cpp',
'src/jacktrip_globals.cpp',
'src/JackTripWorker.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',
moc_h = ['src/DataProtocol.h',
'src/JackTrip.h',
'src/ProcessPlugin.h',
+ 'src/AudioSocket.h',
'src/Meter.h',
'src/Monitor.h',
'src/StereoToMono.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')]
'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'
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
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
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
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()
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')
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')
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
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));
#include <cmath>
#include <iostream>
+#include "AudioSocket.h"
#include "JackTrip.h"
+#include "ProcessPlugin.h"
using std::cout;
using std::endl;
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();
}
//*******************************************************************************
}
#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++) {
#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());
}
/// 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());
}
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());
}
}
+ 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();
}
//*******************************************************************************
-void AudioInterface::appendProcessPluginToNetwork(ProcessPlugin* plugin)
+void AudioInterface::appendProcessPluginToNetwork(QSharedPointer<ProcessPlugin>& plugin)
{
- if (!plugin) {
+ if (plugin.isNull()) {
return;
}
mProcessPluginsToNetwork.append(plugin);
}
-void AudioInterface::appendProcessPluginFromNetwork(ProcessPlugin* plugin)
+void AudioInterface::appendProcessPluginFromNetwork(QSharedPointer<ProcessPlugin>& plugin)
{
- if (!plugin) {
+ if (plugin.isNull()) {
return;
}
mProcessPluginsFromNetwork.append(plugin);
}
-void AudioInterface::appendProcessPluginToMonitor(ProcessPlugin* plugin)
+void AudioInterface::appendProcessPluginToMonitor(QSharedPointer<ProcessPlugin>& plugin)
{
- if (!plugin) {
+ if (plugin.isNull()) {
return;
}
mProcessPluginsToMonitor.append(plugin);
}
+void AudioInterface::appendAudioSocket(QSharedPointer<AudioSocket>& s)
+{
+ if (s.isNull())
+ return;
+ static_cast<FromAudioSocketPlugin*>(s->getFromAudioSocketPlugin().data())
+ ->setPassthrough(true);
+ mAudioSockets.append(s);
+}
+
void AudioInterface::initPlugins(bool verbose)
{
const int nChansIn = (MIXTOMONO == mInputMixMode) ? 1 : mInputChans.size();
const int nChansOut = mOutputChans.size();
const int nChansMon = getNumMonChannels();
int nPlugins = mProcessPluginsFromNetwork.size() + mProcessPluginsToNetwork.size()
- + mProcessPluginsToMonitor.size();
+ + mProcessPluginsToMonitor.size() + (mAudioSockets.size() * 2);
if (nPlugins > 0) {
if (verbose) {
std::cout << "Initializing Faust plugins (have " << nPlugins
<< ") at sampling rate " << mSampleRate << "\n";
}
- for (ProcessPlugin* plugin : std::as_const(mProcessPluginsFromNetwork)) {
+ for (auto& plugin : qAsConst(mProcessPluginsFromNetwork)) {
plugin->setOutgoingToNetwork(false);
plugin->updateNumChannels(nChansIn, nChansOut);
plugin->init(mSampleRate, mBufferSizeInSamples);
}
- for (ProcessPlugin* plugin : std::as_const(mProcessPluginsToNetwork)) {
+ for (auto& plugin : qAsConst(mProcessPluginsToNetwork)) {
plugin->setOutgoingToNetwork(true);
plugin->updateNumChannels(nChansIn, nChansOut);
plugin->init(mSampleRate, mBufferSizeInSamples);
}
- for (ProcessPlugin* plugin : std::as_const(mProcessPluginsToMonitor)) {
+ for (auto& plugin : qAsConst(mProcessPluginsToMonitor)) {
plugin->setOutgoingToNetwork(false);
plugin->updateNumChannels(nChansMon, nChansMon);
plugin->init(mSampleRate, mBufferSizeInSamples);
}
+ for (auto& s : qAsConst(mAudioSockets)) {
+ auto* plugin = s->getFromAudioSocketPlugin().get();
+ plugin->setOutgoingToNetwork(true);
+ plugin->updateNumChannels(nChansIn, nChansOut);
+ plugin->init(mSampleRate, mBufferSizeInSamples);
+ plugin = s->getToAudioSocketPlugin().get();
+ plugin->setOutgoingToNetwork(false);
+ plugin->updateNumChannels(nChansIn, nChansOut);
+ plugin->init(mSampleRate, mBufferSizeInSamples);
+ }
}
}
#ifndef __AUDIOINTERFACE_H__
#define __AUDIOINTERFACE_H__
+#include <QSharedPointer>
#include <QVarLengthArray>
#include <QVector>
#include <functional>
#include "AudioTester.h"
-#include "ProcessPlugin.h"
#include "WaitFreeFrameBuffer.h"
#include "jacktrip_types.h"
// Forward declarations
+class AudioSocket;
class JackTrip;
+class ProcessPlugin;
// using namespace JackTripNamespace;
* using something like:\n
* <tt>std::tr1::shared_ptr<ProcessPluginName> loopback(new ProcessPluginName);</tt>
*/
- virtual void appendProcessPluginToNetwork(ProcessPlugin* plugin);
+ virtual void appendProcessPluginToNetwork(QSharedPointer<ProcessPlugin>& plugin);
/** \brief appendProcessPluginFromNetwork():
* Same as appendProcessPluginToNetwork() except that these plugins operate
* -> remote JackTrip server
* -> JackTrip client -> processPlugin from network -> JACK -> audio
*/
- virtual void appendProcessPluginFromNetwork(ProcessPlugin* plugin);
+ virtual void appendProcessPluginFromNetwork(QSharedPointer<ProcessPlugin>& plugin);
/** \brief appendProcessPluginToMonitor():
* Appends plugins used for local monitoring
*/
- virtual void appendProcessPluginToMonitor(ProcessPlugin* plugin);
+ virtual void appendProcessPluginToMonitor(QSharedPointer<ProcessPlugin>& plugin);
+
+ /** \brief appendAudioSocket():
+ * Appends audio socket connections
+ */
+ virtual void appendAudioSocket(QSharedPointer<AudioSocket>& s);
/** \brief initPlugins():
* Initialize all ProcessPlugin modules.
std::string mInputDeviceName, mOutputDeviceName; ///< RTAudio device names
uint32_t mBufferSizeInSamples; ///< Buffer size in samples
size_t mSizeInBytesPerChannel; ///< Size in bytes per audio channel
- QVector<ProcessPlugin*>
+ QVector<QSharedPointer<ProcessPlugin> >
mProcessPluginsFromNetwork; ///< Vector of ProcessPlugin<EM>s</EM>
- QVector<ProcessPlugin*>
+ QVector<QSharedPointer<ProcessPlugin> >
mProcessPluginsToNetwork; ///< Vector of ProcessPlugin<EM>s</EM>
- QVector<ProcessPlugin*>
+ QVector<QSharedPointer<ProcessPlugin> >
mProcessPluginsToMonitor; ///< Vector of ProcessPlugin<EM>s</EM>
+ QVector<QSharedPointer<AudioSocket> >
+ mAudioSockets; ///< Vector of AudioSocket<EM>s</EM>
QVarLengthArray<sample_t*>
mInProcessBuffer; ///< Vector of Input buffers/channel for ProcessPlugin
QVarLengthArray<sample_t*>
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2024-2025 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file AudioSocket.cpp
+ * \author Mike Dickey
+ * \date December 2024
+ * \license MIT
+ */
+
+#include "AudioSocket.h"
+
+#include <QEventLoop>
+#include <iostream>
+
+#include "SocketClient.h"
+#include "jacktrip_globals.h"
+
+using namespace std;
+
+constexpr int BytesPerSample = sizeof(float);
+constexpr int BytesForFullSample = BytesPerSample * AudioSocketNumChannels;
+
+//*******************************************************************************
+ToAudioSocketPlugin::ToAudioSocketPlugin(AudioSocketQueueT& sendQueue,
+ AudioSocketQueueT& receiveQueue)
+ : mSendQueue(sendQueue), mReceiveQueue(receiveQueue)
+{
+ mSendBuffer.resize(AudioSocketMaxSamplesPerBlock * BytesForFullSample
+ + BytesPerSample);
+}
+
+//*******************************************************************************
+ToAudioSocketPlugin::~ToAudioSocketPlugin() {}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::init(int samplingRate, int bufferSize)
+{
+ if (bufferSize < 8) {
+ cerr << "*** ToAudioSocketPlugin " << this << ": bufferSize (" << bufferSize
+ << ") < 8! Setting to 8." << endl;
+ bufferSize = 8;
+ }
+ ProcessPlugin::init(samplingRate, bufferSize);
+ inited = true;
+}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::compute(int nframes, float** inputs,
+ [[maybe_unused]] float** outputs)
+{
+ if (!inited) {
+ cerr << "*** ToAudioSocketPlugin " << this << ": init never called! Doing it now."
+ << endl;
+ init(0, 0);
+ }
+
+ if (!mIsConnected) {
+ return;
+ }
+
+ if (!mSentAudioHeader) {
+ // send audio socket header
+ emit signalSendAudioHeader(getSampleRate(), getBufferSize());
+ mSentAudioHeader = true;
+ return;
+ }
+
+ if (!mRemoteIsReady) {
+ // waiting to receive audio header
+ return;
+ }
+
+ if (nframes > AudioSocketMaxSamplesPerBlock) {
+ // just a sanity check; shouldn't happen
+ nframes = AudioSocketMaxSamplesPerBlock;
+ }
+
+ // interleave samples into send buffer
+ float* framePtr = reinterpret_cast<float*>(mSendBuffer.data());
+ *(framePtr++) = nframes; // first value represents number of samples
+ for (int nextSample = 0; nextSample < nframes; ++nextSample) {
+ for (int i = 0; i < AudioSocketNumChannels; i++) {
+ int chan = i < getNumInputs() ? i : 0; // mono => dual mono
+ *(framePtr++) = inputs[chan][nextSample];
+ }
+ }
+
+ // send the samples to queue
+ mSendQueue.push(reinterpret_cast<int8_t*>(mSendBuffer.data()));
+ emit signalSendAudio();
+
+ // note: outputs are ignored
+}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::updateNumChannels(int nChansIn, int nChansOut)
+{
+ if (outgoingPluginToNetwork) {
+ mNumChannels = nChansIn;
+ } else {
+ mNumChannels = nChansOut;
+ }
+}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::remoteIsReady()
+{
+ mRemoteIsReady = true;
+}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::gotConnection()
+{
+ mSentAudioHeader = false;
+ mRemoteIsReady = false;
+ mIsConnected = true;
+}
+
+//*******************************************************************************
+void ToAudioSocketPlugin::lostConnection()
+{
+ mIsConnected = false;
+}
+
+//*******************************************************************************
+FromAudioSocketPlugin::FromAudioSocketPlugin(AudioSocketQueueT& sendQueue,
+ AudioSocketQueueT& receiveQueue,
+ bool passthrough)
+ : mSendQueue(sendQueue), mReceiveQueue(receiveQueue), mPassthrough(passthrough)
+{
+ mRecvBuffer.resize(AudioSocketMaxSamplesPerBlock * BytesForFullSample
+ + BytesPerSample);
+ mExtraSamples = new float*[AudioSocketNumChannels];
+ for (int i = 0; i < AudioSocketNumChannels; i++) {
+ mExtraSamples[i] = new float[AudioSocketMaxSamplesPerBlock];
+ }
+}
+
+//*******************************************************************************
+FromAudioSocketPlugin::~FromAudioSocketPlugin()
+{
+ for (int i = 0; i < AudioSocketNumChannels; i++) {
+ delete[] mExtraSamples[i];
+ }
+ delete[] mExtraSamples;
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::init(int samplingRate, int bufferSize)
+{
+ if (bufferSize < 8) {
+ cerr << "*** FromAudioSocketPlugin " << this << ": bufferSize (" << bufferSize
+ << ") < 8! Setting to 8." << endl;
+ bufferSize = 8;
+ }
+ ProcessPlugin::init(samplingRate, bufferSize);
+ inited = true;
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::compute(int nframes, [[maybe_unused]] float** inputs,
+ float** outputs)
+{
+ if (!inited) {
+ cerr << "*** FromAudioSocketPlugin " << this
+ << ": init never called! Doing it now." << endl;
+ init(0, 0);
+ }
+
+ // copy inputs to outputs
+ const int bytesPerChannel = nframes * BytesPerSample;
+ for (int i = 0; i < getNumOutputs(); i++) {
+ if (mPassthrough) {
+ memcpy(outputs[i], inputs[i], bytesPerChannel);
+ } else {
+ memset(outputs[i], 0, bytesPerChannel);
+ }
+ }
+
+ if (!mIsConnected) {
+ return;
+ }
+
+ if (!mRemoteIsReady) {
+ // waiting to receive audio header
+ return;
+ }
+
+ int nextSample = 0;
+ while (true) {
+ // use extra samples first
+ while (mNextExtraSample != mLastExtraSample && nextSample < nframes) {
+ for (int i = 0; i < AudioSocketNumChannels; i++) {
+ int chan = i < getNumOutputs() ? i : 0; // mix to mono
+ outputs[chan][nextSample] += mExtraSamples[i][mNextExtraSample];
+ }
+ if (++mNextExtraSample >= AudioSocketMaxSamplesPerBlock) {
+ mNextExtraSample = 0;
+ }
+ ++nextSample;
+ }
+
+ if (nextSample >= nframes) {
+ break;
+ }
+
+ // get bytes from next packet
+ int8_t* recvPtr = reinterpret_cast<int8_t*>(mRecvBuffer.data());
+ if (!mReceiveQueue.pop(recvPtr)) {
+ // qDebug() << "Audio socket glitch: receive queue empty";
+ break;
+ }
+
+ // copy bytes from packet to extras
+ float* framePtr = reinterpret_cast<float*>(mRecvBuffer.data());
+ int newSamples = static_cast<int>(*(framePtr++));
+ for (int j = 0; j < newSamples; j++) {
+ for (int i = 0; i < AudioSocketNumChannels; i++) {
+ mExtraSamples[i][mLastExtraSample] = *(framePtr++);
+ }
+ if (++mLastExtraSample >= AudioSocketMaxSamplesPerBlock) {
+ mLastExtraSample = 0;
+ }
+ }
+ }
+
+ updateQueueStats(nframes);
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::updateNumChannels(int nChansIn, int nChansOut)
+{
+ if (outgoingPluginToNetwork) {
+ mNumChannels = nChansIn;
+ } else {
+ mNumChannels = nChansOut;
+ }
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::remoteIsReady()
+{
+ mNextExtraSample = 0;
+ mLastExtraSample = 0;
+ mQueueCheckSec = 2;
+ mRemoteIsReady = true;
+ resetQueueStats();
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::gotConnection()
+{
+ mRemoteIsReady = false;
+ mIsConnected = true;
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::lostConnection()
+{
+ mIsConnected = false;
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::updateQueueStats(int nframes)
+{
+ // update receive queue stats
+ int remainingPackets = static_cast<int>(mReceiveQueue.size());
+ if (remainingPackets < mMinQueuePackets) {
+ mMinQueuePackets = remainingPackets;
+ }
+ if (remainingPackets > mMaxQueuePackets) {
+ mMaxQueuePackets = remainingPackets;
+ }
+ if (mNextQueueCheck > static_cast<uint32_t>(nframes)) {
+ mNextQueueCheck -= nframes;
+ return;
+ }
+
+ // qDebug() << "Audio socket receive queue: min =" << mMinQueuePackets
+ // << ", max =" << mMaxQueuePackets;
+
+ if (mMinQueuePackets > 0) {
+ // drain the queue to minimize latency
+ // qDebug() << "Audio socket draining" << mMinQueuePackets
+ // << "packets from receive queue";
+ int8_t* recvPtr = reinterpret_cast<int8_t*>(mRecvBuffer.data());
+ do {
+ mReceiveQueue.pop(recvPtr);
+ } while (--mMinQueuePackets > 0);
+ }
+
+ resetQueueStats();
+}
+
+//*******************************************************************************
+void FromAudioSocketPlugin::resetQueueStats()
+{
+ mMinQueuePackets = AudioSocketMaxQueueSize;
+ mMaxQueuePackets = 0;
+ mNextQueueCheck = getSampleRate() * mQueueCheckSec;
+ if (mQueueCheckSec < 512) // max interval of about 8.5 minutes
+ mQueueCheckSec *= 2;
+}
+
+//*******************************************************************************
+AudioSocketWorker::AudioSocketWorker(AudioSocketQueueT& sendQueue,
+ AudioSocketQueueT& receiveQueue,
+ QSharedPointer<QLocalSocket>& s)
+ : mSendQueue(sendQueue), mReceiveQueue(receiveQueue), mSocketPtr(s)
+{
+ mSendBuffer.resize(AudioSocketMaxSamplesPerBlock * BytesForFullSample
+ + BytesPerSample);
+ mRecvBuffer.resize(AudioSocketMaxSamplesPerBlock * BytesForFullSample
+ + BytesPerSample);
+ mPopBuffer.resize(AudioSocketMaxSamplesPerBlock * BytesForFullSample
+ + BytesPerSample);
+}
+
+//*******************************************************************************
+AudioSocketWorker::~AudioSocketWorker()
+{
+#ifdef HAVE_LIBSAMPLERATE
+ if (mSrcStatePtr != nullptr) {
+ src_delete(mSrcStatePtr);
+ }
+ delete[] mSrcInDataPtr;
+#endif
+}
+
+//****************************************************************************
+void AudioSocketWorker::start()
+{
+ setRealtimeProcessPriority();
+}
+
+//****************************************************************************
+void AudioSocketWorker::connect()
+{
+ if (isConnected()) {
+ return;
+ }
+
+ SocketClient c(mSocketPtr);
+
+ if (!c.connect()) {
+ emit signalConnectionFailed();
+ return;
+ }
+
+ if (!c.sendHeader("audio")) {
+ mSocketPtr->close();
+ emit signalConnectionFailed();
+ return;
+ }
+
+ cout << "Established audio socket connection" << endl;
+ emit signalConnectionEstablished();
+}
+
+//*******************************************************************************
+void AudioSocketWorker::close()
+{
+ if (mSocketPtr->state() == QLocalSocket::UnconnectedState
+ || mSocketPtr->state() == QLocalSocket::ClosingState) {
+ return;
+ }
+ mSocketPtr->close();
+ mSocketPtr->disconnect();
+}
+
+//*******************************************************************************
+void AudioSocketWorker::sendAudioHeader(uint32_t sampleRate, uint16_t bufferSize)
+{
+ mLocalSampleRate = sampleRate;
+
+ // send audio socket header
+ QByteArray headerBuffer;
+ headerBuffer.resize(AudioSocketHeaderSize);
+ char* headPtr = headerBuffer.data();
+ memcpy(headPtr, &sampleRate, sizeof(uint32_t));
+ headPtr += 4;
+ memcpy(headPtr, &bufferSize, sizeof(uint16_t));
+ mSocketPtr->write(headerBuffer);
+ mSocketPtr->waitForBytesWritten(-1);
+
+ // read audio header from remote to get settings
+ emit signalReadAudioHeader();
+}
+
+//*******************************************************************************
+void AudioSocketWorker::readAudioHeader()
+{
+ if (!mSocketPtr->waitForReadyRead(100)) {
+ // check if connection was lost
+ if (isConnected()) {
+ // schedule another attempt
+ emit signalReadAudioHeader();
+ } else {
+ // lost audio socket connection
+ cout << "Lost audio socket connection" << endl;
+ mSocketPtr->disconnect();
+ emit signalLostConnection();
+ }
+ return;
+ }
+
+ uint32_t headSampleRate;
+ uint16_t headBufferSize;
+ QByteArray headerBuffer;
+ headerBuffer.resize(AudioSocketHeaderSize);
+ memset(headerBuffer.data(), 0, AudioSocketHeaderSize);
+ mSocketPtr->read(headerBuffer.data(), AudioSocketHeaderSize);
+ char* headPtr = headerBuffer.data();
+ memcpy(&headSampleRate, headPtr, sizeof(uint32_t));
+ headPtr += 4;
+ memcpy(&headBufferSize, headPtr, sizeof(uint16_t));
+
+ // sanity checks (should never happen)
+ if (headSampleRate != 44100 && headSampleRate != 48000 && headSampleRate != 96000) {
+ cerr << "Audio socket received invalid sample rate = " << headSampleRate << endl;
+ mSocketPtr->close();
+ return;
+ }
+ if (headBufferSize < 2) {
+ cerr << "Audio socket received invalid buffer size = " << headBufferSize << endl;
+ mSocketPtr->close();
+ return;
+ }
+
+ cout << "Received audio socket header: sample rate = " << headSampleRate
+ << ", buffer size = " << headBufferSize << endl;
+
+ mRemoteSampleRate = headSampleRate;
+
+#ifdef HAVE_LIBSAMPLERATE
+ if (mRemoteSampleRate != mLocalSampleRate) {
+ if (mSrcStatePtr == nullptr) {
+ int srcErr;
+ mSrcStatePtr = src_new(SRC_SINC_BEST_QUALITY, 2, &srcErr);
+ if (mSrcStatePtr == nullptr) {
+ cerr << "Failed to prepare sample rate converter: "
+ << src_strerror(srcErr) << endl;
+ mSocketPtr->close();
+ return;
+ }
+ if (mSrcInDataPtr == nullptr) {
+ mSrcInDataPtr =
+ new float[AudioSocketMaxSamplesPerBlock * BytesForFullSample];
+ }
+ mSrcData.data_in = mSrcInDataPtr;
+ mSrcData.data_out =
+ reinterpret_cast<float*>(mRecvBuffer.data() + BytesPerSample);
+ mSrcData.output_frames = AudioSocketMaxSamplesPerBlock;
+ } else {
+ src_reset(mSrcStatePtr);
+ }
+ mSrcData.src_ratio = static_cast<double>(mLocalSampleRate) / mRemoteSampleRate;
+ mSrcData.end_of_input = 0;
+ mSrcInSamples = 0;
+ }
+#else
+ if (mRemoteSampleRate != mLocalSampleRate) {
+ cerr << "Audio socket sample rate conversion not supported: " << mRemoteSampleRate
+ << " != " << mLocalSampleRate << endl;
+ mSocketPtr->close();
+ return;
+ }
+#endif
+
+ QObject::connect(mSocketPtr.data(), &QLocalSocket::readyRead, this,
+ &AudioSocketWorker::receiveAudio, Qt::QueuedConnection);
+ emit signalRemoteIsReady();
+}
+
+//*******************************************************************************
+void AudioSocketWorker::sendAudio()
+{
+ if (!mSocketPtr->isValid() || mSocketPtr->state() != QLocalSocket::ConnectedState) {
+ // lost audio socket connection
+ cout << "Lost audio socket connection" << endl;
+ mSocketPtr->disconnect();
+ emit signalLostConnection();
+ return;
+ }
+
+ if (mSendQueue.empty()) {
+ return;
+ }
+
+ // send local audio packets to remote
+ int8_t* popPtr = reinterpret_cast<int8_t*>(mPopBuffer.data());
+ while (mSendQueue.pop(popPtr)) {
+ float* framePtr = reinterpret_cast<float*>(mPopBuffer.data());
+ int bytesToSend = *(framePtr++) * BytesForFullSample;
+ mSendBuffer.resize(bytesToSend);
+ memcpy(mSendBuffer.data(), framePtr, bytesToSend);
+ mSocketPtr->write(mSendBuffer);
+ }
+ mSocketPtr->waitForBytesWritten(-1);
+}
+
+//*******************************************************************************
+void AudioSocketWorker::receiveAudio()
+{
+ while (mSocketPtr->bytesAvailable() > BytesForFullSample) {
+ qint64 bytesToRead = mSocketPtr->bytesAvailable();
+ if (bytesToRead + BytesPerSample > mRecvBuffer.size())
+ bytesToRead = mRecvBuffer.size() - BytesPerSample;
+ if (bytesToRead % BytesForFullSample > 0)
+ bytesToRead -= (bytesToRead % BytesForFullSample);
+ int newSamples = bytesToRead / BytesForFullSample;
+
+#ifdef HAVE_LIBSAMPLERATE
+ if (mRemoteSampleRate == mLocalSampleRate) {
+ mSocketPtr->read(mRecvBuffer.data() + BytesPerSample, bytesToRead);
+ } else {
+ // convert remote to local sample rate
+ mSrcData.input_frames = newSamples + mSrcInSamples;
+ mSocketPtr->read(reinterpret_cast<char*>(mSrcInDataPtr)
+ + (mSrcInSamples * BytesForFullSample),
+ bytesToRead);
+ int srcErr = src_process(mSrcStatePtr, &mSrcData);
+ if (srcErr != 0) {
+ cerr << "Sample rate conversion failure: " << src_strerror(srcErr)
+ << endl;
+ mSocketPtr->close();
+ return;
+ }
+ mSrcInSamples = mSrcData.input_frames - mSrcData.input_frames_used;
+ if (mSrcInSamples > 0) {
+ // save remaining input frames for later
+ if (mSrcData.input_frames_used > 0) {
+ // shift samples in memory buffer
+ char* nextFramePtr =
+ reinterpret_cast<char*>(mSrcInDataPtr)
+ + (mSrcData.input_frames_used * BytesForFullSample);
+ memmove(mSrcInDataPtr, nextFramePtr,
+ mSrcInSamples * BytesForFullSample);
+ }
+ }
+ newSamples = mSrcData.output_frames_gen;
+ }
+#else
+ mSocketPtr->read(mRecvBuffer.data() + BytesPerSample, bytesToRead);
+#endif
+
+ if (newSamples > 0) {
+ // first value represents number of samples
+ float* framePtr = reinterpret_cast<float*>(mRecvBuffer.data());
+ *framePtr = newSamples;
+ mReceiveQueue.push(reinterpret_cast<int8_t*>(mRecvBuffer.data()));
+ }
+ }
+}
+
+//*******************************************************************************
+void AudioSocketWorker::scheduleReconnect()
+{
+ if (mRetryConnection) {
+ qDebug() << "Attempting to reconnect audio socket";
+ if (mTimerPtr.isNull()) {
+ mTimerPtr.reset(new QTimer);
+ QObject::connect(mTimerPtr.data(), &QTimer::timeout, this,
+ &AudioSocketWorker::connect);
+ }
+ mTimerPtr->start(1000); // try reconnecting in 1 second
+ }
+}
+
+//*******************************************************************************
+AudioSocket::AudioSocket(bool retryConnection)
+ : mThread()
+ , mSendQueue(AudioSocketMaxSamplesPerBlock * BytesForFullSample + BytesPerSample)
+ , mReceiveQueue(AudioSocketMaxSamplesPerBlock * BytesForFullSample + BytesPerSample)
+ , mToAudioSocketPluginPtr(new ToAudioSocketPlugin(mSendQueue, mReceiveQueue))
+ , mFromAudioSocketPluginPtr(new FromAudioSocketPlugin(mSendQueue, mReceiveQueue))
+{
+ mThread.setObjectName("AudioSocket");
+ mThread.start();
+
+ QSharedPointer<QLocalSocket> s(new QLocalSocket);
+ s->moveToThread(&mThread);
+
+ mWorkerPtr.reset(new AudioSocketWorker(mSendQueue, mReceiveQueue, s));
+ mWorkerPtr->moveToThread(&mThread);
+ mWorkerPtr->setRetryConnection(retryConnection);
+
+ initWorker();
+}
+
+//*******************************************************************************
+AudioSocket::AudioSocket(QSharedPointer<QLocalSocket>& s)
+ : mThread()
+ , mSendQueue(AudioSocketMaxSamplesPerBlock * BytesForFullSample + BytesPerSample)
+ , mReceiveQueue(AudioSocketMaxSamplesPerBlock * BytesForFullSample + BytesPerSample)
+ , mToAudioSocketPluginPtr(new ToAudioSocketPlugin(mSendQueue, mReceiveQueue))
+ , mFromAudioSocketPluginPtr(new FromAudioSocketPlugin(mSendQueue, mReceiveQueue))
+ , mWorkerPtr(new AudioSocketWorker(mSendQueue, mReceiveQueue, s))
+{
+ mThread.setObjectName("AudioSocket");
+ mThread.start();
+
+ s->moveToThread(&mThread);
+ mWorkerPtr->moveToThread(&mThread);
+
+ initWorker();
+}
+
+//*******************************************************************************
+void AudioSocket::initWorker()
+{
+ auto* toPluginPtr = static_cast<ToAudioSocketPlugin*>(mToAudioSocketPluginPtr.get());
+ auto* fromPluginPtr =
+ static_cast<FromAudioSocketPlugin*>(mFromAudioSocketPluginPtr.get());
+
+ QObject::connect(this, &AudioSocket::signalConnect, mWorkerPtr.data(),
+ &AudioSocketWorker::connect, Qt::QueuedConnection);
+ QObject::connect(this, &AudioSocket::signalClose, mWorkerPtr.data(),
+ &AudioSocketWorker::close, Qt::QueuedConnection);
+ QObject::connect(this, &AudioSocket::signalStartWorker, mWorkerPtr.data(),
+ &AudioSocketWorker::start, Qt::QueuedConnection);
+ QObject::connect(toPluginPtr, &ToAudioSocketPlugin::signalSendAudioHeader,
+ mWorkerPtr.data(), &AudioSocketWorker::sendAudioHeader,
+ Qt::QueuedConnection);
+ QObject::connect(toPluginPtr, &ToAudioSocketPlugin::signalSendAudio,
+ mWorkerPtr.data(), &AudioSocketWorker::sendAudio,
+ Qt::QueuedConnection);
+ QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalRemoteIsReady,
+ toPluginPtr, &ToAudioSocketPlugin::remoteIsReady,
+ Qt::DirectConnection);
+ QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalRemoteIsReady,
+ fromPluginPtr, &FromAudioSocketPlugin::remoteIsReady,
+ Qt::DirectConnection);
+ QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalConnectionEstablished,
+ toPluginPtr, &ToAudioSocketPlugin::gotConnection,
+ Qt::DirectConnection);
+ QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalConnectionEstablished,
+ fromPluginPtr, &FromAudioSocketPlugin::gotConnection,
+ Qt::DirectConnection);
+ QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalLostConnection,
+ toPluginPtr, &ToAudioSocketPlugin::lostConnection,
+ Qt::DirectConnection);
+ QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalLostConnection,
+ fromPluginPtr, &FromAudioSocketPlugin::lostConnection,
+ Qt::DirectConnection);
+ QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalLostConnection,
+ mWorkerPtr.data(), &AudioSocketWorker::scheduleReconnect,
+ Qt::DirectConnection);
+ QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalConnectionFailed,
+ mWorkerPtr.data(), &AudioSocketWorker::scheduleReconnect,
+ Qt::DirectConnection);
+ QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalReadAudioHeader,
+ mWorkerPtr.data(), &AudioSocketWorker::readAudioHeader,
+ Qt::QueuedConnection);
+
+ if (isConnected()) {
+ toPluginPtr->gotConnection();
+ fromPluginPtr->gotConnection();
+ }
+
+ emit signalStartWorker();
+}
+
+//*******************************************************************************
+AudioSocket::~AudioSocket()
+{
+ mThread.quit();
+ mThread.wait();
+ mWorkerPtr.reset();
+}
+
+//*******************************************************************************
+bool AudioSocket::connect(int samplingRate, int bufferSize)
+{
+ if (mWorkerPtr->isConnected()) {
+ return true;
+ }
+
+ mFromAudioSocketPluginPtr->init(samplingRate, bufferSize);
+ mToAudioSocketPluginPtr->init(samplingRate, bufferSize);
+ emit signalConnect();
+
+ QTimer timer;
+ timer.setTimerType(Qt::CoarseTimer);
+ timer.setSingleShot(true);
+
+ QEventLoop loop;
+ QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalConnectionEstablished,
+ &loop, &QEventLoop::quit);
+ QObject::connect(mWorkerPtr.data(), &AudioSocketWorker::signalConnectionFailed, &loop,
+ &QEventLoop::quit);
+ QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
+ timer.start(1000);
+ loop.exec();
+
+ return mWorkerPtr->isConnected();
+}
+
+//*******************************************************************************
+void AudioSocket::compute(int nframes, float** inputs, float** outputs)
+{
+ mToAudioSocketPluginPtr->compute(nframes, inputs, outputs);
+ mFromAudioSocketPluginPtr->compute(nframes, inputs, outputs);
+}
+
+//*******************************************************************************
+void AudioSocket::close()
+{
+ emit signalClose();
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2024-2025 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file AudioSocket.h
+ * \author Mike Dickey
+ * \date December 2024
+ * \license MIT
+ */
+
+#ifndef __AUDIOSOCKET_H__
+#define __AUDIOSOCKET_H__
+
+#include <QLocalSocket>
+#include <QObject>
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <QThread>
+#include <QTimer>
+
+#include "ProcessPlugin.h"
+#include "WaitFreeFrameBuffer.h"
+
+#ifdef HAVE_LIBSAMPLERATE
+#include "samplerate.h"
+#endif
+
+// assume stereo audio for this implementation
+constexpr int AudioSocketNumChannels = 2;
+
+// assume max buffer size of 8192 samples
+constexpr int AudioSocketMaxSamplesPerBlock = 8192;
+
+// allow up to 1024 frames
+constexpr int AudioSocketMaxQueueSize = 1024;
+
+// audio header is 4 bytes for the number of samples + 2 bytes for the buffer size
+constexpr int AudioSocketHeaderSize = 4 + 2;
+
+// data type for audio socket circular buffer
+typedef WaitFreeFrameBuffer<AudioSocketMaxQueueSize> AudioSocketQueueT;
+
+/** \brief ToAudioSocketPlugin is used to send audio from a signal chain to an audio
+ * socket
+ */
+class ToAudioSocketPlugin : public ProcessPlugin
+{
+ Q_OBJECT;
+
+ public:
+ ToAudioSocketPlugin(AudioSocketQueueT& sendQueue, AudioSocketQueueT& receiveQueue);
+ virtual ~ToAudioSocketPlugin();
+
+ void init(int samplingRate, int bufferSize) override;
+ int getNumInputs() override { return (mNumChannels); }
+ int getNumOutputs() override { return (mNumChannels); }
+ void compute(int nframes, float** inputs, float** outputs) override;
+ const char* getName() const override { return "ToAudioSocket"; };
+ void updateNumChannels(int nChansIn, int nChansOut) override;
+
+ signals:
+ void signalSendAudioHeader(uint32_t sampleRate, uint16_t bufferSize);
+ void signalSendAudio();
+
+ public slots:
+ void remoteIsReady();
+ void gotConnection();
+ void lostConnection();
+
+ private:
+ AudioSocketQueueT& mSendQueue;
+ AudioSocketQueueT& mReceiveQueue;
+ QByteArray mSendBuffer;
+ int mNumChannels = AudioSocketNumChannels;
+ bool mSentAudioHeader = false;
+ bool mRemoteIsReady = false;
+ bool mIsConnected = false;
+};
+
+/** \brief FromAudioSocketPlugin is used mix audio from an audio socket into a signal
+ * chain
+ */
+class FromAudioSocketPlugin : public ProcessPlugin
+{
+ Q_OBJECT;
+
+ public:
+ FromAudioSocketPlugin(AudioSocketQueueT& sendQueue, AudioSocketQueueT& receiveQueue,
+ bool passthrough = false);
+ virtual ~FromAudioSocketPlugin();
+
+ void init(int samplingRate, int bufferSize) override;
+ int getNumInputs() override { return (mNumChannels); }
+ int getNumOutputs() override { return (mNumChannels); }
+ void compute(int nframes, float** inputs, float** outputs) override;
+ const char* getName() const override { return "FromAudioSocket"; };
+ void updateNumChannels(int nChansIn, int nChansOut) override;
+ void setPassthrough(bool b) { mPassthrough = b; }
+
+ public slots:
+ void remoteIsReady();
+ void gotConnection();
+ void lostConnection();
+
+ protected:
+ void updateQueueStats(int nframes);
+ void resetQueueStats();
+
+ private:
+ AudioSocketQueueT& mSendQueue;
+ AudioSocketQueueT& mReceiveQueue;
+ QByteArray mRecvBuffer;
+ float** mExtraSamples = nullptr;
+ int mNumChannels = AudioSocketNumChannels;
+ int mNextExtraSample = 0;
+ int mLastExtraSample = 0;
+ int mMinQueuePackets = 0;
+ int mMaxQueuePackets = 0;
+ int mQueueCheckSec = 0;
+ uint32_t mNextQueueCheck = 0;
+ bool mRemoteIsReady = false;
+ bool mIsConnected = false;
+ bool mPassthrough = false;
+};
+
+/** \brief AudioSocketWorker is used to perform socket operations in a separate thread
+ */
+class AudioSocketWorker : public QObject
+{
+ Q_OBJECT;
+
+ public:
+ AudioSocketWorker(AudioSocketQueueT& sendQueue, AudioSocketQueueT& receiveQueue,
+ QSharedPointer<QLocalSocket>& s);
+ virtual ~AudioSocketWorker();
+
+ inline void setRetryConnection(bool retry) { mRetryConnection = retry; }
+ inline bool isConnected()
+ {
+ return mSocketPtr->state() == QLocalSocket::ConnectedState;
+ }
+ inline QLocalSocket& getSocket() { return *mSocketPtr; }
+
+ signals:
+ void signalReadAudioHeader();
+ void signalConnectionEstablished();
+ void signalConnectionFailed();
+ void signalLostConnection();
+ void signalRemoteIsReady();
+
+ public slots:
+ // sets a few things up at startup
+ void start();
+
+ // attempts to connect to remote instance's socket server
+ // returns true if connection was successfully established
+ // returns false and schedules retry if connection failed
+ void connect();
+
+ /// \brief closes the connection to remote instance's socket server
+ void close();
+
+ /// \brief send audio header to remote instance
+ void sendAudioHeader(uint32_t sampleRate, uint16_t bufferSize);
+
+ /// \brief read audio header from remote instance
+ void readAudioHeader();
+
+ /// \brief sends audio packets to remote instance
+ void sendAudio();
+
+ /// \brief receives audio bytes from remote instance
+ void receiveAudio();
+
+ /// \brief schedules a reconnect attempt
+ void scheduleReconnect();
+
+ private:
+ AudioSocketQueueT& mSendQueue;
+ AudioSocketQueueT& mReceiveQueue;
+ QScopedPointer<QTimer> mTimerPtr;
+ QSharedPointer<QLocalSocket> mSocketPtr;
+ QByteArray mSendBuffer;
+ QByteArray mRecvBuffer;
+ QByteArray mPopBuffer;
+ bool mRetryConnection = false;
+ int mLocalSampleRate = 0;
+ int mRemoteSampleRate = 0;
+#ifdef HAVE_LIBSAMPLERATE
+ SRC_DATA mSrcData;
+ SRC_STATE* mSrcStatePtr = nullptr;
+ float* mSrcInDataPtr = nullptr;
+ int mSrcInSamples = 0;
+#endif
+};
+
+/** \brief An AudioSocket is used to exchange audio with another processes via a local
+ * socket
+ */
+class AudioSocket : public QObject
+{
+ Q_OBJECT;
+
+ public:
+ AudioSocket(bool retryConnection = false);
+ AudioSocket(QSharedPointer<QLocalSocket>& s);
+ virtual ~AudioSocket();
+
+ inline bool isConnected() { return mWorkerPtr->isConnected(); }
+ inline QLocalSocket& getSocket() { return mWorkerPtr->getSocket(); }
+ inline int getSampleRate() const { return mToAudioSocketPluginPtr->getSampleRate(); }
+ inline int getBufferSize() const { return mToAudioSocketPluginPtr->getBufferSize(); }
+ inline QSharedPointer<ProcessPlugin>& getToAudioSocketPlugin()
+ {
+ return mToAudioSocketPluginPtr;
+ }
+ inline QSharedPointer<ProcessPlugin>& getFromAudioSocketPlugin()
+ {
+ return mFromAudioSocketPluginPtr;
+ }
+ inline void setRetryConnection(bool retry) { mWorkerPtr->setRetryConnection(retry); }
+
+ // attempts to connect to remote instance's socket server
+ // returns true if connection was successfully established
+ // returns false and schedules retry if connection failed
+ bool connect(int samplingRate, int bufferSize);
+
+ /// \brief audio callback for duplex processing
+ void compute(int nframes, float** inputs, float** outputs);
+
+ /// \brief closes the connection to remote instance's socket server
+ void close();
+
+ signals:
+ void signalStartWorker();
+ void signalConnect();
+ void signalClose();
+
+ private:
+ /// \brief initializes worker and worker thread
+ void initWorker();
+
+ QThread mThread;
+ AudioSocketQueueT mSendQueue;
+ AudioSocketQueueT mReceiveQueue;
+ QSharedPointer<ProcessPlugin> mToAudioSocketPluginPtr;
+ QSharedPointer<ProcessPlugin> mFromAudioSocketPluginPtr;
+ QScopedPointer<AudioSocketWorker> mWorkerPtr;
+
+ friend class AudioSocketWorker;
+};
+
+#endif
\ No newline at end of file
void Compressor::init(int samplingRate, int bufferSize)
{
ProcessPlugin::init(samplingRate, bufferSize);
- fs = float(fSamplingFreq);
+ fs = float(mSampleRate);
for (int i = 0; i < mNumChannels; i++) {
static_cast<compressordsp*>(compressorP[i])
->init(fs); // compression filter parameters depend on sampling rate
}
//*******************************************************************************
-void JackTrip::appendProcessPluginToNetwork(ProcessPlugin* plugin)
+void JackTrip::appendProcessPluginToNetwork(QSharedPointer<ProcessPlugin>& plugin)
{
- if (plugin) {
+ if (!plugin.isNull()) {
mProcessPluginsToNetwork.append(plugin); // ownership transferred
// mAudioInterface->appendProcessPluginToNetwork(plugin);
}
}
//*******************************************************************************
-void JackTrip::appendProcessPluginFromNetwork(ProcessPlugin* plugin)
+void JackTrip::appendProcessPluginFromNetwork(QSharedPointer<ProcessPlugin>& plugin)
{
- if (plugin) {
+ if (!plugin.isNull()) {
mProcessPluginsFromNetwork.append(plugin); // ownership transferred
// mAudioInterface->appendProcessPluginFromNetwork(plugin);
}
}
//*******************************************************************************
-void JackTrip::appendProcessPluginToMonitor(ProcessPlugin* plugin)
+void JackTrip::appendProcessPluginToMonitor(QSharedPointer<ProcessPlugin>& plugin)
{
- if (plugin) {
+ if (!plugin.isNull()) {
mProcessPluginsToMonitor.append(plugin); // ownership transferred
// mAudioInterface->appendProcessPluginFromNetwork(plugin);
}
// 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 << "/"
* \param plugin Pointer to ProcessPlugin Class
*/
// void appendProcessPlugin(const std::tr1::shared_ptr<ProcessPlugin> plugin);
- virtual void appendProcessPluginToNetwork(ProcessPlugin* plugin);
- virtual void appendProcessPluginFromNetwork(ProcessPlugin* plugin);
- virtual void appendProcessPluginToMonitor(ProcessPlugin* plugin);
+ virtual void appendProcessPluginToNetwork(QSharedPointer<ProcessPlugin>& plugin);
+ virtual void appendProcessPluginFromNetwork(QSharedPointer<ProcessPlugin>& plugin);
+ virtual void appendProcessPluginToMonitor(QSharedPointer<ProcessPlugin>& plugin);
/// \brief Start the processing threads
virtual void startProcess(
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)
{
return (mAudioInterface == nullptr) ? false
: mAudioInterface->getHighLatencyFlag();
}
+ double getLatency() const
+ {
+ return mReceiveRingBuffer == nullptr ? -1 : mReceiveRingBuffer->getLatency();
+ }
//@}
//------------------------------------------------------------------------------------
JackTrip::hubConnectionModeT
mHubConnectionModeT; ///< Hub Server Jack Audio Patch Connection Mode
- QVector<ProcessPlugin*>
+ QVector<QSharedPointer<ProcessPlugin> >
mProcessPluginsFromNetwork; ///< Vector of ProcessPlugin<EM>s</EM>
- QVector<ProcessPlugin*>
+ QVector<QSharedPointer<ProcessPlugin> >
mProcessPluginsToNetwork; ///< Vector of ProcessPlugin<EM>s</EM>
- QVector<ProcessPlugin*>
+ QVector<QSharedPointer<ProcessPlugin> >
mProcessPluginsToMonitor; ///< Vector of ProcessPlugin<EM>s</EM>
QTimer mTimeoutTimer;
QTimer mRetryTimer;
}
void setBroadcast(int broadcast_queue) { mBroadcastQueue = broadcast_queue; }
void setUseRtUdpPriority(bool use) { mUseRtUdpPriority = use; }
+ void setBufferQueueLength(int queueBufferLength)
+ {
+ QMutexLocker lock(&mMutex);
+ if (mJackTrip.isNull() || mBufferQueueLength == queueBufferLength) {
+ return;
+ }
+ mBufferQueueLength = queueBufferLength;
+ mJackTrip->setBufferQueueLength(mBufferQueueLength);
+ }
void setIOStatTimeout(int timeout) { mIOStatTimeout = timeout; }
void setIOStatStream(QSharedPointer<std::ostream> statStream)
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:
void Limiter::init(int samplingRate, int bufferSize)
{
ProcessPlugin::init(samplingRate, bufferSize);
- fs = float(fSamplingFreq);
+ fs = float(mSampleRate);
for (int i = 0; i < mNumChannels; i++) {
static_cast<limiterdsp*>(limiterP[i])
->init(fs); // compression filter parameters depend on sampling rate
{
ProcessPlugin::init(samplingRate, bufferSize);
- fs = float(fSamplingFreq);
+ fs = float(mSampleRate);
for (int i = 0; i < mNumChannels; i++) {
static_cast<meterdsp*>(meterP[i])->init(fs);
}
void Monitor::init(int samplingRate, int bufferSize)
{
ProcessPlugin::init(samplingRate, bufferSize);
- fs = float(fSamplingFreq);
+ fs = float(mSampleRate);
for (int i = 0; i < mNumChannels; i++) {
static_cast<monitordsp*>(monitorP[i])
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2024 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file OscServer.cpp
+ * \author Nelson Wang
+ * \date November 2024
+ */
+
+#include "OscServer.h"
+
+#include <iostream>
+
+using std::cout;
+using std::endl;
+
+//*******************************************************************************
+OscServer::OscServer(quint16 port, QObject* parent) : QObject(parent), mPort(port) {}
+
+//*******************************************************************************
+OscServer::~OscServer()
+{
+ stop();
+}
+
+//*******************************************************************************
+void OscServer::stop()
+{
+ closeSocket();
+}
+
+//*******************************************************************************
+void OscServer::closeSocket()
+{
+ if (!mOscServerSocket.isNull()) {
+ mOscServerSocket->close();
+ mOscServerSocket.reset();
+ }
+}
+
+//*******************************************************************************
+void OscServer::start()
+{
+ mOscServerSocket.reset(new QUdpSocket(this));
+ qDebug() << "Binding OSC server socket to UDP port " << mPort;
+ if (!mOscServerSocket->bind(QHostAddress::LocalHost, mPort)) {
+ qDebug() << "Error binding OSC server socket";
+ return;
+ }
+
+ connect(mOscServerSocket.get(), &QUdpSocket::readyRead, this,
+ &OscServer::readPendingDatagrams);
+ qDebug() << "OSC server started on UDP port " << mPort;
+}
+
+//*******************************************************************************
+void OscServer::readPendingDatagrams()
+{
+ while (mOscServerSocket->hasPendingDatagrams()) {
+ QByteArray datagram;
+ datagram.resize(mOscServerSocket->pendingDatagramSize());
+ QHostAddress sender;
+ quint16 senderPort;
+
+ mOscServerSocket->readDatagram(datagram.data(), datagram.size(), &sender,
+ &senderPort);
+ qDebug() << "Received datagram from" << sender << ":" << senderPort;
+ qDebug() << " - Data:" << datagram;
+#ifndef NO_OSCPP
+ handlePacket(OSCPP::Server::Packet(datagram.data(), datagram.size()));
+#endif // NO_OSCPP
+ // Send a reply back to the client
+ // QByteArray replyData("Reply from server");
+ // socket->writeDatagram(replyData, sender, senderPort);
+ }
+}
+
+//*******************************************************************************
+#ifndef NO_OSCPP
+void OscServer::handlePacket(const OSCPP::Server::Packet& packet)
+{
+ try {
+ if (packet.isBundle()) {
+ // Convert to bundle
+ OSCPP::Server::Bundle bundle(packet);
+ // Get packet stream
+ OSCPP::Server::PacketStream packets(bundle.packets());
+
+ // Iterate over all the packets and call handlePacket recursively.
+ while (!packets.atEnd()) {
+ handlePacket(packets.next());
+ }
+ } else {
+ // Convert to message
+ OSCPP::Server::Message msg(packet);
+ // Get argument stream
+ OSCPP::Server::ArgStream args(msg.args());
+
+ if (msg == "/config") {
+ const char* key = args.string();
+ const float value = args.float32();
+ cout << "Config received - key (" << key << ") value (" << value << ")"
+ << endl;
+ if (strcmp("queueBuffer", key) == 0) {
+ emit signalQueueBufferChanged(static_cast<int>(value));
+ }
+ } else {
+ // Simply print unknown messages
+ cout << "Unknown message:" << msg.address() << endl;
+ }
+ }
+ } catch (std::exception& e) {
+ cout << "Exception:" << e.what() << endl;
+ }
+}
+#endif // NO_OSCPP
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2024 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file OscServer.h
+ * \author Nelson Wang
+ * \date November 2024
+ */
+
+#ifndef __OSCSERVER_H__
+#define __OSCSERVER_H__
+
+#include <QObject>
+#include <QUdpSocket>
+#include <QtCore>
+
+#ifndef NO_OSCPP
+#include "oscpp/client.hpp"
+#include "oscpp/server.hpp"
+#endif // NO_OSCPP
+
+class OscServer : public QObject
+{
+ Q_OBJECT;
+
+ public:
+ OscServer(quint16 port, QObject* parent = nullptr);
+
+ /// \brief The class destructor
+ virtual ~OscServer();
+ void start();
+ void stop();
+
+ static size_t makeConfigPacket(void* buffer, size_t size, const char* key,
+ float value)
+ {
+#ifndef NO_OSCPP
+ // Construct a packet
+ OSCPP::Client::Packet packet(buffer, size);
+ packet
+ // Open a bundle with a timetag
+ .openBundle(1234ULL)
+ // Add a message with two arguments
+ // for efficiency this needs to be known in advance.
+ .openMessage("/config", 2)
+ // Write the arguments
+ .string(key)
+ .float32(value)
+ // Every `open` needs a corresponding `close`
+ .closeMessage()
+ .closeBundle();
+ return packet.size();
+#else
+ return 0;
+#endif // NO_OSCPP
+ }
+ signals:
+ void signalQueueBufferChanged(int queueBufferSize);
+
+ private slots:
+ void readPendingDatagrams();
+
+ private:
+ void closeSocket();
+#ifndef NO_OSCPP
+ void handlePacket(const OSCPP::Server::Packet& packet);
+#endif // NO_OSCPP
+
+ QSharedPointer<QUdpSocket> mOscServerSocket;
+ quint16 mPort;
+};
+
+#endif
#define __PROCESSPLUGIN_H__
#include <QObject>
-#include <QThread>
/** \brief Interface for the process plugins to add to the JACK callback process in
* JackAudioInterface
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
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);
}
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
// 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 =
//////////////////////////////////////
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
// only increase headroom if doing so would have reduced the number of
// glitches that occured over the past second by 1% or more.
// prevent headroom from growing beyond rolling average of max.
- int skipsAllowed;
+ const int maxHeadroom = pushStat->longTermMax + 1;
+ int glitchesAllowed;
if (mMsecTolerance >= (mPeerFPPdurMsec * 2)) {
- // calculate skips allowed if tolerance if above or equal to duration of two
- // packets
- skipsAllowed =
+ // calculate glitches allowed if tolerance if above or equal to duration of
+ // two packets
+ glitchesAllowed =
static_cast<int>(AutoHeadroomGlitchTolerance * mSampleRate / mPeerFPP);
} else {
- // zero skips allowed if tolerance is below duration of two packets
- skipsAllowed = 0;
+ // zero glitches allowed if tolerance is below duration of two packets
+ glitchesAllowed = 0;
// also don't require two intervals in a row (override)
mSkipAutoHeadroom = false;
}
- if (glitches > 0 && skipped > skipsAllowed
- && mCurrentHeadroom + 1 <= pushStat->longTermMax) {
+ if (skipped > 0 && glitches > glitchesAllowed
+ && mCurrentHeadroom + 1 <= maxHeadroom) {
if (mSkipAutoHeadroom) {
mSkipAutoHeadroom = false;
} else {
// don't increase headroom two intervals in a row
mSkipAutoHeadroom = true;
++mCurrentHeadroom;
- cout << "PLC glitches=" << glitches << " skipped=" << skipped << ">"
- << skipsAllowed << ", increasing headroom to " << mCurrentHeadroom
- << " (max=" << pushStat->longTermMax << ")" << endl;
+ cout << "PLC skipped=" << skipped << " glitches=" << glitches << ">"
+ << glitchesAllowed << ", increasing headroom to " << mCurrentHeadroom
+ << " (max=" << maxHeadroom << ")" << endl;
}
} else {
// thresholds not met: require 2 intervals in a row
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
}
}
+//*******************************************************************************
+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)
{
// 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
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;
}
// 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;
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;
/// @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);
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;
void Reverb::init(int samplingRate, int bufferSize)
{
ProcessPlugin::init(samplingRate, bufferSize);
- fs = float(fSamplingFreq);
+ fs = float(mSampleRate);
if (mReverbLevel <= 1.0) { // freeverb:
static_cast<freeverbdsp*>(freeverbStereoP)
->init(fs); // compression filter parameters depend on sampling rate
std::memset(ptrToReadSlot, 0, mSlotSize);
}
+//*******************************************************************************
+// Not supported in RingBuffer
+void RingBuffer::setQueueBufferLength([[maybe_unused]] int queueBuffer)
+{
+ return;
+}
+
//*******************************************************************************
void RingBuffer::setUnderrunReadSlot(int8_t* ptrToReadSlot)
{
mUnderrunsNew = 0;
mLevel = std::ceil(mLevelCur);
}
+
+//*******************************************************************************
+// Not supported in RingBuffer
+double RingBuffer::getLatency() const
+{
+ return -1;
+}
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;
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.
#include <cstdlib>
#include "JackTrip.h"
+#include "SampleRateConverter.h"
#include "StereoToMono.h"
#include "jacktrip_globals.h"
#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
{
return false;
}
+//*******************************************************************************
+unsigned int RtAudioDevice::getClosestSampleRate(unsigned int srate) const
+{
+ unsigned int bestResult = 0;
+ const unsigned int doubleSrate = srate * 2;
+ for (unsigned int i = 0; i < this->sampleRates.size(); i++) {
+ if (this->sampleRates[i] == srate)
+ return srate;
+ // pick the next highest rate available, or 2x if available
+ if (this->sampleRates[i] == doubleSrate
+ || (bestResult<srate&& this->sampleRates[i]> bestResult))
+ bestResult = this->sampleRates[i];
+ }
+ return bestResult;
+}
+
//*******************************************************************************
RtAudioDevice& RtAudioDevice::operator=(const RtAudio::DeviceInfo& info)
{
}
}
+ 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)
<< getSampleRate();
throw std::runtime_error(errorMsg.toStdString());
}
+#endif
// provide warnings for common known failure cases
const QString out_device_lower_name =
// 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)
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
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
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();
}
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) {
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
// Setup StereoToMonoMixer
// This MUST be after RtAudio::openSteram in case bufferFrames changes
mStereoToMonoMixerPtr.reset(new StereoToMono());
- mStereoToMonoMixerPtr->init(sampleRate, bufferFrames);
+ mStereoToMonoMixerPtr->init(getSampleRate(), bufferFrames);
}
//*******************************************************************************
//*******************************************************************************
int RtAudioInterface::RtAudioCallback(void* outputBuffer, void* inputBuffer,
- unsigned int nFrames, double /*streamTime*/,
+ unsigned int nframes, double /*streamTime*/,
RtAudioStreamStatus /*status*/)
{
- sample_t* inputBuffer_sample = static_cast<sample_t*>(inputBuffer);
- sample_t* outputBuffer_sample = static_cast<sample_t*>(outputBuffer);
- int in_chans_num = getNumInputChannels();
-
if (mDuplexMode) {
- if (inputBuffer_sample == NULL || outputBuffer_sample == NULL) {
+ if (inputBuffer == NULL || outputBuffer == NULL) {
return 0;
}
- } else if (inputBuffer_sample == NULL && outputBuffer_sample == NULL) {
+ } else if (inputBuffer == NULL && outputBuffer == NULL) {
return 0;
}
- // process input before output to minimize monitor latency on duplex devices
- if (inputBuffer_sample != NULL) {
- // copy samples to input buffer
- for (int i = 0; i < mInBuffer.size(); i++) {
- // Input Ports are READ ONLY
- mInBuffer[i] = inputBuffer_sample + (nFrames * i);
+#ifdef HAVE_LIBSAMPLERATE
+ uint32_t bufferSize = getBufferSizeInSamples();
+ if (inputBuffer != NULL && getSampleRate() != mInSampleRate) {
+ int framesAvailable = mInSrcPtr->push(inputBuffer, nframes);
+ if (framesAvailable < 0) {
+ std::cerr << "RtAudioInterface sample rate conversion error" << std::endl;
+ return -1;
}
- if (in_chans_num == 2 && mInBuffer.size() == in_chans_num
- && mInputMixMode == AudioInterface::MIXTOMONO) {
- mStereoToMonoMixerPtr->compute(nFrames, mInBuffer.data(), mInBuffer.data());
+ while (static_cast<uint32_t>(framesAvailable) >= bufferSize) {
+ prepareInputBuffer(mInSrcPtr->pop(bufferSize), bufferSize);
+ AudioInterface::audioInputCallback(mInBuffer, bufferSize);
+ framesAvailable -= bufferSize;
}
- AudioInterface::audioInputCallback(mInBuffer, nFrames);
+ inputBuffer = NULL;
+ }
+ if (outputBuffer != NULL && getSampleRate() != mOutSampleRate) {
+ prepareOutputBuffer(mOutTmpPtr.get(), bufferSize);
+ int framesAvailable = mOutSrcPtr->getFramesAvailable();
+ while (static_cast<unsigned int>(framesAvailable) < nframes) {
+ AudioInterface::audioOutputCallback(mOutBuffer, bufferSize);
+ framesAvailable = mOutSrcPtr->push(mOutTmpPtr.get(), bufferSize);
+ if (framesAvailable < 0) {
+ std::cerr << "RtAudioInterface sample rate conversion error" << std::endl;
+ return -1;
+ }
+ }
+ memcpy(outputBuffer, mOutSrcPtr->pop(nframes),
+ nframes * mOutBuffer.size() * sizeof(float));
+ outputBuffer = NULL;
}
+#endif
- if (outputBuffer_sample != NULL) {
- // copy samples to output buffer
- for (int i = 0; i < mOutBuffer.size(); i++) {
- // Output Ports are WRITABLE
- mOutBuffer[i] = outputBuffer_sample + (nFrames * i);
- }
- AudioInterface::audioOutputCallback(mOutBuffer, nFrames);
+ // process input before output to minimize monitor latency on duplex devices
+ if (inputBuffer != NULL) {
+ prepareInputBuffer(static_cast<sample_t*>(inputBuffer), nframes);
+ AudioInterface::audioInputCallback(mInBuffer, nframes);
+ }
+
+ if (outputBuffer != NULL) {
+ prepareOutputBuffer(static_cast<sample_t*>(outputBuffer), nframes);
+ AudioInterface::audioOutputCallback(mOutBuffer, nframes);
}
return 0;
}
+//*******************************************************************************
+void RtAudioInterface::prepareInputBuffer(sample_t* ptr, unsigned int nframes)
+{
+ for (int i = 0; i < mInBuffer.size(); i++) {
+ // Input Ports are READ ONLY
+ mInBuffer[i] = ptr + (nframes * i);
+ }
+ if (getNumInputChannels() == 2 && mInBuffer.size() == getNumInputChannels()
+ && mInputMixMode == AudioInterface::MIXTOMONO) {
+ mStereoToMonoMixerPtr->compute(nframes, mInBuffer.data(), mInBuffer.data());
+ }
+}
+
+//*******************************************************************************
+void RtAudioInterface::prepareOutputBuffer(sample_t* ptr, unsigned int nframes)
+{
+ for (int i = 0; i < mOutBuffer.size(); i++) {
+ // Output Ports are WRITABLE
+ mOutBuffer[i] = ptr + (nframes * i);
+ }
+}
+
//*******************************************************************************
int RtAudioInterface::wrapperRtAudioCallback(void* outputBuffer, void* inputBuffer,
unsigned int nFrames, double streamTime,
#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
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);
};
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);
bool mDuplexMode; ///< true if using duplex stream mode (input device == output
///< device)
QScopedPointer<StereoToMono> mStereoToMonoMixerPtr;
+ QScopedPointer<SampleRateConverter> mInSrcPtr;
+ QScopedPointer<SampleRateConverter> mOutSrcPtr;
+ QScopedPointer<float> mOutTmpPtr;
+ uint32_t mInSampleRate; ///< Actual sampling rate for input device
+ uint32_t mOutSampleRate; ///< Actual sampling rate for output device
};
#endif // __RTAUDIOINTERFACE_H__
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2025 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file SampleRateConverter.cpp
+ * \author Mike Dickey
+ * \date January 2025
+ */
+
+#include "SampleRateConverter.h"
+
+#include <cstring>
+#include <stdexcept>
+#include <string>
+
+using namespace std;
+
+constexpr int BufferSizeMultiple = 3;
+constexpr int BytesPerSample = sizeof(float);
+
+//*******************************************************************************
+SampleRateConverter::SampleRateConverter(unsigned int inRate, unsigned int outRate,
+ unsigned int numChans, unsigned int bufferSize)
+ : mBytesPerFrame(numChans * BytesPerSample)
+ , mNumChannels(numChans)
+ , mBufferSize(bufferSize)
+{
+#ifdef HAVE_LIBSAMPLERATE
+ int srcErr;
+ mStatePtr = src_new(SRC_SINC_BEST_QUALITY, numChans, &srcErr);
+ if (mStatePtr == nullptr) {
+ string errorMsg("Failed to prepare sample rate converter: ");
+ errorMsg += src_strerror(srcErr);
+ throw runtime_error(errorMsg);
+ }
+ mInDataPtr = new char[BufferSizeMultiple * mBufferSize * mBytesPerFrame];
+ mOutDataPtr = new char[BufferSizeMultiple * mBufferSize * mBytesPerFrame];
+ mOutPopPtr = new float[mBufferSize * mNumChannels];
+ mData.data_in = reinterpret_cast<float*>(mInDataPtr);
+ mData.src_ratio = static_cast<double>(outRate) / inRate;
+ mData.end_of_input = 0;
+ mInFramesLeftover = 0;
+ mOutFramesAvailable = 0;
+#else
+ throw runtime_error("JackTrip was not built with support for sample rate conversion");
+#endif
+}
+
+//*******************************************************************************
+SampleRateConverter::~SampleRateConverter()
+{
+#ifdef HAVE_LIBSAMPLERATE
+ if (mStatePtr != nullptr) {
+ src_delete(mStatePtr);
+ }
+ delete[] mInDataPtr;
+ delete[] mOutDataPtr;
+ delete[] mOutPopPtr;
+#endif
+}
+
+//*******************************************************************************
+int SampleRateConverter::push(void* inPtr, unsigned int nframes)
+{
+#ifdef HAVE_LIBSAMPLERATE
+ char* framePtr = mOutDataPtr + (mOutFramesAvailable * mBytesPerFrame);
+ mData.data_out = reinterpret_cast<float*>(framePtr);
+ mData.output_frames = (BufferSizeMultiple * mBufferSize) - mOutFramesAvailable;
+ mData.input_frames = nframes + mInFramesLeftover;
+ // interleave input
+ float* fromPtr = reinterpret_cast<float*>(inPtr);
+ float* toPtr =
+ reinterpret_cast<float*>(mInDataPtr + (mInFramesLeftover * mBytesPerFrame));
+ for (unsigned int i = 0; i < nframes; ++i) {
+ for (unsigned int c = 0; c < mNumChannels; ++c) {
+ *(toPtr++) = fromPtr[i + (c * nframes)];
+ }
+ }
+ if (src_process(mStatePtr, &mData) != 0)
+ return -1;
+ mInFramesLeftover = mData.input_frames - mData.input_frames_used;
+ if (mInFramesLeftover > 0 && mData.input_frames_used > 0) {
+ memmove(mInDataPtr, mInDataPtr + (mData.input_frames_used * mBytesPerFrame),
+ mInFramesLeftover * mBytesPerFrame);
+ }
+ mOutFramesAvailable += mData.output_frames_gen;
+#endif
+ return mOutFramesAvailable;
+}
+
+//*******************************************************************************
+float* SampleRateConverter::pop(unsigned int nframes)
+{
+#ifdef HAVE_LIBSAMPLERATE
+ // de-interleave output
+ float* fromPtr = reinterpret_cast<float*>(mOutDataPtr);
+ float* toPtr = mOutPopPtr;
+ for (unsigned int c = 0; c < mNumChannels; ++c) {
+ for (unsigned int i = 0; i < nframes; ++i) {
+ *(toPtr++) = fromPtr[c + (i * mNumChannels)];
+ }
+ }
+ // pop frames
+ const unsigned int remainingFrames = mOutFramesAvailable - nframes;
+ if (remainingFrames > 0) {
+ memmove(mOutDataPtr, mOutDataPtr + (nframes * mBytesPerFrame),
+ (remainingFrames * mBytesPerFrame));
+ }
+ mOutFramesAvailable = remainingFrames;
+ return mOutPopPtr;
+#else
+ return nullptr;
+#endif
+}
--- /dev/null
+//*****************************************************************
+/*
+ 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__
{
size_t commaPos;
char delim = ',';
- if (std::count(nameArg.begin(), nameArg.end(), delim) > 1) {
+
+ // Some audio device names contain commas. Allow these to be escaped with a backslash.
+ int delimCount = std::count(nameArg.begin(), nameArg.end(), delim);
+ std::string escaped = "\\,";
+ std::vector<size_t> escapedPositions;
+
+ size_t position = nameArg.find(escaped, 0);
+ while (position != std::string::npos) {
+ // Store our comma locations for future reference.
+ escapedPositions.push_back(position + 1);
+ cout << position + 1 << endl;
+ position = nameArg.find(escaped, position + escaped.length());
+ }
+
+ if (delimCount - escapedPositions.size() > 1) {
throw std::invalid_argument(
- "Found multiple commas in the --audiodevice argument, cannot parse "
+ "Found multiple unescaped commas in the --audiodevice argument, cannot parse "
"reliably.");
}
- commaPos = nameArg.rfind(delim);
+ int index = escapedPositions.size() - 1;
+ commaPos = nameArg.rfind(delim);
+ while (commaPos > 0 && index >= 0 && commaPos == escapedPositions.at(index)) {
+ commaPos = nameArg.rfind(delim, commaPos - 1);
+ index--;
+ }
+
if (commaPos || nameArg[0] == delim) {
mInputDeviceName = nameArg.substr(0, commaPos);
mOutputDeviceName = nameArg.substr(commaPos + 1);
} else {
mInputDeviceName = mOutputDeviceName = nameArg;
}
+
+ if (escapedPositions.size() > 0) {
+ // We have to get rid of instances of our escape character.
+ position = 0;
+ while ((position = mInputDeviceName.find(escaped, position))
+ != std::string::npos) {
+ mInputDeviceName.replace(position, escaped.length(), ",");
+ position++;
+ }
+ position = 0;
+ while ((position = mOutputDeviceName.find(escaped, position))
+ != std::string::npos) {
+ mOutputDeviceName.replace(position, escaped.length(), ",");
+ position++;
+ }
+ }
}
+
#endif
//*******************************************************************************
std::vector<ProcessPlugin*> outgoingEffects =
mEffects.allocateOutgoingEffects(mNumAudioInputChans - nReservedChans);
for (auto p : outgoingEffects) {
- jackTrip->appendProcessPluginToNetwork(p);
+ QSharedPointer<ProcessPlugin> pShared(p);
+ jackTrip->appendProcessPluginToNetwork(pShared);
}
std::vector<ProcessPlugin*> incomingEffects =
mEffects.allocateIncomingEffects(mNumAudioOutputChans - nReservedChans);
for (auto p : incomingEffects) {
- jackTrip->appendProcessPluginFromNetwork(p);
+ QSharedPointer<ProcessPlugin> pShared(p);
+ jackTrip->appendProcessPluginFromNetwork(pShared);
}
#ifdef WAIR // WAIR
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2025 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file SocketClient.cpp
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date December 2024
+ */
+
+#include "SocketClient.h"
+
+#include <QDebug>
+
+SocketClient::SocketClient(QObject* parent)
+ : QObject(parent), m_socket(new QLocalSocket(this)), m_owns_socket(true)
+{
+}
+
+SocketClient::SocketClient(QSharedPointer<QLocalSocket>& s, QObject* parent)
+ : QObject(parent), m_socket(s), m_owns_socket(false)
+{
+}
+
+SocketClient::~SocketClient()
+{
+ if (isConnected() && m_owns_socket) {
+ m_socket->close();
+ m_socket->waitForDisconnected(1000); // wait for up to 1 second
+ }
+}
+
+bool SocketClient::connect()
+{
+ if (isConnected()) {
+ return true;
+ }
+ m_socket->connectToServer(JACKTRIP_SOCKET_NAME);
+ return m_socket->waitForConnected(1000); // wait for up to 1 second
+}
+
+void SocketClient::close()
+{
+ if (isConnected()) {
+ m_socket->close();
+ m_socket->waitForDisconnected(1000); // wait for up to 1 second
+ }
+}
+
+bool SocketClient::sendHeader(const QString& handler)
+{
+ // sanity check
+ if (!isConnected()) {
+ return false;
+ }
+ QString headerStr = "JackTrip/1.0 ";
+ headerStr += handler;
+ headerStr += "\n";
+ QByteArray headerBytes = headerStr.toLocal8Bit();
+ qint64 writeBytes = m_socket->write(headerBytes);
+ m_socket->waitForBytesWritten(-1);
+ return writeBytes > 0;
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2025 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file SocketClient.h
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date December 2024
+ */
+
+#ifndef __SocketClient_H__
+#define __SocketClient_H__
+
+#include <QLocalSocket>
+#include <QSharedPointer>
+
+// name of the local socket used by JackTrip
+constexpr const char* JACKTRIP_SOCKET_NAME = "JackTrip";
+
+// SocketClient lists for local socket connections from remote JackTrip processes
+class SocketClient : public QObject
+{
+ Q_OBJECT
+
+ public:
+ // default constructor
+ SocketClient(QObject* parent = nullptr);
+
+ // construct with an existing socket
+ SocketClient(QSharedPointer<QLocalSocket>& s, QObject* parent = nullptr);
+
+ // virtual destructor since it inherits from QObject
+ virtual ~SocketClient();
+
+ // return local socket connection
+ inline bool isConnected()
+ {
+ return m_socket->state() == QLocalSocket::ConnectedState;
+ }
+
+ // return local socket connection
+ inline QLocalSocket& getSocket() { return *m_socket; }
+
+ // attempts to connect to remote instance's socket server
+ // returns true if connection was successfully established
+ // returns false if connection failed
+ bool connect();
+
+ // closes the connection to remote instance's socket server
+ void close();
+
+ // send connection header with name of handler to use
+ bool sendHeader(const QString& handler);
+
+ private:
+ // used to check if there is another server already running
+ QSharedPointer<QLocalSocket> m_socket;
+
+ // true if a this owns the socket and should close on destruction
+ bool m_owns_socket = false;
+};
+
+#endif // __SocketClient_H__
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2025 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file SocketServer.cpp
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date December 2024
+ */
+
+#include "SocketServer.h"
+
+#include <QDebug>
+#include <iostream>
+
+#include "SocketClient.h"
+
+using namespace std;
+
+bool SocketServer::start()
+{
+ // sanity check for repeated calls
+ if (!m_instanceServer.isNull()) {
+ m_serverStarted = true;
+ return m_serverStarted;
+ }
+
+ // attempt local socket connection to check for an existing instance
+ SocketClient c;
+ bool established = c.connect();
+
+ if (established) {
+ c.close();
+ m_serverStarted = false;
+ } else {
+ // confirmed that no other jacktrip instance is running
+ m_instanceServer.reset(new QLocalServer());
+ m_instanceServer->setSocketOptions(QLocalServer::WorldAccessOption);
+ QObject::connect(m_instanceServer.data(), &QLocalServer::newConnection, this,
+ &SocketServer::handlePendingConnections, Qt::QueuedConnection);
+ QString serverName(JACKTRIP_SOCKET_NAME);
+ QLocalServer::removeServer(serverName);
+ m_serverStarted = m_instanceServer->listen(serverName);
+ if (m_serverStarted) {
+ cout << "Listening for local connections: "
+ << m_instanceServer->fullServerName().toStdString() << endl;
+ } else {
+ cerr << "Error listening for local connections: "
+ << m_instanceServer->errorString().toStdString() << endl;
+ }
+ }
+
+ // return true if a local socket server was started
+ return m_serverStarted;
+}
+
+void SocketServer::handlePendingConnections()
+{
+ while (m_instanceServer->hasPendingConnections()) {
+ QLocalSocket* connectedSocket = m_instanceServer->nextPendingConnection();
+
+ if (connectedSocket == nullptr || !connectedSocket->waitForConnected()) {
+ qDebug() << "Socket server: never received connection";
+ continue;
+ }
+
+ if (!connectedSocket->waitForReadyRead()
+ && connectedSocket->bytesAvailable() <= 0) {
+ qDebug() << "Socket server: not ready and no bytes available: "
+ << connectedSocket->errorString();
+ continue;
+ }
+
+ if (connectedSocket->bytesAvailable() < (int)sizeof(quint16)) {
+ qDebug() << "Socket server: ready but no bytes available";
+ continue;
+ }
+
+ // first line should be in the format "JackTrip/1.0 HandlerName"
+ // where HandlerName indicates which handler should be used
+ QByteArray in(connectedSocket->readLine());
+ QString header(in);
+ if (!header.startsWith("JackTrip/1.0 ")) {
+ if (header.startsWith("JackTrip/")) {
+ cerr << "Socket server: unknown version: " << header.toStdString()
+ << endl;
+ } else {
+ cerr << "Socket server: invalid header: " << header.toStdString() << endl;
+ }
+ continue;
+ }
+ QString handlerName(header);
+ handlerName.replace("JackTrip/1.0 ", "");
+ handlerName.replace("\n", "");
+
+ cout << "Socket server: received connection for " << handlerName.toStdString()
+ << endl;
+ connectedSocket->setParent(nullptr);
+ QSharedPointer<QLocalSocket> sharedSocket(connectedSocket);
+ handleConnection(handlerName, sharedSocket);
+ }
+}
+
+void SocketServer::handleConnection(const QString& name,
+ QSharedPointer<QLocalSocket>& socket)
+{
+ auto it = m_handlers.find(name);
+ if (it == m_handlers.end()) {
+ cerr << "Socket server: request for unknown handler: " << name.toStdString()
+ << endl;
+ return;
+ }
+ it.value()(socket);
+}
\ No newline at end of file
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2022-2025 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file SocketServer.h
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date December 2024
+ */
+
+#ifndef __SocketServer_H__
+#define __SocketServer_H__
+
+#include <QHash>
+#include <QLocalServer>
+#include <QLocalSocket>
+#include <QScopedPointer>
+#include <QSharedPointer>
+#include <functional>
+
+// SocketHandler is used to handle a new socket connection
+typedef std::function<void(QSharedPointer<QLocalSocket>&)> SocketHandler;
+
+// SocketServer lists for local socket connections from remote JackTrip processes
+class SocketServer : public QObject
+{
+ Q_OBJECT
+
+ public:
+ // default constructor
+ SocketServer() {}
+
+ // virtual destructor since it inherits from QObject
+ virtual ~SocketServer() {}
+
+ // sets handler for local socket connections
+ void addHandler(QString name, SocketHandler f) { m_handlers[name] = f; }
+
+ // attempts to start the local socket server
+ // returns true if it started successfully
+ // returns false if already running in another JackTrip process
+ bool start();
+
+ private slots:
+
+ // called by local socket server to handle requests
+ void handlePendingConnections();
+
+ private:
+ // called by local socket server to handle requests
+ void handleConnection(const QString& name, QSharedPointer<QLocalSocket>& socket);
+
+ // used to listen for requests via local socket connections
+ QScopedPointer<QLocalServer> m_instanceServer;
+
+ // used to handle requests
+ QHash<QString, SocketHandler> m_handlers;
+
+ // true if a local socket server was started, false if remote was detected
+ bool m_serverStarted = false;
+};
+
+#endif // __SocketServer_H__
{
ProcessPlugin::init(samplingRate, bufferSize);
- fs = float(fSamplingFreq);
+ fs = float(mSampleRate);
static_cast<stereotomonodsp*>(stereoToMonoP)->init(fs);
inited = true;
void Tone::init(int samplingRate, int bufferSize)
{
ProcessPlugin::init(samplingRate, bufferSize);
- fs = float(fSamplingFreq);
+ fs = float(mSampleRate);
for (int i = 0; i < mNumChannels; i++) {
static_cast<tonedsp*>(toneP[i])->init(
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;
}
}
+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)
#include "Patcher.h"
#endif
#include "Auth.h"
+#include "OscServer.h"
#include "SslServer.h"
class JackTripWorker; // forward declaration
}
void receivedNewConnection();
void stopCheck();
+ void queueBufferChanged(int queueBufferSize);
signals:
void signalStarted();
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.
// JackTripWorker* mJTWorker; ///< Class that will be used as prototype
QVector<JackTripWorker*>* mJTWorkers; ///< Vector of JackTripWorkers
+ // Pointer to OscServer
+ OscServer* mOscServer;
SslServer mTcpServer;
int mServerPort; //< Server known port number
void Volume::init(int samplingRate, int bufferSize)
{
ProcessPlugin::init(samplingRate, bufferSize);
- fs = float(fSamplingFreq);
+ fs = float(mSampleRate);
for (int i = 0; i < mNumChannels; i++) {
static_cast<volumedsp*>(volumeP[i])
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());
</layout>
</widget>
<resources>
- <include location="qjacktrip.qrc"/>
+ <include location="../images/images.qrc"/>
</resources>
<connections/>
</ui>
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());
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());
// These effects are currently deleted by the AudioInterface of jacktrip.
// May need to change this code if we move to smart pointers.
if (m_ui->outCompressorCheckBox->isChecked()) {
- jackTrip->appendProcessPluginToNetwork(
+ QSharedPointer<ProcessPlugin> pluginPtr(
new Compressor(numSendChannels, false, CompressorPresets::voice));
+ jackTrip->appendProcessPluginToNetwork(pluginPtr);
}
if (m_ui->inCompressorCheckBox->isChecked()) {
- jackTrip->appendProcessPluginFromNetwork(
+ QSharedPointer<ProcessPlugin> pluginPtr(
new Compressor(numRecvChannels, false, CompressorPresets::voice));
+ jackTrip->appendProcessPluginFromNetwork(pluginPtr);
}
if (m_ui->outZitarevCheckBox->isChecked()) {
qreal wetness = m_ui->outZitarevWetnessSlider->value() / 100.0;
- jackTrip->appendProcessPluginToNetwork(
+ QSharedPointer<ProcessPlugin> pluginPtr(
new Reverb(numSendChannels, numSendChannels, 1.0 + wetness));
+ jackTrip->appendProcessPluginToNetwork(pluginPtr);
}
if (m_ui->inZitarevCheckBox->isChecked()) {
qreal wetness = m_ui->inZitarevWetnessSlider->value() / 100.0;
- jackTrip->appendProcessPluginFromNetwork(
+ QSharedPointer<ProcessPlugin> pluginPtr(
new Reverb(numRecvChannels, numRecvChannels, 1.0 + wetness));
+ jackTrip->appendProcessPluginFromNetwork(pluginPtr);
}
if (m_ui->outFreeverbCheckBox->isChecked()) {
qreal wetness = m_ui->outFreeverbWetnessSlider->value() / 100.0;
- jackTrip->appendProcessPluginToNetwork(
+ QSharedPointer<ProcessPlugin> pluginPtr(
new Reverb(numSendChannels, numSendChannels, wetness));
+ jackTrip->appendProcessPluginToNetwork(pluginPtr);
}
if (m_ui->inFreeverbCheckBox->isChecked()) {
qreal wetness = m_ui->inFreeverbWetnessSlider->value() / 100.0;
- jackTrip->appendProcessPluginFromNetwork(
+ QSharedPointer<ProcessPlugin> pluginPtr(
new Reverb(numRecvChannels, numRecvChannels, wetness));
+ jackTrip->appendProcessPluginFromNetwork(pluginPtr);
}
// Limiters go last in the plugin sequence.
if (m_ui->outLimiterCheckBox->isChecked()) {
- jackTrip->appendProcessPluginToNetwork(
+ QSharedPointer<ProcessPlugin> pluginPtr(
new Limiter(numSendChannels, m_ui->outClientsSpinBox->value()));
+ jackTrip->appendProcessPluginToNetwork(pluginPtr);
}
if (m_ui->inLimiterCheckBox->isChecked()) {
- jackTrip->appendProcessPluginFromNetwork(new Limiter(numRecvChannels, 1));
+ QSharedPointer<ProcessPlugin> pluginPtr(new Limiter(numRecvChannels, 1));
+ jackTrip->appendProcessPluginFromNetwork(pluginPtr);
}
}
void QJackTrip::createMeters(quint32 inputChannels, quint32 outputChannels)
{
// These pointers are also deleted by AudioInterface.
- Meter* inputMeter = new Meter(inputChannels);
- Meter* outputMeter = new Meter(outputChannels);
- m_jackTrip->appendProcessPluginToNetwork(inputMeter);
- m_jackTrip->appendProcessPluginFromNetwork(outputMeter);
+ QSharedPointer<ProcessPlugin> inputMeterPtr(new Meter(inputChannels));
+ QSharedPointer<ProcessPlugin> outputMeterPtr(new Meter(outputChannels));
+ m_jackTrip->appendProcessPluginToNetwork(inputMeterPtr);
+ m_jackTrip->appendProcessPluginFromNetwork(outputMeterPtr);
// Create our widgets.
for (quint32 i = 0; i < inputChannels; i++) {
}
m_outputLayout->setRowStretch(outputChannels, 100);
- QObject::connect(inputMeter, &Meter::onComputedVolumeMeasurements, this,
+ QObject::connect(static_cast<Meter*>(inputMeterPtr.get()),
+ &Meter::onComputedVolumeMeasurements, this,
&QJackTrip::updatedInputMeasurements);
- QObject::connect(outputMeter, &Meter::onComputedVolumeMeasurements, this,
+ QObject::connect(static_cast<Meter*>(outputMeterPtr.get()),
+ &Meter::onComputedVolumeMeasurements, this,
&QJackTrip::updatedOutputMeasurements);
}
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
<tabstop>disconnectScriptBrowse</tabstop>
</tabstops>
<resources>
- <include location="qjacktrip.qrc"/>
+ <include location="../images/images.qrc"/>
</resources>
<connections/>
</ui>
<RCC>
<qresource prefix="images">
<file>icon_256.png</file>
+ <file alias="icon_128@2x.png">icon_256.png</file>
<file>icon_128.png</file>
<file>icon_32.png</file>
</qresource>
#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
constexpr int gDefaultCombFilterFeedback = 0;
#endif // endwhere
-// const JackAudioInterface::audioBitResolutionT gDefaultBitResolutionMode =
-// JackAudioInterface::BIT16;
-constexpr AudioInterface::audioBitResolutionT gDefaultBitResolutionMode =
- AudioInterface::BIT16;
constexpr int gDefaultQueueLength = 4;
constexpr int gDefaultOutputQueueLength = 4;
constexpr uint32_t gDefaultSampleRate = 48000;
constexpr const char* gDefaultLocalAddress = "";
constexpr int gDefaultRedundancy = 1;
constexpr int gTimeOutMultiThreadedServer = 10000; // seconds
-constexpr int gUdpWaitTimeout = 50; // milliseconds
+constexpr int gUdpWaitTimeout = 512; // milliseconds
//@}
//*******************************************************************************
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
+ }
+ }
}
}
}
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"
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
Rectangle {
id: audioSettingsView
width: parent.width;
- height: parent.height;
color: backgroundColour
radius: 6 * virtualstudio.uiScale
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 {
userFeedbackSurvey.serverId = serverId;
userFeedbackModal.open();
}
+
+ function onCloseFeedbackSurveyModal() {
+ userFeedbackModal.close();
+ }
}
}
return idx;
}
- function getQueueBufferString () {
- if (audio.queueBuffer == 0) {
- return "auto";
- }
- return audio.queueBuffer + " ms";
- }
-
Rectangle {
id: audioSettingsView
width: 0.8 * parent.width
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
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) {
--- /dev/null
+<?xml version="1.0" encoding="iso-8859-1"?>\r
+<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\r
+<svg fill="#000000" height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" \r
+ viewBox="0 0 462.587 462.587" xml:space="preserve">\r
+<g id="XMLID_12_">\r
+ <g>\r
+ <path d="M457.012,206.595c-1.509-1.656-3.647-2.608-5.888-2.608h-15.69c0,0-54.307-103.321-57.115-108.681\r
+ c-3.012-5.747-7.57-10.778-14.387-10.778c-33.177,0-64.138-7.079-87.179-19.935c-11.496-6.414-20.164-13.917-25.46-21.803V20\r
+ c0-11.046-8.954-20-20-20c-11.046,0-20,8.954-20,20v22.79c-5.296,7.886-13.964,15.389-25.46,21.803\r
+ c-23.042,12.855-54.003,19.935-87.18,19.935c-6.817,0-10.923,4.181-14.387,10.778c-8.099,15.424-57.114,108.681-57.114,108.681\r
+ H11.463c-2.242,0-4.381,0.945-5.891,2.602c-1.51,1.658-2.25,3.874-2.045,6.107c4.394,47.706,44.521,85.063,93.374,85.063\r
+ s88.98-37.357,93.374-85.063c0.205-2.231-0.539-4.445-2.048-6.101c-1.509-1.656-3.647-2.608-5.888-2.608h-15.69l-47.389-90.282\r
+ c36.922-2.961,69.151-13.791,92.034-29.701v328.583h-90.649c-13.807,0-25,11.193-25,25s11.193,25,25,25h217.453\r
+ c13.807,0,25-11.193,25-25s-11.193-25-25-25h-86.804V84.004c22.883,15.91,55.112,26.741,92.033,29.701l-47.389,90.282h-15.69\r
+ c-2.242,0-4.381,0.945-5.891,2.602c-1.51,1.658-2.25,3.874-2.044,6.107c4.393,47.706,44.521,85.063,93.374,85.063\r
+ c48.853,0,88.98-37.357,93.374-85.063C459.266,210.465,458.521,208.252,457.012,206.595z M132.767,203.987H61.034l35.866-68.33\r
+ L132.767,203.987z M329.819,203.987l35.867-68.331l35.867,68.331H329.819z"/>\r
+ </g>\r
+</g>\r
+</svg>
\ No newline at end of file
// https://bugreports.qt.io/browse/QTBUG-55199
#include <QSvgGenerator>
+#include "../AudioSocket.h"
#include "../JackTrip.h"
#include "../Settings.h"
+#include "../SocketClient.h"
+#include "../SocketServer.h"
#include "../jacktrip_globals.h"
#include "JTApplication.h"
#include "WebSocketTransport.h"
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
});
// Register clipboard Qml type
qmlRegisterType<VsQmlClipboard>("VS", 1, 0, "Clipboard");
+ // on window focus, attempt to refresh the access token if the token is more than 1
+ // hour old
+ connect(m_view.data(), &VsQuickView::focusGained, this, [=]() {
+ QString refreshToken = m_auth->refreshToken();
+ if (refreshToken.isEmpty()) {
+ return;
+ }
+
+ qint64 maxElapsedTimeInMs = 1000 * 60 * 60; // 1 hour
+ QDateTime accessTokenTimestamp = m_auth->accessTokenTimestamp();
+ // only refresh after auth process completed the first time
+ if (accessTokenTimestamp.toMSecsSinceEpoch() > 0) {
+ QDateTime accessTokenDeadline = QDateTime::fromMSecsSinceEpoch(
+ accessTokenTimestamp.toMSecsSinceEpoch() + maxElapsedTimeInMs);
+ if (QDateTime::currentDateTime() > accessTokenDeadline) {
+ m_auth->refreshAccessToken(refreshToken);
+ }
+ }
+ });
+
// setup QML view
m_view->engine()->rootContext()->setContextProperty(QStringLiteral("virtualstudio"),
this);
connect(m_view.get(), &VsQuickView::windowClose, this, &VirtualStudio::exit,
Qt::QueuedConnection);
- // prepare handler for deeplinks jacktrip://join/<StudioID>
- m_deepLinkPtr.reset(new VsDeeplink(m_interface.getSettings().getDeeplink()));
- if (!m_deepLinkPtr->getDeeplink().isEmpty()) {
- bool readyForExit = m_deepLinkPtr->waitForReady();
- if (readyForExit)
- std::exit(0);
- }
-
// Log to file
QString logPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
QDir logDir;
ts = new QTextStream(&outFile);
qInstallMessageHandler(qtMessageHandler);
- // Get ready for deep link signals
- QObject::connect(m_deepLinkPtr.get(), &VsDeeplink::signalDeeplink, this,
+ // check if started with a deep link to handle jacktrip://join/<StudioID>
+ QString deepLinkStr = m_interface.getSettings().getDeeplink();
+ if (!deepLinkStr.isEmpty()) {
+ // started with a deep link; check if another instance is already running
+ SocketClient c;
+ if (c.connect()) {
+ // existing instance found; send deeplink to it and exit
+ if (!c.sendHeader("deeplink")) {
+ c.close();
+ std::cerr << "Failed to send deeplink header" << std::endl;
+ std::exit(1);
+ }
+ QLocalSocket& s = c.getSocket();
+ QByteArray deepLinkBytes = deepLinkStr.toLocal8Bit();
+ qint64 bytesWritten = s.write(deepLinkBytes);
+ s.flush();
+ s.waitForBytesWritten(1000);
+ if (bytesWritten != deepLinkBytes.size()) {
+ std::cerr << "Failed to send deeplink" << std::endl;
+ std::exit(1);
+ }
+ std::cout << "sent deeplink: " << deepLinkStr.toStdString() << std::endl;
+ std::exit(0);
+ }
+ }
+
+ // prepare handler for deep link requests
+ m_deepLinkPtr.reset(new VsDeeplink());
+ QObject::connect(m_deepLinkPtr.get(), &VsDeeplink::signalVsDeeplink, this,
&VirtualStudio::handleDeeplinkRequest, Qt::QueuedConnection);
- m_deepLinkPtr->readyForSignals();
+ if (!deepLinkStr.isEmpty()) {
+ QUrl deepLinkUrl(deepLinkStr);
+ m_deepLinkPtr->handleUrl(deepLinkUrl);
+ }
+
+ // prepare handler for local socket connections
+ m_socketServerPtr.reset(new SocketServer());
+ m_socketServerPtr->addHandler("deeplink", [=](QSharedPointer<QLocalSocket>& socket) {
+ m_deepLinkPtr->handleVsDeeplinkRequest(socket);
+ });
+ m_socketServerPtr->addHandler("audio", [=](QSharedPointer<QLocalSocket>& socket) {
+ this->handleAudioSocketRequest(socket);
+ });
+ m_socketServerPtr->start();
+
+ // initialize default QtWebEngineProfile
+ m_qwebEngineProfile = QWebEngineProfile::defaultProfile();
}
void VirtualStudio::show()
return m_webChannelPort;
}
-bool VirtualStudio::hasRefreshToken()
-{
- return !m_refreshToken.isEmpty();
-}
-
QString VirtualStudio::versionString()
{
return QLatin1String(gVersion);
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;
// just to reduce risk of running into a deadlock
emit scheduleStudioRefresh(-1, false);
}
+ if (m_windowState != "browse") {
+ emit closeFeedbackSurveyModal();
+ }
emit windowStateUpdated();
}
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 =
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;
m_refreshToken.clear();
m_userMetadata = QJsonObject();
m_userId.clear();
- emit hasRefreshTokenChanged();
// reset window state
setWindowState(QStringLiteral("login"));
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
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,
}
#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");
// 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("");
// otherwise, assume we are on setup screens and can let the normal flow handle it
}
+void VirtualStudio::handleAudioSocketRequest(QSharedPointer<QLocalSocket>& socket)
+{
+ QSharedPointer<AudioSocket> audioSocketPtr(new AudioSocket(socket));
+ m_audioConfigPtr->registerAudioSocket(audioSocketPtr);
+ triggerReconnect(true);
+}
+
void VirtualStudio::exit()
{
// if multiple close events are received, emit the signalExit to force-quit the app
}
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);
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()
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"
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());
serverState[QStringLiteral("sessionId")].toString());
completeConnection();
}
+ } else if (m_useStudioQueueBuffer && !m_devicePtr.isNull()) {
+ m_devicePtr->setQueueBuffer(m_currentStudio.queueBuffer());
}
emit currentStudioChanged();
// 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[])
#include <QMap>
#include <QMutex>
#include <QNetworkAccessManager>
+#include <QNetworkCookie>
#include <QObject>
#include <QScopedPointer>
#include <QSharedPointer>
#include <QUrl>
#include <QVector>
#include <QWebChannel>
+#include <QWebEngineCookieStore>
+#include <QWebEngineProfile>
#include <QWebSocketServer>
#include "../Settings.h"
#include "vsServerInfo.h"
class JackTrip;
+class QLocalSocket;
+class SocketServer;
class VsAudio;
class VsApi;
class VsAuth;
{
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)
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)
void raiseToTop();
int webChannelPort();
- bool hasRefreshToken();
QString versionString();
QString buildString();
QString copyrightString();
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();
void showAbout();
void openLink(const QString& url);
void handleDeeplinkRequest(const QUrl& url);
+ void handleAudioSocketRequest(QSharedPointer<QLocalSocket>& socket);
void udpWaitingTooLong();
void setWindowState(QString state);
void joinStudio();
void disconnected();
void refreshFinished(int index);
void webChannelPortChanged(int webChannelPort);
- void hasRefreshTokenChanged();
void logoSectionChanged();
void connectedErrorMsgChanged();
void serverModelChanged();
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);
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();
UserInterface& m_interface;
VsServerInfo m_currentStudio;
QNetworkAccessManager* m_networkAccessManagerPtr;
+ QWebEngineProfile* m_qwebEngineProfile;
+ QSharedPointer<SocketServer> m_socketServerPtr;
QScopedPointer<VsQuickView> m_view;
QSharedPointer<VsDeeplink> m_deepLinkPtr;
QSharedPointer<VsAuth> m_auth;
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;
<file>language.svg</file>
<file>micoff.svg</file>
<file>help.svg</file>
+ <file>balance.svg</file>
<file>quiet.svg</file>
<file>loud.svg</file>
<file>refresh.svg</file>
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)
QNetworkRequest request = QNetworkRequest(url);
request.setRawHeader(QByteArray("User-Agent"),
QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
- request.setRawHeader(QByteArray("Authorization"),
- QString("Bearer %1").arg(m_accessToken).toUtf8());
+ QList<QNetworkCookie> cookies;
+ QNetworkCookie authCookie =
+ QNetworkCookie(QByteArray("auth_code"), m_accessToken.toUtf8());
+ cookies.append(authCookie);
+ request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
QNetworkReply* reply = m_networkAccessManager->get(request);
return reply;
}
QNetworkRequest request = QNetworkRequest(url);
request.setRawHeader(QByteArray("User-Agent"),
QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
- request.setRawHeader(QByteArray("Authorization"),
- QString("Bearer %1").arg(m_accessToken).toUtf8());
request.setRawHeader(QByteArray("Content-Type"),
QString("application/json").toUtf8());
+ QList<QNetworkCookie> cookies;
+ QNetworkCookie authCookie =
+ QNetworkCookie(QByteArray("auth_code"), m_accessToken.toUtf8());
+ cookies.append(authCookie);
+ request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
QNetworkReply* reply = m_networkAccessManager->post(request, data);
return reply;
}
QNetworkRequest request = QNetworkRequest(url);
request.setRawHeader(QByteArray("User-Agent"),
QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
- request.setRawHeader(QByteArray("Authorization"),
- QString("Bearer %1").arg(m_accessToken).toUtf8());
request.setRawHeader(QByteArray("Content-Type"),
QString("application/json").toUtf8());
+
+ QList<QNetworkCookie> cookies;
+ QNetworkCookie authCookie =
+ QNetworkCookie(QByteArray("auth_code"), m_accessToken.toUtf8());
+ cookies.append(authCookie);
+ request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
QNetworkReply* reply = m_networkAccessManager->put(request, data);
return reply;
}
QNetworkRequest request = QNetworkRequest(url);
request.setRawHeader(QByteArray("User-Agent"),
QString("JackTrip/%1 (Qt)").arg(gVersion).toUtf8());
- request.setRawHeader(QByteArray("Authorization"),
- QString("Bearer %1").arg(m_accessToken).toUtf8());
+ QList<QNetworkCookie> cookies;
+ QNetworkCookie authCookie =
+ QNetworkCookie(QByteArray("auth_code"), m_accessToken.toUtf8());
+ cookies.append(authCookie);
+ request.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
QNetworkReply* reply = m_networkAccessManager->deleteResource(request);
return reply;
}
\ No newline at end of file
#include <QEventLoop>
#include <QJsonParseError>
+#include <QList>
#include <QMap>
#include <QNetworkAccessManager>
+#include <QNetworkCookie>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QString>
#include "../Analyzer.h"
#endif
+#include "../AudioSocket.h"
#include "../JackTrip.h"
#include "../Meter.h"
#include "../Monitor.h"
, 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();
#else
m_permissionsPtr.reset(new VsPermissions());
#endif
+ qDebug() << "Microphone permissions: " << m_permissionsPtr->micPermission();
}
VsAudio::~VsAudio()
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)
if (!getAudioReady())
return;
emit signalStopAudio();
+ clearAudioSockets(); // force audio sockets to reconnect
if (!block)
return;
WaitForSignal(this, &VsAudio::signalAudioIsNotReady);
}
setBufferSize(settings.value(QStringLiteral("BufferSize"), 128).toInt());
- setQueueBuffer(settings.value(QStringLiteral("QueueBuffer"), 0).toInt());
setFeedbackDetectionEnabled(
settings.value(QStringLiteral("FeedbackDetectionEnabled"), true).toBool());
settings.endGroup();
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();
setInputMuted(false);
// Create plugins
- m_inputMeterPluginPtr = new Meter(numInputChannels);
- m_outputMeterPluginPtr = new Meter(numOutputChannels);
- m_inputVolumePluginPtr = new Volume(numInputChannels);
- m_outputVolumePluginPtr = new Volume(numOutputChannels);
+ Meter* inputMeterPluginPtr = new Meter(numInputChannels);
+ Meter* outputMeterPluginPtr = new Meter(numOutputChannels);
+ Volume* inputVolumePluginPtr = new Volume(numInputChannels);
+ Volume* outputVolumePluginPtr = new Volume(numOutputChannels);
+ QSharedPointer<ProcessPlugin> inputVolumePluginSharedPtr(inputVolumePluginPtr);
+ QSharedPointer<ProcessPlugin> outputVolumePluginSharedPtr(outputVolumePluginPtr);
+ QSharedPointer<ProcessPlugin> inputMeterPluginSharedPtr(inputMeterPluginPtr);
+ QSharedPointer<ProcessPlugin> outputMeterPluginSharedPtr(outputMeterPluginPtr);
// initialize input and output volumes
- m_outputVolumePluginPtr->volumeUpdated(m_outMultiplier);
- m_inputVolumePluginPtr->volumeUpdated(m_inMultiplier);
- m_inputVolumePluginPtr->muteUpdated(m_inMuted);
+ outputVolumePluginPtr->volumeUpdated(m_outMultiplier);
+ inputVolumePluginPtr->volumeUpdated(m_inMultiplier);
+ inputVolumePluginPtr->muteUpdated(m_inMuted);
// Connect plugins for communication with UI
- connect(m_inputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
+ connect(inputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
&VsAudio::updatedInputVuMeasurements);
- connect(m_outputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
+ connect(outputMeterPluginPtr, &Meter::onComputedVolumeMeasurements, this,
&VsAudio::updatedOutputVuMeasurements);
- connect(this, &VsAudio::updatedInputVolume, m_inputVolumePluginPtr,
+ connect(this, &VsAudio::updatedInputVolume, inputVolumePluginPtr,
&Volume::volumeUpdated);
- connect(this, &VsAudio::updatedOutputVolume, m_outputVolumePluginPtr,
+ connect(this, &VsAudio::updatedOutputVolume, outputVolumePluginPtr,
&Volume::volumeUpdated);
- connect(this, &VsAudio::updatedInputMuted, m_inputVolumePluginPtr,
+ connect(this, &VsAudio::updatedInputMuted, inputVolumePluginPtr,
&Volume::muteUpdated);
// Note that plugin ownership is passed to the JackTrip class
// In particular, the AudioInterface that it uses to connect
- audioInterface.appendProcessPluginToNetwork(m_inputVolumePluginPtr);
- audioInterface.appendProcessPluginToNetwork(m_inputMeterPluginPtr);
+ audioInterface.appendProcessPluginToNetwork(inputVolumePluginSharedPtr);
+ audioInterface.appendProcessPluginToNetwork(inputMeterPluginSharedPtr);
if (forJackTrip) {
// plugins for stream going to audio interface
- audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr);
+ audioInterface.appendProcessPluginFromNetwork(outputVolumePluginSharedPtr);
// Setup monitor
// Note: Constructor determines how many internal monitor buffers to allocate
- m_monitorPluginPtr = new Monitor(std::max(numInputChannels, numOutputChannels));
- m_monitorPluginPtr->volumeUpdated(m_monMultiplier);
- audioInterface.appendProcessPluginToMonitor(m_monitorPluginPtr);
- connect(this, &VsAudio::updatedMonitorVolume, m_monitorPluginPtr,
+ Monitor* monitorPluginPtr =
+ new Monitor(std::max(numInputChannels, numOutputChannels));
+ monitorPluginPtr->volumeUpdated(m_monMultiplier);
+ connect(this, &VsAudio::updatedMonitorVolume, monitorPluginPtr,
&Monitor::volumeUpdated);
+ QSharedPointer<ProcessPlugin> monitorPluginSharedPtr(monitorPluginPtr);
+ audioInterface.appendProcessPluginToMonitor(monitorPluginSharedPtr);
#ifndef NO_FEEDBACK
// Setup output analyzer
if (m_feedbackDetectionEnabled) {
- m_outputAnalyzerPluginPtr = new Analyzer(numOutputChannels);
- m_outputAnalyzerPluginPtr->setIsMonitoringAnalyzer(true);
- audioInterface.appendProcessPluginToMonitor(m_outputAnalyzerPluginPtr);
- connect(m_outputAnalyzerPluginPtr, &Analyzer::signalFeedbackDetected, this,
+ Analyzer* outputAnalyzerPluginPtr = new Analyzer(numOutputChannels);
+ outputAnalyzerPluginPtr->setIsMonitoringAnalyzer(true);
+ connect(outputAnalyzerPluginPtr, &Analyzer::signalFeedbackDetected, this,
&VsAudio::detectedFeedbackLoop);
+ QSharedPointer<ProcessPlugin> outputAnalyzerPluginSharedPtr(
+ outputAnalyzerPluginPtr);
+ audioInterface.appendProcessPluginToMonitor(outputAnalyzerPluginSharedPtr);
}
#endif
// Setup output meter
// Note: Add this to monitor process to include self-volume
- m_outputMeterPluginPtr->setIsMonitoringMeter(true);
- audioInterface.appendProcessPluginToMonitor(m_outputMeterPluginPtr);
+ outputMeterPluginPtr->setIsMonitoringMeter(true);
+ audioInterface.appendProcessPluginToMonitor(outputMeterPluginSharedPtr);
} else {
// tone plugin is used to test audio output
Tone* outputTonePluginPtr = new Tone(getNumOutputChannels());
connect(this, &VsAudio::signalPlayOutputAudio, outputTonePluginPtr,
&Tone::triggerPlayback);
- audioInterface.appendProcessPluginFromNetwork(outputTonePluginPtr);
+ QSharedPointer<ProcessPlugin> outputTonePluginSharedPtr(outputTonePluginPtr);
+ audioInterface.appendProcessPluginFromNetwork(outputTonePluginSharedPtr);
// plugins for stream going to audio interface
- audioInterface.appendProcessPluginFromNetwork(m_outputVolumePluginPtr);
- audioInterface.appendProcessPluginFromNetwork(m_outputMeterPluginPtr);
+ audioInterface.appendProcessPluginFromNetwork(outputVolumePluginSharedPtr);
+ audioInterface.appendProcessPluginFromNetwork(outputMeterPluginSharedPtr);
+ }
+
+ // clear out any audio sockets that have disconnected
+ QMutexLocker locker(&m_audioSocketMutex);
+ for (auto i = m_audioSockets.begin(); i != m_audioSockets.end();) {
+ if ((*i)->isConnected()) {
+ audioInterface.appendAudioSocket(*i);
+ ++i;
+ } else {
+ i = m_audioSockets.erase(i);
+ }
}
}
+void VsAudio::registerAudioSocket(QSharedPointer<AudioSocket>& s)
+{
+ QMutexLocker locker(&m_audioSocketMutex);
+ m_audioSockets.push_back(s);
+}
+
+void VsAudio::clearAudioSockets()
+{
+ QMutexLocker locker(&m_audioSocketMutex);
+ m_audioSockets.clear();
+}
+
void VsAudio::setDeviceModels(QJsonArray inputComboModel, QJsonArray outputComboModel)
{
m_inputComboModel = inputComboModel;
#include <QJsonArray>
#include <QList>
+#include <QMutex>
#include <QObject>
#include <QSharedPointer>
#include <QString>
#endif
class Analyzer;
+class AudioSocket;
class JackTrip;
class Meter;
class Monitor;
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
}
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; }
const QString& getDevicesWarningHelpUrl() const { return m_devicesWarningHelpUrl; }
const QString& getDevicesErrorHelpUrl() const { return m_devicesErrorHelpUrl; }
bool getHighLatencyFlag() const { return m_highLatencyFlag; }
+
+ // called by local socket server to process audio requests
+ void registerAudioSocket(QSharedPointer<AudioSocket>& s);
+ void clearAudioSockets();
+
public slots:
// setters for state shared with QML
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);
void audioBackendChanged(bool useRtAudio);
void sampleRateChanged();
void bufferSizeChanged();
- void queueBufferChanged();
void numInputChannelsChanged(int numChannels);
void numOutputChannelsChanged(int numChannels);
void baseInputChannelChanged(int baseChannel);
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;
// other state not shared with QML
QSharedPointer<VsPermissions> m_permissionsPtr;
QScopedPointer<VsAudioWorker> m_audioWorkerPtr;
+ QVector<QSharedPointer<AudioSocket>> m_audioSockets;
+ QMutex m_audioSocketMutex;
QThread* m_workerThreadPtr;
QTimer m_inputClipTimer;
QTimer m_outputClipTimer;
- Meter* m_inputMeterPluginPtr;
- Meter* m_outputMeterPluginPtr;
- Volume* m_inputVolumePluginPtr;
- Volume* m_outputVolumePluginPtr;
- Monitor* m_monitorPluginPtr;
bool mHasErrors; ///< true if one or more error callbacks have been triggered
-#ifndef NO_FEEDBACK
- Analyzer* m_outputAnalyzerPluginPtr;
-#endif
-
QStringList m_audioBackendComboModel = {"JACK", "RtAudio"};
QStringList m_bufferSizeComboModel = {"16", "32", "64", "128", "256", "512", "1024"};
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));
&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");
}
emit deviceCodeExpired();
}
+void VsAuth::refreshTimerTimedOut()
+{
+ if (m_refreshToken != "") {
+ refreshAccessToken(m_refreshToken);
+ }
+}
+
void VsAuth::handleRefreshSucceeded(QString accessToken)
{
qDebug() << "Successfully refreshed access token";
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)
m_errorMessage = QStringLiteral("");
m_attemptingRefreshToken = false;
m_isAuthenticated = true;
+ m_accessTokenTimestamp = QDateTime::currentDateTime();
+
+ m_refreshTimer->start();
emit updatedUserId(m_userId);
emit updatedAuthenticationStage(m_authenticationStage);
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();
m_authenticationMethod = QStringLiteral("");
m_attemptingRefreshToken = false;
m_isAuthenticated = false;
+ m_accessTokenTimestamp = QDateTime::fromMSecsSinceEpoch(0);
emit updatedUserId(m_userId);
emit updatedAuthenticationStage(m_authenticationStage);
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();
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()
}
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);
#ifndef VSAUTH_H
#define VSAUTH_H
+#include <QDateTime>
#include <QNetworkAccessManager>
#include <QQmlContext>
#include <QQmlEngine>
#include <QString>
+#include <QTimer>
#include <iostream>
#include "vsApi.h"
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);
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();
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);
QString m_userId;
QString m_accessToken;
QString m_refreshToken;
+ QDateTime m_accessTokenTimestamp = QDateTime::fromMSecsSinceEpoch(0);
QNetworkAccessManager* m_networkAccessManager;
VsApi* m_api;
QScopedPointer<VsDeviceCodeFlow> m_deviceCodeFlow;
+ QScopedPointer<QTimer> m_refreshTimer;
};
#endif
\ No newline at end of file
//*****************************************************************
/**
- * \file vsDeeplink.cpp
- * \author Aaron Wyatt, based on code by Matt Horton
- * \date February 2023
+ * \file VsDeeplink.cpp
+ * \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
+ * \date December 2024
*/
#include "vsDeeplink.h"
#include <QDesktopServices>
#include <QDir>
#include <QEventLoop>
+#include <QLocalSocket>
#include <QMutexLocker>
#include <QSettings>
#include <QTimer>
-VsDeeplink::VsDeeplink(const QString& deeplink) : m_deeplink(deeplink)
+VsDeeplink::VsDeeplink()
{
setUrlScheme();
- checkForInstance();
QDesktopServices::setUrlHandler(QStringLiteral("jacktrip"), this, "handleUrl");
}
QDesktopServices::unsetUrlHandler(QStringLiteral("jacktrip"));
}
-bool VsDeeplink::waitForReady()
-{
- while (!m_isReady) {
- QTimer timer;
- timer.setTimerType(Qt::CoarseTimer);
- timer.setSingleShot(true);
-
- QEventLoop loop;
- QObject::connect(this, &VsDeeplink::signalIsReady, &loop, &QEventLoop::quit);
- QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
- timer.start(100); // wait for 100ms
- loop.exec();
- }
- return m_readyToExit;
-}
-
-void VsDeeplink::readyForSignals()
-{
- m_readyForSignals = true;
- if (!m_deeplink.isEmpty()) {
- emit signalDeeplink(m_deeplink);
- m_deeplink.clear();
- }
-}
-
void VsDeeplink::handleUrl(const QUrl& url)
{
- if (m_readyForSignals) {
- emit signalDeeplink(url);
- } else {
- m_deeplink = url;
- }
-}
-
-void VsDeeplink::checkForInstance()
-{
- // Create socket
- m_instanceCheckSocket.reset(new QLocalSocket(this));
- QObject::connect(m_instanceCheckSocket.data(), &QLocalSocket::connected, this,
- &VsDeeplink::connectionReceived, Qt::QueuedConnection);
- // Create instanceServer to prevent new instances from being created
- void (QLocalSocket::*errorFunc)(QLocalSocket::LocalSocketError);
-#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
- errorFunc = &QLocalSocket::error;
-#else
- errorFunc = &QLocalSocket::errorOccurred;
-#endif
- QObject::connect(m_instanceCheckSocket.data(), errorFunc, this,
- &VsDeeplink::connectionFailed);
- // Check for existing instance
- m_instanceCheckSocket->connectToServer("jacktripExists");
+ emit signalVsDeeplink(url);
}
-void VsDeeplink::connectionReceived()
+void VsDeeplink::handleVsDeeplinkRequest(QSharedPointer<QLocalSocket>& socket)
{
- // another jacktrip instance is running
- if (!m_deeplink.isEmpty()) {
- // pass deeplink to existing instance before quitting
- QString deeplinkStr = m_deeplink.toString();
- QByteArray baDeeplink = deeplinkStr.toLocal8Bit();
- qint64 writeBytes = m_instanceCheckSocket->write(baDeeplink);
- if (writeBytes < 0) {
- qDebug() << "sending deeplink failed";
- } else {
- qDebug() << "Sent deeplink request to remote instance";
- }
-
- // make sure it isn't processed again
- m_deeplink.clear();
-
- // End process if another instance exists
- m_readyToExit = true;
+ if (!socket->waitForReadyRead() && socket->bytesAvailable() <= 0) {
+ qDebug() << "VsDeeplink socket: not ready and no bytes available: "
+ << socket->errorString();
+ socket->close();
+ return;
}
- m_instanceCheckSocket->waitForBytesWritten();
- m_instanceCheckSocket->disconnectFromServer(); // remove next
-
- // let main thread know we are finished
- m_isReady = true;
- emit signalIsReady();
-}
-
-void VsDeeplink::connectionFailed(QLocalSocket::LocalSocketError socketError)
-{
- switch (socketError) {
- case QLocalSocket::ServerNotFoundError:
- case QLocalSocket::SocketTimeoutError:
- case QLocalSocket::ConnectionRefusedError:
- // no other jacktrip instance is running, so we will take over handling deep links
- qDebug() << "Listening for deep link requests";
- m_instanceServer.reset(new QLocalServer(this));
- m_instanceServer->setSocketOptions(QLocalServer::WorldAccessOption);
- m_instanceServer->listen("jacktripExists");
- QObject::connect(m_instanceServer.data(), &QLocalServer::newConnection, this,
- &VsDeeplink::handleDeeplinkRequest, Qt::QueuedConnection);
- break;
- case QLocalSocket::PeerClosedError:
- break;
- default:
- qDebug() << m_instanceCheckSocket->errorString();
+ if (socket->bytesAvailable() < (int)sizeof(quint16)) {
+ qDebug() << "VsDeeplink socket: ready but no bytes available";
+ socket->close();
+ return;
}
- // let main thread know we are finished
- m_isReady = true;
- emit signalIsReady();
-}
-
-void VsDeeplink::handleDeeplinkRequest()
-{
- while (m_instanceServer->hasPendingConnections()) {
- // Receive URL from 2nd instance
- QLocalSocket* connectedSocket = m_instanceServer->nextPendingConnection();
-
- if (connectedSocket == nullptr || !connectedSocket->waitForConnected()) {
- qDebug() << "Deeplink socket: never received connection";
- return;
- }
-
- if (!connectedSocket->waitForReadyRead()
- && connectedSocket->bytesAvailable() <= 0) {
- qDebug() << "Deeplink socket: not ready and no bytes available: "
- << connectedSocket->errorString();
- return;
- }
-
- if (connectedSocket->bytesAvailable() < (int)sizeof(quint16)) {
- qDebug() << "Deeplink socket: ready but no bytes available";
- break;
- }
-
- QByteArray in(connectedSocket->readAll());
- QString urlString(in);
- handleUrl(urlString);
- }
+ QByteArray in(socket->readAll());
+ socket->close();
+ QString urlString(in);
+ handleUrl(urlString);
}
void VsDeeplink::setUrlScheme()
//*****************************************************************
/**
- * \file vsDeeplink.h
+ * \file VsDeeplink.h
* \author Mike Dickey, based on code by Aaron Wyatt and Matt Horton
- * \date August 2023
+ * \date December 2024
*/
#ifndef __VSDEEPLINK_H__
#define __VSDEEPLINK_H__
-#include <QLocalServer>
-#include <QLocalSocket>
-#include <QScopedPointer>
+#include <QObject>
+#include <QSharedPointer>
#include <QString>
#include <QUrl>
+class QLocalSocket;
+
class VsDeeplink : public QObject
{
Q_OBJECT
public:
// construct with an instance of the application, to parse command line args
- VsDeeplink(const QString& deeplink);
+ VsDeeplink();
// virtual destructor since it inherits from QObject
// this is used to unregister url handler
virtual ~VsDeeplink();
- // blocks main thread until local socket server is ready
- // returns true if a deeplink was handled and we should exit now
- bool waitForReady();
-
- // used to let us know VirtualStudio is ready to process deeplink signals
- void readyForSignals();
-
- // returns deeplink extracted from command line, if any
- const QUrl& getDeeplink() const { return m_deeplink; }
-
- signals:
-
- // signalIsReady is emitted when the local socket server is ready
- void signalIsReady();
-
- // signalDeeplink is emitted when we want the local instance to process a deeplink
- void signalDeeplink(const QUrl& url);
-
- private slots:
-
- // handleUrl is called to trigger processing of a deeplink
+ public slots:
+ // handleUrl is called to trigger processing of a VsDeeplink
void handleUrl(const QUrl& url);
- // checks to see if another instance of jacktrip is available to process requests.
- // if there is, this will send any command line deeplinks to it and exit.
- // if there isn't, this will start listening for requests.
- void checkForInstance();
-
- // called if a connection was established with another instance of VS
- void connectionReceived();
+ // called by local socket server to process VsDeeplink requests
+ void handleVsDeeplinkRequest(QSharedPointer<QLocalSocket>& socket);
- // called if unable to connect to another instance of VS
- void connectionFailed(QLocalSocket::LocalSocketError socketError);
-
- // called by local socket server to process deeplink requests
- void handleDeeplinkRequest();
+ signals:
+ // signalVsDeeplink is emitted when we want the local instance to process a VsDeeplink
+ void signalVsDeeplink(const QUrl& url);
private:
// sets url scheme for windows machines; does nothing on other platforms
static void setUrlScheme();
-
- // used to check if there is a virtual studio instance already running
- QScopedPointer<QLocalSocket> m_instanceCheckSocket;
-
- // used to listen for deeplink requests via local socket connections
- QScopedPointer<QLocalServer> m_instanceServer;
-
- // used to synchronize with main thread at startup
- bool m_isReady = false;
- bool m_readyForSignals = false;
- bool m_readyToExit = false;
- QUrl m_deeplink;
};
#endif // __VSDEEPLINK_H__
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 = {};
((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);
}
[[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,
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());
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()
{
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);
public slots:
void syncDeviceSettings();
+ void setQueueBuffer(int queueBuffer);
private slots:
void handleJackTripError();
QScopedPointer<JackTrip> m_jackTrip;
QRandomGenerator m_randomizer;
QTimer m_sendVolumeTimer;
+ int m_queueBuffer = 0;
bool m_networkOutage = false;
bool m_stopping = false;
};
{
// 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 {
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;
#include <QAbstractSocket>
#include <QDateTime>
+#include <QList>
+#include <QNetworkRequest>
#include <QObject>
#include <QTimer>
#include <QUrl>
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();
}
signals:
void windowClose();
+ void focusGained();
+ void focusLost();
private slots:
void closeWindow();
}
QNetworkRequest req = QNetworkRequest(QUrl(m_url));
- QString authVal = "Bearer ";
- authVal.append(m_token);
req.setRawHeader(QByteArray("Upgrade"), QByteArray("websocket"));
req.setRawHeader(QByteArray("Connection"), QByteArray("Upgrade"));
- req.setRawHeader(QByteArray("Authorization"), authVal.toUtf8());
req.setRawHeader(QByteArray("Origin"), QByteArray("http://jacktrip.local"));
req.setRawHeader(QByteArray("APIPrefix"), m_apiPrefix.toUtf8());
req.setRawHeader(QByteArray("APISecret"), m_apiSecret.toUtf8());
+ QList<QNetworkCookie> cookies;
+ QNetworkCookie authCookie = QNetworkCookie(QByteArray("auth_code"), m_token.toUtf8());
+ cookies.append(authCookie);
+ req.setHeader(QNetworkRequest::CookieHeader, QVariant::fromValue(cookies));
+
if (!m_webSocket.isNull()) {
m_webSocket->open(req);
qDebug() << "Opened websocket:" << QUrl(m_url).toString(QUrl::RemoveQuery);
// 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();
#define VSWEBSOCKET_H
#include <QList>
+#include <QNetworkRequest>
#include <QObject>
#include <QScopedPointer>
#include <QSslError>
--- /dev/null
+//*****************************************************************
+/*
+ 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);
--- /dev/null
+//*****************************************************************
+/*
+ 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);
+ }
+}
--- /dev/null
+//*****************************************************************
+/*
+ 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};
+};
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2025 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+// Based on the VST3 SDK Data Exchange tutorial at
+// https://steinbergmedia.github.io/vst3_dev_portal/pages/Tutorials/Data+Exchange.html
+
+#pragma once
+
+#include <cstdint>
+
+#include "public.sdk/source/vst/utility/dataexchange.h"
+
+// this is currently overkill for a bool, but we can use it for other things
+// such as volume meters in the future
+struct DataBlock {
+ bool connectedState;
+};
+
+inline DataBlock* toDataBlock(const Steinberg::Vst::DataExchangeBlock& block)
+{
+ if (block.blockID != Steinberg::Vst::InvalidDataExchangeBlockID)
+ return reinterpret_cast<DataBlock*>(block.data);
+ return nullptr;
+}
--- /dev/null
+//*****************************************************************
+/*
+ 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
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2024-2025 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+// Based on the Hello World VST 3 example from Steinberg
+// https://github.com/steinbergmedia/vst3_example_plugin_hello_world
+
+#include "JackTripVSTProcessor.h"
+
+#include "../AudioSocket.h"
+#include "JackTripVST.h"
+#include "JackTripVSTDataBlock.h"
+#include "base/source/fstreamer.h"
+#include "pluginterfaces/vst/ivstparameterchanges.h"
+
+using namespace std;
+using namespace Steinberg;
+
+// uncomment to generate log file, for debugging purposes
+// #define JACKTRIP_VST_LOG
+
+#ifdef JACKTRIP_VST_LOG
+#if defined(_WIN32)
+#define JACKTRIP_VST_LOG_PATH "c:/JackTripTemp"
+#define JACKTRIP_VST_LOG_FILE "c:/JackTripTemp/vst.log"
+#else
+#define JACKTRIP_VST_LOG_PATH "/tmp/jacktrip"
+#define JACKTRIP_VST_LOG_FILE "/tmp/jacktrip/vst.log"
+#endif
+#include <filesystem>
+#include <fstream>
+#include <iostream>
+
+static ofstream kLogFile;
+
+void qtMessageHandler([[maybe_unused]] QtMsgType type,
+ [[maybe_unused]] const QMessageLogContext& context,
+ const QString& msg)
+{
+ kLogFile << msg.toStdString() << endl;
+}
+#endif
+
+// any multiplier less than this is considered to be silent
+constexpr double kSilentMul = 0.0000001;
+
+static QCoreApplication* sQtAppPtr = nullptr;
+
+static QCoreApplication* getQtAppPtr()
+{
+ if (sQtAppPtr == nullptr) {
+ sQtAppPtr = QCoreApplication::instance();
+ if (sQtAppPtr == nullptr) {
+ int argc = 0;
+ sQtAppPtr = new QCoreApplication(argc, nullptr);
+ sQtAppPtr->setAttribute(Qt::AA_NativeWindows);
+ }
+ }
+ return sQtAppPtr;
+}
+
+//------------------------------------------------------------------------
+// JackTripVSTProcessor
+//------------------------------------------------------------------------
+JackTripVSTProcessor::JackTripVSTProcessor()
+{
+ //--- set the wanted controller for our processor
+ setControllerClass(kJackTripVSTControllerUID);
+ mCurrentExchangeBlock =
+ Vst::DataExchangeBlock{nullptr, 0, Vst::InvalidDataExchangeBlockID};
+}
+
+//------------------------------------------------------------------------
+JackTripVSTProcessor::~JackTripVSTProcessor() {}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::initialize(FUnknown* context)
+{
+ // Here the Plug-in will be instantiated
+
+ //---always initialize the parent-------
+ tresult result = AudioEffect::initialize(context);
+ // if everything Ok, continue
+ if (result != kResultOk) {
+ return result;
+ }
+
+ //--- create Audio IO ------
+ addAudioInput(STR16("Stereo In"), Vst::SpeakerArr::kStereo);
+ addAudioOutput(STR16("Stereo Out"), Vst::SpeakerArr::kStereo);
+
+ getQtAppPtr();
+
+ mInputBuffer = new float*[AudioSocketNumChannels];
+ mOutputBuffer = new float*[AudioSocketNumChannels];
+ for (int i = 0; i < AudioSocketNumChannels; i++) {
+ mInputBuffer[i] = new float[AudioSocketMaxSamplesPerBlock];
+ mOutputBuffer[i] = new float[AudioSocketMaxSamplesPerBlock];
+ }
+
+#ifdef JACKTRIP_VST_LOG
+ if (!filesystem::is_directory(JACKTRIP_VST_LOG_PATH)) {
+ if (!filesystem::create_directory(JACKTRIP_VST_LOG_PATH)) {
+ qDebug() << "Failed to create VST log directory: " << JACKTRIP_VST_LOG_PATH;
+ }
+ }
+ kLogFile.open(JACKTRIP_VST_LOG_FILE, ios::app);
+ if (kLogFile.is_open()) {
+ kLogFile << "JackTrip VST initialized" << endl;
+ kLogFile.flush();
+ cout.rdbuf(kLogFile.rdbuf());
+ cerr.rdbuf(kLogFile.rdbuf());
+ } else {
+ qDebug() << "Failed to open VST log file: " << JACKTRIP_VST_LOG_FILE;
+ }
+ qInstallMessageHandler(qtMessageHandler);
+#endif
+
+ qDebug() << "JackTrip VST initialized";
+
+ return kResultOk;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::terminate()
+{
+ mSocketPtr.reset();
+
+ for (int i = 0; i < AudioSocketNumChannels; i++) {
+ delete[] mInputBuffer[i];
+ delete[] mOutputBuffer[i];
+ }
+ delete[] mInputBuffer;
+ delete[] mOutputBuffer;
+
+ qDebug() << "JackTrip VST terminated";
+
+#ifdef JACKTRIP_VST_LOG
+ kLogFile.close();
+#endif
+
+ //---do not forget to call parent ------
+ return AudioEffect::terminate();
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::connect(Vst::IConnectionPoint* other)
+{
+ auto result = Vst::AudioEffect::connect(other);
+ if (result == kResultTrue) {
+ auto configCallback = [](Vst::DataExchangeHandler::Config& config,
+ [[maybe_unused]] const Vst::ProcessSetup& setup) {
+ config.blockSize = sizeof(DataBlock);
+ config.numBlocks = 2; // max number of pending blocks allowed
+ config.alignment = 32;
+ config.userContextID = 0;
+ return true;
+ };
+ mDataExchangePtr.reset(new Vst::DataExchangeHandler(this, configCallback));
+ mDataExchangePtr->onConnect(other, getHostContext());
+ }
+ return result;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::disconnect(Vst::IConnectionPoint* other)
+{
+ if (!mDataExchangePtr.isNull()) {
+ mDataExchangePtr->onDisconnect(other);
+ mDataExchangePtr.reset();
+ }
+ return AudioEffect::disconnect(other);
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API
+JackTripVSTProcessor::setBusArrangements(Vst::SpeakerArrangement* inputs, int32 numIns,
+ Vst::SpeakerArrangement* outputs, int32 numOuts)
+{
+ // based on again example from sdk (support 1->1 or 2->2)
+ if (numIns == 1 && numOuts == 1) {
+ // the host wants Mono => Mono (or 1 channel -> 1 channel)
+ if (Vst::SpeakerArr::getChannelCount(inputs[0]) == 1
+ && Vst::SpeakerArr::getChannelCount(outputs[0]) == 1) {
+ auto* bus = FCast<Steinberg::Vst::AudioBus>(audioInputs.at(0));
+ if (bus) {
+ // check if we are Mono => Mono, if not we need to recreate the busses
+ if (bus->getArrangement() != inputs[0]) {
+ getAudioInput(0)->setArrangement(inputs[0]);
+ getAudioInput(0)->setName(STR16("Mono In"));
+ getAudioOutput(0)->setArrangement(outputs[0]);
+ getAudioOutput(0)->setName(STR16("Mono Out"));
+ }
+ return kResultOk;
+ }
+ } else {
+ // the host wants something else than Mono => Mono,
+ // in this case we are always Stereo => Stereo
+ auto* bus = FCast<Steinberg::Vst::AudioBus>(audioInputs.at(0));
+ if (bus) {
+ tresult result = kResultFalse;
+ // the host wants 2->2 (could be LsRs -> LsRs)
+ if (Vst::SpeakerArr::getChannelCount(inputs[0]) == 2
+ && Vst::SpeakerArr::getChannelCount(outputs[0]) == 2) {
+ getAudioInput(0)->setArrangement(inputs[0]);
+ getAudioInput(0)->setName(STR16("Stereo In"));
+ getAudioOutput(0)->setArrangement(outputs[0]);
+ getAudioOutput(0)->setName(STR16("Stereo Out"));
+ result = kResultTrue;
+ } else if (bus->getArrangement() != Vst::SpeakerArr::kStereo) {
+ // the host want something different than 1->1 or 2->2 : in this case
+ // we want stereo
+ getAudioInput(0)->setArrangement(Vst::SpeakerArr::kStereo);
+ getAudioInput(0)->setName(STR16("Stereo In"));
+ getAudioOutput(0)->setArrangement(Vst::SpeakerArr::kStereo);
+ getAudioOutput(0)->setName(STR16("Stereo Out"));
+ result = kResultFalse;
+ }
+ return result;
+ }
+ }
+ }
+ return kResultFalse;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::setActive(TBool state)
+{
+ if (state) {
+ // sanity check to ensure these were initialized by setupProcessing()
+ if (mSampleRate == 0 || mBufferSize == 0) {
+ return kResultFalse;
+ }
+ // create a audio new socket
+ if (mSocketPtr.isNull()) {
+ // not yet initialized
+ mSocketPtr.reset(new AudioSocket(true));
+ // automatically retry to establish connection
+ mSocketPtr->setRetryConnection(true);
+ mSocketPtr->connect(mSampleRate, mBufferSize);
+ }
+ // activate data exchange API
+ if (!mDataExchangePtr.isNull()) {
+ mDataExchangePtr->onActivate(processSetup);
+ }
+ } else {
+ // disconnect from remote when inactive
+ mSocketPtr.reset();
+ // deactivate data exchange API
+ if (!mDataExchangePtr.isNull()) {
+ mDataExchangePtr->onDeactivate();
+ }
+ }
+
+ qDebug() << "JackTrip VST setActive(" << int(state) << ")";
+
+ //--- called when the Plug-in is enable/disable (On/Off) -----
+ return AudioEffect::setActive(state);
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::setProcessing(TBool state)
+{
+ qDebug() << "JackTrip VST setProcessing(" << int(state) << ")";
+ return AudioEffect::setProcessing(state);
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::process(Vst::ProcessData& data)
+{
+ // sanity check; should never happen
+ if (mSocketPtr.isNull())
+ return kResultFalse;
+
+ //--- Read inputs parameter changes-----------
+ if (data.inputParameterChanges) {
+ int32 numParamsChanged = data.inputParameterChanges->getParameterCount();
+ for (int32 index = 0; index < numParamsChanged; index++) {
+ Vst::IParamValueQueue* paramQueue =
+ data.inputParameterChanges->getParameterData(index);
+ if (paramQueue) {
+ Vst::ParamValue value;
+ int32 sampleOffset;
+ int32 numPoints = paramQueue->getPointCount();
+ switch (paramQueue->getParameterId()) {
+ case JackTripVSTParams::kParamGainSendId:
+ if (paramQueue->getPoint(numPoints - 1, sampleOffset, value)
+ == kResultTrue)
+ mSendGain = value;
+ break;
+ case JackTripVSTParams::kParamMixOutputId:
+ if (paramQueue->getPoint(numPoints - 1, sampleOffset, value)
+ == kResultTrue)
+ mOutputMix = value;
+ break;
+ case JackTripVSTParams::kParamGainOutputId:
+ if (paramQueue->getPoint(numPoints - 1, sampleOffset, value)
+ == kResultTrue)
+ mOutputGain = value;
+ break;
+ case JackTripVSTParams::kParamConnectedId:
+ if (paramQueue->getPoint(numPoints - 1, sampleOffset, value)
+ == kResultTrue)
+ mConnected = value > 0;
+ break;
+ case JackTripVSTParams::kBypassId:
+ if (paramQueue->getPoint(numPoints - 1, sampleOffset, value)
+ == kResultTrue)
+ mBypass = value > 0;
+ break;
+ }
+ }
+ }
+ if (numParamsChanged > 0)
+ updateVolumeMultipliers();
+ }
+
+#if 0
+ if (mLogFile.is_open()) {
+ mLogFile << "JackTrip VST process: inputs=" << data.numInputs
+ << ", outputs=" << data.numOutputs
+ << ", samples=" << data.numSamples
+ << endl;
+ }
+#endif
+
+ // handle connection state change
+ if (mConnected != mSocketPtr->isConnected()) {
+ // try both methods because some hosts only support one or the other.
+ // first try to use data output parameters, if available.
+ bool updatedConnectedState = false;
+ if (data.outputParameterChanges) {
+ int32 index = 0;
+ Steinberg::Vst::IParamValueQueue* paramQueue =
+ data.outputParameterChanges->addParameterData(kParamConnectedId, index);
+ if (paramQueue) {
+ int8 connectedState = mSocketPtr->isConnected() ? 1 : 0;
+ int32 index2 = 0;
+ if (paramQueue->addPoint(0, connectedState, index2) == kResultOk) {
+ updatedConnectedState = true;
+ }
+ }
+ }
+ // if unsuccessful, try to use data exchange API
+ if (!updatedConnectedState && !mDataExchangePtr.isNull()) {
+ if (mCurrentExchangeBlock.blockID == Vst::InvalidDataExchangeBlockID) {
+ acquireNewExchangeBlock();
+ }
+ if (auto block = toDataBlock(mCurrentExchangeBlock)) {
+ block->connectedState = mSocketPtr->isConnected();
+ if (mDataExchangePtr->sendCurrentBlock()) {
+ updatedConnectedState = true;
+ }
+ // we need to acquire a new block before the current one will be sent
+ acquireNewExchangeBlock();
+ }
+ }
+ if (updatedConnectedState) {
+ // we can update our state after successfully deliver the change
+ mConnected = mSocketPtr->isConnected();
+ }
+ }
+
+ //--- Process Audio---------------------
+ //--- ----------------------------------
+ if (data.numInputs == 0 || data.numOutputs == 0) {
+ // nothing to do
+ return kResultOk;
+ }
+
+ if (data.numSamples <= 0) {
+ // nothing to do
+ return kResultOk;
+ }
+
+ if (data.numSamples > AudioSocketMaxSamplesPerBlock) {
+ // just a sanity check; shouldn't happen
+ data.numSamples = AudioSocketMaxSamplesPerBlock;
+ }
+
+ if (mBypass) {
+ // copy input to output
+ for (int i = 0; i < data.inputs[0].numChannels && i < data.outputs[0].numChannels;
+ i++) {
+ memcpy(data.outputs[0].channelBuffers32[i],
+ data.inputs[0].channelBuffers32[i],
+ data.numSamples * sizeof(Vst::Sample32));
+ }
+ data.outputs[0].silenceFlags = data.inputs[0].silenceFlags;
+ return kResultOk;
+ }
+
+ // clear buffers
+ for (int i = 0; i < AudioSocketNumChannels; i++) {
+ memset(mInputBuffer[i], 0, data.numSamples * sizeof(float));
+ memset(mOutputBuffer[i], 0, data.numSamples * sizeof(float));
+ }
+
+ // copy input to buffer
+ if (mSendMul >= kSilentMul) {
+ uint64 isSilentFlag = 1;
+ int channelsIn = min(data.inputs[0].numChannels, AudioSocketNumChannels);
+ for (int i = 0; i < channelsIn; i++) {
+ bool isSilent = isSilentFlag & data.inputs[0].silenceFlags;
+ isSilentFlag <<= 1;
+ if (isSilent)
+ continue;
+ Vst::Sample32* inBuffer = data.inputs[0].channelBuffers32[i];
+ for (int j = 0; j < data.numSamples; j++) {
+ mInputBuffer[i][j] = inBuffer[j] * mSendMul;
+ }
+ }
+ }
+
+ // send to audio socket
+ mSocketPtr->compute(data.numSamples, mInputBuffer, mOutputBuffer);
+
+ // copy buffer to output
+ for (int i = 0; i < data.outputs[0].numChannels; i++) {
+ bool silent = true;
+ memset(data.outputs[0].channelBuffers32[i], 0,
+ data.numSamples * sizeof(Vst::Sample32));
+ if (mPassMul >= kSilentMul || mRecvMul >= kSilentMul) {
+ Vst::Sample32* outBuffer = data.outputs[0].channelBuffers32[i];
+ for (int j = 0; j < data.numSamples; j++) {
+ if (i < AudioSocketNumChannels && mRecvMul >= kSilentMul) {
+ outBuffer[j] = mOutputBuffer[i][j] * mRecvMul;
+ }
+ if (i < data.inputs[0].numChannels && mPassMul >= kSilentMul) {
+ outBuffer[j] += data.inputs[0].channelBuffers32[i][j] * mPassMul;
+ }
+ if (silent && outBuffer[j] != 0) {
+ silent = false;
+ }
+ }
+ }
+ if (silent) {
+ data.outputs[0].silenceFlags |= static_cast<Steinberg::uint64>(1) << i;
+ }
+ }
+
+ return kResultOk;
+}
+
+//------------------------------------------------------------------------
+float JackTripVSTProcessor::gainToVol(double gain)
+{
+ // handle min and max
+ if (gain < kSilentMul)
+ return 0;
+ if (gain > 0.9999999)
+ return 1.0;
+ // simple logarithmic conversion
+ return exp(log(1000) * gain) / 1000.0;
+}
+
+//------------------------------------------------------------------------
+void JackTripVSTProcessor::updateVolumeMultipliers()
+{
+ // convert [0-1.0] gain (dB) values into [0-1.0] volume multiplers
+ float outMul = gainToVol(mOutputGain);
+ mSendMul = gainToVol(mSendGain);
+ mRecvMul = mOutputMix * outMul;
+ mPassMul = (1.0f - mOutputMix) * outMul;
+
+ qDebug() << "JackTrip VST send =" << mSendMul << "(" << mSendGain
+ << "), out =" << outMul << "(" << mOutputGain << "), mix =" << mOutputMix
+ << ", recv =" << mRecvMul << ", pass =" << mPassMul;
+}
+
+//------------------------------------------------------------------------
+void JackTripVSTProcessor::acquireNewExchangeBlock()
+{
+ mCurrentExchangeBlock = mDataExchangePtr->getCurrentOrNewBlock();
+ if (auto block = toDataBlock(mCurrentExchangeBlock)) {
+ block->connectedState = false; // default
+ }
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::setupProcessing(Vst::ProcessSetup& newSetup)
+{
+ mSampleRate = newSetup.sampleRate;
+ mBufferSize = static_cast<int>(newSetup.maxSamplesPerBlock);
+
+ qDebug() << "JackTrip VST setupProcessing: mSampleRate=" << mSampleRate
+ << ", mbufferSize=" << mBufferSize;
+
+ //--- called before any processing ----
+ return AudioEffect::setupProcessing(newSetup);
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::canProcessSampleSize(int32 symbolicSampleSize)
+{
+ // by default kSample32 is supported
+ if (symbolicSampleSize == Vst::kSample32)
+ return kResultTrue;
+
+ // disable the following comment if your processing support kSample64
+ /* if (symbolicSampleSize == Vst::kSample64)
+ return kResultTrue; */
+
+ return kResultFalse;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::setState(IBStream* state)
+{
+ if (!state)
+ return kResultFalse;
+
+ // called when we load a preset or project, the model has to be reloaded
+
+ IBStreamer streamer(state, kLittleEndian);
+
+ float sendGain = 1.f;
+ if (streamer.readFloat(sendGain) == false)
+ return kResultFalse;
+
+ float outputMix = 1.f;
+ if (streamer.readFloat(outputMix) == false)
+ return kResultFalse;
+
+ float outputGain = 1.f;
+ if (streamer.readFloat(outputGain) == false)
+ return kResultFalse;
+
+ int8 connectedState = 0;
+ if (streamer.readInt8(connectedState) == false)
+ return kResultFalse;
+
+ int32 bypassState = 0;
+ if (streamer.readInt32(bypassState) == false)
+ return kResultFalse;
+
+ mSendGain = sendGain;
+ mOutputMix = outputMix;
+ mOutputGain = outputGain;
+ mConnected = connectedState > 0;
+ mBypass = bypassState > 0;
+
+ updateVolumeMultipliers();
+
+ return kResultOk;
+}
+
+//------------------------------------------------------------------------
+tresult PLUGIN_API JackTripVSTProcessor::getState(IBStream* state)
+{
+ // here we need to save the model (preset or project)
+
+ float sendGain = mSendGain;
+ float outputMix = mOutputMix;
+ float outputGain = mOutputGain;
+ int8 connectedState = mConnected ? 1 : 0;
+ int32 bypassState = mBypass ? 1 : 0;
+
+ IBStreamer streamer(state, kLittleEndian);
+ streamer.writeFloat(sendGain);
+ streamer.writeFloat(outputMix);
+ streamer.writeFloat(outputGain);
+ streamer.writeInt8(connectedState);
+ streamer.writeInt32(bypassState);
+
+ return kResultOk;
+}
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2024-2025 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+// Based on the Hello World VST 3 example from Steinberg
+// https://github.com/steinbergmedia/vst3_example_plugin_hello_world
+
+#pragma once
+
+#include <QCoreApplication>
+#include <QScopedPointer>
+#include <QThread>
+
+#include "public.sdk/source/vst/utility/dataexchange.h"
+#include "public.sdk/source/vst/vstaudioeffect.h"
+
+class AudioSocket;
+
+//------------------------------------------------------------------------
+// JackTripVSTProcessor
+//------------------------------------------------------------------------
+class JackTripVSTProcessor : public Steinberg::Vst::AudioEffect
+{
+ public:
+ JackTripVSTProcessor();
+ ~JackTripVSTProcessor() SMTG_OVERRIDE;
+
+ // Create function
+ static Steinberg::FUnknown* createInstance(void* /*context*/)
+ {
+ return (Steinberg::Vst::IAudioProcessor*)new JackTripVSTProcessor;
+ }
+
+ //--- ---------------------------------------------------------------------
+ // AudioEffect overrides:
+ //--- ---------------------------------------------------------------------
+ /** Called at first after constructor */
+ Steinberg::tresult PLUGIN_API initialize(Steinberg::FUnknown* context) SMTG_OVERRIDE;
+
+ /** Called at the end before destructor */
+ Steinberg::tresult PLUGIN_API terminate() SMTG_OVERRIDE;
+
+ /** Called to connect data exchange API */
+ Steinberg::tresult PLUGIN_API
+ connect(Steinberg::Vst::IConnectionPoint* other) override;
+
+ /** Called to disconnect data exchange API */
+ Steinberg::tresult PLUGIN_API
+ disconnect(Steinberg::Vst::IConnectionPoint* other) override;
+
+ /** Called to set bus arrangements */
+ Steinberg::tresult PLUGIN_API setBusArrangements(
+ Steinberg::Vst::SpeakerArrangement* inputs, Steinberg::int32 numIns,
+ Steinberg::Vst::SpeakerArrangement* outputs,
+ Steinberg::int32 numOuts) SMTG_OVERRIDE;
+
+ /** Switch the Plug-in on/off */
+ Steinberg::tresult PLUGIN_API setActive(Steinberg::TBool state) SMTG_OVERRIDE;
+
+ /** Called by audio thread immediately before processing starts, and after it ends */
+ Steinberg::tresult PLUGIN_API setProcessing(Steinberg::TBool state) SMTG_OVERRIDE;
+
+ /** Will be called before any process call */
+ Steinberg::tresult PLUGIN_API setupProcessing(Steinberg::Vst::ProcessSetup& newSetup)
+ SMTG_OVERRIDE;
+
+ /** Asks if a given sample size is supported see SymbolicSampleSizes. */
+ Steinberg::tresult PLUGIN_API
+ canProcessSampleSize(Steinberg::int32 symbolicSampleSize) SMTG_OVERRIDE;
+
+ /** Here we go...the process call */
+ Steinberg::tresult PLUGIN_API process(Steinberg::Vst::ProcessData& data)
+ SMTG_OVERRIDE;
+
+ /** For persistence */
+ Steinberg::tresult PLUGIN_API setState(Steinberg::IBStream* state) SMTG_OVERRIDE;
+ Steinberg::tresult PLUGIN_API getState(Steinberg::IBStream* state) SMTG_OVERRIDE;
+
+ //------------------------------------------------------------------------
+ protected:
+ static float gainToVol(double gain);
+ void updateVolumeMultipliers();
+ void acquireNewExchangeBlock();
+
+ Steinberg::Vst::ParamValue mSendGain = 1.f;
+ Steinberg::Vst::ParamValue mOutputMix = 0;
+ Steinberg::Vst::ParamValue mOutputGain = 1.f;
+ float mSendMul = 1.f;
+ float mRecvMul = 0;
+ float mPassMul = 1.f;
+ bool mConnected = false;
+ bool mBypass = false;
+
+ private:
+ QScopedPointer<AudioSocket> mSocketPtr;
+ QScopedPointer<Steinberg::Vst::DataExchangeHandler> mDataExchangePtr;
+ Steinberg::Vst::DataExchangeBlock mCurrentExchangeBlock;
+ float** mInputBuffer;
+ float** mOutputBuffer;
+ Steinberg::Vst::SampleRate mSampleRate = 0;
+ int mBufferSize = 0;
+};
--- /dev/null
+{
+ "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
--- /dev/null
+{
+ "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
--- /dev/null
+#include <windows.h>
+#include "../source/version.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// Version
+/////////////////////////////////////////////////////////////////////////////
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION MAJOR_VERSION_INT,SUB_VERSION_INT,RELEASE_NUMBER_INT,BUILD_NUMBER_INT
+ PRODUCTVERSION MAJOR_VERSION_INT,SUB_VERSION_INT,RELEASE_NUMBER_INT,BUILD_NUMBER_INT
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS 0x40004L
+ FILETYPE 0x1L
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040004e4"
+ BEGIN
+ VALUE "FileVersion", FULL_VERSION_STR"\0"
+ VALUE "ProductVersion", FULL_VERSION_STR"\0"
+ VALUE "OriginalFilename", stringOriginalFilename"\0"
+ VALUE "FileDescription", stringFileDescription"\0"
+ VALUE "InternalName", stringFileDescription"\0"
+ VALUE "ProductName", stringFileDescription"\0"
+ VALUE "CompanyName", stringCompanyName"\0"
+ VALUE "LegalCopyright", stringLegalCopyright"\0"
+ VALUE "LegalTrademarks", stringLegalTrademarks"\0"
+ //VALUE "PrivateBuild", " \0"
+ //VALUE "SpecialBuild", " \0"
+ //VALUE "Comments", " \0"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x400, 1252
+ END
+END
--- /dev/null
+[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
--- /dev/null
+//*****************************************************************
+/*
+ JackTrip: A System for High-Quality Audio Network Performance
+ over the Internet
+
+ Copyright (c) 2024 JackTrip Labs, Inc.
+
+ Permission is hereby granted, free of charge, to any person
+ obtaining a copy of this software and associated documentation
+ files (the "Software"), to deal in the Software without
+ restriction, including without limitation the rights to use,
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following
+ conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ OTHER DEALINGS IN THE SOFTWARE.
+*/
+//*****************************************************************
+
+/**
+ * \file audio_socket_tests.cpp
+ * \author Mike Dickey
+ * \date December 2024
+ * \license MIT
+ */
+
+#include <iostream>
+#include <QCoreApplication>
+
+#include "AudioSocket.h"
+#include "jacktrip_globals.h"
+
+using std::cout;
+using std::cerr;
+using std::endl;
+
+const int SAMPLE_RATE = 48000;
+const int BUFFER_SIZE = 128;
+const int NUM_CHANNELS = 2;
+
+class MyThread : public QThread
+{
+public:
+ MyThread(AudioSocket& socket) : s(socket) {}
+ virtual ~MyThread() {}
+ void run() override {
+ float **inputs = new float*[NUM_CHANNELS];
+ float **outputs = new float*[NUM_CHANNELS];
+ for (int i = 0; i < NUM_CHANNELS; i++) {
+ inputs[i] = new float[BUFFER_SIZE];
+ outputs[i] = new float[BUFFER_SIZE];
+ for (int j = 0; j < BUFFER_SIZE; j++) {
+ inputs[i][j] = j;
+ }
+ }
+
+ setRealtimeProcessPriority();
+
+ do {
+ s.compute(BUFFER_SIZE, inputs, outputs);
+ QThread::usleep(BUFFER_SIZE * 1000000 / SAMPLE_RATE);
+ } while (isRunning());
+
+ cout << "Exiting" << endl;
+ }
+
+private:
+ AudioSocket& s;
+};
+
+int main(int argc, char** argv)
+{
+ QCoreApplication app(argc, argv);
+
+ AudioSocket s;
+ if (!s.connect(SAMPLE_RATE, BUFFER_SIZE)) {
+ cerr << "Failed to connect: " << s.getSocket().errorString().toStdString() << endl;
+ return -1;
+ }
+ s.setRetryConnection(true);
+
+ MyThread thread(s);
+ QObject::connect(&thread, &QThread::finished, &app, &QCoreApplication::quit);
+ thread.start();
+
+ return app.exec();
+}
)\r
if exist ..\builddir\release\jacktrip.exe (set JACKTRIP=..\builddir\release\jacktrip.exe) else (set JACKTRIP=..\builddir\jacktrip.exe)\r
copy %JACKTRIP% deploy\\r
+if exist ..\builddir\JackTrip.vst3 (\r
+ echo Including JackTrip.vst3\r
+ mkdir deploy\JackTrip.vst3\r
+ mkdir deploy\JackTrip.vst3\Contents\r
+ xcopy /E ..\src\vst3\resources deploy\JackTrip.vst3\Contents\Resources\\r
+ copy ..\LICENSE.md deploy\JackTrip.vst3\Contents\Resources\LICENSE.md\r
+ xcopy /E ..\LICENSES deploy\JackTrip.vst3\Contents\Resources\LICENSES\\r
+ mkdir deploy\JackTrip.vst3\Contents\x86_64-win\r
+ copy ..\builddir\JackTrip.vst3 deploy\JackTrip.vst3\Contents\x86_64-win\JackTrip.vst3\r
+)\r
cd deploy\r
\r
set "WIXDEFINES="\r
set WIXDEFINES=!WIXDEFINES! -ddynamic -dqt%QTVERSION%\r
)\r
\r
-copy ..\jacktrip.wxs .\\r
-copy ..\qt%QTVERSION%.wxs .\\r
.\jacktrip --test-gui\r
if %ERRORLEVEL% NEQ 0 (\r
echo You need to build jacktrip with gui support to build the installer.\r
exit /b 1\r
)\r
+\r
rem Get our version number\r
for /f "tokens=*" %%a in ('.\jacktrip -v ^| findstr VERSION') do for %%b in (%%~a) do set VERSION=%%b\r
for /f "tokens=1 delims=-" %%a in ("%VERSION%") do set VERSION=%%a\r
echo Version=%VERSION%\r
-candle.exe -arch x64 -ext WixUIExtension -ext WixUtilExtension -dVersion=%VERSION%%WIXDEFINES% ..\jacktrip.wxs ..\qt%QTVERSION%.wxs\r
-light.exe -ext WixUIExtension -ext WixUtilExtension -o JackTrip.msi jacktrip.wixobj qt%QTVERSION%.wixobj\r
+\r
+if exist JackTrip.vst3 (\r
+ powershell -Command "(gc JackTrip.vst3\Contents\Resources\moduleinfo.json) -replace '%%VERSION%%', '%VERSION%' | Out-File -encoding ASCII JackTrip.vst3\Contents\Resources\moduleinfo.json"\r
+ candle.exe -arch x64 -ext WixUIExtension -ext WixUtilExtension -dvst=true -dVersion=%VERSION%%WIXDEFINES% ..\jacktrip.wxs ..\jacktrip-vst3.wxs ..\qt%QTVERSION%.wxs\r
+ light.exe -ext WixUIExtension -ext WixUtilExtension -o JackTrip.msi jacktrip.wixobj jacktrip-vst3.wixobj qt%QTVERSION%.wixobj\r
+) else (\r
+ candle.exe -arch x64 -ext WixUIExtension -ext WixUtilExtension -dVersion=%VERSION%%WIXDEFINES% ..\jacktrip.wxs ..\qt%QTVERSION%.wxs\r
+ light.exe -ext WixUIExtension -ext WixUtilExtension -o JackTrip.msi jacktrip.wixobj qt%QTVERSION%.wixobj\r
+)\r
endlocal\r
--- /dev/null
+<?xml version="1.0" encoding="utf-8"?>\r
+<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">\r
+ <Fragment>\r
+ <DirectoryRef Id="VST3DIR">\r
+ <Directory Id="dirC529895EE77450F3C72B644D3B50C911" Name="JackTrip.vst3" />\r
+ </DirectoryRef>\r
+ </Fragment>\r
+ <Fragment>\r
+ <ComponentGroup Id="jacktripvst3">\r
+ <Component Id="cmpA89E2B3B233F97AB53B4FE9FA684CAB2" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{E1C88C7E-8D90-434C-940C-2A867C70CFDE}">\r
+ <File Id="fil0E885921A1CB51256D60C9E0024B88D2" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\background.png" />\r
+ </Component>\r
+ <Component Id="cmp3A3529091478AB40CC0D6F40EC0F7DB1" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{09CF4480-E124-45E7-BBF9-878C695E4A40}">\r
+ <File Id="filB09E0C2581F99AFB520F0ADBA4A2DFB2" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\background_2x.png" />\r
+ </Component>\r
+ <Component Id="cmp542902F0C5B22695EE2E3E0A78590723" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{353BA4A9-64F7-4609-B1B7-81EDDDECEC78}">\r
+ <File Id="filD75564B094910F7A9BC39FBC5B9DE70F" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\Dual_LED.png" />\r
+ </Component>\r
+ <Component Id="cmp811349B294D2CD0BB3FC552CCE42DD6B" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{D7364DBB-E501-4B99-B7D1-58ED45702A5A}">\r
+ <File Id="filBC885E8370B8BD97431A54039D53EDED" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\JackTripEditor.uidesc" />\r
+ </Component>\r
+ <Component Id="cmp9F06646184EC3EA2FAF8242332D67338" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{D061BD66-EC3F-4F6E-BEF9-8282D24FD046}">\r
+ <File Id="filD2134FEF8A44C8959D9A355ECB8D93A5" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\LICENSE.md" />\r
+ </Component>\r
+ <Component Id="cmpB981C2A08F9547568395028EB4709838" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{366731F7-5AE7-4DA9-B3E7-73F0662AB52E}">\r
+ <File Id="fil710883FC5749D9E8A486068DAADEF594" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\moduleinfo.json" />\r
+ </Component>\r
+ <Component Id="cmpC5B85F125D058623BA6601590DCF5D45" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{C336EFB0-3773-4F2C-82C2-3F6FA19E27F4}">\r
+ <File Id="filDCBF690BADDC59592CB00A70CB303639" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\Sercan_Moog_Knob.png" />\r
+ </Component>\r
+ <Component Id="cmp91C15F6937472EDADF18A3DF2850B68B" Directory="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Guid="{F340688E-5925-4D32-9039-730CFFBA31F5}">\r
+ <File Id="filAFDCAB68DCCBE776AA6F238B56AA907F" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\win32resource.rc" />\r
+ </Component>\r
+ <Component Id="cmp695B40111D773D55335931EEDF9B3871" Directory="dir3E32C5B4E1995E12C8A983E30AC42ED0" Guid="{A385F0E4-A33B-4B1D-9701-E50A96933115}">\r
+ <File Id="fil8849A500FEBD18AD3169AF868F318EBF" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\LICENSES\AVC.txt" />\r
+ </Component>\r
+ <Component Id="cmp30746D59FCB8AB350A1516C27BC53EAC" Directory="dir3E32C5B4E1995E12C8A983E30AC42ED0" Guid="{E39D3840-A8C5-46DA-B312-598B24D7526B}">\r
+ <File Id="fil19A081EB090F775872B98B344AF8D03F" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\LICENSES\GPL-3.0.txt" />\r
+ </Component>\r
+ <Component Id="cmp2425D35CB0206AC69A746C931011450C" Directory="dir3E32C5B4E1995E12C8A983E30AC42ED0" Guid="{EF7ED563-EBE0-4301-947B-1CE45AC69F8C}">\r
+ <File Id="fil971B4767893F685F1E7D68C66CD9710E" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\LICENSES\LGPL-3.0-only.txt" />\r
+ </Component>\r
+ <Component Id="cmpF438EE21A995DA392086A47877FAEC24" Directory="dir3E32C5B4E1995E12C8A983E30AC42ED0" Guid="{60E31672-8BAC-4BC0-9221-B3AB9949B5AC}">\r
+ <File Id="fil56278695D7A872F1D231A820110CC704" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\Resources\LICENSES\MIT.txt" />\r
+ </Component>\r
+ <Component Id="cmpC4A0D6F318491F474FFDF80A46C53B24" Directory="dir7528754B446ECAFE2C3463C0916A775D" Guid="{BB89EFC4-CD0B-417B-9AC2-56444262C0ED}">\r
+ <File Id="fil5F012D81F27C3F619CE5A017C47B3727" KeyPath="yes" Source="SourceDir\JackTrip.vst3\Contents\x86_64-win\JackTrip.vst3" />\r
+ </Component>\r
+ </ComponentGroup>\r
+ </Fragment>\r
+ <Fragment>\r
+ <DirectoryRef Id="dirC529895EE77450F3C72B644D3B50C911">\r
+ <Directory Id="dir8DFAD377CE63CA353FD57A8086B458C5" Name="Contents" />\r
+ </DirectoryRef>\r
+ </Fragment>\r
+ <Fragment>\r
+ <DirectoryRef Id="dir8DFAD377CE63CA353FD57A8086B458C5">\r
+ <Directory Id="dir7528754B446ECAFE2C3463C0916A775D" Name="x86_64-win" />\r
+ </DirectoryRef>\r
+ </Fragment>\r
+ <Fragment>\r
+ <DirectoryRef Id="dir8DFAD377CE63CA353FD57A8086B458C5">\r
+ <Directory Id="dirD7DDD8DA90BC893A3523C3F478EDBFA5" Name="Resources" />\r
+ </DirectoryRef>\r
+ </Fragment>\r
+ <Fragment>\r
+ <DirectoryRef Id="dirD7DDD8DA90BC893A3523C3F478EDBFA5">\r
+ <Directory Id="dir3E32C5B4E1995E12C8A983E30AC42ED0" Name="LICENSES" />\r
+ </DirectoryRef>\r
+ </Fragment>\r
+</Wix>
\ No newline at end of file
<Directory Id='INSTALLDIR' Name='JackTrip'>\r
</Directory>\r
</Directory>\r
+ <?ifdef vst?>\r
+ <Directory Id="CommonFiles64Folder" Name="CFiles">\r
+ <Directory Id='VST3DIR' Name='VST3'>\r
+ </Directory>\r
+ </Directory>\r
+ <?endif?>\r
<Directory Id='ProgramMenuFolder' Name='Programs'>\r
<Directory Id='ProgramMenuDir' Name='JackTrip'>\r
<Component Id="ProgramMenuDir" Guid="2f25e96a-47a5-4eca-b367-a78ce536d9b9">\r
<ComponentRef Id='ProgramMenuDir' />\r
</Feature>\r
\r
+ <?ifdef vst?>\r
+ <Feature Id='VST3Install' Title='JackTrip VST3 Plugin' Description='VST3 Plugin' Level='1'>\r
+ <ComponentGroupRef Id='jacktripvst3' />\r
+ </Feature>\r
+ <?endif?>\r
+\r
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Launch JackTrip" />\r
<Property Id="WixShellExecTarget" Value="[INSTALLDIR]\jacktrip.exe" />\r
<CustomAction Id="LaunchApplication" BinaryKey="WixCA" DllEntry="WixShellExec" Impersonate="yes" />\r