diff --git a/.github/workflows/master_build.yml b/.github/workflows/master_build.yml
index 7db5823c75f..d30c19dab06 100644
--- a/.github/workflows/master_build.yml
+++ b/.github/workflows/master_build.yml
@@ -124,7 +124,8 @@ jobs:
sudo apt install -y ${{ matrix.apt-dependencies }} || exit 1
else # macOS
echo "Downloading MacOSX10.12 SDK.."
- curl --progress-bar -L -o macOS_SDK10.12.4.tar.xz "https://cloudflare-ipfs.com/ipfs/QmTDLEYdo616YgZKSYC2mySSw8EKQWTz7CH8UyMupMBpzK?filename=macOS_SDK10.12.4.tar.xz" || exit 1
+ # TODO: do this in cmake or python build scripts to make use of centralized external asset configuration with fallback gateways.
+ curl --progress-bar -L -o macOS_SDK10.12.4.tar.xz "https://ipfs.vircadia.dev/ipfs/QmTDLEYdo616YgZKSYC2mySSw8EKQWTz7CH8UyMupMBpzK?filename=macOS_SDK10.12.4.tar.xz" || exit 1
echo " done\n"
echo "Checking MacOSX10.12 SDK.."
echo "c00e03cb64bed8d0d75d7a01c93de094b6ffaa3bc3318e7b29a9bdc354f0e29e363c454b7db89db8a3b78e9702668d1c661f1af279807de8db11accecb0ca498 macOS_SDK10.12.4.tar.xz" >> macOS_SDK10.12.4.tar.xz.checksum
diff --git a/.github/workflows/pr_build.yml b/.github/workflows/pr_build.yml
index 06dc710e693..38f88053e05 100644
--- a/.github/workflows/pr_build.yml
+++ b/.github/workflows/pr_build.yml
@@ -158,7 +158,8 @@ jobs:
sudo apt install -y ${{ matrix.apt-dependencies }} || exit 1
else # macOS
echo "Downloading MacOSX10.12 SDK.."
- curl --progress-bar -L -o macOS_SDK10.12.4.tar.xz "https://cloudflare-ipfs.com/ipfs/QmTDLEYdo616YgZKSYC2mySSw8EKQWTz7CH8UyMupMBpzK?filename=macOS_SDK10.12.4.tar.xz" || exit 1
+ # TODO: do this in cmake or python build scripts to make use of centralized external asset configuration with fallback gateways.
+ curl --progress-bar -L -o macOS_SDK10.12.4.tar.xz "https://ipfs.vircadia.dev/ipfs/QmTDLEYdo616YgZKSYC2mySSw8EKQWTz7CH8UyMupMBpzK?filename=macOS_SDK10.12.4.tar.xz" || exit 1
echo " done\n"
echo "Checking MacOSX10.12 SDK.."
echo "c00e03cb64bed8d0d75d7a01c93de094b6ffaa3bc3318e7b29a9bdc354f0e29e363c454b7db89db8a3b78e9702668d1c661f1af279807de8db11accecb0ca498 macOS_SDK10.12.4.tar.xz" >> macOS_SDK10.12.4.tar.xz.checksum
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e923a893167..84d09cfed04 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -227,11 +227,19 @@ set(BUILD_INSTALLER_OPTION ON)
set(DISABLE_QML_OPTION OFF)
set(DOWNLOAD_SERVERLESS_CONTENT_OPTION OFF)
set(ENABLE_WEBRTC_DATA_CHANNELS_OPTION OFF)
+set(ENABLE_WEBRTC_AUDIO_OPTION OFF)
if (WIN32 OR (UNIX AND NOT APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64"))
set(ENABLE_WEBRTC_DATA_CHANNELS_OPTION ON)
endif()
+if (WIN32 OR (UNIX AND NOT ANDROID AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64"))
+ # We don't yet have a working libwebrtc for android.
+ # WebRTC is difficult to build on aarch64 Linux.
+ # an alternative is https://gitlab.freedesktop.org/pulseaudio/webrtc-audio-processing.
+ set(ENABLE_WEBRTC_AUDIO_OPTION ON)
+endif()
+
if (ANDROID OR UWP)
set(BUILD_SERVER_OPTION OFF)
set(BUILD_TOOLS_OPTION OFF)
@@ -304,6 +312,7 @@ option(
${DOWNLOAD_SERVERLESS_CONTENT_OPTION}
)
option(ENABLE_WEBRTC_DATA_CHANNELS "Use WebRTC for client-server connection in parallel with UDP" ${ENABLE_WEBRTC_DATA_CHANNELS_OPTION})
+option(ENABLE_WEBRTC_AUDIO "Use WebRTC for audio processing (e.g. echo cancelation)." ${ENABLE_WEBRTC_AUDIO_OPTION})
set(PLATFORM_QT_GL OpenGL)
@@ -311,6 +320,10 @@ if (ENABLE_WEBRTC_DATA_CHANNELS)
add_compile_definitions(WEBRTC_DATA_CHANNELS=1)
endif()
+if (ENABLE_WEBRTC_AUDIO)
+ add_compile_definitions(WEBRTC_AUDIO=1)
+endif()
+
if (USE_KHR_ROBUSTNESS)
add_definitions(-DUSE_KHR_ROBUSTNESS)
endif()
@@ -338,6 +351,7 @@ MESSAGE(STATUS "Build installer: " ${BUILD_INSTALLER})
MESSAGE(STATUS "GL ES: " ${USE_GLES})
MESSAGE(STATUS "DL serverless content: " ${DOWNLOAD_SERVERLESS_CONTENT})
MESSAGE(STATUS "WebRTC Data Channels: " ${ENABLE_WEBRTC_DATA_CHANNELS})
+MESSAGE(STATUS "WebRTC Audio: " ${ENABLE_WEBRTC_AUDIO})
MESSAGE(STATUS "Static standard libraries: " ${STATIC_STDLIB})
MESSAGE(STATUS "Shared internal libraries: " ${BUILD_SHARED_LIBS})
diff --git a/INSTALLER.md b/INSTALLER.md
index d6c8e0253d6..b53e00ef577 100644
--- a/INSTALLER.md
+++ b/INSTALLER.md
@@ -32,22 +32,24 @@ To produce an executable installer on Windows, the following are required:
1. Extract Zip
1. Copy `Include\nsProcess.nsh` to `C:\Program Files (x86)\NSIS\Include\`
1. Copy `Plugins\nsProcess.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\`
- 1. Copy `Plugins\nsProcessW.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\`
+ 1. Copy `Plugins\nsProcessW.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\` and rename the filename to `nsProcess.dll` (remove the 'W' character)
1. [InetC Plug-in for Nullsoft](http://nsis.sourceforge.net/Inetc_plug-in) - 1.0
1. Extract Zip
1. Copy `Plugin\x86-ansi\InetC.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\`
1. Copy `Plugin\x86-unicode\InetC.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\`
-1. [NSISpcre Plug-in for Nullsoft](http://nsis.sourceforge.net/NSISpcre_plug-in) - 1.0
+1. [NSISpcre Plug-in for Nullsoft](https://github.com/mazinsw/NSISpcre/archive/refs/heads/master.zip) - 1.0
1. Extract Zip
- 1. Copy `NSISpre.nsh` to `C:\Program Files (x86)\NSIS\Include\`
- 1. Copy `NSISpre.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\`
+ 1. Copy `include\NSISpre.nsh` to `C:\Program Files (x86)\NSIS\Include\`
+ 1. Copy `bin\x86-ansi\NSISpre.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\`
+ 1. Copy `bin\x86-unicode\NSISpre.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\`
+
1. [nsisSlideshow Plug-in for Nullsoft]() - 1.7
1. Extract Zip
1. Copy `bin\nsisSlideshow.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-ansi\`
- 1. Copy `bin\nsisSlideshowW.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\`
+ 1. Copy `bin\nsisSlideshowW.dll` to `C:\Program Files (x86)\NSIS\Plugins\x86-unicode\` and rename the filename to `nsisSlideshow.dll` (remove the 'W' character)
1. [Nsisunz plug-in for Nullsoft](http://nsis.sourceforge.net/Nsisunz_plug-in)
1. Download both Zips and unzip
diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp
index 4b9b2d5095f..cd4c28cf0f9 100644
--- a/assignment-client/src/Agent.cpp
+++ b/assignment-client/src/Agent.cpp
@@ -475,11 +475,6 @@ void Agent::executeScript() {
packetType = PacketType::SilentAudioFrame;
}
- Transform audioTransform;
- auto headOrientation = scriptedAvatar->getHeadOrientation();
- audioTransform.setTranslation(scriptedAvatar->getWorldPosition());
- audioTransform.setRotation(headOrientation);
-
QByteArray encodedBuffer;
if (_encoder) {
_encoder->encode(audio, encodedBuffer);
@@ -488,7 +483,7 @@ void Agent::executeScript() {
}
AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), audioSequenceNumber, false,
- audioTransform, scriptedAvatar->getWorldPosition(), glm::vec3(0),
+ {scriptedAvatar->getWorldPosition(), scriptedAvatar->getHeadOrientation()}, scriptedAvatar->getWorldPosition(), glm::vec3(0),
packetType, _selectedCodecName);
});
@@ -719,7 +714,7 @@ void Agent::processAgentAvatarAudio() {
if (isPlayingRecording && !_shouldMuteRecordingAudio) {
_shouldMuteRecordingAudio = true;
}
-
+
auto audioData = _avatarSound->getAudioData();
nextSoundOutput = reinterpret_cast(audioData->rawData()
+ _numAvatarSoundSentBytes);
@@ -880,7 +875,7 @@ void Agent::aboutToFinish() {
{
DependencyManager::get()->shutdownScripting();
}
-
+
DependencyManager::destroy();
DependencyManager::destroy();
diff --git a/assignment-client/src/audio/AudioMixerSlave.cpp b/assignment-client/src/audio/AudioMixerSlave.cpp
index 204095ab940..76020aa4ef7 100644
--- a/assignment-client/src/audio/AudioMixerSlave.cpp
+++ b/assignment-client/src/audio/AudioMixerSlave.cpp
@@ -279,7 +279,7 @@ bool shouldBeSkipped(MixableStream& stream, const Node& listener,
bool shouldCheckIgnoreBox = (listenerAudioStream.isIgnoreBoxEnabled() ||
stream.positionalStream->isIgnoreBoxEnabled());
if (shouldCheckIgnoreBox &&
- listenerAudioStream.getIgnoreBox().touches(stream.positionalStream->getIgnoreBox())) {
+ isTouching(listenerAudioStream.getIgnoreBox(), stream.positionalStream->getIgnoreBox())) {
return true;
}
@@ -587,7 +587,7 @@ void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream&
glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition();
float distance = glm::max(glm::length(relativePosition), EPSILON);
- float gain = isEcho ? 1.0f : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd,
+ float gain = isEcho ? 1.0f : computeGain(masterAvatarGain, masterInjectorGain, listeningNodeStream, *streamToAdd,
relativePosition, distance);
float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition);
@@ -815,14 +815,14 @@ float computeAzimuth(const AvatarAudioStream& listeningNodeStream,
float rotatedSourcePositionLength2 = glm::length2(rotatedSourcePosition);
if (rotatedSourcePositionLength2 > SOURCE_DISTANCE_THRESHOLD) {
-
+
// produce an oriented angle about the y-axis
glm::vec3 direction = rotatedSourcePosition * (1.0f / fastSqrtf(rotatedSourcePositionLength2));
float angle = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward"
return (direction.x < 0.0f) ? -angle : angle;
- } else {
+ } else {
// no azimuth if they are in same spot
- return 0.0f;
+ return 0.0f;
}
}
diff --git a/cmake/ExternalAssetsConfig.cmake b/cmake/ExternalAssetsConfig.cmake
index bda442311bd..19dc9f5a555 100644
--- a/cmake/ExternalAssetsConfig.cmake
+++ b/cmake/ExternalAssetsConfig.cmake
@@ -3,19 +3,21 @@
include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/macros/ExternalAssetsUtils.cmake")
set(USE_IPFS_EXTERNAL_BUILD_ASSETS true CACHE STRING "Enable IPFS gateway for external assets (default: true)")
-set(USE_DIRECT_EXTERNAL_BUILD_ASSETS true CACHE STRING "Enable direct links for external assets (default: true)")
+set(USE_DIRECT_EXTERNAL_BUILD_ASSETS false CACHE STRING "Enable direct links for external assets (default: false)")
set(EXTERNAL_BUILD_ASSETS_DIRECT_BASE_URLS
"https://athena-public.s3.amazonaws.com"
"http://motofckr9k.ddns.net"
"https://data.moto9000.moe"
+ "https://cdn-1.vircadia.com/eu-c-1/vircadia-public/dependencies/android"
+ "https://cdn-1.vircadia.com/eu-c-1/vircadia-public/dependencies/"
CACHE
STRING "Direct base urls for external assets"
)
set(EXTERNAL_BUILD_ASSETS_IPFS_BASE_URLS
"http://localhost:8080/ipfs"
- "http://ipfs.vircadia.dev/ipfs"
+ "https://ipfs.vircadia.dev/ipfs"
"https://cf-ipfs.com/ipfs"
"https://gateway.ipfs.io/ipfs"
"https://cloudflare-ipfs.com/ipfs"
@@ -301,3 +303,108 @@ set(EXTERNAL_QT_DEBIAN_AARCH64_SHA512 "c18f8a6ff2ca091fb14947e437c3179368d85c1da
set(EXTERNAL_QT_DEBIAN_AARCH64_MD5 "097069159999c32b6cf626fe05f22e7a")
define_external_asset_urls(QT_DEBIAN_AARCH64)
+set(EXTERNAL_QT_LINUX_ARMV8_LIBCPP_OPENSSL_PATCHED_PATH "qt-5.11.1_linux_armv8-libcpp_openssl_patched.tgz")
+set(EXTERNAL_QT_LINUX_ARMV8_LIBCPP_OPENSSL_PATCHED_IPFS_CID "Qma2EUEymBG8KB1DGxFaD8jZZD8VC4CtpVzbSsg3CQBKYT")
+set(EXTERNAL_QT_LINUX_ARMV8_LIBCPP_OPENSSL_PATCHED_MD5 "aa449d4bfa963f3bc9a9dfe558ba29df")
+define_external_asset_urls(QT_LINUX_ARMV8_LIBCPP_OPENSSL_PATCHED)
+
+set(EXTERNAL_QT_WIN_ARMV8_LIBCPP_OPENSSL_PATCHED_PATH "qt-5.11.1_win_armv8-libcpp_openssl_patched.tgz")
+set(EXTERNAL_QT_WIN_ARMV8_LIBCPP_OPENSSL_PATCHED_IPFS_CID "QmPXk7uJnY7tFH1gBFPiFmnWQmvMq57Ltpg9PcbfhSk77K")
+set(EXTERNAL_QT_WIN_ARMV8_LIBCPP_OPENSSL_PATCHED_MD5 "0582191cc55431aa4f660848a542883e")
+define_external_asset_urls(QT_WIN_ARMV8_LIBCPP_OPENSSL_PATCHED)
+
+set(EXTERNAL_QT_MAC_ARMV8_LIBCPP_OPENSSL_PATCHED_PATH "qt-5.11.1_osx_armv8-libcpp_openssl_patched.tgz")
+set(EXTERNAL_QT_MAC_ARMV8_LIBCPP_OPENSSL_PATCHED_IPFS_CID "QmesL4YUQi1hw3LYx8FWKm43RJ9K9gmC7wanYaw9rby245")
+set(EXTERNAL_QT_MAC_ARMV8_LIBCPP_OPENSSL_PATCHED_MD5 "c83cc477c08a892e00c71764dca051a0")
+define_external_asset_urls(QT_MAC_ARMV8_LIBCPP_OPENSSL_PATCHED)
+
+set(EXTERNAL_BULLET_ARMV8_LIBCPP_PATH "bullet-2.88_armv8-libcpp.tgz")
+set(EXTERNAL_BULLET_ARMV8_LIBCPP_IPFS_CID "QmdRdQWXS4oxg5cS7x1BMnovfivJEkHEkrZ26Zuy7Zxy2m")
+set(EXTERNAL_BULLET_ARMV8_LIBCPP_MD5 "81642779ccb110f8c7338e8739ac38a0")
+define_external_asset_urls(BULLET_ARMV8_LIBCPP)
+
+set(EXTERNAL_DRACO_ARMV8_LIBCPP_PATH "draco_armv8-libcpp.tgz")
+set(EXTERNAL_DRACO_ARMV8_LIBCPP_IPFS_CID "QmQuA9gUT7DUxPPjEb95FkvkYZ3B4by5r9H3uGv8iU8ig6")
+set(EXTERNAL_DRACO_ARMV8_LIBCPP_MD5 "617a80d213a5ec69fbfa21a1f2f738cd")
+define_external_asset_urls(DRACO_ARMV8_LIBCPP)
+
+set(EXTERNAL_GLAD_ARMV8_LIBCPP_PATH "glad_armv8-libcpp.zip")
+set(EXTERNAL_GLAD_ARMV8_LIBCPP_IPFS_CID "QmVpvmDg9MEPudkfKTDJfPZg4ckq1jcUQzhZ1YyBiNs2TR")
+set(EXTERNAL_GLAD_ARMV8_LIBCPP_MD5 "a8ee8584cf1ccd34766c7ddd9d5e5449")
+define_external_asset_urls(GLAD_ARMV8_LIBCPP)
+
+set(EXTERNAL_GVRSDK_PATH "gvrsdk_v1.101.0.tgz")
+set(EXTERNAL_GVRSDK_IPFS_CID "QmbPZqU7dKoQEARUbF6ztcSyPWN5LW6VPzfATjhL6x2aN3")
+set(EXTERNAL_GVRSDK_MD5 "57fd02baa069176ba18597a29b6b4fc7")
+define_external_asset_urls(GVRSDK)
+
+set(EXTERNAL_NVTT_ARMV8_LIBCPP_PATH "nvtt_armv8-libcpp.zip")
+set(EXTERNAL_NVTT_ARMV8_LIBCPP_IPFS_CID "QmcxwNX5HPVqJ9Xbqyk7vWe9i2skm2kPp7gd9MMHwJ7y8T")
+set(EXTERNAL_NVTT_ARMV8_LIBCPP_MD5 "eb46d0b683e66987190ed124aabf8910")
+define_external_asset_urls(NVTT_ARMV8_LIBCPP)
+
+set(EXTERNAL_OVR_SDK_MOBILE_PATH "ovr_sdk_mobile_1.37.0.zip")
+set(EXTERNAL_OVR_SDK_MOBILE_IPFS_CID "Qmek4kWXEMiGx7vyn5ntLAKtB7uVaAaGUbYcRown1b9KLj")
+set(EXTERNAL_OVR_SDK_MOBILE_MD5 "6040e1966f335a3e5015295154cd7383")
+define_external_asset_urls(OVR_SDK_MOBILE)
+
+set(EXTERNAL_OVR_PLATFORM_SDK_ANDROID_PATH "ovr_platform_sdk_23.0.0.zip")
+set(EXTERNAL_OVR_PLATFORM_SDK_ANDROID_IPFS_CID "QmW2CamwqPd3mAz5KtCsbnoFz17pJw8vCyvLXmRa7m7WZ4")
+set(EXTERNAL_OVR_PLATFORM_SDK_ANDROID_MD5 "29d02b560f60d0fa7b8a64cd965dd55b")
+define_external_asset_urls(OVR_PLATFORM_SDK_ANDROID)
+
+set(EXTERNAL_POLYVOX_ARMV8_LIBCPP_PATH "polyvox_armv8-libcpp.tgz")
+set(EXTERNAL_POLYVOX_ARMV8_LIBCPP_IPFS_CID "QmXHgaQ3wo3GHSGbxPQo26twARQUtE9uQpDJqW5HxsBm9A")
+set(EXTERNAL_POLYVOX_ARMV8_LIBCPP_MD5 "dba88b3a098747af4bb169e9eb9af57e")
+define_external_asset_urls(POLYVOX_ARMV8_LIBCPP)
+
+set(EXTERNAL_TBB_ARMV8_LIBCPP_PATH "tbb-2018_U1_armv8_libcpp.tgz")
+set(EXTERNAL_TBB_ARMV8_LIBCPP_IPFS_CID "QmWB2BAitm8N2iECVYg56LZAVs8SqvGRzDkrdY4wGxxk13")
+set(EXTERNAL_TBB_ARMV8_LIBCPP_MD5 "20768f298f53b195e71b414b0ae240c4")
+define_external_asset_urls(TBB_ARMV8_LIBCPP)
+
+set(EXTERNAL_CODECSDK_ANDROID_ARMV8_PATH "codecSDK-android_armv8-2.0.zip")
+set(EXTERNAL_CODECSDK_ANDROID_ARMV8_IPFS_CID "QmWP6AZizmuDkMboFW78qxAWEhQZYriXP5tg28iCZkFCxL")
+set(EXTERNAL_CODECSDK_ANDROID_ARMV8_MD5 "1cbef929675818fc64c4101b72f84a6a")
+define_external_asset_urls(CODECSDK_ANDROID_ARMV8)
+
+set(EXTERNAL_ETC2COMP_PATCHED_ARMV8_LIBCPP_PATH "etc2comp-patched-armv8-libcpp.tgz")
+set(EXTERNAL_ETC2COMP_PATCHED_ARMV8_LIBCPP_IPFS_CID "QmZSCzQsTs7EsdoskNZDz9coywhzbWV1d8PP192rKCBVte")
+set(EXTERNAL_ETC2COMP_PATCHED_ARMV8_LIBCPP_MD5 "14b02795d774457a33bbc60e00a786bc")
+define_external_asset_urls(ETC2COMP_PATCHED_ARMV8_LIBCPP)
+
+set(EXTERNAL_BREAKPAD_PATH "breakpad.tgz")
+set(EXTERNAL_BREAKPAD_IPFS_CID "QmSTohJ6X1L5MNyPs98izFRKzm1tt3sLPytoJVNo9ehz6B")
+set(EXTERNAL_BREAKPAD_MD5 "ddcb23df336b08017042ba4786db1d9e")
+define_external_asset_urls(BREAKPAD)
+
+set(EXTERNAL_WEBRTC_ANDROID_PATH "webrtc-20190626-android.tar.gz")
+set(EXTERNAL_WEBRTC_ANDROID_IPFS_CID "QmcwRmgYmfyFyQ2Y8bhAw8aH5BQ63R9HBrNiRAUVWroEoE")
+set(EXTERNAL_WEBRTC_ANDROID_MD5 "e2dccd3d8efdcba6d428c87ba7fb2a53")
+define_external_asset_urls(WEBRTC_ANDROID)
+
+set(EXTERNAL_NLOHMANN_JSON_JSONHPP_PATH "nlohmann-json-v3.3.0/json.hpp")
+set(EXTERNAL_NLOHMANN_JSON_JSONHPP_IPFS_CID "QmRe7unCwvNjzmt7CExnphS13Ltnk8WBSmNqHmS3VTmBFs")
+set(EXTERNAL_NLOHMANN_JSON_JSONHPP_SHA512 "c4e4bb84d1488f87a02c4e12409491225e345cc508e6dbbee1a3542fbd4953052c256d0fe78c4d3ce02d44c3a2155fe66f0c8a93a3851ddf94fec4f9f3fd6918")
+define_external_asset_urls(NLOHMANN_JSON_JSONHPP)
+
+set(EXTERNAL_NLOHMANN_JSON_LICENSE_PATH "nlohmann-json-v3.3.0/LICENSE.MIT")
+set(EXTERNAL_NLOHMANN_JSON_LICENSE_IPFS_CID "QmRSAu3zMMfoZAdpU8nhN5hnJMHFBsP5X3vxGGgTwUfiXD")
+set(EXTERNAL_NLOHMANN_JSON_LICENSE_SHA512 "0fdb404547467f4523579acde53066badf458504d33edbb6e39df0ae145ed27d48a720189a60c225c0aab05f2aa4ce4050dcb241b56dc693f7ee9f54c8728a75")
+define_external_asset_urls(NLOHMANN_JSON_LICENSE)
+
+set(EXTERNAL_OPENSSL_102S_PATH "openssl-1.0.2s.tar.gz")
+set(EXTERNAL_OPENSSL_102S_IPFS_CID "QmXRKtBhXJbYJm2FhrVobYo7sboU5V5C8xXMc3ryNs6z8F")
+set(EXTERNAL_OPENSSL_102S_SHA512 "9f745452c4f777df694158e95003cde78a2cf8199bc481a563ec36644664c3c1415a774779b9791dd18f2aeb57fa1721cb52b3db12d025955e970071d5b66d2a")
+define_external_asset_urls(OPENSSL_102S)
+
+set(EXTERNAL_OPENSSL_111H_PATH "openssl-1.1.1h.tar.gz")
+set(EXTERNAL_OPENSSL_111H_IPFS_CID "QmYks763hXmjvMooXx3y2gi75RVGB3KbbcCFqPmouu9gWk")
+set(EXTERNAL_OPENSSL_111H_SHA512 "da50fd99325841ed7a4367d9251c771ce505a443a73b327d8a46b2c6a7d2ea99e43551a164efc86f8743b22c2bdb0020bf24a9cbd445e9d68868b2dc1d34033a")
+define_external_asset_urls(OPENSSL_111H)
+
+set(EXTERNAL_ZLIB_PATH "zlib-1.2.11.tar.gz")
+set(EXTERNAL_ZLIB_IPFS_CID "QmVUxkKQnLaiWK7nSwMFQ97LYSeGuwtRFjhw9ooMpJd5Gr")
+set(EXTERNAL_ZLIB_SHA512 "73fd3fff4adeccd4894084c15ddac89890cd10ef105dd5e1835e1e9bbb6a49ff229713bd197d203edfa17c2727700fce65a2a235f07568212d820dca88b528ae")
+define_external_asset_urls(ZLIB)
+
diff --git a/cmake/macros/ExternalAssetsUtils.cmake b/cmake/macros/ExternalAssetsUtils.cmake
index a44f640b2e5..c9640fbb282 100644
--- a/cmake/macros/ExternalAssetsUtils.cmake
+++ b/cmake/macros/ExternalAssetsUtils.cmake
@@ -20,6 +20,7 @@ macro(define_external_asset_urls ASSETNAME)
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/_env/EXTERNAL_${ASSETNAME}_URLS.txt" "${EXTERNAL_${ASSETNAME}_URLS}")
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/_env/EXTERNAL_${ASSETNAME}_SHA512.txt" "${EXTERNAL_${ASSETNAME}_SHA512}")
+ file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/_env/EXTERNAL_${ASSETNAME}_MD5.txt" "${EXTERNAL_${ASSETNAME}_MD5}")
endmacro(define_external_asset_urls)
diff --git a/cmake/ports/nlohmann-json/portfile.cmake b/cmake/ports/nlohmann-json/portfile.cmake
index 5cee9565e82..caba976e052 100644
--- a/cmake/ports/nlohmann-json/portfile.cmake
+++ b/cmake/ports/nlohmann-json/portfile.cmake
@@ -1,17 +1,20 @@
include(vcpkg_common_functions)
-set(SOURCE_VERSION 3.3.0)
+file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_NLOHMANN_JSON_JSONHPP_URLS.txt" EXTERNAL_NLOHMANN_JSON_JSONHPP_URLS)
+file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_NLOHMANN_JSON_LICENSE_URLS.txt" EXTERNAL_NLOHMANN_JSON_LICENSE_URLS)
+file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_NLOHMANN_JSON_JSONHPP_SHA512.txt" EXTERNAL_NLOHMANN_JSON_JSONHPP_SHA512)
+file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_NLOHMANN_JSON_LICENSE_SHA512.txt" EXTERNAL_NLOHMANN_JSON_LICENSE_SHA512)
vcpkg_download_distfile(HEADER
- URLS "https://github.com/nlohmann/json/releases/download/v${SOURCE_VERSION}/json.hpp"
- FILENAME "nlohmann-json-${SOURCE_VERSION}.hpp"
- SHA512 c4e4bb84d1488f87a02c4e12409491225e345cc508e6dbbee1a3542fbd4953052c256d0fe78c4d3ce02d44c3a2155fe66f0c8a93a3851ddf94fec4f9f3fd6918
+ URLS ${EXTERNAL_NLOHMANN_JSON_JSONHPP_URLS}
+ FILENAME "nlohmann-json.hpp"
+ SHA512 ${EXTERNAL_NLOHMANN_JSON_JSONHPP_SHA512}
)
vcpkg_download_distfile(LICENSE
- URLS "https://github.com/nlohmann/json/raw/v${SOURCE_VERSION}/LICENSE.MIT"
- FILENAME "nlohmann-json-LICENSE-${SOURCE_VERSION}.txt"
- SHA512 0fdb404547467f4523579acde53066badf458504d33edbb6e39df0ae145ed27d48a720189a60c225c0aab05f2aa4ce4050dcb241b56dc693f7ee9f54c8728a75
+ URLS ${EXTERNAL_NLOHMANN_JSON_LICENSE_URLS}
+ FILENAME "nlohmann-json-LICENSE.txt"
+ SHA512 ${EXTERNAL_NLOHMANN_JSON_LICENSE_SHA512}
)
file(INSTALL ${HEADER} DESTINATION ${CURRENT_PACKAGES_DIR}/include/nlohmann RENAME json.hpp)
diff --git a/cmake/ports/openssl-unix/portfile.cmake b/cmake/ports/openssl-unix/portfile.cmake
index 1047bdeecb1..bafee0ad6a2 100644
--- a/cmake/ports/openssl-unix/portfile.cmake
+++ b/cmake/ports/openssl-unix/portfile.cmake
@@ -1,5 +1,6 @@
include(vcpkg_common_functions)
+
if(VCPKG_CMAKE_SYSTEM_NAME STREQUAL "WindowsStore" OR NOT VCPKG_CMAKE_SYSTEM_NAME)
message(FATAL_ERROR "This port is only for openssl on Unix-like systems")
endif()
@@ -16,12 +17,15 @@ endif()
vcpkg_find_acquire_program(PERL)
+file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_OPENSSL_102S_URLS.txt" EXTERNAL_OPENSSL_102S_URLS)
+file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_OPENSSL_102S_SHA512.txt" EXTERNAL_OPENSSL_102S_SHA512)
+
set(OPENSSL_VERSION 1.0.2s)
vcpkg_download_distfile(OPENSSL_SOURCE_ARCHIVE
- URLS "https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz" "https://www.openssl.org/source/old/1.0.2/openssl-${OPENSSL_VERSION}.tar.gz"
+ URLS ${EXTERNAL_OPENSSL_102S_URLS}
FILENAME "openssl-${OPENSSL_VERSION}.tar.gz"
- SHA512 9f745452c4f777df694158e95003cde78a2cf8199bc481a563ec36644664c3c1415a774779b9791dd18f2aeb57fa1721cb52b3db12d025955e970071d5b66d2a
+ SHA512 ${EXTERNAL_OPENSSL_102S_SHA512}
)
vcpkg_extract_source_archive_ex(
diff --git a/cmake/ports/openssl-windows/portfile.cmake b/cmake/ports/openssl-windows/portfile.cmake
index edf826b4eb7..634a547afd7 100644
--- a/cmake/ports/openssl-windows/portfile.cmake
+++ b/cmake/ports/openssl-windows/portfile.cmake
@@ -11,10 +11,13 @@ vcpkg_find_acquire_program(PERL)
get_filename_component(PERL_EXE_PATH ${PERL} DIRECTORY)
set(ENV{PATH} "$ENV{PATH};${PERL_EXE_PATH}")
+file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_OPENSSL_111H_URLS.txt" EXTERNAL_OPENSSL_111H_URLS)
+file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_OPENSSL_111H_SHA512.txt" EXTERNAL_OPENSSL_111H_SHA512)
+
vcpkg_download_distfile(OPENSSL_SOURCE_ARCHIVE
- URLS "https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz" "https://www.openssl.org/source/old/1.1.1/openssl-${OPENSSL_VERSION}.tar.gz"
+ URLS ${EXTERNAL_OPENSSL_111H_URLS}
FILENAME "openssl-${OPENSSL_VERSION}.tar.gz"
- SHA512 da50fd99325841ed7a4367d9251c771ce505a443a73b327d8a46b2c6a7d2ea99e43551a164efc86f8743b22c2bdb0020bf24a9cbd445e9d68868b2dc1d34033a
+ SHA512 ${EXTERNAL_OPENSSL_111H_SHA512}
)
vcpkg_extract_source_archive(${OPENSSL_SOURCE_ARCHIVE})
diff --git a/cmake/ports/zlib/portfile.cmake b/cmake/ports/zlib/portfile.cmake
index 78030309b1b..9d776472c19 100644
--- a/cmake/ports/zlib/portfile.cmake
+++ b/cmake/ports/zlib/portfile.cmake
@@ -2,10 +2,13 @@ include(vcpkg_common_functions)
set(VERSION 1.2.11)
+file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_ZLIB_URLS.txt" EXTERNAL_ZLIB_URLS)
+file(READ "${VCPKG_ROOT_DIR}/_env/EXTERNAL_ZLIB_SHA512.txt" EXTERNAL_ZLIB_SHA512)
+
vcpkg_download_distfile(ARCHIVE_FILE
- URLS "http://www.zlib.net/zlib-${VERSION}.tar.gz" "https://downloads.sourceforge.net/project/libpng/zlib/${VERSION}/zlib-${VERSION}.tar.gz"
+ URLS ${EXTERNAL_ZLIB_URLS}
FILENAME "zlib1211.tar.gz"
- SHA512 73fd3fff4adeccd4894084c15ddac89890cd10ef105dd5e1835e1e9bbb6a49ff229713bd197d203edfa17c2727700fce65a2a235f07568212d820dca88b528ae
+ SHA512 ${EXTERNAL_ZLIB_SHA512}
)
vcpkg_extract_source_archive_ex(
diff --git a/hifi_android.py b/hifi_android.py
index 07ea00d270f..27a6a8bb115 100644
--- a/hifi_android.py
+++ b/hifi_android.py
@@ -10,95 +10,75 @@
print = functools.partial(print, flush=True)
-ANDROID_PACKAGE_URL = 'https://cdn-1.vircadia.com/eu-c-1/vircadia-public/dependencies/android/'
-
ANDROID_PACKAGES = {
'qt' : {
- 'file': 'qt-5.11.1_linux_armv8-libcpp_openssl_patched.tgz',
- 'checksum': 'aa449d4bfa963f3bc9a9dfe558ba29df',
+ 'extAssetID': 'QT_LINUX_ARMV8_LIBCPP_OPENSSL_PATCHED',
},
'bullet': {
- 'file': 'bullet-2.88_armv8-libcpp.tgz',
- 'checksum': '81642779ccb110f8c7338e8739ac38a0',
- },
+ 'extAssetID': 'BULLET_ARMV8_LIBCPP',
+ },
'draco': {
- 'file': 'draco_armv8-libcpp.tgz',
- 'checksum': '617a80d213a5ec69fbfa21a1f2f738cd',
+ 'extAssetID': 'DRACO_ARMV8_LIBCPP',
},
'glad': {
- 'file': 'glad_armv8-libcpp.zip',
- 'checksum': 'a8ee8584cf1ccd34766c7ddd9d5e5449',
+ 'extAssetID': 'GLAD_ARMV8_LIBCPP',
},
'gvr': {
- 'file': 'gvrsdk_v1.101.0.tgz',
- 'checksum': '57fd02baa069176ba18597a29b6b4fc7',
+ 'extAssetID': 'GVRSDK',
},
'nvtt': {
- 'file': 'nvtt_armv8-libcpp.zip',
- 'checksum': 'eb46d0b683e66987190ed124aabf8910',
+ 'extAssetID': 'NVTT_ARMV8_LIBCPP',
'sharedLibFolder': 'lib',
'includeLibs': ['libnvtt.so', 'libnvmath.so', 'libnvimage.so', 'libnvcore.so']
},
'ovr_sdk_mobile_1.37.0': {
- 'file': 'ovr_sdk_mobile_1.37.0.zip',
- 'checksum': '6040e1966f335a3e5015295154cd7383',
+ 'extAssetID': 'OVR_SDK_MOBILE',
'sharedLibFolder': 'VrApi/Libs/Android/arm64-v8a/Release',
'includeLibs': ['libvrapi.so']
},
'ovr_platform_sdk_23.0.0': {
- 'file': 'ovr_platform_sdk_23.0.0.zip',
- 'checksum': '29d02b560f60d0fa7b8a64cd965dd55b',
+ 'extAssetID': 'OVR_PLATFORM_SDK_ANDROID',
'sharedLibFolder': 'Android/libs/arm64-v8a',
'includeLibs': ['libovrplatformloader.so']
},
'openssl': {
- 'file': 'openssl-1.1.0g_armv8.tgz',
- 'checksum': 'cabb681fbccd79594f65fcc266e02f32'
+ 'extAssetID': 'OPENSSL_ANDROID',
},
'polyvox': {
- 'file': 'polyvox_armv8-libcpp.tgz',
- 'checksum': 'dba88b3a098747af4bb169e9eb9af57e',
+ 'extAssetID': 'POLYVOX_ARMV8_LIBCPP',
'sharedLibFolder': 'lib',
'includeLibs': ['Release/libPolyVoxCore.so', 'libPolyVoxUtil.so'],
},
'tbb': {
- 'file': 'tbb-2018_U1_armv8_libcpp.tgz',
- 'checksum': '20768f298f53b195e71b414b0ae240c4',
+ 'extAssetID': 'TBB_ARMV8_LIBCPP',
'sharedLibFolder': 'lib/release',
'includeLibs': ['libtbb.so', 'libtbbmalloc.so'],
},
'hifiAC': {
- 'baseUrl': 'https://cdn-1.vircadia.com/eu-c-1/vircadia-public/dependencies/',
- 'file': 'codecSDK-android_armv8-2.0.zip',
- 'checksum': '1cbef929675818fc64c4101b72f84a6a'
+ 'extAssetID': 'CODECSDK_ANDROID_ARMV8',
},
'etc2comp': {
- 'file': 'etc2comp-patched-armv8-libcpp.tgz',
- 'checksum': '14b02795d774457a33bbc60e00a786bc'
+ 'extAssetID': 'ETC2COMP_PATCHED_ARMV8_LIBCPP',
},
'breakpad': {
- 'file': 'breakpad.tgz',
- 'checksum': 'ddcb23df336b08017042ba4786db1d9e',
+ 'extAssetID': 'BREAKPAD',
'sharedLibFolder': 'lib',
'includeLibs': {'libbreakpad_client.a'}
},
'webrtc': {
- 'file': 'webrtc-20190626-android.tar.gz',
- 'checksum': 'e2dccd3d8efdcba6d428c87ba7fb2a53'
+ 'extAssetID': 'WEBRTC_ANDROID',
}
}
ANDROID_PLATFORM_PACKAGES = {
'Darwin' : {
'qt': {
- 'file': 'qt-5.11.1_osx_armv8-libcpp_openssl_patched.tgz',
- 'checksum': 'c83cc477c08a892e00c71764dca051a0'
+ 'extAssetID': 'QT_MAC_ARMV8_LIBCPP_OPENSSL_PATCHED',
},
},
'Windows' : {
'qt': {
- 'file': 'qt-5.11.1_win_armv8-libcpp_openssl_patched.tgz',
- 'checksum': '0582191cc55431aa4f660848a542883e'
+ 'extAssetID': 'QT_WIN_ARMV8_LIBCPP_OPENSSL_PATCHED',
},
}
}
@@ -134,15 +114,6 @@ def getPlatformPackages():
result = { **result, **platformPackages }
return result
-def getPackageUrl(package):
- url = ANDROID_PACKAGE_URL
- if 'baseUrl' in package:
- url = package['baseUrl']
- url += package['file']
- if 'versionId' in package:
- url += '?versionId=' + package['versionId']
- return url
-
def copyAndroidLibs(packagePath, appPath):
androidPackages = getPlatformPackages()
jniPath = os.path.join(appPath, 'src/main/jniLibs/arm64-v8a')
@@ -211,7 +182,7 @@ def copyQtDeps(self):
continue
tree = ET.parse(androidDeps)
- root = tree.getroot()
+ root = tree.getroot()
for item in root.findall('./dependencies/lib/depends/*'):
if (item.tag == 'lib') or (item.tag == 'bundled'):
relativeFilename = item.attrib['file']
@@ -236,8 +207,8 @@ def scanQmlImports(self):
qmlRootPath = hifi_utils.scriptRelative('interface/resources/qml')
qmlImportPath = os.path.join(self.qtRootPath, 'qml')
commandResult = hifi_utils.executeSubprocessCapture([
- qmlImportCommandFile,
- '-rootPath', qmlRootPath,
+ qmlImportCommandFile,
+ '-rootPath', qmlRootPath,
'-importPath', qmlImportPath
])
qmlImportResults = json.loads(commandResult)
@@ -318,15 +289,15 @@ def generateAssetsFileList(self):
#
# Gradle version
#
- # DataOutputStream fos = new DataOutputStream(new FileOutputStream(outputFile));
+ # DataOutputStream fos = new DataOutputStream(new FileOutputStream(outputFile))
# for (Map.Entry> e: directoryContents.entrySet()) {
# def entryList = e.getValue()
# fos.writeInt(e.key.length()*2); // 2 bytes per char
- # fos.writeChars(e.key);
- # fos.writeInt(entryList.size());
+ # fos.writeChars(e.key)
+ # fos.writeInt(entryList.size())
# for (String entry: entryList) {
- # fos.writeInt(entry.length()*2);
- # fos.writeChars(entry);
+ # fos.writeInt(entry.length()*2)
+ # fos.writeChars(entry)
# }
# }
diff --git a/hifi_vcpkg.py b/hifi_vcpkg.py
index daff91e9b0c..efa241389ce 100644
--- a/hifi_vcpkg.py
+++ b/hifi_vcpkg.py
@@ -299,10 +299,10 @@ def setupAndroidDependencies(self):
dest = os.path.join(self.androidPackagePath, packageName)
if os.path.isdir(dest):
continue
- url = hifi_android.getPackageUrl(package)
- zipFile = package['file'].endswith('.zip')
- print("Android archive {}".format(package['file']))
- hifi_utils.downloadAndExtract([url], dest, hash=package['checksum'], hasher=hashlib.md5)
+ urls = self.readVar('EXTERNAL_' + package['extAssetID'] + '_URLS').split(';')
+ checksum = self.readVar('EXTERNAL_' + package['extAssetID'] + '_MD5')
+ print("Android archive {}".format(packageName))
+ hifi_utils.downloadAndExtract(urls, dest, hash=checksum, hasher=hashlib.md5)
def writeTag(self):
if self.noClean:
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 30766b892bd..92dea435e4b 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -6994,6 +6994,7 @@ void Application::updateRenderArgs(float deltaTime) {
});
}
+// FIXME: incorporate into avatars-core or vircadia-client
void Application::queryAvatars() {
if (!isInterstitialMode()) {
auto avatarPacket = NLPacket::create(PacketType::AvatarQuery);
@@ -7353,6 +7354,7 @@ void Application::nodeAdded(SharedNodePointer node) {
}
}
+// FIXME: incorporate into native client library
void Application::nodeActivated(SharedNodePointer node) {
if (node->getType() == NodeType::AssetServer) {
// asset server just connected - check if we have the asset browser showing
@@ -7387,6 +7389,7 @@ void Application::nodeActivated(SharedNodePointer node) {
}
}
+ // FIXME: this will be handled in AudioClient, once it's derived from AudioPacketHandler
if (node->getType() == NodeType::AudioMixer && !isInterstitialMode()) {
DependencyManager::get()->negotiateAudioFormat();
}
diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index 0d650bf2e73..c45731c0531 100644
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -1146,11 +1146,11 @@ void MyAvatar::updateSensorToWorldMatrix() {
_sensorToWorldMatrixCache.set(_sensorToWorldMatrix);
updateJointFromController(controller::Action::LEFT_HAND, _controllerLeftHandMatrixCache);
updateJointFromController(controller::Action::RIGHT_HAND, _controllerRightHandMatrixCache);
-
+
if (hasSensorToWorldScaleChanged) {
emit sensorToWorldScaleChanged(sensorToWorldScale);
}
-
+
}
glm::vec3 MyAvatar::getLeftHandPosition() const {
@@ -1601,7 +1601,7 @@ bool MyAvatar::hasAvatarEntities() const {
void MyAvatar::handleCanRezAvatarEntitiesChanged(bool canRezAvatarEntities) {
if (canRezAvatarEntities) {
// Start displaying avatar entities.
- // Allow time for the avatar mixer to be updated with the user's permissions so that it doesn't discard the avatar
+ // Allow time for the avatar mixer to be updated with the user's permissions so that it doesn't discard the avatar
// entities sent. In theory, typical worst case would be Interface running on same PC as server and the timings of
// Interface and the avatar mixer sending DomainListRequest to the domain server being such that the avatar sends its
// DomainListRequest and gets its DomainList response DOMAIN_SERVER_CHECK_IN_MSECS after Interface does. Allow extra
@@ -1733,7 +1733,7 @@ void MyAvatar::handleChangedAvatarEntityData() {
}
});
}
-
+
}
// CHANGE real entities
@@ -2589,7 +2589,7 @@ void MyAvatar::setSkeletonModelURL(const QUrl& skeletonModelURL) {
}
QObject::disconnect(*skeletonConnection);
});
-
+
saveAvatarUrl();
emit skeletonChanged();
}
@@ -2968,9 +2968,9 @@ void MyAvatar::attach(const QString& modelURL, const QString& jointName,
bool allowDuplicates, bool useSaved) {
if (QThread::currentThread() != thread()) {
BLOCKING_INVOKE_METHOD(this, "attach",
- Q_ARG(const QString&, modelURL),
- Q_ARG(const QString&, jointName),
- Q_ARG(const glm::vec3&, translation),
+ Q_ARG(const QString&, modelURL),
+ Q_ARG(const QString&, jointName),
+ Q_ARG(const glm::vec3&, translation),
Q_ARG(const glm::quat&, rotation),
Q_ARG(float, scale),
Q_ARG(bool, isSoft),
@@ -3068,7 +3068,7 @@ void MyAvatar::setAttachmentData(const QVector& attachmentData)
emit attachmentsChanged();
}
-QVector MyAvatar::getAttachmentData() const {
+QVector MyAvatar::getAttachmentData() const {
QVector attachmentData;
if (!DependencyManager::get()->getThisNodeCanRezAvatarEntities()) {
@@ -3124,7 +3124,7 @@ void MyAvatar::setAttachmentsVariant(const QVariantList& variant) {
newAttachments.append(attachment);
}
}
- setAttachmentData(newAttachments);
+ setAttachmentData(newAttachments);
}
bool MyAvatar::findAvatarEntity(const QString& modelURL, const QString& jointName, QUuid& entityID) {
@@ -3432,6 +3432,7 @@ void MyAvatar::preDisplaySide(const RenderArgs* renderArgs) {
_prevShouldDrawHead = shouldDrawHead;
}
+// FIXME: incorporate into avatars-core library
int MyAvatar::sendAvatarDataPacket(bool sendAll) {
using namespace std::chrono;
auto now = Clock::now();
@@ -3515,7 +3516,7 @@ void MyAvatar::updateOrientation(float deltaTime) {
// Smoothly rotate body with arrow keys
float targetSpeed = getDriveKey(YAW) * _yawSpeed;
CameraMode mode = qApp->getCamera().getMode();
- bool computeLookAt = isReadyForPhysics() && !qApp->isHMDMode() &&
+ bool computeLookAt = isReadyForPhysics() && !qApp->isHMDMode() &&
(mode == CAMERA_MODE_FIRST_PERSON_LOOK_AT || mode == CAMERA_MODE_LOOK_AT || mode == CAMERA_MODE_SELFIE);
bool smoothCameraYaw = computeLookAt && mode != CAMERA_MODE_FIRST_PERSON_LOOK_AT;
if (smoothCameraYaw) {
@@ -3815,16 +3816,16 @@ void MyAvatar::updateOrientation(float deltaTime) {
if (_firstPersonSteadyHeadTimer < FIRST_PERSON_RECENTER_SECONDS) {
if (_firstPersonSteadyHeadTimer > 0.0f) {
_firstPersonSteadyHeadTimer += deltaTime;
- }
+ }
} else {
_shouldTurnToFaceCamera = true;
_firstPersonSteadyHeadTimer = 0.0f;
- }
+ }
} else {
_firstPersonSteadyHeadTimer = deltaTime;
}
}
-
+
} else {
head->setBaseYaw(0.0f);
head->setBasePitch(getHead()->getBasePitch() + getDriveKey(PITCH) * _pitchSpeed * deltaTime
@@ -3902,7 +3903,7 @@ glm::vec3 MyAvatar::scaleMotorSpeed(const glm::vec3 forward, const glm::vec3 rig
zSpeed != 0.0f && xSpeed != 0.0f && !isFlying()){
direction = (zSpeed * forward);
}
-
+
auto length = glm::length(direction);
if (length > EPSILON) {
direction /= length;
@@ -5453,7 +5454,7 @@ void MyAvatar::setIsInSittingState(bool isSitting) {
// In updateSitStandState, we only change state if this timer is above a threshold (STANDING_TIMEOUT, SITTING_TIMEOUT).
// This avoids changing state if the user sits and stands up quickly.
_sitStandStateTimer = 0.0f;
-
+
_isInSittingState.set(isSitting);
setResetMode(true);
setSitStandStateChange(true);
@@ -5541,7 +5542,7 @@ void MyAvatar::setWalkBackwardSpeed(float value) {
changed = false;
break;
}
-
+
if (changed && prevVal != value) {
emit walkBackwardSpeedChanged(value);
}
@@ -5863,7 +5864,7 @@ bool MyAvatar::FollowHelper::shouldActivateHorizontal_userStanding(
}
}
}
-
+
if (!stepDetected) {
glm::vec3 defaultHipsPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Hips"));
glm::vec3 defaultHeadPosition = myAvatar.getAbsoluteDefaultJointTranslationInObjectFrame(myAvatar.getJointIndex("Head"));
@@ -6528,6 +6529,7 @@ SpatialParentTree* MyAvatar::getParentTree() const {
return entityTree.get();
}
+//FIXME: incorporate into avatars-core library
const QUuid MyAvatar::grab(const QUuid& targetID, int parentJointIndex,
glm::vec3 positionalOffset, glm::quat rotationalOffset) {
auto grabID = QUuid::createUuid();
@@ -6559,6 +6561,7 @@ const QUuid MyAvatar::grab(const QUuid& targetID, int parentJointIndex,
return grabID;
}
+//FIXME: incorporate into avatars-core library
void MyAvatar::releaseGrab(const QUuid& grabID) {
bool tellHandler { false };
@@ -6697,15 +6700,15 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys
/*@jsdoc
* Flow options currently used in flow simulation.
* @typedef {object} MyAvatar.FlowData
- * @property {boolean} initialized - true if flow has been initialized for the current avatar, false
+ * @property {boolean} initialized - true if flow has been initialized for the current avatar, false
* if it hasn't.
* @property {boolean} active - true if flow is enabled, false if it isn't.
* @property {boolean} colliding - true if collisions are enabled, false if they aren't.
- * @property {Object} physicsData - The physics configuration for each group of joints
+ * @property {Object} physicsData - The physics configuration for each group of joints
* that has been configured.
- * @property {Object} collisions - The collisions configuration for each joint that
+ * @property {Object} collisions - The collisions configuration for each joint that
* has collisions configured.
- * @property {Object} threads - The threads that have been configured, with the first joint's name as the
+ * @property {Object} threads - The threads that have been configured, with the first joint's name as the
* ThreadName and value as an array of the indexes of all the joints in the thread.
*/
/*@jsdoc
@@ -6756,7 +6759,7 @@ QVariantMap MyAvatar::getFlowData() {
}
groupJointsMap[groupName].push_back(joint.second.getIndex());
}
- }
+ }
for (auto &group : groups) {
QVariantMap settingsObject;
QString groupName = group.first;
diff --git a/libraries/audio-client-core/CMakeLists.txt b/libraries/audio-client-core/CMakeLists.txt
new file mode 100644
index 00000000000..9dafc504d2f
--- /dev/null
+++ b/libraries/audio-client-core/CMakeLists.txt
@@ -0,0 +1,22 @@
+set(TARGET_NAME audio-client-core)
+if (ANDROID)
+ set(PLATFORM_QT_COMPONENTS AndroidExtras)
+endif ()
+setup_hifi_library(Network)
+link_hifi_libraries(audio)
+include_hifi_library_headers(shared)
+include_hifi_library_headers(networking)
+
+if (ENABLE_WEBRTC_AUDIO)
+ target_webrtc()
+endif ()
+
+# append audio includes to our list of includes to bubble
+target_include_directories(${TARGET_NAME} PUBLIC "${HIFI_LIBRARY_DIR}/audio/src")
+
+# have CMake grab externals for us
+if (APPLE)
+ find_library(CoreAudio CoreAudio)
+ find_library(CoreFoundation CoreFoundation)
+ target_link_libraries(${TARGET_NAME} ${CoreAudio} ${CoreFoundation})
+endif ()
diff --git a/libraries/audio-client-core/src/AudioClientLogging.cpp b/libraries/audio-client-core/src/AudioClientLogging.cpp
new file mode 100644
index 00000000000..f497e84c4cb
--- /dev/null
+++ b/libraries/audio-client-core/src/AudioClientLogging.cpp
@@ -0,0 +1,14 @@
+//
+// AudioClientLogging.cpp
+// libraries/audio-client/src
+//
+// Created by Seth Alves on 4/6/15.
+// Copyright 2014 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#include "AudioClientLogging.h"
+
+Q_LOGGING_CATEGORY(audioclient, "hifi.audioclient")
diff --git a/libraries/audio-client-core/src/AudioClientLogging.h b/libraries/audio-client-core/src/AudioClientLogging.h
new file mode 100644
index 00000000000..462f8108ac8
--- /dev/null
+++ b/libraries/audio-client-core/src/AudioClientLogging.h
@@ -0,0 +1,20 @@
+//
+// AudioClientLogging.h
+// libraries/audio-client/src
+//
+// Created by Seth Alves on 4/6/15.
+// Copyright 2014 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#ifndef hifi_AudioClientLogging_h
+#define hifi_AudioClientLogging_h
+
+#include
+
+Q_DECLARE_LOGGING_CATEGORY(audioclient)
+
+#endif // hifi_AudioClientLogging_h
+
diff --git a/libraries/audio-client-core/src/AudioIOStats.cpp b/libraries/audio-client-core/src/AudioIOStats.cpp
new file mode 100644
index 00000000000..ad1057c8498
--- /dev/null
+++ b/libraries/audio-client-core/src/AudioIOStats.cpp
@@ -0,0 +1,209 @@
+//
+// AudioStats.cpp
+// interface/src/audio
+//
+// Created by Stephen Birarda on 2014-12-16.
+// Copyright 2014 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#include "AudioIOStats.h"
+
+#include
+#include
+#include
+#include
+
+// This is called 1x/sec (see AudioClient) and we want it to log the last 5s
+static const int INPUT_READS_WINDOW = 5;
+static const int INPUT_UNPLAYED_WINDOW = 5;
+static const int OUTPUT_UNPLAYED_WINDOW = 5;
+
+static const int APPROXIMATELY_30_SECONDS_OF_AUDIO_PACKETS = (int)(30.0f * 1000.0f / AudioConstants::NETWORK_FRAME_MSECS);
+
+
+AudioIOStats::AudioIOStats(MixedProcessedAudioStream* receivedAudioStream) :
+ _interface(new AudioStatsInterface(this)),
+ _inputMsRead(1, INPUT_READS_WINDOW),
+ _inputMsUnplayed(1, INPUT_UNPLAYED_WINDOW),
+ _outputMsUnplayed(1, OUTPUT_UNPLAYED_WINDOW),
+ _lastSentPacketTime(0),
+ _packetTimegaps(1, APPROXIMATELY_30_SECONDS_OF_AUDIO_PACKETS),
+ _receivedAudioStream(receivedAudioStream)
+{
+
+}
+
+void AudioIOStats::reset() {
+ _receivedAudioStream->resetStats();
+
+ _inputMsRead.reset();
+ _inputMsUnplayed.reset();
+ _outputMsUnplayed.reset();
+ _packetTimegaps.reset();
+
+ _interface->updateLocalBuffers(_inputMsRead, _inputMsUnplayed, _outputMsUnplayed, _packetTimegaps);
+ _interface->updateMixerStream(AudioStreamStats());
+ _interface->updateClientStream(AudioStreamStats());
+ _interface->updateInjectorStreams(QHash());
+}
+
+void AudioIOStats::sentPacket() const {
+ // first time this is 0
+ if (_lastSentPacketTime == 0) {
+ _lastSentPacketTime = usecTimestampNow();
+ } else {
+ quint64 now = usecTimestampNow();
+ quint64 gap = now - _lastSentPacketTime;
+ _lastSentPacketTime = now;
+ _packetTimegaps.update(gap);
+ }
+}
+
+void AudioIOStats::processStreamStatsPacket(QSharedPointer message, SharedNodePointer sendingNode) {
+ // parse the appendFlag, clear injected audio stream stats if 0
+ quint8 appendFlag;
+ message->readPrimitive(&appendFlag);
+
+ if (appendFlag & AudioStreamStats::START) {
+ _injectorStreams.clear();
+ }
+
+ // parse the number of stream stats structs to follow
+ quint16 numStreamStats;
+ message->readPrimitive(&numStreamStats);
+
+ // parse the stream stats
+ AudioStreamStats streamStats;
+ for (quint16 i = 0; i < numStreamStats; i++) {
+ message->readPrimitive(&streamStats);
+
+ if (streamStats._streamType == PositionalAudioStream::Microphone) {
+ _interface->updateMixerStream(streamStats);
+ } else {
+ _injectorStreams[streamStats._streamIdentifier] = streamStats;
+ }
+ }
+
+ if (appendFlag & AudioStreamStats::END) {
+ _interface->updateInjectorStreams(_injectorStreams);
+ }
+}
+
+void AudioIOStats::publish() {
+ // call _receivedAudioStream's per-second callback
+ _receivedAudioStream->perSecondCallbackForUpdatingStats();
+
+ auto nodeList = DependencyManager::get();
+ SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer);
+ if (!audioMixer) {
+ return;
+ }
+
+ quint8 appendFlag = AudioStreamStats::START | AudioStreamStats::END;
+ quint16 numStreamStatsToPack = 1;
+ AudioStreamStats stats = _receivedAudioStream->getAudioStreamStats();
+
+ // update the interface
+ _interface->updateLocalBuffers(_inputMsRead, _inputMsUnplayed, _outputMsUnplayed, _packetTimegaps);
+ _interface->updateClientStream(stats);
+
+ // prepare a packet to the mixer
+ int statsPacketSize = sizeof(appendFlag) + sizeof(numStreamStatsToPack) + sizeof(stats);
+ auto statsPacket = NLPacket::create(PacketType::AudioStreamStats, statsPacketSize);
+
+ // pack append flag
+ statsPacket->writePrimitive(appendFlag);
+
+ // pack number of stats packed
+ statsPacket->writePrimitive(numStreamStatsToPack);
+
+ // pack downstream audio stream stats
+ statsPacket->writePrimitive(stats);
+
+ // send packet
+ nodeList->sendPacket(std::move(statsPacket), *audioMixer);
+}
+
+AudioStreamStatsInterface::AudioStreamStatsInterface(QObject* parent) :
+ QObject(parent) {}
+
+void AudioStreamStatsInterface::updateStream(const AudioStreamStats& stats) {
+ lossRate(stats._packetStreamStats.getLostRate());
+ lossCount(stats._packetStreamStats._lost);
+ lossRateWindow(stats._packetStreamWindowStats.getLostRate());
+ lossCountWindow(stats._packetStreamWindowStats._lost);
+
+ framesDesired(stats._desiredJitterBufferFrames);
+ framesAvailable(stats._framesAvailable);
+ framesAvailableAvg(stats._framesAvailableAverage);
+
+ unplayedMsMax(stats._unplayedMs);
+
+ starveCount(stats._starveCount);
+ lastStarveDurationCount(stats._consecutiveNotMixedCount);
+ dropCount(stats._framesDropped);
+ overflowCount(stats._overflowCount);
+
+ timegapMsMax(stats._timeGapMax / USECS_PER_MSEC);
+ timegapMsAvg(stats._timeGapAverage / USECS_PER_MSEC);
+ timegapMsMaxWindow(stats._timeGapWindowMax / USECS_PER_MSEC);
+ timegapMsAvgWindow(stats._timeGapWindowAverage / USECS_PER_MSEC);
+}
+
+AudioStatsInterface::AudioStatsInterface(QObject* parent) :
+ QObject(parent),
+ _client(new AudioStreamStatsInterface(this)),
+ _mixer(new AudioStreamStatsInterface(this)),
+ _injectors(new QObject(this)) {}
+
+
+void AudioStatsInterface::updateLocalBuffers(const MovingMinMaxAvg& inputMsRead,
+ const MovingMinMaxAvg& inputMsUnplayed,
+ const MovingMinMaxAvg& outputMsUnplayed,
+ const MovingMinMaxAvg& timegaps) {
+ if (SharedNodePointer audioNode = DependencyManager::get()->soloNodeOfType(NodeType::AudioMixer)) {
+ pingMs(audioNode->getPingMs());
+ }
+
+ inputReadMsMax(inputMsRead.getWindowMax());
+ inputUnplayedMsMax(inputMsUnplayed.getWindowMax());
+ outputUnplayedMsMax(outputMsUnplayed.getWindowMax());
+
+ sentTimegapMsMax(timegaps.getMax() / USECS_PER_MSEC);
+ sentTimegapMsAvg(timegaps.getAverage() / USECS_PER_MSEC);
+ sentTimegapMsMaxWindow(timegaps.getWindowMax() / USECS_PER_MSEC);
+ sentTimegapMsAvgWindow(timegaps.getWindowAverage() / USECS_PER_MSEC);
+}
+
+void AudioStatsInterface::updateInjectorStreams(const QHash& stats) {
+ // Get existing injectors
+ auto injectorIds = _injectors->dynamicPropertyNames();
+
+ // Go over reported injectors
+ QHash::const_iterator injector = stats.constBegin();
+ while (injector != stats.constEnd()) {
+ const auto id = injector.key().toByteArray();
+ // Mark existing injector (those left will be removed)
+ injectorIds.removeOne(id);
+ auto injectorProperty = _injectors->property(id);
+ // Add new injector
+ if (!injectorProperty.isValid()) {
+ injectorProperty = QVariant::fromValue(new AudioStreamStatsInterface(this));
+ _injectors->setProperty(id, injectorProperty);
+ }
+ // Update property with reported injector
+ injectorProperty.value()->updateStream(injector.value());
+ ++injector;
+ }
+
+ // Remove unreported injectors
+ for (auto& id : injectorIds) {
+ _injectors->property(id).value()->deleteLater();
+ _injectors->setProperty(id, QVariant());
+ }
+
+ emit injectorStreamsChanged();
+}
diff --git a/libraries/audio-client-core/src/AudioIOStats.h b/libraries/audio-client-core/src/AudioIOStats.h
new file mode 100644
index 00000000000..3a996663541
--- /dev/null
+++ b/libraries/audio-client-core/src/AudioIOStats.h
@@ -0,0 +1,425 @@
+//
+// AudioIOStats.h
+// interface/src/audio
+//
+// Created by Stephen Birarda on 2014-12-16.
+// Copyright 2014 High Fidelity, Inc.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#ifndef hifi_AudioIOStats_h
+#define hifi_AudioIOStats_h
+
+#include "MovingMinMaxAvg.h"
+
+#include
+#include
+
+#include
+#include
+#include
+
+class MixedProcessedAudioStream;
+
+#define AUDIO_PROPERTY(TYPE, NAME) \
+ Q_PROPERTY(TYPE NAME READ NAME NOTIFY NAME##Changed) \
+ public: \
+ TYPE NAME() const { return _##NAME; } \
+ void NAME(TYPE value) { \
+ if (_##NAME != value) { \
+ _##NAME = value; \
+ emit NAME##Changed(value); \
+ } \
+ } \
+ Q_SIGNAL void NAME##Changed(TYPE value); \
+ private: \
+ TYPE _##NAME{ (TYPE)0 };
+
+class AudioStreamStatsInterface : public QObject {
+ Q_OBJECT
+
+ /*@jsdoc
+ * Statistics for an audio stream.
+ *
+ *
Provided in properties of the {@link AudioStats} API.
+ *
+ * @class AudioStats.AudioStreamStats
+ * @hideconstructor
+ *
+ * @hifi-interface
+ * @hifi-client-entity
+ * @hifi-avatar
+ *
+ * @property {number} dropCount - The number of silent or old audio frames dropped.
+ * Read-only.
+ * @property {number} framesAvailable - The number of audio frames containing data available.
+ * Read-only.
+ * @property {number} framesAvailableAvg - The time-weighted average of audio frames containing data available.
+ * Read-only.
+ * @property {number} framesDesired - The desired number of audio frames for the jitter buffer.
+ * Read-only.
+ * @property {number} lastStarveDurationCount - The most recent number of consecutive times that audio frames have not been
+ * available for processing.
+ * Read-only.
+ * @property {number} lossCount - The total number of audio packets lost.
+ * Read-only.
+ * @property {number} lossCountWindow - The number of audio packets lost since the previous statistic.
+ * Read-only.
+ * @property {number} lossRate - The ratio of the total number of audio packets lost to the total number of audio packets
+ * expected.
+ * Read-only.
+ * @property {number} lossRateWindow - The ratio of the number of audio packets lost to the number of audio packets
+ * expected since the previous statistic.
+ * Read-only.
+ * @property {number} overflowCount - The number of times that the audio ring buffer has overflowed.
+ * Read-only.
+ * @property {number} starveCount - The total number of times that audio frames have not been available for processing.
+ * Read-only.
+ * @property {number} timegapMsAvg - The overall average time between data packets, in ms.
+ * Read-only.
+ * @property {number} timegapMsAvgWindow - The recent average time between data packets, in ms.
+ * Read-only.
+ * @property {number} timegapMsMax - The overall maximum time between data packets, in ms.
+ * Read-only.
+ * @property {number} timegapMsMaxWindow - The recent maximum time between data packets, in ms.
+ * Read-only.
+ * @property {number} unplayedMsMax - The duration of audio waiting to be played, in ms.
+ * Read-only.
+ */
+
+ /*@jsdoc
+ * Triggered when the ratio of the total number of audio packets lost to the total number of audio packets expected changes.
+ * @function AudioStats.AudioStreamStats.lossRateChanged
+ * @param {number} lossRate - The ratio of the total number of audio packets lost to the total number of audio packets
+ * expected.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(float, lossRate)
+
+ /*@jsdoc
+ * Triggered when the total number of audio packets lost changes.
+ * @function AudioStats.AudioStreamStats.lossCountChanged
+ * @param {number} lossCount - The total number of audio packets lost.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(float, lossCount)
+
+ /*@jsdoc
+ * Triggered when the ratio of the number of audio packets lost to the number of audio packets expected since the previous
+ * statistic changes.
+ * @function AudioStats.AudioStreamStats.lossRateWindowChanged
+ * @param {number} lossRateWindow - The ratio of the number of audio packets lost to the number of audio packets expected
+ * since the previous statistic.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(float, lossRateWindow)
+
+ /*@jsdoc
+ * Triggered when the number of audio packets lost since the previous statistic changes.
+ * @function AudioStats.AudioStreamStats.lossCountWindowChanged
+ * @param {number} lossCountWindow - The number of audio packets lost since the previous statistic.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(float, lossCountWindow)
+
+ /*@jsdoc
+ * Triggered when the desired number of audio frames for the jitter buffer changes.
+ * @function AudioStats.AudioStreamStats.framesDesiredChanged
+ * @param {number} framesDesired - The desired number of audio frames for the jitter buffer.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(int, framesDesired)
+
+ /*@jsdoc
+ * Triggered when the number of audio frames containing data available changes.
+ * @function AudioStats.AudioStreamStats.framesAvailableChanged
+ * @param {number} framesAvailable - The number of audio frames containing data available.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(int, framesAvailable)
+
+ /*@jsdoc
+ * Triggered when the time-weighted average of audio frames containing data available changes.
+ * @function AudioStats.AudioStreamStats.framesAvailableAvgChanged
+ * @param {number} framesAvailableAvg - The time-weighted average of audio frames containing data available.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(int, framesAvailableAvg)
+
+ /*@jsdoc
+ * Triggered when the duration of audio waiting to be played changes.
+ * @function AudioStats.AudioStreamStats.unplayedMsMaxChanged
+ * @param {number} unplayedMsMax - The duration of audio waiting to be played, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(float, unplayedMsMax)
+
+ /*@jsdoc
+ * Triggered when the total number of times that audio frames have not been available for processing changes.
+ * @function AudioStats.AudioStreamStats.starveCountChanged
+ * @param {number} starveCount - The total number of times that audio frames have not been available for processing.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(int, starveCount)
+
+ /*@jsdoc
+ * Triggered when the most recenbernumber of consecutive times that audio frames have not been available for processing
+ * changes.
+ * @function AudioStats.AudioStreamStats.lastStarveDurationCountChanged
+ * @param {number} lastStarveDurationCount - The most recent number of consecutive times that audio frames have not been
+ * available for processing.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(int, lastStarveDurationCount)
+
+ /*@jsdoc
+ * Triggered when the number of silent or old audio frames dropped changes.
+ * @function AudioStats.AudioStreamStats.dropCountChanged
+ * @param {number} dropCount - The number of silent or old audio frames dropped.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(int, dropCount)
+
+ /*@jsdoc
+ * Triggered when the number of times that the audio ring buffer has overflowed changes.
+ * @function AudioStats.AudioStreamStats.overflowCountChanged
+ * @param {number} overflowCount - The number of times that the audio ring buffer has overflowed.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(int, overflowCount)
+
+ /*@jsdoc
+ * Triggered when the overall maximum time between data packets changes.
+ * @function AudioStats.AudioStreamStats.timegapMsMaxChanged
+ * @param {number} timegapMsMax - The overall maximum time between data packets, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(quint64, timegapMsMax)
+
+ /*@jsdoc
+ * Triggered when the overall average time between data packets changes.
+ * @function AudioStats.AudioStreamStats.timegapMsAvgChanged
+ * @param {number} timegapMsAvg - The overall average time between data packets, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(quint64, timegapMsAvg)
+
+ /*@jsdoc
+ * Triggered when the recent maximum time between data packets changes.
+ * @function AudioStats.AudioStreamStats.timegapMsMaxWindowChanged
+ * @param {number} timegapMsMaxWindow - The recent maximum time between data packets, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(quint64, timegapMsMaxWindow)
+
+ /*@jsdoc
+ * Triggered when the recent average time between data packets changes.
+ * @function AudioStats.AudioStreamStats.timegapMsAvgWindowChanged
+ * @param {number} timegapMsAvgWindow - The recent average time between data packets, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(quint64, timegapMsAvgWindow)
+
+public:
+ void updateStream(const AudioStreamStats& stats);
+
+private:
+ friend class AudioStatsInterface;
+ AudioStreamStatsInterface(QObject* parent);
+};
+
+class AudioStatsInterface : public QObject {
+ Q_OBJECT
+
+ /*@jsdoc
+ * The AudioStats API provides statistics of the client and mixer audio.
+ *
+ * @namespace AudioStats
+ *
+ * @hifi-interface
+ * @hifi-client-entity
+ * @hifi-avatar
+ *
+ * @property {AudioStats.AudioStreamStats} clientStream - Statistics of the client's audio stream.
+ * Read-only.
+ * @property {number} inputReadMsMax - The maximum duration of a block of audio data recently read from the microphone, in
+ * ms.
+ * Read-only.
+ * @property {number} inputUnplayedMsMax - The maximum duration of microphone audio recently in the input buffer waiting to
+ * be played, in ms.
+ * Read-only.
+ * @property {AudioStats.AudioStreamStats} mixerStream - Statistics of the audio mixer's stream.
+ * Read-only.
+ * @property {number} outputUnplayedMsMax - The maximum duration of output audio recently in the output buffer waiting to
+ * be played, in ms.
+ * Read-only.
+ * @property {number} pingMs - The current ping time to the audio mixer, in ms.
+ * Read-only.
+ * @property {number} sentTimegapMsAvg - The overall average time between sending data packets to the audio mixer, in ms.
+ * Read-only.
+ * @property {number} sentTimegapMsAvgWindow - The recent average time between sending data packets to the audio mixer, in
+ * ms.
+ * Read-only.
+ * @property {number} sentTimegapMsMax - The overall maximum time between sending data packets to the audio mixer, in ms.
+ * Read-only.
+ * @property {number} sentTimegapMsMaxWindow - The recent maximum time between sending data packets to the audio mixer, in
+ * ms.
+ * Read-only.
+ */
+
+ /*@jsdoc
+ * Triggered when the ping time to the audio mixer changes.
+ * @function AudioStats.pingMsChanged
+ * @param {number} pingMs - The ping time to the audio mixer, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(float, pingMs);
+
+
+ /*@jsdoc
+ * Triggered when the maximum duration of a block of audio data recently read from the microphone changes.
+ * @function AudioStats.inputReadMsMaxChanged
+ * @param {number} inputReadMsMax - The maximum duration of a block of audio data recently read from the microphone, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(float, inputReadMsMax);
+
+ /*@jsdoc
+ * Triggered when the maximum duration of microphone audio recently in the input buffer waiting to be played changes.
+ * @function AudioStats.inputUnplayedMsMaxChanged
+ * @param {number} inputUnplayedMsMax - The maximum duration of microphone audio recently in the input buffer waiting to be
+ * played, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(float, inputUnplayedMsMax);
+
+ /*@jsdoc
+ * Triggered when the maximum duration of output audio recently in the output buffer waiting to be played changes.
+ * @function AudioStats.outputUnplayedMsMaxChanged
+ * @param {number} outputUnplayedMsMax - The maximum duration of output audio recently in the output buffer waiting to be
+ * played, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(float, outputUnplayedMsMax);
+
+
+ /*@jsdoc
+ * Triggered when the overall maximum time between sending data packets to the audio mixer changes.
+ * @function AudioStats.sentTimegapMsMaxChanged
+ * @param {number} sentTimegapMsMax - The overall maximum time between sending data packets to the audio mixer, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(quint64, sentTimegapMsMax);
+
+ /*@jsdoc
+ * Triggered when the overall average time between sending data packets to the audio mixer changes.
+ * @function AudioStats.sentTimegapMsAvgChanged
+ * @param {number} sentTimegapMsAvg - The overall average time between sending data packets to the audio mixer, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(quint64, sentTimegapMsAvg);
+
+ /*@jsdoc
+ * Triggered when the recent maximum time between sending data packets to the audio mixer changes.
+ * @function AudioStats.sentTimegapMsMaxWindowChanged
+ * @param {number} sentTimegapMsMaxWindow - The recent maximum time between sending data packets to the audio mixer, in ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(quint64, sentTimegapMsMaxWindow);
+
+ /*@jsdoc
+ * Triggered when the recent average time between sending data packets to the audio mixer changes.
+ * @function AudioStats.sentTimegapMsAvgWindowChanged
+ * @param {number} sentTimegapMsAvgWindow - The recent average time between sending data packets to the audio mixer, in
+ * ms.
+ * @returns {Signal}
+ */
+ AUDIO_PROPERTY(quint64, sentTimegapMsAvgWindow);
+
+ Q_PROPERTY(AudioStreamStatsInterface* mixerStream READ getMixerStream NOTIFY mixerStreamChanged);
+ Q_PROPERTY(AudioStreamStatsInterface* clientStream READ getClientStream NOTIFY clientStreamChanged);
+
+ // FIXME: The injectorStreams property isn't available in JavaScript but the notification signal is.
+ Q_PROPERTY(QObject* injectorStreams READ getInjectorStreams NOTIFY injectorStreamsChanged);
+
+public:
+ AudioStreamStatsInterface* getMixerStream() const { return _mixer; }
+ AudioStreamStatsInterface* getClientStream() const { return _client; }
+ QObject* getInjectorStreams() const { return _injectors; }
+
+ void updateLocalBuffers(const MovingMinMaxAvg& inputMsRead,
+ const MovingMinMaxAvg& inputMsUnplayed,
+ const MovingMinMaxAvg& outputMsUnplayed,
+ const MovingMinMaxAvg& timegaps);
+ void updateMixerStream(const AudioStreamStats& stats) { _mixer->updateStream(stats); emit mixerStreamChanged(); }
+ void updateClientStream(const AudioStreamStats& stats) { _client->updateStream(stats); emit clientStreamChanged(); }
+ void updateInjectorStreams(const QHash& stats);
+
+signals:
+
+ /*@jsdoc
+ * Triggered when the mixer's stream statistics have been updated.
+ * @function AudioStats.mixerStreamChanged
+ * @returns {Signal}
+ */
+ void mixerStreamChanged();
+
+ /*@jsdoc
+ * Triggered when the client's stream statisticss have been updated.
+ * @function AudioStats.clientStreamChanged
+ * @returns {Signal}
+ */
+ void clientStreamChanged();
+
+ /*@jsdoc
+ * Triggered when the injector streams' statistics have been updated.
+ *
Note: The injector streams' statistics are currently not provided.
+ * @function AudioStats.injectorStreamsChanged
+ * @returns {Signal}
+ */
+ void injectorStreamsChanged();
+
+private:
+ friend class AudioIOStats;
+ AudioStatsInterface(QObject* parent);
+ AudioStreamStatsInterface* _client;
+ AudioStreamStatsInterface* _mixer;
+ QObject* _injectors;
+};
+
+class AudioIOStats : public QObject {
+ Q_OBJECT
+public:
+ AudioIOStats(MixedProcessedAudioStream* receivedAudioStream);
+
+ void reset();
+
+ AudioStatsInterface* data() const { return _interface; }
+
+ void updateInputMsRead(float ms) const { _inputMsRead.update(ms); }
+ void updateInputMsUnplayed(float ms) const { _inputMsUnplayed.update(ms); }
+ void updateOutputMsUnplayed(float ms) const { _outputMsUnplayed.update(ms); }
+ void sentPacket() const;
+
+ void publish();
+
+public slots:
+ void processStreamStatsPacket(QSharedPointer message, SharedNodePointer sendingNode);
+
+private:
+ AudioStatsInterface* _interface;
+
+ mutable MovingMinMaxAvg _inputMsRead;
+ mutable MovingMinMaxAvg _inputMsUnplayed;
+ mutable MovingMinMaxAvg _outputMsUnplayed;
+
+ mutable quint64 _lastSentPacketTime;
+ mutable MovingMinMaxAvg _packetTimegaps;
+
+ MixedProcessedAudioStream* _receivedAudioStream;
+ QHash _injectorStreams;
+};
+
+#endif // hifi_AudioIOStats_h
diff --git a/libraries/audio-client-core/src/AudioPacketHandler.h b/libraries/audio-client-core/src/AudioPacketHandler.h
new file mode 100644
index 00000000000..181cb8583b1
--- /dev/null
+++ b/libraries/audio-client-core/src/AudioPacketHandler.h
@@ -0,0 +1,462 @@
+//
+// AudioPacketHandler.h
+// libraries/audio-client-core/src
+//
+// Created by Nshan G. on 4 July 2022
+// Copyright 2013 High Fidelity, Inc.
+// Copyright 2022 Vircadia contributors.
+// Copyright 2022 DigiSomni LLC.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#ifndef LIBRARIES_AUDIO_CLIENT_CORE_SRC_AUDIOPACKETHANDLER_H
+#define LIBRARIES_AUDIO_CLIENT_CORE_SRC_AUDIOPACKETHANDLER_H
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "AudioIOStats.h"
+
+#if defined(WEBRTC_AUDIO)
+# define WEBRTC_APM_DEBUG_DUMP 0
+# include
+# include "modules/audio_processing/audio_processing_impl.h"
+#endif
+
+#ifdef _WIN32
+#pragma warning( push )
+#pragma warning( disable : 4273 )
+#pragma warning( disable : 4305 )
+#endif
+
+#ifdef _WIN32
+#pragma warning( pop )
+#endif
+
+#if defined (Q_OS_ANDROID)
+#define VOICE_RECOGNITION "voicerecognition"
+#define VOICE_COMMUNICATION "voicecommunication"
+
+#define SETTING_AEC_KEY "Android/aec"
+#define DEFAULT_AEC_ENABLED true
+#endif
+
+class QIODevice;
+
+class Transform;
+class NLPacket;
+
+#define DEFAULT_STARVE_DETECTION_ENABLED true
+#define DEFAULT_BUFFER_FRAMES 1
+
+inline auto defaultAudioPositionGetter() {
+ return Vectors::ZERO;
+}
+
+inline auto defaultAudioOrientationGetter() {
+ return Quaternions::IDENTITY;
+}
+
+struct AudioFormat {
+ enum SampleTag {
+ Signed16,
+ Float
+ };
+
+ SampleTag sampleType = SampleTag(-1);
+ int sampleRate = -1;
+ int channelCount = -1;
+
+ int getSampleSize() const {
+ switch(sampleType) {
+ case Signed16: return sizeof(int16_t);
+ case Float: return sizeof(float) ;
+ default: return 0;
+ };
+ };
+
+ int getSampleBits() const {
+ return getSampleSize() * CHAR_BIT;
+ };
+
+ qint32 bytesForDuration(qint64 microseconds) const {
+ return microseconds * sampleRate * channelCount * getSampleSize() / USECS_PER_SECOND;
+ }
+
+ bool operator==(const AudioFormat& other) const {
+ return sampleType == other.sampleType && sampleRate == other.sampleRate && channelCount == other.channelCount;
+ }
+ bool operator!=(const AudioFormat& other) const {
+ return !(*this == other);
+ }
+
+ bool isValid() const {
+ return sampleRate != -1 && channelCount != -1 &&
+ (sampleType == Signed16 || sampleType == Float);
+ }
+};
+
+template
+class AudioPacketHandler {
+ using LocalInjectorsStream = AudioMixRingBuffer;
+public:
+ static constexpr int MIN_BUFFER_FRAMES = 1;
+ static constexpr int MAX_BUFFER_FRAMES = 20;
+
+ using AudioPositionGetter = std::function;
+ using AudioOrientationGetter = std::function;
+
+ using Mutex = std::mutex;
+ using Lock = std::unique_lock;
+
+ class AudioOutputIODevice : public QIODevice {
+ public:
+ AudioOutputIODevice(LocalInjectorsStream& localInjectorsStream, MixedProcessedAudioStream& receivedAudioStream,
+ AudioPacketHandler* audio) :
+ _localInjectorsStream(localInjectorsStream), _receivedAudioStream(receivedAudioStream),
+ _audio(audio), _unfulfilledReads(0) {}
+
+ void open() { QIODevice::open(QIODevice::ReadOnly | QIODevice::Unbuffered); }
+ qint64 readData(char* data, qint64 maxSize) override;
+ qint64 writeData(const char* data, qint64 maxSize) override { return 0; }
+ int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; }
+ private:
+ using QIODevice::open; // silence overloaded virtual warning
+
+ LocalInjectorsStream& _localInjectorsStream;
+ MixedProcessedAudioStream& _receivedAudioStream;
+ AudioPacketHandler* _audio;
+ int _unfulfilledReads;
+ };
+
+ void startThread();
+ void negotiateAudioFormat();
+ void selectAudioFormat(const QString& selectedCodecName);
+
+ Q_INVOKABLE QString getSelectedAudioFormat() const { return _selectedCodecName; }
+ Q_INVOKABLE bool getNoiseGateOpen() const { return _audioGateOpen; }
+ Q_INVOKABLE float getSilentInboundPPS() const { return _silentInbound.rate(); }
+ Q_INVOKABLE float getAudioInboundPPS() const { return _audioInbound.rate(); }
+ Q_INVOKABLE float getSilentOutboundPPS() const { return _silentOutbound.rate(); }
+ Q_INVOKABLE float getAudioOutboundPPS() const { return _audioOutbound.rate(); }
+
+ const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; }
+ MixedProcessedAudioStream& getReceivedAudioStream() { return _receivedAudioStream; }
+
+ float getLastInputLoudness() const { return _lastInputLoudness; }
+
+ float getTimeSinceLastClip() const { return _timeSinceLastClip; }
+ float getAudioAverageInputLoudness() const { return _lastInputLoudness; }
+
+ const AudioIOStats& getStats() const { return _stats; }
+
+ bool isSimulatingJitter() { return _gate.isSimulatingJitter(); }
+ void setIsSimulatingJitter(bool enable) { _gate.setIsSimulatingJitter(enable); }
+
+ int getGateThreshold() { return _gate.getThreshold(); }
+ void setGateThreshold(int threshold) { _gate.setThreshold(threshold); }
+
+ void setPositionGetter(AudioPositionGetter positionGetter) { _positionGetter = positionGetter; }
+ void setOrientationGetter(AudioOrientationGetter orientationGetter) { _orientationGetter = orientationGetter; }
+
+ void setIsPlayingBackRecording(bool isPlayingBackRecording) { _isPlayingBackRecording = isPlayingBackRecording; }
+
+ Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale);
+
+ // FIXME: CRTP should override virtual member of in audio-client/sec/AudioClient
+ bool outputLocalInjector(const AudioInjectorPointer& injector);
+
+ // HifiAudioDeviceInfo getActiveAudioDevice(QAudio::Mode mode) const;
+ // QList getAudioDevices(QAudio::Mode mode) const;
+
+ // void enablePeakValues(bool enable) { _enablePeakValues = enable; }
+ // bool peakValuesAvailable() const;
+
+ // bool getNamedAudioDeviceForModeExists(QAudio::Mode mode, const QString& deviceName);
+
+ void setAudioPaused(bool pause);
+
+ // FIXME: CRTP should override virtual member of in audio-client/sec/AudioClient
+ AudioSolo& getAudioSolo() { return _solo; }
+
+ int getNumLocalInjectors();
+
+ void start();
+ void stop();
+
+ void handleAudioEnvironmentDataPacket(QSharedPointer message);
+ void handleAudioDataPacket(QSharedPointer message);
+ void handleNoisyMutePacket(QSharedPointer message);
+ void handleMuteEnvironmentPacket(QSharedPointer message);
+ void handleSelectedAudioFormat(QSharedPointer message);
+ void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec);
+
+ void sendDownstreamAudioStatsPacket() { _stats.publish(); }
+ void handleMicAudioInput(const char* data, int size);
+ void sendInput();
+ void handleRecordedAudioInput(const QByteArray& audio);
+ void reset();
+ void audioMixerKilled();
+
+ void setMuted(bool muted, bool emitSignal = true);
+ bool isMuted() { return _isMuted; }
+
+ void setNoiseReduction(bool isNoiseGateEnabled, bool emitSignal = true);
+ bool isNoiseReductionEnabled() const { return _isNoiseGateEnabled; }
+
+ void setNoiseReductionAutomatic(bool isNoiseGateAutomatic, bool emitSignal = true);
+ bool isNoiseReductionAutomatic() const { return _isNoiseReductionAutomatic; }
+
+ void setNoiseReductionThreshold(float noiseReductionThreshold, bool emitSignal = true);
+ float noiseReductionThreshold() const { return _noiseReductionThreshold; }
+
+ void setAcousticEchoCancellation(bool isAECEnabled, bool emitSignal = true);
+ bool isAcousticEchoCancellationEnabled() const { return _isAECEnabled; }
+
+ // FIXME: CRTP these should override virtual members of in audio-client/sec/AudioClient
+ bool getServerEcho() { return _shouldEchoToServer; }
+ void setServerEcho(bool serverEcho) { _shouldEchoToServer = serverEcho; }
+ void toggleServerEcho() { _shouldEchoToServer = !_shouldEchoToServer; }
+
+ void processReceivedSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer);
+ void sendMuteEnvironmentPacket();
+
+ int setOutputBufferSize(int numFrames, bool persist = true);
+
+ void setReverb(bool reverb);
+ void setReverbOptions(const AudioEffectOptions* options);
+
+ void setLocalInjectorGain(float gain) { _localInjectorGain = gain; };
+ void setSystemInjectorGain(float gain) { _systemInjectorGain = gain; };
+ void setOutputGain(float gain) { _outputGain = gain; };
+
+protected:
+ AudioPacketHandler();
+ ~AudioPacketHandler();
+
+ void cleanupInput();
+ bool setupInput(AudioFormat);
+ void setupDummyInput();
+ bool isDummyInput();
+
+ void cleanupOutput();
+ bool setupOutput(AudioFormat);
+
+ AudioOutputIODevice _audioOutputIODevice {_localInjectorsStream, _receivedAudioStream, this};
+ bool _isMuted {false};
+ glm::vec3 avatarBoundingBoxCorner {};
+ glm::vec3 avatarBoundingBoxScale {};
+ AudioFormat _inputFormat {};
+ AudioFormat _outputFormat {};
+ QString _selectedCodecName;
+
+private:
+ static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES{ 100 };
+ // OUTPUT_CHANNEL_COUNT is audio pipeline output format, which is always 2 channel.
+ // _outputFormat.channelCount() is device output format, which may be 1 or multichannel.
+ static const int OUTPUT_CHANNEL_COUNT{ 2 };
+ static const int STARVE_DETECTION_THRESHOLD{ 3 };
+ static const int STARVE_DETECTION_PERIOD{ 10 * 1000 }; // 10 Seconds
+
+ friend class CheckDevicesThread;
+ friend class LocalInjectorsThread;
+
+ Derived& derived();
+ const Derived& derived() const;
+
+ float loudnessToLevel(float loudness);
+
+ void outputFormatChanged();
+ void handleAudioInput(QByteArray& audioBuffer);
+ void prepareLocalAudioInjectors(std::unique_ptr localAudioLock = nullptr);
+ bool mixLocalAudioInjectors(float* mixBuffer);
+ float azimuthForSource(const glm::vec3& relativePosition);
+ float gainForSource(float distance, float volume);
+
+ class Gate {
+ public:
+ Gate(AudioPacketHandler* audioClient);
+
+ bool isSimulatingJitter() { return _isSimulatingJitter; }
+ void setIsSimulatingJitter(bool enable);
+
+ int getThreshold() { return _threshold; }
+ void setThreshold(int threshold);
+
+ void insert(QSharedPointer message);
+
+ private:
+ void flush();
+
+ AudioPacketHandler* _audioClient;
+ std::queue> _queue;
+ std::mutex _mutex;
+
+ int _index {0};
+ int _threshold {1};
+ bool _isSimulatingJitter {false};
+ };
+
+ Gate _gate{ this };
+
+ Mutex _injectorsMutex;
+ QTimer* _dummyAudioInput {nullptr};
+ AudioFormat _desiredInputFormat;
+ std::atomic _audioOutputInitialized {false};
+ AudioFormat _desiredOutputFormat;
+ int _outputFrameSize {0};
+ int _numOutputCallbackBytes {0};
+ std::vector _inputTypeConversionBuffer {};
+ AudioRingBuffer _inputRingBuffer {0};
+ LocalInjectorsStream _localInjectorsStream {0 , 1};
+ // In order to use _localInjectorsStream as a lock-free pipe,
+ // use it with a single producer/consumer, and track available samples and injectors
+ std::atomic _localSamplesAvailable {0};
+ std::atomic _localInjectorsAvailable {false};
+ MixedProcessedAudioStream _receivedAudioStream {RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES};
+
+ quint64 _outputStarveDetectionStartTimeMsec {0};
+ int _outputStarveDetectionCount {0};
+
+ int _sessionOutputBufferSizeFrames {DEFAULT_BUFFER_FRAMES};
+
+ float _lastRawInputLoudness {0.0f}; // before mute/gate
+ float _lastSmoothedRawInputLoudness {0.0f};
+ float _lastInputLoudness {0.0f}; // after mute/gate
+ float _timeSinceLastClip {-1.0f};
+ int _totalInputAudioSamples;
+
+ bool _shouldEchoLocally {false};
+ bool _shouldEchoToServer {false};
+ bool _isNoiseGateEnabled {true};
+ bool _isNoiseReductionAutomatic {true};
+ float _noiseReductionThreshold {0.1f};
+ bool _isAECEnabled {true};
+
+ bool _reverb {false};
+ AudioEffectOptions _scriptReverbOptions;
+ AudioEffectOptions _zoneReverbOptions;
+ AudioEffectOptions* _reverbOptions {&_scriptReverbOptions};
+ AudioReverb _sourceReverb {AudioConstants::SAMPLE_RATE};
+ AudioReverb _listenerReverb {AudioConstants::SAMPLE_RATE};
+ AudioReverb _localReverb {AudioConstants::SAMPLE_RATE};
+
+ // possible streams needed for resample
+ AudioSRC* _inputToNetworkResampler {nullptr};
+ AudioSRC* _networkToOutputResampler {nullptr};
+ AudioSRC* _localToOutputResampler {nullptr};
+
+ // for network audio (used by network audio thread)
+ int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
+
+ // for output audio (used by this thread)
+ int _outputPeriod {0};
+ float* _outputMixBuffer {NULL};
+ int16_t* _outputScratchBuffer {NULL};
+ std::atomic _outputGain {1.0f};
+ float _lastOutputGain {1.0f};
+
+ // for local audio (used by audio injectors thread)
+ std::atomic _localInjectorGain {1.0f};
+ std::atomic _systemInjectorGain {1.0f};
+ float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
+ int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
+ float* _localOutputMixBuffer {NULL};
+ Mutex _localAudioMutex;
+ AudioLimiter _audioLimiter {AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT};
+
+ // Adds Reverb
+ void configureReverb();
+ void updateReverbOptions();
+
+#if defined(WEBRTC_AUDIO)
+ static const int WEBRTC_SAMPLE_RATE_MAX = 96000;
+ static const int WEBRTC_CHANNELS_MAX = 2;
+ static const int WEBRTC_FRAMES_MAX = webrtc::AudioProcessing::kChunkSizeMs * WEBRTC_SAMPLE_RATE_MAX / 1000;
+
+ webrtc::AudioProcessing* _apm {nullptr};
+
+ int16_t _fifoFarEnd[WEBRTC_CHANNELS_MAX * WEBRTC_FRAMES_MAX] {};
+ int _numFifoFarEnd = 0; // numFrames saved in fifo
+
+ void configureWebrtc();
+ void processWebrtcFarEnd(const int16_t* samples, int numFrames, int numChannels, int sampleRate);
+ void processWebrtcNearEnd(int16_t* samples, int numFrames, int numChannels, int sampleRate);
+#endif
+
+ // Callback acceleration dependent calculations
+ int calculateNumberOfInputCallbackBytes(const AudioFormat& format) const;
+ int calculateNumberOfFrameSamples(int numBytes) const;
+
+ quint16 _outgoingAvatarAudioSequenceNumber {0};
+
+ AudioIOStats _stats {&_receivedAudioStream};
+
+ AudioGate* _audioGate {nullptr};
+ bool _audioGateOpen {true};
+
+ AudioPositionGetter _positionGetter{ defaultAudioPositionGetter };
+ AudioOrientationGetter _orientationGetter{ defaultAudioOrientationGetter };
+
+ bool _hasReceivedFirstPacket {false};
+
+ QVector _activeLocalAudioInjectors;
+
+ bool _isPlayingBackRecording {false};
+ bool _audioPaused {false};
+
+ std::shared_ptr _codec;
+ Encoder* _encoder {nullptr}; // for outbound mic stream
+
+ RateCounter<> _silentOutbound;
+ RateCounter<> _audioOutbound;
+ RateCounter<> _silentInbound;
+ RateCounter<> _audioInbound;
+
+#if defined(Q_OS_ANDROID)
+ bool _shouldRestartInputSetup {true}; // Should we restart the input device because of an unintended stop?
+#endif
+
+ AudioSolo _solo;
+
+ QFuture _localPrepInjectorFuture;
+
+ bool _isRecording {false};
+};
+
+#endif /* end of include guard */
diff --git a/libraries/audio-client-core/src/AudioPacketHandler.hpp b/libraries/audio-client-core/src/AudioPacketHandler.hpp
new file mode 100644
index 00000000000..b75d0487a18
--- /dev/null
+++ b/libraries/audio-client-core/src/AudioPacketHandler.hpp
@@ -0,0 +1,2033 @@
+//
+// AudioPacketHandler.hpp
+// libraries/audio-client-core/src
+//
+// Created by Nshan G on 4 July 2022.
+// Copyright 2013 High Fidelity, Inc.
+// Copyright 2022 Vircadia contributors.
+// Copyright 2022 DigiSomni LLC.
+//
+// Distributed under the Apache License, Version 2.0.
+// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#ifndef LIBRARIES_AUDIO_CLIENT_CORE_SRC_AUDIOPACKETHANDLER_HPP
+#define LIBRARIES_AUDIO_CLIENT_CORE_SRC_AUDIOPACKETHANDLER_HPP
+
+#include "AudioPacketHandler.h"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+#ifdef __APPLE__
+#include
+#endif
+
+#ifdef WIN32
+#define WIN32_LEAN_AND_MEAN 1
+#include
+#include
+#include
+#include
+#include
+#include
+#endif
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "AudioClientLogging.h"
+#include "AudioLogging.h"
+#include "AudioHelpers.h"
+
+using Lock = std::unique_lock;
+
+inline QDebug& operator<<(QDebug& debug, const AudioFormat& audioFormat) {
+ QString sampleTypeString = audioFormat.sampleType == AudioFormat::Signed16 ? "Signed 16bit" :
+ audioFormat.sampleType == AudioFormat::Float ? "Float" :
+ "!INVALID!";
+ return debug << QString("AudioFormat{ sampleType: %1, sampleRate: %2, channelCount: %3 }")
+ .arg(sampleTypeString, QString::number(audioFormat.sampleRate), QString::number(audioFormat.channelCount));
+}
+
+template
+auto channelUpmix(const Source* source, Destination* destination, int numSamples, int numExtraChannels) {
+ Source* dest = reinterpret_cast(destination);
+ for (int i = 0; i < numSamples/2; i++) {
+
+ *dest++ = *source++; // left
+ *dest++ = *source++; // right
+ // and the rest
+ for (int n = 0; n < numExtraChannels; n++) {
+ *dest++ = 0;
+ }
+ }
+ return reinterpret_cast(dest) - reinterpret_cast(destination);
+}
+
+template
+auto channelDownmix(const Source* source, Destination* destination, int numSamples) {
+ Source* dest = reinterpret_cast(destination);
+ for (int i = 0; i < numSamples/2; i++) {
+
+ // read 2 samples
+ int16_t left = *source++;
+ int16_t right = *source++;
+
+ // write 1 sample
+ *dest++ = (left + right) / 2;
+ }
+ return reinterpret_cast(dest) - reinterpret_cast(destination);
+}
+
+template
+auto copyWithChannelConversion(const Source* source, int sourceChannels, Destination* destination, int destinationChannels, int numSamples) {
+ if (destinationChannels == sourceChannels) {
+ const std::ptrdiff_t byteCount = numSamples * sizeof(Source);
+ memcpy(destination, source, byteCount);
+ return byteCount;
+ } else if (destinationChannels > sourceChannels) {
+ int extraChannels = destinationChannels - sourceChannels;
+ return channelUpmix(source, destination, numSamples, extraChannels);
+ } else {
+ return channelDownmix(source, destination, numSamples);
+ }
+}
+
+inline bool detectClipping(int16_t* samples, int numSamples, int numChannels) {
+
+ const int32_t CLIPPING_THRESHOLD = 32392; // -0.1 dBFS
+ const int CLIPPING_DETECTION = 3; // consecutive samples over threshold
+
+ bool isClipping = false;
+
+ if (numChannels == 2) {
+ int oversLeft = 0;
+ int oversRight = 0;
+
+ for (int i = 0; i < numSamples/2; i++) {
+ int32_t left = std::abs((int32_t)samples[2*i+0]);
+ int32_t right = std::abs((int32_t)samples[2*i+1]);
+
+ if (left > CLIPPING_THRESHOLD) {
+ isClipping |= (++oversLeft >= CLIPPING_DETECTION);
+ } else {
+ oversLeft = 0;
+ }
+ if (right > CLIPPING_THRESHOLD) {
+ isClipping |= (++oversRight >= CLIPPING_DETECTION);
+ } else {
+ oversRight = 0;
+ }
+ }
+ } else {
+ int overs = 0;
+
+ for (int i = 0; i < numSamples; i++) {
+ int32_t sample = std::abs((int32_t)samples[i]);
+
+ if (sample > CLIPPING_THRESHOLD) {
+ isClipping |= (++overs >= CLIPPING_DETECTION);
+ } else {
+ overs = 0;
+ }
+ }
+ }
+
+ return isClipping;
+}
+
+inline float computeLoudness(int16_t* samples, int numSamples) {
+
+ float scale = numSamples ? 1.0f / numSamples : 0.0f;
+
+ int32_t loudness = 0;
+ for (int i = 0; i < numSamples; i++) {
+ loudness += std::abs((int32_t)samples[i]);
+ }
+ return (float)loudness * scale;
+}
+
+template
+static void applyGainSmoothing(float* buffer, int numFrames, float gain0, float gain1) {
+
+ // fast path for unity gain
+ if (gain0 == 1.0f && gain1 == 1.0f) {
+ return;
+ }
+
+ // cubic poly from gain0 to gain1
+ float c3 = -2.0f * (gain1 - gain0);
+ float c2 = 3.0f * (gain1 - gain0);
+ float c0 = gain0;
+
+ float t = 0.0f;
+ float tStep = 1.0f / numFrames;
+
+ for (int i = 0; i < numFrames; i++) {
+
+ // evaluate poly over t=[0,1)
+ float gain = (c3 * t + c2) * t * t + c0;
+ t += tStep;
+
+ // apply gain to all channels
+ for (int ch = 0; ch < NUM_CHANNELS; ch++) {
+ buffer[NUM_CHANNELS*i + ch] *= gain;
+ }
+ }
+}
+
+inline float convertToFloat(int16_t sample) {
+ return static_cast(sample * (1.f / std::numeric_limits::max()));
+}
+
+inline float convertToSignedInt(float sample) {
+ return static_cast(sample * std::numeric_limits::max());
+}
+
+template
+Derived& AudioPacketHandler::derived() {
+ return *static_cast(this);
+}
+
+template
+const Derived& AudioPacketHandler::derived() const {
+ return *static_cast(this);
+}
+
+// FIXME: CRTP this will be needed in audio-client/src/AudioClient
+// QAudioFormat qAudioFormatFrom(AudioFormat format) {
+// QAudioFormat result;
+// result.setSampleRate(format.sampleRate);
+// result.setSampleSize(format.getSampleBits());
+// result.setCodec("audio/pcm");
+// result.setSampleType(format.sampleType == AudioFormat::Signed16 ? QAudioFormat::SignedInt :
+// format.sampleType == AudioFormat::Float ? QAudioFormat::Float :
+// QAudioFormat::Unknown);
+// result.setByteOrder(QAudioFormat::LittleEndian);
+// result.setChannelCount(format.channelCount);
+// return result;
+// }
+
+template
+AudioPacketHandler::AudioPacketHandler() {
+
+ // FIXME: CRTP,
+ // _sessionOutputBufferSizeFrames = _outputBufferSizeFrames.get();
+
+ // set up the desired audio format
+ _desiredInputFormat.sampleRate = AudioConstants::SAMPLE_RATE;
+ _desiredInputFormat.sampleType = AudioFormat::Signed16;
+ _desiredInputFormat.channelCount = AudioConstants::MONO;
+
+ _desiredOutputFormat = _desiredInputFormat;
+ _desiredOutputFormat.channelCount = OUTPUT_CHANNEL_COUNT;
+
+
+ // avoid putting a lock in the device callback
+ assert(_localSamplesAvailable.is_lock_free());
+}
+
+template
+AudioPacketHandler::~AudioPacketHandler() {
+
+ stop();
+
+ if (_codec && _encoder) {
+ _codec->releaseEncoder(_encoder);
+ _encoder = nullptr;
+ }
+
+ if (_dummyAudioInput) {
+ _dummyAudioInput->stop();
+ _dummyAudioInput->deleteLater();
+ _dummyAudioInput = nullptr;
+ }
+}
+
+template
+void AudioPacketHandler::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& receivedCodec) {
+ qCDebug(audioclient) << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "receivedCodec:" << receivedCodec;
+ selectAudioFormat(receivedCodec);
+}
+
+template
+void AudioPacketHandler::reset() {
+ _receivedAudioStream.reset();
+ _stats.reset();
+ _sourceReverb.reset();
+ _listenerReverb.reset();
+ _localReverb.reset();
+}
+
+template
+void AudioPacketHandler::audioMixerKilled() {
+ _hasReceivedFirstPacket = false;
+ _outgoingAvatarAudioSequenceNumber = 0;
+ _stats.reset();
+ // FIXME: CRTP
+ // emit disconnected();
+}
+
+template
+void AudioPacketHandler::setAudioPaused(bool pause) {
+ if (_audioPaused != pause) {
+ _audioPaused = pause;
+
+ if (!_audioPaused) {
+ negotiateAudioFormat();
+ }
+ }
+}
+
+bool sampleChannelConversion(const int16_t* sourceSamples, int16_t* destinationSamples, int numSourceSamples,
+ const int sourceChannelCount, const int destinationChannelCount) {
+ if (sourceChannelCount == 2 && destinationChannelCount == 1) {
+ // loop through the stereo input audio samples and average every two samples
+ for (int i = 0; i < numSourceSamples; i += 2) {
+ destinationSamples[i / 2] = (sourceSamples[i] / 2) + (sourceSamples[i + 1] / 2);
+ }
+
+ return true;
+ } else if (sourceChannelCount == 1 && destinationChannelCount == 2) {
+
+ // loop through the mono input audio and repeat each sample twice
+ for (int i = 0; i < numSourceSamples; ++i) {
+ destinationSamples[i * 2] = destinationSamples[(i * 2) + 1] = sourceSamples[i];
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+int possibleResampling(AudioSRC* resampler,
+ const int16_t* sourceSamples, int16_t* destinationSamples,
+ int numSourceSamples, int maxDestinationSamples,
+ const int sourceChannelCount, const int destinationChannelCount) {
+
+ int numSourceFrames = numSourceSamples / sourceChannelCount;
+ int numDestinationFrames = 0;
+
+ if (numSourceSamples > 0) {
+ if (!resampler) {
+ if (!sampleChannelConversion(sourceSamples, destinationSamples, numSourceSamples,
+ sourceChannelCount, destinationChannelCount)) {
+ // no conversion, we can copy the samples directly across
+ memcpy(destinationSamples, sourceSamples, numSourceSamples * AudioConstants::SAMPLE_SIZE);
+ }
+ numDestinationFrames = numSourceFrames;
+ } else {
+ if (sourceChannelCount != destinationChannelCount) {
+
+ int16_t* channelConversionSamples = new int16_t[numSourceFrames * destinationChannelCount];
+
+ sampleChannelConversion(sourceSamples, channelConversionSamples, numSourceSamples,
+ sourceChannelCount, destinationChannelCount);
+
+ numDestinationFrames = resampler->render(channelConversionSamples, destinationSamples, numSourceFrames);
+
+ delete[] channelConversionSamples;
+ } else {
+ numDestinationFrames = resampler->render(sourceSamples, destinationSamples, numSourceFrames);
+ }
+ }
+ }
+
+ int numDestinationSamples = numDestinationFrames * destinationChannelCount;
+ if (numDestinationSamples > maxDestinationSamples) {
+ qCWarning(audioclient) << "Resampler overflow! numDestinationSamples =" << numDestinationSamples
+ << "but maxDestinationSamples =" << maxDestinationSamples;
+ }
+ return numDestinationSamples;
+}
+
+template
+void AudioPacketHandler::start() {
+ // deprecate legacy settings
+ {
+ Setting::Handle::Deprecated("maxFramesOverDesired", InboundAudioStream::MAX_FRAMES_OVER_DESIRED);
+ Setting::Handle::Deprecated("windowStarveThreshold", InboundAudioStream::WINDOW_STARVE_THRESHOLD);
+ Setting::Handle::Deprecated("windowSecondsForDesiredCalcOnTooManyStarves", InboundAudioStream::WINDOW_SECONDS_FOR_DESIRED_CALC_ON_TOO_MANY_STARVES);
+ Setting::Handle::Deprecated("windowSecondsForDesiredReduction", InboundAudioStream::WINDOW_SECONDS_FOR_DESIRED_REDUCTION);
+ Setting::Handle::Deprecated("useStDevForJitterCalc", InboundAudioStream::USE_STDEV_FOR_JITTER);
+ Setting::Handle::Deprecated("repetitionWithFade", InboundAudioStream::REPETITION_WITH_FADE);
+ }
+
+ derived().connect(&_receivedAudioStream, &MixedProcessedAudioStream::processSamples,
+ &derived(), [this](const QByteArray& decodedBuffer, QByteArray& outputBuffer)
+ { processReceivedSamples(decodedBuffer, outputBuffer); },
+ Qt::DirectConnection);
+
+ // FIXME: CRTP
+ // connect(this, &AudioClient::changeDevice, this, [=](const HifiAudioDeviceInfo& outputDeviceInfo) {
+ // qCDebug(audioclient)<< "got AudioClient::changeDevice signal, about to call switchOutputToAudioDevice() outputDeviceInfo: ["<< outputDeviceInfo.deviceName() << "]";
+ // switchOutputToAudioDevice(outputDeviceInfo);
+ // });
+
+ derived().connect(&_receivedAudioStream, &InboundAudioStream::mismatchedAudioCodec,
+ &derived(), [this](SharedNodePointer node, const QString& currentCodec, const QString& receivedCodec)
+ { handleMismatchAudioFormat(node, currentCodec, receivedCodec); });
+
+ // FIXME: CRTP
+ // initialize wasapi; if getAvailableDevices is called from the CheckDevicesThread before this, it will crash
+ // defaultAudioDeviceName(QAudio::AudioInput);
+ // defaultAudioDeviceName(QAudio::AudioOutput);
+
+ // FIXME: CRTP
+ // start a thread to detect any device changes
+ // _checkDevicesTimer = new QTimer(this);
+ // const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000;
+ // connect(_checkDevicesTimer, &QTimer::timeout, this, [=] {
+ // QtConcurrent::run(QThreadPool::globalInstance(), [=] {
+ // checkDevices();
+ // // On some systems (Ubuntu) checking all the audio devices can take more than 2 seconds. To
+ // // avoid consuming all of the thread pool, don't start the check interval until the previous
+ // // check has completed.
+ // QMetaObject::invokeMethod(_checkDevicesTimer, "start", Q_ARG(int, DEVICE_CHECK_INTERVAL_MSECS));
+ // });
+ // });
+ // _checkDevicesTimer->setSingleShot(true);
+ // _checkDevicesTimer->start(DEVICE_CHECK_INTERVAL_MSECS);
+
+ // FIXME: CRTP
+ // start a thread to detect peak value changes
+ // _checkPeakValuesTimer = new QTimer(this);
+ // connect(_checkPeakValuesTimer, &QTimer::timeout, this, [this] {
+ // QtConcurrent::run(QThreadPool::globalInstance(), [this] { checkPeakValues(); });
+ // });
+ // const unsigned long PEAK_VALUES_CHECK_INTERVAL_MSECS = 50;
+ // _checkPeakValuesTimer->start(PEAK_VALUES_CHECK_INTERVAL_MSECS);
+ //
+ // configureReverb();
+
+#if defined(WEBRTC_AUDIO)
+ configureWebrtc();
+#endif
+
+ auto nodeList = DependencyManager::get();
+ auto& packetReceiver = nodeList->getPacketReceiver();
+ packetReceiver.registerListener(PacketType::AudioStreamStats,
+ PacketReceiver::makeSourcedListenerReference(&_stats, &AudioIOStats::processStreamStatsPacket));
+ packetReceiver.registerListener(PacketType::AudioEnvironment,
+ PacketReceiver::makeUnsourcedListenerReference(&derived(), &AudioPacketHandler::handleAudioEnvironmentDataPacket));
+ packetReceiver.registerListener(PacketType::SilentAudioFrame,
+ PacketReceiver::makeUnsourcedListenerReference(&derived(), &AudioPacketHandler::handleAudioDataPacket));
+ packetReceiver.registerListener(PacketType::MixedAudio,
+ PacketReceiver::makeUnsourcedListenerReference(&derived(), &AudioPacketHandler::handleAudioDataPacket));
+ packetReceiver.registerListener(PacketType::NoisyMute,
+ PacketReceiver::makeUnsourcedListenerReference(&derived(), &AudioPacketHandler::handleNoisyMutePacket));
+ packetReceiver.registerListener(PacketType::MuteEnvironment,
+ PacketReceiver::makeUnsourcedListenerReference(&derived(), &AudioPacketHandler::handleMuteEnvironmentPacket));
+ packetReceiver.registerListener(PacketType::SelectedAudioFormat,
+ PacketReceiver::makeUnsourcedListenerReference(&derived(), &AudioPacketHandler::handleSelectedAudioFormat));
+
+ auto& domainHandler = nodeList->getDomainHandler();
+ derived().connect(&domainHandler, &DomainHandler::disconnectedFromDomain, &derived(), [this] {
+ _solo.reset();
+ });
+ derived().connect(nodeList.data(), &NodeList::nodeActivated, &derived(), [this](SharedNodePointer node) {
+ if (node->getType() == NodeType::AudioMixer) {
+ _solo.resend();
+ negotiateAudioFormat();
+ }
+ });
+
+ //initialize input to the dummy device to prevent starves
+ cleanupInput();
+ setupDummyInput();
+
+ derived().onStart();
+
+// FIXME: CRTP
+// switchOutputToAudioDevice(defaultAudioDeviceForMode(QAudio::AudioOutput, QString()));
+// #if defined(Q_OS_ANDROID)
+// connect(&_checkInputTimer, &QTimer::timeout, this, &AudioClient::checkInputTimeout);
+// _checkInputTimer.start(CHECK_INPUT_READS_MSECS);
+// #endif
+}
+
+template
+void AudioPacketHandler::stop() {
+ qCDebug(audioclient) << "AudioPacketHandler::stop(), requesting audio input device to shut down";
+ cleanupInput();
+ qCDebug(audioclient) << "The audio input device has shut down.";
+
+
+ qCDebug(audioclient) << "AudioPacketHandler::stop(), requesting audio output device to shut down";
+ cleanupOutput();
+ qCDebug(audioclient) << "The audio output device has shut down.";
+
+ // FIXME: CRTP
+ // Stop triggering the checks
+ // QObject::disconnect(_checkPeakValuesTimer, &QTimer::timeout, nullptr, nullptr);
+ // QObject::disconnect(_checkDevicesTimer, &QTimer::timeout, nullptr, nullptr);
+ // Destruction of the pointers will occur when the parent object (this) is destroyed)
+ // {
+ // Lock lock(_checkDevicesMutex);
+ // _checkDevicesTimer->stop();
+ // _checkDevicesTimer = nullptr;
+ // }
+ // {
+ // Lock lock(_checkPeakValuesMutex);
+ // _checkPeakValuesTimer = nullptr;
+ // }
+
+// FIXME: CRTP
+// #if defined(Q_OS_ANDROID)
+// _checkInputTimer.stop();
+// disconnect(&_checkInputTimer, &QTimer::timeout, 0, 0);
+// #endif
+}
+
+template
+void AudioPacketHandler::handleAudioEnvironmentDataPacket(QSharedPointer message) {
+ char bitset;
+ message->readPrimitive(&bitset);
+
+ bool hasReverb = oneAtBit(bitset, HAS_REVERB_BIT);
+
+ if (hasReverb) {
+ float reverbTime, wetLevel;
+ message->readPrimitive(&reverbTime);
+ message->readPrimitive(&wetLevel);
+ _receivedAudioStream.setReverb(reverbTime, wetLevel);
+ } else {
+ _receivedAudioStream.clearReverb();
+ }
+}
+
+template
+void AudioPacketHandler::handleAudioDataPacket(QSharedPointer message) {
+ if (message->getType() == PacketType::SilentAudioFrame) {
+ _silentInbound.increment();
+ } else {
+ _audioInbound.increment();
+ }
+
+ auto nodeList = DependencyManager::get();
+ nodeList->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveFirstAudioPacket);
+
+ if (_audioOutputInitialized.load(std::memory_order_acquire)) {
+
+ if (!_hasReceivedFirstPacket) {
+ _hasReceivedFirstPacket = true;
+
+ // have the audio scripting interface emit a signal to say we just connected to mixer
+ // FIXME: CRTP
+ // emit receivedFirstPacket();
+ }
+
+#if DEV_BUILD || PR_BUILD
+ _gate.insert(message);
+#else
+ // Audio output must exist and be correctly set up if we're going to process received audio
+ _receivedAudioStream.parseData(*message);
+#endif
+ }
+}
+
+template
+AudioPacketHandler::Gate::Gate(AudioPacketHandler* audioClient) :
+ _audioClient(audioClient) {}
+
+template
+void AudioPacketHandler::Gate::setIsSimulatingJitter(bool enable) {
+ std::lock_guard lock(_mutex);
+ flush();
+ _isSimulatingJitter = enable;
+}
+
+template
+void AudioPacketHandler::Gate::setThreshold(int threshold) {
+ std::lock_guard lock(_mutex);
+ flush();
+ _threshold = std::max(threshold, 1);
+}
+
+template
+void AudioPacketHandler::Gate::insert(QSharedPointer message) {
+ std::lock_guard lock(_mutex);
+
+ // Short-circuit for normal behavior
+ if (_threshold == 1 && !_isSimulatingJitter) {
+ _audioClient->_receivedAudioStream.parseData(*message);
+ return;
+ }
+
+ // Throttle the current packet until the next flush
+ _queue.push(message);
+ _index++;
+
+ // When appropriate, flush all held packets to the received audio stream
+ if (_isSimulatingJitter) {
+ // The JITTER_FLUSH_CHANCE defines the discrete probability density function of jitter (ms),
+ // where f(t) = pow(1 - JITTER_FLUSH_CHANCE, (t / 10) * JITTER_FLUSH_CHANCE
+ // for t (ms) = 10, 20, ... (because typical packet timegap is 10ms),
+ // because there is a JITTER_FLUSH_CHANCE of any packet instigating a flush of all held packets.
+ static const float JITTER_FLUSH_CHANCE = 0.6f;
+ // It is set at 0.6 to give a low chance of spikes (>30ms, 2.56%) so that they are obvious,
+ // but settled within the measured 5s window in audio network stats.
+ if (randFloat() < JITTER_FLUSH_CHANCE) {
+ flush();
+ }
+ } else if (!(_index % _threshold)) {
+ flush();
+ }
+}
+
+template
+void AudioPacketHandler::Gate::flush() {
+ // Send all held packets to the received audio stream to be (eventually) played
+ while (!_queue.empty()) {
+ _audioClient->_receivedAudioStream.parseData(*_queue.front());
+ _queue.pop();
+ }
+ _index = 0;
+}
+
+
+template
+void AudioPacketHandler::handleNoisyMutePacket(QSharedPointer message) {
+ if (!_isMuted) {
+ setMuted(true);
+
+ derived().onMutedByMixer();
+ // FIXME: CRTP
+ // have the audio scripting interface emit a signal to say we were muted by the mixer
+ // emit mutedByMixer();
+ }
+}
+
+template
+void AudioPacketHandler::handleMuteEnvironmentPacket(QSharedPointer message) {
+ glm::vec3 position;
+ float radius;
+
+ message->readPrimitive(&position);
+ message->readPrimitive(&radius);
+
+ // FIXME: CRTP
+ // emit muteEnvironmentRequested(position, radius);
+}
+
+template
+void AudioPacketHandler::negotiateAudioFormat() {
+ auto nodeList = DependencyManager::get();
+ auto negotiateFormatPacket = NLPacket::create(PacketType::NegotiateAudioFormat);
+ const auto& codecs = derived().getSupportedCodecs();
+ // FIXME: CRTP
+ // const auto& codecPlugins = PluginManager::getInstance()->getCodecPlugins();
+ quint8 numberOfCodecs = (quint8)codecs.size();
+ negotiateFormatPacket->writePrimitive(numberOfCodecs);
+ for (const auto& codec : codecs) {
+ auto codecName = codec->getName();
+ negotiateFormatPacket->writeString(codecName);
+ }
+
+ // grab our audio mixer from the NodeList, if it exists
+ SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer);
+
+ if (audioMixer) {
+ // send off this mute packet
+ nodeList->sendPacket(std::move(negotiateFormatPacket), *audioMixer);
+ }
+}
+
+template
+void AudioPacketHandler::handleSelectedAudioFormat(QSharedPointer message) {
+ QString selectedCodecName = message->readString();
+ selectAudioFormat(selectedCodecName);
+}
+
+template
+void AudioPacketHandler::selectAudioFormat(const QString& selectedCodecName) {
+
+ _selectedCodecName = selectedCodecName;
+
+ qCDebug(audioclient) << "Selected codec:" << _selectedCodecName << "; Is stereo input:" << (_desiredInputFormat.channelCount == AudioConstants::STEREO);
+
+ // release any old codec encoder/decoder first...
+ if (_codec && _encoder) {
+ _codec->releaseEncoder(_encoder);
+ _encoder = nullptr;
+ _codec = nullptr;
+ }
+ _receivedAudioStream.cleanupCodec();
+
+ const auto& codecs = derived().getSupportedCodecs();
+ // FIXME: CRTP
+ // const auto& codecPlugins = PluginManager::getInstance()->getCodecPlugins();
+ for (const auto& codec : codecs) {
+ if (_selectedCodecName == codec->getName()) {
+ _codec = codec;
+ _receivedAudioStream.setupCodec(codec, _selectedCodecName, AudioConstants::STEREO);
+ _encoder = codec->createEncoder(_desiredInputFormat.sampleRate, _desiredInputFormat.channelCount);
+ qCDebug(audioclient) << "Selected codec:" << _codec.get();
+ break;
+ }
+ }
+
+}
+
+template
+void AudioPacketHandler::configureReverb() {
+ ReverbParameters p;
+
+ p.sampleRate = AudioConstants::SAMPLE_RATE;
+ p.bandwidth = _reverbOptions->getBandwidth();
+ p.preDelay = _reverbOptions->getPreDelay();
+ p.lateDelay = _reverbOptions->getLateDelay();
+ p.reverbTime = _reverbOptions->getReverbTime();
+ p.earlyDiffusion = _reverbOptions->getEarlyDiffusion();
+ p.lateDiffusion = _reverbOptions->getLateDiffusion();
+ p.roomSize = _reverbOptions->getRoomSize();
+ p.density = _reverbOptions->getDensity();
+ p.bassMult = _reverbOptions->getBassMult();
+ p.bassFreq = _reverbOptions->getBassFreq();
+ p.highGain = _reverbOptions->getHighGain();
+ p.highFreq = _reverbOptions->getHighFreq();
+ p.modRate = _reverbOptions->getModRate();
+ p.modDepth = _reverbOptions->getModDepth();
+ p.earlyGain = _reverbOptions->getEarlyGain();
+ p.lateGain = _reverbOptions->getLateGain();
+ p.earlyMixLeft = _reverbOptions->getEarlyMixLeft();
+ p.earlyMixRight = _reverbOptions->getEarlyMixRight();
+ p.lateMixLeft = _reverbOptions->getLateMixLeft();
+ p.lateMixRight = _reverbOptions->getLateMixRight();
+ p.wetDryMix = _reverbOptions->getWetDryMix();
+
+ _listenerReverb.setParameters(&p);
+ _localReverb.setParameters(&p);
+
+ // used only for adding self-reverb to loopback audio
+ p.sampleRate = _outputFormat.sampleRate;
+ p.wetDryMix = 100.0f;
+ p.preDelay = 0.0f;
+ p.earlyGain = -96.0f; // disable ER
+ p.lateGain += _reverbOptions->getWetDryMix() * (24.0f / 100.0f) - 24.0f; // -0dB to -24dB, based on wetDryMix
+ p.lateMixLeft = 0.0f;
+ p.lateMixRight = 0.0f;
+
+ _sourceReverb.setParameters(&p);
+}
+
+template
+void AudioPacketHandler::updateReverbOptions() {
+ bool reverbChanged = false;
+ if (_receivedAudioStream.hasReverb()) {
+
+ if (_zoneReverbOptions.getReverbTime() != _receivedAudioStream.getRevebTime()) {
+ _zoneReverbOptions.setReverbTime(_receivedAudioStream.getRevebTime());
+ reverbChanged = true;
+ }
+ if (_zoneReverbOptions.getWetDryMix() != _receivedAudioStream.getWetLevel()) {
+ _zoneReverbOptions.setWetDryMix(_receivedAudioStream.getWetLevel());
+ reverbChanged = true;
+ }
+
+ if (_reverbOptions != &_zoneReverbOptions) {
+ _reverbOptions = &_zoneReverbOptions;
+ reverbChanged = true;
+ }
+ } else if (_reverbOptions != &_scriptReverbOptions) {
+ _reverbOptions = &_scriptReverbOptions;
+ reverbChanged = true;
+ }
+
+ if (reverbChanged) {
+ configureReverb();
+ }
+}
+
+template
+void AudioPacketHandler::setReverb(bool reverb) {
+ _reverb = reverb;
+
+ if (!_reverb) {
+ _sourceReverb.reset();
+ _listenerReverb.reset();
+ _localReverb.reset();
+ }
+}
+
+template
+void AudioPacketHandler::setReverbOptions(const AudioEffectOptions* options) {
+ // Save the new options
+ _scriptReverbOptions.setBandwidth(options->getBandwidth());
+ _scriptReverbOptions.setPreDelay(options->getPreDelay());
+ _scriptReverbOptions.setLateDelay(options->getLateDelay());
+ _scriptReverbOptions.setReverbTime(options->getReverbTime());
+ _scriptReverbOptions.setEarlyDiffusion(options->getEarlyDiffusion());
+ _scriptReverbOptions.setLateDiffusion(options->getLateDiffusion());
+ _scriptReverbOptions.setRoomSize(options->getRoomSize());
+ _scriptReverbOptions.setDensity(options->getDensity());
+ _scriptReverbOptions.setBassMult(options->getBassMult());
+ _scriptReverbOptions.setBassFreq(options->getBassFreq());
+ _scriptReverbOptions.setHighGain(options->getHighGain());
+ _scriptReverbOptions.setHighFreq(options->getHighFreq());
+ _scriptReverbOptions.setModRate(options->getModRate());
+ _scriptReverbOptions.setModDepth(options->getModDepth());
+ _scriptReverbOptions.setEarlyGain(options->getEarlyGain());
+ _scriptReverbOptions.setLateGain(options->getLateGain());
+ _scriptReverbOptions.setEarlyMixLeft(options->getEarlyMixLeft());
+ _scriptReverbOptions.setEarlyMixRight(options->getEarlyMixRight());
+ _scriptReverbOptions.setLateMixLeft(options->getLateMixLeft());
+ _scriptReverbOptions.setLateMixRight(options->getLateMixRight());
+ _scriptReverbOptions.setWetDryMix(options->getWetDryMix());
+
+ if (_reverbOptions == &_scriptReverbOptions) {
+ // Apply them to the reverb instances
+ configureReverb();
+ }
+}
+
+#if defined(WEBRTC_AUDIO)
+
+static void deinterleaveToFloat(const int16_t* src, float* const* dst, int numFrames, int numChannels) {
+ for (int i = 0; i < numFrames; i++) {
+ for (int ch = 0; ch < numChannels; ch++) {
+ float f = *src++;
+ f *= (1/32768.0f); // scale
+ dst[ch][i] = f; // deinterleave
+ }
+ }
+}
+
+static void interleaveToInt16(const float* const* src, int16_t* dst, int numFrames, int numChannels) {
+ for (int i = 0; i < numFrames; i++) {
+ for (int ch = 0; ch < numChannels; ch++) {
+ float f = src[ch][i];
+ f *= 32768.0f; // scale
+ f += (f < 0.0f) ? -0.5f : 0.5f; // round
+ f = std::max(std::min(f, 32767.0f), -32768.0f); // saturate
+ *dst++ = (int16_t)f; // interleave
+ }
+ }
+}
+
+template
+void AudioPacketHandler::configureWebrtc() {
+ _apm = webrtc::AudioProcessingBuilder().Create();
+
+ webrtc::AudioProcessing::Config config;
+
+ config.pre_amplifier.enabled = false;
+ config.high_pass_filter.enabled = false;
+ config.echo_canceller.enabled = true;
+ config.echo_canceller.mobile_mode = false;
+#if defined(WEBRTC_LEGACY)
+ config.echo_canceller.use_legacy_aec = false;
+#endif
+ config.noise_suppression.enabled = false;
+ config.noise_suppression.level = webrtc::AudioProcessing::Config::NoiseSuppression::kModerate;
+ config.voice_detection.enabled = false;
+ config.gain_controller1.enabled = false;
+ config.gain_controller2.enabled = false;
+ config.gain_controller2.fixed_digital.gain_db = 0.0f;
+ config.gain_controller2.adaptive_digital.enabled = false;
+ config.residual_echo_detector.enabled = true;
+ config.level_estimation.enabled = false;
+
+ _apm->ApplyConfig(config);
+}
+
+// rebuffer into 10ms chunks
+template
+void AudioPacketHandler::processWebrtcFarEnd(const int16_t* samples, int numFrames, int numChannels, int sampleRate) {
+
+ const webrtc::StreamConfig streamConfig = webrtc::StreamConfig(sampleRate, numChannels);
+ const int numChunk = (int)streamConfig.num_frames();
+
+ static int32_t lastWarningHash = 0;
+ if (sampleRate > WEBRTC_SAMPLE_RATE_MAX || numChannels > WEBRTC_CHANNELS_MAX) {
+ if (lastWarningHash != ((sampleRate << 8) | numChannels)) {
+ lastWarningHash = ((sampleRate << 8) | numChannels);
+ qCWarning(audioclient) << "AEC not unsupported for output format: sampleRate =" << sampleRate << "numChannels =" << numChannels;
+ }
+ return;
+ }
+
+ while (numFrames > 0) {
+
+ // number of frames to fill
+ int numFill = std::min(numFrames, numChunk - _numFifoFarEnd);
+
+ // refill fifo
+ memcpy(&_fifoFarEnd[_numFifoFarEnd], samples, numFill * numChannels * sizeof(int16_t));
+ samples += numFill * numChannels;
+ numFrames -= numFill;
+ _numFifoFarEnd += numFill;
+
+ if (_numFifoFarEnd == numChunk) {
+
+ // convert audio format
+ float buffer[WEBRTC_CHANNELS_MAX][WEBRTC_FRAMES_MAX];
+ float* const buffers[WEBRTC_CHANNELS_MAX] = { buffer[0], buffer[1] };
+ deinterleaveToFloat(_fifoFarEnd, buffers, numChunk, numChannels);
+
+ // process one chunk
+ int error = _apm->ProcessReverseStream(buffers, streamConfig, streamConfig, buffers);
+ if (error != _apm->kNoError) {
+ qCWarning(audioclient) << "WebRTC ProcessReverseStream() returned ERROR:" << error;
+ }
+ _numFifoFarEnd = 0;
+ }
+ }
+}
+
+template
+void AudioPacketHandler::processWebrtcNearEnd(int16_t* samples, int numFrames, int numChannels, int sampleRate) {
+
+ const webrtc::StreamConfig streamConfig = webrtc::StreamConfig(sampleRate, numChannels);
+ assert(numFrames == (int)streamConfig.num_frames()); // WebRTC requires exactly 10ms of input
+
+ static int32_t lastWarningHash = 0;
+ if (sampleRate > WEBRTC_SAMPLE_RATE_MAX || numChannels > WEBRTC_CHANNELS_MAX) {
+ if (lastWarningHash != ((sampleRate << 8) | numChannels)) {
+ lastWarningHash = ((sampleRate << 8) | numChannels);
+ qCWarning(audioclient) << "AEC not unsupported for input format: sampleRate =" << sampleRate << "numChannels =" << numChannels;
+ }
+ return;
+ }
+
+ // convert audio format
+ float buffer[WEBRTC_CHANNELS_MAX][WEBRTC_FRAMES_MAX];
+ float* const buffers[WEBRTC_CHANNELS_MAX] = { buffer[0], buffer[1] };
+ deinterleaveToFloat(samples, buffers, numFrames, numChannels);
+
+ // process one chunk
+ int error = _apm->ProcessStream(buffers, streamConfig, streamConfig, buffers);
+ if (error != _apm->kNoError) {
+ qCWarning(audioclient) << "WebRTC ProcessStream() returned ERROR:" << error;
+ } else {
+ // modify samples in-place
+ interleaveToInt16(buffers, samples, numFrames, numChannels);
+ }
+}
+
+#endif // WEBRTC_AUDIO
+
+template
+float AudioPacketHandler::loudnessToLevel(float loudness) {
+ float level = loudness * (1 / 32768.0f); // level in [0, 1]
+ level = 6.02059991f * fastLog2f(level); // convert to dBFS
+ level = (level + 48.0f) * (1 / 42.0f); // map [-48, -6] dBFS to [0, 1]
+ return glm::clamp(level, 0.0f, 1.0f);
+}
+
+template
+void AudioPacketHandler::handleAudioInput(QByteArray& audioBuffer) {
+ if (!_audioPaused) {
+
+ bool audioGateOpen = false;
+
+ if (!_isMuted) {
+ int16_t* samples = reinterpret_cast(audioBuffer.data());
+ int numSamples = audioBuffer.size() / AudioConstants::SAMPLE_SIZE;
+ int numFrames = numSamples / _desiredInputFormat.channelCount;
+
+ if (_isNoiseGateEnabled && _isNoiseReductionAutomatic) {
+ // The audio gate includes DC removal
+ audioGateOpen = _audioGate->render(samples, samples, numFrames);
+ } else if (_isNoiseGateEnabled && !_isNoiseReductionAutomatic &&
+ loudnessToLevel(_lastSmoothedRawInputLoudness) >= _noiseReductionThreshold) {
+ audioGateOpen = _audioGate->removeDC(samples, samples, numFrames);
+ } else if (_isNoiseGateEnabled && !_isNoiseReductionAutomatic) {
+ audioGateOpen = false;
+ } else {
+ audioGateOpen = _audioGate->removeDC(samples, samples, numFrames);
+ }
+
+ // FIXME: CRTP
+ // emit inputReceived(audioBuffer);
+ }
+
+ // loudness after mute/gate
+ _lastInputLoudness = (_isMuted || !audioGateOpen) ? 0.0f : _lastRawInputLoudness;
+
+ // detect gate opening and closing
+ bool openedInLastBlock = !_audioGateOpen && audioGateOpen; // the gate just opened
+ bool closedInLastBlock = _audioGateOpen && !audioGateOpen; // the gate just closed
+ _audioGateOpen = audioGateOpen;
+
+ if (openedInLastBlock) {
+ //FIXME: CRTP
+ // emit noiseGateOpened();
+ } else if (closedInLastBlock) {
+ //FIXME: CRTP
+ // emit noiseGateClosed();
+ }
+
+ // the codec must be flushed to silence before sending silent packets,
+ // so delay the transition to silent packets by one packet after becoming silent.
+ auto packetType = _shouldEchoToServer ? PacketType::MicrophoneAudioWithEcho : PacketType::MicrophoneAudioNoEcho;
+ if (!audioGateOpen && !closedInLastBlock) {
+ packetType = PacketType::SilentAudioFrame;
+ _silentOutbound.increment();
+ } else {
+ _audioOutbound.increment();
+ }
+
+ QByteArray encodedBuffer;
+ if (_encoder) {
+ _encoder->encode(audioBuffer, encodedBuffer);
+ } else {
+ encodedBuffer = audioBuffer;
+ }
+
+ AbstractAudioInterface::emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, _desiredInputFormat.channelCount == AudioConstants::STEREO,
+ {_positionGetter(), _orientationGetter()}, avatarBoundingBoxCorner, avatarBoundingBoxScale,
+ packetType, _selectedCodecName);
+ _stats.sentPacket();
+ }
+}
+
+// CRTP: replaced libraries/audio-client/AudioClient::handleMicAudioInput
+// void AudioClient::handleMicAudioInput() {
+// if (!_inputDevice || _isPlayingBackRecording) {
+// return;
+// }
+// QByteArray inputByteArray = _inputDevice->readAll();
+//
+// #if defined(Q_OS_ANDROID)
+// _inputReadsSinceLastCheck++;
+// #endif
+//
+// handleLocalEchoAndReverb(inputByteArray);
+//
+// handleMicAudioInput(inputByteArray.data(), inputByteArray.size());
+// sendInput();
+// }
+
+template
+void AudioPacketHandler::handleMicAudioInput(const char* data, int size) {
+
+ const auto sampleBytes = _inputFormat.getSampleSize();
+ const auto trailingBytes = size % sampleBytes;
+ if (trailingBytes != 0) {
+ qCWarning(audioclient) << "Input buffer trailing bytes: " << trailingBytes;
+ size -= trailingBytes; // we ignore trailing bytes
+ }
+
+ if (_inputFormat.sampleType == AudioFormat::Float) {
+ _inputTypeConversionBuffer.resize(size / sizeof(float));
+ const auto floats = reinterpret_cast(data);
+ for (int i = 0; i < (int)_inputTypeConversionBuffer.size(); i+= _inputFormat.channelCount) {
+ for (int j = 0; j < _inputFormat.channelCount; ++j) {
+ _inputTypeConversionBuffer[i+j] = convertToSignedInt(floats[i+j]);
+ }
+ }
+
+ data = reinterpret_cast(_inputTypeConversionBuffer.data());
+ size = _inputTypeConversionBuffer.size() * sizeof(int16_t);
+ }
+
+ _inputRingBuffer.writeData(data, size);
+
+ float audioInputMsecsRead = size / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC));
+ _stats.updateInputMsRead(audioInputMsecsRead);
+}
+
+template
+void AudioPacketHandler::sendInput() {
+ const int numNetworkBytes = _desiredInputFormat.channelCount == AudioConstants::STEREO
+ ? AudioConstants::NETWORK_FRAME_BYTES_STEREO
+ : AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL;
+ const int numNetworkSamples = _desiredInputFormat.channelCount == AudioConstants::STEREO
+ ? AudioConstants::NETWORK_FRAME_SAMPLES_STEREO
+ : AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL;
+
+ // input samples required to produce exactly NETWORK_FRAME_SAMPLES of output
+ const int inputSamplesRequired = (_inputToNetworkResampler ?
+ _inputToNetworkResampler->getMinInput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) :
+ AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * _inputFormat.channelCount;
+
+ static int16_t networkAudioSamples[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
+ while (_inputRingBuffer.samplesAvailable() >= inputSamplesRequired) {
+
+ // TODO: this should at least be an std::vector to avoid
+ // unnecessary re-allocations, and better resized in
+ // setupInput() not here.
+ const auto inputAudioSamples = std::unique_ptr(new int16_t[inputSamplesRequired]);
+
+ _inputRingBuffer.readSamples(inputAudioSamples.get(), inputSamplesRequired);
+
+ // detect clipping on the raw input
+ bool isClipping = detectClipping(inputAudioSamples.get(), inputSamplesRequired, _inputFormat.channelCount);
+ if (isClipping) {
+ _timeSinceLastClip = 0.0f;
+ } else if (_timeSinceLastClip >= 0.0f) {
+ _timeSinceLastClip += AudioConstants::NETWORK_FRAME_SECS;
+ }
+ isClipping = (_timeSinceLastClip >= 0.0f) && (_timeSinceLastClip < 2.0f); // 2 second hold time
+
+#if defined(WEBRTC_AUDIO)
+ if (_isAECEnabled) {
+ processWebrtcNearEnd(inputAudioSamples.get(), inputSamplesRequired / _inputFormat.channelCount,
+ _inputFormat.channelCount, _inputFormat.sampleRate);
+ }
+#endif
+
+ float loudness = computeLoudness(inputAudioSamples.get(), inputSamplesRequired);
+ _lastRawInputLoudness = loudness;
+
+ // envelope detection
+ float tc = (loudness > _lastSmoothedRawInputLoudness) ? 0.378f : 0.967f; // 10ms attack, 300ms release @ 100Hz
+ loudness += tc * (_lastSmoothedRawInputLoudness - loudness);
+ _lastSmoothedRawInputLoudness = loudness;
+
+ // FIXME: CRTP
+ // emit inputLoudnessChanged(_lastSmoothedRawInputLoudness, isClipping);
+
+ if (!_isMuted) {
+ possibleResampling(_inputToNetworkResampler,
+ inputAudioSamples.get(), networkAudioSamples,
+ inputSamplesRequired, numNetworkSamples,
+ _inputFormat.channelCount, _desiredInputFormat.channelCount);
+ }
+ int bytesInInputRingBuffer = _inputRingBuffer.samplesAvailable() * AudioConstants::SAMPLE_SIZE;
+ float msecsInInputRingBuffer = bytesInInputRingBuffer / (float)(_inputFormat.bytesForDuration(USECS_PER_MSEC));
+ _stats.updateInputMsUnplayed(msecsInInputRingBuffer);
+
+ QByteArray audioBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes);
+ handleAudioInput(audioBuffer);
+ }
+}
+
+template
+void AudioPacketHandler::handleRecordedAudioInput(const QByteArray& audio) {
+ QByteArray audioBuffer(audio);
+ handleAudioInput(audioBuffer);
+}
+
+template
+void AudioPacketHandler::prepareLocalAudioInjectors(std::unique_ptr localAudioLock) {
+ bool doSynchronously = localAudioLock.operator bool();
+ if (!localAudioLock) {
+ localAudioLock.reset(new Lock(_localAudioMutex));
+ }
+
+ int samplesNeeded = std::numeric_limits::max();
+ while (samplesNeeded > 0) {
+ if (!doSynchronously) {
+ // unlock between every write to allow device switching
+ localAudioLock->unlock();
+ localAudioLock->lock();
+ }
+
+ // in case of a device switch, consider bufferCapacity volatile across iterations
+ if (_outputPeriod == 0) {
+ return;
+ }
+
+ int bufferCapacity = _localInjectorsStream.getSampleCapacity();
+ int maxOutputSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * AudioConstants::STEREO;
+ if (_localToOutputResampler) {
+ maxOutputSamples =
+ _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) *
+ AudioConstants::STEREO;
+ }
+
+ samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed);
+ if (samplesNeeded < maxOutputSamples) {
+ // avoid overwriting the buffer to prevent losing frames
+ break;
+ }
+
+ // get a network frame of local injectors' audio
+ if (!mixLocalAudioInjectors(_localMixBuffer)) {
+ break;
+ }
+
+ // reverb
+ if (_reverb) {
+ _localReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
+ }
+
+ int samples;
+ if (_localToOutputResampler) {
+ // resample to output sample rate
+ int frames = _localToOutputResampler->render(_localMixBuffer, _localOutputMixBuffer,
+ AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
+
+ // write to local injectors' ring buffer
+ samples = frames * AudioConstants::STEREO;
+ _localInjectorsStream.writeSamples(_localOutputMixBuffer, samples);
+
+ } else {
+ // write to local injectors' ring buffer
+ samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO;
+ _localInjectorsStream.writeSamples(_localMixBuffer,
+ AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
+ }
+
+ _localSamplesAvailable.fetch_add(samples, std::memory_order_release);
+ samplesNeeded -= samples;
+ }
+}
+
+template
+bool AudioPacketHandler::mixLocalAudioInjectors(float* mixBuffer) {
+ // check the flag for injectors before attempting to lock
+ if (!_localInjectorsAvailable.load(std::memory_order_acquire)) {
+ return false;
+ }
+
+ // lock the injectors
+ Lock lock(_injectorsMutex);
+
+ QVector injectorsToRemove;
+
+ memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float));
+
+ for (const AudioInjectorPointer& injector : _activeLocalAudioInjectors) {
+ // the lock guarantees that injectorBuffer, if found, is invariant
+ auto injectorBuffer = injector->getLocalBuffer();
+ if (injectorBuffer) {
+
+ auto options = injector->getOptions();
+
+ static const int HRTF_DATASET_INDEX = 1;
+
+ int numChannels = options.ambisonic ? AudioConstants::AMBISONIC : (options.stereo ? AudioConstants::STEREO : AudioConstants::MONO);
+ size_t bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL;
+
+ // get one frame from the injector
+ memset(_localScratchBuffer, 0, bytesToRead);
+ if (0 < injectorBuffer->readData((char*)_localScratchBuffer, bytesToRead)) {
+
+ bool isSystemSound = !options.positionSet && !options.ambisonic;
+
+ float gain = options.volume * (isSystemSound ? _systemInjectorGain : _localInjectorGain);
+
+ if (options.ambisonic) {
+
+ if (options.positionSet) {
+
+ // distance attenuation
+ glm::vec3 relativePosition = options.position - _positionGetter();
+ float distance = glm::max(glm::length(relativePosition), EPSILON);
+ gain = gainForSource(distance, gain);
+ }
+
+ //
+ // Calculate the soundfield orientation relative to the listener.
+ // Injector orientation can be used to align a recording to our world coordinates.
+ //
+ glm::quat relativeOrientation = options.orientation * glm::inverse(_orientationGetter());
+
+ // convert from Y-up (OpenGL) to Z-up (Ambisonic) coordinate system
+ float qw = relativeOrientation.w;
+ float qx = -relativeOrientation.z;
+ float qy = -relativeOrientation.x;
+ float qz = relativeOrientation.y;
+
+ // spatialize into mixBuffer
+ injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX,
+ qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
+ } else if (options.stereo) {
+
+ if (options.positionSet) {
+
+ // distance attenuation
+ glm::vec3 relativePosition = options.position - _positionGetter();
+ float distance = glm::max(glm::length(relativePosition), EPSILON);
+ gain = gainForSource(distance, gain);
+ }
+
+ // direct mix into mixBuffer
+ injector->getLocalHRTF().mixStereo(_localScratchBuffer, mixBuffer, gain,
+ AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
+ } else { // injector is mono
+
+ if (options.positionSet) {
+
+ // distance attenuation
+ glm::vec3 relativePosition = options.position - _positionGetter();
+ float distance = glm::max(glm::length(relativePosition), EPSILON);
+ gain = gainForSource(distance, gain);
+
+ float azimuth = azimuthForSource(relativePosition);
+
+ // spatialize into mixBuffer
+ injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX,
+ azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
+ } else {
+
+ // direct mix into mixBuffer
+ injector->getLocalHRTF().mixMono(_localScratchBuffer, mixBuffer, gain,
+ AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
+ }
+ }
+
+ } else {
+
+ //qCDebug(audioclient) << "injector has no more data, marking finished for removal";
+ injector->finishLocalInjection();
+ injectorsToRemove.append(injector);
+ }
+
+ } else {
+
+ //qCDebug(audioclient) << "injector has no local buffer, marking as finished for removal";
+ injector->finishLocalInjection();
+ injectorsToRemove.append(injector);
+ }
+ }
+
+ for (const AudioInjectorPointer& injector : injectorsToRemove) {
+ //qCDebug(audioclient) << "removing injector";
+ _activeLocalAudioInjectors.removeOne(injector);
+ }
+
+ // update the flag
+ _localInjectorsAvailable.exchange(!_activeLocalAudioInjectors.empty(), std::memory_order_release);
+
+ return true;
+}
+
+template
+void AudioPacketHandler::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) {
+
+ const int16_t* decodedSamples = reinterpret_cast(decodedBuffer.data());
+ assert(decodedBuffer.size() == AudioConstants::NETWORK_FRAME_BYTES_STEREO);
+
+ outputBuffer.resize(_outputFrameSize * AudioConstants::SAMPLE_SIZE);
+ int16_t* outputSamples = reinterpret_cast