also subject to Qt's license. The builds provided on GitHub's Releases page use
open source distribution of Qt, licensed under LGPL.
+Windows builds of JackTrip may include support for ASIO.
+ASIO is a trademark and software of Steinberg Media Technologies GmbH.
+
Using JackTrip to join Virtual Studios on Windows computers may use AVC (h264)
video encoders and decoders subject to the AVC Patent Portfolio License.
---
### LGPL License
---8<-- "LICENSES/LGPL-3.0-only.txt"
\ No newline at end of file
+--8<-- "LICENSES/LGPL-3.0-only.txt"
+
+---
+### AVC License
+--8<-- "LICENSES/AVC.txt"
\ No newline at end of file
### Ubuntu and Debian/Raspbian (Qt6)
```sh
apt install --no-install-recommends build-essential autoconf automake libtool make libjack-jackd2-dev git help2man libclang-dev libdbus-1-dev libdbus-1-dev python3-jinja2
+apt install -y libqt6core6 libqt6gui6 libqt6network6 libqt6widgets6 libqt6qml6 libqt6qmlcore6 libqt6quick6 libqt6quickcontrols2-6 libqt6svg6 libqt6webchannel6 libqt6webengine6-data libqt6webenginecore6 libqt6webenginecore6-bin libqt6webenginequick6 libqt6websockets6 libqt6shadertools6 qt6-qpa-plugins qml6-module-qtquick-controls qml6-module-qtqml-workerscript qml6-module-qtquick-templates qml6-module-qtquick-layouts qml6-module-qt5compat-graphicaleffects qml6-module-qtwebchannel qml6-module-qtwebengine qml6-module-qtquick-window
apt install qt6-base-dev qt6-base-dev-tools qmake6 qt6-tools-dev qt6-declarative-dev qt6-webengine-dev qt6-webview-dev qt6-webview-plugins libqt6svg6-dev libqt6websockets6-dev libqt6core5compat6-dev libqt6shadertools6-dev libgl1-mesa-dev
# for GUI builds
apt install libfreetype6-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev libdrm-dev libglu1-mesa-dev libwayland-dev libwayland-egl1-mesa libgles2-mesa-dev libwayland-server0 libwayland-egl-backend-dev libxcb1-dev libxext-dev libfontconfig1-dev libxrender-dev libxcb-keysyms1-dev libxcb-image0-dev libxcb-shm0-dev libxcb-icccm4-dev '^libxcb.*-dev' libxcb-render-util0-dev libxcomposite-dev libgtk-3-dev
+- Version: "2.2.2"
+ Date: 2024-02-09
+ Description:
+ - (updated) VS Mode updated network connection thresholds
+ - (updated) VS Mode improved sample rate flexibility for Windows
+ - (fixed) VS Mode inconsistent deep link handling on Windows
+ - (fixed) Throttle console errors for UDP waiting too long
- Version: "2.2.1"
Date: 2024-01-29
Description:
{
"app_name": "JackTrip",
"releases": [
+ {
+ "version": "2.2.1",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.1",
+ "download": {
+ "date": "2024-01-30T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.1-macOS-x64-signed-installer.pkg",
+ "downloadSize": "177373978",
+ "sha256": "d02e5de0cee389ee39c789ad6fe8859823944cf2c6af15f8d80249f3134f4653"
+ }
+ },
+ {
+ "version": "2.2.0",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0",
+ "download": {
+ "date": "2024-01-22T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-macOS-x64-signed-installer.pkg",
+ "downloadSize": "177373618",
+ "sha256": "ecef1ac2ae1fd3f2da40017f5da1fca6d966946a016ba80116ca960d88f04a53"
+ }
+ },
+ {
+ "version": "2.2.0-beta1",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0-beta1",
+ "download": {
+ "date": "2024-01-17T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-beta1-macOS-x64-signed-installer.pkg",
+ "downloadSize": "177373175",
+ "sha256": "6512e524d022eebe5b2e928d008c0fdb85dbaa453179952ee232f0d73d0d68eb"
+ }
+ },
{
"version": "2.1.0",
"changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0",
{
"app_name": "JackTrip",
"releases": [
+ {
+ "version": "2.2.1",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.1",
+ "download": {
+ "date": "2024-01-30T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.1-Windows-x64-signed-installer.msi",
+ "downloadSize": "108511232",
+ "sha256": "193825d24745cd5a052ae57f1345b02924fc269aa69324428e7a177b9c58aa05"
+ }
+ },
+ {
+ "version": "2.2.0",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0",
+ "download": {
+ "date": "2024-01-22T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-Windows-x64-signed-installer.msi",
+ "downloadSize": "108511232",
+ "sha256": "92aeb6ba74fcb5cade48962aa4696a77fb7b2434622c22097cfa9da037b32fb3"
+ }
+ },
+ {
+ "version": "2.2.0-beta1",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0-beta1",
+ "download": {
+ "date": "2024-01-17T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-beta1-Windows-x64-signed-installer.msi",
+ "downloadSize": "108511232",
+ "sha256": "24401a0adaf8753f68d4303bda0d08fed35032168254ab7445766216cfb73980"
+ }
+ },
{
"version": "2.1.0",
"changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0",
{
"app_name": "JackTrip",
"releases": [
+ {
+ "version": "2.2.1",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.1",
+ "download": {
+ "date": "2024-01-30T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.1-Linux-x64-binary.zip",
+ "downloadSize": "1239788",
+ "sha256": "bfd986377b54c1ab84f16e0c0fd5ca61ed50e6cec281f7505e95d2b663af32f7"
+ }
+ },
+ {
+ "version": "2.2.0",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0",
+ "download": {
+ "date": "2024-01-22T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-Linux-x64-binary.zip",
+ "downloadSize": "1239784",
+ "sha256": "c5ce96f64ea204f1a17a951e9d39fd247e925fd21f55ae48a8bc27d6767cf675"
+ }
+ },
{
"version": "2.1.0",
"changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0",
{
"app_name": "JackTrip",
"releases": [
+ {
+ "version": "2.2.1",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.1",
+ "download": {
+ "date": "2024-01-30T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.1-macOS-x64-signed-installer.pkg",
+ "downloadSize": "177373978",
+ "sha256": "d02e5de0cee389ee39c789ad6fe8859823944cf2c6af15f8d80249f3134f4653"
+ }
+ },
+ {
+ "version": "2.2.0",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0",
+ "download": {
+ "date": "2024-01-22T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-macOS-x64-signed-installer.pkg",
+ "downloadSize": "177373618",
+ "sha256": "ecef1ac2ae1fd3f2da40017f5da1fca6d966946a016ba80116ca960d88f04a53"
+ }
+ },
{
"version": "2.1.0",
"changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0",
{
"app_name": "JackTrip",
"releases": [
+ {
+ "version": "2.2.1",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.1",
+ "download": {
+ "date": "2024-01-30T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.1-Windows-x64-signed-installer.msi",
+ "downloadSize": "108511232",
+ "sha256": "193825d24745cd5a052ae57f1345b02924fc269aa69324428e7a177b9c58aa05"
+ }
+ },
+ {
+ "version": "2.2.0",
+ "changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.2.0",
+ "download": {
+ "date": "2024-01-22T00:00:00Z",
+ "url": "https://files.jacktrip.org/app-builds/JackTrip-v2.2.0-Windows-x64-signed-installer.msi",
+ "downloadSize": "108511232",
+ "sha256": "92aeb6ba74fcb5cade48962aa4696a77fb7b2434622c22097cfa9da037b32fb3"
+ }
+ },
{
"version": "2.1.0",
"changelog": "Full changelog at https://github.com/jacktrip/jacktrip/releases/tag/v2.1.0",
delete[] mAPInBuffer[i];
}
#endif // endwhere
- for (auto* i : qAsConst(mProcessPluginsFromNetwork)) {
+ for (auto* i : std::as_const(mProcessPluginsFromNetwork)) {
i->disconnect();
delete i;
}
- for (auto* i : qAsConst(mProcessPluginsToNetwork)) {
+ for (auto* i : std::as_const(mProcessPluginsToNetwork)) {
i->disconnect();
delete i;
}
- for (auto* i : qAsConst(mProcessPluginsToMonitor)) {
+ for (auto* i : std::as_const(mProcessPluginsToMonitor)) {
i->disconnect();
delete i;
}
#endif // not WAIR
// process incoming signal from audio interface using process plugins
- for (auto* p : qAsConst(mProcessPluginsToNetwork)) {
+ for (auto* p : std::as_const(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 : qAsConst(mProcessPluginsFromNetwork)) {
+ for (auto* p : std::as_const(mProcessPluginsFromNetwork)) {
if (p->getInited()) {
p->compute(n_frames, out_buffer.data(), out_buffer.data());
}
<< ") at sampling rate " << mSampleRate << "\n";
}
- for (ProcessPlugin* plugin : qAsConst(mProcessPluginsFromNetwork)) {
+ for (ProcessPlugin* plugin : std::as_const(mProcessPluginsFromNetwork)) {
plugin->setOutgoingToNetwork(false);
plugin->updateNumChannels(nChansIn, nChansOut);
plugin->init(mSampleRate, mBufferSizeInSamples);
}
- for (ProcessPlugin* plugin : qAsConst(mProcessPluginsToNetwork)) {
+ for (ProcessPlugin* plugin : std::as_const(mProcessPluginsToNetwork)) {
plugin->setOutgoingToNetwork(true);
plugin->updateNumChannels(nChansIn, nChansOut);
plugin->init(mSampleRate, mBufferSizeInSamples);
}
- for (ProcessPlugin* plugin : qAsConst(mProcessPluginsToMonitor)) {
+ for (ProcessPlugin* plugin : std::as_const(mProcessPluginsToMonitor)) {
plugin->setOutgoingToNetwork(false);
plugin->updateNumChannels(nChansMon, nChansMon);
plugin->init(mSampleRate, mBufferSizeInSamples);
#include <QVarLengthArray>
#include <QVector>
+#include <functional>
#include "AudioTester.h"
#include "ProcessPlugin.h"
// using namespace JackTripNamespace;
+// callback function for audio interface errors
+typedef std::function<void(const std::string& errorText)> AudioErrorCallback;
+
/** \brief Base Class that provides an interface with audio
*/
class AudioInterface
virtual void setLoopBack(bool b) { mLoopBack = b; }
virtual void enableBroadcastOutput() {}
virtual void setAudioTesterP(AudioTester* atp) { mAudioTesterP = atp; }
+ void setErrorCallback(AudioErrorCallback c) { mErrorCallback = c; }
//------------------------------------------------------------------
//--------------GETTERS---------------------------------------------
std::string mWarningHelpUrl;
std::string mErrorHelpUrl;
bool mHighLatencyFlag;
+ AudioErrorCallback mErrorCallback;
};
#endif // __AUDIOINTERFACE_H__
errorMsg += reason;
}
if (arg != nullptr) {
- static_cast<JackAudioInterface*>(arg)->mErrorMsg = errorMsg;
+ JackAudioInterface* ifPtr = static_cast<JackAudioInterface*>(arg);
+ ifPtr->mErrorMsg = errorMsg;
+ if (ifPtr->mErrorCallback) {
+ ifPtr->mErrorCallback(errorMsg);
+ }
}
std::cerr << errorMsg << std::endl;
JackTrip::sAudioStopped = true;
constexpr double AutoInitDur = 3000.0; // kick in auto after this many msec
constexpr double AutoInitValFactor =
0.5; // scale for initial mMsecTolerance during init phase if unspecified
-constexpr double MaxWaitTime = 30; // msec
// tweak
constexpr int WindowDivisor = 8; // for faster auto tracking
UNDERRUN : {
pullStat->plcUnderruns++; // count late
if ((mLastSeqNumOut == lastSeqNumIn)
- && ((now - mIncomingTiming[mLastSeqNumOut]) > MaxWaitTime)) {
+ && ((now - mIncomingTiming[mLastSeqNumOut]) > gUdpWaitTimeout)) {
goto ZERO_OUTPUT;
}
// "good underrun", not a stuck client
// discard measurements that exceed the max wait time
// this prevents temporary outages from skewing jitter metrics
- if (msElapsed > MaxWaitTime)
+ if (msElapsed > gUdpWaitTimeout)
return false;
if (ctr != window) {
errorMsg += errorText;
}
if (arg != nullptr) {
- static_cast<RtAudioInterface*>(arg)->mErrorMsg = errorMsg;
+ RtAudioInterface* ifPtr = static_cast<RtAudioInterface*>(arg);
+ ifPtr->mErrorMsg = errorText;
+ if (ifPtr->mErrorCallback) {
+ ifPtr->mErrorCallback(errorText);
+ }
}
std::cerr << errorMsg << std::endl;
JackTrip::sAudioStopped = true;
// This QT method gave me a lot of trouble, so I replaced it with my own 'waitForReady'
// that uses signals and slots and can also report with packets have not
// arrive for a longer time
- //timeout = UdpSocket.waitForReadyRead(30);
+ //timeout = UdpSocket.waitForReadyRead(gUdpWaitTimeout);
// timeout = cc unused!
#if defined (MANUAL_POLL)
waitForReady(60000); //60 seconds
// Send exit packet (with 1 redundant packet).
cout << "sending exit packet" << endl;
- QByteArray exitPacket = QByteArray(mControlPacketSize, 0xff);
+ QByteArray exitPacket = QByteArray(mControlPacketSize, static_cast<char>(0xff));
sendPacket(exitPacket.constData(), mControlPacketSize);
sendPacket(exitPacket.constData(), mControlPacketSize);
emit signalCeaseTransmission();
//*******************************************************************************
void UdpDataProtocol::printUdpWaitedTooLong(int wait_msec)
{
- int wait_time = 30; // msec
- if (!(wait_msec % wait_time)) {
- std::cerr << "UDP waiting too long (more than " << wait_time << "ms) for "
- << mPeerAddress.toString().toStdString() << "..." << endl;
+ if (!(wait_msec % gUdpWaitTimeout)) {
+ // only log error once per gap in audio, rather than every 30ms
+ if (wait_msec <= gUdpWaitTimeout) {
+ std::cerr << "UDP waiting too long (more than " << gUdpWaitTimeout << "ms) for "
+ << mPeerAddress.toString().toStdString() << "..." << endl;
+ }
emit signalUdpWaitingTooLong();
}
}
connected: false
studioId: modelData.id ? modelData.id : ""
inviteKeyString: modelData.inviteKey ? modelData.inviteKey : ""
+ sampleRate: modelData.sampleRate
}
section { property: "modelData.type"; criteria: ViewSection.FullString; delegate: SectionHeading {} }
color: backgroundColour
clip: true
+ property string statsOrange: "#b26a00"
property string connectionStateColor: getConnectionStateColor()
property variant networkStatsText: getNetworkStatsText()
texts[1] = "<b>" + minRtt + " ms - " + maxRtt + " ms</b>, avg " + avgRtt + " ms";
let quality = "Poor";
let color = meterRed;
- if (avgRtt <= 25) {
- if (maxRtt <= 30) {
- quality = "Excellent";
- color = meterGreen;
- } else {
- quality = "Good";
- color = meterGreen;
- }
- } else if (avgRtt <= 30) {
- quality = "Good";
+ if (avgRtt < 10 && maxRtt < 15) {
+ quality = "Excellent";
color = meterGreen;
- } else if (avgRtt <= 35) {
- quality = "Fair";
+ } else if (avgRtt < 20 && maxRtt < 30) {
+ quality = "Good";
color = meterYellow;
+ } else if (avgRtt < 30 && maxRtt < 40) {
+ quality = "Fair";
+ color = statsOrange;
}
texts[0] = quality
function onMicPermissionUpdated() {
if (permissions.micPermission === "granted") {
- if (virtualstudio.studioToJoin.toString() === "") {
+ if (virtualstudio.studioToJoin === "") {
virtualstudio.windowState = "browse";
- } else if (virtualstudio.showDeviceSetup) {
- virtualstudio.windowState = "setup";
- audio.startAudio();
} else {
- virtualstudio.windowState = "connected";
+ virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
virtualstudio.joinStudio();
}
}
virtualstudio.saveSettings();
if (permissions.micPermission !== "granted") {
virtualstudio.windowState = "permissions";
- } else if (virtualstudio.studioToJoin.toString() === "") {
+ } else if (virtualstudio.studioToJoin === "") {
virtualstudio.windowState = "browse";
- } else if (virtualstudio.showDeviceSetup) {
- virtualstudio.windowState = "setup";
- audio.startAudio();
} else {
- virtualstudio.windowState = "connected";
+ virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
virtualstudio.joinStudio();
}
}
virtualstudio.saveSettings();
if (permissions.micPermission !== "granted") {
virtualstudio.windowState = "permissions";
- } else if (virtualstudio.studioToJoin.toString() === "") {
+ } else if (virtualstudio.studioToJoin === "") {
virtualstudio.windowState = "browse";
- } else if (virtualstudio.showDeviceSetup) {
- virtualstudio.windowState = "setup";
- audio.startAudio();
} else {
- virtualstudio.windowState = "connected";
+ virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
virtualstudio.joinStudio();
}
}
}
enabled: !Boolean(audio.devicesError) && audio.backendAvailable && audio.audioReady
onClicked: {
- audio.stopAudio(true);
+ virtualstudio.studioToJoin = virtualstudio.currentStudio.id;
virtualstudio.windowState = "connected";
virtualstudio.saveSettings();
virtualstudio.joinStudio();
property string studioName: "Test Studio"
property string studioId: ""
property string inviteKeyString: ""
+ property int sampleRate: 48000
property bool publicStudio: false
property bool admin: false
property bool available: true
}
visible: !connected
onClicked: {
- virtualstudio.studioToJoin = `jacktrip://join/${studioId}`
- if (virtualstudio.showDeviceSetup) {
- virtualstudio.windowState = "setup";
- audio.startAudio();
- } else {
- virtualstudio.windowState = "connected";
- virtualstudio.joinStudio();
- }
+ virtualstudio.studioToJoin = studioId;
+ virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
+ virtualstudio.joinStudio();
}
Image {
id: join
this->done(0);
});
+ // Replace %VERSION% and %QTVERSION%
m_ui->aboutLabel->setText(
m_ui->aboutLabel->text().replace(QLatin1String("%VERSION%"), gVersion));
m_ui->aboutLabel->setText(
m_ui->aboutLabel->text().replace(QLatin1String("%QTVERSION%"), qVersion()));
+
+ // Replace %LICENSE%
+ QString licenseText;
+#if defined(_WIN32) && defined(RT_AUDIO)
+ licenseText = QLatin1String(
+ "This build of JackTrip includes support for ASIO. ASIO is a trademark and "
+ "software of Steinberg Media Technologies GmbH.</p><p></p><p>");
+#endif
#ifdef QT_OPENSOURCE
- m_ui->aboutLabel->setText(m_ui->aboutLabel->text().replace(
- QLatin1String("%LICENSE%"),
- QLatin1String("This build of JackTrip is subject to LGPL license. ")));
-#else
- m_ui->aboutLabel->setText(m_ui->aboutLabel->text().replace("%LICENSE%", ""));
+ licenseText += QLatin1String("This build of JackTrip is subject to LGPL license. ");
#endif
+ m_ui->aboutLabel->setText(
+ m_ui->aboutLabel->text().replace(QLatin1String("%LICENSE%"), licenseText));
+
+ // Replace %BUILD%
+ QString buildString;
if (!s_buildType.isEmpty() || !s_buildID.isEmpty()) {
- QString buildString = QStringLiteral("<br/>(");
+ buildString = QStringLiteral("<br/>(");
if (!s_buildType.isEmpty()) {
buildString.append(s_buildType);
if (!s_buildID.isEmpty()) {
buildString.append(QStringLiteral("Build %1").arg(s_buildID));
}
buildString.append(")");
- m_ui->aboutLabel->setText(
- m_ui->aboutLabel->text().replace(QLatin1String("%BUILD%"), buildString));
- } else {
- m_ui->aboutLabel->setText(m_ui->aboutLabel->text().replace(
- QLatin1String("%BUILD%"), QLatin1String("")));
}
+ m_ui->aboutLabel->setText(
+ m_ui->aboutLabel->text().replace(QLatin1String("%BUILD%"), buildString));
+
#ifdef __APPLE__
m_ui->aboutImage->setPixmap(QPixmap(":/qjacktrip/about@2x.png"));
#endif
emit testModeChanged();
}
-QUrl VirtualStudio::studioToJoin()
+QString VirtualStudio::studioToJoin()
{
return m_studioToJoin;
}
-void VirtualStudio::setStudioToJoin(const QUrl& url)
+void VirtualStudio::setStudioToJoin(const QString& id)
{
- if (m_studioToJoin == url)
+ if (m_studioToJoin == id)
return;
- m_studioToJoin = url;
+ m_studioToJoin = id;
emit studioToJoinChanged();
}
void VirtualStudio::joinStudio()
{
+ // nothing to do unless on setup or connected windows with studio to join
+ if ((m_windowState != "setup" && m_windowState != "connected")
+ || !m_auth->isAuthenticated() || m_studioToJoin.isEmpty())
+ return;
+
+ // make sure we've retrieved a list of servers
QMutexLocker locker(&m_refreshMutex);
- bool authenticated = m_auth->isAuthenticated();
- if (!authenticated || m_studioToJoin.isEmpty() || m_servers.isEmpty()) {
+ if (m_servers.isEmpty()) {
// No servers yet. Making sure we have them.
// getServerList emits refreshFinished which
// will come back to this function.
- if (authenticated && !m_studioToJoin.isEmpty() && m_servers.isEmpty()) {
- locker.unlock();
- getServerList(true);
- }
+ locker.unlock();
+ getServerList(true);
return;
}
- if (m_windowState != "connected") {
- return; // on audio setup screen before joining the studio
- }
- QString scheme = m_studioToJoin.scheme();
- QString path = m_studioToJoin.path();
- QString url = m_studioToJoin.toString();
- setStudioToJoin(QUrl(""));
+ // pop studioToJoin
+ const QString targetId = m_studioToJoin;
+ setStudioToJoin("");
+ emit studioToJoinChanged();
- m_failedMessage = "";
- if (scheme != "jacktrip" || path.length() <= 1) {
- m_failedMessage = "Invalid join request received: " + url;
- emit failedMessageChanged();
- emit failed();
- return;
- }
- QString targetId = path.remove(0, 1);
+ // stop audio if already running (settings or setup windows)
+ m_audioConfigPtr->stopAudio(true);
+ // find and populate data for current studio
VsServerInfoPointer sPtr;
for (const VsServerInfoPointer& s : m_servers) {
if (s->id() == targetId) {
}
locker.unlock();
- if (!sPtr.isNull()) {
- connectToStudio(*sPtr);
+ if (sPtr.isNull()) {
+ m_failedMessage = "Unable to find studio " + targetId;
+ emit failedMessageChanged();
+ emit failed();
return;
}
- m_failedMessage = "Unable to find studio " + targetId;
- emit failedMessageChanged();
- emit failed();
+ m_currentStudio = *sPtr;
+ emit currentStudioChanged();
+
+ if (m_windowState == "setup") {
+ m_audioConfigPtr->setSampleRate(m_currentStudio.sampleRate());
+ m_audioConfigPtr->startAudio();
+ return;
+ }
+
+ // m_windowState == "connected"
+ connectToStudio();
}
void VirtualStudio::toStandard()
m_audioConfigPtr->saveSettings();
}
-void VirtualStudio::connectToStudio(VsServerInfo& studio)
+void VirtualStudio::connectToStudio()
{
m_refreshTimer.stop();
m_networkStats = QJsonObject();
emit networkStatsChanged();
- m_currentStudio = studio;
- emit currentStudioChanged();
m_onConnectedScreen = true;
m_studioSocketPtr.reset(new VsWebSocket(
return;
}
jackTrip->setIOStatTimeout(m_cliSettings->getIOStatTimeout());
+ m_audioConfigPtr->setSampleRate(jackTrip->getSampleRate());
// this passes ownership to JackTrip
jackTrip->setAudioInterface(m_audioConfigPtr->newAudioInterface(jackTrip));
void VirtualStudio::handleDeeplinkRequest(const QUrl& link)
{
// check link is valid
- if (link.scheme() != QLatin1String("jacktrip")
- || link.host() != QLatin1String("join")) {
- qDebug() << "Ignoring invalid deeplink to " << link;
+ QString studioId;
+ if (link.scheme() != QLatin1String("jacktrip") || link.path().length() <= 1) {
+ qDebug() << "Ignoring invalid deeplink to" << link;
+ return;
+ }
+ if (link.host() == QLatin1String("join")) {
+ studioId = link.path().remove(0, 1);
+ } else if (link.host().isEmpty() && link.path().startsWith("join/")) {
+ studioId = link.path().remove(0, 5);
+ } else {
+ qDebug() << "Ignoring invalid deeplink to" << link;
return;
}
// check if already connected (ignore)
if (m_windowState == "connected" || m_windowState == "change_devices") {
- qDebug() << "Already connected; ignoring deeplink to " << link;
+ qDebug() << "Already connected; ignoring deeplink to" << link;
return;
}
- qDebug() << "Handling deeplink to " << link;
- setStudioToJoin(link);
+ if (m_windowState == "setup"
+ && (m_studioToJoin == studioId || m_currentStudio.id() == studioId)) {
+ qDebug() << "Already preparing to connect; ignoring deeplink to" << link;
+ return;
+ }
+
+ qDebug() << "Handling deeplink to" << link;
+ setStudioToJoin(studioId);
raiseToTop();
// Switch to virtual studio mode, if necessary
}
}
- // special case if on settings screen
- if (m_windowState == "settings") {
- if (showDeviceSetup()) {
- // audio is already active, so we can just flip screens
- setWindowState("setup");
- } else {
- // we need to stop audio before connecting
- setWindowState("connected");
- m_audioConfigPtr->stopAudio(true);
- joinStudio();
- }
- return;
- }
-
- // special case if on create_studio screen:
+ // automatically navigate if on certain window screens
// note that the studio creation happens inside of the web view,
// and the app doesn't really know anything about it. we depend
// on the web app triggering a deep link join event, which is
// noticed yet, so we don't join right away; otherwise we'd just
// get an unknown studio error. instead, we trigger a refresh and
// rely on it to kick off the join afterwards.
- if (m_windowState == "create_studio") {
- refreshStudios(0, true);
- if (showDeviceSetup()) {
- setWindowState("setup");
- m_audioConfigPtr->startAudio();
- } else {
- setWindowState("connected");
- }
- return;
- }
-
- // special case if on browsing and failed screens
- if (m_windowState == "browse" || m_windowState == "failed") {
+ if (m_windowState == "browse" || m_windowState == "create_studio"
+ || m_windowState == "settings" || m_windowState == "setup"
+ || m_windowState == "failed") {
if (showDeviceSetup()) {
setWindowState("setup");
- m_audioConfigPtr->startAudio();
} else {
setWindowState("connected");
- joinStudio();
}
- return;
+ refreshStudios(0, true);
}
// otherwise, assume we are on setup screens and can let the normal flow handle it
} else {
m_audioConfigPtr->validateDevices(true);
}
- connectToStudio(m_currentStudio);
+ connectToStudio();
}
return;
}
});
}
-void VirtualStudio::stopStudio()
-{
- if (m_currentStudio.id() == "") {
- return;
- }
-
- QJsonObject json = {{QLatin1String("enabled"), false}};
- QJsonDocument request = QJsonDocument(json);
- m_currentStudio.setHost(QLatin1String(""));
- QNetworkReply* reply = m_api->updateServer(m_currentStudio.id(), request.toJson());
- connect(reply, &QNetworkReply::finished, this, [=]() {
- if (m_isExiting && !m_jackTripRunning) {
- emit signalExit();
- }
- reply->deleteLater();
- });
-}
-
bool VirtualStudio::readyToJoin()
{
// FTUX shows warnings and device setup views
Q_PROPERTY(
QVector<VsServerInfo*> serverModel READ getServerModel NOTIFY serverModelChanged)
Q_PROPERTY(VsServerInfo* currentStudio READ currentStudio NOTIFY currentStudioChanged)
- Q_PROPERTY(QUrl studioToJoin READ studioToJoin WRITE setStudioToJoin NOTIFY
+ Q_PROPERTY(QString studioToJoin READ studioToJoin WRITE setStudioToJoin NOTIFY
studioToJoinChanged)
Q_PROPERTY(QJsonObject regions READ regions NOTIFY regionsChanged)
Q_PROPERTY(QJsonObject userMetadata READ userMetadata NOTIFY userMetadataChanged)
void setCollapseDeviceControls(bool collapseDeviceControls);
bool testMode();
void setTestMode(bool test);
- QUrl studioToJoin();
- void setStudioToJoin(const QUrl& url);
+ QString studioToJoin();
+ void setStudioToJoin(const QString& id);
bool showDeviceSetup();
void setShowDeviceSetup(bool show);
bool showWarnings();
void getSubscriptions();
void getRegions();
void getUserMetadata();
- void stopStudio();
bool readyToJoin();
- void connectToStudio(VsServerInfo& studio);
+ void connectToStudio();
void completeConnection();
private:
QTimer m_heartbeatTimer;
QTimer m_networkOutageTimer;
QMutex m_refreshMutex;
- QUrl m_studioToJoin;
+ QString m_studioToJoin;
QString m_updateChannel;
QString m_refreshToken;
QString m_userId;
}
if (virtualstudio.showWarnings) {
virtualstudio.windowState = "recommendations";
- } else if (virtualstudio.studioToJoin.toString() === "") {
+ } else if (virtualstudio.studioToJoin === "") {
virtualstudio.windowState = "browse";
- } else if (virtualstudio.showDeviceSetup) {
- virtualstudio.windowState = "setup";
- audio.startAudio();
} else {
- virtualstudio.windowState = "connected";
+ virtualstudio.windowState = virtualstudio.showDeviceSetup ? "setup" : "connected";
virtualstudio.joinStudio();
}
}
QJsonArray::fromStringList(QStringList(QLatin1String(""))))
, m_inputMixModeComboModel(QJsonArray::fromStringList(QStringList(QLatin1String(""))))
, m_audioWorkerPtr(new VsAudioWorker(this))
+ , m_workerThreadPtr(nullptr)
+ , m_inputMeterPluginPtr(nullptr)
+ , m_outputMeterPluginPtr(nullptr)
+ , m_inputVolumePluginPtr(nullptr)
+ , m_outputVolumePluginPtr(nullptr)
+ , m_monitorPluginPtr(nullptr)
+ , mHasErrors(false)
{
loadSettings();
emit feedbackDetectionEnabledChanged();
}
+void VsAudio::setSampleRate(int sampleRate)
+{
+ if (m_audioSampleRate == sampleRate)
+ return;
+ m_audioSampleRate = sampleRate;
+ emit sampleRateChanged();
+}
+
void VsAudio::setBufferSize(int bufSize)
{
if (m_audioBufferSize == bufSize)
if (ifPtr == nullptr)
return ifPtr;
+ mHasErrors = false;
+ ifPtr->setErrorCallback([this, jackTripPtr](const std::string& errorText) {
+ this->errorCallback(errorText, jackTripPtr);
+ });
+
// AudioInterface::setup() can return a different buffer size
// if the audio interface doesn't support the one that was requested
if (ifPtr->getBufferSizeInSamples() != uint32_t(getBufferSize())) {
jackTripPtr != nullptr, jackTripPtr);
ifPtr->setClientName(QStringLiteral("JackTrip"));
#if defined(__unix__)
- AudioInterface::setPipewireLatency(
- getBufferSize(),
- jackTripPtr == nullptr ? 48000 : jackTripPtr->getSampleRate());
+ AudioInterface::setPipewireLatency(getBufferSize(), getSampleRate());
#endif
ifPtr->setup(true);
}
inputChans, outputChans,
static_cast<AudioInterface::inputMixModeT>(getInputMixMode()),
m_audioBitResolution, jackTripPtr != nullptr, jackTripPtr);
- ifPtr->setSampleRate(jackTripPtr == nullptr ? 48000 : jackTripPtr->getSampleRate());
+ ifPtr->setSampleRate(getSampleRate());
ifPtr->setInputDevice(getInputDevice().toStdString());
ifPtr->setOutputDevice(getOutputDevice().toStdString());
ifPtr->setBufferSizeInSamples(getBufferSize());
return ifPtr;
}
+void VsAudio::errorCallback(const std::string& errorText,
+ [[maybe_unused]] JackTrip* jackTripPtr)
+{
+ const QString errorMsg(QString::fromStdString(errorText));
+ setDevicesErrorMsg(errorMsg);
+#ifdef _WIN32
+ // handle special case for Windows ASIO drivers that trigger
+ // asynchronous errors shortly after you try opening the
+ // RtAudio stream with a different sample rate (only for audio tester)
+ if (jackTripPtr == nullptr && getUseRtAudio()
+ && errorMsg.contains("sample rate changed")) {
+ // only refresh devices once
+ if (mHasErrors)
+ return;
+ mHasErrors = true;
+ // asynchronously refresh devices
+ refreshDevices(false);
+ }
+#else
+ mHasErrors = true;
+#endif
+}
+
// VsAudioWorker methods
VsAudioWorker::VsAudioWorker(VsAudio* ptr) : m_parentPtr(ptr) {}
Q_PROPERTY(bool backendAvailable READ backendAvailable CONSTANT)
Q_PROPERTY(QString audioBackend READ getAudioBackend WRITE setAudioBackend NOTIFY
audioBackendChanged)
+ Q_PROPERTY(
+ int sampleRate READ getSampleRate WRITE setSampleRate NOTIFY sampleRateChanged)
Q_PROPERTY(
int bufferSize READ getBufferSize WRITE setBufferSize NOTIFY bufferSizeChanged)
Q_PROPERTY(int bufferStrategy READ getBufferStrategy WRITE setBufferStrategy NOTIFY
{
return getUseRtAudio() ? QStringLiteral("RtAudio") : QStringLiteral("JACK");
}
+ int getSampleRate() const { return m_audioSampleRate; }
int getBufferSize() const { return m_audioBufferSize; }
int getBufferStrategy() const { return m_bufferStrategy; }
int getNumInputChannels() const { return getUseRtAudio() ? m_numInputChannels : 2; }
// setters for state shared with QML
void setFeedbackDetectionEnabled(bool enabled);
void setAudioBackend(const QString& backend);
+ void setSampleRate(int sampleRate);
void setBufferSize(int bufSize);
void setBufferStrategy(int bufStrategy);
void setNumInputChannels(int numChannels);
void signalScanningDevicesChanged();
void deviceModelsInitializedChanged(bool initialized);
void audioBackendChanged(bool useRtAudio);
+ void sampleRateChanged();
void bufferSizeChanged();
void bufferStrategyChanged();
void numInputChannelsChanged(int numChannels);
void updateDeviceMessages(AudioInterface& audioInterface);
AudioInterface* newJackAudioInterface(JackTrip* jackTripPtr = nullptr);
AudioInterface* newRtAudioInterface(JackTrip* jackTripPtr = nullptr);
+ void errorCallback(const std::string& errorText, JackTrip* jackTripPtr = nullptr);
// range for volume meters
static constexpr float m_meterMax = 0.0;
bool m_scanningDevices = false;
bool m_feedbackDetectionEnabled = true;
bool m_deviceModelsInitialized = false;
+ int m_audioSampleRate = gDefaultSampleRate;
int m_audioBufferSize =
gDefaultBufferSizeInSamples; ///< Audio buffer size to process on each callback
int m_bufferStrategy = 0;
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;
int getNumOutputChannels() const { return m_parentPtr->getNumOutputChannels(); }
int getBaseInputChannel() const { return m_parentPtr->getBaseInputChannel(); }
int getBaseOutputChannel() const { return m_parentPtr->getBaseOutputChannel(); }
+ int getSampleRate() const { return m_parentPtr->getSampleRate(); }
int getBufferSize() const { return m_parentPtr->getBufferSize(); }
int getInputMixMode() const { return m_parentPtr->getInputMixMode(); }
const QString& getInputDevice() const { return m_parentPtr->getInputDevice(); }
m_readyToExit = true;
}
- m_instanceCheckSocket->flush();
+ m_instanceCheckSocket->waitForBytesWritten();
m_instanceCheckSocket->disconnectFromServer(); // remove next
// let main thread know we are finished
// Receive URL from 2nd instance
QLocalSocket* connectedSocket = m_instanceServer->nextPendingConnection();
- if (!connectedSocket->waitForConnected()) {
- qDebug() << "Never received connection";
+ if (connectedSocket == nullptr || !connectedSocket->waitForConnected()) {
+ qDebug() << "Deeplink socket: never received connection";
return;
}
- if (!connectedSocket->waitForReadyRead()) {
- qDebug() << "Never ready to read";
- if (!(connectedSocket->bytesAvailable() > 0)) {
- qDebug() << "Not ready and no bytes available";
- return;
- }
+ if (!connectedSocket->waitForReadyRead()
+ && connectedSocket->bytesAvailable() <= 0) {
+ qDebug() << "Deeplink socket: not ready and no bytes available: "
+ << connectedSocket->errorString();
+ return;
}
if (connectedSocket->bytesAvailable() < (int)sizeof(quint16)) {
- qDebug() << "no bytes available";
+ qDebug() << "Deeplink socket: ready but no bytes available";
break;
}
#include "AudioInterface.h"
-constexpr const char* const gVersion = "2.2.1"; ///< JackTrip version
+constexpr const char* const gVersion = "2.2.2"; ///< JackTrip version
//*******************************************************************************
/// \name Default Values
constexpr int gDefaultRedundancy = 1;
constexpr int gTimeOutMultiThreadedServer = 10000; // seconds
constexpr int gWaitCounter = 60;
+constexpr int gUdpWaitTimeout = 30; // milliseconds
//@}
//*******************************************************************************