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(outputBuffer.data()); + + bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); + + // apply stereo reverb + if (hasReverb) { + updateReverbOptions(); + int16_t* reverbSamples = _networkToOutputResampler ? _networkScratchBuffer : outputSamples; + _listenerReverb.render(decodedSamples, reverbSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } + + // resample to output sample rate + if (_networkToOutputResampler) { + const int16_t* inputSamples = hasReverb ? _networkScratchBuffer : decodedSamples; + _networkToOutputResampler->render(inputSamples, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } + + // if no transformations were applied, we still need to copy the buffer + if (!hasReverb && !_networkToOutputResampler) { + memcpy(outputSamples, decodedSamples, decodedBuffer.size()); + } +} + +template +void AudioPacketHandler::sendMuteEnvironmentPacket() { + auto nodeList = DependencyManager::get(); + + int dataSize = sizeof(glm::vec3) + sizeof(float); + + auto mutePacket = NLPacket::create(PacketType::MuteEnvironment, dataSize); + + const float MUTE_RADIUS = 50; + + glm::vec3 currentSourcePosition = _positionGetter(); + + mutePacket->writePrimitive(currentSourcePosition); + mutePacket->writePrimitive(MUTE_RADIUS); + + // 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(mutePacket), *audioMixer); + } +} + +template +void AudioPacketHandler::setMuted(bool muted, bool emitSignal) { + if (_isMuted != muted) { + _isMuted = muted; + if (emitSignal) { + // FIXME: CRTP + // emit muteToggled(_isMuted); + } + } +} + +template +void AudioPacketHandler::setNoiseReduction(bool enable, bool emitSignal) { + if (_isNoiseGateEnabled != enable) { + _isNoiseGateEnabled = enable; + if (emitSignal) { + // FIXME: CRTP + // emit noiseReductionChanged(_isNoiseGateEnabled); + } + } +} + +template +void AudioPacketHandler::setNoiseReductionAutomatic(bool enable, bool emitSignal) { + if (_isNoiseReductionAutomatic != enable) { + _isNoiseReductionAutomatic = enable; + if (emitSignal) { + // FIXME: CRTP + // emit noiseReductionAutomaticChanged(_isNoiseReductionAutomatic); + } + } +} + +template +void AudioPacketHandler::setNoiseReductionThreshold(float threshold, bool emitSignal) { + if (_noiseReductionThreshold != threshold) { + _noiseReductionThreshold = threshold; + if (emitSignal) { + // FIXME: CRTP + // emit noiseReductionThresholdChanged(_noiseReductionThreshold); + } + } +} + +template +void AudioPacketHandler::setAcousticEchoCancellation(bool enable, bool emitSignal) { + if (_isAECEnabled != enable) { + _isAECEnabled = enable; + if (emitSignal) { + // FIXME: CRTP + // emit acousticEchoCancellationChanged(_isAECEnabled); + } + } +} + +template +bool AudioPacketHandler::outputLocalInjector(const AudioInjectorPointer& injector) { + auto injectorBuffer = injector->getLocalBuffer(); + if (injectorBuffer) { + // local injectors are on the AudioInjectorsThread, so we must guard access + Lock lock(_injectorsMutex); + if (!_activeLocalAudioInjectors.contains(injector)) { + //qCDebug(audioclient) << "adding new injector"; + _activeLocalAudioInjectors.append(injector); + + // update the flag + _localInjectorsAvailable.exchange(true, std::memory_order_release); + } else { + qCDebug(audioclient) << "injector exists in active list already"; + } + + return true; + + } else { + // no local buffer + return false; + } +} + +template +int AudioPacketHandler::getNumLocalInjectors() { + Lock lock(_injectorsMutex); + return _activeLocalAudioInjectors.size(); +} + +template +void AudioPacketHandler::outputFormatChanged() { + _outputFrameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * OUTPUT_CHANNEL_COUNT * _outputFormat.sampleRate) / + _desiredOutputFormat.sampleRate; + _receivedAudioStream.outputFormatChanged(_outputFormat.sampleRate, OUTPUT_CHANNEL_COUNT); + + _audioOutputIODevice.open(); + + // FIXEM: CRTP + // // setup our general output device for audio-mixer audio + // _audioOutput = new QAudioOutput(_outputDeviceInfo.getDevice(), _outputFormat, this); + // + // _audioOutput->setBufferSize(_outputPeriod * 16); // magic number introduced with commit 47af406440daaccdc48bf01cb3669233a940f811 + // + // connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify); + // + // // start the output device + // _audioOutput->start(&_audioOutputIODevice); + // + // // initialize mix buffers + // + // // restrict device callback to _outputPeriod samples + // _outputPeriod = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + // // device callback may exceed reported period, so double it to avoid stutter + // _outputPeriod *= 2; + +} + +template +void AudioPacketHandler::cleanupInput() { + +// FIXME: CRTP +// if (_loopbackResampler) { +// delete _loopbackResampler; +// _loopbackResampler = NULL; +// } +// +// // cleanup any previously initialized device +// if (_audioInput) { +// // The call to stop() causes _inputDevice to be destructed. +// // That in turn causes it to be disconnected (see for example +// // http://stackoverflow.com/questions/9264750/qt-signals-and-slots-object-disconnect). +// _audioInput->stop(); +// _inputDevice = NULL; +// +// _audioInput->deleteLater(); +// _audioInput = NULL; +// +// _inputDeviceInfo.setDevice(QAudioDeviceInfo()); +// } +// +// +// #if defined(Q_OS_ANDROID) +// _shouldRestartInputSetup = false; // avoid a double call to _audioInput->start() from audioInputStateChanged +// #endif + + + if (_dummyAudioInput) { + _dummyAudioInput->stop(); + _dummyAudioInput->deleteLater(); + _dummyAudioInput = nullptr; + } + + // cleanup any resamplers + if (_inputToNetworkResampler) { + delete _inputToNetworkResampler; + _inputToNetworkResampler = NULL; + } + + if (_audioGate) { + delete _audioGate; + _audioGate = nullptr; + } + +} + +template +bool AudioPacketHandler::setupInput(AudioFormat inputFormat) { + bool supportedFormat = false; + + if (inputFormat.channelCount > 2) { + qCDebug(audioclient) << "Audio input has too many channels: " << inputFormat.channelCount; + return false; + } + + _inputFormat = inputFormat; + + // if necessary reinitialize the codec + if (_inputFormat.channelCount != _desiredInputFormat.channelCount) { + if (_codec) { + if (_encoder) { + _codec->releaseEncoder(_encoder); + } + _encoder = _codec->createEncoder(_desiredInputFormat.sampleRate, _inputFormat.channelCount); + qCDebug(audioclient) << "Reset Codec:" << _selectedCodecName << "isStereoInput:" << (_inputFormat.channelCount == 2); + } + _desiredInputFormat.channelCount = inputFormat.channelCount; + } + + qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat; + + // we've got the best we can get for input + // if required, setup a resampler for this input to our desired network format + if (_inputFormat != _desiredInputFormat + && _inputFormat.sampleRate != _desiredInputFormat.sampleRate) { + qCDebug(audioclient) << "Attemping to create a resampler for input format to network format."; + + assert(_desiredInputFormat.getSampleBits() == 16); + int channelCount = (_inputFormat.channelCount == 2 && _desiredInputFormat.channelCount == 2) ? 2 : 1; + + _inputToNetworkResampler = new AudioSRC(_inputFormat.sampleRate, _desiredInputFormat.sampleRate, channelCount); + + } else { + qCDebug(audioclient) << "No resampling required for audio input to match desired network format."; + } + + // the audio gate runs after the resampler + _audioGate = new AudioGate(_desiredInputFormat.sampleRate, _desiredInputFormat.channelCount); + qCDebug(audioclient) << "Noise gate created with" << _desiredInputFormat.channelCount << "channels."; + auto numInputCallbackBytes = calculateNumberOfInputCallbackBytes(_inputFormat); + // how do we want to handle input working, but output not working? + int numFrameSamples = calculateNumberOfFrameSamples(numInputCallbackBytes); + _inputRingBuffer.resizeForFrameSize(numFrameSamples); + + return supportedFormat; +} + +template +void AudioPacketHandler::setupDummyInput() { + // Generates audio callbacks on a timer to simulate a mic stream of silent packets. + // This enables clients without a mic to still receive an audio stream from the mixer. + + // FIXME: CRTP + // qCDebug(audioclient) << "Audio input device is not available, using dummy input."; + // _inputDeviceInfo.setDevice(QAudioDeviceInfo()); + // emit deviceChanged(QAudio::AudioInput, _inputDeviceInfo); + + _inputFormat = _desiredInputFormat; + qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat; + qCDebug(audioclient) << "No re-sampling required for audio input to match desired network format."; + + _audioGate = new AudioGate(_desiredInputFormat.sampleRate, _desiredInputFormat.channelCount); + qCDebug(audioclient) << "Noise gate created with" << _desiredInputFormat.channelCount << "channels."; + + // generate audio callbacks at the network sample rate + _dummyAudioInput = new QTimer(); + _dummyAudioInput->connect(_dummyAudioInput, &QTimer::timeout, [this](){ + const int numNetworkBytes = _desiredInputFormat.channelCount == AudioConstants::STEREO + ? AudioConstants::NETWORK_FRAME_BYTES_STEREO + : AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; + + QByteArray audioBuffer(numNetworkBytes, 0); // silent + handleAudioInput(audioBuffer); + }); + _dummyAudioInput->start((int)(AudioConstants::NETWORK_FRAME_MSECS + 0.5f)); +} + +template +bool AudioPacketHandler::isDummyInput() { + return _dummyAudioInput != nullptr; +} + +// FIXME: CRTP move to derived, different implementation +// bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDeviceInfo, bool isShutdownRequest) { +// Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); +// +// qCDebug(audioclient) << __FUNCTION__ << "_inputDeviceInfo: [" << _inputDeviceInfo.deviceName() << ":" << _inputDeviceInfo.getDevice().deviceName() +// << "-- inputDeviceInfo:" << inputDeviceInfo.deviceName() << ":" << inputDeviceInfo.getDevice().deviceName() << "]"; +// // NOTE: device start() uses the Qt internal device list +// Lock lock(_deviceMutex); +// +// bool supportedFormat = false; +// +// cleanupInput(); +// +// if (!inputDeviceInfo.getDevice().isNull()) { +// qCDebug(audioclient) << "The audio input device" << inputDeviceInfo.deviceName() << ":" << inputDeviceInfo.getDevice().deviceName() << "is available."; +// +// //do not update UI that we're changing devices if default or same device +// _inputDeviceInfo = inputDeviceInfo; +// emit deviceChanged(QAudio::AudioInput, _inputDeviceInfo); +// +// QAudioFormat format; +// if (adjustedFormatForAudioDevice(_inputDeviceInfo.getDevice(), _desiredInputFormat, format)) { +// setupInput(format); +// +// auto numInputCallbackBytes = calculateNumberOfInputCallbackBytes(_inputFormat); +// // if the user wants stereo but this device can't provide then bail +// if (!_isStereoInput || _inputFormat.channelCount() == 2) { +// _audioInput = new QAudioInput(_inputDeviceInfo.getDevice(), _inputFormat, this); +// _audioInput->setBufferSize(numInputCallbackBytes * CALLBACK_ACCELERATOR_RATIO); +// // different audio input devices may have different volumes +// emit inputVolumeChanged(_audioInput->volume()); +// +// #if defined(Q_OS_ANDROID) +// if (_audioInput) { +// _shouldRestartInputSetup = true; +// connect(_audioInput, &QAudioInput::stateChanged, this, &AudioClient::audioInputStateChanged); +// } +// #endif +// _inputDevice = _audioInput->start(); +// +// if (_inputDevice) { +// connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput())); +// supportedFormat = true; +// } else { +// qCDebug(audioclient) << "Error starting audio input -" << _audioInput->error(); +// _audioInput->deleteLater(); +// _audioInput = NULL; +// } +// } +// } +// } +// +// // If there is no working input device, use the dummy input device. +// if (!_audioInput) { +// setupDummyInput(); +// } +// +// return supportedFormat; +// } + +template +void AudioPacketHandler::cleanupOutput() { + // FIXME: CRTP + // cleanup any previously initialized device + // if (_audioOutput) { + // _audioOutput->stop(); + // + // //must be deleted in next eventloop cycle when its called from notify() + // _audioOutput->deleteLater(); + // _audioOutput = NULL; + // + // _loopbackOutputDevice = NULL; + // //must be deleted in next eventloop cycle when its called from notify() + // _loopbackAudioOutput->deleteLater(); + // _loopbackAudioOutput = NULL; + // + // _outputDeviceInfo.setDevice(QAudioDeviceInfo()); + // } + // + // if (_loopbackResampler) { + // delete _loopbackResampler; + // _loopbackResampler = NULL; + // } + + _audioOutputInitialized = false; + + if (_audioOutputIODevice.isOpen()) { + _audioOutputIODevice.close(); + } + + if (_outputMixBuffer) { + delete[] _outputMixBuffer; + _outputMixBuffer = NULL; + + delete[] _outputScratchBuffer; + _outputScratchBuffer = NULL; + + delete[] _localOutputMixBuffer; + _localOutputMixBuffer = NULL; + } + + // cleanup any resamplers + if (_networkToOutputResampler) { + delete _networkToOutputResampler; + _networkToOutputResampler = NULL; + } + + if (_localToOutputResampler) { + delete _localToOutputResampler; + _localToOutputResampler = NULL; + } +} + +template +bool AudioPacketHandler::setupOutput(AudioFormat outputFormat) { + _outputFormat = outputFormat; + + qCDebug(audioclient) << "The format to be used for audio output is" << _outputFormat; + + // we've got the best we can get for input + // if required, setup a resampler for this input to our desired network format + if (_desiredOutputFormat != _outputFormat + && _desiredOutputFormat.sampleRate != _outputFormat.sampleRate) { + qCDebug(audioclient) << "Attemping to create a resampler for network format to output format."; + + assert(_desiredOutputFormat.getSampleBits() == 16); + + _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate, _outputFormat.sampleRate, OUTPUT_CHANNEL_COUNT); + _localToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate, _outputFormat.sampleRate, OUTPUT_CHANNEL_COUNT); + + } else { + qCDebug(audioclient) << "No resampling required for network output to match actual output format."; + } + + + int frameSize = (AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * _outputFormat.channelCount * _outputFormat.sampleRate) / _desiredOutputFormat.sampleRate; + int requestedSize = _sessionOutputBufferSizeFrames * frameSize * AudioConstants::SAMPLE_SIZE; + + _outputPeriod = requestedSize; + + outputFormatChanged(); + + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; + + // size local output mix buffer based on resampled network frame size + int networkPeriod = _localToOutputResampler ? _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO) : AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + _localOutputMixBuffer = new float[networkPeriod]; + + // local period should be at least twice the output period, + // in case two device reads happen before more data can be read (worst case) + int localPeriod = _outputPeriod * 2; + // round up to an exact multiple of networkPeriod + localPeriod = ((localPeriod + networkPeriod - 1) / networkPeriod) * networkPeriod; + // this ensures lowest latency without stutter from underrun + _localInjectorsStream.resizeForFrameSize(localPeriod); + + _audioOutputInitialized = true; + + return true; + +} + +// FIXME: CRTP +// bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDeviceInfo, bool isShutdownRequest) { +// Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); +// +// qCDebug(audioclient) << __FUNCTION__ << "_outputdeviceInfo: [" << _outputDeviceInfo.deviceName() << ":" << _outputDeviceInfo.getDevice().deviceName() +// << "-- outputDeviceInfo:" << outputDeviceInfo.deviceName() << ":" << outputDeviceInfo.getDevice().deviceName() << "]"; +// bool supportedFormat = false; +// +// // NOTE: device start() uses the Qt internal device list +// Lock lock(_deviceMutex); +// +// _localSamplesAvailable.exchange(0, std::memory_order_release); +// +// //wait on local injectors prep to finish running +// if ( !_localPrepInjectorFuture.isFinished()) { +// _localPrepInjectorFuture.waitForFinished(); +// } +// +// Lock localAudioLock(_localAudioMutex); +// +// cleanupOutput(); +// +// if (!outputDeviceInfo.getDevice().isNull()) { +// qCDebug(audioclient) << "The audio output device" << outputDeviceInfo.deviceName() << ":" << outputDeviceInfo.getDevice().deviceName() << "is available."; +// +// //do not update UI that we're changing devices if default or same device +// _outputDeviceInfo = outputDeviceInfo; +// emit deviceChanged(QAudio::AudioOutput, _outputDeviceInfo); +// +// AudioFormat outputFormat; +// if (adjustedFormatForAudioDevice(_outputDeviceInfo.getDevice(), _desiredOutputFormat, outputFormat)) { +// setupOutput(outputFormat); +// +// int bufferSize = _audioOutput->bufferSize(); +// int bufferSamples = bufferSize / AudioConstants::SAMPLE_SIZE; +// int bufferFrames = bufferSamples / (float)frameSize; +// qCDebug(audioclient) << "frame (samples):" << frameSize; +// qCDebug(audioclient) << "buffer (frames):" << bufferFrames; +// qCDebug(audioclient) << "buffer (samples):" << bufferSamples; +// qCDebug(audioclient) << "buffer (bytes):" << bufferSize; +// qCDebug(audioclient) << "requested (bytes):" << requestedSize; +// qCDebug(audioclient) << "period (samples):" << _outputPeriod; +// qCDebug(audioclient) << "local buffer (samples):" << localPeriod; +// +// // unlock to avoid a deadlock with the device callback (which always succeeds this initialization) +// localAudioLock.unlock(); +// +// // setup a loopback audio output device +// _loopbackAudioOutput = new QAudioOutput(outputDeviceInfo.getDevice(), _outputFormat, this); +// +// supportedFormat = true; +// } +// } +// +// return supportedFormat; +// } + +template +int AudioPacketHandler::setOutputBufferSize(int numFrames, bool persist) { + qCDebug(audioclient) << __FUNCTION__ << "numFrames:" << numFrames << "persist:" << persist; + + numFrames = std::min(std::max(numFrames, MIN_BUFFER_FRAMES), MAX_BUFFER_FRAMES); + qCDebug(audioclient) << __FUNCTION__ << "clamped numFrames:" << numFrames << "_sessionOutputBufferSizeFrames:" << _sessionOutputBufferSizeFrames; + + if (numFrames != _sessionOutputBufferSizeFrames) { + qCInfo(audioclient, "Audio output buffer set to %d frames", numFrames); + _sessionOutputBufferSizeFrames = numFrames; + if (persist) { + // FIXME: CRTP + // _outputBufferSizeFrames.set(numFrames); + } + } + return numFrames; +} + +template +int AudioPacketHandler::calculateNumberOfInputCallbackBytes(const AudioFormat& format) const { + int numInputCallbackBytes = (int)(((AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL + * format.channelCount + * ((float) format.sampleRate / AudioConstants::SAMPLE_RATE))) + 0.5f); + + return numInputCallbackBytes; +} + +template +int AudioPacketHandler::calculateNumberOfFrameSamples(int numBytes) const { + int frameSamples = numBytes / AudioConstants::SAMPLE_SIZE; + return frameSamples; +} + +template +float AudioPacketHandler::azimuthForSource(const glm::vec3& relativePosition) { + glm::quat inverseOrientation = glm::inverse(_orientationGetter()); + + glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; + + // project the rotated source position vector onto the XZ plane + rotatedSourcePosition.y = 0.0f; + + static const float SOURCE_DISTANCE_THRESHOLD = 1e-30f; + + 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 { + // no azimuth if they are in same spot + return 0.0f; + } +} + +template +float AudioPacketHandler::gainForSource(float distance, float volume) { + + // attenuation = -6dB * log2(distance) + // reference attenuation of 0dB at distance = ATTN_DISTANCE_REF + float d = (1.0f / ATTN_DISTANCE_REF) * std::max(distance, HRTF_NEARFIELD_MIN); + float gain = volume / d; + gain = std::min(gain, ATTN_GAIN_MAX); + + return gain; +} + +template +qint64 AudioPacketHandler::AudioOutputIODevice::readData(char * data, qint64 maxSize) { + + // lock-free wait for initialization to avoid races + if (!_audio->_audioOutputInitialized.load(std::memory_order_acquire)) { + memset(data, 0, maxSize); + return maxSize; + } + + // max samples requested from OUTPUT_CHANNEL_COUNT + int deviceChannelCount = _audio->_outputFormat.channelCount; + int maxSamplesRequested = (int)(maxSize / _audio->_outputFormat.getSampleSize()) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; + // restrict samplesRequested to the size of our mix/scratch buffers + maxSamplesRequested = std::min(maxSamplesRequested, _audio->_outputPeriod); + + int16_t* scratchBuffer = _audio->_outputScratchBuffer; + float* mixBuffer = _audio->_outputMixBuffer; + + int samplesRequested = maxSamplesRequested; + int networkSamplesPopped; + if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { + qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable(), samplesRequested); + AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); + lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); + for (int i = 0; i < networkSamplesPopped; i++) { + mixBuffer[i] = convertToFloat(scratchBuffer[i]); + } + samplesRequested = networkSamplesPopped; + } + + int injectorSamplesPopped = 0; + { + bool append = networkSamplesPopped > 0; + // check the samples we have available locklessly; this is possible because only two functions add to the count: + // - prepareLocalAudioInjectors will only increase samples count + // - switchOutputToAudioDevice will zero samples count, + // stop the device - so that readData will exhaust the existing buffer or see a zeroed samples count, + // and start the device - which can then only see a zeroed samples count + int samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire); + + // if we do not have enough samples buffered despite having injectors, buffer them synchronously + if (samplesAvailable < samplesRequested && _audio->_localInjectorsAvailable.load(std::memory_order_acquire)) { + // try_to_lock, in case the device is being shut down already + std::unique_ptr localAudioLock(new Lock(_audio->_localAudioMutex, std::try_to_lock)); + if (localAudioLock->owns_lock()) { + _audio->prepareLocalAudioInjectors(std::move(localAudioLock)); + samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire); + } + } + + samplesRequested = std::min(samplesRequested, samplesAvailable); + if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { + _audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release); + qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); + } + } + + // prepare injectors for the next callback + _audio->_localPrepInjectorFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { + _audio->prepareLocalAudioInjectors(); + }); + + int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); + if (samplesPopped == 0) { + // nothing on network, don't grab anything from injectors, and fill with silence + samplesPopped = maxSamplesRequested; + memset(mixBuffer, 0, samplesPopped * sizeof(float)); + } + int framesPopped = samplesPopped / OUTPUT_CHANNEL_COUNT; + + // apply output gain + float newGain = _audio->_outputGain.load(std::memory_order_acquire); + float oldGain = _audio->_lastOutputGain; + _audio->_lastOutputGain = newGain; + + applyGainSmoothing(mixBuffer, framesPopped, oldGain, newGain); + + // limit the audio + _audio->_audioLimiter.render(mixBuffer, scratchBuffer, framesPopped); + +#if defined(WEBRTC_AUDIO) + if (_audio->_isAECEnabled) { + _audio->processWebrtcFarEnd(scratchBuffer, framesPopped, OUTPUT_CHANNEL_COUNT, _audio->_outputFormat.sampleRate); + } +#endif + + int bytesWritten = 0; + + if (_audio->_outputFormat.sampleType == AudioFormat::Float) { + // Convert samples to normalized float. This is used in the client + // library, since a lot of audio APIs prefer to restrict themselves to + // float samples. + for (int i = 0; i < samplesPopped; i++) { + mixBuffer[i] = convertToFloat(scratchBuffer[i]); + } + + // if required, upmix or downmix to deviceChannelCount + bytesWritten = copyWithChannelConversion(mixBuffer, OUTPUT_CHANNEL_COUNT, data, deviceChannelCount, samplesPopped); + } + else + { + // if required, upmix or downmix to deviceChannelCount + bytesWritten = copyWithChannelConversion(scratchBuffer, OUTPUT_CHANNEL_COUNT, data, deviceChannelCount, samplesPopped); + } + + assert(bytesWritten <= maxSize); + + // FIXME: CRTP + // send output buffer for recording + // if (_audio->_isRecording) { + // Lock lock(_recordMutex); + // _audio->_audioFileWav.addRawAudioChunk(data, bytesWritten); + // } + + // FIXEM: CRTP + // int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); + // float msecsAudioOutputUnplayed = bytesAudioOutputUnplayed / (float)_audio->_outputFormat.bytesForDuration(USECS_PER_MSEC); + // _audio->_stats.updateOutputMsUnplayed(msecsAudioOutputUnplayed); + // + // if (bytesAudioOutputUnplayed == 0) { + // _unfulfilledReads++; + // } + + return bytesWritten; +} + +template +void AudioPacketHandler::setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale) { + avatarBoundingBoxCorner = corner; + avatarBoundingBoxScale = scale; +} + + +template +void AudioPacketHandler::startThread() { + moveToNewNamedThread(&derived(), "Audio Thread", [this] { start(); }, QThread::TimeCriticalPriority); +} + +#endif /* end of include guard */ diff --git a/libraries/audio-client/CMakeLists.txt b/libraries/audio-client/CMakeLists.txt index d82e94bd695..46a6f976423 100644 --- a/libraries/audio-client/CMakeLists.txt +++ b/libraries/audio-client/CMakeLists.txt @@ -8,8 +8,7 @@ include_hifi_library_headers(shared) include_hifi_library_headers(shared-gui) include_hifi_library_headers(networking) -if (ANDROID) -else () +if (ENABLE_WEBRTC_AUDIO) target_webrtc() endif () diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index eb6820421de..52c77c036ab 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -95,7 +95,7 @@ void AudioClient::setHmdAudioName(QAudio::Mode mode, const QString& name) { // thread-safe QList getAvailableDevices(QAudio::Mode mode, const QString& hmdName) { //get hmd device name prior to locking device mutex. in case of shutdown, this thread will be locked and audio client - //cannot properly shut down. + //cannot properly shut down. QString defDeviceName = defaultAudioDeviceName(mode); // NOTE: availableDevices() clobbers the Qt internal device list @@ -132,7 +132,7 @@ QList getAvailableDevices(QAudio::Mode mode, const QString& break; } } - + if (!hmdDevice.getDevice().isNull()) { newDevices.push_front(hmdDevice); } @@ -159,7 +159,7 @@ void AudioClient::checkDevices() { auto inputDevices = getAvailableDevices(QAudio::AudioInput, hmdInputName); auto outputDevices = getAvailableDevices(QAudio::AudioOutput, hmdOutputName); - + static const QMetaMethod devicesChangedSig= QMetaMethod::fromSignal(&AudioClient::devicesChanged); //only emit once the scripting interface has connected to the signal if (isSignalConnected(devicesChangedSig)) { @@ -173,7 +173,7 @@ void AudioClient::checkDevices() { _outputDevices.swap(outputDevices); emit devicesChanged(QAudio::AudioOutput, _outputDevices); } - } + } } HifiAudioDeviceInfo AudioClient::getActiveAudioDevice(QAudio::Mode mode) const { @@ -280,7 +280,7 @@ static float computeLoudness(int16_t* samples, int numSamples) { 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; @@ -295,7 +295,7 @@ static void applyGainSmoothing(float* buffer, int numFrames, float gain0, float 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; @@ -386,7 +386,7 @@ AudioClient::AudioClient() { PacketReceiver::makeUnsourcedListenerReference(this, &AudioClient::handleSelectedAudioFormat)); auto& domainHandler = nodeList->getDomainHandler(); - connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this] { + connect(&domainHandler, &DomainHandler::disconnectedFromDomain, this, [this] { _solo.reset(); }); connect(nodeList.data(), &NodeList::nodeActivated, this, [this](SharedNodePointer node) { @@ -578,7 +578,7 @@ QString defaultAudioDeviceName(QAudio::Mode mode) { waveInGetDevCaps(WAVE_MAPPER, &wic, sizeof(wic)); //Use the received manufacturer id to get the device's real name waveInGetDevCaps(wic.wMid, &wic, sizeof(wic)); -#if !defined(NDEBUG) +#if !defined(NDEBUG) qCDebug(audioclient) << "input device:" << wic.szPname; #endif deviceName = wic.szPname; @@ -588,7 +588,7 @@ QString defaultAudioDeviceName(QAudio::Mode mode) { waveOutGetDevCaps(WAVE_MAPPER, &woc, sizeof(woc)); //Use the received manufacturer id to get the device's real name waveOutGetDevCaps(woc.wMid, &woc, sizeof(woc)); -#if !defined(NDEBUG) +#if !defined(NDEBUG) qCDebug(audioclient) << "output device:" << woc.szPname; #endif deviceName = woc.szPname; @@ -613,8 +613,8 @@ QString defaultAudioDeviceName(QAudio::Mode mode) { CoUninitialize(); } -#if !defined(NDEBUG) - qCDebug(audioclient) << "defaultAudioDeviceForMode mode: " << (mode == QAudio::AudioOutput ? "Output" : "Input") +#if !defined(NDEBUG) + qCDebug(audioclient) << "defaultAudioDeviceForMode mode: " << (mode == QAudio::AudioOutput ? "Output" : "Input") << " [" << deviceName << "] [" << "]"; #endif @@ -795,7 +795,7 @@ void AudioClient::start() { _desiredOutputFormat = _desiredInputFormat; _desiredOutputFormat.setChannelCount(OUTPUT_CHANNEL_COUNT); - + QString inputName; QString outputName; { @@ -803,10 +803,10 @@ void AudioClient::start() { inputName = _hmdInputName; outputName = _hmdOutputName; } - + //initialize input to the dummy device to prevent starves switchInputToAudioDevice(HifiAudioDeviceInfo()); - switchOutputToAudioDevice(defaultAudioDeviceForMode(QAudio::AudioOutput, QString())); + switchOutputToAudioDevice(defaultAudioDeviceForMode(QAudio::AudioOutput, QString())); #if defined(Q_OS_ANDROID) connect(&_checkInputTimer, &QTimer::timeout, this, &AudioClient::checkInputTimeout); @@ -1015,7 +1015,7 @@ void AudioClient::selectAudioFormat(const QString& selectedCodecName) { bool AudioClient::switchAudioDevice(QAudio::Mode mode, const HifiAudioDeviceInfo& deviceInfo) { auto device = deviceInfo; if (deviceInfo.getDevice().isNull()) { - qCDebug(audioclient) << __FUNCTION__ << " switching to null device :" + qCDebug(audioclient) << __FUNCTION__ << " switching to null device :" << deviceInfo.deviceName() << " : " << deviceInfo.getDevice().deviceName(); } @@ -1401,10 +1401,6 @@ void AudioClient::handleAudioInput(QByteArray& audioBuffer) { _audioOutbound.increment(); } - Transform audioTransform; - audioTransform.setTranslation(_positionGetter()); - audioTransform.setRotation(_orientationGetter()); - QByteArray encodedBuffer; if (_encoder) { _encoder->encode(audioBuffer, encodedBuffer); @@ -1413,7 +1409,7 @@ void AudioClient::handleAudioInput(QByteArray& audioBuffer) { } emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, _isStereoInput, - audioTransform, avatarBoundingBoxCorner, avatarBoundingBoxScale, + {_positionGetter(), _orientationGetter()}, avatarBoundingBoxCorner, avatarBoundingBoxScale, packetType, _selectedCodecName); _stats.sentPacket(); } @@ -1868,7 +1864,7 @@ void AudioClient::outputFormatChanged() { bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDeviceInfo, bool isShutdownRequest) { Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); - qCDebug(audioclient) << __FUNCTION__ << "_inputDeviceInfo: [" << _inputDeviceInfo.deviceName() << ":" << _inputDeviceInfo.getDevice().deviceName() + qCDebug(audioclient) << __FUNCTION__ << "_inputDeviceInfo: [" << _inputDeviceInfo.deviceName() << ":" << _inputDeviceInfo.getDevice().deviceName() << "-- inputDeviceInfo:" << inputDeviceInfo.deviceName() << ":" << inputDeviceInfo.getDevice().deviceName() << "]"; bool supportedFormat = false; @@ -1923,7 +1919,7 @@ bool AudioClient::switchInputToAudioDevice(const HifiAudioDeviceInfo inputDevice if (!inputDeviceInfo.getDevice().isNull()) { qCDebug(audioclient) << "The audio input device" << inputDeviceInfo.deviceName() << ":" << inputDeviceInfo.getDevice().deviceName() << "is available."; - + //do not update UI that we're changing devices if default or same device _inputDeviceInfo = inputDeviceInfo; emit deviceChanged(QAudio::AudioInput, _inputDeviceInfo); @@ -2097,13 +2093,13 @@ void AudioClient::outputNotify() { void AudioClient::noteAwakening() { qCDebug(audioclient) << "Restarting the audio devices."; - switchInputToAudioDevice(_inputDeviceInfo); + switchInputToAudioDevice(_inputDeviceInfo); switchOutputToAudioDevice(_outputDeviceInfo); } bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDeviceInfo, bool isShutdownRequest) { Q_ASSERT_X(QThread::currentThread() == thread(), Q_FUNC_INFO, "Function invoked on wrong thread"); - + qCDebug(audioclient) << __FUNCTION__ << "_outputdeviceInfo: [" << _outputDeviceInfo.deviceName() << ":" << _outputDeviceInfo.getDevice().deviceName() << "-- outputDeviceInfo:" << outputDeviceInfo.deviceName() << ":" << outputDeviceInfo.getDevice().deviceName() << "]"; bool supportedFormat = false; @@ -2143,7 +2139,7 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi delete[] _localOutputMixBuffer; _localOutputMixBuffer = NULL; - + _outputDeviceInfo.setDevice(QAudioDeviceInfo()); } @@ -2170,7 +2166,7 @@ bool AudioClient::switchOutputToAudioDevice(const HifiAudioDeviceInfo outputDevi if (!outputDeviceInfo.getDevice().isNull()) { qCDebug(audioclient) << "The audio output device" << outputDeviceInfo.deviceName() << ":" << outputDeviceInfo.getDevice().deviceName() << "is available."; - + //do not update UI that we're changing devices if default or same device _outputDeviceInfo = outputDeviceInfo; emit deviceChanged(QAudio::AudioOutput, _outputDeviceInfo); @@ -2398,7 +2394,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); } } - + // prepare injectors for the next callback _audio->_localPrepInjectorFuture = QtConcurrent::run(QThreadPool::globalInstance(), [this] { _audio->prepareLocalAudioInjectors(); diff --git a/libraries/audio/CMakeLists.txt b/libraries/audio/CMakeLists.txt index ae9035e2267..d1b177fd854 100644 --- a/libraries/audio/CMakeLists.txt +++ b/libraries/audio/CMakeLists.txt @@ -5,4 +5,4 @@ if (ANDROID) add_definitions("-D__STDC_CONSTANT_MACROS") endif () -link_hifi_libraries(networking shared shared-gui plugins) +link_hifi_libraries(networking shared) diff --git a/libraries/audio/src/AbstractAudioInterface.cpp b/libraries/audio/src/AbstractAudioInterface.cpp index 99e6ef38704..33bc2dc74ef 100644 --- a/libraries/audio/src/AbstractAudioInterface.cpp +++ b/libraries/audio/src/AbstractAudioInterface.cpp @@ -13,12 +13,13 @@ #include #include #include -#include + +#include #include "AudioConstants.h" void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber, bool isStereo, - const Transform& transform, glm::vec3 avatarBoundingBoxCorner, glm::vec3 avatarBoundingBoxScale, + const Vantage& vantage, glm::vec3 avatarBoundingBoxCorner, glm::vec3 avatarBoundingBoxScale, PacketType packetType, QString codecName) { static std::mutex _mutex; using Locker = std::unique_lock; @@ -48,12 +49,12 @@ void AbstractAudioInterface::emitAudioPacket(const void* audioData, size_t bytes } // at this point we'd better be sending the mixer a valid position, or it won't consider us for mixing - assert(!isNaN(transform.getTranslation())); + assert(!isNaN(vantage.position)); // pack the three float positions - audioPacket->writePrimitive(transform.getTranslation()); + audioPacket->writePrimitive(vantage.position); // pack the orientation - audioPacket->writePrimitive(transform.getRotation()); + audioPacket->writePrimitive(vantage.rotation); audioPacket->writePrimitive(avatarBoundingBoxCorner); audioPacket->writePrimitive(avatarBoundingBoxScale); diff --git a/libraries/audio/src/AbstractAudioInterface.h b/libraries/audio/src/AbstractAudioInterface.h index e9e40e95f94..96d0bb2c109 100644 --- a/libraries/audio/src/AbstractAudioInterface.h +++ b/libraries/audio/src/AbstractAudioInterface.h @@ -13,7 +13,6 @@ #define hifi_AbstractAudioInterface_h #include -#include #include @@ -23,7 +22,11 @@ class AudioInjector; class AudioInjectorLocalBuffer; -class Transform; + +struct Vantage { + glm::vec3 position; + glm::quat rotation; +}; class AbstractAudioInterface : public QObject { Q_OBJECT @@ -31,7 +34,7 @@ class AbstractAudioInterface : public QObject { AbstractAudioInterface(QObject* parent = 0) : QObject(parent) {}; static void emitAudioPacket(const void* audioData, size_t bytes, quint16& sequenceNumber, bool isStereo, - const Transform& transform, glm::vec3 avatarBoundingBoxCorner, glm::vec3 avatarBoundingBoxScale, + const Vantage& vantage, glm::vec3 avatarBoundingBoxCorner, glm::vec3 avatarBoundingBoxScale, PacketType packetType, QString codecName = QString("")); // threadsafe diff --git a/libraries/audio/src/AudioInjectorOptions.cpp b/libraries/audio/src/AudioInjectorOptions.cpp index 39b807fd769..59bf4ade690 100644 --- a/libraries/audio/src/AudioInjectorOptions.cpp +++ b/libraries/audio/src/AudioInjectorOptions.cpp @@ -11,10 +11,6 @@ #include "AudioInjectorOptions.h" -#include - -#include - #include "AudioLogging.h" AudioInjectorOptions::AudioInjectorOptions() : @@ -32,98 +28,3 @@ AudioInjectorOptions::AudioInjectorOptions() : { } -QScriptValue injectorOptionsToScriptValue(QScriptEngine* engine, const AudioInjectorOptions& injectorOptions) { - QScriptValue obj = engine->newObject(); - if (injectorOptions.positionSet) { - obj.setProperty("position", vec3ToScriptValue(engine, injectorOptions.position)); - } - obj.setProperty("volume", injectorOptions.volume); - obj.setProperty("loop", injectorOptions.loop); - obj.setProperty("orientation", quatToScriptValue(engine, injectorOptions.orientation)); - obj.setProperty("ignorePenumbra", injectorOptions.ignorePenumbra); - obj.setProperty("localOnly", injectorOptions.localOnly); - obj.setProperty("secondOffset", injectorOptions.secondOffset); - obj.setProperty("pitch", injectorOptions.pitch); - return obj; -} - -/*@jsdoc - * Configures where and how an audio injector plays its audio. - * @typedef {object} AudioInjector.AudioInjectorOptions - * @property {Vec3} position=Vec3.ZERO - The position in the domain to play the sound. - * @property {Quat} orientation=Quat.IDENTITY - The orientation in the domain to play the sound in. - * @property {number} volume=1.0 - Playback volume, between 0.0 and 1.0. - * @property {number} pitch=1.0 - Alter the pitch of the sound, within +/- 2 octaves. The value is the relative sample rate to - * resample the sound at, range 0.062516.0.
- * A value of 0.0625 lowers the pitch by 2 octaves.
- * A value of 1.0 means there is no change in pitch.
- * A value of 16.0 raises the pitch by 2 octaves. - * @property {boolean} loop=false - If true, the sound is played repeatedly until playback is stopped. - * @property {number} secondOffset=0 - Starts playback from a specified time (seconds) within the sound file, ≥ - * 0. - * @property {boolean} localOnly=false - If true, the sound is played back locally on the client rather than to - * others via the audio mixer. - * @property {boolean} ignorePenumbra=false -

Deprecated: This property is deprecated and will be - * removed.

- */ -void injectorOptionsFromScriptValue(const QScriptValue& object, AudioInjectorOptions& injectorOptions) { - if (!object.isObject()) { - qWarning() << "Audio injector options is not an object."; - return; - } - - if (injectorOptions.positionSet == false) { - qWarning() << "Audio injector options: injectorOptionsFromScriptValue() called more than once?"; - } - injectorOptions.positionSet = false; - - QScriptValueIterator it(object); - while (it.hasNext()) { - it.next(); - - if (it.name() == "position") { - vec3FromScriptValue(object.property("position"), injectorOptions.position); - injectorOptions.positionSet = true; - } else if (it.name() == "orientation") { - quatFromScriptValue(object.property("orientation"), injectorOptions.orientation); - } else if (it.name() == "volume") { - if (it.value().isNumber()) { - injectorOptions.volume = it.value().toNumber(); - } else { - qCWarning(audio) << "Audio injector options: volume is not a number"; - } - } else if (it.name() == "loop") { - if (it.value().isBool()) { - injectorOptions.loop = it.value().toBool(); - } else { - qCWarning(audio) << "Audio injector options: loop is not a boolean"; - } - } else if (it.name() == "ignorePenumbra") { - if (it.value().isBool()) { - injectorOptions.ignorePenumbra = it.value().toBool(); - } else { - qCWarning(audio) << "Audio injector options: ignorePenumbra is not a boolean"; - } - } else if (it.name() == "localOnly") { - if (it.value().isBool()) { - injectorOptions.localOnly = it.value().toBool(); - } else { - qCWarning(audio) << "Audio injector options: localOnly is not a boolean"; - } - } else if (it.name() == "secondOffset") { - if (it.value().isNumber()) { - injectorOptions.secondOffset = it.value().toNumber(); - } else { - qCWarning(audio) << "Audio injector options: secondOffset is not a number"; - } - } else if (it.name() == "pitch") { - if (it.value().isNumber()) { - injectorOptions.pitch = it.value().toNumber(); - } else { - qCWarning(audio) << "Audio injector options: pitch is not a number"; - } - } else { - qCWarning(audio) << "Unknown audio injector option:" << it.name(); - } - } -} diff --git a/libraries/audio/src/AudioInjectorOptions.h b/libraries/audio/src/AudioInjectorOptions.h index 5dec8a02403..92ed9711b2d 100644 --- a/libraries/audio/src/AudioInjectorOptions.h +++ b/libraries/audio/src/AudioInjectorOptions.h @@ -12,8 +12,6 @@ #ifndef hifi_AudioInjectorOptions_h #define hifi_AudioInjectorOptions_h -#include - #include #include @@ -33,9 +31,4 @@ class AudioInjectorOptions { float pitch; // multiplier, where 2.0f shifts up one octave }; -Q_DECLARE_METATYPE(AudioInjectorOptions); - -QScriptValue injectorOptionsToScriptValue(QScriptEngine* engine, const AudioInjectorOptions& injectorOptions); -void injectorOptionsFromScriptValue(const QScriptValue& object, AudioInjectorOptions& injectorOptions); - #endif // hifi_AudioInjectorOptions_h diff --git a/libraries/audio/src/InboundAudioStream.cpp b/libraries/audio/src/InboundAudioStream.cpp index e946c6afb7e..005d554039c 100644 --- a/libraries/audio/src/InboundAudioStream.cpp +++ b/libraries/audio/src/InboundAudioStream.cpp @@ -144,7 +144,7 @@ int InboundAudioStream::parseData(ReceivedMessage& message) { } case SequenceNumberStats::Early: { // Packet is early. Treat the packets as if all the packets between the last - // OnTime packet and this packet were lost. If we're using a codec this will + // OnTime packet and this packet were lost. If we're using a codec this will // also result in allowing the codec to interpolate lost data. Then // fall through to the "on time" logic to actually handle this packet int packetsDropped = arrivalInfo._seqDiffFromExpected; @@ -270,7 +270,7 @@ int InboundAudioStream::lostAudioData(int numPackets) { int InboundAudioStream::parseAudioData(const QByteArray& packetAfterStreamProperties) { QByteArray decodedBuffer; - // may block on the real-time thread, which is acceptible as + // may block on the real-time thread, which is acceptible as // parseAudioData is only called by the packet processing // thread which, while high performance, is not as sensitive to // delays as the real-time thread. @@ -287,22 +287,22 @@ int InboundAudioStream::parseAudioData(const QByteArray& packetAfterStreamProper int InboundAudioStream::writeDroppableSilentFrames(int silentFrames) { // We can't guarentee that all clients have faded the stream down - // to silence and encoded that silence before sending us a + // to silence and encoded that silence before sending us a // SilentAudioFrame. If the encoder has truncated the stream it will - // leave the decoder holding some unknown loud state. To handle this + // leave the decoder holding some unknown loud state. To handle this // case we will call the decoder's lostFrame() method, which indicates - // that it should interpolate from its last known state down toward + // that it should interpolate from its last known state down toward // silence. { - // may block on the real-time thread, which is acceptible as + // may block on the real-time thread, which is acceptible as // writeDroppableSilentFrames is only called by the packet processing // thread which, while high performance, is not as sensitive to // delays as the real-time thread. QMutexLocker lock(&_decoderMutex); if (_decoder) { - // FIXME - We could potentially use the output from the codec, in which - // case we might get a cleaner fade toward silence. NOTE: The below logic - // attempts to catch up in the event that the jitter buffers have grown. + // FIXME - We could potentially use the output from the codec, in which + // case we might get a cleaner fade toward silence. NOTE: The below logic + // attempts to catch up in the event that the jitter buffers have grown. // The better long term fix is to use the output from the decode, detect // when it actually reaches silence, and then delete the silent portions // of the jitter buffers. Or petentially do a cross fade from the decode @@ -338,7 +338,7 @@ int InboundAudioStream::writeDroppableSilentFrames(int silentFrames) { } int ret = _ringBuffer.addSilentSamples(silentSamples - numSilentFramesToDrop * samplesPerFrame); - + return ret; } @@ -360,7 +360,7 @@ int InboundAudioStream::popSamples(int maxSamples, bool allOrNothing) { popSamplesNoCheck(samplesAvailable); samplesPopped = samplesAvailable; } else { - // we can't pop any samples, set this stream to starved for jitter + // we can't pop any samples, set this stream to starved for jitter // buffer calculations. setToStarved(); _consecutiveNotMixedCount++; @@ -485,7 +485,7 @@ void InboundAudioStream::setStaticJitterBufferFrames(int staticJitterBufferFrame } void InboundAudioStream::packetReceivedUpdateTimingStats() { - + // update our timegap stats and desired jitter buffer frames if necessary // discard the first few packets we receive since they usually have gaps that aren't represensative of normal jitter const quint32 NUM_INITIAL_PACKETS_DISCARD = 1000; // 10s @@ -567,7 +567,7 @@ float calculateRepeatedFrameFadeFactor(int indexOfRepeat) { return 0.0f; } -void InboundAudioStream::setupCodec(CodecPluginPointer codec, const QString& codecName, int numChannels) { +void InboundAudioStream::setupCodec(std::shared_ptr codec, const QString& codecName, int numChannels) { cleanupCodec(); // cleanup any previously allocated coders first _codec = codec; _selectedCodecName = codecName; diff --git a/libraries/audio/src/InboundAudioStream.h b/libraries/audio/src/InboundAudioStream.h index b42609d5769..9e604e47c95 100644 --- a/libraries/audio/src/InboundAudioStream.h +++ b/libraries/audio/src/InboundAudioStream.h @@ -18,8 +18,7 @@ #include #include #include - -#include +#include #include "AudioRingBuffer.h" #include "MovingMinMaxAvg.h" @@ -75,7 +74,7 @@ class InboundAudioStream : public NodeData { /// returns the desired number of jitter buffer frames under the dyanmic jitter buffers scheme int getCalculatedJitterBufferFrames() const { return _calculatedJitterBufferFrames; } - + bool dynamicJitterBufferEnabled() const { return _dynamicJitterBufferEnabled; } int getStaticJitterBufferFrames() { return _staticJitterBufferFrames; } int getDesiredJitterBufferFrames() { return _desiredJitterBufferFrames; } @@ -95,14 +94,14 @@ class InboundAudioStream : public NodeData { int getOverflowCount() const { return _ringBuffer.getOverflowCount(); } int getPacketsReceived() const { return _incomingSequenceNumberStats.getReceived(); } - + bool hasReverb() const { return _hasReverb; } float getRevebTime() const { return _reverbTime; } float getWetLevel() const { return _wetLevel; } void setReverb(float reverbTime, float wetLevel); void clearReverb() { _hasReverb = false; } - void setupCodec(CodecPluginPointer codec, const QString& codecName, int numChannels); + void setupCodec(std::shared_ptr codec, const QString& codecName, int numChannels); void cleanupCodec(); signals: @@ -139,7 +138,7 @@ public slots: /// writes silent frames to the buffer that may be dropped to reduce latency caused by the buffer virtual int writeDroppableSilentFrames(int silentFrames); - + protected: AudioRingBuffer _ringBuffer; @@ -147,7 +146,7 @@ public slots: bool _lastPopSucceeded { false }; AudioRingBuffer::ConstIterator _lastPopOutput; - + bool _dynamicJitterBufferEnabled { DEFAULT_DYNAMIC_JITTER_BUFFER_ENABLED }; int _staticJitterBufferFrames { DEFAULT_STATIC_JITTER_FRAMES }; int _desiredJitterBufferFrames; @@ -185,7 +184,7 @@ public slots: float _reverbTime { 0.0f }; float _wetLevel { 0.0f }; - CodecPluginPointer _codec; + std::shared_ptr _codec; QString _selectedCodecName; QMutex _decoderMutex; Decoder* _decoder { nullptr }; diff --git a/libraries/audio/src/MixedProcessedAudioStream.h b/libraries/audio/src/MixedProcessedAudioStream.h index 5732f32e902..c237c4e8e18 100644 --- a/libraries/audio/src/MixedProcessedAudioStream.h +++ b/libraries/audio/src/MixedProcessedAudioStream.h @@ -14,8 +14,6 @@ #include "InboundAudioStream.h" -class AudioClient; - class MixedProcessedAudioStream : public InboundAudioStream { Q_OBJECT public: diff --git a/libraries/audio/src/PositionalAudioStream.cpp b/libraries/audio/src/PositionalAudioStream.cpp index 4161b660608..4c370571661 100644 --- a/libraries/audio/src/PositionalAudioStream.cpp +++ b/libraries/audio/src/PositionalAudioStream.cpp @@ -81,7 +81,7 @@ int PositionalAudioStream::parsePositionalData(const QByteArray& positionalByteA packetStream.readRawData(reinterpret_cast(&_avatarBoundingBoxCorner), sizeof(_avatarBoundingBoxCorner)); packetStream.readRawData(reinterpret_cast(&_avatarBoundingBoxScale), sizeof(_avatarBoundingBoxScale)); - if (_avatarBoundingBoxCorner != _ignoreBox.getCorner()) { + if (_avatarBoundingBoxCorner != _ignoreBox.corner) { // if the ignore box corner changes, we need to re-calculate the ignore box calculateIgnoreBox(); } @@ -117,7 +117,7 @@ void PositionalAudioStream::calculateIgnoreBox() { scale *= IGNORE_BOX_SCALE_FACTOR; // create the box (we use a box for the zone for convenience) - _ignoreBox.setBox(_avatarBoundingBoxCorner, scale); + _ignoreBox = AABoxData{_avatarBoundingBoxCorner, scale}; } } diff --git a/libraries/audio/src/PositionalAudioStream.h b/libraries/audio/src/PositionalAudioStream.h index 01a714aeb42..6bf93bfe18b 100644 --- a/libraries/audio/src/PositionalAudioStream.h +++ b/libraries/audio/src/PositionalAudioStream.h @@ -13,7 +13,7 @@ #define hifi_PositionalAudioStream_h #include -#include +#include #include "InboundAudioStream.h" @@ -69,7 +69,7 @@ class PositionalAudioStream : public InboundAudioStream { const glm::vec3& getAvatarBoundingBoxCorner() const { return _avatarBoundingBoxCorner; } const glm::vec3& getAvatarBoundingBoxScale() const { return _avatarBoundingBoxScale; } - using IgnoreBox = AABox; + using IgnoreBox = AABoxData; // called from single AudioMixerSlave while processing packets for node void enableIgnoreBox(); diff --git a/libraries/avatars-core/CMakeLists.txt b/libraries/avatars-core/CMakeLists.txt new file mode 100644 index 00000000000..dd86c1e5c76 --- /dev/null +++ b/libraries/avatars-core/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME avatars-core) +setup_hifi_library(Network) +link_hifi_libraries(shared networking) diff --git a/libraries/avatars-core/src/AssociatedTraitValues.h b/libraries/avatars-core/src/AssociatedTraitValues.h new file mode 100644 index 00000000000..0df8cd9bb52 --- /dev/null +++ b/libraries/avatars-core/src/AssociatedTraitValues.h @@ -0,0 +1,207 @@ +// +// AssociatedTraitValues.h +// libraries/avatars/src +// +// Created by Stephen Birarda on 8/8/18. +// Copyright 2018 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_AssociatedTraitValues_h +#define hifi_AssociatedTraitValues_h + +#include "AvatarTraits.h" + +// This templated class is admittedly fairly confusing to look at. It is used +// to hold some associated value of type T for both simple (non-instanced) and instanced traits. +// Most of the complexity comes from the fact that simple and instanced trait types are +// handled differently. For each simple trait type there can be a value T, but for +// each instance of each instanced trait +// (keyed by a TraitInstanceID, which at the time of this writing is a UUID) there can be a value T. +// There are separate methods in most cases for simple traits and instanced traits +// because of this different behaviour. This class is not used to hold the values +// of the traits themselves, but instead an associated value like the latest version +// of each trait (see TraitVersions) or a state associated with each trait (like added/changed/deleted). + +namespace AvatarTraits { + template + class AssociatedTraitValues { + using SimpleTypesArray = std::array; + public: + // constructor that pre-fills _simpleTypes with the default value specified by the template + AssociatedTraitValues() { std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue); } + + /// inserts the given value for the given simple trait type + void insert(TraitType type, T value) { _simpleTypes[type] = value; } + /// resets the simple trait type value to the default + void erase(TraitType type) { _simpleTypes[type] = defaultValue; } + + /// returns a reference to the value for a given instance for a given instanced trait type + T& getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID); + + /// inserts the passed value for the given instance for the given instanced trait type + void instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value); + + struct InstanceIDValuePair { + TraitInstanceID id; + T value; + + InstanceIDValuePair(TraitInstanceID id, T value) : id(id), value(value) {}; + }; + + using InstanceIDValuePairs = std::vector; + + /// returns a vector of InstanceIDValuePair objects for the given instanced trait type + InstanceIDValuePairs& getInstanceIDValuePairs(TraitType traitType); + + /// erases the a given instance for a given instanced trait type + void instanceErase(TraitType traitType, TraitInstanceID instanceID); + /// erases the value for all instances for a given instanced trait type + void eraseAllInstances(TraitType traitType); + + /// value getters for simple trait types, will be default value if value has been erased or not set + T operator[](TraitType traitType) const { return _simpleTypes[traitType]; } + T& operator[](TraitType traitType) { return _simpleTypes[traitType]; } + + /// resets all simple trait types to the default value and erases all values for instanced trait types + void reset() { + std::fill(_simpleTypes.begin(), _simpleTypes.end(), defaultValue); + _instancedTypes.clear(); + } + + /// const iterators for the vector of simple type values + typename SimpleTypesArray::const_iterator simpleCBegin() const { return _simpleTypes.cbegin(); } + typename SimpleTypesArray::const_iterator simpleCEnd() const { return _simpleTypes.cend(); } + + /// non-const iterators for the vector of simple type values + typename SimpleTypesArray::iterator simpleBegin() { return _simpleTypes.begin(); } + typename SimpleTypesArray::iterator simpleEnd() { return _simpleTypes.end(); } + + struct TraitWithInstances { + TraitType traitType; + InstanceIDValuePairs instances; + + TraitWithInstances(TraitType traitType) : traitType(traitType) {}; + TraitWithInstances(TraitType traitType, TraitInstanceID instanceID, T value) : + traitType(traitType), instances({{ instanceID, value }}) {}; + }; + + /// const iterators for the vector of TraitWithInstances objects + typename std::vector::const_iterator instancedCBegin() const { return _instancedTypes.cbegin(); } + typename std::vector::const_iterator instancedCEnd() const { return _instancedTypes.cend(); } + + /// non-const iterators for the vector of TraitWithInstances objects + typename std::vector::iterator instancedBegin() { return _instancedTypes.begin(); } + typename std::vector::iterator instancedEnd() { return _instancedTypes.end(); } + + private: + SimpleTypesArray _simpleTypes; + + /// return the iterator to the matching TraitWithInstances object for a given instanced trait type + typename std::vector::iterator instancesForTrait(TraitType traitType) { + return std::find_if(_instancedTypes.begin(), _instancedTypes.end(), + [traitType](TraitWithInstances& traitWithInstances){ + return traitWithInstances.traitType == traitType; + }); + } + + std::vector _instancedTypes; + }; + + /// returns a reference to the InstanceIDValuePairs object for a given instanced trait type + template + inline typename AssociatedTraitValues::InstanceIDValuePairs& + AssociatedTraitValues::getInstanceIDValuePairs(TraitType traitType) { + // first check if we already have some values for instances of this trait type + auto it = instancesForTrait(traitType); + + if (it != _instancedTypes.end()) { + return it->instances; + } else { + // if we didn't have any values for instances of the instanced trait type + // add an empty InstanceIDValuePairs object first and then return the reference to it + _instancedTypes.emplace_back(traitType); + return _instancedTypes.back().instances; + } + } + + // returns a reference to value for the given instance of the given instanced trait type + template + inline T& AssociatedTraitValues::getInstanceValueRef(TraitType traitType, TraitInstanceID instanceID) { + // first check if we already have some values for instances of this trait type + auto it = instancesForTrait(traitType); + + if (it != _instancedTypes.end()) { + // grab the matching vector of instances + auto& instancesVector = it->instances; + + // check if we have a value for this specific instance ID + auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(), + [instanceID](InstanceIDValuePair& idValuePair){ + return idValuePair.id == instanceID; + }); + if (instanceIt != instancesVector.end()) { + return instanceIt->value; + } else { + // no value for this specific instance ID, insert the default value and return it + instancesVector.emplace_back(instanceID, defaultValue); + return instancesVector.back().value; + } + } else { + // no values for any instances of this trait type + // insert the default value for the specific instance for the instanced trait type + _instancedTypes.emplace_back(traitType, instanceID, defaultValue); + return _instancedTypes.back().instances.back().value; + } + } + + /// inserts the passed value for the specific instance of the given instanced trait type + template + inline void AssociatedTraitValues::instanceInsert(TraitType traitType, TraitInstanceID instanceID, T value) { + // first check if we already have some instances for this trait type + auto it = instancesForTrait(traitType); + + if (it != _instancedTypes.end()) { + // found some instances for the instanced trait type, check if our specific instance is one of them + auto& instancesVector = it->instances; + auto instanceIt = std::find_if(instancesVector.begin(), instancesVector.end(), + [instanceID](InstanceIDValuePair& idValuePair){ + return idValuePair.id == instanceID; + }); + if (instanceIt != instancesVector.end()) { + // the instance already existed, update the value + instanceIt->value = value; + } else { + // the instance was not present, emplace the new value + instancesVector.emplace_back(instanceID, value); + } + } else { + // there were no existing instances for the given trait type + // setup the container for instances and insert the passed value for this instance ID + _instancedTypes.emplace_back(traitType, instanceID, value); + } + } + + /// erases the value for a specific instance of the given instanced trait type + template + inline void AssociatedTraitValues::instanceErase(TraitType traitType, TraitInstanceID instanceID) { + // check if we have any instances at all for this instanced trait type + auto it = instancesForTrait(traitType); + + if (it != _instancedTypes.end()) { + // we have some instances, erase the value for the passed instance ID if it is present + auto& instancesVector = it->instances; + instancesVector.erase(std::remove_if(instancesVector.begin(), + instancesVector.end(), + [&instanceID](InstanceIDValuePair& idValuePair){ + return idValuePair.id == instanceID; + })); + } + } + + using TraitVersions = AssociatedTraitValues; +}; + +#endif // hifi_AssociatedTraitValues_h diff --git a/libraries/avatars-core/src/AvatarDataStream.cpp b/libraries/avatars-core/src/AvatarDataStream.cpp new file mode 100644 index 00000000000..69245e430ba --- /dev/null +++ b/libraries/avatars-core/src/AvatarDataStream.cpp @@ -0,0 +1,144 @@ +// +// AvatarDataStream.cpp +// libraries/avatars-core/src +// +// Created by Nshan G. on 9 May 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 +// + +#include "AvatarDataStream.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AvatarLogging.h" +#include "AvatarTraits.h" +#include "ClientTraitsHandler.h" +#include "ResourceRequestObserver.h" + +size_t AvatarDataPacket::maxFaceTrackerInfoSize(size_t numBlendshapeCoefficients) { + return FACE_TRACKER_INFO_SIZE + numBlendshapeCoefficients * sizeof(float); +} + +size_t AvatarDataPacket::maxJointDataSize(size_t numJoints) { + const size_t validityBitsSize = calcBitVectorSize((int)numJoints); + + size_t totalSize = sizeof(uint8_t); // numJoints + + totalSize += validityBitsSize; // Orientations mask + totalSize += numJoints * sizeof(SixByteQuat); // Orientations + totalSize += validityBitsSize; // Translations mask + totalSize += sizeof(float); // maxTranslationDimension + totalSize += numJoints * sizeof(SixByteTrans); // Translations + return totalSize; +} + +size_t AvatarDataPacket::minJointDataSize(size_t numJoints) { + const size_t validityBitsSize = calcBitVectorSize((int)numJoints); + + size_t totalSize = sizeof(uint8_t); // numJoints + + totalSize += validityBitsSize; // Orientations mask + // assume no valid rotations + totalSize += validityBitsSize; // Translations mask + totalSize += sizeof(float); // maxTranslationDimension + // assume no valid translations + + return totalSize; +} + +size_t AvatarDataPacket::maxJointDefaultPoseFlagsSize(size_t numJoints) { + const size_t bitVectorSize = calcBitVectorSize((int)numJoints); + size_t totalSize = sizeof(uint8_t); // numJoints + + // one set of bits for rotation and one for translation + const size_t NUM_BIT_VECTORS_IN_DEFAULT_POSE_FLAGS_SECTION = 2; + totalSize += NUM_BIT_VECTORS_IN_DEFAULT_POSE_FLAGS_SECTION * bitVectorSize; + + return totalSize; +} + +/*@jsdoc + * Information on an attachment worn by the avatar. + * @typedef {object} AttachmentData + * @property {string} modelUrl - The URL of the glTF, FBX, or OBJ model file. glTF models may be in JSON or binary format + * (".gltf" or ".glb" URLs respectively). + * @property {string} jointName - The name of the joint that the attachment is parented to. + * @property {Vec3} translation - The offset from the joint that the attachment is positioned at. + * @property {Vec3} rotation - The rotation applied to the model relative to the joint orientation. + * @property {number} scale - The scale applied to the attachment model. + * @property {boolean} soft - If true and the model has a skeleton, the bones of the attached model's skeleton are + * rotated to fit the avatar's current pose. If true, the translation, rotation, and + * scale parameters are ignored. + */ +QVariant AttachmentData::toVariant() const { + QVariantMap result; + result["modelUrl"] = modelURL; + result["jointName"] = jointName; + result["translation"] = vec3ToQMap(translation); + result["rotation"] = vec3ToQMap(glm::degrees(safeEulerAngles(rotation))); + result["scale"] = scale; + result["soft"] = isSoft; + return result; +} + +glm::vec3 variantToVec3(const QVariant& var) { + auto map = var.toMap(); + glm::vec3 result; + result.x = map["x"].toFloat(); + result.y = map["y"].toFloat(); + result.z = map["z"].toFloat(); + return result; +} + +bool AttachmentData::fromVariant(const QVariant& variant) { + bool isValid = false; + auto map = variant.toMap(); + if (map.contains("modelUrl")) { + auto urlString = map["modelUrl"].toString(); + modelURL = urlString; + isValid = true; + } + if (map.contains("jointName")) { + jointName = map["jointName"].toString(); + } + if (map.contains("translation")) { + translation = variantToVec3(map["translation"]); + } + if (map.contains("rotation")) { + rotation = glm::quat(glm::radians(variantToVec3(map["rotation"]))); + } + if (map.contains("scale")) { + scale = map["scale"].toFloat(); + } + if (map.contains("soft")) { + isSoft = map["soft"].toBool(); + } + return isValid; +} diff --git a/libraries/avatars-core/src/AvatarDataStream.h b/libraries/avatars-core/src/AvatarDataStream.h new file mode 100755 index 00000000000..826369a9e0e --- /dev/null +++ b/libraries/avatars-core/src/AvatarDataStream.h @@ -0,0 +1,751 @@ +// +// AvatarDataStream.h +// libraries/avatars-core/src +// +// Created by Nshan G. on 21 Apr 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_AVATARS_CORE_SRC_AVATARDATASTREAM_H +#define LIBRARIES_AVATARS_CORE_SRC_AVATARDATASTREAM_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 "PathUtils.h" + +#include "AvatarTraits.h" +#include "ClientTraitsHandler.h" + +using AvatarEntityMap = QMap; +using PackedAvatarEntityMap = QMap; // similar to AvatarEntityMap, but different internal format +using AvatarEntityIDs = QSet; + +using AvatarGrabDataMap = QMap; + +using AvatarDataSequenceNumber = uint16_t; + +const int MAX_NUM_AVATAR_ENTITIES = 42; + +// avatar motion behaviors +const quint32 AVATAR_MOTION_ACTION_MOTOR_ENABLED = 1U << 0; +const quint32 AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED = 1U << 1; + +const quint32 AVATAR_MOTION_DEFAULTS = + AVATAR_MOTION_ACTION_MOTOR_ENABLED | + AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED; + +// these bits will be expanded as features are exposed +const quint32 AVATAR_MOTION_SCRIPTABLE_BITS = + AVATAR_MOTION_SCRIPTED_MOTOR_ENABLED; + +// Bitset of state flags - we store the key state, hand state, Faceshift, eye tracking, and existence of +// referential data in this bit set. The hand state is an octal, but is split into two sections to maintain +// backward compatibility. The bits are ordered as such (0-7 left to right). +// AA 6/1/18 added three more flags bits 8,9, and 10 for procedural audio, blink, and eye saccade enabled +// +// +-----+-----+-+-+-+--+--+--+--+-----+ +// |K0,K1|H0,H1|F|E|R|H2|Au|Bl|Ey|xxxxx| +// +-----+-----+-+-+-+--+--+--+--+-----+ +// +// Key state - K0,K1 is found in the 1st and 2nd bits +// Hand state - H0,H1,H2 is found in the 3rd, 4th, and 8th bits +// Face tracker - F is found in the 5th bit +// Eye tracker - E is found in the 6th bit +// Referential Data - R is found in the 7th bit +// Procedural audio to mouth movement is enabled 8th bit +// Procedural Blink is enabled 9th bit +// Procedural Eyelid is enabled 10th bit +// Procedural PROCEDURAL_BLINK_FACE_MOVEMENT is enabled 11th bit +// Procedural Collide with other avatars is enabled 12th bit +// Procedural Has Hero Priority is enabled 13th bit + +const int KEY_STATE_START_BIT = 0; // 1st and 2nd bits (UNUSED) +const int HAND_STATE_START_BIT = 2; // 3rd and 4th bits (UNUSED) +const int HAS_SCRIPTED_BLENDSHAPES = 4; // 5th bit +const int HAS_PROCEDURAL_EYE_MOVEMENT = 5; // 6th bit +const int HAS_REFERENTIAL = 6; // 7th bit +const int HAND_STATE_FINGER_POINTING_BIT = 7; // 8th bit (UNUSED) +const int AUDIO_ENABLED_FACE_MOVEMENT = 8; // 9th bit +const int PROCEDURAL_EYE_FACE_MOVEMENT = 9; // 10th bit +const int PROCEDURAL_BLINK_FACE_MOVEMENT = 10; // 11th bit +const int COLLIDE_WITH_OTHER_AVATARS = 11; // 12th bit +const int HAS_HERO_PRIORITY = 12; // 13th bit (be scared) + +/*@jsdoc + *

The pointing state of the hands is specified by the following values:

+ * + * + * + * + * + * + * + * + * + * + *
ValueDescription
0No hand is pointing.
1The left hand is pointing.
2The right hand is pointing.
4It is the index finger that is pointing.
+ *

The values for the hand states are added together to give the HandState value. For example, if the left + * hand's finger is pointing, the value is 1 + 4 == 5. + * @typedef {number} HandState + */ +const char HAND_STATE_NULL = 0; +const char LEFT_HAND_POINTING_FLAG = 1; +const char RIGHT_HAND_POINTING_FLAG = 2; +const char IS_FINGER_POINTING_FLAG = 4; + +// AvatarData state flags - we store the details about the packet encoding in the first byte, +// before the "header" structure +const char AVATARDATA_FLAGS_MINIMUM = 0; + +using SmallFloat = uint16_t; // a compressed float with less precision, user defined radix + +namespace AvatarSkeletonTrait { + enum BoneType : uint8_t { + SkeletonRoot = 0, + SkeletonChild, + NonSkeletonRoot, + NonSkeletonChild + }; + + PACKED_BEGIN struct Header { + float maxTranslationDimension; + float maxScaleDimension; + uint8_t numJoints; + uint16_t stringTableLength; + } PACKED_END; + + PACKED_BEGIN struct JointData { + uint16_t stringStart; + uint8_t stringLength; + uint8_t boneType; + uint8_t defaultTranslation[6]; + uint8_t defaultRotation[6]; + uint16_t defaultScale; + uint16_t jointIndex; + uint16_t parentIndex; + } PACKED_END; + + struct UnpackedJointData { + int stringStart; + int stringLength; + int boneType; + glm::vec3 defaultTranslation; + glm::quat defaultRotation; + float defaultScale; + int jointIndex; + int parentIndex; + QString jointName; + }; + + inline bool operator==(const AvatarSkeletonTrait::UnpackedJointData& a, const AvatarSkeletonTrait::UnpackedJointData& b) { + return a.stringStart == b.stringStart && + a.stringLength == b.stringLength && + a.defaultTranslation == b.defaultTranslation && + a.defaultRotation == b.defaultRotation && + a.defaultScale == b.defaultScale && + a.jointIndex == b.jointIndex && + a.parentIndex == b.parentIndex && + a.jointName == b.jointName && + a.boneType == b.boneType; + } + inline bool operator!=(const AvatarSkeletonTrait::UnpackedJointData& a, const AvatarSkeletonTrait::UnpackedJointData& b) { + return !(a == b); + } +} + +class AttachmentData; + +namespace AvatarDataPacket { + + // NOTE: every time avatar data is sent from mixer to client, it also includes the GUIID for the session + // this is 16bytes of data at 45hz that's 5.76kbps + // it might be nice to use a dictionary to compress that + + // Packet State Flags - we store the details about the existence of other records in this bitset: + // AvatarGlobalPosition, Avatar face tracker, eye tracking, and existence of + using HasFlags = uint16_t; + const HasFlags PACKET_HAS_AVATAR_GLOBAL_POSITION = 1U << 0; + const HasFlags PACKET_HAS_AVATAR_BOUNDING_BOX = 1U << 1; + const HasFlags PACKET_HAS_AVATAR_ORIENTATION = 1U << 2; + const HasFlags PACKET_HAS_AVATAR_SCALE = 1U << 3; + const HasFlags PACKET_HAS_LOOK_AT_POSITION = 1U << 4; + const HasFlags PACKET_HAS_AUDIO_LOUDNESS = 1U << 5; + const HasFlags PACKET_HAS_SENSOR_TO_WORLD_MATRIX = 1U << 6; + const HasFlags PACKET_HAS_ADDITIONAL_FLAGS = 1U << 7; + const HasFlags PACKET_HAS_PARENT_INFO = 1U << 8; + const HasFlags PACKET_HAS_AVATAR_LOCAL_POSITION = 1U << 9; + const HasFlags PACKET_HAS_HAND_CONTROLLERS = 1U << 10; + const HasFlags PACKET_HAS_FACE_TRACKER_INFO = 1U << 11; + const HasFlags PACKET_HAS_JOINT_DATA = 1U << 12; + const HasFlags PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS = 1U << 13; + const HasFlags PACKET_HAS_GRAB_JOINTS = 1U << 14; + const HasFlags ALL_HAS_FLAGS = (1U << 15) - 1; + const size_t AVATAR_HAS_FLAGS_SIZE = 2; + + using SixByteQuat = uint8_t[6]; + using SixByteTrans = uint8_t[6]; + + // NOTE: AvatarDataPackets start with a uint16_t sequence number that is not reflected in the Header structure. + + PACKED_BEGIN struct Header { + HasFlags packetHasFlags; // state flags, indicated which additional records are included in the packet + } PACKED_END; + const size_t HEADER_SIZE = 2; + static_assert(sizeof(Header) == HEADER_SIZE, "AvatarDataPacket::Header size doesn't match."); + + PACKED_BEGIN struct Vec3 { + float data[3]; // vector coordinates + } PACKED_END; + + PACKED_BEGIN struct Quat { + float data[4]; // quaternion components + } PACKED_END; + + PACKED_BEGIN struct AvatarGlobalPosition { + Vec3 globalPosition; // avatar's position + } PACKED_END; + const size_t AVATAR_GLOBAL_POSITION_SIZE = 12; + static_assert(sizeof(AvatarGlobalPosition) == AVATAR_GLOBAL_POSITION_SIZE, "AvatarDataPacket::AvatarGlobalPosition size doesn't match."); + + PACKED_BEGIN struct AvatarBoundingBox { + Vec3 avatarDimensions; // avatar's bounding box in world space units, but relative to the position. + Vec3 boundOriginOffset; // offset from the position of the avatar to the origin of the bounding box + } PACKED_END; + const size_t AVATAR_BOUNDING_BOX_SIZE = 24; + static_assert(sizeof(AvatarBoundingBox) == AVATAR_BOUNDING_BOX_SIZE, "AvatarDataPacket::AvatarBoundingBox size doesn't match."); + + PACKED_BEGIN struct AvatarOrientation { + SixByteQuat avatarOrientation; // encodeded and compressed by packOrientationQuatToSixBytes() + } PACKED_END; + const size_t AVATAR_ORIENTATION_SIZE = 6; + static_assert(sizeof(AvatarOrientation) == AVATAR_ORIENTATION_SIZE, "AvatarDataPacket::AvatarOrientation size doesn't match."); + + PACKED_BEGIN struct AvatarScale { + SmallFloat scale; // avatar's scale, compressed by packFloatRatioToTwoByte() + } PACKED_END; + const size_t AVATAR_SCALE_SIZE = 2; + static_assert(sizeof(AvatarScale) == AVATAR_SCALE_SIZE, "AvatarDataPacket::AvatarScale size doesn't match."); + + PACKED_BEGIN struct LookAtPosition { + Vec3 lookAtPosition; // world space position that eyes are focusing on. + // FIXME - unless the person has an eye tracker, this is simulated... + // a) maybe we can just have the client calculate this + // b) at distance this will be hard to discern and can likely be + // descimated or dropped completely + // + // POTENTIAL SAVINGS - 12 bytes + } PACKED_END; + const size_t LOOK_AT_POSITION_SIZE = 12; + static_assert(sizeof(LookAtPosition) == LOOK_AT_POSITION_SIZE, "AvatarDataPacket::LookAtPosition size doesn't match."); + + PACKED_BEGIN struct AudioLoudness { + uint8_t audioLoudness; // current loudness of microphone compressed with packFloatGainToByte() + } PACKED_END; + const size_t AUDIO_LOUDNESS_SIZE = 1; + static_assert(sizeof(AudioLoudness) == AUDIO_LOUDNESS_SIZE, "AvatarDataPacket::AudioLoudness size doesn't match."); + + PACKED_BEGIN struct SensorToWorldMatrix { + // FIXME - these 20 bytes are only used by viewers if my avatar has "attachments" + // we could save these bytes if no attachments are active. + // + // POTENTIAL SAVINGS - 20 bytes + + SixByteQuat sensorToWorldQuat; // 6 byte compressed quaternion part of sensor to world matrix + uint16_t sensorToWorldScale; // uniform scale of sensor to world matrix + float sensorToWorldTrans[3]; // fourth column of sensor to world matrix + // FIXME - sensorToWorldTrans might be able to be better compressed if it was + // relative to the avatar position. + } PACKED_END; + const size_t SENSOR_TO_WORLD_SIZE = 20; + static_assert(sizeof(SensorToWorldMatrix) == SENSOR_TO_WORLD_SIZE, "AvatarDataPacket::SensorToWorldMatrix size doesn't match."); + + PACKED_BEGIN struct AdditionalFlags { + uint16_t flags; // additional flags: hand state, key state, eye tracking + } PACKED_END; + const size_t ADDITIONAL_FLAGS_SIZE = 2; + static_assert(sizeof(AdditionalFlags) == ADDITIONAL_FLAGS_SIZE, "AvatarDataPacket::AdditionalFlags size doesn't match."); + + // only present if HAS_REFERENTIAL flag is set in AvatarInfo.flags + PACKED_BEGIN struct ParentInfo { + uint8_t parentUUID[NUM_BYTES_RFC4122_UUID ]; // rfc 4122 encoded + uint16_t parentJointIndex; + } PACKED_END; + const size_t PARENT_INFO_SIZE = 18; + static_assert(sizeof(ParentInfo) == PARENT_INFO_SIZE, "AvatarDataPacket::ParentInfo size doesn't match."); + + // will only ever be included if the avatar has a parent but can change independent of changes to parent info + // and so we keep it a separate record + PACKED_BEGIN struct AvatarLocalPosition { + Vec3 localPosition; // parent frame translation of the avatar + } PACKED_END; + + const size_t AVATAR_LOCAL_POSITION_SIZE = 12; + static_assert(sizeof(AvatarLocalPosition) == AVATAR_LOCAL_POSITION_SIZE, "AvatarDataPacket::AvatarLocalPosition size doesn't match."); + + PACKED_BEGIN struct HandControllers { + SixByteQuat leftHandRotation; + SixByteTrans leftHandTranslation; + SixByteQuat rightHandRotation; + SixByteTrans rightHandTranslation; + } PACKED_END; + static const size_t HAND_CONTROLLERS_SIZE = 24; + static_assert(sizeof(HandControllers) == HAND_CONTROLLERS_SIZE, "AvatarDataPacket::HandControllers size doesn't match."); + + const size_t MAX_CONSTANT_HEADER_SIZE = HEADER_SIZE + + AVATAR_GLOBAL_POSITION_SIZE + + AVATAR_BOUNDING_BOX_SIZE + + AVATAR_ORIENTATION_SIZE + + AVATAR_SCALE_SIZE + + LOOK_AT_POSITION_SIZE + + AUDIO_LOUDNESS_SIZE + + SENSOR_TO_WORLD_SIZE + + ADDITIONAL_FLAGS_SIZE + + PARENT_INFO_SIZE + + AVATAR_LOCAL_POSITION_SIZE + + HAND_CONTROLLERS_SIZE; + + // variable length structure follows + + // only present if HAS_SCRIPTED_BLENDSHAPES flag is set in AvatarInfo.flags + PACKED_BEGIN struct FaceTrackerInfo { + float leftEyeBlink; + float rightEyeBlink; + float averageLoudness; + float browAudioLift; + uint8_t numBlendshapeCoefficients; + // float blendshapeCoefficients[numBlendshapeCoefficients]; + } PACKED_END; + const size_t FACE_TRACKER_INFO_SIZE = 17; + static_assert(sizeof(FaceTrackerInfo) == FACE_TRACKER_INFO_SIZE, "AvatarDataPacket::FaceTrackerInfo size doesn't match."); + size_t maxFaceTrackerInfoSize(size_t numBlendshapeCoefficients); + + /* + struct JointData { + uint8_t numJoints; + uint8_t rotationValidityBits[ceil(numJoints / 8)]; // one bit per joint, if true then a compressed rotation follows. + SixByteQuat rotation[numValidRotations]; // encodeded and compressed by packOrientationQuatToSixBytes() + uint8_t translationValidityBits[ceil(numJoints / 8)]; // one bit per joint, if true then a compressed translation follows. + float maxTranslationDimension; // used to normalize fixed point translation values. + SixByteTrans translation[numValidTranslations]; // normalized and compressed by packFloatVec3ToSignedTwoByteFixed() + SixByteQuat leftHandControllerRotation; + SixByteTrans leftHandControllerTranslation; + SixByteQuat rightHandControllerRotation; + SixByteTrans rightHandControllerTranslation; + }; + */ + size_t maxJointDataSize(size_t numJoints); + size_t minJointDataSize(size_t numJoints); + + /* + struct JointDefaultPoseFlags { + uint8_t numJoints; + uint8_t rotationIsDefaultPoseBits[ceil(numJoints / 8)]; + uint8_t translationIsDefaultPoseBits[ceil(numJoints / 8)]; + }; + */ + size_t maxJointDefaultPoseFlagsSize(size_t numJoints); + + PACKED_BEGIN struct FarGrabJoints { + Vec3 leftFarGrabPosition; // left controller far-grab joint position + Quat leftFarGrabRotation; // left controller far-grab joint rotation + Vec3 rightFarGrabPosition; // right controller far-grab joint position + Quat rightFarGrabRotation; // right controller far-grab joint rotation + Vec3 mouseFarGrabPosition; // mouse far-grab joint position + Quat mouseFarGrabRotation; // mouse far-grab joint rotation + } PACKED_END; + const size_t FAR_GRAB_JOINTS_SIZE = 84; + static_assert(sizeof(FarGrabJoints) == FAR_GRAB_JOINTS_SIZE, "AvatarDataPacket::FarGrabJoints size doesn't match."); + + static const size_t MIN_BULK_PACKET_SIZE = NUM_BYTES_RFC4122_UUID + HEADER_SIZE; + + // AvatarIdentity packet: + enum class IdentityFlag: quint32 {none, isReplicated = 0x1, lookAtSnapping = 0x2, verificationFailed = 0x4}; + Q_DECLARE_FLAGS(IdentityFlags, IdentityFlag) + + struct Identity { + QVector attachmentData; + QString displayName; + QString sessionDisplayName; + IdentityFlags identityFlags; + + bool operator==(const Identity& other) const { + return attachmentData == other.attachmentData && + displayName == other.displayName && + sessionDisplayName == other.sessionDisplayName && + identityFlags == other.identityFlags; + } + bool operator!=(const Identity& other) const { + return !(*this == other); + } + }; + + struct SendStatus { + HasFlags itemFlags { 0 }; + bool sendUUID { false }; + int rotationsSent { 0 }; // ie: index of next unsent joint + int translationsSent { 0 }; + operator bool() { return itemFlags == 0; } + }; +} + +const float MAX_AUDIO_LOUDNESS = 1000.0f; // close enough for mouth animation + +const QString DEFAULT_FULL_AVATAR_MODEL_NAME = QString("Default"); + +// how often should we send a full report about joint rotations, even if they haven't changed? +const float AVATAR_SEND_FULL_UPDATE_RATIO = 0.02f; +// this controls how large a change in joint-rotation must be before the interface sends it to the avatar mixer +const float AVATAR_MIN_ROTATION_DOT = 0.9999999f; +const float AVATAR_MIN_TRANSLATION = 0.0001f; + +// quaternion dot products +const float ROTATION_CHANGE_2D = 0.99984770f; // 2 degrees +const float ROTATION_CHANGE_4D = 0.99939083f; // 4 degrees +const float ROTATION_CHANGE_6D = 0.99862953f; // 6 degrees +const float ROTATION_CHANGE_15D = 0.99144486f; // 15 degrees +const float ROTATION_CHANGE_179D = 0.00872653f; // 179 degrees + +// rotation culling distance thresholds +const float AVATAR_DISTANCE_LEVEL_1 = 12.5f; // meters +const float AVATAR_DISTANCE_LEVEL_2 = 16.6f; // meters +const float AVATAR_DISTANCE_LEVEL_3 = 25.0f; // meters +const float AVATAR_DISTANCE_LEVEL_4 = 50.0f; // meters +const float AVATAR_DISTANCE_LEVEL_5 = 200.0f; // meters + +// Where one's own Avatar begins in the world (will be overwritten if avatar data file is found). +// This is the start location in the Sandbox (xyz: 6270, 211, 6000). +const glm::vec3 START_LOCATION(6270, 211, 6000); + +// Avatar Transit Constants +const float AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE = 1.0f; +const float AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE = 30.0f; +const int AVATAR_TRANSIT_FRAME_COUNT = 5; +const float AVATAR_TRANSIT_FRAMES_PER_METER = 0.5f; +const float AVATAR_TRANSIT_ABORT_DISTANCE = 0.1f; +const bool AVATAR_TRANSIT_DISTANCE_BASED = false; +const float AVATAR_TRANSIT_FRAMES_PER_SECOND = 30.0f; +const float AVATAR_PRE_TRANSIT_FRAME_COUNT = 10.0f; +const float AVATAR_POST_TRANSIT_FRAME_COUNT = 27.0f; + +enum KeyState : uint8_t { + NO_KEY_DOWN = 0, + INSERT_KEY_DOWN, + DELETE_KEY_DOWN +}; + +enum KillAvatarReason : uint8_t { + NoReason = 0, + AvatarDisconnected, + AvatarIgnored, + TheirAvatarEnteredYourBubble, + YourAvatarEnteredTheirBubble +}; + +Q_DECLARE_METATYPE(KillAvatarReason); + +class QDataStream; + +class AvatarDataRate { +public: + RateCounter<> globalPositionRate; + RateCounter<> localPositionRate; + RateCounter<> handControllersRate; + RateCounter<> avatarBoundingBoxRate; + RateCounter<> avatarOrientationRate; + RateCounter<> avatarScaleRate; + RateCounter<> lookAtPositionRate; + RateCounter<> audioLoudnessRate; + RateCounter<> sensorToWorldRate; + RateCounter<> additionalFlagsRate; + RateCounter<> parentInfoRate; + RateCounter<> faceTrackerRate; + RateCounter<> jointDataRate; + RateCounter<> jointDefaultPoseFlagsRate; + RateCounter<> farGrabJointRate; +}; + + +template +class AvatarDataStream { + + +public: + + AvatarDataStream(); + ~AvatarDataStream(); + + typedef enum { + NoData, + PALMinimum, + MinimumData, + CullSmallData, + IncludeSmallData, + SendAllData + } AvatarDataDetail; + + // FIXME: CRTP move to derived + // virtual QByteArray toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking = false); + // virtual QByteArray toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, const QVector& lastSentJointData, + // AvatarDataPacket::SendStatus& sendStatus, bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, + // QVector* sentJointDataOut, int maxDataSize = 0, AvatarDataRate* outboundDataRateOut = nullptr) const; + QByteArray toByteArray(AvatarDataPacket::HasFlags itemFlagsInit, AvatarDataDetail dataDetail, + const QVector& lastSentJointData, AvatarDataPacket::SendStatus& sendStatus, + bool distanceAdjust, glm::vec3 viewerPosition, + QVector* sentJointDataOut, + int maxDataSize, AvatarDataRate* outboundDataRateOut) const; + + void doneEncoding(bool cullSmallChanges, const AvatarDataPacket::SendStatus& sendStatus); + void doneEncoding(bool cullSmallChanges); + + int parseDataFromBuffer(const QByteArray& buffer); + + int getJointIndex(const QString& name) const; + + void storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& payload); + + void updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData); + + void clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree = true); + + void clearAvatarEntityInternal(const QUuid& entityID); + + void clearAvatarEntities(); + + QList getAvatarEntityIDs() const; + + // identityChanged returns true if identity has changed, false otherwise. Similarly for displayNameChanged and skeletonModelUrlChange. + void processAvatarIdentity(QDataStream& packetStream, bool& identityChanged, bool& displayNameChanged); + + QByteArray packTrait(AvatarTraits::TraitType traitType) const; + QByteArray packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); + + void processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData); + void processTraitInstance(AvatarTraits::TraitType traitType, + AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData); + void processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID); + + void prepareResetTraitInstances(); + + QByteArray identityByteArray(bool setIsReplicated = false) const; + + void sendSkeletonModelURL(); + + int getAverageBytesReceivedPerSecond() const; + int getReceiveRate() const; + + void setAvatarEntityDataChanged(bool value) { _avatarEntityDataChanged = value; } + AvatarEntityIDs getAndClearRecentlyRemovedIDs(); + + float getDataRate(const QString& rateName = QString("")) const; + + float getUpdateRate(const QString& rateName = QString("")) const; + + QVector getLastSentJointData(); + + bool hasProcessedFirstIdentity() const { return _hasProcessedFirstIdentity; } + + void setReplicaIndex(int replicaIndex) { _replicaIndex = replicaIndex; } + int getReplicaIndex() { return _replicaIndex; } + + void setIsNewAvatar(bool isNewAvatar) { _isNewAvatar = isNewAvatar; } + bool getIsNewAvatar() { return _isNewAvatar; } + void setIsClientAvatar(bool isClientAvatar) { _isClientAvatar = isClientAvatar; } + void sendSkeletonData(); + + int sendAllPackets(AvatarDataDetail, AvatarDataPacket::SendStatus&); + + int sendAvatarDataPacket(AvatarDataDetail, AvatarDataPacket::SendStatus&); + + int sendAvatarDataPacket(const QByteArray&); + + /*@jsdoc + * @function Avatar.sendAvatarDataPacket + * @param {boolean} [sendAll=false] - Send all. + * @returns {number} + * @deprecated This function is deprecated and will be removed. + */ + // FIXME: CRTP move to derived + // virtual int sendAvatarDataPacket(bool sendAll = false); + + int sendIdentityPacket(); + + QUuid grab(const QUuid& targetID, int parentJointIndex, + glm::vec3 positionalOffset, glm::quat rotationalOffset); + void releaseGrab(const QUuid& grabID); + +protected: + using Clock = std::chrono::system_clock; + using TimePoint = Clock::time_point; + + void insertRemovedEntityID(const QUuid entityID); + + float getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition) const; + float getDistanceBasedMinTranslationDistance(glm::vec3 viewerPosition) const; + + QByteArray packSkeletonData() const; + QByteArray packSkeletonModelURL() const; + QByteArray packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); + QByteArray packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID); + + void unpackSkeletonModelURL(const QByteArray& data); + void unpackSkeletonData(const QByteArray& data); + + QVector _lastSentJointData; ///< the state of the skeleton joints last time we transmitted + + bool _hasNewJointData { true }; // set in here, cleared in Avatar + + QUrl _skeletonModelURL; + + SimpleMovingAverage _averageBytesReceived; + + // Some rate data for incoming data in bytes + RateCounter<> _parseBufferRate; + RateCounter<> _globalPositionRate; + RateCounter<> _localPositionRate; + RateCounter<> _handControllersRate; + RateCounter<> _avatarBoundingBoxRate; + RateCounter<> _avatarOrientationRate; + RateCounter<> _avatarScaleRate; + RateCounter<> _lookAtPositionRate; + RateCounter<> _audioLoudnessRate; + RateCounter<> _sensorToWorldRate; + RateCounter<> _additionalFlagsRate; + RateCounter<> _parentInfoRate; + RateCounter<> _faceTrackerRate; + RateCounter<> _jointDataRate; + RateCounter<> _jointDefaultPoseFlagsRate; + RateCounter<> _farGrabJointRate; + + // Some rate data for incoming data updates + RateCounter<> _parseBufferUpdateRate; + RateCounter<> _globalPositionUpdateRate; + RateCounter<> _localPositionUpdateRate; + RateCounter<> _handControllersUpdateRate; + RateCounter<> _avatarBoundingBoxUpdateRate; + RateCounter<> _avatarOrientationUpdateRate; + RateCounter<> _avatarScaleUpdateRate; + RateCounter<> _lookAtPositionUpdateRate; + RateCounter<> _audioLoudnessUpdateRate; + RateCounter<> _sensorToWorldUpdateRate; + RateCounter<> _additionalFlagsUpdateRate; + RateCounter<> _parentInfoUpdateRate; + RateCounter<> _faceTrackerUpdateRate; + RateCounter<> _jointDataUpdateRate; + RateCounter<> _jointDefaultPoseFlagsUpdateRate; + RateCounter<> _farGrabJointUpdateRate; + + // Some rate data for outgoing data + AvatarDataRate _outboundDataRate; + + mutable ReadWriteLockable _avatarEntitiesLock; + AvatarEntityIDs _avatarEntityRemoved; // recently removed AvatarEntity ids + PackedAvatarEntityMap _packedAvatarEntityData; + bool _avatarEntityDataChanged { false }; + + mutable ReadWriteLockable _avatarGrabsLock; + AvatarGrabDataMap _avatarGrabData; + bool _avatarGrabDataChanged { false }; // by network + + int getFauxJointIndex(const QString& name) const; + + bool _hasProcessedFirstIdentity { false }; + int _replicaIndex { 0 }; + bool _isNewAvatar { true }; + bool _isClientAvatar { false }; + + bool updateAvatarGrabData(const QUuid& grabID, const QByteArray& grabData); + void clearAvatarGrabData(const QUuid& grabID); + +private: + Derived& derived(); + const Derived& derived() const; + + TimePoint _nextTraitsSendWindow; + +}; + +class AttachmentData { +public: + QUrl modelURL; + QString jointName; + glm::vec3 translation; + glm::quat rotation; + float scale { 1.0f }; + bool isSoft { false }; + + bool isValid() const { return modelURL.isValid(); } + + bool operator==(const AttachmentData& other) const; + + QVariant toVariant() const; + bool fromVariant(const QVariant& variant); +}; + +QDataStream& operator<<(QDataStream& out, const AttachmentData& attachment); +QDataStream& operator>>(QDataStream& in, AttachmentData& attachment); + +Q_DECLARE_METATYPE(AttachmentData) +Q_DECLARE_METATYPE(QVector) + +struct HandControllerVantage { + glm::vec3 position; + glm::quat orientation; +}; + +// faux joint indexes (-1 means invalid) +const int NO_JOINT_INDEX = 65535; // -1 +const int SENSOR_TO_WORLD_MATRIX_INDEX = 65534; // -2 +const int CONTROLLER_RIGHTHAND_INDEX = 65533; // -3 +const int CONTROLLER_LEFTHAND_INDEX = 65532; // -4 +const int CAMERA_RELATIVE_CONTROLLER_RIGHTHAND_INDEX = 65531; // -5 +const int CAMERA_RELATIVE_CONTROLLER_LEFTHAND_INDEX = 65530; // -6 +const int CAMERA_MATRIX_INDEX = 65529; // -7 +const int FARGRAB_RIGHTHAND_INDEX = 65528; // -8 +const int FARGRAB_LEFTHAND_INDEX = 65527; // -9 +const int FARGRAB_MOUSE_INDEX = 65526; // -10 + +const int LOWEST_PSEUDO_JOINT_INDEX = 65526; + +const int MAX_NUM_AVATAR_GRABS = 6; + +#endif /* end of include guard */ diff --git a/libraries/avatars-core/src/AvatarDataStream.hpp b/libraries/avatars-core/src/AvatarDataStream.hpp new file mode 100755 index 00000000000..8dac4fb24ca --- /dev/null +++ b/libraries/avatars-core/src/AvatarDataStream.hpp @@ -0,0 +1,2520 @@ +// +// AvatarDataStream.hpp +// libraries/avatars-core/src +// +// Created by Nshan G. on 21 Apr 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_AVATARS_CORE_SRC_AVATARDATASTREAM_HPP +#define LIBRARIES_AVATARS_CORE_SRC_AVATARDATASTREAM_HPP + +#include "AvatarDataStream.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 "AvatarLogging.h" +#include "AvatarTraits.h" +#include "ClientTraitsHandler.hpp" +#include "ResourceRequestObserver.h" + +//#define WANT_DEBUG + +using namespace std; + +inline const int TRANSLATION_COMPRESSION_RADIX = 14; +inline const int HAND_CONTROLLER_COMPRESSION_RADIX = 12; +inline const int SENSOR_TO_WORLD_SCALE_RADIX = 10; +inline const float AUDIO_LOUDNESS_SCALE = 1024.0f; +inline const float DEFAULT_AVATAR_DENSITY = 1000.0f; // density of water + +#define ASSERT(COND) do { if (!(COND)) { abort(); } } while(0) + +template +AvatarDataStream::AvatarDataStream() {} + +template +AvatarDataStream::~AvatarDataStream() {} + +template +Derived& AvatarDataStream::derived() { + return *static_cast(this); +} + +template +const Derived& AvatarDataStream::derived() const { + return *static_cast(this); +} + +template +float AvatarDataStream::getDistanceBasedMinRotationDOT(glm::vec3 viewerPosition) const { + auto pos = derived().getGlobalPositionOut(); + auto distance = glm::distance(glm::vec3(pos.globalPosition.data[0], pos.globalPosition.data[1], pos.globalPosition.data[2]), viewerPosition); + float result = ROTATION_CHANGE_179D; // assume worst + if (distance < AVATAR_DISTANCE_LEVEL_1) { + result = AVATAR_MIN_ROTATION_DOT; + } else if (distance < AVATAR_DISTANCE_LEVEL_2) { + result = ROTATION_CHANGE_2D; + } else if (distance < AVATAR_DISTANCE_LEVEL_3) { + result = ROTATION_CHANGE_4D; + } else if (distance < AVATAR_DISTANCE_LEVEL_4) { + result = ROTATION_CHANGE_6D; + } else if (distance < AVATAR_DISTANCE_LEVEL_5) { + result = ROTATION_CHANGE_15D; + } + return result; +} + +template +QUuid AvatarDataStream::grab(const QUuid& targetID, int parentJointIndex, + glm::vec3 positionalOffset, glm::quat rotationalOffset) { + auto grabID = QUuid::createUuid(); + // create a temporary grab object to get grabData + + QString hand = "none"; + if (parentJointIndex == CONTROLLER_RIGHTHAND_INDEX || + parentJointIndex == CAMERA_RELATIVE_CONTROLLER_RIGHTHAND_INDEX || + parentJointIndex == FARGRAB_RIGHTHAND_INDEX || + parentJointIndex == derived().getJointIndex("RightHand")) { + hand = "right"; + } else if (parentJointIndex == CONTROLLER_LEFTHAND_INDEX || + parentJointIndex == CAMERA_RELATIVE_CONTROLLER_LEFTHAND_INDEX || + parentJointIndex == FARGRAB_LEFTHAND_INDEX || + parentJointIndex == derived().getJointIndex("LeftHand")) { + hand = "left"; + } + + Grab tmpGrab(derived().getSessionUUIDOut(), + targetID, parentJointIndex, hand, positionalOffset, rotationalOffset); + QByteArray grabData = tmpGrab.toByteArray(); + bool dataChanged = updateAvatarGrabData(grabID, grabData); + + if (dataChanged && derived().getClientTraitsHandler()) { + // indicate that the changed data should be sent to the mixer + derived().getClientTraitsHandler()->markInstancedTraitUpdated(AvatarTraits::Grab, grabID); + } + + return grabID; +} + +template +void AvatarDataStream::releaseGrab(const QUuid& grabID) { + bool tellHandler { false }; + + _avatarGrabsLock.withWriteLock([&] { + + // FIXME: CRTP + // std::map::iterator itr; + // itr = _avatarGrabs.find(grabID); + // if (itr != _avatarGrabs.end()) { + // GrabPointer grab = itr->second; + // if (grab) { + // grab->setReleased(true); + // bool success; + // SpatiallyNestablePointer target = SpatiallyNestable::findByID(grab->getTargetID(), success); + // if (target && success) { + // target->disableGrab(grab); + // } + // } + // } + + if (_avatarGrabData.remove(grabID)) { + derived().onGrabRemoved(grabID); + // FIXE: CRTP + // _grabsToDelete.push_back(grabID); + tellHandler = true; + } + }); + + if (tellHandler && derived().getClientTraitsHandler()) { + // indicate the deletion of the data to the mixer + derived().getClientTraitsHandler()->markInstancedTraitDeleted(AvatarTraits::Grab, grabID); + } +} + +template +float AvatarDataStream::getDistanceBasedMinTranslationDistance(glm::vec3 viewerPosition) const { + return AVATAR_MIN_TRANSLATION; // Eventually make this distance sensitive as well +} + + +// FIXME: CRTP move to derived +// we want to track outbound data in this case... +// QByteArray AvatarData::toByteArrayStateful(AvatarDataDetail dataDetail, bool dropFaceTracking) { +// auto lastSentTime = _lastToByteArray; +// _lastToByteArray = usecTimestampNow(); +// AvatarDataPacket::SendStatus sendStatus; +// auto avatarByteArray = AvatarData::toByteArray(dataDetail, lastSentTime, getLastSentJointData(), +// sendStatus, dropFaceTracking, false, glm::vec3(0), nullptr, 0, &_outboundDataRate); +// return avatarByteArray; +// } + +// FIXME: CRTP move to derived +// QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSentTime, +// const QVector& lastSentJointData, AvatarDataPacket::SendStatus& sendStatus, +// bool dropFaceTracking, bool distanceAdjust, glm::vec3 viewerPosition, +// QVector* sentJointDataOut, +// int maxDataSize, AvatarDataRate* outboundDataRateOut) const { +// AvatarDataPacket::HasFlags itemFlagsInit = 0; +// +// lazyInitHeadData(); +// if(sendStatus.itemFlags == 0) { // New avatar ... +// bool hasAvatarGlobalPosition = true; // always include global position +// bool hasAvatarOrientation = false; +// bool hasAvatarBoundingBox = false; +// bool hasAvatarScale = false; +// bool hasLookAtPosition = false; +// bool hasAudioLoudness = false; +// bool hasSensorToWorldMatrix = false; +// bool hasJointData = false; +// bool hasJointDefaultPoseFlags = false; +// bool hasAdditionalFlags = false; +// +// // local position, and parent info only apply to avatars that are parented. The local position +// // and the parent info can change independently though, so we track their "changed since" +// // separately +// bool hasParentInfo = false; +// bool hasAvatarLocalPosition = false; +// bool hasHandControllers = false; +// +// bool hasFaceTrackerInfo = false; +// +// if (dataDetail == PALMinimum) { +// hasAudioLoudness = true; +// } else { +// bool sendAll = (dataDetail == SendAllData); +// hasAvatarOrientation = sendAll || rotationChangedSince(lastSentTime); +// hasAvatarBoundingBox = sendAll || avatarBoundingBoxChangedSince(lastSentTime); +// hasAvatarScale = sendAll || avatarScaleChangedSince(lastSentTime); +// hasLookAtPosition = sendAll || lookAtPositionChangedSince(lastSentTime); +// hasAudioLoudness = sendAll || audioLoudnessChangedSince(lastSentTime); +// hasSensorToWorldMatrix = sendAll || sensorToWorldMatrixChangedSince(lastSentTime); +// hasAdditionalFlags = sendAll || additionalFlagsChangedSince(lastSentTime); +// hasParentInfo = sendAll || parentInfoChangedSince(lastSentTime); +// hasAvatarLocalPosition = hasParent() && (sendAll || +// tranlationChangedSince(lastSentTime) || +// parentInfoChangedSince(lastSentTime)); +// hasHandControllers = _controllerLeftHandMatrixCache.isValid() || _controllerRightHandMatrixCache.isValid(); +// hasFaceTrackerInfo = !dropFaceTracking && (getHasScriptedBlendshapes() || _headData->_hasInputDrivenBlendshapes) && +// (sendAll || faceTrackerInfoChangedSince(lastSentTime)); +// hasJointData = !(dataDetail == MinimumData); + // hasJointDefaultPoseFlags = hasJointData; +// } +// +// bool leftValid = _farGrabLeftMatrixCache.isValid(); +// bool rightValid = _farGrabRightMatrixCache.isValid(); +// bool mouseValid = _farGrabMouseMatrixCache.isValid(); +// +// itemFlagsInit = +// (hasAvatarGlobalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_GLOBAL_POSITION : 0) +// | (hasAvatarBoundingBox ? AvatarDataPacket::PACKET_HAS_AVATAR_BOUNDING_BOX : 0) +// | (hasAvatarOrientation ? AvatarDataPacket::PACKET_HAS_AVATAR_ORIENTATION : 0) +// | (hasAvatarScale ? AvatarDataPacket::PACKET_HAS_AVATAR_SCALE : 0) +// | (hasLookAtPosition ? AvatarDataPacket::PACKET_HAS_LOOK_AT_POSITION : 0) +// | (hasAudioLoudness ? AvatarDataPacket::PACKET_HAS_AUDIO_LOUDNESS : 0) +// | (hasSensorToWorldMatrix ? AvatarDataPacket::PACKET_HAS_SENSOR_TO_WORLD_MATRIX : 0) +// | (hasAdditionalFlags ? AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS : 0) +// | (hasParentInfo ? AvatarDataPacket::PACKET_HAS_PARENT_INFO : 0) +// | (hasAvatarLocalPosition ? AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION : 0) +// | (hasHandControllers ? AvatarDataPacket::PACKET_HAS_HAND_CONTROLLERS : 0) +// | (hasFaceTrackerInfo ? AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO : 0) +// | (hasJointData ? AvatarDataPacket::PACKET_HAS_JOINT_DATA : 0) +// | (hasJointDefaultPoseFlags ? AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS : 0) +// | (hasJointData && (leftValid || rightValid || mouseValid) ? AvatarDataPacket::PACKET_HAS_GRAB_JOINTS : 0); +// } +// +// return toByteArray(itemFlagsInit, dataDetail, lastSentJointData, sendStatus, distanceAdjust, viewerPosition, +// sentJointDataOut, maxDataSize, outboundDataRateOut); +// } + +template +QByteArray AvatarDataStream::toByteArray(AvatarDataPacket::HasFlags itemFlagsInit, AvatarDataDetail dataDetail, + const QVector& lastSentJointData, AvatarDataPacket::SendStatus& sendStatus, + bool distanceAdjust, glm::vec3 viewerPosition, + QVector* sentJointDataOut, + int maxDataSize, AvatarDataRate* outboundDataRateOut) const { + + ASSERT(maxDataSize == 0 || (size_t)maxDataSize >= AvatarDataPacket::MIN_BULK_PACKET_SIZE); + + // special case, if we were asked for no data, then just include the flags all set to nothing + if (dataDetail == NoData) { + sendStatus.itemFlags = 0; + + QByteArray avatarDataByteArray; + if (sendStatus.sendUUID) { + auto convertedId = derived().getSessionUUIDOut().toRfc4122(); + avatarDataByteArray.append(convertedId.data(), NUM_BYTES_RFC4122_UUID); + } + + avatarDataByteArray.append((char*) &sendStatus.itemFlags, sizeof sendStatus.itemFlags); + return avatarDataByteArray; + } + + bool cullSmallChanges = (dataDetail == CullSmallData); + bool sendAll = (dataDetail == SendAllData); + + // Leading flags, to indicate how much data is actually included in the packet... + AvatarDataPacket::HasFlags wantedFlags = 0; + AvatarDataPacket::HasFlags includedFlags = 0; + AvatarDataPacket::HasFlags extraReturnedFlags = 0; // For partial joint data. + + + // FIXME - + // + // BUG -- if you enter a space bubble, and then back away, the avatar has wrong orientation until "send all" happens... + // this is an iFrame issue... what to do about that? + // + // BUG -- Resizing avatar seems to "take too long"... the avatar doesn't redraw at smaller size right away + // + // TODO consider these additional optimizations in the future + // 1) SensorToWorld - should we only send this for avatars with attachments?? - 20 bytes - 7.20 kbps + // 2) GUIID for the session change to 2byte index (savings) - 14 bytes - 5.04 kbps + // 3) Improve Joints -- currently we use rotational tolerances, but if we had skeleton/bone length data + // we could do a better job of determining if the change in joints actually translates to visible + // changes at distance. + // + // Potential savings: + // 63 rotations * 6 bytes = 136kbps + // 3 translations * 6 bytes = 6.48kbps + // + + QUuid parentID; + + if (sendStatus.itemFlags == 0) { // New avatar ... + wantedFlags = itemFlagsInit; + sendStatus.itemFlags = wantedFlags; + sendStatus.rotationsSent = 0; + sendStatus.translationsSent = 0; + } else { // Continuing avatar ... + wantedFlags = sendStatus.itemFlags; + if (wantedFlags & AvatarDataPacket::PACKET_HAS_GRAB_JOINTS) { + // Must send joints for grab joints - + wantedFlags |= AvatarDataPacket::PACKET_HAS_JOINT_DATA; + } + } + + auto [parentUUID, parentJointIndex] = derived().getParentInfoOut(); + // FIXME: CRTP + // parentUUID = parentID.toRfc4122(); + // parentJointIndex = getParentJointIndex(); + + auto [ + leftEyeBlink, + rightEyeBlink, + averageLoudness, + browAudioLift, + headDataBlendhapeCoefficientsSize, + headDataBlendhapeCoefficients + ] = derived().getFaceTrackerInfoOut(); + + // FIXME: CRTP + // leftEyeBlink = _headData->_leftEyeBlink; + // rightEyeBlink = _headData->_rightEyeBlink; + // averageLoudness = _headData->_averageLoudness; + // browAudioLift = _headData->_browAudioLift; + // headDataBlendhapeCoefficientsSize = _headData->getBlendshapeCoefficients().size(); + // headDataBlendhapeCoefficients = _headData->getBlendshapeCoefficients().data(); + + const int jointDataSize = derived().getJointDataSizeOut(); + + const size_t byteArraySize = AvatarDataPacket::MAX_CONSTANT_HEADER_SIZE + NUM_BYTES_RFC4122_UUID + + AvatarDataPacket::maxFaceTrackerInfoSize(headDataBlendhapeCoefficientsSize) + + AvatarDataPacket::maxJointDataSize(jointDataSize) + + AvatarDataPacket::maxJointDefaultPoseFlagsSize(jointDataSize) + + AvatarDataPacket::FAR_GRAB_JOINTS_SIZE; + + if (maxDataSize == 0) { + maxDataSize = (int)byteArraySize; + } + + QByteArray avatarDataByteArray((int)byteArraySize, 0); + unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); + const unsigned char* const startPosition = destinationBuffer; + const unsigned char* const packetEnd = destinationBuffer + maxDataSize; + +#define AVATAR_MEMCPY(src) \ + memcpy(destinationBuffer, &(src), sizeof(src)); \ + destinationBuffer += sizeof(src); + +// If we want an item and there's sufficient space: +#define IF_AVATAR_SPACE(flag, space) \ + if ((wantedFlags & AvatarDataPacket::flag) \ + && (packetEnd - destinationBuffer) >= (ptrdiff_t)(space) \ + && (includedFlags |= AvatarDataPacket::flag)) + + if (sendStatus.sendUUID) { + auto convertedId = derived().getSessionUUIDOut().toRfc4122(); + memcpy(destinationBuffer, convertedId.data(), NUM_BYTES_RFC4122_UUID); + destinationBuffer += NUM_BYTES_RFC4122_UUID; + } + + unsigned char * packetFlagsLocation = destinationBuffer; + destinationBuffer += sizeof(wantedFlags); + + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_GLOBAL_POSITION, sizeof( AvatarDataPacket::AvatarGlobalPosition)) { + auto startSection = destinationBuffer; + AvatarDataPacket::AvatarGlobalPosition globalPosition = derived().getGlobalPositionOut(); + AVATAR_MEMCPY(globalPosition); + + int numBytes = destinationBuffer - startSection; + + if (outboundDataRateOut) { + outboundDataRateOut->globalPositionRate.increment(numBytes); + } + } + + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_BOUNDING_BOX, sizeof(AvatarDataPacket::AvatarBoundingBox)) { + auto startSection = destinationBuffer; + AvatarDataPacket::AvatarBoundingBox bounds = derived().getBoundingBoxOut(); + + // FIXME: CRTP + // AVATAR_MEMCPY(_globalBoundingBoxDimensions); + // AVATAR_MEMCPY(_globalBoundingBoxOffset); + AVATAR_MEMCPY(bounds); + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->avatarBoundingBoxRate.increment(numBytes); + } + } + + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_ORIENTATION, sizeof(AvatarDataPacket::SixByteQuat)) { + auto startSection = destinationBuffer; + glm::quat localOrientation {}; + + destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, derived().getOrientationOut()); + // FIXME: CRTP Derived::getOrientationOut should + // return getOrientationOutbound(); + // which is virtual + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->avatarOrientationRate.increment(numBytes); + } + } + + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_SCALE, sizeof(AvatarDataPacket::AvatarScale)) { + auto startSection = destinationBuffer; + auto data = reinterpret_cast(destinationBuffer); + float scale = derived().getScaleOut(); + // FIXME: CRTP + // scale = getDomainLimitedScale(); + packFloatRatioToTwoByte((uint8_t*)(&data->scale), scale); + destinationBuffer += sizeof(AvatarDataPacket::AvatarScale); + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->avatarScaleRate.increment(numBytes); + } + } + + + IF_AVATAR_SPACE(PACKET_HAS_LOOK_AT_POSITION, sizeof(AvatarDataPacket::LookAtPosition) ) { + AvatarDataPacket::LookAtPosition lookAtPosition = derived().getLookAtPositionOut(); + + // TODO: CRTP + // lookAtPosition.LookAtPosition[0] = _headData->getLookAtPosition().x; + // lookAtPosition.LookAtPosition[0] = _headData->getLookAtPosition().y; + // lookAtPosition.LookAtPosition[0] = _headData->getLookAtPosition().z; + + auto startSection = destinationBuffer; + AVATAR_MEMCPY(lookAtPosition); + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->lookAtPositionRate.increment(numBytes); + } + } + + IF_AVATAR_SPACE(PACKET_HAS_AUDIO_LOUDNESS, sizeof(AvatarDataPacket::AudioLoudness)) { + auto startSection = destinationBuffer; + auto data = reinterpret_cast(destinationBuffer); + // FIXME: CRTP return gerAudioLoudness from Derived::getAudioLoudnessOut + data->audioLoudness = packFloatGainToByte(derived().getAudioLoudnessOut() / AUDIO_LOUDNESS_SCALE); + destinationBuffer += sizeof(AvatarDataPacket::AudioLoudness); + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->audioLoudnessRate.increment(numBytes); + } + } + + IF_AVATAR_SPACE(PACKET_HAS_SENSOR_TO_WORLD_MATRIX, sizeof(AvatarDataPacket::SensorToWorldMatrix)) { + auto startSection = destinationBuffer; + + auto data = reinterpret_cast(destinationBuffer); + + auto matrix = derived().getSensorToWorldMatrixOut(); + + // FIXME: CRTP + // glm::mat4 sensorToWorldMatrix = getSensorToWorldMatrix(); + // rotation = glmExtractRotation(sensorToWorldMatrix); + // scale = extractScale(sensorToWorldMatrix); + // translation = sensorToWorldMatrix[3]; + + data->sensorToWorldTrans[0] = matrix.translation[0]; + data->sensorToWorldTrans[1] = matrix.translation[1]; + data->sensorToWorldTrans[2] = matrix.translation[2]; + packOrientationQuatToSixBytes(data->sensorToWorldQuat, matrix.rotation); + packFloatScalarToSignedTwoByteFixed((uint8_t*)&data->sensorToWorldScale, matrix.scale.x, SENSOR_TO_WORLD_SCALE_RADIX); + + destinationBuffer += sizeof(AvatarDataPacket::SensorToWorldMatrix); + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->sensorToWorldRate.increment(numBytes); + } + } + + IF_AVATAR_SPACE(PACKET_HAS_ADDITIONAL_FLAGS, sizeof (AvatarDataPacket::AdditionalFlags)) { + auto startSection = destinationBuffer; + auto data = reinterpret_cast(destinationBuffer); + + uint16_t flags { 0 }; + + auto [ + keyState, + handState, + headHasScriptedBlendshapes, + headHasProceduralEyeMovement, + headHasAudioEnabledFaceMovement, + headHasProceduralEyeFaceMovement, + headHasProceduralBlinkFaceMovement, collideWithOtherAvatars, + hasPriority + ] = derived().getAdditionalFlagsOut(); + + // FIXME: CRTP + // keyState = _keyState; + // handState = _handState; + // headHasScriptedBlendshapes = _headData->_hasScriptedBlendshapes || _headData->_hasInputDrivenBlendshapes + // headHasProceduralEyeMovement = _headData->getProceduralAnimationFlag(HeadData::SaccadeProceduralEyeJointAnimation) && + // !_headData->getSuppressProceduralAnimationFlag(HeadData::SaccadeProceduralEyeJointAnimation); + // headHasAudioEnabledFaceMovement = _headData->getProceduralAnimationFlag(HeadData::AudioProceduralBlendshapeAnimation) && + // !_headData->getSuppressProceduralAnimationFlag(HeadData::AudioProceduralBlendshapeAnimation) + // headHasProceduralEyeFaceMovement = _headData->getProceduralAnimationFlag(HeadData::LidAdjustmentProceduralBlendshapeAnimation) && + // !_headData->getSuppressProceduralAnimationFlag(HeadData::LidAdjustmentProceduralBlendshapeAnimation); + // headHasProceduralBlinkFaceMovement = _headData->getProceduralAnimationFlag(HeadData::BlinkProceduralBlendshapeAnimation) && + // !_headData->getSuppressProceduralAnimationFlag(HeadData::BlinkProceduralBlendshapeAnimation); + // collideWithOtherAvatars = _collideWithOtherAvatars; + // hasPriority = getHasPriority(); + + setSemiNibbleAt(flags, KEY_STATE_START_BIT, keyState); + + // hand state + bool isFingerPointing = handState & IS_FINGER_POINTING_FLAG; + setSemiNibbleAt(flags, HAND_STATE_START_BIT, handState & ~IS_FINGER_POINTING_FLAG); + if (isFingerPointing) { + setAtBit16(flags, HAND_STATE_FINGER_POINTING_BIT); + } + + // face tracker state + if (headHasScriptedBlendshapes) { + setAtBit16(flags, HAS_SCRIPTED_BLENDSHAPES); + } + + // eye tracker state + if (headHasProceduralEyeMovement) { + setAtBit16(flags, HAS_PROCEDURAL_EYE_MOVEMENT); + } + + bool parentIsNull = std::all_of(parentUUID.begin(), parentUUID.end(), + std::logical_not<>()); + if (!parentIsNull) { + setAtBit16(flags, HAS_REFERENTIAL); + } + + // audio face movement + if (headHasAudioEnabledFaceMovement) { + setAtBit16(flags, AUDIO_ENABLED_FACE_MOVEMENT); + } + + // procedural eye face movement + if (headHasProceduralEyeFaceMovement) { + setAtBit16(flags, PROCEDURAL_EYE_FACE_MOVEMENT); + } + + // procedural blink face movement + if (headHasProceduralBlinkFaceMovement) { + setAtBit16(flags, PROCEDURAL_BLINK_FACE_MOVEMENT); + } + + // avatar collisions enabled + if (collideWithOtherAvatars) { + setAtBit16(flags, COLLIDE_WITH_OTHER_AVATARS); + } + + // Avatar has hero priority + if (hasPriority) { + setAtBit16(flags, HAS_HERO_PRIORITY); + } + + data->flags = flags; + destinationBuffer += sizeof(AvatarDataPacket::AdditionalFlags); + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->additionalFlagsRate.increment(numBytes); + } + } + + IF_AVATAR_SPACE(PACKET_HAS_PARENT_INFO, sizeof(AvatarDataPacket::ParentInfo)) { + auto startSection = destinationBuffer; + auto parentInfo = reinterpret_cast(destinationBuffer); + + memcpy(parentInfo->parentUUID, parentUUID.data(), parentUUID.size()); + parentInfo->parentJointIndex = parentJointIndex; + destinationBuffer += sizeof(AvatarDataPacket::ParentInfo); + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->parentInfoRate.increment(numBytes); + } + } + + IF_AVATAR_SPACE(PACKET_HAS_AVATAR_LOCAL_POSITION, AvatarDataPacket::AVATAR_LOCAL_POSITION_SIZE) { + auto startSection = destinationBuffer; + AvatarDataPacket::AvatarLocalPosition localPosition = derived().getLocalPositionOut(); + // FIXME: CRTP + // localPosition = getLocalPosition(); + AVATAR_MEMCPY(localPosition); + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->localPositionRate.increment(numBytes); + } + } + + IF_AVATAR_SPACE(PACKET_HAS_HAND_CONTROLLERS, AvatarDataPacket::HAND_CONTROLLERS_SIZE) { + auto startSection = destinationBuffer; + + auto handControllers = derived().getHandControllersOut(); + + // FIXME: CRTP + // Transform controllerLeftHandTransform = Transform(getControllerLeftHandMatrix()); + // Transform controllerRightHandTransform = Transform(getControllerRightHandMatrix()); + // leftRotation = controllerLeftHandTransform.getRotation(); + // leftTranslation = controllerLeftHandTransform.getTranslation(); + // rightRotation = controllerRightHandTransform.getRotation(); + // rightTranslation = controllerRightHandTransform.getTranslation(); + + destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, handControllers.left.orientation); + destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, handControllers.left.position, HAND_CONTROLLER_COMPRESSION_RADIX); + destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, handControllers.right.orientation); + destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, handControllers.right.position, HAND_CONTROLLER_COMPRESSION_RADIX); + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->handControllersRate.increment(numBytes); + } + } + + IF_AVATAR_SPACE(PACKET_HAS_FACE_TRACKER_INFO, sizeof(AvatarDataPacket::FaceTrackerInfo) + headDataBlendhapeCoefficientsSize * sizeof(float)) { + auto startSection = destinationBuffer; + auto faceTrackerInfo = reinterpret_cast(destinationBuffer); + // note: we don't use the blink and average loudness, we just use the numBlendShapes and + // compute the procedural info on the client side. + + faceTrackerInfo->leftEyeBlink = leftEyeBlink; + faceTrackerInfo->rightEyeBlink = rightEyeBlink; + faceTrackerInfo->averageLoudness = averageLoudness; + faceTrackerInfo->browAudioLift = browAudioLift; + + assert(headDataBlendhapeCoefficientsSize <= std::numeric_limits::max()); + faceTrackerInfo->numBlendshapeCoefficients = static_cast(headDataBlendhapeCoefficientsSize); + destinationBuffer += sizeof(AvatarDataPacket::FaceTrackerInfo); + + auto blendshapeDataSize = headDataBlendhapeCoefficientsSize * sizeof(float); + memcpy(destinationBuffer, headDataBlendhapeCoefficients, blendshapeDataSize); + destinationBuffer += blendshapeDataSize; + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->faceTrackerRate.increment(numBytes); + } + } + + const bool packetHasJointData = wantedFlags & (AvatarDataPacket::PACKET_HAS_JOINT_DATA | AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS); + const int numJoints = packetHasJointData ? jointDataSize : 0; + + assert(numJoints <= 255); + const int jointBitVectorSize = calcBitVectorSize(numJoints); + + // include jointData if there is room for the most minimal section. i.e. no translations or rotations. + IF_AVATAR_SPACE(PACKET_HAS_JOINT_DATA, AvatarDataPacket::minJointDataSize(numJoints)) { + auto startSection = destinationBuffer; + { + // FIXME: CRTP + // QReadLocker readLock(&_jointDataLock); + + // Minimum space required for another rotation joint - + // size of joint + following translation bit-vector + translation scale: + const ptrdiff_t minSizeForJoint = sizeof(AvatarDataPacket::SixByteQuat) + jointBitVectorSize + sizeof(float); + + + // compute maxTranslationDimension before we send any joint data. + float maxTranslationDimension = 0.001f; + for (int i = sendStatus.translationsSent; i < numJoints; ++i) { + const JointData& data = derived().getJointDataOut(i); + // FIXME: CRTP + // const JointData& data = _jointData[i]; + if (!data.translationIsDefaultPose) { + maxTranslationDimension = glm::max(fabsf(data.translation.x), maxTranslationDimension); + maxTranslationDimension = glm::max(fabsf(data.translation.y), maxTranslationDimension); + maxTranslationDimension = glm::max(fabsf(data.translation.z), maxTranslationDimension); + } + } + + // joint rotation data + *destinationBuffer++ = (uint8_t)numJoints; + + unsigned char* validityPosition = destinationBuffer; + memset(validityPosition, 0, jointBitVectorSize); + +#ifdef WANT_DEBUG + int rotationSentCount = 0; + unsigned char* beforeRotations = destinationBuffer; +#endif + + destinationBuffer += jointBitVectorSize; // Move pointer past the validity bytes + + // sentJointDataOut and lastSentJointData might be the same vector + if (sentJointDataOut) { + sentJointDataOut->resize(numJoints); // Make sure the destination is resized before using it + } + JointData *const sentJoints = sentJointDataOut ? sentJointDataOut->data() : nullptr; + + float minRotationDOT = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinRotationDOT(viewerPosition) : AVATAR_MIN_ROTATION_DOT; + + int i = sendStatus.rotationsSent; + for (; i < numJoints; ++i) { + const JointData& data = derived().getJointDataOut(i); + const JointData& last = lastSentJointData[i]; + + if (packetEnd - destinationBuffer >= minSizeForJoint) { + if (!data.rotationIsDefaultPose) { + // The dot product for larger rotations is a lower number, + // so if the dot() is less than the value, then the rotation is a larger angle of rotation + if (sendAll || last.rotationIsDefaultPose || (!cullSmallChanges && last.rotation != data.rotation) + || (cullSmallChanges && fabsf(glm::dot(last.rotation, data.rotation)) < minRotationDOT)) { + validityPosition[i / BITS_IN_BYTE] |= 1 << (i % BITS_IN_BYTE); +#ifdef WANT_DEBUG + rotationSentCount++; +#endif + destinationBuffer += packOrientationQuatToSixBytes(destinationBuffer, data.rotation); + if (sentJoints) { + sentJoints[i].rotation = data.rotation; + } + } + } + } else { + break; + } + + if (sentJoints) { + sentJoints[i].rotationIsDefaultPose = data.rotationIsDefaultPose; + } + + } + sendStatus.rotationsSent = i; + + // joint translation data + validityPosition = destinationBuffer; + +#ifdef WANT_DEBUG + int translationSentCount = 0; + unsigned char* beforeTranslations = destinationBuffer; +#endif + + memset(destinationBuffer, 0, jointBitVectorSize); + destinationBuffer += jointBitVectorSize; // Move pointer past the validity bytes + + // write maxTranslationDimension + AVATAR_MEMCPY(maxTranslationDimension); + + float minTranslation = (distanceAdjust && cullSmallChanges) ? getDistanceBasedMinTranslationDistance(viewerPosition) : AVATAR_MIN_TRANSLATION; + + i = sendStatus.translationsSent; + for (; i < numJoints; ++i) { + const JointData& data = derived().getJointDataOut(i); + const JointData& last = lastSentJointData[i]; + + // Note minSizeForJoint is conservative since there isn't a following bit-vector + scale. + if (packetEnd - destinationBuffer >= minSizeForJoint) { + if (!data.translationIsDefaultPose) { + if (sendAll || last.translationIsDefaultPose || (!cullSmallChanges && last.translation != data.translation) + || (cullSmallChanges && glm::distance(data.translation, lastSentJointData[i].translation) > minTranslation)) { + validityPosition[i / BITS_IN_BYTE] |= 1 << (i % BITS_IN_BYTE); +#ifdef WANT_DEBUG + translationSentCount++; +#endif + destinationBuffer += packFloatVec3ToSignedTwoByteFixed(destinationBuffer, data.translation / maxTranslationDimension, + TRANSLATION_COMPRESSION_RADIX); + + if (sentJoints) { + sentJoints[i].translation = data.translation; + } + } + } + } else { + break; + } + + if (sentJoints) { + sentJoints[i].translationIsDefaultPose = data.translationIsDefaultPose; + } + + } + sendStatus.translationsSent = i; + } + + IF_AVATAR_SPACE(PACKET_HAS_GRAB_JOINTS, sizeof (AvatarDataPacket::FarGrabJoints)) { + + AvatarDataPacket::FarGrabJoints farGrabJoints = derived().getFarGrabJointsOut(); + + // the far-grab joints may range further than 3 meters, so we can't use packFloatVec3ToSignedTwoByteFixed etc + auto startSection = destinationBuffer; + + // glm::mat4 leftFarGrabMatrix = _farGrabLeftMatrixCache.isValid() ? + // _farGrabLeftMatrixCache.get() : glm::mat4(); + // glm::mat4 rightFarGrabMatrix = _farGrabRightMatrixCache.isValid() ? + // _farGrabRightMatrixCache.get() : glm::mat4(); + // glm::mat4 mouseFarGrabMatrix = _farGrabMouseMatrixCache.isValid() ? + // _farGrabMouseMatrixCache.get() : glm::mat4() ; + // + // glm::vec3 leftFarGrabPosition = extractTranslation(leftFarGrabMatrix); + // glm::quat leftFarGrabRotation = extractRotation(leftFarGrabMatrix); + // glm::vec3 rightFarGrabPosition = extractTranslation(rightFarGrabMatrix); + // glm::quat rightFarGrabRotation = extractRotation(rightFarGrabMatrix); + // glm::vec3 mouseFarGrabPosition = extractTranslation(mouseFarGrabMatrix); + // glm::quat mouseFarGrabRotation = extractRotation(mouseFarGrabMatrix); + // + // farGrabJoints = AvatarDataPacket::FarGrabJoints { + // { leftFarGrabPosition.x, leftFarGrabPosition.y, leftFarGrabPosition.z }, + // { leftFarGrabRotation.w, leftFarGrabRotation.x, leftFarGrabRotation.y, leftFarGrabRotation.z }, + // { rightFarGrabPosition.x, rightFarGrabPosition.y, rightFarGrabPosition.z }, + // { rightFarGrabRotation.w, rightFarGrabRotation.x, rightFarGrabRotation.y, rightFarGrabRotation.z }, + // { mouseFarGrabPosition.x, mouseFarGrabPosition.y, mouseFarGrabPosition.z }, + // { mouseFarGrabRotation.w, mouseFarGrabRotation.x, mouseFarGrabRotation.y, mouseFarGrabRotation.z } + // }; + + memcpy(destinationBuffer, &farGrabJoints, sizeof(farGrabJoints)); + destinationBuffer += sizeof(AvatarDataPacket::FarGrabJoints); + int numBytes = destinationBuffer - startSection; + + if (outboundDataRateOut) { + outboundDataRateOut->farGrabJointRate.increment(numBytes); + } + } + +#ifdef WANT_DEBUG + if (sendAll) { + qCDebug(avatars) << "AvatarDataStream::toByteArray" << cullSmallChanges << sendAll + << "rotations:" << rotationSentCount << "translations:" << translationSentCount + << "largest:" << maxTranslationDimension + << "size:" + << (beforeRotations - startPosition) << "+" + << (beforeTranslations - beforeRotations) << "+" + << (destinationBuffer - beforeTranslations) << "=" + << (destinationBuffer - startPosition); + } +#endif + + if (sendStatus.rotationsSent != numJoints || sendStatus.translationsSent != numJoints) { + extraReturnedFlags |= AvatarDataPacket::PACKET_HAS_JOINT_DATA; + } + + int numBytes = destinationBuffer - startSection; + if (outboundDataRateOut) { + outboundDataRateOut->jointDataRate.increment(numBytes); + } + } + + IF_AVATAR_SPACE(PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS, 1 + 2 * jointBitVectorSize) { + auto startSection = destinationBuffer; + + // FIXME: CRTP + // QReadLocker readLock(&_jointDataLock); + + // write numJoints + *destinationBuffer++ = (uint8_t)numJoints; + + // write rotationIsDefaultPose bits + destinationBuffer += writeBitVector(destinationBuffer, numJoints, [&](int i) { + return derived().getJointDataOut(i).rotationIsDefaultPose; + }); + + // write translationIsDefaultPose bits + destinationBuffer += writeBitVector(destinationBuffer, numJoints, [&](int i) { + return derived().getJointDataOut(i).translationIsDefaultPose; + }); + + if (outboundDataRateOut) { + size_t numBytes = destinationBuffer - startSection; + outboundDataRateOut->jointDefaultPoseFlagsRate.increment(numBytes); + } + } + + memcpy(packetFlagsLocation, &includedFlags, sizeof(includedFlags)); + // Return dropped items. + sendStatus.itemFlags = (wantedFlags & ~includedFlags) | extraReturnedFlags; + + int avatarDataSize = destinationBuffer - startPosition; + + if (avatarDataSize > (int)byteArraySize) { + qCCritical(avatars) << "AvatarDataStream::toByteArray buffer overflow"; // We've overflown into the heap + ASSERT(false); + } + + return avatarDataByteArray.left(avatarDataSize); + +#undef AVATAR_MEMCPY +#undef IF_AVATAR_SPACE +} + +template +void AvatarDataStream::doneEncoding(bool cullSmallChanges) { + const auto jointDataSize = derived().getJointDataSizeOut(); + AvatarDataPacket::SendStatus SendStatus { 0, false, jointDataSize, jointDataSize }; + doneEncoding(cullSmallChanges, SendStatus); +} + +// NOTE: This is never used in a "distanceAdjust" mode, so it's ok that it doesn't use a variable minimum rotation/translation +template +void AvatarDataStream::doneEncoding(bool cullSmallChanges, const AvatarDataPacket::SendStatus& sendStatus) { + // The server has sent some joint-data to other nodes. Update _lastSentJointData. + const auto jointDataSize = derived().getJointDataSizeOut(); + + // FIXME: CRTP + // QReadLocker readLock(&_jointDataLock); + + if (jointDataSize > _lastSentJointData.size()) { + _lastSentJointData.resize(jointDataSize); + } + + for (int i = 0; i < sendStatus.rotationsSent; ++i) { + const JointData& data = derived().getJointDataOut(i); + if (_lastSentJointData[i].rotation != data.rotation) { + if (!cullSmallChanges || + fabsf(glm::dot(data.rotation, _lastSentJointData[i].rotation)) <= AVATAR_MIN_ROTATION_DOT) { + if (!data.rotationIsDefaultPose) { + _lastSentJointData[i].rotation = data.rotation; + } + } + } + } + + for (int i = 0; i < sendStatus.translationsSent; ++i) { + const JointData& data = derived().getJointDataOut(i); + if (_lastSentJointData[i].translation != data.translation) { + if (!cullSmallChanges || + glm::distance(data.translation, _lastSentJointData[i].translation) > AVATAR_MIN_TRANSLATION) { + if (!data.translationIsDefaultPose) { + _lastSentJointData[i].translation = data.translation; + } + } + } + } + +} + +// FIXME: CRTP move to base +// void setMatrixCache(ThreadSafeValueCache& matrixCache, const HandControllerVantage& vantage) { +// Transform transform; +// transform.setTranslation(vantage.position); +// transform.setRotation(vantage.orientation); +// matrixCache.set(transform.getMatrix()); +// } + +inline HandControllerVantage unpackHandController(const unsigned char*& sourceBuffer) { + HandControllerVantage vantage{}; + sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, vantage.orientation); + sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, vantage.position, HAND_CONTROLLER_COMPRESSION_RADIX); + return vantage; +} + + +// read data in packet starting at byte offset and return number of bytes parsed +template +int AvatarDataStream::parseDataFromBuffer(const QByteArray& buffer) { + + // FIXME: CRTP move to derived + // lazily allocate memory for HeadData in case we're not an Avatar instance + // lazyInitHeadData(); + + // FIXME: CRTP + // parentID = getParentID(); + + AvatarDataPacket::HasFlags packetStateFlags; + + const unsigned char* startPosition = reinterpret_cast(buffer.data()); + const unsigned char* endPosition = startPosition + buffer.size(); + const unsigned char* sourceBuffer = startPosition; + + auto packetReadCheck = [&](const char* name, int sizeToRead) { + if ((endPosition - sourceBuffer) < (int)sizeToRead) { + derived().onPacketTooSmallError(name, sizeToRead, endPosition - sourceBuffer); + // FIXME: CRTP + // if (shouldLogError(now)) { + // qCWarning(avatars) << "Avatar data packet too small, attempting to read " << + // #ITEM_NAME << ", only " << (endPosition - sourceBuffer) << + // " bytes left, " << getSessionUUID(); + // } + return false; + } + return true; + }; + + // read the packet flags + memcpy(&packetStateFlags, sourceBuffer, sizeof(packetStateFlags)); + sourceBuffer += sizeof(packetStateFlags); + + #define HAS_FLAG(B,F) ((B & F) == F) + + bool hasAvatarGlobalPosition = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_AVATAR_GLOBAL_POSITION); + bool hasAvatarBoundingBox = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_AVATAR_BOUNDING_BOX); + bool hasAvatarOrientation = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_AVATAR_ORIENTATION); + bool hasAvatarScale = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_AVATAR_SCALE); + bool hasLookAtPosition = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_LOOK_AT_POSITION); + bool hasAudioLoudness = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_AUDIO_LOUDNESS); + bool hasSensorToWorldMatrix = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_SENSOR_TO_WORLD_MATRIX); + bool hasAdditionalFlags = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_ADDITIONAL_FLAGS); + bool hasParentInfo = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_PARENT_INFO); + bool hasAvatarLocalPosition = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_AVATAR_LOCAL_POSITION); + bool hasHandControllers = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_HAND_CONTROLLERS); + bool hasFaceTrackerInfo = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_FACE_TRACKER_INFO); + bool hasJointData = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_JOINT_DATA); + bool hasJointDefaultPoseFlags = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_JOINT_DEFAULT_POSE_FLAGS); + bool hasGrabJoints = HAS_FLAG(packetStateFlags, AvatarDataPacket::PACKET_HAS_GRAB_JOINTS); + + // FIXME: CRTP will be needed in derived + // quint64 now = usecTimestampNow(); + + if (hasAvatarGlobalPosition) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("AvatarGlobalPosition", sizeof(AvatarDataPacket::AvatarGlobalPosition))) { + return buffer.size(); + } + + auto data = reinterpret_cast(sourceBuffer); + + derived().setGlobalPositionIn(*data); + + // FIXME: CRTP move to Derived::setGlobalPositionIn; + // glm::vec3 offset = glm::vec3(0.0f, 0.0f, 0.0f); + // if (_replicaIndex > 0) { + // const float SPACE_BETWEEN_AVATARS = 2.0f; + // const int AVATARS_PER_ROW = 3; + // int row = _replicaIndex % AVATARS_PER_ROW; + // int col = floor(_replicaIndex / AVATARS_PER_ROW); + // offset = glm::vec3(row * SPACE_BETWEEN_AVATARS, 0.0f, col * SPACE_BETWEEN_AVATARS); + // } + // _serverPosition = glm::vec3(data->globalPosition[0], data->globalPosition[1], data->globalPosition[2]) + offset; + // if (_isClientAvatar) { + // auto oneStepDistance = glm::length(_globalPosition - _serverPosition); + // if (oneStepDistance <= AVATAR_TRANSIT_MIN_TRIGGER_DISTANCE || oneStepDistance >= AVATAR_TRANSIT_MAX_TRIGGER_DISTANCE) { + // _globalPosition = _serverPosition; + // // if we don't have a parent, make sure to also set our local position + // if (parentID.isNull()) { + // setLocalPosition(_serverPosition); + // } + // } + // if (_globalPosition != _serverPosition) { + // _globalPositionChanged = now; + // } + // } else { + // if (_globalPosition != _serverPosition) { + // _globalPosition = _serverPosition; + // _globalPositionChanged = now; + // } + // if (parentID.isNull()) { + // setLocalPosition(_serverPosition); + // } + // } + + sourceBuffer += sizeof(AvatarDataPacket::AvatarGlobalPosition); + int numBytesRead = sourceBuffer - startSection; + _globalPositionRate.increment(numBytesRead); + _globalPositionUpdateRate.increment(); + } + + if (hasAvatarBoundingBox) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("AvatarBoundingBox", sizeof(AvatarDataPacket::AvatarBoundingBox))) { + return buffer.size(); + } + + auto data = reinterpret_cast(sourceBuffer); + + derived().setBoundingBoxIn(*data); + // FIXME: CRTP + // auto newDimensions = glm::vec3(data->avatarDimensions[0], data->avatarDimensions[1], data->avatarDimensions[2]); + // auto newOffset = glm::vec3(data->boundOriginOffset[0], data->boundOriginOffset[1], data->boundOriginOffset[2]); + // if (_globalBoundingBoxDimensions != newDimensions) { + // _globalBoundingBoxDimensions = newDimensions; + // _avatarBoundingBoxChanged = now; + // } + // if (_globalBoundingBoxOffset != newOffset) { + // _globalBoundingBoxOffset = newOffset; + // _avatarBoundingBoxChanged = now; + // } + // _defaultBubbleBox = computeBubbleBox(); + + sourceBuffer += sizeof(AvatarDataPacket::AvatarBoundingBox); + int numBytesRead = sourceBuffer - startSection; + _avatarBoundingBoxRate.increment(numBytesRead); + _avatarBoundingBoxUpdateRate.increment(); + } + + if (hasAvatarOrientation) { + auto startSection = sourceBuffer; + if (!packetReadCheck("AvatarOrientation", sizeof(AvatarDataPacket::AvatarOrientation))) { + return buffer.size(); + } + glm::quat newOrientation; + sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, newOrientation); + + derived().setOrientationIn(newOrientation); + // FIXME: CRTP + // glm::quat currentOrientation = getLocalOrientation(); + // if (currentOrientation != newOrientation) { + // _hasNewJointData = true; + // setLocalOrientation(newOrientation); + // } + + int numBytesRead = sourceBuffer - startSection; + _avatarOrientationRate.increment(numBytesRead); + _avatarOrientationUpdateRate.increment(); + } + + if (hasAvatarScale) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("AvatarScale", sizeof(AvatarDataPacket::AvatarScale))) { + return buffer.size(); + } + + auto data = reinterpret_cast(sourceBuffer); + float scale; + unpackFloatRatioFromTwoByte((uint8_t*)&data->scale, scale); + if (isNaN(scale)) { + derived().onParseError("Discard avatar data packet: scale NaN."); + // FIXME: CRTP + // if (shouldLogError(now)) { + // qCWarning(avatars) << "Discard avatar data packet: scale NaN, uuid " << getSessionUUID(); + // } + return buffer.size(); + } + derived().setScaleIn(scale); + // FIXME: CRTP + // setTargetScale(scale); + sourceBuffer += sizeof(AvatarDataPacket::AvatarScale); + int numBytesRead = sourceBuffer - startSection; + _avatarScaleRate.increment(numBytesRead); + _avatarScaleUpdateRate.increment(); + } + + if (hasLookAtPosition) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("LookAtPosition", sizeof(AvatarDataPacket::LookAtPosition))) { + return buffer.size(); + } + + auto data = reinterpret_cast(sourceBuffer); + if (std::any_of(+data->lookAtPosition.data, data->lookAtPosition.data + 3, (bool(*)(float))isNaN)) { + derived().onParseError("Discard avatar data packet: lookAtPosition is NaN"); + // FIXME: CRTP + // if (shouldLogError(now)) { + // qCWarning(avatars) << "Discard avatar data packet: lookAtPosition is NaN, uuid " << getSessionUUID(); + // } + return buffer.size(); + } + derived().setLookAtPositionIn(*data); + // FIXME: CRTP + // glm::vec3 lookAt = glm::vec3(data->lookAtPosition[0], data->lookAtPosition[1], data->lookAtPosition[2]); + // _headData->setLookAtPosition(lookAt); + sourceBuffer += sizeof(AvatarDataPacket::LookAtPosition); + int numBytesRead = sourceBuffer - startSection; + _lookAtPositionRate.increment(numBytesRead); + _lookAtPositionUpdateRate.increment(); + } + + if (hasAudioLoudness) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("AudioLoudness", sizeof(AvatarDataPacket::AudioLoudness))) { + return buffer.size(); + } + + auto data = reinterpret_cast(sourceBuffer); + float audioLoudness; + audioLoudness = unpackFloatGainFromByte(data->audioLoudness) * AUDIO_LOUDNESS_SCALE; + sourceBuffer += sizeof(AvatarDataPacket::AudioLoudness); + + if (isNaN(audioLoudness)) { + derived().onParseError("Discard avatar data packet: audioLoudness is NaN"); + // FIXME: CRTP + // if (shouldLogError(now)) { + // qCWarning(avatars) << "Discard avatar data packet: audioLoudness is NaN, uuid " << getSessionUUID(); + // } + return buffer.size(); + } + // FIXME: CRTP + // setAudioLoudness(audioLoudness); + derived().setAudioLoudnessIn(audioLoudness); + int numBytesRead = sourceBuffer - startSection; + _audioLoudnessRate.increment(numBytesRead); + _audioLoudnessUpdateRate.increment(); + } + + if (hasSensorToWorldMatrix) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("SensorToWorldMatrix", sizeof(AvatarDataPacket::SensorToWorldMatrix))) { + return buffer.size(); + } + + auto data = reinterpret_cast(sourceBuffer); + glm::quat sensorToWorldQuat; + unpackOrientationQuatFromSixBytes(data->sensorToWorldQuat, sensorToWorldQuat); + float sensorToWorldScale; + // Grab a local copy of sensorToWorldScale to be able to use the unpack function with a pointer on it, + // a direct pointer on the struct attribute triggers warnings because of potential misalignement. + auto srcSensorToWorldScale = data->sensorToWorldScale; + unpackFloatScalarFromSignedTwoByteFixed((int16_t*)&srcSensorToWorldScale, &sensorToWorldScale, SENSOR_TO_WORLD_SCALE_RADIX); + glm::vec3 sensorToWorldTrans(data->sensorToWorldTrans[0], data->sensorToWorldTrans[1], data->sensorToWorldTrans[2]); + + derived().setSensorToWorldMatrixIn({ + sensorToWorldTrans, + sensorToWorldQuat, + glm::vec3(sensorToWorldScale) + }); + // FIXME: CRTP + // glm::mat4 sensorToWorldMatrix = createMatFromScaleQuatAndPos(glm::vec3(sensorToWorldScale), sensorToWorldQuat, sensorToWorldTrans); + // if (_sensorToWorldMatrixCache.get() != sensorToWorldMatrix) { + // _sensorToWorldMatrixCache.set(sensorToWorldMatrix); + // _sensorToWorldMatrixChanged = now; + // } + + sourceBuffer += sizeof(AvatarDataPacket::SensorToWorldMatrix); + int numBytesRead = sourceBuffer - startSection; + _sensorToWorldRate.increment(numBytesRead); + _sensorToWorldUpdateRate.increment(); + } + + if (hasAdditionalFlags) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("AdditionalFlags", sizeof(AvatarDataPacket::AdditionalFlags))) { + return buffer.size(); + } + + auto data = reinterpret_cast(sourceBuffer); + uint16_t bitItems = data->flags; + + // key state, stored as a semi-nibble in the bitItems + auto newKeyState = (KeyState)getSemiNibbleAt(bitItems, KEY_STATE_START_BIT); + + // hand state, stored as a semi-nibble plus a bit in the bitItems + // we store the hand state as well as other items in a shared bitset. The hand state is an octal, but is split + // into two sections to maintain backward compatibility. The bits are ordered as such (0-7 left to right). + // AA 6/1/18 added three more flags bits 8,9, and 10 for procedural audio, blink, and eye saccade enabled + // +---+-----+-----+--+--+--+--+--+----+ + // |x,x|H0,H1|x,x,x|H2|Au|Bl|Ey|He|xxxx| + // +---+-----+-----+--+--+--+--+--+----+ + // Hand state - H0,H1,H2 is found in the 3rd, 4th, and 8th bits + // Hero-avatar status (He) - 12th bit + auto newHandState = getSemiNibbleAt(bitItems, HAND_STATE_START_BIT) + + (oneAtBit16(bitItems, HAND_STATE_FINGER_POINTING_BIT) ? IS_FINGER_POINTING_FLAG : 0); + + auto newHasScriptedBlendshapes = oneAtBit16(bitItems, HAS_SCRIPTED_BLENDSHAPES); + auto newHasProceduralEyeMovement = oneAtBit16(bitItems, HAS_PROCEDURAL_EYE_MOVEMENT); + auto newHasAudioEnabledFaceMovement = oneAtBit16(bitItems, AUDIO_ENABLED_FACE_MOVEMENT); + auto newHasProceduralEyeFaceMovement = oneAtBit16(bitItems, PROCEDURAL_EYE_FACE_MOVEMENT); + auto newHasProceduralBlinkFaceMovement = oneAtBit16(bitItems, PROCEDURAL_BLINK_FACE_MOVEMENT); + + auto newCollideWithOtherAvatars = oneAtBit16(bitItems, COLLIDE_WITH_OTHER_AVATARS); + auto newHasPriority = oneAtBit16(bitItems, HAS_HERO_PRIORITY); + + derived().setAdditionalFlagsIn({ + newKeyState, + static_cast(newHandState), + newHasScriptedBlendshapes, + newHasProceduralEyeMovement, + newHasAudioEnabledFaceMovement, + newHasProceduralEyeFaceMovement, + newHasProceduralBlinkFaceMovement, + newCollideWithOtherAvatars, + newHasPriority + }); + + // FIXME: CRTP + // KeyState keyState {}; + // keyState = _keyState; + + // FIXME: CRTP + // char handState {}; + // handState = _handState; + + //FIXME: CRTP + // bool headDataHasScriptedBlendshaped = false; + // bool headDataHasProceduralEyeMovement = false; + // bool headDataHasAudioEnabledFaceMovement = false; + // bool headDataHasProceduralEyeFaceMovement = false; + // bool headDataHasProceduralBlinkFaceMovement = false; + // bool collideWithOtherAvatars = false; + // bool hasPriority = false; + + // headDataHasScriptedBlendshaped = _headData->getHasScriptedBlendshapes(); + // headDataHasProceduralEyeMovement = _headData->getProceduralAnimationFlag(HeadData::SaccadeProceduralEyeJointAnimation); + // headDataHasAudioEnabledFaceMovement = _headData->getProceduralAnimationFlag(HeadData::AudioProceduralBlendshapeAnimation); + // hasProceduralEyeFaceMovement = _headData->getProceduralAnimationFlag(HeadData::LidAdjustmentProceduralBlendshapeAnimation); + // hasProceduralBlinkFaceMovement = _headData->getProceduralAnimationFlag(HeadData::BlinkProceduralBlendshapeAnimation); + // collideWithOtherAvatars = _collideWithOtherAvatars; + // hasPriority = getHasPriority(); + + // bool keyStateChanged = (keyState != newKeyState); + // bool handStateChanged = (handState != newHandState); + // bool faceStateChanged = (headDataHasScriptedBlendshaped != newHasScriptedBlendshapes); + + // bool eyeStateChanged = (headDataHasProceduralEyeMovement != newHasProceduralEyeMovement); + // bool audioEnableFaceMovementChanged = (headDataHasAudioEnabledFaceMovement != newHasAudioEnabledFaceMovement); + // bool proceduralEyeFaceMovementChanged = (headDataHasProceduralEyeFaceMovement != newHasProceduralEyeFaceMovement); + // bool proceduralBlinkFaceMovementChanged = (headDataHasProceduralBlinkFaceMovement != newHasProceduralBlinkFaceMovement); + // bool collideWithOtherAvatarsChanged = (collideWithOtherAvatars != newCollideWithOtherAvatars); + // bool hasPriorityChanged = (hasPriority != newHasPriority); + // bool somethingChanged = keyStateChanged || handStateChanged || faceStateChanged || eyeStateChanged || audioEnableFaceMovementChanged || + // proceduralEyeFaceMovementChanged || + // proceduralBlinkFaceMovementChanged || collideWithOtherAvatarsChanged || hasPriorityChanged; + + // FIXME: CRTP + // _keyState = newKeyState; + // _handState = newHandState; + // TODO: this really belongs in headData + // if (!newHasScriptedBlendshapes && headDataHasScriptedBlendshaped) { + // // if scripted blendshapes have just been turned off, slam blendshapes back to zero. + // _headData->clearBlendshapeCoefficients(); + // } + // _headData->setHasScriptedBlendshapes(newHasScriptedBlendshapes); + // _headData->setProceduralAnimationFlag(HeadData::SaccadeProceduralEyeJointAnimation, newHasProceduralEyeMovement); + // _headData->setProceduralAnimationFlag(HeadData::AudioProceduralBlendshapeAnimation, newHasAudioEnabledFaceMovement); + // _headData->setProceduralAnimationFlag(HeadData::LidAdjustmentProceduralBlendshapeAnimation, newHasProceduralEyeFaceMovement); + // _headData->setProceduralAnimationFlag(HeadData::BlinkProceduralBlendshapeAnimation, newHasProceduralBlinkFaceMovement); + // _collideWithOtherAvatars = newCollideWithOtherAvatars; + // setHasPriorityWithoutTimestampReset(newHasPriority); + + sourceBuffer += sizeof(AvatarDataPacket::AdditionalFlags); + + // FIXME: CRTP + // if (collideWithOtherAvatarsChanged) { + // setCollisionWithOtherAvatarsFlags(); + // } + // if (somethingChanged) { + // _additionalFlagsChanged = now; + // } + int numBytesRead = sourceBuffer - startSection; + _additionalFlagsRate.increment(numBytesRead); + _additionalFlagsUpdateRate.increment(); + } + + if (hasParentInfo) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("ParentInfo", sizeof(AvatarDataPacket::ParentInfo))) { + return buffer.size(); + } + + auto parentInfo = reinterpret_cast(sourceBuffer); + sourceBuffer += sizeof(AvatarDataPacket::ParentInfo); + + derived().setParentInfoIn(parentInfo->parentUUID, parentInfo->parentJointIndex); + // FIXME: CRTP + // QByteArray byteArray((const char*)parentInfo->parentUUID, NUM_BYTES_RFC4122_UUID); + // auto newParentID = QUuid::fromRfc4122(byteArray); + // if ((getParentID() != newParentID) || (getParentJointIndex() != parentInfo->parentJointIndex)) { + // SpatiallyNestable::setParentID(newParentID); + // SpatiallyNestable::setParentJointIndex(parentInfo->parentJointIndex); + // _parentChanged = now; + // } + + int numBytesRead = sourceBuffer - startSection; + _parentInfoRate.increment(numBytesRead); + _parentInfoUpdateRate.increment(); + } + + if (hasAvatarLocalPosition) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("AvatarLocalPosition", sizeof(AvatarDataPacket::AvatarLocalPosition))) { + return buffer.size(); + } + + auto data = reinterpret_cast(sourceBuffer); + + if (std::any_of(+data->localPosition.data, data->localPosition.data + 3, (bool(*)(float))isNaN)) { + derived().onParseError("Discard avatar data packet: position NaN."); + // FIXME: CRTP + // if (shouldLogError(now)) { + // qCWarning(avatars) << "Discard avatar data packet: position NaN, uuid " << getSessionUUID(); + // } + return buffer.size(); + } + derived().setLocalPositionIn(*data); + // FIXME: CRTP + // glm::vec3 position = glm::vec3(data->localPosition[0], data->localPosition[1], data->localPosition[2]); + // if (parentID.isNull()) { + // qCWarning(avatars) << "received localPosition for avatar with no parent"; + // } else { + // setLocalPosition(position); + // } + sourceBuffer += sizeof(AvatarDataPacket::AvatarLocalPosition); + int numBytesRead = sourceBuffer - startSection; + _localPositionRate.increment(numBytesRead); + _localPositionUpdateRate.increment(); + } + + if (hasHandControllers) { + auto startSection = sourceBuffer; + + + derived().setHandControllersIn({ + unpackHandController(sourceBuffer), + unpackHandController(sourceBuffer) + }); + + // FIXME: CRTP + // setMatrixCache(_controllerLeftHandMatrixCache, leftVantage); + // setMatrixCache(_controllerRightHandMatrixCache, rightVantage); + + int numBytesRead = sourceBuffer - startSection; + _handControllersRate.increment(numBytesRead); + _handControllersUpdateRate.increment(); + } else { + // FIXME: CRTP + // _controllerLeftHandMatrixCache.invalidate(); + // _controllerRightHandMatrixCache.invalidate(); + } + + if (hasFaceTrackerInfo) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("FaceTrackerInfo", sizeof(AvatarDataPacket::FaceTrackerInfo))) { + return buffer.size(); + } + + auto faceTrackerInfo = reinterpret_cast(sourceBuffer); + sourceBuffer += sizeof(AvatarDataPacket::FaceTrackerInfo); + + const int coefficientsSize = sizeof(float) * faceTrackerInfo->numBlendshapeCoefficients; + + if (!packetReadCheck("FaceTrackerCoefficients", coefficientsSize)) { + return buffer.size(); + } + + derived().setFaceTrackerInfoIn({ + faceTrackerInfo->leftEyeBlink, + faceTrackerInfo->rightEyeBlink, + faceTrackerInfo->averageLoudness, + faceTrackerInfo->browAudioLift, + faceTrackerInfo->numBlendshapeCoefficients, + sourceBuffer + }); + + // TODO: CRTP + // int numCoefficients = faceTrackerInfo->numBlendshapeCoefficients; + // const int coefficientsSize = sizeof(float) * numCoefficients; + // _headData->_blendshapeCoefficients.resize(std::min(numCoefficients, (int)Blendshapes::BlendshapeCount)); // make sure there's room for the copy! + //only copy the blendshapes to headData, not the procedural face info + // memcpy(_headData->_blendshapeCoefficients.data(), sourceBuffer, coefficientsSize); + + sourceBuffer += coefficientsSize; + + int numBytesRead = sourceBuffer - startSection; + _faceTrackerRate.increment(numBytesRead); + _faceTrackerUpdateRate.increment(); + } + + if (hasJointData) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("NumJoints", sizeof(uint8_t))) { + return buffer.size(); + } + + int numJoints = *sourceBuffer++; + const int bytesOfValidity = (int)ceil((float)numJoints / (float)BITS_IN_BYTE); + + if (!packetReadCheck("JointRotationValidityBits", bytesOfValidity)) { + return buffer.size(); + } + + int numValidJointRotations = 0; + QVector validRotations; + validRotations.resize(numJoints); + { // rotation validity bits + unsigned char validity = 0; + int validityBit = 0; + for (int i = 0; i < numJoints; i++) { + if (validityBit == 0) { + validity = *sourceBuffer++; + } + bool valid = (bool)(validity & (1 << validityBit)); + if (valid) { + ++numValidJointRotations; + } + validRotations[i] = valid; + validityBit = (validityBit + 1) % BITS_IN_BYTE; + } + } + + { + // FIXME: CRTP + // QWriteLocker writeLock(&_jointDataLock); + derived().setJointDataSizeIn(numJoints); + + // each joint rotation is stored in 6 bytes. + const int COMPRESSED_QUATERNION_SIZE = 6; + if (!packetReadCheck("JointRotations", numValidJointRotations * COMPRESSED_QUATERNION_SIZE)) { + return buffer.size(); + } + + for (int i = 0; i < numJoints; i++) { + glm::quat rotation{}; + if (validRotations[i]) { + sourceBuffer += unpackOrientationQuatFromSixBytes(sourceBuffer, rotation); + _hasNewJointData = true; + derived().setJointDataRotationIn(i, rotation); + derived().setJointDataRotationDefaultIn(i, false); + } + } + + if (!packetReadCheck("JointTranslationValidityBits", bytesOfValidity)) { + return buffer.size(); + } + + // get translation validity bits -- these indicate which translations were packed + int numValidJointTranslations = 0; + QVector validTranslations; + validTranslations.resize(numJoints); + { // translation validity bits + unsigned char validity = 0; + int validityBit = 0; + for (int i = 0; i < numJoints; i++) { + if (validityBit == 0) { + validity = *sourceBuffer++; + } + bool valid = (bool)(validity & (1 << validityBit)); + if (valid) { + ++numValidJointTranslations; + } + validTranslations[i] = valid; + validityBit = (validityBit + 1) % BITS_IN_BYTE; + } + } // 1 + bytesOfValidity bytes + + // read maxTranslationDimension + float maxTranslationDimension; + + if (!packetReadCheck("JointMaxTranslationDimension", sizeof(float))) { + return buffer.size(); + } + + memcpy(&maxTranslationDimension, sourceBuffer, sizeof(float)); + sourceBuffer += sizeof(float); + + // each joint translation component is stored in 6 bytes. + const int COMPRESSED_TRANSLATION_SIZE = 6; + if (!packetReadCheck("JointTranslation", numValidJointTranslations * COMPRESSED_TRANSLATION_SIZE)) { + return buffer.size(); + } + + for (int i = 0; i < numJoints; i++) { + glm::vec3 translation{}; + if (validTranslations[i]) { + sourceBuffer += unpackFloatVec3FromSignedTwoByteFixed(sourceBuffer, translation, TRANSLATION_COMPRESSION_RADIX); + translation *= maxTranslationDimension; + derived().setJointDataPositionIn(i, translation); + derived().setJointDataPositionDefaultIn(i, false); + _hasNewJointData = true; + } + } + +#ifdef WANT_DEBUG + if (numValidJointRotations > 15) { + qCDebug(avatars) << "RECEIVING -- rotations:" << numValidJointRotations + << "translations:" << numValidJointTranslations + << "size:" << (int)(sourceBuffer - startPosition); + } +#endif + int numBytesRead = sourceBuffer - startSection; + _jointDataRate.increment(numBytesRead); + _jointDataUpdateRate.increment(); + } + + if (hasGrabJoints) { + auto startSection = sourceBuffer; + + if (!packetReadCheck("FarGrabJoints", sizeof(AvatarDataPacket::FarGrabJoints))) { + return buffer.size(); + } + + AvatarDataPacket::FarGrabJoints farGrabJoints; + memcpy(&farGrabJoints, sourceBuffer, sizeof(farGrabJoints)); // to avoid misaligned floats + + derived().setFarGrabJointsIn(farGrabJoints); + + // FIXME: CRTP + // glm::vec3 leftFarGrabPosition = glm::vec3(farGrabJoints.leftFarGrabPosition[0], + // farGrabJoints.leftFarGrabPosition[1], + // farGrabJoints.leftFarGrabPosition[2]); + // glm::quat leftFarGrabRotation = glm::quat(farGrabJoints.leftFarGrabRotation[0], + // farGrabJoints.leftFarGrabRotation[1], + // farGrabJoints.leftFarGrabRotation[2], + // farGrabJoints.leftFarGrabRotation[3]); + // glm::vec3 rightFarGrabPosition = glm::vec3(farGrabJoints.rightFarGrabPosition[0], + // farGrabJoints.rightFarGrabPosition[1], + // farGrabJoints.rightFarGrabPosition[2]); + // glm::quat rightFarGrabRotation = glm::quat(farGrabJoints.rightFarGrabRotation[0], + // farGrabJoints.rightFarGrabRotation[1], + // farGrabJoints.rightFarGrabRotation[2], + // farGrabJoints.rightFarGrabRotation[3]); + // glm::vec3 mouseFarGrabPosition = glm::vec3(farGrabJoints.mouseFarGrabPosition[0], + // farGrabJoints.mouseFarGrabPosition[1], + // farGrabJoints.mouseFarGrabPosition[2]); + // glm::quat mouseFarGrabRotation = glm::quat(farGrabJoints.mouseFarGrabRotation[0], + // farGrabJoints.mouseFarGrabRotation[1], + // farGrabJoints.mouseFarGrabRotation[2], + // farGrabJoints.mouseFarGrabRotation[3]); + // + // _farGrabLeftMatrixCache.set(createMatFromQuatAndPos(leftFarGrabRotation, leftFarGrabPosition)); + // _farGrabRightMatrixCache.set(createMatFromQuatAndPos(rightFarGrabRotation, rightFarGrabPosition)); + // _farGrabMouseMatrixCache.set(createMatFromQuatAndPos(mouseFarGrabRotation, mouseFarGrabPosition)); + + sourceBuffer += sizeof(AvatarDataPacket::FarGrabJoints); + int numBytesRead = sourceBuffer - startSection; + _farGrabJointRate.increment(numBytesRead); + _farGrabJointUpdateRate.increment(); + } + } + + if (hasJointDefaultPoseFlags) { + auto startSection = sourceBuffer; + + // FIXME: CRTP + // QWriteLocker writeLock(&_jointDataLock); + + if (!packetReadCheck("JointDefaultPoseFlagsNumJoints", sizeof(uint8_t))) { + return buffer.size(); + } + + int numJoints = (int)*sourceBuffer++; + + derived().setJointDataSizeIn(numJoints); + + int bitVectorSize = calcBitVectorSize(numJoints); + + if (!packetReadCheck("JointDefaultPoseFlagsRotationFlags", bitVectorSize)) { + return buffer.size(); + } + + sourceBuffer += readBitVector(sourceBuffer, numJoints, [&](int i, bool value) { + derived().setJointDataRotationDefaultIn(i, value); + }); + + if (!packetReadCheck("JointDefaultPoseFlagsTranslationFlags", bitVectorSize)) { + return buffer.size(); + } + sourceBuffer += readBitVector(sourceBuffer, numJoints, [&](int i, bool value) { + derived().setJointDataPositionDefaultIn(i, value); + }); + + int numBytesRead = sourceBuffer - startSection; + _jointDefaultPoseFlagsRate.increment(numBytesRead); + _jointDefaultPoseFlagsUpdateRate.increment(); + } + + int numBytesRead = sourceBuffer - startPosition; + _averageBytesReceived.updateAverage(numBytesRead); + + _parseBufferRate.increment(numBytesRead); + _parseBufferUpdateRate.increment(); + + return numBytesRead; +} + +template +float AvatarDataStream::getDataRate(const QString& rateName) const { + if (rateName == "") { + return _parseBufferRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "globalPosition") { + return _globalPositionRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "localPosition") { + return _localPositionRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "handControllers") { + return _handControllersRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "avatarBoundingBox") { + return _avatarBoundingBoxRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "avatarOrientation") { + return _avatarOrientationRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "avatarScale") { + return _avatarScaleRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "lookAtPosition") { + return _lookAtPositionRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "audioLoudness") { + return _audioLoudnessRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "sensorToWorkMatrix") { + return _sensorToWorldRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "additionalFlags") { + return _additionalFlagsRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "parentInfo") { + return _parentInfoRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "faceTracker") { + return _faceTrackerRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "jointData") { + return _jointDataRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "jointDefaultPoseFlagsRate") { + return _jointDefaultPoseFlagsRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "farGrabJointRate") { + return _farGrabJointRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "globalPositionOutbound") { + return _outboundDataRate.globalPositionRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "localPositionOutbound") { + return _outboundDataRate.localPositionRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "avatarBoundingBoxOutbound") { + return _outboundDataRate.avatarBoundingBoxRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "avatarOrientationOutbound") { + return _outboundDataRate.avatarOrientationRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "avatarScaleOutbound") { + return _outboundDataRate.avatarScaleRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "lookAtPositionOutbound") { + return _outboundDataRate.lookAtPositionRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "audioLoudnessOutbound") { + return _outboundDataRate.audioLoudnessRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "sensorToWorkMatrixOutbound") { + return _outboundDataRate.sensorToWorldRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "additionalFlagsOutbound") { + return _outboundDataRate.additionalFlagsRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "parentInfoOutbound") { + return _outboundDataRate.parentInfoRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "faceTrackerOutbound") { + return _outboundDataRate.faceTrackerRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "jointDataOutbound") { + return _outboundDataRate.jointDataRate.rate() / BYTES_PER_KILOBIT; + } else if (rateName == "jointDefaultPoseFlagsOutbound") { + return _outboundDataRate.jointDefaultPoseFlagsRate.rate() / BYTES_PER_KILOBIT; + } + return 0.0f; +} + +template +float AvatarDataStream::getUpdateRate(const QString& rateName) const { + if (rateName == "") { + return _parseBufferUpdateRate.rate(); + } else if (rateName == "globalPosition") { + return _globalPositionUpdateRate.rate(); + } else if (rateName == "localPosition") { + return _localPositionUpdateRate.rate(); + } else if (rateName == "handControllers") { + return _handControllersUpdateRate.rate(); + } else if (rateName == "avatarBoundingBox") { + return _avatarBoundingBoxUpdateRate.rate(); + } else if (rateName == "avatarOrientation") { + return _avatarOrientationUpdateRate.rate(); + } else if (rateName == "avatarScale") { + return _avatarScaleUpdateRate.rate(); + } else if (rateName == "lookAtPosition") { + return _lookAtPositionUpdateRate.rate(); + } else if (rateName == "audioLoudness") { + return _audioLoudnessUpdateRate.rate(); + } else if (rateName == "sensorToWorkMatrix") { + return _sensorToWorldUpdateRate.rate(); + } else if (rateName == "additionalFlags") { + return _additionalFlagsUpdateRate.rate(); + } else if (rateName == "parentInfo") { + return _parentInfoUpdateRate.rate(); + } else if (rateName == "faceTracker") { + return _faceTrackerUpdateRate.rate(); + } else if (rateName == "jointData") { + return _jointDataUpdateRate.rate(); + } else if (rateName == "farGrabJointData") { + return _farGrabJointUpdateRate.rate(); + } + return 0.0f; +} + +template +QVector AvatarDataStream::getLastSentJointData() { + // FIXME: CRTP + // QReadLocker readLock(&_jointDataLock); + // _lastSentJointData.resize(_jointData.size()); + _lastSentJointData.resize(derived().getJointDataSizeOut()); + return _lastSentJointData; +} + +template +int AvatarDataStream::getAverageBytesReceivedPerSecond() const { + return lrint(_averageBytesReceived.getAverageSampleValuePerSecond()); +} + +template +int AvatarDataStream::getReceiveRate() const { + return lrint(1.0f / _averageBytesReceived.getEventDeltaAverage()); +} + +template +int AvatarDataStream::getFauxJointIndex(const QString& name) const { + static constexpr QChar fauxJointFirstChar('_');// The first character of all the faux joint names. + if (!name.startsWith(fauxJointFirstChar)) { + return -1; + }; + if (name == "_SENSOR_TO_WORLD_MATRIX") { + return SENSOR_TO_WORLD_MATRIX_INDEX; + } + if (name == "_CONTROLLER_LEFTHAND") { + return CONTROLLER_LEFTHAND_INDEX; + } + if (name == "_CONTROLLER_RIGHTHAND") { + return CONTROLLER_RIGHTHAND_INDEX; + } + if (name == "_CAMERA_RELATIVE_CONTROLLER_LEFTHAND") { + return CAMERA_RELATIVE_CONTROLLER_LEFTHAND_INDEX; + } + if (name == "_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND") { + return CAMERA_RELATIVE_CONTROLLER_RIGHTHAND_INDEX; + } + if (name == "_CAMERA_MATRIX") { + return CAMERA_MATRIX_INDEX; + } + if (name == "_FARGRAB_RIGHTHAND") { + return FARGRAB_RIGHTHAND_INDEX; + } + if (name == "_FARGRAB_LEFTHAND") { + return FARGRAB_LEFTHAND_INDEX; + } + if (name == "_FARGRAB_MOUSE") { + return FARGRAB_MOUSE_INDEX; + } + return -1; +} + +template +int AvatarDataStream::getJointIndex(const QString& name) const { + int result = getFauxJointIndex(name); + return result; +} + +template +void AvatarDataStream::processAvatarIdentity(QDataStream& packetStream, bool& identityChanged, + bool& displayNameChanged) { + + QUuid avatarSessionID; + + // peek the sequence number, this will tell us if we should be processing this identity packet at all + udt::SequenceNumber::Type incomingSequenceNumberType; + packetStream >> avatarSessionID >> incomingSequenceNumberType; + udt::SequenceNumber incomingSequenceNumber(incomingSequenceNumberType); + + if (!_hasProcessedFirstIdentity) { + derived().setIdentitySequenceNumberIn(incomingSequenceNumber - 1); + // FIXME: CRTP + // _identitySequenceNumber = incomingSequenceNumber - 1; + _hasProcessedFirstIdentity = true; + qCDebug(avatars) << "Processing first identity packet for" << avatarSessionID << "-" + << (udt::SequenceNumber::Type) incomingSequenceNumber; + } + + AvatarDataPacket::Identity identity; + + packetStream + >> identity.attachmentData + >> identity.displayName + >> identity.sessionDisplayName + >> identity.identityFlags + ; + + + const auto currentIdentitySequenceNumber = derived().getIdentitySequenceNumberOut(); + // FIXME: CRTP Derived::getIdentitySequenceNumberOut + // return _identitySequenceNumber; + if (incomingSequenceNumber > currentIdentitySequenceNumber) { + + // set the store identity sequence number to match the incoming identity + derived().setIdentitySequenceNumberIn(incomingSequenceNumber); + // FIXME: CRTP + // _identitySequenceNumber = incomingSequenceNumber; + + auto current = derived().getIdentityDataOut(); + identityChanged = identity != current; + displayNameChanged = identity.displayName != current.displayName; + + derived().setIdentityDataIn(std::move(identity)); + + // FIXME: CRTP + // QString displayName{}; + // bool isReplicated{}; + // bool lookAtSnappingEnabled{}; + // bool verificationFailed{}; + // QVector attachmentData; + // + // displayName = _displayName; + // isReplicated = _isReplicated; + // lookAtSnappingEnabled = _lookAtSnappingEnabled; + // verificationFailed = _verificationFailed; + // attachmentData = _attachmentData; + // + // if (identity.displayName != displayName) { + // _displayName = identity.displayName; + // } + // + // maybeUpdateSessionDisplayNameFromTransport(identity.sessionDisplayName); + // + // bool flagValue; + // flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::isReplicated); + // if ( flagValue != isReplicated) { + // _isReplicated = flagValue; + // } + // + // flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::lookAtSnapping); + // if ( flagValue != lookAtSnappingEnabled) { + // setProperty("lookAtSnappingEnabled", flagValue); + // } + // + // flagValue = identity.identityFlags.testFlag(AvatarDataPacket::IdentityFlag::verificationFailed); + // if (flagValue != verificationFailed) { + // _verificationFailed = flagValue; + // setSkeletonModelURL(_skeletonModelURL); + // if (flagValue) { + // QString sessionDisplayName {}; + // sessionDisplayName = getSessionDisplayName(); + // qCDebug(avatars) << "Avatar" << sessionDisplayName << "marked as VERIFY-FAILED"; + // } + // }; + // + // if (identity.attachmentData != attachmentData) { + // setAttachmentData(identity.attachmentData); + // } + +#ifdef WANT_DEBUG + qCDebug(avatars) << __FUNCTION__ + << "identity.uuid:" << identity.uuid + << "identity.displayName:" << identity.displayName + << "identity.sessionDisplayName:" << identity.sessionDisplayName; + } else { + + qCDebug(avatars) << "Refusing to process identity for" << uuidStringWithoutCurlyBraces(avatarSessionID) << "since" + << (udt::SequenceNumber::Type) derived().getIdentitySequenceNumberOut() + << "is >=" << (udt::SequenceNumber::Type) incomingSequenceNumber; +#endif + } +} + +template +QByteArray AvatarDataStream::packSkeletonData() const { + // Send an avatar trait packet with the skeleton data before the mesh is loaded + int avatarDataSize = 0; + QByteArray avatarDataByteArray; + + // FIXME: CRTP + // _avatarSkeletonDataLock.withReadLock([&] { + + auto&& avatarSkeletonData = derived().getSkeletonDataOut(); + + // Add header + AvatarSkeletonTrait::Header header; + header.maxScaleDimension = 0.0f; + header.maxTranslationDimension = 0.0f; + header.numJoints = (uint8_t)avatarSkeletonData.size(); + header.stringTableLength = 0; + + for (size_t i = 0; i < avatarSkeletonData.size(); i++) { + header.stringTableLength += (uint16_t)avatarSkeletonData[i].jointName.size(); + auto& translation = avatarSkeletonData[i].defaultTranslation; + header.maxTranslationDimension = std::max(header.maxTranslationDimension, std::max(std::max(translation.x, translation.y), translation.z)); + header.maxScaleDimension = std::max(header.maxScaleDimension, avatarSkeletonData[i].defaultScale); + } + + const int byteArraySize = (int)sizeof(AvatarSkeletonTrait::Header) + (int)(header.numJoints * sizeof(AvatarSkeletonTrait::JointData)) + header.stringTableLength; + avatarDataByteArray = QByteArray(byteArraySize, 0); + unsigned char* destinationBuffer = reinterpret_cast(avatarDataByteArray.data()); + const unsigned char* const startPosition = destinationBuffer; + + memcpy(destinationBuffer, &header, sizeof(header)); + destinationBuffer += sizeof(AvatarSkeletonTrait::Header); + + QString stringTable = ""; + for (size_t i = 0; i < avatarSkeletonData.size(); i++) { + AvatarSkeletonTrait::JointData jdata; + jdata.boneType = avatarSkeletonData[i].boneType; + jdata.parentIndex = avatarSkeletonData[i].parentIndex; + packFloatRatioToTwoByte((uint8_t*)(&jdata.defaultScale), avatarSkeletonData[i].defaultScale / header.maxScaleDimension); + packOrientationQuatToSixBytes(jdata.defaultRotation, avatarSkeletonData[i].defaultRotation); + packFloatVec3ToSignedTwoByteFixed(jdata.defaultTranslation, avatarSkeletonData[i].defaultTranslation / header.maxTranslationDimension, TRANSLATION_COMPRESSION_RADIX); + jdata.jointIndex = (uint16_t)i; + jdata.stringStart = (uint16_t)avatarSkeletonData[i].stringStart; + jdata.stringLength = (uint8_t)avatarSkeletonData[i].stringLength; + stringTable += avatarSkeletonData[i].jointName; + memcpy(destinationBuffer, &jdata, sizeof(AvatarSkeletonTrait::JointData)); + destinationBuffer += sizeof(AvatarSkeletonTrait::JointData); + } + + memcpy(destinationBuffer, stringTable.toUtf8(), header.stringTableLength); + destinationBuffer += header.stringTableLength; + + avatarDataSize = destinationBuffer - startPosition; + return avatarDataByteArray.left(avatarDataSize); +} + +template +QByteArray AvatarDataStream::packSkeletonModelURL() const { + return derived().getSkeletonModelURLOut(); + // FIXME: CRTP + // return getWireSafeSkeletonModelURL().toEncoded(); +} + +template +void AvatarDataStream::unpackSkeletonData(const QByteArray& data) { + + const unsigned char* startPosition = reinterpret_cast(data.data()); + const unsigned char* sourceBuffer = startPosition; + + auto header = reinterpret_cast(sourceBuffer); + sourceBuffer += sizeof(const AvatarSkeletonTrait::Header); + + std::vector joints; + for (uint8_t i = 0; i < header->numJoints; i++) { + auto jointData = reinterpret_cast(sourceBuffer); + sourceBuffer += sizeof(const AvatarSkeletonTrait::JointData); + AvatarSkeletonTrait::UnpackedJointData uJointData; + uJointData.boneType = (int)jointData->boneType; + uJointData.jointIndex = (int)i; + uJointData.stringLength = (int)jointData->stringLength; + uJointData.stringStart = (int)jointData->stringStart; + uJointData.parentIndex = ((uJointData.boneType == AvatarSkeletonTrait::BoneType::SkeletonRoot) || + (uJointData.boneType == AvatarSkeletonTrait::BoneType::NonSkeletonRoot)) ? -1 : (int)jointData->parentIndex; + unpackOrientationQuatFromSixBytes(reinterpret_cast(&jointData->defaultRotation), uJointData.defaultRotation); + unpackFloatVec3FromSignedTwoByteFixed(reinterpret_cast(&jointData->defaultTranslation), uJointData.defaultTranslation, TRANSLATION_COMPRESSION_RADIX); + unpackFloatRatioFromTwoByte(reinterpret_cast(&jointData->defaultScale), uJointData.defaultScale); + uJointData.defaultTranslation *= header->maxTranslationDimension; + uJointData.defaultScale *= header->maxScaleDimension; + joints.push_back(uJointData); + } + QString table = QString::fromUtf8(reinterpret_cast(sourceBuffer), (int)header->stringTableLength); + for (size_t i = 0; i < joints.size(); i++) { + QStringRef subString(&table, joints[i].stringStart, joints[i].stringLength); + joints[i].jointName = subString.toString(); + } + derived().setSkeletonDataIn(std::move(joints)); + // TODO: CRTP + // if (derived().getClientTraitsHandler()) { + // derived().getClientTraitsHandler()->markTraitUpdated(AvatarTraits::SkeletonData); + // } +} + +template +void AvatarDataStream::unpackSkeletonModelURL(const QByteArray& data) { + derived().setSkeletonModelURLIn(data); + // FIXME: CRTP + // auto skeletonModelURL = QUrl::fromEncoded(data); + // setSkeletonModelURL(skeletonModelURL); +} + +template +QByteArray AvatarDataStream::packAvatarEntityTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) { + // grab a read lock on the avatar entities and check for entity data for the given ID + QByteArray entityBinaryData; + _avatarEntitiesLock.withReadLock([this, &entityBinaryData, &traitInstanceID] { + if (_packedAvatarEntityData.contains(traitInstanceID)) { + entityBinaryData = _packedAvatarEntityData[traitInstanceID]; + } + }); + + return entityBinaryData; +} + +template +QByteArray AvatarDataStream::packGrabTraitInstance(AvatarTraits::TraitInstanceID traitInstanceID) { + // grab a read lock on the avatar grabs and check for grab data for the given ID + QByteArray grabBinaryData; + _avatarGrabsLock.withReadLock([this, &grabBinaryData, &traitInstanceID] { + if (_avatarGrabData.contains(traitInstanceID)) { + grabBinaryData = _avatarGrabData[traitInstanceID]; + } + }); + + return grabBinaryData; +} + +template +QByteArray AvatarDataStream::packTrait(AvatarTraits::TraitType traitType) const { + QByteArray traitBinaryData; + + // Call packer function + if (traitType == AvatarTraits::SkeletonModelURL) { + traitBinaryData = packSkeletonModelURL(); + } else if (traitType == AvatarTraits::SkeletonData) { + traitBinaryData = packSkeletonData(); + } + + return traitBinaryData; +} + +template +QByteArray AvatarDataStream::packTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID traitInstanceID) { + QByteArray traitBinaryData; + + // Call packer function + if (traitType == AvatarTraits::AvatarEntity) { + traitBinaryData = packAvatarEntityTraitInstance(traitInstanceID); + } else if (traitType == AvatarTraits::Grab) { + traitBinaryData = packGrabTraitInstance(traitInstanceID); + } + + return traitBinaryData; +} + +template +void AvatarDataStream::processTrait(AvatarTraits::TraitType traitType, QByteArray traitBinaryData) { + if (traitType == AvatarTraits::SkeletonModelURL) { + unpackSkeletonModelURL(traitBinaryData); + } else if (traitType == AvatarTraits::SkeletonData) { + unpackSkeletonData(traitBinaryData); + } +} + +template +void AvatarDataStream::processTraitInstance(AvatarTraits::TraitType traitType, + AvatarTraits::TraitInstanceID instanceID, QByteArray traitBinaryData) { + if (traitType == AvatarTraits::AvatarEntity) { + storeAvatarEntityDataPayload(instanceID, traitBinaryData); + } else if (traitType == AvatarTraits::Grab) { + updateAvatarGrabData(instanceID, traitBinaryData); + } +} + +template +void AvatarDataStream::processDeletedTraitInstance(AvatarTraits::TraitType traitType, AvatarTraits::TraitInstanceID instanceID) { + if (traitType == AvatarTraits::AvatarEntity) { + clearAvatarEntityInternal(instanceID); + } else if (traitType == AvatarTraits::Grab) { + clearAvatarGrabData(instanceID); + } +} + +template +void AvatarDataStream::prepareResetTraitInstances() { + if (derived().getClientTraitsHandler()) { + _avatarEntitiesLock.withReadLock([this]{ + foreach (auto entityID, _packedAvatarEntityData.keys()) { + derived().getClientTraitsHandler()->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); + } + foreach (auto grabID, _avatarGrabData.keys()) { + derived().getClientTraitsHandler()->markInstancedTraitUpdated(AvatarTraits::Grab, grabID); + } + }); + } +} + +template +QByteArray AvatarDataStream::identityByteArray(bool setIsReplicated) const { + QByteArray identityData; + QDataStream identityStream(&identityData, QIODevice::Append); + using namespace AvatarDataPacket; + + QUuid id = derived().getSessionUUIDOut(); + AvatarDataPacket::Identity identity = derived().getIdentityDataOut(); + + //FIXME: CRTP to libraries/avatars/AvatarData + // bool isReplicated{}; + // bool lookAtSnappingEnabled{}; + // bool isCertifyFailed{}; + // QVector attachmentData{}; + // QString displayName{}; + // QString sessionDisplayName{}; + // + // isReplicated = _isReplicated; + // lookAtSnappingEnabled = _lookAtSnappingEnabled; + // isCertifyFailed = isCertifyFailed(); + // attachmentData = _attachmentData; + // displayName = _displayName; + // sessionDisplayName = getSessionDisplayNameForTransport(); // depends on _sessionDisplayName + // IdentityFlags identityFlags = IdentityFlag::none; + // if (isReplicated || setIsReplicated) { + // identityFlags.setFlag(IdentityFlag::isReplicated); + // } + // if (lookAtSnappingEnabled) { + // identityFlags.setFlag(IdentityFlag::lookAtSnapping); + // } + // + // if (isCertifyFailed) { + // identityFlags.setFlag(IdentityFlag::verificationFailed); + // } + + // when mixers send identity packets to agents, they simply forward along the last incoming sequence number they received + // whereas agents send a fresh outgoing sequence number when identity data has changed + + identityStream << id + << (udt::SequenceNumber::Type) derived().getIdentitySequenceNumberOut() + << identity.attachmentData + << identity.displayName + << identity.sessionDisplayName + << identity.identityFlags; + + return identityData; +} + +// CRTP: move to derived, the client traits handler code has been separated out +// void AvatarData::setSkeletonModelURL(const QUrl& skeletonModelURL) { +// if (skeletonModelURL.isEmpty()) { +// qCDebug(avatars) << __FUNCTION__ << "caller called with empty URL."; +// } +// +// const QUrl& defaultURL = AvatarData::defaultFullAvatarModelUrl(); +// +// const QUrl& expanded = skeletonModelURL.isEmpty() ? defaultURL : skeletonModelURL; +// if (expanded == _skeletonModelURL) { +// return; +// } +// +// _skeletonModelURL = expanded; +// +// sendSkeletonModelURL(); +// +// emit skeletonModelURLChanged +// } + +template +void AvatarDataStream::sendSkeletonModelURL() { + if (derived().getClientTraitsHandler()) { + derived().getClientTraitsHandler()->markTraitUpdated(AvatarTraits::SkeletonModelURL); + } +} + +template +int AvatarDataStream::sendAvatarDataPacket(const QByteArray& avatarByteArray) { + auto nodeList = DependencyManager::get(); + + static AvatarDataSequenceNumber sequenceNumber = 0; + + auto avatarPacket = NLPacket::create(PacketType::AvatarData, avatarByteArray.size() + sizeof(sequenceNumber)); + avatarPacket->writePrimitive(sequenceNumber++); + avatarPacket->write(avatarByteArray); + auto packetSize = avatarPacket->getWireSize(); + + nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); + + return packetSize; +} + + +template +int AvatarDataStream::sendAvatarDataPacket(AvatarDataDetail dataDetail, AvatarDataPacket::SendStatus& sendStatus) { + // about 2% of the time, we send a full update (meaning, we transmit all the joint data), even if nothing has changed. + // this is to guard against a joint moving once, the packet getting lost, and the joint never moving again. + if (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO) { + dataDetail = SendAllData; + } + + int maxDataSize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber); + + auto data = toByteArray(AvatarDataPacket::HasFlags{}, dataDetail, getLastSentJointData(), sendStatus, + false, // distanceAdjust + glm::vec3{0}, // viewerPosition + nullptr, // sentJointDataOut + maxDataSize, // maxDataSize + &_outboundDataRate + ); + + doneEncoding(dataDetail == CullSmallData, sendStatus); + + return sendAvatarDataPacket(data); +} + +// TODO: handle immediate packet sending failures (for example when mixer is not active) +template +int AvatarDataStream::sendAllPackets(AvatarDataDetail dataDetail, AvatarDataPacket::SendStatus& sendStatus) { + using namespace std::chrono; + auto now = Clock::now(); + + int MAX_DATA_RATE_MBPS = 3; + int maxDataRateBytesPerSeconds = MAX_DATA_RATE_MBPS * BYTES_PER_KILOBYTE * KILO_PER_MEGA / BITS_IN_BYTE; + int maxDataRateBytesPerMilliseconds = maxDataRateBytesPerSeconds / MSECS_PER_SECOND; + + auto bytesSent = 0; + + if (now > _nextTraitsSendWindow) { + if (derived().getIdentityDataChanged()) { + bytesSent += sendIdentityPacket(); + } + + if (derived().getClientTraitsHandler()) { + + if (derived().getSkeletonModelURLChanged()) { + sendSkeletonModelURL(); + } + + if (derived().getSkeletonDataChanged()) { + sendSkeletonData(); + } + + bytesSent += derived().getClientTraitsHandler()->sendChangedTraitsToMixer(); + + derived().onClientTraitsSent(); + } + + // Compute the next send window based on how much data we sent and what + // data rate we're trying to max at. + milliseconds timeUntilNextSend { bytesSent / maxDataRateBytesPerMilliseconds }; + _nextTraitsSendWindow += timeUntilNextSend; + + // Don't let the next send window lag behind if we're not sending a lot of data. + if (_nextTraitsSendWindow < now) { + _nextTraitsSendWindow = now; + } + } + + bytesSent += sendAvatarDataPacket(dataDetail, sendStatus); + + return bytesSent; +} + +// FIXME: CRTP move to derived +// int AvatarData::sendAvatarDataPacket(bool sendAll) { +// +// // about 2% of the time, we send a full update (meaning, we transmit all the joint data), even if nothing has changed. +// // this is to guard against a joint moving once, the packet getting lost, and the joint never moving again. +// +// bool cullSmallData = !sendAll && (randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); +// auto dataDetail = cullSmallData ? SendAllData : CullSmallData; +// +// // NOTE: Unless sendAll is true toByteArrayStateful checks for changes (...ChangedSince() functions) based on modification timestamps compared to _lastToByteArray, which it sets to current time "afterwards" (technically it sets it beforehand, but uses the old value for the checks). This means that on subsequent toByteArrayStateful calls here this information is discarded and none of these modification are serialized for sending. It's Unclear whether this is intentional or not. +// QByteArray avatarByteArray = toByteArrayStateful(dataDetail); +// +// int maximumByteArraySize = NLPacket::maxPayloadSize(PacketType::AvatarData) - sizeof(AvatarDataSequenceNumber); +// +// if (avatarByteArray.size() > maximumByteArraySize) { +// avatarByteArray = toByteArrayStateful(dataDetail, true); +// +// if (avatarByteArray.size() > maximumByteArraySize) { +// avatarByteArray = toByteArrayStateful(MinimumData, true); +// +// if (avatarByteArray.size() > maximumByteArraySize) { +// qCWarning(avatars) << "toByteArrayStateful() MinimumData resulted in very large buffer:" << avatarByteArray.size() << "... FAIL!!"; +// return 0; +// } +// } +// } +// +// doneEncoding(cullSmallData); +// +// return sendAvatarDataPacket(avatarByteArray); +// } + +template +int AvatarDataStream::sendIdentityPacket() { + auto nodeList = DependencyManager::get(); + + if (derived().getIdentityDataChanged()) { + // if the identity data has changed, push the sequence number forwards + derived().pushIdentitySequenceNumber(); + // FIXME: CRTP + // ++_identitySequenceNumber; + } + QByteArray identityData = identityByteArray(); + + auto packetList = NLPacketList::create(PacketType::AvatarIdentity, QByteArray(), true, true); + packetList->write(identityData); + nodeList->eachMatchingNode( + [](const SharedNodePointer& node)->bool { + return node->getType() == NodeType::AvatarMixer && node->getActiveSocket(); + }, + [&](const SharedNodePointer& node) { + nodeList->sendPacketList(std::move(packetList), *node); + }); + + derived().onIdentityDataSent(); + // FIXME: CRTP move to derived + // _identityDataChanged = false; + return identityData.size(); +} + +bool AttachmentData::operator==(const AttachmentData& other) const { + return modelURL == other.modelURL && jointName == other.jointName && translation == other.translation && + rotation == other.rotation && scale == other.scale && isSoft == other.isSoft; +} + +QDataStream& operator<<(QDataStream& out, const AttachmentData& attachment) { + return out << attachment.modelURL << attachment.jointName << + attachment.translation << attachment.rotation << attachment.scale << attachment.isSoft; +} + +QDataStream& operator>>(QDataStream& in, AttachmentData& attachment) { + return in >> attachment.modelURL >> attachment.jointName >> + attachment.translation >> attachment.rotation >> attachment.scale >> attachment.isSoft; +} + +template +void AvatarDataStream::storeAvatarEntityDataPayload(const QUuid& entityID, const QByteArray& data) { + bool changed = false; + _avatarEntitiesLock.withWriteLock([&] { + auto itr = _packedAvatarEntityData.find(entityID); + if (itr == _packedAvatarEntityData.end()) { + if (_packedAvatarEntityData.size() < MAX_NUM_AVATAR_ENTITIES) { + _packedAvatarEntityData.insert(entityID, data); + changed = true; + } + } else { + itr.value() = data; + changed = true; + } + }); + + if (changed) { + _avatarEntityDataChanged = true; + + if (derived().getClientTraitsHandler()) { + // we have a client traits handler, so we need to mark this instanced trait as changed + // so that changes will be sent next frame + derived().getClientTraitsHandler()->markInstancedTraitUpdated(AvatarTraits::AvatarEntity, entityID); + } + } +} + +template +void AvatarDataStream::updateAvatarEntity(const QUuid& entityID, const QByteArray& entityData) { + // overridden where needed + // expects 'entityData' to be a JavaScript EntityItemProperties Object in QByteArray form +} + +template +void AvatarDataStream::clearAvatarEntity(const QUuid& entityID, bool requiresRemovalFromTree) { + // NOTE: requiresRemovalFromTree is unused + clearAvatarEntityInternal(entityID); +} + +template +void AvatarDataStream::clearAvatarEntityInternal(const QUuid& entityID) { + bool removedEntity = false; + _avatarEntitiesLock.withWriteLock([this, &removedEntity, &entityID] { + removedEntity = _packedAvatarEntityData.remove(entityID); + }); + insertRemovedEntityID(entityID); + if (removedEntity && derived().getClientTraitsHandler()) { + // we have a client traits handler, so we need to mark this removed instance trait as deleted + // so that changes are sent next frame + derived().getClientTraitsHandler()->markInstancedTraitDeleted(AvatarTraits::AvatarEntity, entityID); + } +} + +template +void AvatarDataStream::clearAvatarEntities() { + QList avatarEntityIDs; + _avatarEntitiesLock.withReadLock([&] { + avatarEntityIDs = _packedAvatarEntityData.keys(); + }); + for (const auto& entityID : avatarEntityIDs) { + clearAvatarEntityInternal(entityID); + } +} + +template +QList AvatarDataStream::getAvatarEntityIDs() const { + QList avatarEntityIDs; + _avatarEntitiesLock.withReadLock([&] { + avatarEntityIDs = _packedAvatarEntityData.keys(); + }); + return avatarEntityIDs; +} + +template +void AvatarDataStream::insertRemovedEntityID(const QUuid entityID) { + _avatarEntitiesLock.withWriteLock([&] { + _avatarEntityRemoved.insert(entityID); + }); + _avatarEntityDataChanged = true; +} + +template +AvatarEntityIDs AvatarDataStream::getAndClearRecentlyRemovedIDs() { + AvatarEntityIDs result; + _avatarEntitiesLock.withWriteLock([&] { + result = _avatarEntityRemoved; + _avatarEntityRemoved.clear(); + }); + return result; +} + +template +void AvatarDataStream::sendSkeletonData() { + if (derived().getClientTraitsHandler()) { + derived().getClientTraitsHandler()->markTraitUpdated(AvatarTraits::SkeletonData); + } +} + +template +bool AvatarDataStream::updateAvatarGrabData(const QUuid& grabID, const QByteArray& grabData) { + bool changed { false }; + _avatarGrabsLock.withWriteLock([&] { + AvatarGrabDataMap::iterator itr = _avatarGrabData.find(grabID); + if (itr == _avatarGrabData.end()) { + // create a new one + if (_avatarGrabData.size() < MAX_NUM_AVATAR_GRABS) { + _avatarGrabData.insert(grabID, grabData); + _avatarGrabDataChanged = true; + changed = true; + } else { + qCWarning(avatars) << "Can't create more grabs on avatar, limit reached."; + } + } else { + // update an existing one + if (itr.value() != grabData) { + itr.value() = grabData; + _avatarGrabDataChanged = true; + changed = true; + } + } + }); + + return changed; +} + +template +void AvatarDataStream::clearAvatarGrabData(const QUuid& grabID) { + _avatarGrabsLock.withWriteLock([&] { + if (_avatarGrabData.remove(grabID)) { + _avatarGrabDataChanged = true; + } + }); +} + +#endif /* end of include guard */ diff --git a/libraries/avatars-core/src/AvatarLogging.cpp b/libraries/avatars-core/src/AvatarLogging.cpp new file mode 100644 index 00000000000..86a6327bebc --- /dev/null +++ b/libraries/avatars-core/src/AvatarLogging.cpp @@ -0,0 +1,14 @@ +// +// AvatarLogging.cpp +// libraries/avatars/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 "AvatarLogging.h" + +Q_LOGGING_CATEGORY(avatars, "hifi.avatars") diff --git a/libraries/avatars-core/src/AvatarLogging.h b/libraries/avatars-core/src/AvatarLogging.h new file mode 100644 index 00000000000..698a59fcdf0 --- /dev/null +++ b/libraries/avatars-core/src/AvatarLogging.h @@ -0,0 +1,20 @@ +// +// AvatarLogging.h +// libraries/avatars/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_AvatarLogging_h +#define hifi_AvatarLogging_h + +#include + +Q_DECLARE_LOGGING_CATEGORY(avatars) + +#endif // hifi_AvatarLogging_h + diff --git a/libraries/avatars-core/src/AvatarPacketHandler.h b/libraries/avatars-core/src/AvatarPacketHandler.h new file mode 100644 index 00000000000..06e0c37b12f --- /dev/null +++ b/libraries/avatars-core/src/AvatarPacketHandler.h @@ -0,0 +1,72 @@ +// +// AvatarPacketHandler.h +// libraries/avatars-core/src +// +// Created by Nshan G. on 11 May 2022. +// Copyright 2014 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_AVATARS_CORE_SRC_AVATARPACKETHANDLER_H +#define LIBRARIES_AVATARS_CORE_SRC_AVATARPACKETHANDLER_H + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include + +#include "AssociatedTraitValues.h" +#include "AvatarDataStream.h" + +const int CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND = 50; +const quint64 MIN_TIME_BETWEEN_MY_AVATAR_DATA_SENDS = USECS_PER_SECOND / CLIENT_TO_AVATAR_MIXER_BROADCAST_FRAMES_PER_SECOND; + +template +class AvatarPacketHandler { + +protected: + AvatarPacketHandler(); + + void parseAvatarData(QSharedPointer message, SharedNodePointer sendingNode); + void clearOtherAvatars(); + + void query(); + +private: + + void sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& oldUUID); + + void processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode); + + void processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode); + + void processBulkAvatarTraits(QSharedPointer message, SharedNodePointer sendingNode); + + void processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode); + + void processKillAvatar(const QUuid&, KillAvatarReason); + + Derived& derived(); + const Derived& derived() const; + + AvatarPtr getAvatar(const QUuid& sessionUUID, const QWeakPointer& mixer, bool& isNew); + + QUuid _lastOwnerSessionUUID; + std::unordered_map _processedTraitVersions; + std::chrono::steady_clock::time_point queryExpiry; +}; + +#endif /* end of include guard */ diff --git a/libraries/avatars-core/src/AvatarPacketHandler.hpp b/libraries/avatars-core/src/AvatarPacketHandler.hpp new file mode 100644 index 00000000000..eb53595f63b --- /dev/null +++ b/libraries/avatars-core/src/AvatarPacketHandler.hpp @@ -0,0 +1,439 @@ +// +// AvatarPacketHandler.hpp +// libraries/avatars-core/src +// +// Created by Nshan G. on 11 May 2022. +// Copyright 2014 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_AVATARS_CORE_SRC_AVATARPACKETHANDLER_HPP +#define LIBRARIES_AVATARS_CORE_SRC_AVATARPACKETHANDLER_HPP + +#include "AvatarPacketHandler.h" + +#include + +#include + +#include +#include +#include +#include + +#include "AvatarLogging.h" +#include "AvatarTraits.h" + +#include "Profile.h" + +template +Derived& AvatarPacketHandler::derived() { + return *static_cast(this); +} + +template +const Derived& AvatarPacketHandler::derived() const { + return *static_cast(this); +} + +template +AvatarPtr AvatarPacketHandler::getAvatar(const QUuid& sessionUUID, const QWeakPointer& mixer, bool& isNew) { + AvatarPtr avatar = derived().newOrExistingAvatar(sessionUUID, mixer, isNew); + if (isNew) { + // FIXME: CRTP, derived sets this multiple times, but with this it's no longer necessary + avatar->setSessionUUID(sessionUUID); + } + return avatar; +} + +template +AvatarPacketHandler::AvatarPacketHandler() { + auto nodeList = DependencyManager::get(); + + auto& packetReceiver = nodeList->getPacketReceiver(); + packetReceiver.registerListener(PacketType::BulkAvatarData, + PacketReceiver::makeSourcedListenerReference(&derived(), &AvatarPacketHandler::processAvatarDataPacket)); + packetReceiver.registerListener(PacketType::KillAvatar, + PacketReceiver::makeSourcedListenerReference(&derived(), &AvatarPacketHandler::processKillAvatar)); + packetReceiver.registerListener(PacketType::AvatarIdentity, + PacketReceiver::makeSourcedListenerReference(&derived(), &AvatarPacketHandler::processAvatarIdentityPacket)); + packetReceiver.registerListener(PacketType::BulkAvatarTraits, + PacketReceiver::makeSourcedListenerReference(&derived(), &AvatarPacketHandler::processBulkAvatarTraits)); + + derived().connect(nodeList.data(), &NodeList::uuidChanged, &derived(), [this](const QUuid& sessionUUID, const QUuid& oldUUID){ + sessionUUIDChanged(sessionUUID, oldUUID); + }); + + derived().connect(nodeList.data(), &NodeList::nodeKilled, &derived(), [this](SharedNodePointer killedNode){ + if (killedNode->getType() == NodeType::AvatarMixer) { + derived().clearOtherAvatars(); + } + }); + + derived().connect(nodeList.data(), &NodeList::nodeActivated, &derived(), [this](SharedNodePointer node){ + if (node->getType() == NodeType::AvatarMixer) { + queryExpiry = std::chrono::steady_clock::now(); + derived().onAvatarMixerActivated(); + } + }); + + derived().connect(nodeList.data(), &NodeList::ignoredNode, &derived(), [this](const QUuid& nodeID, bool enabled) { + if (enabled) { + processKillAvatar(nodeID, KillAvatarReason::AvatarIgnored); + } + derived().onNodeIngored(nodeID, enabled); + //FIXME: CRTP interface/src/avatar/AvatarManager has similar ignored node handler with this additional logic + // if (!enabled) { + // auto avatar = std::static_pointer_cast(getAvatarBySessionID(nodeID)); + // if (avatar) { + // avatar->createOrb(); + // } + // } + }); + +} + +template +void AvatarPacketHandler::processAvatarDataPacket(QSharedPointer message, SharedNodePointer sendingNode) { + DETAILED_PROFILE_RANGE(network, __FUNCTION__); + PerformanceTimer perfTimer("receiveAvatar"); + // enumerate over all of the avatars in this packet + // only add them if mixerWeakPointer points to something (meaning that mixer is still around) + while (message->getBytesLeftToRead()) { + parseAvatarData(message, sendingNode); + } +} + +template +void AvatarPacketHandler::parseAvatarData(QSharedPointer message, SharedNodePointer sendingNode) { + QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + + int positionBeforeRead = message->getPosition(); + + QByteArray byteArray = message->readWithoutCopy(message->getBytesLeftToRead()); + + // make sure this isn't our own avatar data or for a previously ignored node + auto nodeList = DependencyManager::get(); + bool isNewAvatar; + if (sessionUUID != _lastOwnerSessionUUID && (!nodeList->isIgnoringNode(sessionUUID) || nodeList->getRequestsDomainListData())) { + + AvatarPtr avatar = getAvatar(sessionUUID, sendingNode, isNewAvatar); + + // FIXME: CRTP move to Derived::newOrExistingAvatar + // if (isNewAvatar) { + // QWriteLocker locker(&_hashLock); + // avatar->setIsNewAvatar(true); + // auto replicaIDs = _replicas.getReplicaIDs(sessionUUID); + // for (auto replicaID : replicaIDs) { + // auto replicaAvatar = addAvatar(replicaID, sendingNode); + // replicaAvatar->setIsNewAvatar(true); + // _replicas.addReplica(sessionUUID, replicaAvatar); + // } + // } + + // have the matching (or new) avatar parse the data from the packet + int bytesRead = avatar->parseDataFromBuffer(byteArray); + derived().onAvatarDataReceived(sessionUUID, byteArray); + + // FIXME: CRTP move to Derived::onAvatarDataReceived + // _replicas.parseDataFromBuffer(sessionUUID, byteArray); + + message->seek(positionBeforeRead + bytesRead); + } else { + // Shouldn't happen if mixer functioning correctly - debugging for BUGZ-781: + qCDebug(avatars) << "Discarding received avatar data" << sessionUUID << (sessionUUID == _lastOwnerSessionUUID ? "(is self)" : "") + << "isIgnoringNode = " << nodeList->isIgnoringNode(sessionUUID); + + // create a dummy AvatarData class to throw this data on the ground + typename std::pointer_traits::element_type dummyData; + int bytesRead = dummyData.parseDataFromBuffer(byteArray); + message->seek(positionBeforeRead + bytesRead); + } +} + +template +void AvatarPacketHandler::processAvatarIdentityPacket(QSharedPointer message, SharedNodePointer sendingNode) { + QDataStream avatarIdentityStream(message->getMessage()); + + while (!avatarIdentityStream.atEnd()) { + // peek the avatar UUID from the incoming packet + avatarIdentityStream.startTransaction(); + QUuid identityUUID; + avatarIdentityStream >> identityUUID; + avatarIdentityStream.rollbackTransaction(); + + if (identityUUID.isNull()) { + qCDebug(avatars) << "Refusing to process identity packet for null avatar ID"; + return; + } + + // make sure this isn't for an ignored avatar + auto nodeList = DependencyManager::get(); + + identityUUID = derived().mapIdentityUUID(identityUUID); + + // FIXME: CRTP Derived::mapIdentityUUID + // { + // static auto EMPTY = QUuid(); + // QReadLocker locker(&_hashLock); + // auto me = _avatarHash.find(EMPTY); + // if ((me != _avatarHash.end()) && (identityUUID == me.value()->getSessionUUID())) { + // // We add MyAvatar to _avatarHash with an empty UUID. Code relies on this. In order to correctly handle an + // // identity packet for ourself (such as when we are assigned a sessionDisplayName by the mixer upon joining), + // // we make things match here. + // identityUUID = EMPTY; + // } + // } + + if (!nodeList->isIgnoringNode(identityUUID) || nodeList->getRequestsDomainListData()) { + // mesh URL for a UUID, find avatar in our list + bool isNewAvatar; + AvatarPtr avatar = getAvatar(identityUUID, sendingNode, isNewAvatar); + bool identityChanged = false; + bool displayNameChanged = false; + // In this case, the "sendingNode" is the Avatar Mixer. + avatar->processAvatarIdentity(avatarIdentityStream, identityChanged, displayNameChanged); + + derived().onAvatarIdentityReceived(identityUUID, message->getMessage()); + + // FIXME: CRTP move to Derived::onAvatarIdentityReceived + // _replicas.processAvatarIdentity(identityUUID, message->getMessage(), identityChanged, displayNameChanged); + } + } +} + +template +void AvatarPacketHandler::processBulkAvatarTraits(QSharedPointer message, SharedNodePointer sendingNode) { + AvatarTraits::TraitMessageSequence seq; + + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < (qint64)sizeof(AvatarTraits::TraitMessageSequence)) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + + message->readPrimitive(&seq); + + auto traitsAckPacket = NLPacket::create(PacketType::BulkAvatarTraitsAck, sizeof(AvatarTraits::TraitMessageSequence), true); + traitsAckPacket->writePrimitive(seq); + auto nodeList = DependencyManager::get(); + SharedNodePointer avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer); + if (!avatarMixer.isNull()) { + // we have a mixer to send to, acknowledge that we received these + // traits. + nodeList->sendPacket(std::move(traitsAckPacket), *avatarMixer); + } + + while (message->getBytesLeftToRead() > 0) { + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < qint64(NUM_BYTES_RFC4122_UUID + + sizeof(AvatarTraits::TraitType))) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + + // read the avatar ID to figure out which avatar this is for + auto avatarID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + + // grab the avatar so we can ask it to process trait data + bool isNewAvatar; + AvatarPtr avatar = getAvatar(avatarID, sendingNode, isNewAvatar); + + // read the first trait type for this avatar + AvatarTraits::TraitType traitType; + message->readPrimitive(&traitType); + + // grab the last trait versions for this avatar + auto& lastProcessedVersions = _processedTraitVersions[avatarID]; + + while (traitType != AvatarTraits::NullTrait && message->getBytesLeftToRead() > 0) { + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitVersion))) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + + AvatarTraits::TraitVersion packetTraitVersion; + message->readPrimitive(&packetTraitVersion); + + AvatarTraits::TraitWireSize traitBinarySize; + bool skipBinaryTrait = false; + + if (AvatarTraits::isSimpleTrait(traitType)) { + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitWireSize))) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + + message->readPrimitive(&traitBinarySize); + + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < traitBinarySize) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + + // check if this trait version is newer than what we already have for this avatar + if (packetTraitVersion > lastProcessedVersions[traitType]) { + auto traitData = message->read(traitBinarySize); + avatar->processTrait(traitType, traitData); + + // FIXME::CRTP + // _replicas.processTrait(avatarID, traitType, traitData); + + lastProcessedVersions[traitType] = packetTraitVersion; + } else { + skipBinaryTrait = true; + } + } else { + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < qint64(NUM_BYTES_RFC4122_UUID + + sizeof(AvatarTraits::TraitWireSize))) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + + AvatarTraits::TraitInstanceID traitInstanceID = + QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + + message->readPrimitive(&traitBinarySize); + + // Trying to read more bytes than available, bail + if (traitBinarySize < -1 || message->getBytesLeftToRead() < traitBinarySize) { + qWarning() << "Malformed bulk trait packet, bailling"; + return; + } + + auto& processedInstanceVersion = lastProcessedVersions.getInstanceValueRef(traitType, traitInstanceID); + if (packetTraitVersion > processedInstanceVersion) { + if (traitBinarySize == AvatarTraits::DELETED_TRAIT_SIZE) { + avatar->processDeletedTraitInstance(traitType, traitInstanceID); + // FIXME::CRTP + // _replicas.processDeletedTraitInstance(avatarID, traitType, traitInstanceID); + } else { + auto traitData = message->read(traitBinarySize); + avatar->processTraitInstance(traitType, traitInstanceID, traitData); + // FIXME::CRTP + // _replicas.processTraitInstance(avatarID, traitType, traitInstanceID, traitData); + } + processedInstanceVersion = packetTraitVersion; + } else { + skipBinaryTrait = true; + } + } + + if (skipBinaryTrait && traitBinarySize > 0) { + // we didn't read this trait because it was older or because we didn't have an avatar to process it for + message->seek(message->getPosition() + traitBinarySize); + } + + // read the next trait type, which is null if there are no more traits for this avatar + message->readPrimitive(&traitType); + } + } +} + +template +void AvatarPacketHandler::processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode) { + // read the node id + QUuid sessionUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + + KillAvatarReason reason; + message->readPrimitive(&reason); + + processKillAvatar(sessionUUID, reason); +} + +template +void AvatarPacketHandler::processKillAvatar(const QUuid& sessionUUID, KillAvatarReason reason) { + // remove any information about processed traits for this avatar + // TODO: CRTP removeAvatar on derived returns nothing, this expects it to return the removed avatar (not replicas) + auto removed = derived().removeAvatar(sessionUUID, reason); + if (removed) { + _processedTraitVersions.erase(removed->getSessionUUID()); + } +} + +// FIXME: move to Derived, some code was removed in comparison to what's there now +// void AvatarHashMap::handleRemovedAvatar(const AvatarSharedPointer& removedAvatar, KillAvatarReason removalReason) { +// qCDebug(avatars) << "Removed avatar with UUID" << uuidStringWithoutCurlyBraces(removedAvatar->getSessionUUID()) +// << "from AvatarHashMap" << removalReason; +// emit avatarRemovedEvent(removedAvatar->getSessionUUID()); +// } + +template +void AvatarPacketHandler::sessionUUIDChanged(const QUuid& sessionUUID, const QUuid& oldUUID) { + _lastOwnerSessionUUID = oldUUID; + + derived().onSessionUUIDChanged(sessionUUID, oldUUID); + // FIXME: CRTP + // emit avatarSessionChangedEvent(sessionUUID, oldUUID); +} + +template +void AvatarPacketHandler::clearOtherAvatars() { + _processedTraitVersions.clear(); + // FIXME: CRTP, virtual overridden in AvatarManager without calling base, but this should be called + // QList removedAvatars; + // + // { + // QWriteLocker locker(&_hashLock); + // + // // grab a copy of the current avatars so we can call handleRemoveAvatar for them + // removedAvatars = _avatarHash.values(); + // + // _avatarHash.clear(); + // } + // + // for (auto& av : removedAvatars) { + // handleRemovedAvatar(av); + // } +} + +template +void AvatarPacketHandler::query() { + bool viewIsDifferentEnough = false; + auto& views = derived().getViews(); + auto& lastQueriedViews = derived().getLastQueriedViews(); + if (views.size() == lastQueriedViews.size()) { + for (size_t i = 0; i < views.size(); ++i) { + if (!views[i].isVerySimilar(lastQueriedViews[i])) { + viewIsDifferentEnough = true; + break; + } + } + } else { + viewIsDifferentEnough = true; + } + + // if it's been a while since our last query or the view has significantly changed then send a query, otherwise suppress it + static const std::chrono::seconds MIN_PERIOD_BETWEEN_QUERIES { 3 }; + auto now = std::chrono::steady_clock::now(); + if (now > queryExpiry || viewIsDifferentEnough) { + auto avatarPacket = NLPacket::create(PacketType::AvatarQuery); + auto destinationBuffer = reinterpret_cast(avatarPacket->getPayload()); + unsigned char* bufferStart = destinationBuffer; + + uint8_t numFrustums = (uint8_t)views.size(); + memcpy(destinationBuffer, &numFrustums, sizeof(numFrustums)); + destinationBuffer += sizeof(numFrustums); + + for (const auto& view : views) { + destinationBuffer += view.serialize(destinationBuffer); + } + + avatarPacket->setPayloadSize(destinationBuffer - bufferStart); + + DependencyManager::get()->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); + + lastQueriedViews = views; + queryExpiry = now + MIN_PERIOD_BETWEEN_QUERIES; + } +} + +#endif /* end of include guard */ diff --git a/libraries/avatars-core/src/AvatarTraits.cpp b/libraries/avatars-core/src/AvatarTraits.cpp new file mode 100644 index 00000000000..8400f5c6a1a --- /dev/null +++ b/libraries/avatars-core/src/AvatarTraits.cpp @@ -0,0 +1,30 @@ +// +// AvatarTraits.hpp +// libraries/avatars-core/src +// +// Created by Nshan G. on 21 Apr 2022. +// Copyright 2019 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 +// + +#include "AvatarTraits.h" + +#include + +namespace AvatarTraits { + qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, + TraitVersion traitVersion) { + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive(traitType); + if (traitVersion > DEFAULT_TRAIT_VERSION) { + bytesWritten += destination.writePrimitive(traitVersion); + } + bytesWritten += destination.write(instanceID.toRfc4122()); + bytesWritten += destination.writePrimitive(DELETED_TRAIT_SIZE); + return bytesWritten; + } +}; diff --git a/libraries/avatars-core/src/AvatarTraits.h b/libraries/avatars-core/src/AvatarTraits.h new file mode 100644 index 00000000000..ed9c4e5e147 --- /dev/null +++ b/libraries/avatars-core/src/AvatarTraits.h @@ -0,0 +1,88 @@ +// +// AvatarTraits.h +// libraries/avatars-core/src +// +// Created by Nshan G. on 21 Apr 2022. +// Copyright 2018 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_AVATARS_CORE_SRC_AVATARTRAITS_H +#define LIBRARIES_AVATARS_CORE_SRC_AVATARTRAITS_H + +#include +#include +#include +#include + +#include + +class ExtendedIODevice; + +namespace AvatarTraits { + enum TraitType : int8_t { + // Null trait + NullTrait = -1, + + // Simple traits + SkeletonModelURL = 0, + SkeletonData, + // Instanced traits + FirstInstancedTrait, + AvatarEntity = FirstInstancedTrait, + Grab, + + // Traits count + TotalTraitTypes + }; + + const int NUM_SIMPLE_TRAITS = (int)FirstInstancedTrait; + const int NUM_INSTANCED_TRAITS = (int)TotalTraitTypes - (int)FirstInstancedTrait; + const int NUM_TRAITS = (int)TotalTraitTypes; + + using TraitInstanceID = QUuid; + + inline bool isSimpleTrait(TraitType traitType) { + return traitType > NullTrait && traitType < FirstInstancedTrait; + } + + using TraitVersion = int32_t; + const TraitVersion DEFAULT_TRAIT_VERSION = 0; + const TraitVersion NULL_TRAIT_VERSION = -1; + + using TraitWireSize = int16_t; + const TraitWireSize DELETED_TRAIT_SIZE = -1; + const TraitWireSize MAXIMUM_TRAIT_SIZE = INT16_MAX; + + using TraitMessageSequence = int64_t; + const TraitMessageSequence FIRST_TRAIT_SEQUENCE = 0; + const TraitMessageSequence MAX_TRAIT_SEQUENCE = INT64_MAX; + + + template + qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarDataStream& avatar); + + template + qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination, + TraitVersion traitVersion, const AvatarDataStream& avatar); + + template + qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + + ExtendedIODevice& destination, AvatarDataStream& avatar); + + template + qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, TraitVersion traitVersion, + AvatarDataStream& avatar); + + qint64 packInstancedTraitDelete(TraitType traitType, TraitInstanceID instanceID, ExtendedIODevice& destination, + TraitVersion traitVersion = NULL_TRAIT_VERSION); + +}; + +#endif /* end of include guard */ diff --git a/libraries/avatars-core/src/AvatarTraits.hpp b/libraries/avatars-core/src/AvatarTraits.hpp new file mode 100644 index 00000000000..548f4bd7eae --- /dev/null +++ b/libraries/avatars-core/src/AvatarTraits.hpp @@ -0,0 +1,131 @@ +// +// AvatarTraits.hpp +// libraries/avatars-core/src +// +// Created by Nshan G. on 21 Apr 2022. +// Copyright 2019 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_AVATARS_CORE_SRC_AVATARTRAITS_HPP +#define LIBRARIES_AVATARS_CORE_SRC_AVATARTRAITS_HPP + +#include "AvatarTraits.h" + +#include + +namespace AvatarTraits { + + template + qint64 packTrait(TraitType traitType, ExtendedIODevice& destination, const AvatarDataStream& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTrait(traitType); + auto traitBinaryDataSize = traitBinaryData.size(); + + // Verify packed data + if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + return bytesWritten; + } + + template + qint64 packVersionedTrait(TraitType traitType, ExtendedIODevice& destination, + TraitVersion traitVersion, const AvatarDataStream& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTrait(traitType); + auto traitBinaryDataSize = traitBinaryData.size(); + + // Verify packed data + if (traitBinaryDataSize > MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack simple trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size" << MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitVersion)traitVersion); + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + return bytesWritten; + } + + + template + qint64 packTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, AvatarDataStream& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID); + auto traitBinaryDataSize = traitBinaryData.size(); + + + // Verify packed data + if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.write(traitInstanceID.toRfc4122()); + + if (!traitBinaryData.isNull()) { + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + } else { + bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); + } + + return bytesWritten; + } + + template + qint64 packVersionedTraitInstance(TraitType traitType, TraitInstanceID traitInstanceID, + ExtendedIODevice& destination, TraitVersion traitVersion, + AvatarDataStream& avatar) { + // Call packer function + auto traitBinaryData = avatar.packTraitInstance(traitType, traitInstanceID); + auto traitBinaryDataSize = traitBinaryData.size(); + + + // Verify packed data + if (traitBinaryDataSize > AvatarTraits::MAXIMUM_TRAIT_SIZE) { + qWarning() << "Refusing to pack instanced trait" << traitType << "of size" << traitBinaryDataSize + << "bytes since it exceeds the maximum size " << AvatarTraits::MAXIMUM_TRAIT_SIZE << "bytes"; + return 0; + } + + // Write packed data to stream + qint64 bytesWritten = 0; + bytesWritten += destination.writePrimitive((TraitType)traitType); + bytesWritten += destination.writePrimitive((TraitVersion)traitVersion); + bytesWritten += destination.write(traitInstanceID.toRfc4122()); + + if (!traitBinaryData.isNull()) { + bytesWritten += destination.writePrimitive((TraitWireSize)traitBinaryDataSize); + bytesWritten += destination.write(traitBinaryData); + } else { + bytesWritten += destination.writePrimitive(AvatarTraits::DELETED_TRAIT_SIZE); + } + + return bytesWritten; + } +}; + +#endif /* end of include guard */ diff --git a/libraries/avatars-core/src/ClientTraitsHandler.h b/libraries/avatars-core/src/ClientTraitsHandler.h new file mode 100644 index 00000000000..928971323e6 --- /dev/null +++ b/libraries/avatars-core/src/ClientTraitsHandler.h @@ -0,0 +1,68 @@ +// +// ClientTraitsHandler.h +// libraries/avatars-core/src +// +// Created by Nshan G. on 21 Apr 2022. +// Copyright 2018 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_AVATARS_CORE_SRC_CLIENTTRAITSHANDLER_H +#define LIBRARIES_AVATARS_CORE_SRC_CLIENTTRAITSHANDLER_H + +#include +#include + +#include +#include + +#include "AssociatedTraitValues.h" + +template +class ClientTraitsHandler { + +public: + ClientTraitsHandler(AvatarPtr owningAvatar); + + int sendChangedTraitsToMixer(); + + bool hasChangedTraits() const { return _hasChangedTraits; } + + void markTraitUpdated(AvatarTraits::TraitType updatedTrait); + void markInstancedTraitUpdated(AvatarTraits::TraitType traitType, QUuid updatedInstanceID); + void markInstancedTraitDeleted(AvatarTraits::TraitType traitType, QUuid deleteInstanceID); + + void resetForNewMixer(SharedNodePointer mixerNode); + + void processTraitOverride(QSharedPointer message, SharedNodePointer sendingNode); + +private: + using Mutex = std::recursive_mutex; + using Lock = std::lock_guard; + + enum ClientTraitStatus { + Unchanged, + Updated, + Deleted + }; + + const AvatarPtr _owningAvatar; + + Mutex _traitLock; + AvatarTraits::AssociatedTraitValues _traitStatuses; + + AvatarTraits::TraitVersion _currentTraitVersion { AvatarTraits::DEFAULT_TRAIT_VERSION }; + AvatarTraits::TraitVersion _currentSkeletonVersion { AvatarTraits::NULL_TRAIT_VERSION }; + + bool _shouldPerformInitialSend { false }; + bool _hasChangedTraits { false }; + + Derived& derived(); + const Derived& derived() const; +}; + +#endif /* end of include guard */ diff --git a/libraries/avatars-core/src/ClientTraitsHandler.hpp b/libraries/avatars-core/src/ClientTraitsHandler.hpp new file mode 100644 index 00000000000..99748abc397 --- /dev/null +++ b/libraries/avatars-core/src/ClientTraitsHandler.hpp @@ -0,0 +1,223 @@ +// +// ClientTraitsHandler.hpp +// libraries/avatars-core/src +// +// Created by Nshan G. on 21 Apr 2022. +// Copyright 2018 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_AVATARS_CORE_SRC_CLIENTTRAITSHANDLER_HPP +#define LIBRARIES_AVATARS_CORE_SRC_CLIENTTRAITSHANDLER_HPP + +#include "ClientTraitsHandler.h" + +#include + +#include +#include + +#include "AvatarTraits.hpp" + +template +ClientTraitsHandler::ClientTraitsHandler(AvatarPtr owningAvatar) : + _owningAvatar(owningAvatar) +{ + auto nodeList = DependencyManager::get(); + derived().connect(nodeList.data(), &NodeList::nodeAdded, + &derived(), [this](auto mixer){ resetForNewMixer(mixer); }); + + nodeList->getPacketReceiver().registerListener(PacketType::SetAvatarTraits, + PacketReceiver::makeSourcedListenerReference(&derived(), &ClientTraitsHandler::processTraitOverride)); +} + +template +void ClientTraitsHandler::markTraitUpdated(AvatarTraits::TraitType updatedTrait) { + Lock lock(_traitLock); + _traitStatuses[updatedTrait] = Updated; + _hasChangedTraits = true; +} + +template +void ClientTraitsHandler::markInstancedTraitUpdated(AvatarTraits::TraitType traitType, QUuid updatedInstanceID) { + Lock lock(_traitLock); + _traitStatuses.instanceInsert(traitType, updatedInstanceID, Updated); + _hasChangedTraits = true; +} + +template +void ClientTraitsHandler::markInstancedTraitDeleted(AvatarTraits::TraitType traitType, QUuid deleteInstanceID) { + Lock lock(_traitLock); + _traitStatuses.instanceInsert(traitType, deleteInstanceID, Deleted); + _hasChangedTraits = true; +} + +template +void ClientTraitsHandler::resetForNewMixer(SharedNodePointer mixerNode) { + if (mixerNode->getType() != NodeType::AvatarMixer) { + return; + } + + Lock lock(_traitLock); + + // re-set the current version to 0 + _currentTraitVersion = AvatarTraits::DEFAULT_TRAIT_VERSION; + + // mark that all traits should be sent next time + _shouldPerformInitialSend = true; + + // reset the trait statuses + _traitStatuses.reset(); + + // pre-fill the instanced statuses that we will need to send next frame + _owningAvatar->prepareResetTraitInstances(); +} + +template +int ClientTraitsHandler::sendChangedTraitsToMixer() { + std::unique_lock lock(_traitLock); + int bytesWritten = 0; + + if (hasChangedTraits() || _shouldPerformInitialSend) { + // we have at least one changed trait to send + + auto nodeList = DependencyManager::get(); + auto avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer); + if (!avatarMixer || !avatarMixer->getActiveSocket()) { + // we don't have an avatar mixer with an active socket, we can't send changed traits at this time + return 0; + } + + // we have a mixer to send to, setup our set traits packet + auto traitsPacketList = NLPacketList::create(PacketType::SetAvatarTraits, QByteArray(), true, true); + + // bump and write the current trait version to an extended header + // the trait version is the same for all traits in this packet list + traitsPacketList->writePrimitive(++_currentTraitVersion); + + // take a copy of the set of changed traits and clear the stored set + auto traitStatusesCopy { _traitStatuses }; + _traitStatuses.reset(); + _hasChangedTraits = false; + + // if this was an initial send of all traits, consider it completed + bool initialSend = _shouldPerformInitialSend; + _shouldPerformInitialSend = false; + + // we can release the lock here since we've taken a copy of statuses + // and will setup the packet using the information in the copy + lock.unlock(); + + auto simpleIt = traitStatusesCopy.simpleCBegin(); + while (simpleIt != traitStatusesCopy.simpleCEnd()) { + // because the vector contains all trait types (for access using trait type as index) + // we double check that it is a simple iterator here + auto traitType = static_cast(std::distance(traitStatusesCopy.simpleCBegin(), simpleIt)); + + if (initialSend || *simpleIt == Updated) { + bytesWritten += AvatarTraits::packTrait(traitType, *traitsPacketList, *_owningAvatar); + + if (traitType == AvatarTraits::SkeletonModelURL) { + // keep track of our skeleton version in case we get an override back + _currentSkeletonVersion = _currentTraitVersion; + } + } + + ++simpleIt; + } + + auto instancedIt = traitStatusesCopy.instancedCBegin(); + while (instancedIt != traitStatusesCopy.instancedCEnd()) { + for (auto& instanceIDValuePair : instancedIt->instances) { + if ((initialSend && instanceIDValuePair.value != Deleted) + || instanceIDValuePair.value == Updated) { + // this is a changed trait we need to send or we haven't send out trait information yet + // ask the owning avatar to pack it + bytesWritten += AvatarTraits::packTraitInstance(instancedIt->traitType, instanceIDValuePair.id, + *traitsPacketList, *_owningAvatar); + + } else if (!initialSend && instanceIDValuePair.value == Deleted) { + // pack delete for this trait instance + bytesWritten += AvatarTraits::packInstancedTraitDelete(instancedIt->traitType, instanceIDValuePair.id, + *traitsPacketList); + } + } + + ++instancedIt; + } + + nodeList->sendPacketList(std::move(traitsPacketList), *avatarMixer); + } + + return bytesWritten; +} + +template +void ClientTraitsHandler::processTraitOverride(QSharedPointer message, SharedNodePointer sendingNode) { + if (sendingNode->getType() == NodeType::AvatarMixer) { + Lock lock(_traitLock); + + while (message->getBytesLeftToRead() > 0) { + // Trying to read more bytes than available, bail + if (message->getBytesLeftToRead() < qint64(sizeof(AvatarTraits::TraitType) + + sizeof(AvatarTraits::TraitVersion) + + sizeof(AvatarTraits::TraitWireSize))) { + qWarning() << "Malformed trait override packet, bailling"; + return; + } + + AvatarTraits::TraitType traitType; + message->readPrimitive(&traitType); + + AvatarTraits::TraitVersion traitVersion; + message->readPrimitive(&traitVersion); + + AvatarTraits::TraitWireSize traitBinarySize; + message->readPrimitive(&traitBinarySize); + + // Trying to read more bytes than available, bail + if (traitBinarySize < -1 || message->getBytesLeftToRead() < traitBinarySize) { + qWarning() << "Malformed trait override packet, bailling"; + return; + } + + // only accept an override if this is for a trait type we override + // and the version matches what we last sent for skeleton + if (traitType == AvatarTraits::SkeletonModelURL + && traitVersion == _currentSkeletonVersion + && _traitStatuses[AvatarTraits::SkeletonModelURL] != Updated) { + + // override the skeleton URL but do not mark the trait as having changed + // so that we don't unecessarily send a new trait packet to the mixer with the overriden URL + + auto hasChangesBefore = _hasChangedTraits; + + auto traitBinaryData = message->readWithoutCopy(traitBinarySize); + _owningAvatar->processTrait(traitType, traitBinaryData); + + // setSkeletonModelURL will flag us for changes to the SkeletonModelURL so we reset some state here to + // avoid unnecessarily sending the overriden skeleton model URL back to the mixer + _traitStatuses.erase(AvatarTraits::SkeletonModelURL); + _hasChangedTraits = hasChangesBefore; + } else { + message->seek(message->getPosition() + traitBinarySize); + } + } + } +} + +template +Derived& ClientTraitsHandler::derived() { + return *static_cast(this); +} + +template +const Derived& ClientTraitsHandler::derived() const { + return *static_cast(this); +} + +#endif /* end of include guard */ diff --git a/libraries/opus-codec/CMakeLists.txt b/libraries/opus-codec/CMakeLists.txt new file mode 100644 index 00000000000..ef3467bf97c --- /dev/null +++ b/libraries/opus-codec/CMakeLists.txt @@ -0,0 +1,4 @@ +set(TARGET_NAME opus-codec) +setup_hifi_library() +link_hifi_libraries(shared audio) +target_opus() diff --git a/libraries/opus-codec/src/OpusCodec.cpp b/libraries/opus-codec/src/OpusCodec.cpp new file mode 100644 index 00000000000..3aeec062a67 --- /dev/null +++ b/libraries/opus-codec/src/OpusCodec.cpp @@ -0,0 +1,34 @@ +// +// OpusCodec.cpp +// libraries/opus-codec/src +// +// Created by Nshan G. on 3 July 2022. +// Copyright 2019 Michael Bailey +// 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 +// + +#include "OpusCodec.h" +#include "OpusEncoder.h" +#include "OpusDecoder.h" + +const char* OpusCodec::NAME { "opus" }; + +Encoder* OpusCodec::createEncoder(int sampleRate, int numChannels) { + return new OpusEncoder(sampleRate, numChannels); +} + +Decoder* OpusCodec::createDecoder(int sampleRate, int numChannels) { + return new OpusDecoder(sampleRate, numChannels); +} + +void OpusCodec::releaseEncoder(Encoder* encoder) { + delete encoder; +} + +void OpusCodec::releaseDecoder(Decoder* decoder) { + delete decoder; +} diff --git a/libraries/opus-codec/src/OpusCodec.h b/libraries/opus-codec/src/OpusCodec.h new file mode 100644 index 00000000000..0ae193175de --- /dev/null +++ b/libraries/opus-codec/src/OpusCodec.h @@ -0,0 +1,35 @@ +// +// OpusCodec.h +// libraries/opus-codec/src +// +// Created by Nshan G. on 3 July 2022. +// Copyright 2019 Michael Bailey +// 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_OPUS_CODEC_SRC_OPUSCODEC_H +#define LIBRARIES_OPUS_CODEC_SRC_OPUSCODEC_H + +#include + +class OpusCodec : public Codec { + +public: + const QString getName() const override { return NAME; } + + virtual Encoder* createEncoder(int sampleRate, int numChannels) override; + virtual Decoder* createDecoder(int sampleRate, int numChannels) override; + virtual void releaseEncoder(Encoder* encoder) override; + virtual void releaseDecoder(Decoder* decoder) override; + + static const char* getNameCString() { return NAME; } + +private: + static const char* NAME; +}; + +#endif /* end of include guard */ diff --git a/plugins/opusCodec/src/OpusDecoder.cpp b/libraries/opus-codec/src/OpusDecoder.cpp similarity index 89% rename from plugins/opusCodec/src/OpusDecoder.cpp rename to libraries/opus-codec/src/OpusDecoder.cpp index e3e4e3645a4..3b403802eb0 100644 --- a/plugins/opusCodec/src/OpusDecoder.cpp +++ b/libraries/opus-codec/src/OpusDecoder.cpp @@ -1,6 +1,6 @@ // -// OpusCodecManager.h -// plugins/opusCodec/src +// OpusDecoder.h +// libraries/opus-codec/src // // Copyright 2020 Dale Glass // @@ -14,7 +14,7 @@ #include "OpusDecoder.h" -static QLoggingCategory decoder("AthenaOpusDecoder"); +static QLoggingCategory decoder("OpusDecoder"); static QString error_to_string(int error) { switch (error) { @@ -38,7 +38,7 @@ static QString error_to_string(int error) { } -AthenaOpusDecoder::AthenaOpusDecoder(int sampleRate, int numChannels) { +OpusDecoder::OpusDecoder(int sampleRate, int numChannels) { int error; _opusSampleRate = sampleRate; @@ -56,16 +56,16 @@ AthenaOpusDecoder::AthenaOpusDecoder(int sampleRate, int numChannels) { qCDebug(decoder) << "Opus decoder initialized, sampleRate = " << sampleRate << "; numChannels = " << numChannels; } -AthenaOpusDecoder::~AthenaOpusDecoder() { +OpusDecoder::~OpusDecoder() { if (_decoder) { opus_decoder_destroy(_decoder); } } -void AthenaOpusDecoder::decode(const QByteArray &encodedBuffer, QByteArray &decodedBuffer) { +void OpusDecoder::decode(const QByteArray &encodedBuffer, QByteArray &decodedBuffer) { assert(_decoder); - PerformanceTimer perfTimer("AthenaOpusDecoder::decode"); + PerformanceTimer perfTimer("OpusDecoder::decode"); // The audio system encodes and decodes always in fixed size chunks int bufferSize = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * static_cast(sizeof(int16_t)) @@ -96,10 +96,10 @@ void AthenaOpusDecoder::decode(const QByteArray &encodedBuffer, QByteArray &deco } -void AthenaOpusDecoder::lostFrame(QByteArray &decodedBuffer) { +void OpusDecoder::lostFrame(QByteArray &decodedBuffer) { assert(_decoder); - PerformanceTimer perfTimer("AthenaOpusDecoder::lostFrame"); + PerformanceTimer perfTimer("OpusDecoder::lostFrame"); int bufferSize = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * static_cast(sizeof(int16_t)) * _opusNumChannels; diff --git a/plugins/opusCodec/src/OpusDecoder.h b/libraries/opus-codec/src/OpusDecoder.h similarity index 63% rename from plugins/opusCodec/src/OpusDecoder.h rename to libraries/opus-codec/src/OpusDecoder.h index 095893856bf..f9d4f9cc4b0 100644 --- a/plugins/opusCodec/src/OpusDecoder.h +++ b/libraries/opus-codec/src/OpusDecoder.h @@ -1,6 +1,6 @@ // -// OpusCodecManager.h -// plugins/opusCodec/src +// OpusDecoder.h +// libraries/opus-codec/src // // Copyright 2020 Dale Glass // @@ -8,18 +8,17 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef OPUSDECODER_H -#define OPUSDECODER_H +#ifndef LIBRARIES_OPUS_CODEC_SRC_OPUSDECODER_H +#define LIBRARIES_OPUS_CODEC_SRC_OPUSDECODER_H -#include +#include #include - -class AthenaOpusDecoder : public Decoder { +class OpusDecoder : public Decoder { public: - AthenaOpusDecoder(int sampleRate, int numChannels); - ~AthenaOpusDecoder() override; + OpusDecoder(int sampleRate, int numChannels); + ~OpusDecoder() override; virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override; @@ -35,5 +34,4 @@ class AthenaOpusDecoder : public Decoder { int _decodedSize = 0; }; - -#endif // OPUSDECODER_H +#endif /* end of include guard */ diff --git a/plugins/opusCodec/src/OpusEncoder.cpp b/libraries/opus-codec/src/OpusEncoder.cpp similarity index 82% rename from plugins/opusCodec/src/OpusEncoder.cpp rename to libraries/opus-codec/src/OpusEncoder.cpp index 3408701633b..8a6d098ef46 100644 --- a/plugins/opusCodec/src/OpusEncoder.cpp +++ b/libraries/opus-codec/src/OpusEncoder.cpp @@ -8,13 +8,13 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include "OpusEncoder.h" + #include #include #include -#include "OpusEncoder.h" - -static QLoggingCategory encoder("AthenaOpusEncoder"); +static QLoggingCategory encoder("OpusEncoder"); static QString errorToString(int error) { switch (error) { @@ -39,7 +39,7 @@ static QString errorToString(int error) { -AthenaOpusEncoder::AthenaOpusEncoder(int sampleRate, int numChannels) { +OpusEncoder::OpusEncoder(int sampleRate, int numChannels) { _opusSampleRate = sampleRate; _opusChannels = numChannels; @@ -61,15 +61,15 @@ AthenaOpusEncoder::AthenaOpusEncoder(int sampleRate, int numChannels) { qCDebug(encoder) << "Opus encoder initialized, sampleRate = " << sampleRate << "; numChannels = " << numChannels; } -AthenaOpusEncoder::~AthenaOpusEncoder() { +OpusEncoder::~OpusEncoder() { opus_encoder_destroy(_encoder); } -void AthenaOpusEncoder::encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { +void OpusEncoder::encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) { - PerformanceTimer perfTimer("AthenaOpusEncoder::encode"); + PerformanceTimer perfTimer("OpusEncoder::encode"); assert(_encoder); encodedBuffer.resize(decodedBuffer.size()); @@ -89,14 +89,14 @@ void AthenaOpusEncoder::encode(const QByteArray& decodedBuffer, QByteArray& enco } -int AthenaOpusEncoder::getComplexity() const { +int OpusEncoder::getComplexity() const { assert(_encoder); int returnValue; opus_encoder_ctl(_encoder, OPUS_GET_COMPLEXITY(&returnValue)); return returnValue; } -void AthenaOpusEncoder::setComplexity(int complexity) { +void OpusEncoder::setComplexity(int complexity) { assert(_encoder); int returnValue = opus_encoder_ctl(_encoder, OPUS_SET_COMPLEXITY(complexity)); @@ -105,14 +105,14 @@ void AthenaOpusEncoder::setComplexity(int complexity) { } } -int AthenaOpusEncoder::getBitrate() const { +int OpusEncoder::getBitrate() const { assert(_encoder); int returnValue; opus_encoder_ctl(_encoder, OPUS_GET_BITRATE(&returnValue)); return returnValue; } -void AthenaOpusEncoder::setBitrate(int bitrate) { +void OpusEncoder::setBitrate(int bitrate) { assert(_encoder); int errorCode = opus_encoder_ctl(_encoder, OPUS_SET_BITRATE(bitrate)); @@ -121,14 +121,14 @@ void AthenaOpusEncoder::setBitrate(int bitrate) { } } -int AthenaOpusEncoder::getVBR() const { +int OpusEncoder::getVBR() const { assert(_encoder); int returnValue; opus_encoder_ctl(_encoder, OPUS_GET_VBR(&returnValue)); return returnValue; } -void AthenaOpusEncoder::setVBR(int vbr) { +void OpusEncoder::setVBR(int vbr) { assert(_encoder); int errorCode = opus_encoder_ctl(_encoder, OPUS_SET_VBR(vbr)); @@ -137,14 +137,14 @@ void AthenaOpusEncoder::setVBR(int vbr) { } } -int AthenaOpusEncoder::getVBRConstraint() const { +int OpusEncoder::getVBRConstraint() const { assert(_encoder); int returnValue; opus_encoder_ctl(_encoder, OPUS_GET_VBR_CONSTRAINT(&returnValue)); return returnValue; } -void AthenaOpusEncoder::setVBRConstraint(int vbr_const) { +void OpusEncoder::setVBRConstraint(int vbr_const) { assert(_encoder); int errorCode = opus_encoder_ctl(_encoder, OPUS_SET_VBR_CONSTRAINT(vbr_const)); @@ -153,14 +153,14 @@ void AthenaOpusEncoder::setVBRConstraint(int vbr_const) { } } -int AthenaOpusEncoder::getMaxBandwidth() const { +int OpusEncoder::getMaxBandwidth() const { assert(_encoder); int returnValue; opus_encoder_ctl(_encoder, OPUS_GET_MAX_BANDWIDTH(&returnValue)); return returnValue; } -void AthenaOpusEncoder::setMaxBandwidth(int maxBandwidth) { +void OpusEncoder::setMaxBandwidth(int maxBandwidth) { assert(_encoder); int errorCode = opus_encoder_ctl(_encoder, OPUS_SET_MAX_BANDWIDTH(maxBandwidth)); @@ -169,14 +169,14 @@ void AthenaOpusEncoder::setMaxBandwidth(int maxBandwidth) { } } -int AthenaOpusEncoder::getBandwidth() const { +int OpusEncoder::getBandwidth() const { assert(_encoder); int bandwidth; opus_encoder_ctl(_encoder, OPUS_GET_BANDWIDTH(&bandwidth)); return bandwidth; } -void AthenaOpusEncoder::setBandwidth(int bandwidth) { +void OpusEncoder::setBandwidth(int bandwidth) { assert(_encoder); int errorCode = opus_encoder_ctl(_encoder, OPUS_SET_BANDWIDTH(bandwidth)); @@ -185,14 +185,14 @@ void AthenaOpusEncoder::setBandwidth(int bandwidth) { } } -int AthenaOpusEncoder::getSignal() const { +int OpusEncoder::getSignal() const { assert(_encoder); int signal; opus_encoder_ctl(_encoder, OPUS_GET_SIGNAL(&signal)); return signal; } -void AthenaOpusEncoder::setSignal(int signal) { +void OpusEncoder::setSignal(int signal) { assert(_encoder); int errorCode = opus_encoder_ctl(_encoder, OPUS_SET_SIGNAL(signal)); @@ -201,14 +201,14 @@ void AthenaOpusEncoder::setSignal(int signal) { } } -int AthenaOpusEncoder::getApplication() const { +int OpusEncoder::getApplication() const { assert(_encoder); int applicationValue; opus_encoder_ctl(_encoder, OPUS_GET_APPLICATION(&applicationValue)); return applicationValue; } -void AthenaOpusEncoder::setApplication(int application) { +void OpusEncoder::setApplication(int application) { assert(_encoder); int errorCode = opus_encoder_ctl(_encoder, OPUS_SET_APPLICATION(application)); @@ -217,21 +217,21 @@ void AthenaOpusEncoder::setApplication(int application) { } } -int AthenaOpusEncoder::getLookahead() const { +int OpusEncoder::getLookahead() const { assert(_encoder); int lookAhead; opus_encoder_ctl(_encoder, OPUS_GET_LOOKAHEAD(&lookAhead)); return lookAhead; } -int AthenaOpusEncoder::getInbandFEC() const { +int OpusEncoder::getInbandFEC() const { assert(_encoder); int fec; opus_encoder_ctl(_encoder, OPUS_GET_INBAND_FEC(&fec)); return fec; } -void AthenaOpusEncoder::setInbandFEC(int inBandFEC) { +void OpusEncoder::setInbandFEC(int inBandFEC) { assert(_encoder); int errorCode = opus_encoder_ctl(_encoder, OPUS_SET_INBAND_FEC(inBandFEC)); @@ -240,14 +240,14 @@ void AthenaOpusEncoder::setInbandFEC(int inBandFEC) { } } -int AthenaOpusEncoder::getExpectedPacketLossPercentage() const { +int OpusEncoder::getExpectedPacketLossPercentage() const { assert(_encoder); int lossPercentage; opus_encoder_ctl(_encoder, OPUS_GET_PACKET_LOSS_PERC(&lossPercentage)); return lossPercentage; } -void AthenaOpusEncoder::setExpectedPacketLossPercentage(int percentage) { +void OpusEncoder::setExpectedPacketLossPercentage(int percentage) { assert(_encoder); int errorCode = opus_encoder_ctl(_encoder, OPUS_SET_PACKET_LOSS_PERC(percentage)); @@ -256,14 +256,14 @@ void AthenaOpusEncoder::setExpectedPacketLossPercentage(int percentage) { } } -int AthenaOpusEncoder::getDTX() const { +int OpusEncoder::getDTX() const { assert(_encoder); int dtx; opus_encoder_ctl(_encoder, OPUS_GET_DTX(&dtx)); return dtx; } -void AthenaOpusEncoder::setDTX(int dtx) { +void OpusEncoder::setDTX(int dtx) { assert(_encoder); int errorCode = opus_encoder_ctl(_encoder, OPUS_SET_DTX(dtx)); diff --git a/plugins/opusCodec/src/OpusEncoder.h b/libraries/opus-codec/src/OpusEncoder.h similarity index 82% rename from plugins/opusCodec/src/OpusEncoder.h rename to libraries/opus-codec/src/OpusEncoder.h index 10640bf409f..247a612ac0f 100644 --- a/plugins/opusCodec/src/OpusEncoder.h +++ b/libraries/opus-codec/src/OpusEncoder.h @@ -1,6 +1,6 @@ // -// OpusCodecManager.h -// plugins/opusCodec/src +// OpusEncoder.h +// libraries/opus-codec/src // // Copyright 2020 Dale Glass // @@ -8,17 +8,17 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -#ifndef OPUSENCODER_H -#define OPUSENCODER_H -#include -#include +#ifndef LIBRARIES_OPUS_CODEC_SRC_OPUSENCODER_H +#define LIBRARIES_OPUS_CODEC_SRC_OPUSENCODER_H +#include +#include -class AthenaOpusEncoder : public Encoder { +class OpusEncoder : public Encoder { public: - AthenaOpusEncoder(int sampleRate, int numChannels); - ~AthenaOpusEncoder() override; + OpusEncoder(int sampleRate, int numChannels); + ~OpusEncoder() override; virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override; @@ -74,5 +74,4 @@ class AthenaOpusEncoder : public Encoder { OpusEncoder* _encoder = nullptr; }; - -#endif // OPUSENCODER_H +#endif /* end of include guard */ diff --git a/libraries/pcm-codec/CMakeLists.txt b/libraries/pcm-codec/CMakeLists.txt new file mode 100644 index 00000000000..93ddf68feb0 --- /dev/null +++ b/libraries/pcm-codec/CMakeLists.txt @@ -0,0 +1,3 @@ +set(TARGET_NAME pcm-codec) +setup_hifi_library() +link_hifi_libraries(shared audio) diff --git a/libraries/pcm-codec/src/PCMCodec.cpp b/libraries/pcm-codec/src/PCMCodec.cpp new file mode 100644 index 00000000000..d64ceb41c15 --- /dev/null +++ b/libraries/pcm-codec/src/PCMCodec.cpp @@ -0,0 +1,51 @@ +// +// PCMCodec.cpp +// libraries/pcm-codec/src +// +// Created by Nshan G. on 3 July 2022. +// Copyright 2016 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 +// + +#include "PCMCodec.h" + +const char* PCMCodec::NAME { "pcm" }; + +Encoder* PCMCodec::createEncoder(int sampleRate, int numChannels) { + return this; +} + +Decoder* PCMCodec::createDecoder(int sampleRate, int numChannels) { + return this; +} + +void PCMCodec::releaseEncoder(Encoder* encoder) { + // do nothing +} + +void PCMCodec::releaseDecoder(Decoder* decoder) { + // do nothing +} + +const char* zLibCodec::NAME { "zlib" }; + +Encoder* zLibCodec::createEncoder(int sampleRate, int numChannels) { + return this; +} + +Decoder* zLibCodec::createDecoder(int sampleRate, int numChannels) { + return this; +} + +void zLibCodec::releaseEncoder(Encoder* encoder) { + // do nothing... it wasn't allocated +} + +void zLibCodec::releaseDecoder(Decoder* decoder) { + // do nothing... it wasn't allocated +} + diff --git a/libraries/pcm-codec/src/PCMCodec.h b/libraries/pcm-codec/src/PCMCodec.h new file mode 100644 index 00000000000..9dff8a2a2c5 --- /dev/null +++ b/libraries/pcm-codec/src/PCMCodec.h @@ -0,0 +1,80 @@ +// +// PCMCodec.h +// libraries/pcm-codec/src +// +// Created by Nshan G. on 3 July 2022. +// Copyright 2016 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_PCM_CODEC_SRC_PCMCODEC_H +#define LIBRARIES_PCM_CODEC_SRC_PCMCODEC_H + +#include +#include + +#include + +class PCMCodec : public Codec, public Encoder, public Decoder { + +public: + const QString getName() const override { return NAME; } + + virtual Encoder* createEncoder(int sampleRate, int numChannels) override; + virtual Decoder* createDecoder(int sampleRate, int numChannels) override; + virtual void releaseEncoder(Encoder* encoder) override; + virtual void releaseDecoder(Decoder* decoder) override; + + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { + encodedBuffer = decodedBuffer; + } + + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { + decodedBuffer = encodedBuffer; + } + + virtual void lostFrame(QByteArray& decodedBuffer) override { + decodedBuffer.resize(AudioConstants::NETWORK_FRAME_BYTES_STEREO); + memset(decodedBuffer.data(), 0, decodedBuffer.size()); + } + + static const char* getNameCString() { return NAME; } + +private: + static const char* NAME; +}; + +class zLibCodec : public Codec, public Encoder, public Decoder { + +public: + const QString getName() const override { return NAME; } + + virtual Encoder* createEncoder(int sampleRate, int numChannels) override; + virtual Decoder* createDecoder(int sampleRate, int numChannels) override; + virtual void releaseEncoder(Encoder* encoder) override; + virtual void releaseDecoder(Decoder* decoder) override; + + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { + encodedBuffer = qCompress(decodedBuffer); + } + + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { + decodedBuffer = qUncompress(encodedBuffer); + } + + virtual void lostFrame(QByteArray& decodedBuffer) override { + decodedBuffer.resize(AudioConstants::NETWORK_FRAME_BYTES_STEREO); + memset(decodedBuffer.data(), 0, decodedBuffer.size()); + } + + static const char* getNameCString() { return NAME; } + +private: + static const char* NAME; +}; + +#endif /* end of include guard */ diff --git a/libraries/plugins/src/plugins/CodecPlugin.h b/libraries/plugins/src/plugins/CodecPlugin.h index cb5b857be8e..52f20574158 100644 --- a/libraries/plugins/src/plugins/CodecPlugin.h +++ b/libraries/plugins/src/plugins/CodecPlugin.h @@ -10,26 +10,14 @@ // #pragma once +#include + #include "Plugin.h" -class Encoder { +class CodecPlugin : public Plugin, public Codec { public: - virtual ~Encoder() { } - virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) = 0; + const QString getName() const override { + return static_cast(this)->getName(); + } }; -class Decoder { -public: - virtual ~Decoder() { } - virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) = 0; - - virtual void lostFrame(QByteArray& decodedBuffer) = 0; -}; - -class CodecPlugin : public Plugin { -public: - virtual Encoder* createEncoder(int sampleRate, int numChannels) = 0; - virtual Decoder* createDecoder(int sampleRate, int numChannels) = 0; - virtual void releaseEncoder(Encoder* encoder) = 0; - virtual void releaseDecoder(Decoder* decoder) = 0; -}; diff --git a/libraries/plugins/src/plugins/Plugin.h b/libraries/plugins/src/plugins/Plugin.h index a0494ba6d8b..e6751706bbc 100644 --- a/libraries/plugins/src/plugins/Plugin.h +++ b/libraries/plugins/src/plugins/Plugin.h @@ -17,6 +17,7 @@ class Plugin : public QObject { Q_OBJECT public: + // NOTE: for audio plugins this is a unique identifier used in format negotiation network packets /// \return human-readable name virtual const QString getName() const = 0; diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index a55cac292f8..f8e7500e367 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -12,12 +12,111 @@ #include "AudioScriptingInterface.h" #include +#include #include +#include +#include #include "ScriptAudioInjector.h" #include "ScriptEngineLogging.h" +QScriptValue injectorOptionsToScriptValue(QScriptEngine* engine, const AudioInjectorOptions& injectorOptions) { + QScriptValue obj = engine->newObject(); + if (injectorOptions.positionSet) { + obj.setProperty("position", vec3ToScriptValue(engine, injectorOptions.position)); + } + obj.setProperty("volume", injectorOptions.volume); + obj.setProperty("loop", injectorOptions.loop); + obj.setProperty("orientation", quatToScriptValue(engine, injectorOptions.orientation)); + obj.setProperty("ignorePenumbra", injectorOptions.ignorePenumbra); + obj.setProperty("localOnly", injectorOptions.localOnly); + obj.setProperty("secondOffset", injectorOptions.secondOffset); + obj.setProperty("pitch", injectorOptions.pitch); + return obj; +} + +/*@jsdoc + * Configures where and how an audio injector plays its audio. + * @typedef {object} AudioInjector.AudioInjectorOptions + * @property {Vec3} position=Vec3.ZERO - The position in the domain to play the sound. + * @property {Quat} orientation=Quat.IDENTITY - The orientation in the domain to play the sound in. + * @property {number} volume=1.0 - Playback volume, between 0.0 and 1.0. + * @property {number} pitch=1.0 - Alter the pitch of the sound, within +/- 2 octaves. The value is the relative sample rate to + * resample the sound at, range 0.062516.0.
+ * A value of 0.0625 lowers the pitch by 2 octaves.
+ * A value of 1.0 means there is no change in pitch.
+ * A value of 16.0 raises the pitch by 2 octaves. + * @property {boolean} loop=false - If true, the sound is played repeatedly until playback is stopped. + * @property {number} secondOffset=0 - Starts playback from a specified time (seconds) within the sound file, ≥ + * 0. + * @property {boolean} localOnly=false - If true, the sound is played back locally on the client rather than to + * others via the audio mixer. + * @property {boolean} ignorePenumbra=false -

Deprecated: This property is deprecated and will be + * removed.

+ */ +void injectorOptionsFromScriptValue(const QScriptValue& object, AudioInjectorOptions& injectorOptions) { + if (!object.isObject()) { + qWarning() << "Audio injector options is not an object."; + return; + } + + if (injectorOptions.positionSet == false) { + qWarning() << "Audio injector options: injectorOptionsFromScriptValue() called more than once?"; + } + injectorOptions.positionSet = false; + + QScriptValueIterator it(object); + while (it.hasNext()) { + it.next(); + + if (it.name() == "position") { + vec3FromScriptValue(object.property("position"), injectorOptions.position); + injectorOptions.positionSet = true; + } else if (it.name() == "orientation") { + quatFromScriptValue(object.property("orientation"), injectorOptions.orientation); + } else if (it.name() == "volume") { + if (it.value().isNumber()) { + injectorOptions.volume = it.value().toNumber(); + } else { + qCWarning(audio) << "Audio injector options: volume is not a number"; + } + } else if (it.name() == "loop") { + if (it.value().isBool()) { + injectorOptions.loop = it.value().toBool(); + } else { + qCWarning(audio) << "Audio injector options: loop is not a boolean"; + } + } else if (it.name() == "ignorePenumbra") { + if (it.value().isBool()) { + injectorOptions.ignorePenumbra = it.value().toBool(); + } else { + qCWarning(audio) << "Audio injector options: ignorePenumbra is not a boolean"; + } + } else if (it.name() == "localOnly") { + if (it.value().isBool()) { + injectorOptions.localOnly = it.value().toBool(); + } else { + qCWarning(audio) << "Audio injector options: localOnly is not a boolean"; + } + } else if (it.name() == "secondOffset") { + if (it.value().isNumber()) { + injectorOptions.secondOffset = it.value().toNumber(); + } else { + qCWarning(audio) << "Audio injector options: secondOffset is not a number"; + } + } else if (it.name() == "pitch") { + if (it.value().isNumber()) { + injectorOptions.pitch = it.value().toNumber(); + } else { + qCWarning(audio) << "Audio injector options: pitch is not a number"; + } + } else { + qCWarning(audio) << "Unknown audio injector option:" << it.name(); + } + } +} + void registerAudioMetaTypes(QScriptEngine* engine) { qScriptRegisterMetaType(engine, injectorOptionsToScriptValue, injectorOptionsFromScriptValue); qScriptRegisterMetaType(engine, soundSharedPointerToScriptValue, soundSharedPointerFromScriptValue); @@ -29,7 +128,7 @@ void AudioScriptingInterface::setLocalAudioInterface(AbstractAudioInterface* aud disconnect(_localAudioInterface, &AbstractAudioInterface::isStereoInputChanged, this, &AudioScriptingInterface::isStereoInputChanged); } - + _localAudioInterface = audioInterface; if (_localAudioInterface) { diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index 6bfb7352ee8..b48f07f3529 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -15,6 +15,8 @@ #ifndef hifi_AudioScriptingInterface_h #define hifi_AudioScriptingInterface_h +#include + #include #include #include @@ -22,6 +24,12 @@ class ScriptAudioInjector; +Q_DECLARE_METATYPE(AudioInjectorOptions); + +QScriptValue injectorOptionsToScriptValue(QScriptEngine* engine, const AudioInjectorOptions& injectorOptions); +void injectorOptionsFromScriptValue(const QScriptValue& object, AudioInjectorOptions& injectorOptions); + + /// Provides the Audio scripting API class AudioScriptingInterface : public QObject, public Dependency { Q_OBJECT @@ -45,7 +53,7 @@ class AudioScriptingInterface : public QObject, public Dependency { } /*@jsdoc - * Adds avatars to the audio solo list. If the audio solo list is not empty, only audio from the avatars in the list is + * Adds avatars to the audio solo list. If the audio solo list is not empty, only audio from the avatars in the list is * played. * @function Audio.addToSoloList * @param {Uuid[]} ids - Avatar IDs to add to the solo list. @@ -54,25 +62,25 @@ class AudioScriptingInterface : public QObject, public Dependency { * // Find nearby avatars. * var RANGE = 100; // m * var nearbyAvatars = AvatarList.getAvatarsInRange(MyAvatar.position, RANGE); - * + * * // Remove own avatar from list. * var myAvatarIndex = nearbyAvatars.indexOf(MyAvatar.sessionUUID); * if (myAvatarIndex !== -1) { * nearbyAvatars.splice(myAvatarIndex, 1); * } - * + * * if (nearbyAvatars.length > 0) { * // Listen to only one of the nearby avatars. * var avatarName = AvatarList.getAvatar(nearbyAvatars[0]).displayName; * print("Listening only to " + avatarName); * Audio.addToSoloList([nearbyAvatars[0]]); - * + * * // Stop listening to only the one avatar after a short while. * Script.setTimeout(function () { * print("Finished listening only to " + avatarName); * Audio.resetSoloList(); * }, 10000); // 10s - * + * * } else { * print("No nearby avatars"); * } @@ -82,7 +90,7 @@ class AudioScriptingInterface : public QObject, public Dependency { } /*@jsdoc - * Removes avatars from the audio solo list. If the audio solo list is not empty, only audio from the avatars in the list + * Removes avatars from the audio solo list. If the audio solo list is not empty, only audio from the avatars in the list * is played. * @function Audio.removeFromSoloList * @param {Uuid[]} ids - Avatar IDs to remove from the solo list. @@ -100,44 +108,44 @@ class AudioScriptingInterface : public QObject, public Dependency { } /*@jsdoc - * Gets whether your microphone audio is echoed back to you from the server. When enabled, microphone audio is echoed only + * Gets whether your microphone audio is echoed back to you from the server. When enabled, microphone audio is echoed only * if you're unmuted or are using push-to-talk. * @function Audio.getServerEcho - * @returns {boolean} true if echoing microphone audio back to you from the server is enabled, + * @returns {boolean} true if echoing microphone audio back to you from the server is enabled, * false if it isn't. */ Q_INVOKABLE bool getServerEcho(); /*@jsdoc - * Sets whether your microphone audio is echoed back to you from the server. When enabled, microphone audio is echoed + * Sets whether your microphone audio is echoed back to you from the server. When enabled, microphone audio is echoed * only if you're unmuted or are using push-to-talk. * @function Audio.setServerEcho - * @param {boolean} serverEcho - true to enable echoing microphone back to you from the server, + * @param {boolean} serverEcho - true to enable echoing microphone back to you from the server, * false to disable. */ Q_INVOKABLE void setServerEcho(bool serverEcho); /*@jsdoc - * Toggles the echoing of microphone audio back to you from the server. When enabled, microphone audio is echoed only if + * Toggles the echoing of microphone audio back to you from the server. When enabled, microphone audio is echoed only if * you're unmuted or are using push-to-talk. * @function Audio.toggleServerEcho */ Q_INVOKABLE void toggleServerEcho(); /*@jsdoc - * Gets whether your microphone audio is echoed back to you by the client. When enabled, microphone audio is echoed + * Gets whether your microphone audio is echoed back to you by the client. When enabled, microphone audio is echoed * even if you're muted or not using push-to-talk. * @function Audio.getLocalEcho - * @returns {boolean} true if echoing microphone audio back to you from the client is enabled, + * @returns {boolean} true if echoing microphone audio back to you from the client is enabled, * false if it isn't. */ Q_INVOKABLE bool getLocalEcho(); /*@jsdoc - * Sets whether your microphone audio is echoed back to you by the client. When enabled, microphone audio is echoed + * Sets whether your microphone audio is echoed back to you by the client. When enabled, microphone audio is echoed * even if you're muted or not using push-to-talk. * @function Audio.setLocalEcho - * @parm {boolean} localEcho - true to enable echoing microphone audio back to you from the client, + * @parm {boolean} localEcho - true to enable echoing microphone audio back to you from the client, * false to disable. * @example Echo local audio for a few seconds. * Audio.setLocalEcho(true); @@ -148,7 +156,7 @@ class AudioScriptingInterface : public QObject, public Dependency { Q_INVOKABLE void setLocalEcho(bool localEcho); /*@jsdoc - * Toggles the echoing of microphone audio back to you by the client. When enabled, microphone audio is echoed even if + * Toggles the echoing of microphone audio back to you by the client. When enabled, microphone audio is echoed even if * you're muted or not using push-to-talk. * @function Audio.toggleLocalEcho */ @@ -161,32 +169,32 @@ class AudioScriptingInterface : public QObject, public Dependency { // these methods are protected to stop C++ callers from calling, but invokable from script /*@jsdoc - * Starts playing or "injecting" the content of an audio file. The sound is played globally (sent to the audio - * mixer) so that everyone hears it, unless the injectorOptions has localOnly set to - * true in which case only the client hears the sound played. No sound is played if sent to the audio mixer - * but the client is not connected to an audio mixer. The {@link AudioInjector} object returned by the function can be used + * Starts playing or "injecting" the content of an audio file. The sound is played globally (sent to the audio + * mixer) so that everyone hears it, unless the injectorOptions has localOnly set to + * true in which case only the client hears the sound played. No sound is played if sent to the audio mixer + * but the client is not connected to an audio mixer. The {@link AudioInjector} object returned by the function can be used * to control the playback and get information about its current state. * @function Audio.playSound - * @param {SoundObject} sound - The content of an audio file, loaded using {@link SoundCache.getSound}. See + * @param {SoundObject} sound - The content of an audio file, loaded using {@link SoundCache.getSound}. See * {@link SoundObject} for supported formats. - * @param {AudioInjector.AudioInjectorOptions} [injectorOptions={}] - Configures where and how the audio injector plays the + * @param {AudioInjector.AudioInjectorOptions} [injectorOptions={}] - Configures where and how the audio injector plays the * audio file. * @returns {AudioInjector} The audio injector that plays the audio file. * @example Play a sound. * var sound = SoundCache.getSound("https://cdn-1.vircadia.com/us-c-1/ken/samples/forest_ambiX.wav"); - * + * * function playSound() { * var injectorOptions = { * position: MyAvatar.position * }; * var injector = Audio.playSound(sound, injectorOptions); * } - * + * * function onSoundReady() { * sound.ready.disconnect(onSoundReady); * playSound(); * } - * + * * if (sound.downloaded) { * playSound(); * } else { @@ -196,18 +204,18 @@ class AudioScriptingInterface : public QObject, public Dependency { Q_INVOKABLE ScriptAudioInjector* playSound(SharedSoundPointer sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions()); /*@jsdoc - * Starts playing the content of an audio file locally (isn't sent to the audio mixer). This is the same as calling - * {@link Audio.playSound} with {@link AudioInjector.AudioInjectorOptions} localOnly set true and + * Starts playing the content of an audio file locally (isn't sent to the audio mixer). This is the same as calling + * {@link Audio.playSound} with {@link AudioInjector.AudioInjectorOptions} localOnly set true and * the specified position. * @function Audio.playSystemSound - * @param {SoundObject} sound - The content of an audio file, which is loaded using {@link SoundCache.getSound}. See + * @param {SoundObject} sound - The content of an audio file, which is loaded using {@link SoundCache.getSound}. See * {@link SoundObject} for supported formats. * @returns {AudioInjector} The audio injector that plays the audio file. */ Q_INVOKABLE ScriptAudioInjector* playSystemSound(SharedSoundPointer sound); /*@jsdoc - * Sets whether the audio input should be used in stereo. If the audio input doesn't support stereo then setting a value + * Sets whether the audio input should be used in stereo. If the audio input doesn't support stereo then setting a value * of true has no effect. * @function Audio.setStereoInput * @param {boolean} stereo - true if the audio input should be used in stereo, otherwise false. @@ -217,57 +225,57 @@ class AudioScriptingInterface : public QObject, public Dependency { /*@jsdoc * Gets whether the audio input is used in stereo. * @function Audio.isStereoInput - * @returns {boolean} true if the audio input is used in stereo, otherwise false. + * @returns {boolean} true if the audio input is used in stereo, otherwise false. */ Q_INVOKABLE bool isStereoInput(); signals: /*@jsdoc - * Triggered when the client is muted by the mixer because their loudness value for the noise background has reached the + * Triggered when the client is muted by the mixer because their loudness value for the noise background has reached the * threshold set for the domain (in the server settings). * @function Audio.mutedByMixer - * @returns {Signal} + * @returns {Signal} */ void mutedByMixer(); /*@jsdoc - * Triggered when the client is muted by the mixer because they're within a certain radius (50m) of someone who requested + * Triggered when the client is muted by the mixer because they're within a certain radius (50m) of someone who requested * the mute through Developer > Audio > Mute Environment. * @function Audio.environmentMuted - * @returns {Signal} + * @returns {Signal} */ void environmentMuted(); /*@jsdoc * Triggered when the client receives its first packet from the audio mixer. * @function Audio.receivedFirstPacket - * @returns {Signal} + * @returns {Signal} */ void receivedFirstPacket(); /*@jsdoc * Triggered when the client is disconnected from the audio mixer. * @function Audio.disconnected - * @returns {Signal} + * @returns {Signal} */ void disconnected(); /*@jsdoc - * Triggered when the noise gate is opened. The input audio signal is no longer blocked (fully attenuated) because it has - * risen above an adaptive threshold set just above the noise floor. Only occurs if Audio.noiseReduction is + * Triggered when the noise gate is opened. The input audio signal is no longer blocked (fully attenuated) because it has + * risen above an adaptive threshold set just above the noise floor. Only occurs if Audio.noiseReduction is * true. * @function Audio.noiseGateOpened - * @returns {Signal} + * @returns {Signal} */ void noiseGateOpened(); /*@jsdoc - * Triggered when the noise gate is closed. The input audio signal is blocked (fully attenuated) because it has fallen - * below an adaptive threshold set just above the noise floor. Only occurs if Audio.noiseReduction is + * Triggered when the noise gate is closed. The input audio signal is blocked (fully attenuated) because it has fallen + * below an adaptive threshold set just above the noise floor. Only occurs if Audio.noiseReduction is * true. * @function Audio.noiseGateClosed - * @returns {Signal} + * @returns {Signal} */ void noiseGateClosed(); @@ -275,7 +283,7 @@ class AudioScriptingInterface : public QObject, public Dependency { * Triggered when a frame of audio input is processed. * @function Audio.inputReceived * @param {Int16Array} inputSamples - The audio input processed. - * @returns {Signal} + * @returns {Signal} */ void inputReceived(const QByteArray& inputSamples); diff --git a/libraries/shared-gui/src/AABox.cpp b/libraries/shared-gui/src/AABox.cpp index 2040340a11c..ca438b4496c 100644 --- a/libraries/shared-gui/src/AABox.cpp +++ b/libraries/shared-gui/src/AABox.cpp @@ -20,102 +20,101 @@ const glm::vec3 AABox::INFINITY_VECTOR(std::numeric_limits::infinity()); AABox::AABox(const AACube& other) : - _corner(other.getCorner()), _scale(other.getScale(), other.getScale(), other.getScale()) { + _data{other.getCorner(), {other.getScale(), other.getScale(), other.getScale()}} { } AABox::AABox(const Extents& other) : - _corner(other.minimum), - _scale(other.maximum - other.minimum) { + _data{other.minimum, other.maximum - other.minimum} { } AABox::AABox(const glm::vec3& corner, float size) : - _corner(corner), _scale(size, size, size) { + _data{corner, {size, size, size}} { }; AABox::AABox(const glm::vec3& corner, const glm::vec3& dimensions) : - _corner(corner), _scale(dimensions) { + _data{corner, dimensions} { }; -AABox::AABox() : _corner(INFINITY_VECTOR), _scale(0.0f) { +AABox::AABox() : _data{INFINITY_VECTOR, Vectors::ZERO} { }; glm::vec3 AABox::calcCenter() const { - glm::vec3 center(_corner); - center += (_scale * 0.5f); + glm::vec3 center(_data.corner); + center += (_data.scale * 0.5f); return center; } glm::vec3 AABox::getVertex(BoxVertex vertex) const { switch (vertex) { case BOTTOM_LEFT_NEAR: - return _corner + glm::vec3(_scale.x, 0, 0); + return _data.corner + glm::vec3(_data.scale.x, 0, 0); case BOTTOM_RIGHT_NEAR: - return _corner; + return _data.corner; case TOP_RIGHT_NEAR: - return _corner + glm::vec3(0, _scale.y, 0); + return _data.corner + glm::vec3(0, _data.scale.y, 0); case TOP_LEFT_NEAR: - return _corner + glm::vec3(_scale.x, _scale.y, 0); + return _data.corner + glm::vec3(_data.scale.x, _data.scale.y, 0); case BOTTOM_LEFT_FAR: - return _corner + glm::vec3(_scale.x, 0, _scale.z); + return _data.corner + glm::vec3(_data.scale.x, 0, _data.scale.z); case BOTTOM_RIGHT_FAR: - return _corner + glm::vec3(0, 0, _scale.z); + return _data.corner + glm::vec3(0, 0, _data.scale.z); case TOP_RIGHT_FAR: - return _corner + glm::vec3(0, _scale.y, _scale.z); + return _data.corner + glm::vec3(0, _data.scale.y, _data.scale.z); default: //quiet windows warnings case TOP_LEFT_FAR: - return _corner + _scale; + return _data.corner + _data.scale; } } void AABox::setBox(const glm::vec3& corner, float scale) { - _corner = corner; - _scale = glm::vec3(scale, scale, scale); + _data.corner = corner; + _data.scale = glm::vec3(scale, scale, scale); } void AABox::setBox(const glm::vec3& corner, const glm::vec3& scale) { - _corner = corner; - _scale = scale; + _data.corner = corner; + _data.scale = scale; } glm::vec3 AABox::getFarthestVertex(const glm::vec3& normal) const { - glm::vec3 result = _corner; + glm::vec3 result = _data.corner; // This is a branchless version of: //if (normal.x > 0.0f) { - // result.x += _scale.x; + // result.x += _data.scale.x; //} //if (normal.y > 0.0f) { - // result.y += _scale.y; + // result.y += _data.scale.y; //} //if (normal.z > 0.0f) { - // result.z += _scale.z; + // result.z += _data.scale.z; //} float blend = (float)(normal.x > 0.0f); - result.x += blend * _scale.x + (1.0f - blend) * 0.0f; + result.x += blend * _data.scale.x + (1.0f - blend) * 0.0f; blend = (float)(normal.y > 0.0f); - result.y += blend * _scale.y + (1.0f - blend) * 0.0f; + result.y += blend * _data.scale.y + (1.0f - blend) * 0.0f; blend = (float)(normal.z > 0.0f); - result.z += blend * _scale.z + (1.0f - blend) * 0.0f; + result.z += blend * _data.scale.z + (1.0f - blend) * 0.0f; return result; } glm::vec3 AABox::getNearestVertex(const glm::vec3& normal) const { - glm::vec3 result = _corner; + glm::vec3 result = _data.corner; // This is a branchless version of: //if (normal.x < 0.0f) { - // result.x += _scale.x; + // result.x += _data.scale.x; //} //if (normal.y < 0.0f) { - // result.y += _scale.y; + // result.y += _data.scale.y; //} //if (normal.z < 0.0f) { - // result.z += _scale.z; + // result.z += _data.scale.z; //} float blend = (float)(normal.x < 0.0f); - result.x += blend * _scale.x + (1.0f - blend) * 0.0f; + result.x += blend * _data.scale.x + (1.0f - blend) * 0.0f; blend = (float)(normal.y < 0.0f); - result.y += blend * _scale.y + (1.0f - blend) * 0.0f; + result.y += blend * _data.scale.y + (1.0f - blend) * 0.0f; blend = (float)(normal.z < 0.0f); - result.z += blend * _scale.z + (1.0f - blend) * 0.0f; + result.z += blend * _data.scale.z + (1.0f - blend) * 0.0f; return result; } @@ -124,7 +123,7 @@ bool AABox::contains(const Triangle& triangle) const { } bool AABox::contains(const glm::vec3& point) const { - return aaBoxContains(point, _corner, _scale); + return aaBoxContains(point, _data.corner, _data.scale); } bool AABox::contains(const AABox& otherBox) const { @@ -138,13 +137,7 @@ bool AABox::contains(const AABox& otherBox) const { } bool AABox::touches(const AABox& otherBox) const { - glm::vec3 relativeCenter = _corner - otherBox._corner + ((_scale - otherBox._scale) * 0.5f); - - glm::vec3 totalHalfScale = (_scale + otherBox._scale) * 0.5f; - - return fabsf(relativeCenter.x) <= totalHalfScale.x && - fabsf(relativeCenter.y) <= totalHalfScale.y && - fabsf(relativeCenter.z) <= totalHalfScale.z; + return isTouching(_data, otherBox._data); } bool AABox::contains(const AACube& otherCube) const { @@ -158,9 +151,9 @@ bool AABox::contains(const AACube& otherCube) const { } bool AABox::touches(const AACube& otherCube) const { - glm::vec3 relativeCenter = _corner - otherCube.getCorner() + ((_scale - otherCube.getDimensions()) * 0.5f); + glm::vec3 relativeCenter = _data.corner - otherCube.getCorner() + ((_data.scale - otherCube.getDimensions()) * 0.5f); - glm::vec3 totalHalfScale = (_scale + otherCube.getDimensions()) * 0.5f; + glm::vec3 totalHalfScale = (_data.scale + otherCube.getDimensions()) * 0.5f; return fabsf(relativeCenter.x) <= totalHalfScale.x && fabsf(relativeCenter.y) <= totalHalfScale.y && @@ -173,9 +166,9 @@ static bool isWithinExpanded(float value, float corner, float size, float expans } bool AABox::expandedContains(const glm::vec3& point, float expansion) const { - return isWithinExpanded(point.x, _corner.x, _scale.x, expansion) && - isWithinExpanded(point.y, _corner.y, _scale.y, expansion) && - isWithinExpanded(point.z, _corner.z, _scale.z, expansion); + return isWithinExpanded(point.x, _data.corner.x, _data.scale.x, expansion) && + isWithinExpanded(point.y, _data.corner.y, _data.scale.y, expansion) && + isWithinExpanded(point.z, _data.corner.z, _data.scale.z, expansion); } bool AABox::expandedIntersectsSegment(const glm::vec3& start, const glm::vec3& end, float expansion) const { @@ -184,8 +177,8 @@ bool AABox::expandedIntersectsSegment(const glm::vec3& start, const glm::vec3& e return true; } // check each axis - glm::vec3 expandedCorner = _corner - glm::vec3(expansion, expansion, expansion); - glm::vec3 expandedSize = _scale + glm::vec3(expansion, expansion, expansion) * 2.0f; + glm::vec3 expandedCorner = _data.corner - glm::vec3(expansion, expansion, expansion); + glm::vec3 expandedSize = _data.scale + glm::vec3(expansion, expansion, expansion) * 2.0f; glm::vec3 direction = end - start; float axisDistance; return (findIntersection(start.x, direction.x, expandedCorner.x, expandedSize.x, axisDistance) && @@ -204,19 +197,19 @@ bool AABox::expandedIntersectsSegment(const glm::vec3& start, const glm::vec3& e bool AABox::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, const glm::vec3& invDirection, float& distance, BoxFace& face, glm::vec3& surfaceNormal) const { - return findRayAABoxIntersection(origin, direction, invDirection, _corner, _scale, distance, face, surfaceNormal); + return findRayAABoxIntersection(origin, direction, invDirection, _data.corner, _data.scale, distance, face, surfaceNormal); } bool AABox::findParabolaIntersection(const glm::vec3& origin, const glm::vec3& velocity, const glm::vec3& acceleration, float& parabolicDistance, BoxFace& face, glm::vec3& surfaceNormal) const { - return findParabolaAABoxIntersection(origin, velocity, acceleration, _corner, _scale, parabolicDistance, face, surfaceNormal); + return findParabolaAABoxIntersection(origin, velocity, acceleration, _data.corner, _data.scale, parabolicDistance, face, surfaceNormal); } bool AABox::rayHitsBoundingSphere(const glm::vec3& origin, const glm::vec3& direction) const { glm::vec3 localCenter = calcCenter() - origin; float distance = glm::dot(localCenter, direction); const float ONE_OVER_TWO_SQUARED = 0.25f; - float radiusSquared = ONE_OVER_TWO_SQUARED * glm::length2(_scale); + float radiusSquared = ONE_OVER_TWO_SQUARED * glm::length2(_data.scale); return (glm::length2(localCenter) < radiusSquared || (glm::abs(distance) > 0.0f && glm::distance2(distance * direction, localCenter) < radiusSquared)); } @@ -224,7 +217,7 @@ bool AABox::rayHitsBoundingSphere(const glm::vec3& origin, const glm::vec3& dire bool AABox::parabolaPlaneIntersectsBoundingSphere(const glm::vec3& origin, const glm::vec3& velocity, const glm::vec3& acceleration, const glm::vec3& normal) const { glm::vec3 localCenter = calcCenter() - origin; const float ONE_OVER_TWO_SQUARED = 0.25f; - float radiusSquared = ONE_OVER_TWO_SQUARED * glm::length2(_scale); + float radiusSquared = ONE_OVER_TWO_SQUARED * glm::length2(_data.scale); // origin is inside the sphere if (glm::length2(localCenter) < radiusSquared) { @@ -246,7 +239,7 @@ bool AABox::parabolaPlaneIntersectsBoundingSphere(const glm::vec3& origin, const bool AABox::touchesSphere(const glm::vec3& center, float radius) const { // Avro's algorithm from this paper: http://www.mrtc.mdh.se/projects/3Dgraphics/paperF.pdf - glm::vec3 e = glm::max(_corner - center, Vectors::ZERO) + glm::max(center - _corner - _scale, Vectors::ZERO); + glm::vec3 e = glm::max(_data.corner - center, Vectors::ZERO) + glm::max(center - _data.corner - _data.scale, Vectors::ZERO); return glm::length2(e) <= radius * radius; } @@ -304,29 +297,29 @@ bool AABox::findCapsulePenetration(const glm::vec3& start, const glm::vec3& end, glm::vec3 AABox::getClosestPointOnFace(const glm::vec3& point, BoxFace face) const { switch (face) { case MIN_X_FACE: - return glm::clamp(point, glm::vec3(_corner.x, _corner.y, _corner.z), - glm::vec3(_corner.x, _corner.y + _scale.y, _corner.z + _scale.z)); + return glm::clamp(point, glm::vec3(_data.corner.x, _data.corner.y, _data.corner.z), + glm::vec3(_data.corner.x, _data.corner.y + _data.scale.y, _data.corner.z + _data.scale.z)); case MAX_X_FACE: - return glm::clamp(point, glm::vec3(_corner.x + _scale.x, _corner.y, _corner.z), - glm::vec3(_corner.x + _scale.x, _corner.y + _scale.y, _corner.z + _scale.z)); + return glm::clamp(point, glm::vec3(_data.corner.x + _data.scale.x, _data.corner.y, _data.corner.z), + glm::vec3(_data.corner.x + _data.scale.x, _data.corner.y + _data.scale.y, _data.corner.z + _data.scale.z)); case MIN_Y_FACE: - return glm::clamp(point, glm::vec3(_corner.x, _corner.y, _corner.z), - glm::vec3(_corner.x + _scale.x, _corner.y, _corner.z + _scale.z)); + return glm::clamp(point, glm::vec3(_data.corner.x, _data.corner.y, _data.corner.z), + glm::vec3(_data.corner.x + _data.scale.x, _data.corner.y, _data.corner.z + _data.scale.z)); case MAX_Y_FACE: - return glm::clamp(point, glm::vec3(_corner.x, _corner.y + _scale.y, _corner.z), - glm::vec3(_corner.x + _scale.x, _corner.y + _scale.y, _corner.z + _scale.z)); + return glm::clamp(point, glm::vec3(_data.corner.x, _data.corner.y + _data.scale.y, _data.corner.z), + glm::vec3(_data.corner.x + _data.scale.x, _data.corner.y + _data.scale.y, _data.corner.z + _data.scale.z)); case MIN_Z_FACE: - return glm::clamp(point, glm::vec3(_corner.x, _corner.y, _corner.z), - glm::vec3(_corner.x + _scale.x, _corner.y + _scale.y, _corner.z)); + return glm::clamp(point, glm::vec3(_data.corner.x, _data.corner.y, _data.corner.z), + glm::vec3(_data.corner.x + _data.scale.x, _data.corner.y + _data.scale.y, _data.corner.z)); default: //quiet windows warnings case MAX_Z_FACE: - return glm::clamp(point, glm::vec3(_corner.x, _corner.y, _corner.z + _scale.z), - glm::vec3(_corner.x + _scale.x, _corner.y + _scale.y, _corner.z + _scale.z)); + return glm::clamp(point, glm::vec3(_data.corner.x, _data.corner.y, _data.corner.z + _data.scale.z), + glm::vec3(_data.corner.x + _data.scale.x, _data.corner.y + _data.scale.y, _data.corner.z + _data.scale.z)); } } @@ -376,7 +369,7 @@ glm::vec3 AABox::getClosestPointOnFace(const glm::vec4& origin, const glm::vec4& glm::vec4 thirdAxisMaxPlane = getPlane((BoxFace)(thirdAxis * 2 + 1)); glm::vec4 offset = glm::vec4(0.0f, 0.0f, 0.0f, - glm::dot(glm::vec3(secondAxisMaxPlane + thirdAxisMaxPlane), _scale) * 0.5f); + glm::dot(glm::vec3(secondAxisMaxPlane + thirdAxisMaxPlane), _data.scale) * 0.5f); glm::vec4 diagonals[] = { secondAxisMinPlane + thirdAxisMaxPlane + offset, secondAxisMaxPlane + thirdAxisMaxPlane + offset }; @@ -399,12 +392,12 @@ glm::vec3 AABox::getClosestPointOnFace(const glm::vec4& origin, const glm::vec4& bool AABox::touchesAAEllipsoid(const glm::vec3& center, const glm::vec3& radials) const { // handle case where ellipsoid's alix-aligned box doesn't touch this AABox - if (_corner.x - radials.x > center.x || - _corner.y - radials.y > center.y || - _corner.z - radials.z > center.z || - _corner.x + _scale.x + radials.x < center.x || - _corner.y + _scale.y + radials.y < center.y || - _corner.z + _scale.z + radials.z < center.z) { + if (_data.corner.x - radials.x > center.x || + _data.corner.y - radials.y > center.y || + _data.corner.z - radials.z > center.z || + _data.corner.x + _data.scale.x + radials.x < center.x || + _data.corner.y + _data.scale.y + radials.y < center.y || + _data.corner.z + _data.scale.z + radials.z < center.z) { return false; } @@ -431,13 +424,13 @@ bool AABox::touchesAAEllipsoid(const glm::vec3& center, const glm::vec3& radials glm::vec4 AABox::getPlane(BoxFace face) const { switch (face) { - case MIN_X_FACE: return glm::vec4(-1.0f, 0.0f, 0.0f, _corner.x); - case MAX_X_FACE: return glm::vec4(1.0f, 0.0f, 0.0f, -_corner.x - _scale.x); - case MIN_Y_FACE: return glm::vec4(0.0f, -1.0f, 0.0f, _corner.y); - case MAX_Y_FACE: return glm::vec4(0.0f, 1.0f, 0.0f, -_corner.y - _scale.y); - case MIN_Z_FACE: return glm::vec4(0.0f, 0.0f, -1.0f, _corner.z); + case MIN_X_FACE: return glm::vec4(-1.0f, 0.0f, 0.0f, _data.corner.x); + case MAX_X_FACE: return glm::vec4(1.0f, 0.0f, 0.0f, -_data.corner.x - _data.scale.x); + case MIN_Y_FACE: return glm::vec4(0.0f, -1.0f, 0.0f, _data.corner.y); + case MAX_Y_FACE: return glm::vec4(0.0f, 1.0f, 0.0f, -_data.corner.y - _data.scale.y); + case MIN_Z_FACE: return glm::vec4(0.0f, 0.0f, -1.0f, _data.corner.z); default: //quiet windows warnings - case MAX_Z_FACE: return glm::vec4(0.0f, 0.0f, 1.0f, -_corner.z - _scale.z); + case MAX_Z_FACE: return glm::vec4(0.0f, 0.0f, 1.0f, -_data.corner.z - _data.scale.z); } } @@ -454,7 +447,7 @@ BoxFace AABox::getOppositeFace(BoxFace face) { } AABox AABox::clamp(const glm::vec3& min, const glm::vec3& max) const { - glm::vec3 clampedCorner = glm::clamp(_corner, min, max); + glm::vec3 clampedCorner = glm::clamp(_data.corner, min, max); glm::vec3 clampedTopFarLeft = glm::clamp(calcTopFarLeft(), min, max); glm::vec3 clampedScale = clampedTopFarLeft - clampedCorner; @@ -462,7 +455,7 @@ AABox AABox::clamp(const glm::vec3& min, const glm::vec3& max) const { } AABox AABox::clamp(float min, float max) const { - glm::vec3 clampedCorner = glm::clamp(_corner, min, max); + glm::vec3 clampedCorner = glm::clamp(_data.corner, min, max); glm::vec3 clampedTopFarLeft = glm::clamp(calcTopFarLeft(), min, max); glm::vec3 clampedScale = clampedTopFarLeft - clampedCorner; @@ -470,33 +463,33 @@ AABox AABox::clamp(float min, float max) const { } void AABox::embiggen(float scale) { - _corner += scale * (-0.5f * _scale); - _scale *= scale; + _data.corner += scale * (-0.5f * _data.scale); + _data.scale *= scale; } void AABox::embiggen(const glm::vec3& scale) { - _corner += scale * (-0.5f * _scale); - _scale *= scale; + _data.corner += scale * (-0.5f * _data.scale); + _data.scale *= scale; } void AABox::setScaleStayCentered(const glm::vec3& scale) { - _corner -= 0.5f * (scale - _scale); - _scale = scale; + _data.corner -= 0.5f * (scale - _data.scale); + _data.scale = scale; } void AABox::scale(float scale) { - _corner *= scale; - _scale *= scale; + _data.corner *= scale; + _data.scale *= scale; } void AABox::scale(const glm::vec3& scale) { - _corner *= scale; - _scale *= scale; + _data.corner *= scale; + _data.scale *= scale; } void AABox::rotate(const glm::quat& rotation) { - auto minimum = _corner; - auto maximum = _corner + _scale; + auto minimum = _data.corner; + auto maximum = _data.corner + _data.scale; glm::vec3 bottomLeftNear(minimum.x, minimum.y, minimum.z); glm::vec3 bottomRightNear(maximum.x, minimum.y, minimum.z); @@ -534,8 +527,8 @@ void AABox::rotate(const glm::quat& rotation) { glm::max(topLeftFarRotated, topRightFarRotated))))))); - _corner = minimum; - _scale = maximum - minimum; + _data.corner = minimum; + _data.scale = maximum - minimum; } void AABox::transform(const Transform& transform) { @@ -547,8 +540,8 @@ void AABox::transform(const Transform& transform) { // Logic based on http://clb.demon.fi/MathGeoLib/nightly/docs/AABB.cpp_code.html#471 void AABox::transform(const glm::mat4& matrix) { // FIXME use simd operations - auto halfSize = _scale * 0.5f; - auto center = _corner + halfSize; + auto halfSize = _data.scale * 0.5f; + auto center = _data.corner + halfSize; halfSize = abs(halfSize); auto mm = glm::transpose(glm::mat3(matrix)); vec3 newDir = vec3( @@ -558,43 +551,43 @@ void AABox::transform(const glm::mat4& matrix) { ); auto newCenter = transformPoint(matrix, center); - _corner = newCenter - newDir; - _scale = newDir * 2.0f; + _data.corner = newCenter - newDir; + _data.scale = newDir * 2.0f; } AABox AABox::getOctreeChild(OctreeChild child) const { AABox result(*this); // self switch (child) { case topLeftNear: - result._corner.y += _scale.y / 2.0f; + result._data.corner.y += _data.scale.y / 2.0f; break; case topLeftFar: - result._corner.y += _scale.y / 2.0f; - result._corner.z += _scale.z / 2.0f; + result._data.corner.y += _data.scale.y / 2.0f; + result._data.corner.z += _data.scale.z / 2.0f; break; case topRightNear: - result._corner.y += _scale.y / 2.0f; - result._corner.x += _scale.x / 2.0f; + result._data.corner.y += _data.scale.y / 2.0f; + result._data.corner.x += _data.scale.x / 2.0f; break; case topRightFar: - result._corner.y += _scale.y / 2.0f; - result._corner.x += _scale.x / 2.0f; - result._corner.z += _scale.z / 2.0f; + result._data.corner.y += _data.scale.y / 2.0f; + result._data.corner.x += _data.scale.x / 2.0f; + result._data.corner.z += _data.scale.z / 2.0f; break; case bottomLeftNear: - // _corner = same as parent + // _data.corner = same as parent break; case bottomLeftFar: - result._corner.z += _scale.z / 2.0f; + result._data.corner.z += _data.scale.z / 2.0f; break; case bottomRightNear: - result._corner.x += _scale.x / 2.0f; + result._data.corner.x += _data.scale.x / 2.0f; break; case bottomRightFar: - result._corner.x += _scale.x / 2.0f; - result._corner.z += _scale.z / 2.0f; + result._data.corner.x += _data.scale.x / 2.0f; + result._data.corner.z += _data.scale.z / 2.0f; break; } - result._scale /= 2.0f; // everything is half the scale + result._data.scale /= 2.0f; // everything is half the scale return result; } diff --git a/libraries/shared-gui/src/AABox.h b/libraries/shared-gui/src/AABox.h index c5fb85fdd11..69fe4a01a9c 100644 --- a/libraries/shared-gui/src/AABox.h +++ b/libraries/shared-gui/src/AABox.h @@ -19,6 +19,8 @@ #include +#include + #include "BoxBase.h" #include "GeometryUtil.h" #include "StreamUtils.h" @@ -43,20 +45,20 @@ class AABox { glm::vec3 getFarthestVertex(const glm::vec3& normal) const; // return vertex most parallel to normal glm::vec3 getNearestVertex(const glm::vec3& normal) const; // return vertex most anti-parallel to normal - const glm::vec3& getCorner() const { return _corner; } - const glm::vec3& getScale() const { return _scale; } - const glm::vec3& getDimensions() const { return _scale; } - float getLargestDimension() const { return glm::max(_scale.x, glm::max(_scale.y, _scale.z)); } + const glm::vec3& getCorner() const { return _data.corner; } + const glm::vec3& getScale() const { return _data.scale; } + const glm::vec3& getDimensions() const { return _data.scale; } + float getLargestDimension() const { return glm::max(_data.scale.x, glm::max(_data.scale.y, _data.scale.z)); } glm::vec3 calcCenter() const; - glm::vec3 calcTopFarLeft() const { return _corner + _scale; } + glm::vec3 calcTopFarLeft() const { return _data.corner + _data.scale; } - const glm::vec3& getMinimum() const { return _corner; } - glm::vec3 getMaximum() const { return _corner + _scale; } + const glm::vec3& getMinimum() const { return _data.corner; } + glm::vec3 getMaximum() const { return _data.corner + _data.scale; } glm::vec3 getVertex(BoxVertex vertex) const; - const glm::vec3& getMinimumPoint() const { return _corner; } + const glm::vec3& getMinimumPoint() const { return _data.corner; } glm::vec3 getMaximumPoint() const { return calcTopFarLeft(); } bool contains(const Triangle& triangle) const; @@ -80,31 +82,31 @@ class AABox { bool findSpherePenetration(const glm::vec3& center, float radius, glm::vec3& penetration) const; bool findCapsulePenetration(const glm::vec3& start, const glm::vec3& end, float radius, glm::vec3& penetration) const; - bool isNull() const { return _scale == glm::vec3(0.0f, 0.0f, 0.0f); } + bool isNull() const { return _data.scale == glm::vec3(0.0f, 0.0f, 0.0f); } AABox clamp(const glm::vec3& min, const glm::vec3& max) const; AABox clamp(float min, float max) const; inline AABox& operator+=(const glm::vec3& point) { bool valid = !isInvalid(); - glm::vec3 maximum = glm::max(_corner + _scale, point); - _corner = glm::min(_corner, point); + glm::vec3 maximum = glm::max(_data.corner + _data.scale, point); + _data.corner = glm::min(_data.corner, point); if (valid) { - _scale = maximum - _corner; + _data.scale = maximum - _data.corner; } return (*this); } inline AABox& operator+=(const AABox& box) { if (!box.isInvalid()) { - (*this) += box._corner; + (*this) += box._data.corner; (*this) += box.calcTopFarLeft(); } return (*this); } // Translate the AABox just moving the corner - void translate(const glm::vec3& translation) { _corner += translation; } + void translate(const glm::vec3& translation) { _data.corner += translation; } // Rotate the AABox around its frame origin // meaning rotating the corners of the AABox around the point {0,0,0} and reevaluating the min max @@ -129,9 +131,9 @@ class AABox { static const glm::vec3 INFINITY_VECTOR; - bool isInvalid() const { return _corner.x == std::numeric_limits::infinity(); } + bool isInvalid() const { return _data.corner.x == std::numeric_limits::infinity(); } - void clear() { _corner = INFINITY_VECTOR; _scale = glm::vec3(0.0f); } + void clear() { _data.corner = INFINITY_VECTOR; _data.scale = glm::vec3(0.0f); } typedef enum { topLeftNear, @@ -157,8 +159,7 @@ class AABox { void checkPossibleParabolicIntersection(float t, int i, float& minDistance, const glm::vec3& origin, const glm::vec3& velocity, const glm::vec3& acceleration, bool& hit) const; - glm::vec3 _corner; - glm::vec3 _scale; + AABoxData _data; }; inline bool operator==(const AABox& a, const AABox& b) { diff --git a/libraries/shared-gui/src/shared/ConicalViewFrustum.cpp b/libraries/shared-gui/src/shared/ConicalViewFrustum.cpp index 49d77b43189..4ae6c396438 100644 --- a/libraries/shared-gui/src/shared/ConicalViewFrustum.cpp +++ b/libraries/shared-gui/src/shared/ConicalViewFrustum.cpp @@ -14,49 +14,30 @@ #include #include "../ViewFrustum.h" -#include + void ConicalViewFrustum::set(const ViewFrustum& viewFrustum) { - // The ConicalViewFrustum has two parts: a central sphere (same as ViewFrustum) and a circular cone that bounds the frustum part. - // Why? Because approximate intersection tests are much faster to compute for a cone than for a frustum. - _position = viewFrustum.getPosition(); - _radius = viewFrustum.getCenterRadius(); - _farClip = viewFrustum.getFarClip(); - - auto topLeft = viewFrustum.getNearTopLeft() - _position; - auto topRight = viewFrustum.getNearTopRight() - _position; - auto bottomLeft = viewFrustum.getNearBottomLeft() - _position; - auto bottomRight = viewFrustum.getNearBottomRight() - _position; - auto centerAxis = 0.25f * (topLeft + topRight + bottomLeft + bottomRight); // Take the average - - _direction = glm::normalize(centerAxis); - _angle = std::max(std::max(angleBetween(_direction, topLeft), - angleBetween(_direction, topRight)), - std::max(angleBetween(_direction, bottomLeft), - angleBetween(_direction, bottomRight))); + _data.set(viewFrustum.getPosition(), viewFrustum.getCenterRadius(), viewFrustum.getFarClip(), { + viewFrustum.getNearTopLeft(), + viewFrustum.getNearTopRight(), + viewFrustum.getNearBottomLeft(), + viewFrustum.getNearBottomRight(), + }); } void ConicalViewFrustum::calculate() { // Pre-compute cos and sin for faster checks - _cosAngle = cosf(_angle); + _cosAngle = cosf(_data.angle); _sinAngle = sqrtf(1.0f - _cosAngle * _cosAngle); } bool ConicalViewFrustum::isVerySimilar(const ConicalViewFrustum& other) const { - const float MIN_POSITION_SLOP_SQUARED = 25.0f; // 5 meters squared - const float MIN_ANGLE_BETWEEN = 0.174533f; // radian angle between 2 vectors 10 degrees apart - const float MIN_RELATIVE_ERROR = 0.01f; // 1% - - return glm::distance2(_position, other._position) < MIN_POSITION_SLOP_SQUARED && - angleBetween(_direction, other._direction) < MIN_ANGLE_BETWEEN && - closeEnough(_angle, other._angle, MIN_RELATIVE_ERROR) && - closeEnough(_farClip, other._farClip, MIN_RELATIVE_ERROR) && - closeEnough(_radius, other._radius, MIN_RELATIVE_ERROR); + return _data.isVerySimilar(other._data); } bool ConicalViewFrustum::intersects(const AACube& cube) const { auto radius = 0.5f * SQRT_THREE * cube.getScale(); // radius of bounding sphere - auto position = cube.calcCenter() - _position; // position of bounding sphere in view-frame + auto position = cube.calcCenter() - _data.position; // position of bounding sphere in view-frame float distance = glm::length(position); return intersects(position, distance, radius); @@ -64,7 +45,7 @@ bool ConicalViewFrustum::intersects(const AACube& cube) const { bool ConicalViewFrustum::intersects(const AABox& box) const { auto radius = 0.5f * glm::length(box.getScale()); // radius of bounding sphere - auto position = box.calcCenter() - _position; // position of bounding sphere in view-frame + auto position = box.calcCenter() - _data.position; // position of bounding sphere in view-frame float distance = glm::length(position); return intersects(position, distance, radius); @@ -72,7 +53,7 @@ bool ConicalViewFrustum::intersects(const AABox& box) const { float ConicalViewFrustum::getAngularSize(const AACube& cube) const { auto radius = 0.5f * SQRT_THREE * cube.getScale(); // radius of bounding sphere - auto position = cube.calcCenter() - _position; // position of bounding sphere in view-frame + auto position = cube.calcCenter() - _data.position; // position of bounding sphere in view-frame float distance = glm::length(position); return getAngularSize(distance, radius); @@ -80,7 +61,7 @@ float ConicalViewFrustum::getAngularSize(const AACube& cube) const { float ConicalViewFrustum::getAngularSize(const AABox& box) const { auto radius = 0.5f * glm::length(box.getScale()); // radius of bounding sphere - auto position = box.calcCenter() - _position; // position of bounding sphere in view-frame + auto position = box.calcCenter() - _data.position; // position of bounding sphere in view-frame float distance = glm::length(position); return getAngularSize(distance, radius); @@ -88,23 +69,23 @@ float ConicalViewFrustum::getAngularSize(const AABox& box) const { bool ConicalViewFrustum::intersects(const glm::vec3& relativePosition, float distance, float radius) const { - if (distance < _radius + radius) { + if (distance < _data.radius + radius) { // Inside keyhole radius return true; } - if (distance > _farClip + radius) { + if (distance > _data.farClip + radius) { // Past far clip return false; } - // We check the angle between the center of the cube and the _direction of the view. + // We check the angle between the center of the cube and the direction of the view. // If it is less than the sum of the half-angle from center of cone to outer edge plus // the half apparent angle of the bounding sphere then it is in view. // // The math here is left as an exercise for the reader with the following hints: // (1) We actually check the dot product of the cube's local position rather than the angle and // (2) we take advantage of this trig identity: cos(A+B) = cos(A)*cos(B) - sin(A)*sin(B) - return glm::dot(relativePosition, _direction) > + return glm::dot(relativePosition, _data.direction) > sqrtf(distance * distance - radius * radius) * _cosAngle - radius * _sinAngle; } @@ -115,39 +96,17 @@ float ConicalViewFrustum::getAngularSize(float distance, float radius) const { } int ConicalViewFrustum::serialize(unsigned char* destinationBuffer) const { - const unsigned char* startPosition = destinationBuffer; - - memcpy(destinationBuffer, &_position, sizeof(_position)); - destinationBuffer += sizeof(_position); - memcpy(destinationBuffer, &_direction, sizeof(_direction)); - destinationBuffer += sizeof(_direction); - destinationBuffer += packFloatAngleToTwoByte(destinationBuffer, _angle); - destinationBuffer += packClipValueToTwoByte(destinationBuffer, _farClip); - memcpy(destinationBuffer, &_radius, sizeof(_radius)); - destinationBuffer += sizeof(_radius); - - return destinationBuffer - startPosition; + return _data.serialize(destinationBuffer); } int ConicalViewFrustum::deserialize(const unsigned char* sourceBuffer) { - const unsigned char* startPosition = sourceBuffer; - - memcpy(glm::value_ptr(_position), sourceBuffer, sizeof(_position)); - sourceBuffer += sizeof(_position); - memcpy(glm::value_ptr(_direction), sourceBuffer, sizeof(_direction)); - sourceBuffer += sizeof(_direction); - sourceBuffer += unpackFloatAngleFromTwoByte((uint16_t*)sourceBuffer, &_angle); - sourceBuffer += unpackClipValueFromTwoByte(sourceBuffer, _farClip); - memcpy(&_radius, sourceBuffer, sizeof(_radius)); - sourceBuffer += sizeof(_radius); - + auto size = _data.deserialize(sourceBuffer); calculate(); - - return sourceBuffer - startPosition; + return size; } void ConicalViewFrustum::setPositionAndSimpleRadius(const glm::vec3& position, float radius) { - _position = position; - _radius = radius; - _farClip = radius / 2.0f; + _data.position = position; + _data.radius = radius; + _data.farClip = radius / 2.0f; } diff --git a/libraries/shared-gui/src/shared/ConicalViewFrustum.h b/libraries/shared-gui/src/shared/ConicalViewFrustum.h index d06c724fb74..9bf6f67742b 100644 --- a/libraries/shared-gui/src/shared/ConicalViewFrustum.h +++ b/libraries/shared-gui/src/shared/ConicalViewFrustum.h @@ -16,15 +16,14 @@ #include +#include + class AACube; class AABox; class ViewFrustum; using ViewFrustums = std::vector; const float SQRT_TWO_OVER_TWO = 0.7071067811865f; -const float DEFAULT_VIEW_ANGLE = 1.0f; -const float DEFAULT_VIEW_RADIUS = 10.0f; -const float DEFAULT_VIEW_FAR_CLIP = 100.0f; // ConicalViewFrustum is an approximation of a ViewFrustum for fast calculation of sort priority. class ConicalViewFrustum { @@ -35,11 +34,11 @@ class ConicalViewFrustum { void set(const ViewFrustum& viewFrustum); void calculate(); - const glm::vec3& getPosition() const { return _position; } - const glm::vec3& getDirection() const { return _direction; } - float getAngle() const { return _angle; } - float getRadius() const { return _radius; } - float getFarClip() const { return _farClip; } + const glm::vec3& getPosition() const { return _data.position; } + const glm::vec3& getDirection() const { return _data.direction; } + float getAngle() const { return _data.angle; } + float getRadius() const { return _data.radius; } + float getFarClip() const { return _data.farClip; } bool isVerySimilar(const ConicalViewFrustum& other) const; @@ -58,11 +57,7 @@ class ConicalViewFrustum { void setPositionAndSimpleRadius(const glm::vec3& position, float radius); private: - glm::vec3 _position { 0.0f, 0.0f, 0.0f }; - glm::vec3 _direction { 0.0f, 0.0f, 1.0f }; - float _angle { DEFAULT_VIEW_ANGLE }; - float _radius { DEFAULT_VIEW_RADIUS }; - float _farClip { DEFAULT_VIEW_FAR_CLIP }; + ConicalViewFrustumData _data {}; float _sinAngle { SQRT_TWO_OVER_TWO }; float _cosAngle { SQRT_TWO_OVER_TWO }; diff --git a/libraries/shared/src/AABoxData.h b/libraries/shared/src/AABoxData.h new file mode 100644 index 00000000000..8f728f70cbf --- /dev/null +++ b/libraries/shared/src/AABoxData.h @@ -0,0 +1,34 @@ +// +// AABoxData.h +// libraries/shared/src +// +// Created by Nshan G. on 3 July 2022 +// 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_SHARED_SRC_AABOXDARA_H +#define LIBRARIES_SHARED_SRC_AABOXDARA_H + +#include + +struct AABoxData { + glm::vec3 corner; + glm::vec3 scale; +}; + +inline bool isTouching(AABoxData one, AABoxData other) { + glm::vec3 relativeCenter = one.corner - other.corner + ((one.scale - other.scale) * 0.5f); + + glm::vec3 totalHalfScale = (one.scale + other.scale) * 0.5f; + + return fabsf(relativeCenter.x) <= totalHalfScale.x && + fabsf(relativeCenter.y) <= totalHalfScale.y && + fabsf(relativeCenter.z) <= totalHalfScale.z; +} + +#endif /* end of include guard */ diff --git a/libraries/shared/src/BitVectorHelpers.h b/libraries/shared/src/BitVectorHelpers.h index a619fe39996..f14b81ede85 100644 --- a/libraries/shared/src/BitVectorHelpers.h +++ b/libraries/shared/src/BitVectorHelpers.h @@ -14,7 +14,7 @@ #include "NumericalConstants.h" -int calcBitVectorSize(int numBits) { +inline int calcBitVectorSize(int numBits) { return ((numBits - 1) >> 3) + 1; } diff --git a/libraries/shared/src/Codec.h b/libraries/shared/src/Codec.h new file mode 100644 index 00000000000..6ffb6f2fef4 --- /dev/null +++ b/libraries/shared/src/Codec.h @@ -0,0 +1,41 @@ +// +// Codec.h +// libraries/shared/src +// +// Created by Nshan G. 17 June 2022 +// Copyright 2016 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 VIRCADIA_LIBRARIES_SHARED_SRC_CODEC_H +#define VIRCADIA_LIBRARIES_SHARED_SRC_CODEC_H + +#include +#include + +class Encoder { +public: + virtual ~Encoder() { } + virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) = 0; +}; + +class Decoder { +public: + virtual ~Decoder() { } + virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) = 0; + + virtual void lostFrame(QByteArray& decodedBuffer) = 0; +}; + +class Codec { +public: + virtual const QString getName() const = 0; + virtual Encoder* createEncoder(int sampleRate, int numChannels) = 0; + virtual Decoder* createDecoder(int sampleRate, int numChannels) = 0; + virtual void releaseEncoder(Encoder* encoder) = 0; + virtual void releaseDecoder(Decoder* decoder) = 0; +}; + +#endif /* end of include guard */ diff --git a/libraries/shared/src/Grab.cpp b/libraries/shared/src/Grab.cpp index 195bc61f66c..0f1dd8dc770 100644 --- a/libraries/shared/src/Grab.cpp +++ b/libraries/shared/src/Grab.cpp @@ -1,6 +1,6 @@ // // Grab.cpp -// libraries/avatars/src +// libraries/shared/src // // Created by Seth Alves on 2018-9-1. // Copyright 2018 High Fidelity, Inc. diff --git a/libraries/shared/src/Grab.h b/libraries/shared/src/Grab.h index f16a80befae..deee6e3e7b3 100644 --- a/libraries/shared/src/Grab.h +++ b/libraries/shared/src/Grab.h @@ -1,6 +1,6 @@ // // Grab.h -// libraries/avatars/src +// libraries/shared/src // // Created by Seth Alves on 2018-9-1. // Copyright 2018 High Fidelity, Inc. diff --git a/libraries/shared/src/shared/ConicalViewFrustumData.cpp b/libraries/shared/src/shared/ConicalViewFrustumData.cpp new file mode 100644 index 00000000000..676682f8a42 --- /dev/null +++ b/libraries/shared/src/shared/ConicalViewFrustumData.cpp @@ -0,0 +1,77 @@ +// +// ConicalViewFrustumData.cpp +// libraries/shared/src/shared +// +// Created by Nshan G. on 31 May 2022. +// Copyright 2014 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 +// + +#include "ConicalViewFrustumData.h" + +#include + +int ConicalViewFrustumData::serialize(unsigned char* destinationBuffer) const { + const unsigned char* startPosition = destinationBuffer; + + memcpy(destinationBuffer, &position, sizeof(position)); + destinationBuffer += sizeof(position); + memcpy(destinationBuffer, &direction, sizeof(direction)); + destinationBuffer += sizeof(direction); + destinationBuffer += packFloatAngleToTwoByte(destinationBuffer, angle); + destinationBuffer += packClipValueToTwoByte(destinationBuffer, farClip); + memcpy(destinationBuffer, &radius, sizeof(radius)); + destinationBuffer += sizeof(radius); + + return destinationBuffer - startPosition; +} + +int ConicalViewFrustumData::deserialize(const unsigned char* sourceBuffer) { + const unsigned char* startPosition = sourceBuffer; + + memcpy(glm::value_ptr(position), sourceBuffer, sizeof(position)); + sourceBuffer += sizeof(position); + memcpy(glm::value_ptr(direction), sourceBuffer, sizeof(direction)); + sourceBuffer += sizeof(direction); + sourceBuffer += unpackFloatAngleFromTwoByte((uint16_t*)sourceBuffer, &angle); + sourceBuffer += unpackClipValueFromTwoByte(sourceBuffer, farClip); + memcpy(&radius, sourceBuffer, sizeof(radius)); + sourceBuffer += sizeof(radius); + + return sourceBuffer - startPosition; +} + +bool ConicalViewFrustumData::isVerySimilar(const ConicalViewFrustumData& other) const { + const float MIN_POSITION_SLOP_SQUARED = 25.0f; // 5 meters squared + const float MIN_ANGLE_BETWEEN = 0.174533f; // radian angle between 2 vectors 10 degrees apart + const float MIN_RELATIVE_ERROR = 0.01f; // 1% + + return glm::distance2(position, other.position) < MIN_POSITION_SLOP_SQUARED && + angleBetween(direction, other.direction) < MIN_ANGLE_BETWEEN && + closeEnough(angle, other.angle, MIN_RELATIVE_ERROR) && + closeEnough(farClip, other.farClip, MIN_RELATIVE_ERROR) && + closeEnough(radius, other.radius, MIN_RELATIVE_ERROR); +} + +void ConicalViewFrustumData::set(glm::vec3 position, float radius, float farClip, std::array nearCorners) { + this->position = position; + this->radius = radius; + this->farClip = farClip; + + auto topLeft = nearCorners[0] - position; + auto topRight = nearCorners[1] - position; + auto bottomLeft = nearCorners[2] - position; + auto bottomRight = nearCorners[3] - position; + auto centerAxis = 0.25f * (topLeft + topRight + bottomLeft + bottomRight); // Take the average + + direction = glm::normalize(centerAxis); + angle = std::max(std::max(angleBetween(direction, topLeft), + angleBetween(direction, topRight)), + std::max(angleBetween(direction, bottomLeft), + angleBetween(direction, bottomRight))); +} + diff --git a/libraries/shared/src/shared/ConicalViewFrustumData.h b/libraries/shared/src/shared/ConicalViewFrustumData.h new file mode 100644 index 00000000000..b86db9cbd7d --- /dev/null +++ b/libraries/shared/src/shared/ConicalViewFrustumData.h @@ -0,0 +1,39 @@ +// +// ConicalViewFrustumData.h +// libraries/shared/src/shared +// +// Created by Nshan G. on 31 May 2022. +// Copyright 2017 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_SHARED_SRC_SHARED_CONICALVIEWFRUSTUMUTILS_H +#define LIBRARIES_SHARED_SRC_SHARED_CONICALVIEWFRUSTUMUTILS_H + +#include "../GLMHelpers.h" + +const float DEFAULT_VIEW_ANGLE = 1.0f; +const float DEFAULT_VIEW_RADIUS = 10.0f; +const float DEFAULT_VIEW_FAR_CLIP = 100.0f; + +struct ConicalViewFrustumData { + // The ConicalViewFrustum has two parts: a central sphere (same as ViewFrustum) and a circular cone that bounds the frustum part. + // Why? Because approximate intersection tests are much faster to compute for a cone than for a frustum. + glm::vec3 position { 0.0f, 0.0f, 0.0f }; + glm::vec3 direction { 0.0f, 0.0f, 1.0f }; + float angle { DEFAULT_VIEW_ANGLE }; + float radius { DEFAULT_VIEW_RADIUS }; + float farClip { DEFAULT_VIEW_FAR_CLIP }; + + int serialize(unsigned char* destinationBuffer) const; + int deserialize(const unsigned char* sourceBuffer); + bool isVerySimilar(const ConicalViewFrustumData& other) const; + + void set(glm::vec3 position, float radius, float farClip, std::array nearCorners); +}; + +#endif /* end of include guard */ diff --git a/libraries/shared/src/shared/WebRTC.h b/libraries/shared/src/shared/WebRTC.h index 9f635b11a1d..2e94e05c348 100644 --- a/libraries/shared/src/shared/WebRTC.h +++ b/libraries/shared/src/shared/WebRTC.h @@ -16,30 +16,22 @@ #include #endif -// WEBRTC_AUDIO: WebRTC audio features, e.g., echo canceling. +// WEBRTC_AUDIO: WebRTC audio features, e.g., echo canceling (defined in cmake). // WEBRTC_DATA_CHANNELS: WebRTC client-server connections in parallel with UDP (defined in cmake). #if defined(Q_OS_MAC) -# define WEBRTC_AUDIO 1 # define WEBRTC_POSIX 1 # define WEBRTC_LEGACY 1 #elif defined(Q_OS_WIN) -# define WEBRTC_AUDIO 1 # define WEBRTC_WIN 1 # define NOMINMAX 1 # define WIN32_LEAN_AND_MEAN 1 #elif defined(Q_OS_ANDROID) -// I don't yet have a working libwebrtc for android -// # define WEBRTC_AUDIO 1 // # define WEBRTC_POSIX 1 // # define WEBRTC_LEGACY 1 #elif defined(Q_OS_LINUX) && defined(Q_PROCESSOR_X86_64) -# define WEBRTC_AUDIO 1 # define WEBRTC_POSIX 1 #elif defined(Q_OS_LINUX) && defined(Q_PROCESSOR_ARM) -// WebRTC is basically impossible to build on aarch64 Linux. -// I am looking at https://gitlab.freedesktop.org/pulseaudio/webrtc-audio-processing for an alternative. -// # define WEBRTC_AUDIO 1 // # define WEBRTC_POSIX 1 // # define WEBRTC_LEGACY 1 #endif diff --git a/libraries/vircadia-client/BUILD_ANDROID.md b/libraries/vircadia-client/BUILD_ANDROID.md index ab55886536a..743997fb701 100644 --- a/libraries/vircadia-client/BUILD_ANDROID.md +++ b/libraries/vircadia-client/BUILD_ANDROID.md @@ -5,7 +5,7 @@ These are various hacks and workarounds I used to build the client library for A ### Pre requisites #### Android SDK and NDK, OpenJDK -Install OpenJDK 8, should be available on most pretty much all linux distros. +Install OpenJDK 8, should be available on most linux distros. ``` sudo apt install openjdk-8-jdk ``` @@ -35,7 +35,7 @@ Build and install: make -j4 make install -j4 ``` -Qt is built for all architectures by default so you don't need to worry about that here. +Qt is built for all architectures by default, so you don't need to worry about that here. #### TBB Clone oneTBB repo from github (mater is on 9d2a3477ce276d437bf34b1582781e5b11f9b37a at the time of writing this) @@ -82,6 +82,20 @@ make -j4 make install -j4 ``` +#### Opus + +Download [opus-1.3.1.tar.gz source archive](https://archive.mozilla.org/pub/opus/opus-1.3.1.tar.gz).
+Export the path to android NDK. +``` +export NDk=/home/namark/dev/android/ndk/21.3.6528147 +``` +Create a build directory for specific architecture +``` +mkdir build_armeabi-v7a +cd build_armeabi-v7a +``` +Configure build and install using [android/opus_configure_android.sh](android/opus_configure_android.sh), uncommenting the desired target architecture. The library will be installed in the build directory as under `opus_package`.
+ ### Project setup wreckage Make sure to navigate to the vircadia project root.
Apply wreckage.patch using git @@ -89,8 +103,9 @@ Apply wreckage.patch using git git am libraries/vircadia-client/android/wreckage.patch ``` -Hardcode `QT_CMAKE_PTEFIX_PATH` for android in CMakeLists.txt in project root to the path of qt installation.
-Hardcode `OPENSSL_INSTALL_DIR` for android in `cmake/macros/TargetOpenSSL.cmake` to the path of OpenSSl installation for desired architecture.
+Hardcode `QT_CMAKE_PTEFIX_PATH` for android in CMakeLists.txt in project root to the path of the Qt installation.
+Hardcode `OPENSSL_INSTALL_DIR` for android in `cmake/macros/TargetOpenSSL.cmake` to the path of the OpenSSL installation for desired architecture.
+Hardcode `OPUS_INSTALL_DIR` for android in `cmake/macros/TargetOpus.cmake` to the path of the Opus installation for desired architecture.
Create a build directory for desired architecture. ``` mkdir build_armeabi-v7a @@ -98,7 +113,7 @@ cd build_armeabi-v7a ``` Configure using the following cmake command, adjusting `-DCMAKE_TOOLCHAIN_FILE`, `-DANDROID_ABI` as in TBB instructions above, and -DTBB_DIR according to your TBB installation for specific architecture. ``` -cmake .. -DCMAKE_SYSTEM_NAME=Android -DCMAKE_TOOLCHAIN_FILE=/home/namark/dev/android/ndk-bundle/build/cmake/android.toolchain.cmake -DANDROID_ABI=armeabi-v7a -DANDROID_PLATFORM=android-28 -DHIFI_ANDROID=ON -DTBB_DIR=/home/namark/dev/oneTBB/build/tbb_install/lib/cmake/TBB -DHIFI_ANDROID_APP=dummy -DVIRCADIA_OPTIMIZE=OFF -DUSE_IPFS_EXTERNAL_BUILD_ASSETS=OFF -DVIRCADIA_CPU_ARCHITECTURE="" -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX:PATH=vircadia-client-package +cmake .. -DCMAKE_SYSTEM_NAME=Android -DCMAKE_TOOLCHAIN_FILE=/home/namark/dev/android/ndk-bundle/build/cmake/android.toolchain.cmake -DANDROID_ABI=armeabi-v7a -DANDROID_PLATFORM=android-28 -DHIFI_ANDROID=ON -DTBB_DIR=/home/namark/dev/oneTBB/build/tbb_install/lib/cmake/TBB -DHIFI_ANDROID_APP=dummy -DVIRCADIA_OPTIMIZE=OFF -DVIRCADIA_CPU_ARCHITECTURE="" -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX:PATH=vircadia-client-package ``` Note: First time cmake is ran it may produce an error about the compiler: ``` @@ -113,7 +128,7 @@ Running the second time fixes it somehow. Finally build and install the library (will be installed in `libraries/vircadia-client/vircadia-client-package` directory in the build directory). ``` -cmake --build . --target vircadia +cmake --build . --config Release --target vircadia-client -j4 cd libraries/vircadia-client cmake --build . --target install/strip ``` diff --git a/libraries/vircadia-client/CMakeLists.txt b/libraries/vircadia-client/CMakeLists.txt index 3443e4c90ef..9ac82ea238e 100644 --- a/libraries/vircadia-client/CMakeLists.txt +++ b/libraries/vircadia-client/CMakeLists.txt @@ -20,7 +20,8 @@ set(VIRCADIA_CLIENT_VERSION_YEAR 2022) set(VIRCADIA_CLIENT_VERSION_MAJOR 0) set(VIRCADIA_CLIENT_VERSION_MINOR 1) set(VIRCADIA_CLIENT_VERSION "${VIRCADIA_CLIENT_VERSION_MAJOR}.${VIRCADIA_CLIENT_VERSION_MINOR}") -set(VIRCADIA_CLIENT_VERSION_FULL "v${VIRCADIA_CLIENT_VERSION_YEAR}.${VIRCADIA_CLIENT_VERSION}-git.${VIRCADIA_CLIENT_GIT_COMMIT_SHA_SHORT}") +set(VIRCADIA_CLIENT_VERSION_RELEASE "${VIRCADIA_CLIENT_VERSION_YEAR}.${VIRCADIA_CLIENT_VERSION}") +set(VIRCADIA_CLIENT_VERSION_FULL "v${VIRCADIA_CLIENT_VERSION_RELEASE}-git.${VIRCADIA_CLIENT_GIT_COMMIT_SHA_SHORT}") add_compile_definitions(VIRCADIA_CLIENT_GIT_COMMIT_SHA_SHORT=${VIRCADIA_CLIENT_GIT_COMMIT_SHA_SHORT}) add_compile_definitions(VIRCADIA_CLIENT_VERSION_YEAR=${VIRCADIA_CLIENT_VERSION_YEAR}) @@ -34,19 +35,53 @@ set_property(GLOBAL APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${VIRCADIA_CLIENT_GIT_ROOT}/.git/index") +set(VIRCADIA_CLIENT_INSTALL_HEADERS + src/audio_constants.h + src/audio.h + src/avatar_constants.h + src/avatars.h + src/client.h + src/common.h + src/context.h + src/error.h + src/geometry.h + src/messages.h + src/message_types.h + src/node_list.h + src/node_types.h + src/version.h +) + # check if Doxygen is installed find_package(Doxygen) if (DOXYGEN_FOUND) set(DOXYGEN_PROJECT_NAME "Vircadia Client API") + set(DOXYGEN_PROJECT_BRIEF "C language API/ABI for developing client applications and/or other language bindings for Vircadia Open Source Metaverse Platform.") + set(DOXYGEN_PROJECT_NUMBER ${VIRCADIA_CLIENT_VERSION_RELEASE}) set(DOXYGEN_PROJECT_VERSION ${VIRCADIA_CLIENT_VERSION_FULL}) set(DOXYGEN_GENERATE_MAN YES) + set(DOXYGEN_INLINE_SIMPLE_STRUCTS YES) + set(DOXYGEN_GENERATE_TREEVIEW YES) + set(DOXYGEN_HTML_EXTRA_STYLESHEET + "./docs/doxygen-awesome-css/doxygen-awesome.css" + "./docs/doxygen-awesome-css/doxygen-awesome-sidebar-only.css" + ) + set(DOXYGEN_USE_MDFILE_AS_MAINPAGE "./docs/mainpage.md") doxygen_add_docs( vircadia-client-docs + ${VIRCADIA_CLIENT_INSTALL_HEADERS} + "./docs/mainpage.md" COMMENT "Generate Vircadia Client API documentation." ) unset(DOXYGEN_PROJECT_NAME) + unset(DOXYGEN_PROJECT_BRIEF) + unset(DOXYGEN_PROJECT_NUMBER) unset(DOXYGEN_PROJECT_VERSION) unset(DOXYGEN_GENERATE_MAN) + unset(DOXYGEN_INLINE_SIMPLE_STRUCTS) + unset(DOXYGEN_GENERATE_TREEVIEW) + unset(DOXYGEN_HTML_EXTRA_STYLESHEET) + unset(DOXYGEN_USE_MDFILE_AS_MAINPAGE) else (DOXYGEN_FOUND) message("Doxygen not found. Vircadia Client API documentation target will not be generatred.") endif (DOXYGEN_FOUND) @@ -62,19 +97,9 @@ if(UNIX) target_link_libraries(${TARGET_NAME} Threads::Threads) endif() -link_hifi_libraries(shared networking) +link_hifi_libraries(shared networking avatars-core audio-client-core pcm-codec opus-codec) add_subdirectory(tests) install(TARGETS ${TARGET_NAME} TYPE LIBRARY) -install(FILES - src/client.h - src/common.h - src/context.h - src/error.h - src/messages.h - src/message_types.h - src/node_list.h - src/node_types.h - src/version.h - TYPE INCLUDE) +install(FILES ${VIRCADIA_CLIENT_INSTALL_HEADERS} TYPE INCLUDE) diff --git a/libraries/vircadia-client/README.md b/libraries/vircadia-client/README.md index 98b97c0b592..9bc0b8305a5 100644 --- a/libraries/vircadia-client/README.md +++ b/libraries/vircadia-client/README.md @@ -1,14 +1,14 @@ -### Client API +### Overview -This is C/C++ library for Vircadia client->server communication. +[Vircadia Native Client Library](https://github.com/vircadia/vircadia/blob/unity-sdk/libraries/vircadia-client) is a C API/ABI for Vircadia client->server communication. ### Coding Style -This library is meant to use by other projects in particular other language bindings, and it exposes a C API for that purpose. The C API interfaces adhere to common C coding naming conventions: +This library is meant to use by other projects in particular other language bindings, and it exposes a C API/ABI for that purpose. The interfaces adhere to common C naming conventions: - All words in names are separated by underscores (snake case). - File, variable, function and type names are all lower case. - Global constants (including enum values) and macros are all upper case. -In other aspects they adheres to the [coding standard](https://github.com/namark/vircadia/blob/master/CODING_STANDARD.md). +In other aspects they adheres to the [coding standard](https://github.com/vircadia/vircadia/blob/master/CODING_STANDARD.md). The `src/internal` folder contains the internal C++ implementation details and is not subject to these C style naming convention. The `.cpp` files that correspond to C API header files in the `src` folder make use of these internals and may contain a mixture of styles. @@ -28,7 +28,11 @@ To build the unit tests: ``` cmake --build . --target vircadia-client-tests ``` -The tests can be run with `ctest`. +and to run them: +``` +ctest -j10 +``` +parallelization is important, since some tests have two parts, sender and receiver that communicate. Ideally all test should run in parallel, you can use a number of processes much greater than the processor core count, since most of the test idle most of the time. To build the documentation (requires doxygen): @@ -41,13 +45,13 @@ cmake --build . --target vircadia-client-docs ### Packaging Several build options can be set to minimize dependencies of the library and simplify packaging. -- `ENABLE_WEBRTC_DATA_CHANNELS` can be set to `OFF` since that is only used by the server to support the Web SDK and client. +- `ENABLE_WEBRTC_DATA_CHANNELS` and 'ENABLE_WEBRTC_AUDIO' can be set to `OFF` to avoid having to build and package WebRTC for the target platform. - `BUILD_SHARED_LIBS` can be set to `OFF` to link internal libraries statically. - `STATIC_STDLIB` can be set to `ON` to not require standard runtime libraries that might not be present on a bare-bones system. The following commands in the build directory will produce a package containing all public headers and the dynamic library. ``` -cmake .. -DENABLE_WEBRTC_DATA_CHANNELS=OFF -DBUILD_SHARED_LIBS=OFF -DSTATIC_STDLIB=ON -DCMAKE_INSTALL_PREFIX:PATH=vircadia-client-package +cmake .. -DENABLE_WEBRTC_DATA_CHANNELS=OFF -DENABLE_WEBRTC_AUDIO=OFF -DBUILD_SHARED_LIBS=OFF -DSTATIC_STDLIB=ON -DCMAKE_INSTALL_PREFIX:PATH=vircadia-client-package cmake --build . --config Release --target vircadia-client -j4 cd libraries/vircadia-client cmake --build . --target install/strip diff --git a/libraries/vircadia-client/android/opus_configure_android.sh b/libraries/vircadia-client/android/opus_configure_android.sh new file mode 100755 index 00000000000..b67f8a0576f --- /dev/null +++ b/libraries/vircadia-client/android/opus_configure_android.sh @@ -0,0 +1,24 @@ +# https://developer.android.com/ndk/guides/other_build_systems +export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64 + +# export TARGET=aarch64-linux-android +export TARGET=armv7a-linux-androideabi +# export TARGET=i686-linux-android +# export TARGET=x86_64-linux-android + +# Set this to your minSdkVersion. +export API=21 + +# Configure and build. +export AR=$TOOLCHAIN/bin/llvm-ar +export CC=$TOOLCHAIN/bin/$TARGET$API-clang +export AS=$CC +export CXX=$TOOLCHAIN/bin/$TARGET$API-clang++ +export LD=$TOOLCHAIN/bin/ld +export CCLD=CC +export RANLIB=$TOOLCHAIN/bin/llvm-ranlib +export STRIP=$TOOLCHAIN/bin/llvm-strip +echo Destination $(pwd)/opus_package +../configure --host $TARGET --target $TARGET$API --prefix $(pwd)/opus_package --disable-silent-rules --disable-extra-programs --disable-shared +make install -j4 + diff --git a/libraries/vircadia-client/android/wreckage.patch b/libraries/vircadia-client/android/wreckage.patch index 7e6be30b532..a8af6bd0e57 100644 --- a/libraries/vircadia-client/android/wreckage.patch +++ b/libraries/vircadia-client/android/wreckage.patch @@ -1,6 +1,6 @@ -From e868ad04735adb9396ddf72cd4cf5caf61b80bf5 Mon Sep 17 00:00:00 2001 +From 519bd45dd5e2880805c8607fca0f788bf28fe247 Mon Sep 17 00:00:00 2001 From: namark -Date: Tue, 3 May 2022 20:53:38 +0400 +Date: Sun, 31 Jul 2022 20:02:08 +0400 Subject: [PATCH] Ugly hacks to cross compile native client library for android. @@ -10,20 +10,19 @@ Subject: [PATCH] Ugly hacks to cross compile native client library for .../dummy/src/main/res/values/strings.xml | 3 + cmake/compiler.cmake | 6 +- cmake/macros/TargetOpenSSL.cmake | 4 +- + cmake/macros/TargetOpus.cmake | 9 +- cmake/macros/TargetTBB.cmake | 16 +- - hifi_vcpkg.py | 2 +- libraries/networking/src/AccountManager.cpp | 2 +- libraries/shared/CMakeLists.txt | 4 +- libraries/shared/src/Gzip.cpp | 266 +++++++++--------- libraries/shared/src/SharedUtil.cpp | 4 +- - libraries/shared/src/shared/FileLogger.cpp | 1 - libraries/vircadia-client/CMakeLists.txt | 4 +- - 13 files changed, 159 insertions(+), 159 deletions(-) + 12 files changed, 163 insertions(+), 161 deletions(-) create mode 100644 android/apps/dummy/CMakeLists.txt create mode 100644 android/apps/dummy/src/main/res/values/strings.xml diff --git a/CMakeLists.txt b/CMakeLists.txt -index e923a89316..bb9e5310eb 100644 +index 76f2d19ba5..54f50cbda1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -167,7 +167,7 @@ endif() @@ -86,6 +85,24 @@ index faf5116a25..6e1d9f303e 100644 set(OPENSSL_INCLUDE_DIR "${OPENSSL_INSTALL_DIR}/include" CACHE STRING INTERNAL) set(OPENSSL_LIBRARIES "${OPENSSL_INSTALL_DIR}/lib/libcrypto.a;${OPENSSL_INSTALL_DIR}/lib/libssl.a" CACHE STRING INTERNAL) else() +diff --git a/cmake/macros/TargetOpus.cmake b/cmake/macros/TargetOpus.cmake +index a8faf5139e..c602d642d2 100644 +--- a/cmake/macros/TargetOpus.cmake ++++ b/cmake/macros/TargetOpus.cmake +@@ -6,8 +6,9 @@ + # See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html + # + macro(TARGET_opus) +- find_library(OPUS_LIBRARY_RELEASE NAMES opus PATHS ${VCPKG_INSTALL_ROOT}/lib) +- find_library(OPUS_LIBRARY_DEBUG NAMES opus PATHS ${VCPKG_INSTALL_ROOT}/debug/lib) +- select_library_configurations(OPUS) +- target_link_libraries(${TARGET_NAME} ${OPUS_LIBRARY}) ++ # set(OPUS_INSTALL_DIR ${VCPKG_INSTALL_ROOT}) ++ set(OPUS_INSTALL_DIR /home/namark/dev/opus-1.3.1/build/opus_package) ++ # set(OPUS_INSTALL_DIR /home/namark/dev/opus-1.3.1/build64/opus_package) ++ include_directories(SYSTEM "${OPUS_INSTALL_DIR}/include") ++ target_link_libraries(${TARGET_NAME} ${OPUS_INSTALL_DIR}/lib/libopus.a) + endmacro() diff --git a/cmake/macros/TargetTBB.cmake b/cmake/macros/TargetTBB.cmake index 087f3f3242..156d4e4021 100644 --- a/cmake/macros/TargetTBB.cmake @@ -112,19 +129,6 @@ index 087f3f3242..156d4e4021 100644 +target_link_libraries(${TARGET_NAME} TBB::tbb) endmacro() -diff --git a/hifi_vcpkg.py b/hifi_vcpkg.py -index df14f05799..daff91e9b0 100644 ---- a/hifi_vcpkg.py -+++ b/hifi_vcpkg.py -@@ -302,7 +302,7 @@ endif() - url = hifi_android.getPackageUrl(package) - zipFile = package['file'].endswith('.zip') - print("Android archive {}".format(package['file'])) -- hifi_utils.downloadAndExtract(url, dest, isZip=zipFile, hash=package['checksum'], hasher=hashlib.md5()) -+ hifi_utils.downloadAndExtract([url], dest, hash=package['checksum'], hasher=hashlib.md5) - - def writeTag(self): - if self.noClean: diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index ce449a66c3..63bf824800 100644 --- a/libraries/networking/src/AccountManager.cpp @@ -458,20 +462,8 @@ index e9576a4477..e75ff5666e 100644 } #ifdef Q_OS_WIN -diff --git a/libraries/shared/src/shared/FileLogger.cpp b/libraries/shared/src/shared/FileLogger.cpp -index 8797f15371..06577e7620 100644 ---- a/libraries/shared/src/shared/FileLogger.cpp -+++ b/libraries/shared/src/shared/FileLogger.cpp -@@ -13,7 +13,6 @@ - - #include - #include --#include - - #include "FileUtils.h" - #include "NetworkUtils.h" diff --git a/libraries/vircadia-client/CMakeLists.txt b/libraries/vircadia-client/CMakeLists.txt -index 3443e4c90e..c0cc8def26 100644 +index 8fed87968a..d8fd2533da 100644 --- a/libraries/vircadia-client/CMakeLists.txt +++ b/libraries/vircadia-client/CMakeLists.txt @@ -53,7 +53,7 @@ endif (DOXYGEN_FOUND) @@ -485,7 +477,7 @@ index 3443e4c90e..c0cc8def26 100644 if(UNIX) @@ -64,7 +64,7 @@ endif() - link_hifi_libraries(shared networking) + link_hifi_libraries(shared networking avatars-core audio-client-core pcm-codec opus-codec) -add_subdirectory(tests) +# add_subdirectory(tests) diff --git a/libraries/vircadia-client/docs/doxygen-awesome-css/doxygen-awesome-sidebar-only.css b/libraries/vircadia-client/docs/doxygen-awesome-css/doxygen-awesome-sidebar-only.css new file mode 100644 index 00000000000..b5c4e7cbd60 --- /dev/null +++ b/libraries/vircadia-client/docs/doxygen-awesome-css/doxygen-awesome-sidebar-only.css @@ -0,0 +1,113 @@ +/** + +Doxygen Awesome +https://github.com/jothepro/doxygen-awesome-css + +MIT License + +Copyright (c) 2021 jothepro + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + */ + +html { + /* side nav width. MUST be = `TREEVIEW_WIDTH`. + * Make sure it is wide enough to contain the page title (logo + title + version) + */ + --side-nav-fixed-width: 335px; + --menu-display: none; + + --top-height: 120px; +} + +#projectname { + white-space: nowrap; +} + + +@media screen and (min-width: 768px) { + html { + --searchbar-background: var(--page-background-color); + } + + #side-nav { + min-width: var(--side-nav-fixed-width); + max-width: var(--side-nav-fixed-width); + top: var(--top-height); + overflow: visible; + } + + #nav-tree, #side-nav { + height: calc(100vh - var(--top-height)) !important; + } + + #nav-tree { + padding: 0; + } + + #top { + display: block; + border-bottom: none; + height: var(--top-height); + margin-bottom: calc(0px - var(--top-height)); + max-width: var(--side-nav-fixed-width); + overflow: hidden; + background: var(--side-nav-background); + } + #main-nav { + float: left; + padding-right: 0; + } + + .ui-resizable-handle { + cursor: default; + width: 1px !important; + box-shadow: 0 calc(-2 * var(--top-height)) 0 0 var(--separator-color); + } + + #nav-path { + position: fixed; + right: 0; + left: var(--side-nav-fixed-width); + bottom: 0; + width: auto; + } + + #doc-content { + height: calc(100vh - 31px) !important; + padding-bottom: calc(3 * var(--spacing-large)); + padding-top: calc(var(--top-height) - 80px); + box-sizing: border-box; + margin-left: var(--side-nav-fixed-width) !important; + } + + #MSearchBox { + width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium))); + } + + #MSearchField { + width: calc(var(--side-nav-fixed-width) - calc(2 * var(--spacing-medium)) - 65px); + } + + #MSearchResultsWindow { + left: var(--spacing-medium) !important; + right: auto; + } +} diff --git a/libraries/vircadia-client/docs/doxygen-awesome-css/doxygen-awesome.css b/libraries/vircadia-client/docs/doxygen-awesome-css/doxygen-awesome.css new file mode 100644 index 00000000000..5f420dc42ee --- /dev/null +++ b/libraries/vircadia-client/docs/doxygen-awesome-css/doxygen-awesome.css @@ -0,0 +1,2135 @@ +/** + +Doxygen Awesome +https://github.com/jothepro/doxygen-awesome-css + +MIT License + +Copyright (c) 2021 - 2022 jothepro + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +html { + /* primary theme color. This will affect the entire websites color scheme: links, arrows, labels, ... */ + --primary-color: #1779c4; + --primary-dark-color: #335c80; + --primary-light-color: #70b1e9; + + /* page base colors */ + --page-background-color: white; + --page-foreground-color: #2f4153; + --page-secondary-foreground-color: #637485; + + /* color for all separators on the website: hr, borders, ... */ + --separator-color: #dedede; + + /* border radius for all rounded components. Will affect many components, like dropdowns, memitems, codeblocks, ... */ + --border-radius-large: 8px; + --border-radius-small: 4px; + --border-radius-medium: 6px; + + /* default spacings. Most compontest reference these values for spacing, to provide uniform spacing on the page. */ + --spacing-small: 5px; + --spacing-medium: 10px; + --spacing-large: 16px; + + /* default box shadow used for raising an element above the normal content. Used in dropdowns, Searchresult, ... */ + --box-shadow: 0 2px 8px 0 rgba(0,0,0,.075); + + --odd-color: rgba(0,0,0,.028); + + /* font-families. will affect all text on the website + * font-family: the normal font for text, headlines, menus + * font-family-monospace: used for preformatted text in memtitle, code, fragments + */ + --font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif; + --font-family-monospace: ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace; + + /* font sizes */ + --page-font-size: 15.6px; + --navigation-font-size: 14.4px; + --code-font-size: 14px; /* affects code, fragment */ + --title-font-size: 22px; + + /* content text properties. These only affect the page content, not the navigation or any other ui elements */ + --content-line-height: 27px; + /* The content is centered and constraint in it's width. To make the content fill the whole page, set the variable to auto.*/ + --content-maxwidth: 1000px; + + /* colors for various content boxes: @warning, @note, @deprecated @bug */ + --warning-color: #f8d1cc; + --warning-color-dark: #b61825; + --warning-color-darker: #75070f; + --note-color: #faf3d8; + --note-color-dark: #f3a600; + --note-color-darker: #5f4204; + --todo-color: #e4f3ff; + --todo-color-dark: #1879C4; + --todo-color-darker: #274a5c; + --deprecated-color: #ecf0f3; + --deprecated-color-dark: #5b6269; + --deprecated-color-darker: #43454a; + --bug-color: #e4dafd; + --bug-color-dark: #5b2bdd; + --bug-color-darker: #2a0d72; + --invariant-color: #d8f1e3; + --invariant-color-dark: #44b86f; + --invariant-color-darker: #265532; + + /* blockquote colors */ + --blockquote-background: #f8f9fa; + --blockquote-foreground: #636568; + + /* table colors */ + --tablehead-background: #f1f1f1; + --tablehead-foreground: var(--page-foreground-color); + + /* menu-display: block | none + * Visibility of the top navigation on screens >= 768px. On smaller screen the menu is always visible. + * `GENERATE_TREEVIEW` MUST be enabled! + */ + --menu-display: block; + + --menu-focus-foreground: var(--page-background-color); + --menu-focus-background: var(--primary-color); + --menu-selected-background: rgba(0,0,0,.05); + + + --header-background: var(--page-background-color); + --header-foreground: var(--page-foreground-color); + + /* searchbar colors */ + --searchbar-background: var(--side-nav-background); + --searchbar-foreground: var(--page-foreground-color); + + /* searchbar size + * (`searchbar-width` is only applied on screens >= 768px. + * on smaller screens the searchbar will always fill the entire screen width) */ + --searchbar-height: 33px; + --searchbar-width: 210px; + --searchbar-border-radius: var(--searchbar-height); + + /* code block colors */ + --code-background: #f5f5f5; + --code-foreground: var(--page-foreground-color); + + /* fragment colors */ + --fragment-background: #F8F9FA; + --fragment-foreground: #37474F; + --fragment-keyword: #bb6bb2; + --fragment-keywordtype: #8258b3; + --fragment-keywordflow: #d67c3b; + --fragment-token: #438a59; + --fragment-comment: #969696; + --fragment-link: #5383d6; + --fragment-preprocessor: #46aaa5; + --fragment-linenumber-color: #797979; + --fragment-linenumber-background: #f4f4f5; + --fragment-linenumber-border: #e3e5e7; + --fragment-lineheight: 20px; + + /* sidebar navigation (treeview) colors */ + --side-nav-background: #fbfbfb; + --side-nav-foreground: var(--page-foreground-color); + --side-nav-arrow-opacity: 0; + --side-nav-arrow-hover-opacity: 0.9; + + --toc-background: var(--side-nav-background); + --toc-foreground: var(--side-nav-foreground); + + /* height of an item in any tree / collapsable table */ + --tree-item-height: 30px; + + --memname-font-size: var(--code-font-size); + --memtitle-font-size: 18px; + + --webkit-scrollbar-size: 7px; + --webkit-scrollbar-padding: 4px; + --webkit-scrollbar-color: var(--separator-color); +} + +@media screen and (max-width: 767px) { + html { + --page-font-size: 16px; + --navigation-font-size: 16px; + --code-font-size: 15px; /* affects code, fragment */ + --title-font-size: 22px; + } +} + +@media (prefers-color-scheme: dark) { + html:not(.light-mode) { + color-scheme: dark; + + --primary-color: #1982d2; + --primary-dark-color: #86a9c4; + --primary-light-color: #4779ac; + + --box-shadow: 0 2px 8px 0 rgba(0,0,0,.35); + + --odd-color: rgba(100,100,100,.06); + + --menu-selected-background: rgba(0,0,0,.4); + + --page-background-color: #1C1D1F; + --page-foreground-color: #d2dbde; + --page-secondary-foreground-color: #859399; + --separator-color: #38393b; + --side-nav-background: #252628; + + --code-background: #2a2c2f; + + --tablehead-background: #2a2c2f; + + --blockquote-background: #222325; + --blockquote-foreground: #7e8c92; + + --warning-color: #2e1917; + --warning-color-dark: #ad2617; + --warning-color-darker: #f5b1aa; + --note-color: #3b2e04; + --note-color-dark: #f1b602; + --note-color-darker: #ceb670; + --todo-color: #163750; + --todo-color-dark: #1982D2; + --todo-color-darker: #dcf0fa; + --deprecated-color: #2e323b; + --deprecated-color-dark: #738396; + --deprecated-color-darker: #abb0bd; + --bug-color: #2a2536; + --bug-color-dark: #7661b3; + --bug-color-darker: #ae9ed6; + --invariant-color: #303a35; + --invariant-color-dark: #76ce96; + --invariant-color-darker: #cceed5; + + --fragment-background: #282c34; + --fragment-foreground: #dbe4eb; + --fragment-keyword: #cc99cd; + --fragment-keywordtype: #ab99cd; + --fragment-keywordflow: #e08000; + --fragment-token: #7ec699; + --fragment-comment: #999999; + --fragment-link: #98c0e3; + --fragment-preprocessor: #65cabe; + --fragment-linenumber-color: #cccccc; + --fragment-linenumber-background: #35393c; + --fragment-linenumber-border: #1f1f1f; + } +} + +/* dark mode variables are defined twice, to support both the dark-mode without and with doxygen-awesome-darkmode-toggle.js */ +html.dark-mode { + color-scheme: dark; + + --primary-color: #1982d2; + --primary-dark-color: #86a9c4; + --primary-light-color: #4779ac; + + --box-shadow: 0 2px 8px 0 rgba(0,0,0,.30); + + --odd-color: rgba(100,100,100,.06); + + --menu-selected-background: rgba(0,0,0,.4); + + --page-background-color: #1C1D1F; + --page-foreground-color: #d2dbde; + --page-secondary-foreground-color: #859399; + --separator-color: #38393b; + --side-nav-background: #252628; + + --code-background: #2a2c2f; + + --tablehead-background: #2a2c2f; + + --blockquote-background: #222325; + --blockquote-foreground: #7e8c92; + + --warning-color: #2e1917; + --warning-color-dark: #ad2617; + --warning-color-darker: #f5b1aa; + --note-color: #3b2e04; + --note-color-dark: #f1b602; + --note-color-darker: #ceb670; + --todo-color: #163750; + --todo-color-dark: #1982D2; + --todo-color-darker: #dcf0fa; + --deprecated-color: #2e323b; + --deprecated-color-dark: #738396; + --deprecated-color-darker: #abb0bd; + --bug-color: #2a2536; + --bug-color-dark: #7661b3; + --bug-color-darker: #ae9ed6; + --invariant-color: #303a35; + --invariant-color-dark: #76ce96; + --invariant-color-darker: #cceed5; + + --fragment-background: #282c34; + --fragment-foreground: #dbe4eb; + --fragment-keyword: #cc99cd; + --fragment-keywordtype: #ab99cd; + --fragment-keywordflow: #e08000; + --fragment-token: #7ec699; + --fragment-comment: #999999; + --fragment-link: #98c0e3; + --fragment-preprocessor: #65cabe; + --fragment-linenumber-color: #cccccc; + --fragment-linenumber-background: #35393c; + --fragment-linenumber-border: #1f1f1f; +} + +body { + color: var(--page-foreground-color); + background-color: var(--page-background-color); + font-size: var(--page-font-size); +} + +body, table, div, p, dl, #nav-tree .label, .title, .sm-dox a, .sm-dox a:hover, .sm-dox a:focus, #projectname, .SelectItem, #MSearchField, .navpath li.navelem a, .navpath li.navelem a:hover { + font-family: var(--font-family); +} + +h1, h2, h3, h4, h5 { + margin-top: .9em; + font-weight: 600; + line-height: initial; +} + +p, div, table, dl { + font-size: var(--page-font-size); +} + +a:link, a:visited, a:hover, a:focus, a:active { + color: var(--primary-color) !important; + font-weight: 500; +} + +a.anchor { + scroll-margin-top: var(--spacing-large); +} + +/* + Title and top navigation + */ + +#top { + background: var(--header-background); + border-bottom: 1px solid var(--separator-color); +} + +@media screen and (min-width: 768px) { + #top { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + } +} + +#main-nav { + flex-grow: 5; + padding: var(--spacing-small) var(--spacing-medium); +} + +#titlearea { + width: auto; + padding: var(--spacing-medium) var(--spacing-large); + background: none; + color: var(--header-foreground); + border-bottom: none; +} + +@media screen and (max-width: 767px) { + #titlearea { + padding-bottom: var(--spacing-small); + } +} + +#titlearea table tbody tr { + height: auto !important; +} + +#projectname { + font-size: var(--title-font-size); + font-weight: 600; +} + +#projectnumber { + font-family: inherit; + font-size: 60%; +} + +#projectbrief { + font-family: inherit; + font-size: 80%; +} + +#projectlogo { + vertical-align: middle; +} + +#projectlogo img { + max-height: calc(var(--title-font-size) * 2); + margin-right: var(--spacing-small); +} + +.sm-dox, .tabs, .tabs2, .tabs3 { + background: none; + padding: 0; +} + +.tabs, .tabs2, .tabs3 { + border-bottom: 1px solid var(--separator-color); + margin-bottom: -1px; +} + +@media screen and (max-width: 767px) { + .sm-dox a span.sub-arrow { + background: var(--code-background); + } + + #main-menu a.has-submenu span.sub-arrow { + color: var(--page-secondary-foreground-color); + border-radius: var(--border-radius-medium); + } + + #main-menu a.has-submenu:hover span.sub-arrow { + color: var(--page-foreground-color); + } +} + +@media screen and (min-width: 768px) { + .sm-dox li, .tablist li { + display: var(--menu-display); + } + + .sm-dox a span.sub-arrow { + border-color: var(--header-foreground) transparent transparent transparent; + } + + .sm-dox a:hover span.sub-arrow { + border-color: var(--menu-focus-foreground) transparent transparent transparent; + } + + .sm-dox ul a span.sub-arrow { + border-color: transparent transparent transparent var(--page-foreground-color); + } + + .sm-dox ul a:hover span.sub-arrow { + border-color: transparent transparent transparent var(--menu-focus-foreground); + } +} + +.sm-dox ul { + background: var(--page-background-color); + box-shadow: var(--box-shadow); + border: 1px solid var(--separator-color); + border-radius: var(--border-radius-medium) !important; + padding: var(--spacing-small); + animation: ease-out 150ms slideInMenu; +} + +@keyframes slideInMenu { + from { + opacity: 0; + transform: translate(0px, -2px); + } + + to { + opacity: 1; + transform: translate(0px, 0px); + } +} + +.sm-dox ul a { + color: var(--page-foreground-color) !important; + background: var(--page-background-color); + font-size: var(--navigation-font-size); +} + +.sm-dox>li>ul:after { + border-bottom-color: var(--page-background-color) !important; +} + +.sm-dox>li>ul:before { + border-bottom-color: var(--separator-color) !important; +} + +.sm-dox ul a:hover, .sm-dox ul a:active, .sm-dox ul a:focus { + font-size: var(--navigation-font-size) !important; + color: var(--menu-focus-foreground) !important; + text-shadow: none; + background-color: var(--menu-focus-background); + border-radius: var(--border-radius-small) !important; +} + +.sm-dox a, .sm-dox a:focus, .tablist li, .tablist li a, .tablist li.current a { + text-shadow: none; + background: transparent; + background-image: none !important; + color: var(--header-foreground) !important; + font-weight: normal; + font-size: var(--navigation-font-size); + border-radius: var(--border-radius-small) !important; +} + +.sm-dox a:focus { + outline: auto; +} + +.sm-dox a:hover, .sm-dox a:active, .tablist li a:hover { + text-shadow: none; + font-weight: normal; + background: var(--menu-focus-background); + color: var(--menu-focus-foreground) !important; + border-radius: var(--border-radius-small) !important; + font-size: var(--navigation-font-size); +} + +.tablist li.current { + border-radius: var(--border-radius-small); + background: var(--menu-selected-background); +} + +.tablist li { + margin: var(--spacing-small) 0 var(--spacing-small) var(--spacing-small); +} + +.tablist a { + padding: 0 var(--spacing-large); +} + + +/* + Search box + */ + +#MSearchBox { + height: var(--searchbar-height); + background: var(--searchbar-background); + border-radius: var(--searchbar-border-radius); + border: 1px solid var(--separator-color); + overflow: hidden; + width: var(--searchbar-width); + position: relative; + box-shadow: none; + display: block; + margin-top: 0; +} + +.left #MSearchSelect { + left: 0; + user-select: none; +} + +.SelectionMark { + user-select: none; +} + +.tabs .left #MSearchSelect { + padding-left: 0; +} + +.tabs #MSearchBox { + position: absolute; + right: var(--spacing-medium); +} + +@media screen and (max-width: 767px) { + .tabs #MSearchBox { + position: relative; + right: 0; + margin-left: var(--spacing-medium); + margin-top: 0; + } +} + +#MSearchSelectWindow, #MSearchResultsWindow { + z-index: 9999; +} + +#MSearchBox.MSearchBoxActive { + border-color: var(--primary-color); + box-shadow: inset 0 0 0 1px var(--primary-color); +} + +#main-menu > li:last-child { + margin-right: 0; +} + +@media screen and (max-width: 767px) { + #main-menu > li:last-child { + height: 50px; + } +} + +#MSearchField { + font-size: var(--navigation-font-size); + height: calc(var(--searchbar-height) - 2px); + background: transparent; + width: calc(var(--searchbar-width) - 64px); +} + +.MSearchBoxActive #MSearchField { + color: var(--searchbar-foreground); +} + +#MSearchSelect { + top: calc(calc(var(--searchbar-height) / 2) - 11px); +} + +.left #MSearchSelect { + padding-left: 8px; +} + +#MSearchBox span.left, #MSearchBox span.right { + background: none; +} + +#MSearchBox span.right { + padding-top: calc(calc(var(--searchbar-height) / 2) - 12px); + position: absolute; + right: var(--spacing-small); +} + +.tabs #MSearchBox span.right { + top: calc(calc(var(--searchbar-height) / 2) - 12px); +} + +@keyframes slideInSearchResults { + from { + opacity: 0; + transform: translate(0, 15px); + } + + to { + opacity: 1; + transform: translate(0, 20px); + } +} + +#MSearchResultsWindow { + left: auto !important; + right: var(--spacing-medium); + border-radius: var(--border-radius-large); + border: 1px solid var(--separator-color); + transform: translate(0, 20px); + box-shadow: var(--box-shadow); + animation: ease-out 280ms slideInSearchResults; + background: var(--page-background-color); +} + +iframe#MSearchResults { + margin: 4px; +} + +iframe { + color-scheme: normal; +} + +@media (prefers-color-scheme: dark) { + html:not(.light-mode) iframe#MSearchResults { + filter: invert() hue-rotate(180deg); + } +} + +html.dark-mode iframe#MSearchResults { + filter: invert() hue-rotate(180deg); +} + +#MSearchSelectWindow { + border: 1px solid var(--separator-color); + border-radius: var(--border-radius-medium); + box-shadow: var(--box-shadow); + background: var(--page-background-color); + padding-top: var(--spacing-small); + padding-bottom: var(--spacing-small); +} + +#MSearchSelectWindow a.SelectItem { + font-size: var(--navigation-font-size); + line-height: var(--content-line-height); + margin: 0 var(--spacing-small); + border-radius: var(--border-radius-small); + color: var(--page-foreground-color) !important; + font-weight: normal; +} + +#MSearchSelectWindow a.SelectItem:hover { + background: var(--menu-focus-background); + color: var(--menu-focus-foreground) !important; +} + +@media screen and (max-width: 767px) { + #MSearchBox { + margin-top: var(--spacing-medium); + margin-bottom: var(--spacing-medium); + width: calc(100vw - 30px); + } + + #main-menu > li:last-child { + float: none !important; + } + + #MSearchField { + width: calc(100vw - 110px); + } + + @keyframes slideInSearchResultsMobile { + from { + opacity: 0; + transform: translate(0, 15px); + } + + to { + opacity: 1; + transform: translate(0, 20px); + } + } + + #MSearchResultsWindow { + left: var(--spacing-medium) !important; + right: var(--spacing-medium); + overflow: auto; + transform: translate(0, 20px); + animation: ease-out 280ms slideInSearchResultsMobile; + } + + /* + * Overwrites for fixing the searchbox on mobile in doxygen 1.9.2 + */ + label.main-menu-btn ~ #searchBoxPos1 { + top: 3px !important; + right: 6px !important; + left: 45px; + display: flex; + } + + label.main-menu-btn ~ #searchBoxPos1 > #MSearchBox { + margin-top: 0; + margin-bottom: 0; + flex-grow: 2; + float: left; + } +} + +/* + Tree view + */ + +#side-nav { + padding: 0 !important; + background: var(--side-nav-background); +} + +@media screen and (max-width: 767px) { + #side-nav { + display: none; + } + + #doc-content { + margin-left: 0 !important; + } +} + +#nav-tree { + background: transparent; +} + +#nav-tree .label { + font-size: var(--navigation-font-size); +} + +#nav-tree .item { + height: var(--tree-item-height); + line-height: var(--tree-item-height); +} + +#nav-sync { + bottom: 12px; + right: 12px; + top: auto !important; + user-select: none; +} + +#nav-tree .selected { + text-shadow: none; + background-image: none; + background-color: transparent; + position: relative; +} + +#nav-tree .selected::after { + content: ""; + position: absolute; + top: 1px; + bottom: 1px; + left: 0; + width: 4px; + border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0; + background: var(--primary-color); +} + + +#nav-tree a { + color: var(--side-nav-foreground) !important; + font-weight: normal; +} + +#nav-tree a:focus { + outline-style: auto; +} + +#nav-tree .arrow { + opacity: var(--side-nav-arrow-opacity); +} + +.arrow { + color: inherit; + cursor: pointer; + font-size: 45%; + vertical-align: middle; + margin-right: 2px; + font-family: serif; + height: auto; + text-align: right; +} + +#nav-tree div.item:hover .arrow, #nav-tree a:focus .arrow { + opacity: var(--side-nav-arrow-hover-opacity); +} + +#nav-tree .selected a { + color: var(--primary-color) !important; + font-weight: bolder; + font-weight: 600; +} + +.ui-resizable-e { + background: var(--separator-color); + width: 1px; +} + +/* + Contents + */ + +div.header { + border-bottom: 1px solid var(--separator-color); + background-color: var(--page-background-color); + background-image: none; +} + +div.contents, div.header .title, div.header .summary { + max-width: var(--content-maxwidth); +} + +div.contents, div.header .title { + line-height: initial; + margin: calc(var(--spacing-medium) + .2em) auto var(--spacing-medium) auto; +} + +div.header .summary { + margin: var(--spacing-medium) auto 0 auto; +} + +div.headertitle { + padding: 0; +} + +div.header .title { + font-weight: 600; + font-size: 210%; + padding: var(--spacing-medium) var(--spacing-large); + word-break: break-word; +} + +div.header .summary { + width: auto; + display: block; + float: none; + padding: 0 var(--spacing-large); +} + +td.memSeparator { + border-color: var(--separator-color); +} + +span.mlabel { + background: var(--primary-color); + border: none; + padding: 4px 9px; + border-radius: 12px; + margin-right: var(--spacing-medium); +} + +span.mlabel:last-of-type { + margin-right: 2px; +} + +div.contents { + padding: 0 var(--spacing-large); +} + +div.contents p, div.contents li { + line-height: var(--content-line-height); +} + +div.contents div.dyncontent { + margin: var(--spacing-medium) 0; +} + +@media (prefers-color-scheme: dark) { + html:not(.light-mode) div.contents div.dyncontent img, + html:not(.light-mode) div.contents center img, + html:not(.light-mode) div.contents table img, + html:not(.light-mode) div.contents div.dyncontent iframe, + html:not(.light-mode) div.contents center iframe, + html:not(.light-mode) div.contents table iframe { + filter: hue-rotate(180deg) invert(); + } +} + +html.dark-mode div.contents div.dyncontent img, +html.dark-mode div.contents center img, +html.dark-mode div.contents table img, +html.dark-mode div.contents div.dyncontent iframe, +html.dark-mode div.contents center iframe, +html.dark-mode div.contents table iframe { + filter: hue-rotate(180deg) invert(); +} + +h2.groupheader { + border-bottom: 0px; + color: var(--page-foreground-color); + box-shadow: + 100px 0 var(--page-background-color), + -100px 0 var(--page-background-color), + 100px 0.75px var(--separator-color), + -100px 0.75px var(--separator-color), + 500px 0 var(--page-background-color), + -500px 0 var(--page-background-color), + 500px 0.75px var(--separator-color), + -500px 0.75px var(--separator-color), + 1500px 0 var(--page-background-color), + -1500px 0 var(--page-background-color), + 1500px 0.75px var(--separator-color), + -1500px 0.75px var(--separator-color), + 2000px 0 var(--page-background-color), + -2000px 0 var(--page-background-color), + 2000px 0.75px var(--separator-color), + -2000px 0.75px var(--separator-color); +} + +blockquote { + margin: 0 var(--spacing-medium) 0 var(--spacing-medium); + padding: var(--spacing-small) var(--spacing-large); + background: var(--blockquote-background); + color: var(--blockquote-foreground); + border-left: 0; + overflow: visible; + border-radius: var(--border-radius-medium); + overflow: visible; + position: relative; +} + +blockquote::before, blockquote::after { + font-weight: bold; + font-family: serif; + font-size: 360%; + opacity: .15; + position: absolute; +} + +blockquote::before { + content: "“"; + left: -10px; + top: 4px; +} + +blockquote::after { + content: "”"; + right: -8px; + bottom: -25px; +} + +blockquote p { + margin: var(--spacing-small) 0 var(--spacing-medium) 0; +} +.paramname { + font-weight: 600; + color: var(--primary-dark-color); +} + +.paramname > code { + border: 0; +} + +table.params .paramname { + font-weight: 600; + font-family: var(--font-family-monospace); + font-size: var(--code-font-size); + padding-right: var(--spacing-small); +} + +.glow { + text-shadow: 0 0 15px var(--primary-light-color) !important; +} + +.alphachar a { + color: var(--page-foreground-color); +} + +/* + Table of Contents + */ + +div.toc { + z-index: 10; + position: relative; + background-color: var(--toc-background); + border: 1px solid var(--separator-color); + border-radius: var(--border-radius-medium); + box-shadow: var(--box-shadow); + padding: 0 var(--spacing-large); + margin: 0 0 var(--spacing-medium) var(--spacing-medium); +} + +div.toc h3 { + color: var(--toc-foreground); + font-size: var(--navigation-font-size); + margin: var(--spacing-large) 0; +} + +div.toc li { + font-size: var(--navigation-font-size); + padding: 0; + background: none; +} + +div.toc li:before { + content: '↓'; + font-weight: 800; + font-family: var(--font-family); + margin-right: var(--spacing-small); + color: var(--toc-foreground); + opacity: .4; +} + +div.toc ul li.level1 { + margin: 0; +} + +div.toc ul li.level2, div.toc ul li.level3 { + margin-top: 0; +} + + +@media screen and (max-width: 767px) { + div.toc { + float: none; + width: auto; + margin: 0 0 var(--spacing-medium) 0; + } +} + +/* + Code & Fragments + */ + +code, div.fragment, pre.fragment { + border-radius: var(--border-radius-small); + border: 1px solid var(--separator-color); + overflow: hidden; +} + +code { + display: inline; + background: var(--code-background); + color: var(--code-foreground); + padding: 2px 6px; + word-break: break-word; +} + +div.fragment, pre.fragment { + margin: var(--spacing-medium) 0; + padding: calc(var(--spacing-large) - (var(--spacing-large) / 6)) var(--spacing-large); + background: var(--fragment-background); + color: var(--fragment-foreground); + overflow-x: auto; +} + +@media screen and (max-width: 767px) { + div.fragment, pre.fragment { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: 0; + } + + .contents > div.fragment, + .textblock > div.fragment, + .textblock > pre.fragment, + .contents > .doxygen-awesome-fragment-wrapper > div.fragment, + .textblock > .doxygen-awesome-fragment-wrapper > div.fragment, + .textblock > .doxygen-awesome-fragment-wrapper > pre.fragment { + margin: var(--spacing-medium) calc(0px - var(--spacing-large)); + border-radius: 0; + border-left: 0; + } + + .textblock li > .fragment, + .textblock li > .doxygen-awesome-fragment-wrapper > .fragment { + margin: var(--spacing-medium) calc(0px - var(--spacing-large)); + } + + .memdoc li > .fragment, + .memdoc li > .doxygen-awesome-fragment-wrapper > .fragment { + margin: var(--spacing-medium) calc(0px - var(--spacing-medium)); + } + + .textblock ul, .memdoc ul { + overflow: initial; + } + + .memdoc > div.fragment, + .memdoc > pre.fragment, + dl dd > div.fragment, + dl dd pre.fragment, + .memdoc > .doxygen-awesome-fragment-wrapper > div.fragment, + .memdoc > .doxygen-awesome-fragment-wrapper > pre.fragment, + dl dd > .doxygen-awesome-fragment-wrapper > div.fragment, + dl dd .doxygen-awesome-fragment-wrapper > pre.fragment { + margin: var(--spacing-medium) calc(0px - var(--spacing-medium)); + border-radius: 0; + border-left: 0; + } +} + +code, code a, pre.fragment, div.fragment, div.fragment .line, div.fragment span, div.fragment .line a, div.fragment .line span { + font-family: var(--font-family-monospace); + font-size: var(--code-font-size) !important; +} + +div.line:after { + margin-right: var(--spacing-medium); +} + +div.fragment .line, pre.fragment { + white-space: pre; + word-wrap: initial; + line-height: var(--fragment-lineheight); +} + +div.fragment span.keyword { + color: var(--fragment-keyword); +} + +div.fragment span.keywordtype { + color: var(--fragment-keywordtype); +} + +div.fragment span.keywordflow { + color: var(--fragment-keywordflow); +} + +div.fragment span.stringliteral { + color: var(--fragment-token) +} + +div.fragment span.comment { + color: var(--fragment-comment); +} + +div.fragment a.code { + color: var(--fragment-link) !important; +} + +div.fragment span.preprocessor { + color: var(--fragment-preprocessor); +} + +div.fragment span.lineno { + display: inline-block; + width: 27px; + border-right: none; + background: var(--fragment-linenumber-background); + color: var(--fragment-linenumber-color); +} + +div.fragment span.lineno a { + background: none; + color: var(--fragment-link) !important; +} + +div.fragment .line:first-child .lineno { + box-shadow: -999999px 0px 0 999999px var(--fragment-linenumber-background), -999998px 0px 0 999999px var(--fragment-linenumber-border); +} + +/* + dl warning, attention, note, deprecated, bug, ... + */ + +dl.bug dt a, dl.deprecated dt a, dl.todo dt a { + font-weight: bold !important; +} + +dl.warning, dl.attention, dl.note, dl.deprecated, dl.bug, dl.invariant, dl.pre, dl.todo, dl.remark { + padding: var(--spacing-medium); + margin: var(--spacing-medium) 0; + color: var(--page-background-color); + overflow: hidden; + margin-left: 0; + border-radius: var(--border-radius-small); +} + +dl.section dd { + margin-bottom: 2px; +} + +dl.warning, dl.attention { + background: var(--warning-color); + border-left: 8px solid var(--warning-color-dark); + color: var(--warning-color-darker); +} + +dl.warning dt, dl.attention dt { + color: var(--warning-color-dark); +} + +dl.note, dl.remark { + background: var(--note-color); + border-left: 8px solid var(--note-color-dark); + color: var(--note-color-darker); +} + +dl.note dt, dl.remark dt { + color: var(--note-color-dark); +} + +dl.todo { + background: var(--todo-color); + border-left: 8px solid var(--todo-color-dark); + color: var(--todo-color-darker); +} + +dl.todo dt { + color: var(--todo-color-dark); +} + +dl.bug dt a { + color: var(--todo-color-dark) !important; +} + +dl.bug { + background: var(--bug-color); + border-left: 8px solid var(--bug-color-dark); + color: var(--bug-color-darker); +} + +dl.bug dt a { + color: var(--bug-color-dark) !important; +} + +dl.deprecated { + background: var(--deprecated-color); + border-left: 8px solid var(--deprecated-color-dark); + color: var(--deprecated-color-darker); +} + +dl.deprecated dt a { + color: var(--deprecated-color-dark) !important; +} + +dl.section dd, dl.bug dd, dl.deprecated dd, dl.todo dd { + margin-inline-start: 0px; +} + +dl.invariant, dl.pre { + background: var(--invariant-color); + border-left: 8px solid var(--invariant-color-dark); + color: var(--invariant-color-darker); +} + +dl.invariant dt, dl.pre dt { + color: var(--invariant-color-dark); +} + +/* + memitem + */ + +div.memdoc, div.memproto, h2.memtitle { + box-shadow: none; + background-image: none; + border: none; +} + +div.memdoc { + padding: 0 var(--spacing-medium); + background: var(--page-background-color); +} + +h2.memtitle, div.memitem { + border: 1px solid var(--separator-color); + box-shadow: var(--box-shadow); +} + +h2.memtitle { + box-shadow: 0px var(--spacing-medium) 0 -1px var(--fragment-background), var(--box-shadow); +} + +div.memitem { + transition: none; +} + +div.memproto, h2.memtitle { + background: var(--fragment-background); + text-shadow: none; +} + +h2.memtitle { + font-weight: 500; + font-size: var(--memtitle-font-size); + font-family: var(--font-family-monospace); + border-bottom: none; + border-top-left-radius: var(--border-radius-medium); + border-top-right-radius: var(--border-radius-medium); + word-break: break-all; + position: relative; +} + +h2.memtitle:after { + content: ""; + display: block; + background: var(--fragment-background); + height: var(--spacing-medium); + bottom: calc(0px - var(--spacing-medium)); + left: 0; + right: -14px; + position: absolute; + border-top-right-radius: var(--border-radius-medium); +} + +h2.memtitle > span.permalink { + font-size: inherit; +} + +h2.memtitle > span.permalink > a { + text-decoration: none; + padding-left: 3px; + margin-right: -4px; + user-select: none; + display: inline-block; + margin-top: -6px; +} + +h2.memtitle > span.permalink > a:hover { + color: var(--primary-dark-color) !important; +} + +a:target + h2.memtitle, a:target + h2.memtitle + div.memitem { + border-color: var(--primary-light-color); +} + +div.memitem { + border-top-right-radius: var(--border-radius-medium); + border-bottom-right-radius: var(--border-radius-medium); + border-bottom-left-radius: var(--border-radius-medium); + overflow: hidden; + display: block !important; +} + +div.memdoc { + border-radius: 0; +} + +div.memproto { + border-radius: 0 var(--border-radius-small) 0 0; + overflow: auto; + border-bottom: 1px solid var(--separator-color); + padding: var(--spacing-medium); + margin-bottom: -1px; +} + +div.memtitle { + border-top-right-radius: var(--border-radius-medium); + border-top-left-radius: var(--border-radius-medium); +} + +div.memproto table.memname { + font-family: var(--font-family-monospace); + color: var(--page-foreground-color); + font-size: var(--memname-font-size); +} + +div.memproto div.memtemplate { + font-family: var(--font-family-monospace); + color: var(--primary-dark-color); + font-size: var(--memname-font-size); + margin-left: 2px; +} + +table.mlabels, table.mlabels > tbody { + display: block; +} + +td.mlabels-left { + width: auto; +} + +td.mlabels-right { + margin-top: 3px; + position: sticky; + left: 0; +} + +table.mlabels > tbody > tr:first-child { + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +.memname, .memitem span.mlabels { + margin: 0 +} + +/* + reflist + */ + +dl.reflist { + box-shadow: var(--box-shadow); + border-radius: var(--border-radius-medium); + border: 1px solid var(--separator-color); + overflow: hidden; + padding: 0; +} + + +dl.reflist dt, dl.reflist dd { + box-shadow: none; + text-shadow: none; + background-image: none; + border: none; + padding: 12px; +} + + +dl.reflist dt { + font-weight: 500; + border-radius: 0; + background: var(--code-background); + border-bottom: 1px solid var(--separator-color); + color: var(--page-foreground-color) +} + + +dl.reflist dd { + background: none; +} + +/* + Table + */ + +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) { + display: inline-block; + max-width: 100%; + } + +.contents > table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname):not(.classindex) { + margin-left: calc(0px - var(--spacing-large)); + margin-right: calc(0px - var(--spacing-large)); + max-width: calc(100% + 2 * var(--spacing-large)); +} + +table.markdownTable, table.fieldtable { + border: none; + margin: var(--spacing-medium) 0; + box-shadow: 0 0 0 1px var(--separator-color); + border-radius: var(--border-radius-small); +} + +table.fieldtable { + width: 100%; +} + +th.markdownTableHeadLeft, th.markdownTableHeadRight, th.markdownTableHeadCenter, th.markdownTableHeadNone { + background: var(--tablehead-background); + color: var(--tablehead-foreground); + font-weight: 600; + font-size: var(--page-font-size); +} + +th.markdownTableHeadLeft:first-child, th.markdownTableHeadRight:first-child, th.markdownTableHeadCenter:first-child, th.markdownTableHeadNone:first-child { + border-top-left-radius: var(--border-radius-small); +} + +th.markdownTableHeadLeft:last-child, th.markdownTableHeadRight:last-child, th.markdownTableHeadCenter:last-child, th.markdownTableHeadNone:last-child { + border-top-right-radius: var(--border-radius-small); +} + +table.markdownTable td, table.markdownTable th, table.fieldtable dt { + border: none; + border-right: 1px solid var(--separator-color); + padding: var(--spacing-small) var(--spacing-medium); +} + +table.markdownTable td:last-child, table.markdownTable th:last-child, table.fieldtable dt:last-child { + border: none; +} + +table.markdownTable tr, table.markdownTable tr { + border-bottom: 1px solid var(--separator-color); +} + +table.markdownTable tr:last-child, table.markdownTable tr:last-child { + border-bottom: none; +} + +table.fieldtable th { + font-size: var(--page-font-size); + font-weight: 600; + background-image: none; + background-color: var(--tablehead-background); + color: var(--tablehead-foreground); + border-bottom: 1px solid var(--separator-color); +} + +.fieldtable td.fieldtype, .fieldtable td.fieldname { + border-bottom: 1px solid var(--separator-color); + border-right: 1px solid var(--separator-color); +} + +.fieldtable td.fielddoc { + border-bottom: 1px solid var(--separator-color); +} + +.memberdecls td.glow, .fieldtable tr.glow { + background-color: var(--primary-light-color); + box-shadow: 0 0 15px var(--primary-light-color); +} + +table.memberdecls { + display: block; +} + +table.memberdecls tr[class^='memitem'] { + font-family: var(--font-family-monospace); + font-size: var(--code-font-size); +} + +table.memberdecls tr[class^='memitem'] .memTemplParams { + font-family: var(--font-family-monospace); + font-size: var(--code-font-size); + color: var(--primary-dark-color); +} + +table.memberdecls .memItemLeft, +table.memberdecls .memItemRight, +table.memberdecls .memTemplItemLeft, +table.memberdecls .memTemplItemRight, +table.memberdecls .memTemplParams { + transition: none; + padding-top: var(--spacing-small); + padding-bottom: var(--spacing-small); + border-top: 1px solid var(--separator-color); + border-bottom: 1px solid var(--separator-color); + background-color: var(--fragment-background); +} + +table.memberdecls .memTemplItemLeft, +table.memberdecls .memTemplItemRight { + padding-top: 2px; +} + +table.memberdecls .memTemplParams { + border-bottom: 0; + border-left: 1px solid var(--separator-color); + border-right: 1px solid var(--separator-color); + border-radius: var(--border-radius-small) var(--border-radius-small) 0 0; + padding-bottom: 0; +} + +table.memberdecls .memTemplItemLeft { + border-radius: 0 0 0 var(--border-radius-small); + border-left: 1px solid var(--separator-color); + border-top: 0; +} + +table.memberdecls .memTemplItemRight { + border-radius: 0 0 var(--border-radius-small) 0; + border-right: 1px solid var(--separator-color); + border-top: 0; +} + +table.memberdecls .memItemLeft { + border-radius: var(--border-radius-small) 0 0 var(--border-radius-small); + border-left: 1px solid var(--separator-color); + padding-left: var(--spacing-medium); + padding-right: 0; +} + +table.memberdecls .memItemRight { + border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0; + border-right: 1px solid var(--separator-color); + padding-right: var(--spacing-medium); + padding-left: 0; + +} + +table.memberdecls .mdescLeft, table.memberdecls .mdescRight { + background: none; + color: var(--page-foreground-color); + padding: var(--spacing-small) 0; +} + +table.memberdecls .memSeparator { + background: var(--page-background-color); + height: var(--spacing-large); + border: 0; + transition: none; +} + +table.memberdecls .groupheader { + margin-bottom: var(--spacing-large); +} + +table.memberdecls .inherit_header td { + padding: 0 0 var(--spacing-medium) 0; + text-indent: -12px; + line-height: 1.5em; + color: var(--page-secondary-foreground-color); +} + +@media screen and (max-width: 767px) { + + table.memberdecls .memItemLeft, + table.memberdecls .memItemRight, + table.memberdecls .mdescLeft, + table.memberdecls .mdescRight, + table.memberdecls .memTemplItemLeft, + table.memberdecls .memTemplItemRight, + table.memberdecls .memTemplParams { + display: block; + text-align: left; + padding-left: var(--spacing-large); + margin: 0 calc(0px - var(--spacing-large)) 0 calc(0px - var(--spacing-large)); + border-right: none; + border-left: none; + border-radius: 0; + } + + table.memberdecls .memItemLeft, + table.memberdecls .mdescLeft, + table.memberdecls .memTemplItemLeft { + border-bottom: 0; + padding-bottom: 0; + } + + table.memberdecls .memTemplItemLeft { + padding-top: 0; + } + + table.memberdecls .mdescLeft { + margin-top: calc(0px - var(--page-font-size)); + } + + table.memberdecls .memItemRight, + table.memberdecls .mdescRight, + table.memberdecls .memTemplItemRight { + border-top: 0; + padding-top: 0; + padding-right: var(--spacing-large); + overflow-x: auto; + } + + table.memberdecls tr[class^='memitem']:not(.inherit) { + display: block; + width: calc(100vw - 2 * var(--spacing-large)); + } + + table.memberdecls .mdescRight { + color: var(--page-foreground-color); + } + + table.memberdecls tr.inherit { + visibility: hidden; + } + + table.memberdecls tr[style="display: table-row;"] { + display: block !important; + visibility: visible; + width: calc(100vw - 2 * var(--spacing-large)); + animation: fade .5s; + } + + @keyframes fade { + 0% { + opacity: 0; + max-height: 0; + } + + 100% { + opacity: 1; + max-height: 200px; + } + } +} + + +/* + Horizontal Rule + */ + +hr { + margin-top: var(--spacing-large); + margin-bottom: var(--spacing-large); + height: 1px; + background-color: var(--separator-color); + border: 0; +} + +.contents hr { + box-shadow: 100px 0 0 var(--separator-color), + -100px 0 0 var(--separator-color), + 500px 0 0 var(--separator-color), + -500px 0 0 var(--separator-color), + 1500px 0 0 var(--separator-color), + -1500px 0 0 var(--separator-color), + 2000px 0 0 var(--separator-color), + -2000px 0 0 var(--separator-color); +} + +.contents img, .contents .center, .contents center, .contents div.image object { + max-width: 100%; + overflow: auto; +} + +@media screen and (max-width: 767px) { + .contents .dyncontent > .center, .contents > center { + margin-left: calc(0px - var(--spacing-large)); + margin-right: calc(0px - var(--spacing-large)); + max-width: calc(100% + 2 * var(--spacing-large)); + } +} + +/* + Directories + */ +div.directory { + border-top: 1px solid var(--separator-color); + border-bottom: 1px solid var(--separator-color); + width: auto; +} + +table.directory { + font-family: var(--font-family); + font-size: var(--page-font-size); + font-weight: normal; + width: 100%; +} + +table.directory td.entry { + padding: var(--spacing-small); +} + +table.directory td.desc { + min-width: 250px; +} + +table.directory tr.even { + background-color: var(--odd-color); +} + +.icona { + width: auto; + height: auto; + margin: 0 var(--spacing-small); +} + +.icon { + background: var(--primary-color); + width: 18px; + height: 18px; + line-height: 18px; +} + +.iconfopen, .icondoc, .iconfclosed { + background-position: center; + margin-bottom: 0; +} + +.icondoc { + filter: saturate(0.2); +} + +@media screen and (max-width: 767px) { + div.directory { + margin-left: calc(0px - var(--spacing-large)); + margin-right: calc(0px - var(--spacing-large)); + } +} + +@media (prefers-color-scheme: dark) { + html:not(.light-mode) .iconfopen, html:not(.light-mode) .iconfclosed { + filter: hue-rotate(180deg) invert(); + } +} + +html.dark-mode .iconfopen, html.dark-mode .iconfclosed { + filter: hue-rotate(180deg) invert(); +} + +/* + Class list + */ + +.classindex dl.odd { + background: var(--odd-color); + border-radius: var(--border-radius-small); +} + +/* + Class Index Doxygen 1.8 +*/ + +table.classindex { + margin-left: 0; + margin-right: 0; + width: 100%; +} + +table.classindex table div.ah { + background-image: none; + background-color: initial; + border-color: var(--separator-color); + color: var(--page-foreground-color); + box-shadow: var(--box-shadow); + border-radius: var(--border-radius-large); + padding: var(--spacing-small); +} + +div.qindex { + background-color: var(--odd-color); + border-radius: var(--border-radius-small); + border: 1px solid var(--separator-color); + padding: var(--spacing-small) 0; +} + +/* + Footer and nav-path + */ + +#nav-path { + width: 100%; +} + +#nav-path ul { + background-image: none; + background: var(--page-background-color); + border: none; + border-top: 1px solid var(--separator-color); + border-bottom: 1px solid var(--separator-color); + border-bottom: 0; + box-shadow: 0 0.75px 0 var(--separator-color); + font-size: var(--navigation-font-size); +} + +img.footer { + width: 60px; +} + +.navpath li.footer { + color: var(--page-secondary-foreground-color); +} + +address.footer { + color: var(--page-secondary-foreground-color); + margin-bottom: var(--spacing-large); +} + +#nav-path li.navelem { + background-image: none; + display: flex; + align-items: center; +} + +.navpath li.navelem a { + text-shadow: none; + display: inline-block; + color: var(--primary-color) !important; +} + +.navpath li.navelem b { + color: var(--primary-dark-color); + font-weight: 500; +} + +li.navelem { + padding: 0; + margin-left: -8px; +} + +li.navelem:first-child { + margin-left: var(--spacing-large); +} + +li.navelem:first-child:before { + display: none; +} + +#nav-path li.navelem:after { + content: ''; + border: 5px solid var(--page-background-color); + border-bottom-color: transparent; + border-right-color: transparent; + border-top-color: transparent; + transform: translateY(-1px) scaleY(4.2); + z-index: 10; + margin-left: 6px; +} + +#nav-path li.navelem:before { + content: ''; + border: 5px solid var(--separator-color); + border-bottom-color: transparent; + border-right-color: transparent; + border-top-color: transparent; + transform: translateY(-1px) scaleY(3.2); + margin-right: var(--spacing-small); +} + +.navpath li.navelem a:hover { + color: var(--primary-color); +} + +/* + Scrollbars for Webkit +*/ + +#nav-tree::-webkit-scrollbar, +div.fragment::-webkit-scrollbar, +pre.fragment::-webkit-scrollbar, +div.memproto::-webkit-scrollbar, +.contents center::-webkit-scrollbar, +.contents .center::-webkit-scrollbar, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname)::-webkit-scrollbar { + width: calc(var(--webkit-scrollbar-size) + var(--webkit-scrollbar-padding) + var(--webkit-scrollbar-padding)); + height: calc(var(--webkit-scrollbar-size) + var(--webkit-scrollbar-padding) + var(--webkit-scrollbar-padding)); +} + +#nav-tree::-webkit-scrollbar-thumb, +div.fragment::-webkit-scrollbar-thumb, +pre.fragment::-webkit-scrollbar-thumb, +div.memproto::-webkit-scrollbar-thumb, +.contents center::-webkit-scrollbar-thumb, +.contents .center::-webkit-scrollbar-thumb, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname)::-webkit-scrollbar-thumb { + background-color: transparent; + border: var(--webkit-scrollbar-padding) solid transparent; + border-radius: calc(var(--webkit-scrollbar-padding) + var(--webkit-scrollbar-padding)); + background-clip: padding-box; +} + +#nav-tree:hover::-webkit-scrollbar-thumb, +div.fragment:hover::-webkit-scrollbar-thumb, +pre.fragment:hover::-webkit-scrollbar-thumb, +div.memproto:hover::-webkit-scrollbar-thumb, +.contents center:hover::-webkit-scrollbar-thumb, +.contents .center:hover::-webkit-scrollbar-thumb, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname):hover::-webkit-scrollbar-thumb { + background-color: var(--webkit-scrollbar-color); +} + +#nav-tree::-webkit-scrollbar-track, +div.fragment::-webkit-scrollbar-track, +pre.fragment::-webkit-scrollbar-track, +div.memproto::-webkit-scrollbar-track, +.contents center::-webkit-scrollbar-track, +.contents .center::-webkit-scrollbar-track, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname)::-webkit-scrollbar-track { + background: transparent; +} + +#nav-tree::-webkit-scrollbar-corner { + background-color: var(--side-nav-background); +} + +#nav-tree, +div.fragment, +pre.fragment, +div.memproto, +.contents center, +.contents .center, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) { + overflow-x: auto; + overflow-x: overlay; +} + +#nav-tree { + overflow-x: auto; + overflow-y: auto; + overflow-y: overlay; +} + +/* + Scrollbars for Firefox +*/ + +#nav-tree, +div.fragment, +pre.fragment, +div.memproto, +.contents center, +.contents .center, +.contents table:not(.memberdecls):not(.mlabels):not(.fieldtable):not(.memname) { + scrollbar-width: thin; +} + +/* + Optional Dark mode toggle button +*/ + +doxygen-awesome-dark-mode-toggle { + display: inline-block; + margin: 0 0 0 var(--spacing-small); + padding: 0; + width: var(--searchbar-height); + height: var(--searchbar-height); + background: none; + border: none; + border-radius: var(--searchbar-height); + vertical-align: middle; + text-align: center; + line-height: var(--searchbar-height); + font-size: 22px; + display: flex; + align-items: center; + justify-content: center; + user-select: none; + cursor: pointer; +} + +doxygen-awesome-dark-mode-toggle > svg { + transition: transform .1s ease-in-out; +} + +doxygen-awesome-dark-mode-toggle:active > svg { + transform: scale(.5); +} + +doxygen-awesome-dark-mode-toggle:hover { + background-color: rgba(0,0,0,.03); +} + +html.dark-mode doxygen-awesome-dark-mode-toggle:hover { + background-color: rgba(0,0,0,.18); +} + +/* + Optional fragment copy button +*/ +.doxygen-awesome-fragment-wrapper { + position: relative; +} + +doxygen-awesome-fragment-copy-button { + opacity: 0; + background: var(--fragment-background); + width: 28px; + height: 28px; + position: absolute; + right: calc(var(--spacing-large) - (var(--spacing-large) / 2.5)); + top: calc(var(--spacing-large) - (var(--spacing-large) / 2.5)); + border: 1px solid var(--fragment-foreground); + cursor: pointer; + border-radius: var(--border-radius-small); + display: flex; + justify-content: center; + align-items: center; +} + +.doxygen-awesome-fragment-wrapper:hover doxygen-awesome-fragment-copy-button, doxygen-awesome-fragment-copy-button.success { + opacity: .28; +} + +doxygen-awesome-fragment-copy-button:hover, doxygen-awesome-fragment-copy-button.success { + opacity: 1 !important; +} + +doxygen-awesome-fragment-copy-button:active:not([class~=success]) svg { + transform: scale(.91); +} + +doxygen-awesome-fragment-copy-button svg { + fill: var(--fragment-foreground); + width: 18px; + height: 18px; +} + +doxygen-awesome-fragment-copy-button.success svg { + fill: rgb(14, 168, 14); +} + +doxygen-awesome-fragment-copy-button.success { + border-color: rgb(14, 168, 14); +} + +@media screen and (max-width: 767px) { + .textblock > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button, + .textblock li > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button, + .memdoc li > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button, + .memdoc > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button, + dl dd > .doxygen-awesome-fragment-wrapper > doxygen-awesome-fragment-copy-button { + right: 0; + } +} + +/* + Optional paragraph link button +*/ + +a.anchorlink { + font-size: 90%; + margin-left: var(--spacing-small); + color: var(--page-foreground-color) !important; + text-decoration: none; + opacity: .15; + display: none; + transition: opacity .1s ease-in-out, color .1s ease-in-out; +} + +a.anchorlink svg { + fill: var(--page-foreground-color); +} + +h3 a.anchorlink svg, h4 a.anchorlink svg { + margin-bottom: -3px; + margin-top: -4px; +} + +a.anchorlink:hover { + opacity: .45; +} + +h2:hover a.anchorlink, h1:hover a.anchorlink, h3:hover a.anchorlink, h4:hover a.anchorlink { + display: inline-block; +} diff --git a/libraries/vircadia-client/docs/mainpage.md b/libraries/vircadia-client/docs/mainpage.md new file mode 100644 index 00000000000..22c0971091b --- /dev/null +++ b/libraries/vircadia-client/docs/mainpage.md @@ -0,0 +1,10 @@ +### Summary + +C API/ABI documentation for [Vircadia Client Library](https://github.com/vircadia/vircadia/blob/unity-sdk/libraries/vircadia-client). The main library id divided into several headers that each provide APIs to specific component: +* context.h API for initialization of the library context/state that is used by all other components. +* node_list.h API for connecting to a given server and enumerating nodes. +* messages.h API for communicating with the message mixer node. +* avatars.h API for communicating with the avatar mixer node. +* audio.h API for communicating with the audio mixer node. +* client.h The main library header that included all components. + diff --git a/libraries/vircadia-client/src/audio.cpp b/libraries/vircadia-client/src/audio.cpp new file mode 100644 index 00000000000..f791d957b53 --- /dev/null +++ b/libraries/vircadia-client/src/audio.cpp @@ -0,0 +1,203 @@ +// +// audio.cpp +// libraries/vircadia-client/src +// +// Created by Nshan G. on 9 July 2022. +// 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 +// + +#include "audio.h" + + +#include "internal/audio/AudioClient.h" +#include "internal/Error.h" +#include "internal/Context.h" + +using namespace vircadia::client; + + +VIRCADIA_CLIENT_DYN_API +int vircadia_enable_audio(int context_id) { + return chain(checkContextReady(context_id), [&](auto) { + std::next(std::begin(contexts), context_id)->audio().enable(); + return 0; + }); +} + +int checkAudioEnabled(int id) { + return chain(checkContextReady(id), [&](auto) { + return std::next(std::begin(contexts), id)->audio().isEnabled() + ? 0 + : toInt(ErrorCode::AUDIO_DISABLED); + }); +} + +int validateAudioFormat(const vircadia_audio_format& format, bool input = false) { + if ( + !(format.sample_type == AudioFormat::Signed16 || format.sample_type == AudioFormat::Float) || + format.sample_rate <= 0 || + format.channel_count <= 0 || + (input && format.channel_count > 2) + ) { + return toInt(ErrorCode::AUDIO_FORMAT_INVALID); + } else { + return 0; + } +} + +VIRCADIA_CLIENT_DYN_API +const char* vircadia_get_selected_audio_codec_name(int context_id) { + return chain(checkAudioEnabled(context_id), [&](auto) { + return std::next(std::begin(contexts), context_id)->audio().getSelectedCodecName().c_str(); + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_codec_params(int context_id, const char* codec, vircadia_audio_codec_params params) { + return chain(checkAudioEnabled(context_id), [&](auto) { + auto& audio = std::next(std::begin(contexts), context_id)->audio(); + if (audio.setCodecAllowed(codec, params.allowed)) { + return 0; + } else { + return toInt(ErrorCode::AUDIO_CODEC_INVALID); + } + }); +} + +AudioFormat audioFormatFrom(const vircadia_audio_format& format) { + AudioFormat result{}; + result.sampleType = AudioFormat::SampleTag(format.sample_type); + result.sampleRate = format.sample_rate; + result.channelCount = format.channel_count; + return result; +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_input_format(int context_id, vircadia_audio_format format) { + return chain({ + checkAudioEnabled(context_id), + validateAudioFormat(format, true) + }, [&](auto) { + std::next(std::begin(contexts), context_id)->audio().setInput(audioFormatFrom(format)); + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_output_format(int context_id, vircadia_audio_format format) { + return chain({ + checkAudioEnabled(context_id), + validateAudioFormat(format) + }, [&](auto) { + std::next(std::begin(contexts), context_id)->audio().setOutput(audioFormatFrom(format)); + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +uint8_t* vircadia_get_audio_input_context(int context_id) { + return chain(checkAudioEnabled(context_id), [&](auto) { + auto& audio = std::next(std::begin(contexts), context_id)->audio(); + return reinterpret_cast(audio.getInputContext()); + }); +} + +VIRCADIA_CLIENT_DYN_API +uint8_t* vircadia_get_audio_output_context(int context_id) { + return chain(checkAudioEnabled(context_id), [&](auto) { + auto& audio = std::next(std::begin(contexts), context_id)->audio(); + return reinterpret_cast(audio.getOutputContext()); + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_input_data(uint8_t* audio_context, const uint8_t* data, int size) { + auto audioClient = reinterpret_cast(audio_context); + if (data == nullptr || size < 0) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } else if (audioClient == nullptr) { + return toInt(ErrorCode::AUDIO_CONTEXT_INVALID); + } else { + audioClient->handleMicAudioInput(reinterpret_cast(data), size); + return 0; + } +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_audio_output_data(uint8_t* audio_context, uint8_t* data, int size) { + auto audioClient = reinterpret_cast(audio_context); + if (data == nullptr || size < 0) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } else if (audioClient == nullptr) { + return toInt(ErrorCode::AUDIO_CONTEXT_INVALID); + } + + int bytesWritten = audioClient->getOutputIODevice().read(reinterpret_cast(data), size); + if (bytesWritten== -1) { + return toInt(ErrorCode::AUDIO_CONTEXT_INVALID); + } + + return bytesWritten; +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_bounds(int context_id, vircadia_bounds bounds) { + return chain(checkAudioEnabled(context_id), [&](auto) { + std::next(std::begin(contexts), context_id)->audio().setBounds(bounds); + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_vantage(int context_id, vircadia_vantage vantage) { + return chain(checkAudioEnabled(context_id), [&](auto) { + std::next(std::begin(contexts), context_id)->audio().setVantage(vantage); + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_input_echo(int context_id, uint8_t enabled) { + return chain(checkAudioEnabled(context_id), [&](auto) { + std::next(std::begin(contexts), context_id)->audio().setInputEcho(enabled); + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_output_buffer_frames(int context_id, int frames) { + return chain(checkAudioEnabled(context_id), [&](auto) { + std::next(std::begin(contexts), context_id)->audio().setOutputBufferFrames(frames); + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_mixer_injector_gain(int context_id, float gain) { + return chain(checkAudioEnabled(context_id), [&](auto) { + // TODO: maybe validate gain range? + std::next(std::begin(contexts), context_id)->audio().setServerInjectorGain(gain); + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_input_muted(int context_id, uint8_t muted) { + return chain(checkAudioEnabled(context_id), [&](auto) { + std::next(std::begin(contexts), context_id)->audio().setIsMuted(muted); + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_audio_input_muted_by_mixer(int context_id) { + return chain(checkAudioEnabled(context_id), [&](auto) { + return std::next(std::begin(contexts), context_id)->audio().getIsMutedByMixer() ? 1 : 0; + }); +} + + diff --git a/libraries/vircadia-client/src/audio.h b/libraries/vircadia-client/src/audio.h new file mode 100644 index 00000000000..09fdda78180 --- /dev/null +++ b/libraries/vircadia-client/src/audio.h @@ -0,0 +1,337 @@ +// +// audio.h +// libraries/vircadia-client/src +// +// Created by Nshan G. on 9 July 2022. +// 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 +// + +/// @file + +#ifndef VIRCADIA_LIBRARIES_VIRCADIA_CLIENT_SRC_AUDIO_H +#define VIRCADIA_LIBRARIES_VIRCADIA_CLIENT_SRC_AUDIO_H + +#include + +#include "common.h" +#include "geometry.h" + +/// @brief Basic information of PCM audio data. +/// +/// Used to specify the format of audio input/output +/// (vircadia_set_audio_input_format()/vircadia_set_audio_output_format()). +/// For multiple channels the audio samples are interleaved in both cases. +struct vircadia_audio_format { + + /// @brief The type of a single sample. + /// + /// Valid values are defined in audio_constants.h + uint8_t sample_type; + + /// @brief The rate at which samples are processed. + /// + /// Specified as number of samples per second (Hz). This can be set to any + /// reasonable value (usually 8000-96000), typically 48000. The + /// input/output data will be re-sampled to/from desired network sample rate. + int sample_rate; + + /// @brief The number of channels. + /// + /// Input supports only mono(1 channel) and stereo(2 channels), and these + /// are sent to the mixer as is. \n + /// Output supports any value above zero (usually 1-8), but the incoming + /// mixer data is always stereo, so for other channel counts the data is + /// converted in a straight forward fashion (linear downmix, or upmix with + /// extra channels set to 0). + int channel_count; +}; + +/// @brief Audio codec parameters. +/// +/// Used with vircadia_set_audio_codec_params(). +struct vircadia_audio_codec_params { + + /// @brief Dis/allow the use of the codec for commination with the mixer. + /// + /// 0 - disallow \n + /// 1 - allow \n + /// + /// By default all codecs are allowed, and the mixer decides which to use + /// with the client (vircadia_get_selected_audio_codec_name()). This + /// parameter can be used to limit the choices for the mixer. + uint8_t allowed; + + // TODO: implement and document once + // https://github.com/vircadia/vircadia/pull/1582 is merged + uint8_t encoder_vbr; + uint8_t encoder_fec; + int encoder_bitrate; + int encoder_complexity; + int encoder_packet_loss; +}; + +/// @brief Enable handling of audio input and output. +/// +/// To start sending audio, the input the format must be specified with +/// vircadia_set_audio_input_format(), after which the data can be sent with +/// vircadia_set_audio_input_data(), with a input context retrieved from +/// vircadia_get_audio_input_context(). vircadia_set_audio_input_data() can be +/// called from a different thread with a valid input context. The input +/// context is invalidated if a new format is set (then a new context must be +/// retrieved before sending more data) or when entire client context is +/// destroyed. The audio output works in the same way: +/// vircadia_set_audio_output_format() -> vircadia_get_audio_output_context() +/// -> vircadia_get_audio_output_data(). +/// +/// @param context_id - The id of the client context (context.h). +/// +/// @return non-negative value on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +VIRCADIA_CLIENT_DYN_API +int vircadia_enable_audio(int context_id); + +/// @brief Reads the audio codec selected for communication with the mixer. +/// +/// This is determined by the mixer, but the client can control allowed codecs +/// with vircadia_set_audio_codec_params(). \n +/// Possible values are defined in audio_constants.h. +/// +/// @param context_id - The id of the client context (context.h). +/// +/// @return A codec identifier, or an empty string if a codec has not been +/// selected yet, or null if an error occurred. +VIRCADIA_CLIENT_DYN_API +const char* vircadia_get_selected_audio_codec_name(int context_id); + +/// @brief Sets parameters of specified audio codec. +/// +/// @param context_id - The id of the client context (context.h). +/// @param codec - The id of the codec (audio_constants.h). +/// @param params - The codec parameters to set. +/// +/// @return non-negative value on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_audio_disabled() \n +/// vircadia_error_audio_codec_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_codec_params(int context_id, const char* codec, vircadia_audio_codec_params params); + +/// @brief Sets the format of audio input data. +/// +/// Specifically it's the format of the data to be passed to +/// vircadia_set_audio_input_data(). \n +/// This function initiates a creation of new input context and invalidates any +/// previous input context. The new context can be retrieved using +/// vircadia_get_audio_input_context(). +/// +/// @param context_id - The id of the client context (context.h). +/// @param format - The format of audio input data to be sent. +/// +/// @return non-negative value on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_audio_disabled() \n +/// vircadia_error_audio_format_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_input_format(int context_id, vircadia_audio_format format); + +/// @brief Sets the format of audio output data. +/// +/// Specifically it's the format of the data retrieved from +/// vircadia_get_audio_output_data(). \n +/// This function initiates a creation of new output context and invalidates +/// any previous output context. The new context can be retrieved using +/// vircadia_get_audio_output_context(). +/// +/// @param context_id - The id of the client context (context.h). +/// @param format - The format of audio output data to be received. +/// +/// @return non-negative value on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_audio_disabled() \n +/// vircadia_error_audio_format_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_output_format(int context_id, vircadia_audio_format format); + +/// @brief Retrieves audio input context. +/// +/// This function must be polled to retrieve a new valid input context for +/// vircadia_set_audio_input_data(), after every call to +/// vircadia_set_audio_input_format(). +/// +/// @param context_id - The id of the client context (context.h). +/// +/// @return A pointer to the input context, or null if context is not yet +/// created/ready. +VIRCADIA_CLIENT_DYN_API +uint8_t* vircadia_get_audio_input_context(int context_id); + +/// @brief Retrieves audio output context. +/// +/// This function must be polled to retrieve a new valid output context for +/// vircadia_get_audio_output_data(), after every call to +/// vircadia_set_audio_output_format(). +/// +/// @param context_id - The id of the client context (context.h). +/// +/// @return A pointer to the output context, or null if context is not yet +/// created/ready. +VIRCADIA_CLIENT_DYN_API +uint8_t* vircadia_get_audio_output_context(int context_id); + +/// @brief Set the audio input data. +/// +/// Sets the audio input data to be sent to the mixer. The data is buffered +/// internally and sent out periodically on a different thread. This function +/// must be called with a valid audio input context +/// (vircadia_get_audio_input_context()), and data of specified audio format +/// (vircadia_set_audio_input_format()). This function can be called from a +/// different thread, to be used as a realtime audio callback. +/// +/// @param audio_context - The audio context. Must not be null. +/// @param data - The audio data to set. Must not be null. +/// @param size - The size of data to be set in bytes. Must not be negative. +/// +/// @return non-negative value on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_argument_invalid() \n +/// vircadia_error_audio_context_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_input_data(uint8_t* audio_context, const uint8_t* data, int size); + +/// @brief Get the audio output data. +/// +/// Retrieves the audio output data receives from the mixer. The data is +/// buffered internally as it arrives and must be periodically retrieved to not +/// exhaust the buffer. This function must be called with a valid audio output +/// context (vircadia_get_audio_output_context()), and data of specified audio +/// format (vircadia_set_audio_output_format()). This function can be called +/// from a different thread, to be used as a realtime audio callback. +/// +/// @param audio_context - The audio context. Must not be null. +/// @param data - The audio data buffer to read into. Must not be null. +/// @param size - The maximum size of data to read. Must not be negative. +/// +/// @return The number of bytes read, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_argument_invalid() \n +/// vircadia_error_audio_context_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_audio_output_data(uint8_t* audio_context, uint8_t* data, int size); + +/// @brief Set the bounding box of the audio source. +/// +/// @param context_id - The id of the client context (context.h). +/// @param bounds - The bounds to set. +/// +/// @return non-negative value on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_audio_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_bounds(int context_id, vircadia_bounds bounds); + +/// @brief Set the position and rotation of the audio source. +/// +/// @param context_id - The id of the client context (context.h). +/// @param vantage - The position and rotation (the vantage point) to set. +/// +/// @return non-negative value on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_audio_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_vantage(int context_id, vircadia_vantage vantage); + +/// @brief Specify whether the audio input should be sent back to the client. +/// +/// By default the input is not sent back. +/// +/// @param context_id - The id of the client context (context.h). +/// @param enabled - 0 - not sent back, 1 - sent back. +/// +/// @return non-negative value on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_audio_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_input_echo(int context_id, uint8_t enabled); + +/// @brief Set the audio output buffer size based on network frames. +/// +/// A network frame is 480 samples at 24000Hz. The default value is 1. +/// If the caller of vircadia_get_audio_output_data() expects more +/// (taking sample rate conversion into account) this value will need +/// to be increased to avoid starvation (causes stuttering/cracking). +/// +/// @param context_id - The id of the client context (context.h). +/// @param frames - The number of frames to buffer. +/// +/// @return non-negative value on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_audio_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_output_buffer_frames(int context_id, int frames); + +/// @brief Set the audio mixer injector gain/volume. +/// +/// Injector are usually used for environmental sounds. +/// +/// @param context_id - The id of the client context (context.h). +/// @param gain - gain value in decibel (-60dB to 20dB) +/// +/// @return non-negative value on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_audio_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_mixer_injector_gain(int context_id, float gain); + +/// @brief Un/mutes the audio input. +/// +/// By default the input not muted. +/// +/// @param context_id - The id of the client context (context.h). +/// @param muted - 0 - not muted, 1 - muted. +/// +/// @return non-negative value on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_audio_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_audio_input_muted(int context_id, uint8_t muted); + +/// @brief Determines whether this client has been muted by the mixer. +/// +/// @param context_id - The id of the client context (context.h). +/// +/// @return 1 - muted, 0 - not muted, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_audio_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_audio_input_muted_by_mixer(int context_id); + +// TODO: API for injectors (AudioInjectorManager) +// TODO: API for soling, noise gate, noise reduction, avatar gain and mute (AudioPacketHandler) + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/audio_constants.cpp b/libraries/vircadia-client/src/audio_constants.cpp new file mode 100644 index 00000000000..7e51b3ca502 --- /dev/null +++ b/libraries/vircadia-client/src/audio_constants.cpp @@ -0,0 +1,39 @@ +// +// audio_constants.cpp +// libraries/vircadia-client/src +// +// Created by Nshan G. on 12 July 2022. +// 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 +// + +#include "audio_constants.h" + +#include +#include +#include + + +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_audio_sample_type_float() { + return AudioFormat::Float; +} + +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_audio_sample_type_sint16() { + return AudioFormat::Signed16; +} + +VIRCADIA_CLIENT_DYN_API const char* vircadia_audio_codec_opus() { + return OpusCodec::getNameCString(); +} + +VIRCADIA_CLIENT_DYN_API const char* vircadia_audio_codec_pcm() { + return PCMCodec().getNameCString(); +} + +VIRCADIA_CLIENT_DYN_API const char* vircadia_audio_codec_zlib() { + return zLibCodec().getNameCString(); +} + diff --git a/libraries/vircadia-client/src/audio_constants.h b/libraries/vircadia-client/src/audio_constants.h new file mode 100644 index 00000000000..03427825b40 --- /dev/null +++ b/libraries/vircadia-client/src/audio_constants.h @@ -0,0 +1,49 @@ +// +// audio_constants.h +// libraries/vircadia-client/src +// +// Created by Nshan G. on 12 July 2022. +// 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 +// + +/// @file + +#ifndef LIBRARIES_VIRCADIA_CLIENT_SRC_MESSAGE_TYPES_H +#define LIBRARIES_VIRCADIA_CLIENT_SRC_MESSAGE_TYPES_H + +#include + +#include "common.h" + +/// @brief Value indicating floating point sample type of +/// vircadia_audio_format. +/// +/// @return Positive integer. +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_audio_sample_type_float(); + +/// @brief Value indicating signed 16 bit sample type of +/// vircadia_audio_format. +/// +/// @return Positive integer. +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_audio_sample_type_sint16(); + +/// @brief Unique identifier/name of the Opus codec. +/// +/// @return Null terminated string. +VIRCADIA_CLIENT_DYN_API const char* vircadia_audio_codec_opus(); + +/// @brief Unique identifier/name of the raw PCM codec. +/// +/// @return Null terminated string. +VIRCADIA_CLIENT_DYN_API const char* vircadia_audio_codec_pcm(); + +/// @brief Unique identifier/name of the zlib codec. +/// +/// @return Null terminated string. +VIRCADIA_CLIENT_DYN_API const char* vircadia_audio_codec_zlib(); + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/avatar_constants.cpp b/libraries/vircadia-client/src/avatar_constants.cpp new file mode 100644 index 00000000000..36a0709c9f0 --- /dev/null +++ b/libraries/vircadia-client/src/avatar_constants.cpp @@ -0,0 +1,43 @@ +// +// avatar_constants.cpp +// libraries/vircadia-client/src +// +// Created by Nshan G. on 9 June 2022. +// 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 +// + +#include "avatar_constants.h" + +#include + +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_left_hand_pointing() { + return LEFT_HAND_POINTING_FLAG; +} + +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_right_hand_pointing() { + return RIGHT_HAND_POINTING_FLAG; +} + +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_index_finger_pointing() { + return IS_FINGER_POINTING_FLAG; +} + +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_skeleton_root_bone() { + return AvatarSkeletonTrait::BoneType::SkeletonRoot; +} + +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_skeleton_child_bone() { + return AvatarSkeletonTrait::BoneType::SkeletonChild; +} + +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_root_bone() { + return AvatarSkeletonTrait::BoneType::NonSkeletonRoot; +} + +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_child_bone() { + return AvatarSkeletonTrait::BoneType::NonSkeletonChild; +} diff --git a/libraries/vircadia-client/src/avatar_constants.h b/libraries/vircadia-client/src/avatar_constants.h new file mode 100644 index 00000000000..470e7651c0b --- /dev/null +++ b/libraries/vircadia-client/src/avatar_constants.h @@ -0,0 +1,107 @@ +// +// avatar_constants.h +// libraries/vircadia-client/src +// +// Created by Nshan G. on 9 June 2022. +// 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 +// + +/// @file + +#ifndef LIBRARIES_VIRCADIA_CLIENT_SRC_AVATAR_CONSTANTS_H +#define LIBRARIES_VIRCADIA_CLIENT_SRC_AVATAR_CONSTANTS_H + +#include + +#include "common.h" + +/// @brief Flag value indicating that the avatar's left hand is pointing. +/// +/// See vircadia_avatar_additional_flags::hand_state +/// +/// @return 0b1 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_left_hand_pointing(); + +/// @brief Flag value indicating that the avatar's right hand is pointing. +/// +/// See vircadia_avatar_additional_flags::hand_state +/// +/// @return 0b10 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_right_hand_pointing(); + +/// @brief Flag value indicating that the avatar's index finger on either hand +/// is pointing. +/// +/// See vircadia_avatar_additional_flags::hand_state +/// +/// @return 0b100 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_index_finger_pointing(); + +/// @brief The value corresponding to skeleton root bone type. +/// +/// See vircadia_avatar_bone::type +/// +/// @return 0 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_skeleton_root_bone(); + +/// @brief The value corresponding to skeleton child bone type. +/// +/// See vircadia_avatar_bone::type +/// +/// @return 1 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_skeleton_child_bone(); + +/// @brief The value corresponding to non-skeleton root bone type. +/// +/// See vircadia_avatar_bone::type +/// +/// @return 2 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_root_bone(); + +/// @brief The value corresponding to non-skeleton child bone type. +/// +/// See vircadia_avatar_bone::type +/// +/// @return 3 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_child_bone(); + +/// @brief TODO +/// +/// vircadia_get_avatar_disconnection_reason() +/// +/// @return 0 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_disconnection_unknown(); + +/// @brief TODO +/// +/// vircadia_get_avatar_disconnection_reason() +/// +/// @return 1 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_disconnection_normal(); + +/// @brief TODO +/// +/// vircadia_get_avatar_disconnection_reason() +/// +/// @return 2 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_disconnection_ignored(); + +/// @brief TODO +/// +/// vircadia_get_avatar_disconnection_reason() +/// +/// @return 3 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_disconnection_they_entered_bubble(); + +/// @brief TODO +/// +/// vircadia_get_avatar_disconnection_reason() +/// +/// @return 4 +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_avatar_disconnection_you_entered_bubble(); + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/avatars.cpp b/libraries/vircadia-client/src/avatars.cpp new file mode 100644 index 00000000000..168922f86e1 --- /dev/null +++ b/libraries/vircadia-client/src/avatars.cpp @@ -0,0 +1,783 @@ +// +// avatars.cpp +// libraries/vircadia-client/src +// +// Created by Nshan G. on 16 May 2022. +// 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 +// + +#include "avatars.h" + +#include + +#include "internal/Error.h" +#include "internal/Context.h" +#include "internal/Avatars.h" +#include "internal/avatars/AvatarData.h" +#include "internal/DataTypeConversions.h" + + +using namespace vircadia::client; + +using vircadia::client::operator==; + +VIRCADIA_CLIENT_DYN_API +int vircadia_enable_avatars(int context_id) { + return chain(checkContextReady(context_id), [&](auto) { + std::next(std::begin(contexts), context_id)->avatars().enable(); + return 0; + }); +} + +int checkAvatarsEnabled(int id) { + return chain(checkContextReady(id), [&](auto) { + return std::next(std::begin(contexts), id)->avatars().isEnabled() + ? 0 + : toInt(ErrorCode::AVATARS_DISABLED); + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_update_avatars(int context_id) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + std::next(std::begin(contexts), context_id)->avatars().update(); + return 0; + }); +} + +template +int setProperty(int context_id, T value) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + std::next(std::begin(contexts), context_id)->avatars().myAvatar() + .setProperty(value); + return 0; + }); +} + +template +int resizeProperty(int context_id, int size) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + if (size < 0) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } + std::next(std::begin(contexts), context_id)->avatars().myAvatar() + .resizeProperty(size); + return 0; + }); +} + +template +auto validateAvatarIndex(int context_id, int index, F&& f) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + const auto& avatars = std::next(std::begin(contexts), context_id)->avatars().all(); + return chain(checkIndexValid(avatars, index, ErrorCode::AVATAR_INVALID), [&](auto) { + return std::invoke(std::forward(f), *std::next(std::begin(avatars), index)); + }); + }); +} + +template +struct indexChecker { + int operator()(const AvatarData& avatar, int index) const { + return checkIndexValid(avatar.getProperty(), index, error); + } +}; + +template <> +struct indexChecker { + int operator()(const AvatarData& avatar, int index) const { + return checkIndexValid(avatar.getProperty().attachments, index, ErrorCode::AVATAR_ATTACHMENT_INVALID); + } +}; + +template +auto getIndexChecker() { + if constexpr (Index == AvatarData::JointDataIndex || + Index == AvatarData::JointDefaultPoseFlagsIndex) { + return indexChecker{}; + } else if constexpr (Index == AvatarData::SkeletonDataIndex) { + return indexChecker{}; + } else if constexpr (Index == AvatarData::GrabDataIndex) { + return indexChecker{}; + } +} + +template +auto validatePropertyIndex(int context_id, int avatarIndex, int propertyIndex, Check check, F callback) { + return validateAvatarIndex(context_id, avatarIndex, [&](auto& avatar) { + return chain(check(avatar, propertyIndex), [&](auto) { + return callback(avatar); + }); + }); +} + +template +auto validatePropertyIndex(int context_id, int propertyIndex, Check check, F callback) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + auto& avatar = std::next(std::begin(contexts), context_id)->avatars().myAvatar(); + return chain(check(avatar, propertyIndex), [&](auto) { + return callback(avatar); + }); + }); +} + +template +int setProperty(int context_id, int index, Value value) { + return validatePropertyIndex(context_id, index, getIndexChecker(), [&](AvatarData& avatar) { + avatar.setProperty(index, std::move(value)); + return 0; + }); +} + +template +int setIdentity(int context_id, F&& f) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + auto& avatar = std::next(std::begin(contexts), context_id)->avatars().myAvatar(); + constexpr auto index = AvatarData::IdentityIndex; + auto identity = avatar.getProperty(); + f(identity); + avatar.setProperty(identity); + return 0; + }); +} + +int setIdentityFlag(int context_id, AvatarDataPacket::IdentityFlags flag, uint8_t value) { + return setIdentity(context_id, [flag, value](auto& identity){ + if (value) { + identity.identityFlags |= flag; + } else { + identity.identityFlags &= ~flag; + } + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_display_name(int context_id, const char* display_name) { + if (display_name == nullptr) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } + + return setIdentity(context_id, [display_name](auto& identity){ + identity.displayName = display_name; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_is_replicated(int context_id, uint8_t is_replicated) { + return setIdentityFlag(context_id, + AvatarDataPacket::IdentityFlag::isReplicated, is_replicated); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_look_at_snapping(int context_id, uint8_t look_at_snapping_enabled) { + return setIdentityFlag(context_id, + AvatarDataPacket::IdentityFlag::lookAtSnapping, look_at_snapping_enabled); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_verification(int context_id, uint8_t verification_failed) { + return setIdentityFlag(context_id, + AvatarDataPacket::IdentityFlag::verificationFailed, verification_failed); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_attachment_count(int context_id, int attachment_count) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + auto& avatar = std::next(std::begin(contexts), context_id)->avatars().myAvatar(); + if (attachment_count < 0) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } + avatar.setProperty( overloaded { + [](auto& identity) -> int { + return identity.attachments.size(); + }, + [](auto& identity, auto&& count) { + identity.attachments.resize(count); + } + }, attachment_count); + return 0; + }); +} + +AvatarData::Attachment toAttachment(vircadia_avatar_attachment x) { + return { + x.model_url, + x.joint_name, + x.transform, + bool(x.is_soft) + }; +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_attachment(int context_id, int attachment_index, vircadia_avatar_attachment attachment) { + if (attachment.model_url == nullptr || attachment.joint_name == nullptr) { + return toInt(ErrorCode::AVATAR_INVALID); + } + return chain(checkAvatarsEnabled(context_id), [&](auto) { + auto& avatar = std::next(std::begin(contexts), context_id)->avatars().myAvatar(); + return chain(indexChecker{}(avatar, attachment_index), [&](auto) { + avatar.setProperty( overloaded { + [attachment_index](auto& identity) -> decltype(auto) { + return (identity.attachments[attachment_index]); + }, + [attachment_index](auto& identity, auto&& attachment) { + identity.attachments[attachment_index] = std::forward(attachment); + } + }, toAttachment(attachment)); + return 0; + }); + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_attachments(int context_id, const vircadia_avatar_attachment* attachments, int size) { + return setIdentity(context_id, [&](auto& identity) { + if (size < 0 || (size != 0 && attachments == nullptr)) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } + identity.attachments.clear(); + for (int i = 0; i != size; ++i) { + identity.attachments.push_back(toAttachment(attachments[i])); + } + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_session_display_name(int context_id, const char* session_display_name) { + return setIdentity(context_id, [session_display_name](auto& identity){ + identity.sessionDisplayName = session_display_name; + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_skeleton_model_url(int context_id, const char* skeleton_model_url) { + if (skeleton_model_url == nullptr) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } + return setProperty(context_id, skeleton_model_url); +} + +AvatarData::Bone toBone(vircadia_avatar_bone bone) { + return { + AvatarSkeletonTrait::BoneType(bone.type), + bone.default_transform, + bone.index, + bone.parent_index, + bone.name + }; +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_bone_count(int context_id, int bone_count) { + return resizeProperty(context_id, bone_count); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_bone(int context_id, int bone_index, vircadia_avatar_bone bone) { + if (bone.name == nullptr) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } + return setProperty( + context_id, bone_index, toBone(bone)); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_skeleton_data(int context_id, vircadia_avatar_bone* data, int size) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + auto& avatar = std::next(std::begin(contexts), context_id)->avatars().myAvatar(); + constexpr auto index = AvatarData::SkeletonDataIndex; + if (size < 0 || (size != 0 && data == nullptr)) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } + avatar.resizeProperty(size); + for (int i = 0; i != size; ++i) { + avatar.setProperty(i, toBone(data[i])); + } + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_global_position(int context_id, vircadia_vector position) { + return setProperty(context_id, position); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_bounding_box(int context_id, vircadia_bounds bounding_box) { + return setProperty(context_id, bounding_box); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_orientation(int context_id, vircadia_quaternion orientation) { + return setProperty(context_id, orientation); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_scale(int context_id, float scale) { + return setProperty(context_id, scale); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_look_at(int context_id, vircadia_vector position) { + return setProperty(context_id, position); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_audio_loudness(int context_id, float loudness) { + return setProperty(context_id, loudness); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_sensor_to_world(int context_id, vircadia_transform transform) { + return setProperty(context_id, transform); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_additional_flags(int context_id, vircadia_avatar_additional_flags flags) { + return setProperty(context_id, flags); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_parent_info(int context_id, vircadia_avatar_parent_info info) { + return setProperty(context_id, info); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_local_position(int context_id, vircadia_vector position) { + return setProperty(context_id, position); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_hand_controllers(int context_id, vircadia_avatar_hand_controllers controllers) { + return setProperty(context_id, controllers); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_face_tracker_info(int context_id, vircadia_avatar_face_tracker_info info) { + return setProperty(context_id, info); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_joint_count(int context_id, int joint_count) { + return resizeProperty(context_id, joint_count); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_joint(int context_id, int joint_index, vircadia_vantage joint) { + return setProperty( + context_id, joint_index, joint); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_joint_data(int context_id, const vircadia_vantage* joints, int size) { + if (size < 0 || (size != 0 && joints == nullptr)) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } + return setProperty(context_id, std::vector(joints, joints + size)); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_joint_flags_count(int context_id, int joint_count) { + return resizeProperty(context_id, joint_count); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_joint_flags(int context_id, int joint_index, vircadia_joint_flags flags) { + return setProperty( + context_id, joint_index, flags); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_all_joint_flags(int context_id, const vircadia_joint_flags* joints, int size) { + if (size < 0 || (size != 0 && joints == nullptr)) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } + return setProperty(context_id, std::vector(joints, joints + size)); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_grab_joints(int context_id, vircadia_far_grab_joints joints) { + return setProperty(context_id, joints); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_my_avatar_grab(int context_id, vircadia_avatar_grab grab) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + std::next(std::begin(contexts), context_id)->avatars() + .myAvatar().grabs.added.push_back(grab); + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_my_avatar_release_grab(int context_id, const uint8_t* uuid) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + if (uuid == nullptr) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } + UUID removed; + std::copy_n(uuid, removed.size(), removed.begin()); + std::next(std::begin(contexts), context_id)->avatars() + .myAvatar().grabs.removed.push_back(removed); + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_avatar_view_count(int context_id, int view_count) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + if (view_count < 0) { + return toInt(ErrorCode::ARGUMENT_INVALID); + } + std::next(std::begin(contexts), context_id)-> + avatars().views().resize(view_count); + return 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_set_avatar_view(int context_id, int view_index, vircadia_conical_view_frustum view_frustum) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + auto& views = std::next(std::begin(contexts), context_id)->avatars().views(); + return chain(checkIndexValid(views, view_index, ErrorCode::AVATAR_VIEW_INVALID), [&](auto) { + views[view_index] = ConicalViewFrustumData { + glmVec3From(view_frustum.position), + glmVec3From(view_frustum.direction), + view_frustum.angle, + view_frustum.radius, + view_frustum.far_clip + }; + return 0; + }); + }); +} + +int vircadia_set_avatar_view_corners(int context_id, int view_index, vircadia_view_frustum_corners view_frustum_corners) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + auto& views = std::next(std::begin(contexts), context_id)->avatars().views(); + return chain(checkIndexValid(views, view_index, ErrorCode::AVATAR_VIEW_INVALID), [&](auto) { + views[view_index].set( + glmVec3From(view_frustum_corners.position), + view_frustum_corners.radius, + view_frustum_corners.far_clip, + { + glmVec3From(view_frustum_corners.near_top_left), + glmVec3From(view_frustum_corners.near_top_right), + glmVec3From(view_frustum_corners.near_bottom_left), + glmVec3From(view_frustum_corners.near_bottom_right) + } + ); + return 0; + }); + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_count(int context_id) { + return chain(checkAvatarsEnabled(context_id), [&](auto) -> int { + return std::next(std::begin(contexts), context_id) + ->avatars().all().size(); + }); +} + +VIRCADIA_CLIENT_DYN_API +const uint8_t* vircadia_get_avatar_uuid(int context_id, int avatar_index) { + return validateAvatarIndex(context_id, avatar_index, [](auto& avatar) { + return avatar.sessionUUID.data(); + }); +} + +VIRCADIA_CLIENT_DYN_API +const char* vircadia_get_avatar_display_name(int context_id, int avatar_index) { + return validateAvatarIndex(context_id, avatar_index, [](const AvatarData& avatar) { + return avatar.getProperty().displayName.c_str(); + }); +} + +int getIdentityFlag(int context_id, int avatar_index, AvatarDataPacket::IdentityFlag flag) { + return validateAvatarIndex(context_id, avatar_index, [flag](const AvatarData& avatar) { + return (avatar.getProperty().identityFlags & flag) ? 1 : 0; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_is_replicated(int context_id, int avatar_index) { + return getIdentityFlag(context_id, avatar_index, AvatarDataPacket::IdentityFlag::isReplicated); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_look_at_snapping(int context_id, int avatar_index) { + return getIdentityFlag(context_id, avatar_index, AvatarDataPacket::IdentityFlag::lookAtSnapping); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_verification_failed(int context_id, int avatar_index) { + return getIdentityFlag(context_id, avatar_index, AvatarDataPacket::IdentityFlag::verificationFailed); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_attachment_count(int context_id, int avatar_index) { + return validateAvatarIndex(context_id, avatar_index, [](const AvatarData& avatar) -> int { + return avatar.getProperty().attachments.size(); + }); +} + +template <> +auto vircadia::client::makeError(int errorCode) { + return vircadia_avatar_attachment_result { {}, errorCode }; +} + +VIRCADIA_CLIENT_DYN_API +vircadia_avatar_attachment_result vircadia_get_avatar_attachment(int context_id, int avatar_index, int attachment_index) { + // static, otherwise MSVC captures in lambda and looses constexpr + // https://stackoverflow.com/questions/55136414/constexpr-variable-captured-inside-lambda-loses-its-constexpr-ness + constexpr static auto propertyIndex = AvatarData::IdentityIndex; + return validatePropertyIndex(context_id, avatar_index, + attachment_index, + indexChecker{}, + [&](const AvatarData& avatar) { + const auto& attachment = avatar.getProperty().attachments[attachment_index]; + return vircadia_avatar_attachment_result { + { + attachment.modelURL.c_str(), + attachment.jointName.c_str(), + attachment.transform, + uint8_t(attachment.isSoft ? 1 : 0) + }, + 0 + }; + } + ); +} + +VIRCADIA_CLIENT_DYN_API +const char* vircadia_get_avatar_session_display_name(int context_id, int avatar_index) { + return validateAvatarIndex(context_id, avatar_index, [](const AvatarData& avatar) { + return avatar.getProperty().sessionDisplayName.c_str(); + }); +} + +VIRCADIA_CLIENT_DYN_API +const char* vircadia_get_avatar_skeleton_model_url(int context_id, int avatar_index) { + return validateAvatarIndex(context_id, avatar_index, [](auto& avatar) { + return avatar.template getProperty().c_str(); + }); +} + +template +int getPropertySize(int context_id, int avatar_index) { + return validateAvatarIndex(context_id, avatar_index, [](auto& avatar) -> int { + return avatar.template getProperty().size(); + }); +} + +template +auto getPropertyPtr(int context_id, int avatar_index) { + return validateAvatarIndex(context_id, avatar_index, [](auto& avatar) { + return &avatar.template getProperty(); + }); +} + +template +auto getProperty(int context_id, int avatar_index, int property_index, Converter&& convert) { + return validatePropertyIndex(context_id, avatar_index, + property_index, + getIndexChecker(), + [&](const AvatarData& avatar) { + return convert(avatar.getProperty()[property_index]); + } + ); +} + +template +auto getPropertyPtr(int context_id, int avatar_index, int property_index) { + return getProperty(context_id, avatar_index, + property_index, + [](auto& property) { + return &property; + } + ); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_bone_count(int context_id, int avatar_index) { + return getPropertySize(context_id, avatar_index); +} + +template <> +auto vircadia::client::makeError(int errorCode) { + return vircadia_avatar_bone_result { {}, errorCode }; +} + +VIRCADIA_CLIENT_DYN_API +vircadia_avatar_bone_result vircadia_get_avatar_bone(int context_id, int avatar_index, int bone_index) { + return getProperty(context_id, avatar_index, + bone_index, [](auto& bone) { + return vircadia_avatar_bone_result { + { + bone.type, + bone.defaultTransform, + bone.index, + bone.parentIndex, + bone.name.c_str() + }, + 0 + }; + } + ); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_vector* vircadia_get_avatar_global_position(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_bounds* vircadia_get_avatar_bounding_box(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_quaternion* vircadia_get_avatar_orientation(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const float* vircadia_get_avatar_scale(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_vector* vircadia_get_avatar_look_at_position(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const float* vircadia_get_avatar_audio_loudness(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_transform* vircadia_get_avatar_sensor_to_world(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_avatar_additional_flags* vircadia_get_avatar_additional_flags(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_avatar_parent_info* vircadia_get_avatar_parent_info(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_vector* vircadia_get_avatar_local_position(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_avatar_hand_controllers* vircadia_get_avatar_hand_controllers(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_avatar_face_tracker_info* vircadia_get_avatar_face_tracker_info(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_joint_count(int context_id, int avatar_index) { + return getPropertySize(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_vantage* vircadia_get_avatar_joint(int context_id, int avatar_index, int joint_index) { + return getPropertyPtr(context_id, avatar_index, joint_index); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_joint_flags_count(int context_id, int avatar_index) { + return getPropertySize(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_joint_flags* vircadia_get_avatar_joint_flags(int context_id, int avatar_index, int joint_index) { + return getPropertyPtr(context_id, avatar_index, joint_index); +} + +VIRCADIA_CLIENT_DYN_API +const vircadia_far_grab_joints* vircadia_get_avatar_grab_joints(int context_id, int avatar_index) { + return getPropertyPtr(context_id, avatar_index); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_grabs_count(int context_id, int avatar_index) { + return getPropertySize(context_id, avatar_index); +} + +template <> +auto vircadia::client::makeError(int errorCode) { + return vircadia_avatar_grab_result { {}, {}, errorCode }; +} + +VIRCADIA_CLIENT_DYN_API +vircadia_avatar_grab_result vircadia_get_avatar_grab(int context_id, int avatar_index, int grab_index) { + return getProperty(context_id, avatar_index, + grab_index, [](auto& grab) { + auto& [id, data] = grab; + return vircadia_avatar_grab_result { + id.data(), + data, + 0 + }; + } + ); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_disconnection_count(int context_id) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + std::next(std::begin(contexts), context_id)->avatars().epitaphs().size(); + return 0; + }); +} + +template +auto validateEpitaphIndex(int context_id, int index, F&& f) { + return chain(checkAvatarsEnabled(context_id), [&](auto) { + const auto& epitaphs = std::next(std::begin(contexts), context_id)->avatars().epitaphs(); + return chain(checkIndexValid(epitaphs, index, ErrorCode::AVATAR_EPITAPH_INVALID), [&](auto) { + return std::invoke(std::forward(f), *std::next(std::begin(epitaphs), index)); + }); + }); +} + +VIRCADIA_CLIENT_DYN_API +const uint8_t* vircadia_get_avatar_disconnection_uuid(int context_id, int disconnection_index) { + return validateEpitaphIndex(context_id, disconnection_index, [&](auto& epitaph) { + return epitaph.first.data(); + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_disconnection_reason(int context_id, int disconnection_index) { + return validateEpitaphIndex(context_id, disconnection_index, [&](auto& epitaph) -> int { + return epitaph.second; + }); +} + +VIRCADIA_CLIENT_DYN_API +int vircadia_avatar_changed(int context_id, int avatar_index) { + return validateAvatarIndex(context_id, avatar_index, [&](auto& avatar) { + return avatar.changes.any() ? 1 : 0; + }); +} + diff --git a/libraries/vircadia-client/src/avatars.h b/libraries/vircadia-client/src/avatars.h new file mode 100644 index 00000000000..01f18bac36c --- /dev/null +++ b/libraries/vircadia-client/src/avatars.h @@ -0,0 +1,1402 @@ +// +// avatars.h +// libraries/vircadia-client/src +// +// Created by Nshan G. on 16 May 2022. +// 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 +// + +/// @file + +#ifndef LIBRARIES_VIRCADIA_CLIENT_SRC_AVATARS_H +#define LIBRARIES_VIRCADIA_CLIENT_SRC_AVATARS_H + +#include + +#include "common.h" +#include "geometry.h" + +/// @brief Defines the layout of additional avatar flags. +/// +/// Used with vircadia_set_my_avatar_additional_flags() and +/// vircadia_get_avatar_additional_flags() +struct vircadia_avatar_additional_flags{ + /// @brief A bit set for the pointing state of the avatar's hands. + /// + /// This controls where the laser emanates from. If the index + /// finger is pointing the laser emanates from the tip of that + /// finger, otherwise it emanates from the palm. + /// + /// Valid values are any combination(including 0) of: \n + /// vircadia_avatar_left_hand_pointing() \n + /// vircadia_avatar_right_hand_pointing() \n + /// vircadia_avatar_index_finger_pointing() \n + uint8_t hand_state; + + // Indicates the state of some keyboard keys (???) + // + // Seems to be unused throughout the codebase. \n + // Valid values are: \n + // 0 - no key is down, \n + // 1 - insert key is down, \n + // 2 - delete key is down \n + /// @private + uint8_t key_state; + + /// @brief Indicates that the avatar face movement is controlled by + /// code/script. + uint8_t has_head_scripted_blendshapes; + + // TODO: document or mark private + uint8_t has_procedural_eye_movement; + + /// @brief Indicates that the avatar's mouth blend shapes animate + /// automatically based on detected microphone input. + /// + /// Set this to 0 to fully control the mouth facial blend shapes. + uint8_t has_audio_face_movement; + + /// @brief Indicates that the facial blend shapes for avatar's eyes adjust + /// automatically as the eyes move. + /// + /// This can be set to 1 prevent the iris from being obscured by the upper + /// or lower lids, or to 0 to have full control of the eye blend shapes. + uint8_t has_procedural_eye_face_movement; + + /// @brief Indicates whether the avatar blinks automatically by animating + /// facial blend shapes. + /// + /// Set to 0 to to fully control the blink facial blend shapes. + uint8_t has_procedural_blink_face_movement; + + /// @brief Indicates that the avatar can collide with other avatars. + uint8_t collides_with_avatars; + + /// @brief Read only flag, indicates that the avatar is in "hero" zone. + uint8_t has_priority; +}; + +/// @brief Defines the layout of avatar parent information. +/// +/// Used with vircadia_set_my_avatar_parent_info() and +/// vircadia_get_avatar_parent_info(). +/// Must be set along with local position +/// vircadia_set_my_avatar_local_position() / +/// vircadia_get_avatar_local_position(). +struct vircadia_avatar_parent_info { + /// @brief RFC4122 identifier of the parent. + uint8_t uuid[16]; + /// @brief Joint index of the parent that avatar is relative to. + uint16_t joint_index; +}; + +/// @brief Defines the layout of avatar hand controller information. +/// +/// Used with vircadia_set_my_avatar_hand_controllers() and +/// vircadia_get_avatar_hand_controllers() +struct vircadia_avatar_hand_controllers { + /// @brief The rotation and translation of the left hand controller + /// relative to the avatar. + vircadia_vantage left; + /// @brief The rotation and translation of the left hand controller + /// relative to the avatar. + vircadia_vantage right; +}; + +/// @brief Defines the layout of avatar hand controller information. +/// +/// Used with vircadia_set_my_avatar_face_tracker_info() and +/// vircadia_get_avatar_face_tracker_info() +struct vircadia_avatar_face_tracker_info { + /// @brief Unused. + float left_eye_blink; + /// @brief Unused. + float right_eye_blink; + /// @brief Unused. + float average_loundness; + /// @brief Unused. + float brow_audio_lift; + + /// @brief The size of facial animation blend shape coefficients array. + uint8_t blendshape_coefficients_size; + /// @brief The facial animation blend shape coefficients array. + float blendshape_coefficients[256]; +}; + +/// @brief Defines the layout of avatar controller far grab joints data. +/// +/// Used with vircadia_set_my_avatar_grab_joints() and +/// vircadia_get_avatar_grab_joints() +struct vircadia_far_grab_joints { + /// @brief Left controller far grab joint position and rotation. + vircadia_vantage left; + /// @brief Right controller far grab joints position and rotation. + vircadia_vantage right; + /// @brief Mouse far grab joint position and rotation. + vircadia_vantage mouse; +}; + +/// @brief Defines the layout of avatar joint flags. +/// +/// Used with vircadia_set_my_avatar_joint_flags() and +/// vircadia_get_avatar_joint_flags(). +struct vircadia_joint_flags { + + /// @brief The specified joint translation is in default pose. + /// + /// If this is set the corresponding joint translation value + /// (vircadia_set_my_avatar_joint() / vircadia_get_avatar_joint) has no + /// meaning. + uint8_t translation_is_default; + + /// @brief The specified joint rotation is in default pose. + /// + /// If this is set the corresponding joint rotation value + /// (vircadia_set_my_avatar_joint() / vircadia_get_avatar_joint) has no + /// meaning. + uint8_t rotation_is_default; + +}; + +/// @brief Defines the layout of avatar attachment data. +/// +/// Used with vircadia_set_my_avatar_attachment() and +/// vircadia_get_avatar_attachment(). +struct vircadia_avatar_attachment { + + /// @brief URL of the attachment model. + const char* model_url; + + /// @brief The name of the avatar model is attached to. + const char* joint_name; + + /// @brief The linear transformation of the model relative to the joint it's + /// attached to. + vircadia_transform transform; + + /// @brief Indicates that the model has a skeleton, + /// which is rotated to fit avatar's current pose. + /// + /// This is used, for example, to have clothing that + /// moves with avatar. If this is set the + /// vircadia_avatar_attachment::transform is + /// ignored. + uint8_t is_soft; + +}; + +/// @brief Represents avatar attachment data or an error. +/// +/// Used with vircadia_get_avatar_attachment(). +struct vircadia_avatar_attachment_result { + + /// @brief The attachment data. + vircadia_avatar_attachment result; + + /// @brief The status of the result. + /// + /// 0 - success. \n + /// negative value - error occurred, and the + /// vircadia_avatar_attachment_result::result has no meaning. \n + /// Possible error codes: \n + /// vircadia_error_context_invalid() \n + /// vircadia_error_context_loss() \n + /// vircadia_error_avatar_invalid() \n + /// vircadia_error_avatar_attachment_invalid() + int error; + +}; + +/// @brief Defines the layout of avatar skeleton bone/joint data. +/// +/// Used with vircadia_set_my_avatar_bone() and vircadia_get_avatar_bone(). +struct vircadia_avatar_bone { + + /// @brief The type of the bone/joint. + /// + /// Valid values: + /// vircadia_avatar_skeleton_root_bone() \n + /// vircadia_avatar_skeleton_child_bone() \n + /// vircadia_avatar_root_bone() \n + /// vircadia_avatar_child_bone() + uint8_t type; + + /// @brief The default pose of the bone/joint. + vircadia_transform default_transform; + + /// @brief The index of the bone/joint. + int index; + + /// @brief The index of the parent bone/joint. + int parent_index; + + /// @brief The name of the bone/joint. + const char* name; + +}; + +/// @brief Represents avatar skeleton bone/joint data or an error. +/// +/// Used with vircadia_get_avatar_bone(). +struct vircadia_avatar_bone_result { + + /// @brief The skeleton bone/joint data. + vircadia_avatar_bone result; + + /// @brief The status of the result. + /// + /// 0 - success. \n + /// negative value - error occurred, and the + /// vircadia_avatar_bone_result::result has no meaning. \n + /// Possible error codes: \n + /// vircadia_error_context_invalid() \n + /// vircadia_error_context_loss() \n + /// vircadia_error_avatar_invalid() \n + /// vircadia_error_avatar_bone_invalid() + int error; + +}; + +/// @brief Defines the layout of avatar grab data. +/// +/// Used with vircadia_my_avatar_grab() and vircadia_get_avatar_grab(). +struct vircadia_avatar_grab { + + /// @brief RFC4122 identifier of the grabbed object. + uint8_t target_id[16]; + + /// @brief The index of the grabbing joint. + int joint_index; + + /// @brief The translation and rotation of the grabbed object relative to + /// the grabbing joint. + vircadia_vantage offset; + +}; + +/// @brief Represents avatar grab data or an error. +/// +/// Used with vircadia_get_avatar_grab(). +struct vircadia_avatar_grab_result { + + /// @brief RFC4122 identifier of the grab action. + /// + /// Pass it vircadia_my_avatar_release_grab() to release the grabbed + /// object. + const uint8_t* id; + + /// @brief The grab data. + vircadia_avatar_grab result; + + /// @brief The status of the result. + /// + /// 0 - success. \n + /// negative value - error occurred, vircadia_avatar_grab_result::id is + /// null, and the vircadia_avatar_bone_result::result has no meaning. \n + /// Possible error codes: \n + /// vircadia_error_context_invalid() \n + /// vircadia_error_context_loss() \n + /// vircadia_error_avatar_invalid() \n + /// vircadia_error_avatar_bone_invalid() + int error; + +}; + +/// @brief Defines the layout of conical view frustum. +/// +/// Used with vircadia_set_avatar_view(). +struct vircadia_conical_view_frustum { + + /// @brief The position of the viewer. + vircadia_vector position; + + /// @brief The direction of the view. + vircadia_vector direction; + + /// @brief The angle of the cone. + float angle; + + /// @brief The radius of the sphere around the viewer, separate from the cone. + float radius; + + /// @brief The far clip plane of the view. + float far_clip; + +}; + +/// @brief Defines the layout of view frustum. +/// +/// This is a representation of a view frustum based on near clip plane +/// corners. +/// +/// Used with vircadia_set_avatar_view_corners(). +struct vircadia_view_frustum_corners { + + /// @brief The position of the viewer. + vircadia_vector position; + + /// @brief The radius of the sphere around the viewer. + float radius; + + /// @brief The far clip plane of the view. + float far_clip; + + /// @brief The top left corner of the near clip plane. + vircadia_vector near_top_left; + + /// @brief The top right corner of the near clip plane. + vircadia_vector near_top_right; + + /// @brief The bottom left corner of the near clip plane. + vircadia_vector near_bottom_left; + + /// @brief The bottom right corner of the near clip plane. + vircadia_vector near_bottom_right; + +}; + +/// @brief Enable handling of avatars. +/// +/// Avatar data will be received/sent from/to internal buffers accessible +/// through API functions. This introduces fixed size memory overhead per +/// avatar. Afterwards vircadia_update_avatars() must be called periodically to +/// update internal buffers for sending and receiving. +/// +/// @param context_id - The id of the context (context.h). +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() +VIRCADIA_CLIENT_DYN_API +int vircadia_enable_avatars(int context_id); + +/// @brief Updates internal avatar data buffers. +/// +/// This makes the avatar data received from the mixer up to the point of the +/// call available for reading immediately, and buffers the set avatar data up +/// to the point of the call to be sent to the mixer asynchronously. +/// +/// @param context_id - The id of the context (context.h). +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_update_avatars(int context_id); + +/// @brief Sets the avatar display to be sent to the mixer. +/// +/// This might not be exactly what other's see, the mixer may sanitize and +/// de-duplicate the name. +/// +/// @param context_id - The id of the context (context.h). +/// @param display_name - A null terminated string to set as display name. Must +/// not be null. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_display_name(int context_id, const char* display_name); + +// not sure if this is read-only +/// @private +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_is_replicated(int context_id, uint8_t is_replicated); + +/// @brief Sets the avatar "look at snapping" flag to be sent to the mixer. +/// +/// If enabled make the avatar's eyes snap to look at another avatar's eyes +/// when the other avatar is in the line of sight and also has this flag set. +/// +/// @param context_id - The id of the context (context.h). +/// @param look_at_snapping_enabled - 0 - disabled, 1 - enabled. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_look_at_snapping(int context_id, uint8_t look_at_snapping_enabled); + +// not sure if this is read-only +/// @private +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_verification(int context_id, uint8_t verification_failed); + +/// @brief Sets avatar attachment data at a specified index to be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param attachment_index - The index of the attachment. Must be in range set +/// by vircadia_set_my_avatar_attachment_count(). +/// @param attachment - The data of the attachment. The pointer members must +/// not be null. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_attachment_invalid() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_attachment(int context_id, int attachment_index, vircadia_avatar_attachment attachment); + +/// @brief Sets the total count of avatar attachments be sent to the mixer. +/// +/// All the attachment data must be initialized with +/// vircadia_set_my_avatar_attachment(). Increasing the count from the +/// previously set value preserves the data. Decreasing the count erases the +/// data past the newly set value. +/// +/// @param context_id - The id of the context (context.h). +/// @param attachment_count - The count of attachments. Must not be negative. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_attachment_count(int context_id, int attachment_count); + +/// @brief Sets avatar attachments data be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param attachments - The array of attachments. Must not be null unless +/// attachment_count is 0. +/// @param attachment_count - The count of attachments. Must not be negative. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_attachments(int context_id, const vircadia_avatar_attachment* attachments, int attachment_count); + +// this is read-only +/// @private +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_session_display_name(int context_id, const char* session_display_name); + +/// @brief Sets the URL of avatar model and skeleton data to be sent to the +/// mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param skeleton_model_url - Null terminated URL string. Must not be null. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_skeleton_model_url(int context_id, const char* skeleton_model_url); + +/// @brief Sets the total count of avatar skeleton bones be sent to the mixer. +/// +/// All the bone data must be initialized with vircadia_set_my_avatar_bone(). +/// Increasing the count from the previously set value preserves the data. +/// Decreasing the count erases the data past the newly set value. +/// +/// @param context_id - The id of the context (context.h). +/// @param bone_count - The count of bones. Must not be negative. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_bone_count(int context_id, int bone_count); + +/// @brief Sets avatar skeleton bone data at a specified index to be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param bone_index - The index of the bone. Must be in range set by +/// vircadia_set_my_avatar_bone_count(). +/// @param bone - The data of the bone. The pointer members must not be null. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_bone_invalid() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_bone(int context_id, int bone_index, vircadia_avatar_bone bone); + +/// @brief Sets avatar skeleton data be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param data - The array of skeleton bones. Must not be null unless size is +/// 0. +/// @param size - The count of skeleton bones. Must not be negative. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_skeleton_data(int context_id, vircadia_avatar_bone* data, int size); + +/// @brief Sets avatar global position to be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param position - The position vector. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_global_position(int context_id, vircadia_vector position); + +/// @brief Sets avatar bounding box to be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param bounding_box - The bounding box data. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_bounding_box(int context_id, vircadia_bounds bounding_box); + +/// @brief Sets avatar orientation to be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param orientation - The orientation quaternion. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_orientation(int context_id, vircadia_quaternion orientation); + +/// @brief Sets avatar scale to be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param scale - The scale value. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_scale(int context_id, float scale); + +/// @brief Sets avatar "look at" position to be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param position - The position vector. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_look_at(int context_id, vircadia_vector position); + +/// @brief Sets avatar audio loudness to be sent to the mixer. +/// +/// This represents the instantaneous loudness of the audio input. +/// +/// @param context_id - The id of the context (context.h). +/// @param loudness - The loudness value. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_audio_loudness(int context_id, float loudness); + +/// @brief Sets avatar sensor to world transform to be sent to the mixer. +/// +/// This is the scale, rotation and translation transform from the user's real +/// world to the avatar's size, orientation and position in the virtual world. +/// +/// @param context_id - The id of the context (context.h). +/// @param transform - The linear transformation data. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_sensor_to_world(int context_id, vircadia_transform transform); + +/// @brief Sets avatar's additional flags to be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param flags - The flags data. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_additional_flags(int context_id, vircadia_avatar_additional_flags flags); + +/// @brief Sets avatar's parent information to be sent to the mixer. +/// +/// Avatar's local position (vircadia_set_my_avatar_local_position()) is +/// relative to this parent. +/// +/// @param context_id - The id of the context (context.h). +/// @param info - The parent information. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_parent_info(int context_id, vircadia_avatar_parent_info info); + +/// @brief Sets avatar's local position to be sent to the mixer. +/// +/// This is relative to avatar's parent, which must be set with +/// vircadia_set_my_avatar_parent_info(). +/// +/// @param context_id - The id of the context (context.h). +/// @param position - The position vector. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_local_position(int context_id, vircadia_vector position); + +/// @brief Sets avatar hand controller data to be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param controllers - The position and rotation of controllers. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_hand_controllers(int context_id, vircadia_avatar_hand_controllers controllers); + +/// @brief Sets avatar's facial animation data to be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param info - The facial animation data. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_face_tracker_info(int context_id, vircadia_avatar_face_tracker_info info); + +/// @brief Sets the total count of avatar current pose joints be sent to the +/// mixer. +/// +/// These values define the current pose of the avatar. The individual joint +/// data can be set with vircadia_set_my_avatar_joint(). Increasing the count +/// from the previously set value preserves the data. Decreasing the count +/// erases the data past the newly set value. +/// +/// @param context_id - The id of the context (context.h). +/// @param joint_count - The count of current pose joints. Must not be negative. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_joint_count(int context_id, int joint_count); + +/// @brief Sets avatar current pose joint data at a specified index to be sent +/// to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param joint_index - The index of the joint. Must be in range set by +/// vircadia_set_my_avatar_joint_count(). +/// @param joint - The position and rotation of the joint. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_joint_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_joint(int context_id, int joint_index, vircadia_vantage joint); + +/// @brief Sets avatar current pose data be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param joints - The array of joints that define the current pose. Must not be +/// null unless size is 0. +/// @param size - The count of joints. Must not be negative. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_joint_data(int context_id, const vircadia_vantage* joints, int size); + +/// @brief Sets the total count of avatar current pose joint flags be sent to the +/// mixer. +/// +/// These values define the current pose of the avatar. The individual flag +/// data can be set with vircadia_set_my_avatar_joint_flags(). Increasing the +/// count from the previously set value preserves the data. Decreasing the +/// count erases the data past the newly set value. +/// +/// @param context_id - The id of the context (context.h). +/// @param joint_count - The count of current pose joint flags. Must not be negative. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_joint_flags_count(int context_id, int joint_count); + +/// @brief Sets avatar current pose joint flags at a specified index to be sent +/// to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param joint_index - The index of the joint. Must be in range set by +/// vircadia_set_my_avatar_joint_flags_count(). +/// @param flags - The joint flags. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_joint_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_joint_flags(int context_id, int joint_index, vircadia_joint_flags flags); + +/// @brief Sets avatar current pose flags be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param flags - The array of joint flags that define the current pose. Must +/// not be null unless size is 0. +/// @param size - The count of joints. Must not be negative. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_all_joint_flags(int context_id, const vircadia_joint_flags* flags, int size); + +/// @brief Sets avatar grab joint data be sent to the mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param joints - The far grab joint data. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_my_avatar_grab_joints(int context_id, vircadia_far_grab_joints joints); + +/// @brief Creates a new grab action for the avatar. +/// +/// @param context_id - The id of the context (context.h). +/// @param grab - The grab action data. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_my_avatar_grab(int context_id, vircadia_avatar_grab grab); + +/// @brief Released/Destroys an existing grab action of the avatar. +/// +/// @param context_id - The id of the context (context.h). +/// @param uuid - An RFC4122 identifier of the grab action. Must not be null. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_my_avatar_release_grab(int context_id, const uint8_t* uuid); + +/// @brief Sets the total count of camera views to be sent to the mixer. +/// +/// The mixer will use these to cull the avatar data sent back to the client. +/// The individual view data can be set with vircadia_set_avatar_view() or +/// vircadia_set_avatar_view_corners(). Increasing the count from the +/// previously set value preserves the data. Decreasing the count erases the +/// data past the newly set value. +/// +/// @param context_id - The id of the context (context.h). +/// @param view_count - The count of camera views. Must not be negative. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_argument_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_avatar_view_count(int context_id, int view_count); + +/// @brief Sets the camera view data at a specified index to be sent to the +/// mixer. +/// +/// @param context_id - The id of the context (context.h). +/// @param view_index - The index of the view. Must be in range set by +/// vircadia_set_avatar_view_count(). +/// @param view_frustum - The conical frustum of the camera view. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_view_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_avatar_view(int context_id, int view_index, vircadia_conical_view_frustum view_frustum); + +/// @brief Sets the camera view data at a specified index to be sent to the +/// mixer. +/// +/// Unlike vircadia_set_avatar_view(), this function accepts usual rectangular +/// near plane corners instead of direction and angle of the cone and performs +/// a conversion under the hood. +/// +/// @param context_id - The id of the context (context.h). +/// @param view_index - The index of the view. Must be in range set by +/// vircadia_set_avatar_view_count(). +/// @param view_frustum_corners - The corner data of the camera view frustum. +/// +/// @return 0 on success, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_view_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_set_avatar_view_corners(int context_id, int view_index, vircadia_view_frustum_corners view_frustum_corners); + +/// @brief Gets the total count of avatars in the current domain. +/// +/// @param context_id - The id of the context (context.h). +/// +/// @return The avatar count, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_count(int context_id); + +/// @brief Gets the RFC4122 identifier of the specified avatar. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return The identifier byte array, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const uint8_t* vircadia_get_avatar_uuid(int context_id, int avatar_index); + +/// @brief Gets the display name of the specified avatar. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A null terminated string, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const char* vircadia_get_avatar_display_name(int context_id, int avatar_index); + +/// @brief Indicates that the avatar is a replica of another (used for +/// testing purposes). +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return 1 - is a replica, 0 - is not a replica, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_is_replicated(int context_id, int avatar_index); + +/// @brief Indicates that the avatar has "look at snapping" enabled. +/// +/// If enabled make the avatar's eyes snap to look at another avatar's eyes +/// when the other avatar is in the line of sight and also has this flag set. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return 1 - enabled, 0 - disabled, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_look_at_snapping(int context_id, int avatar_index); + +/// @brief Indicates that the specified avatar's model failed identity +/// verification. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return 1 - verification failed, 0 - verification succeeded, or a negative +/// error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_verification_failed(int context_id, int avatar_index); + +/// @brief Gets the total count of the specified avatar's attachments. +/// +/// Individual attachment data can be accessed with +/// vircadia_get_avatar_attachment(). +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return The count of attachments, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_attachment_count(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's attachments data. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// @param attachment_index - The attachment index in the internal buffer. Must +/// be in range (vircadia_get_avatar_attachment_count()). +/// +/// @return Attachment data or a negative error code. +VIRCADIA_CLIENT_DYN_API +vircadia_avatar_attachment_result vircadia_get_avatar_attachment(int context_id, int avatar_index, int attachment_index); + +/// @brief Gets the specified avatar's session display name. +/// +/// This is a sanitized and default version of the avatar's name defined by the +/// mixer rather than clients. It's unique among all the avatars present in the +/// domain. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return Null terminated string or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const char* vircadia_get_avatar_session_display_name(int context_id, int avatar_index); + +/// @brief Gets the URL of the specified avatar's skeleton and model. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return Null terminated string or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const char* vircadia_get_avatar_skeleton_model_url(int context_id, int avatar_index); + +/// @brief Gets the total count of the specified avatar's skeleton bones. +/// +/// Individual bone data can be accessed with vircadia_get_avatar_bone(). +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return The count of bones, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_bone_count(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's bone data. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// @param bone_index - The bone index in the internal buffer. Must +/// be in range (vircadia_get_avatar_bone_count()). +/// +/// @return Bone data or a negative error code. +VIRCADIA_CLIENT_DYN_API +vircadia_avatar_bone_result vircadia_get_avatar_bone(int context_id, int avatar_index, int bone_index); + +/// @brief Gets the specified avatar's global position. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the position vector, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const vircadia_vector* vircadia_get_avatar_global_position(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's bounding box. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the bounding box data, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const vircadia_bounds* vircadia_get_avatar_bounding_box(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's orientation. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the orientation quaternion, or null in case of an +/// error. +VIRCADIA_CLIENT_DYN_API +const vircadia_quaternion* vircadia_get_avatar_orientation(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's scale. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the scale value, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const float* vircadia_get_avatar_scale(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's "look at" position. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the position vector, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const vircadia_vector* vircadia_get_avatar_look_at_position(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's audio loudness value. +/// +/// This represents the instantaneous loudness of the audio input. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the loudness value, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const float* vircadia_get_avatar_audio_loudness(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's sensor to world transform. +/// +/// This is the scale, rotation and translation transform from the user's real +/// world to the avatar's size, orientation and position in the virtual world. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the transform, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const vircadia_transform* vircadia_get_avatar_sensor_to_world(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's additional flag values. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the flag values, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const vircadia_avatar_additional_flags* vircadia_get_avatar_additional_flags(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's parent information. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the flag values, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const vircadia_avatar_parent_info* vircadia_get_avatar_parent_info(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's local position vector. +/// +/// This is the position relative to the parent +/// (vircadia_get_avatar_parent_info()). +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the position vector, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const vircadia_vector* vircadia_get_avatar_local_position(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's hand controller data. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the controller data, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const vircadia_avatar_hand_controllers* vircadia_get_avatar_hand_controllers(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's facial animation data. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to the facial animation data, or null in case of an +/// error. +VIRCADIA_CLIENT_DYN_API +const vircadia_avatar_face_tracker_info* vircadia_get_avatar_face_tracker_info(int context_id, int avatar_index); + +/// @brief Gets the total count of the specified avatar's current pose joints. +/// +/// Individual joint data can be accessed with vircadia_get_avatar_joint(). +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return The count of joints, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_joint_count(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's current pose joint data. +/// +/// This joint data defines the current pose of the avatar, together with joint +/// flags (vircadia_get_avatar_joint_flags()). +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// @param joint_index - The joint index in the internal buffer. Must be in +/// range (vircadia_get_avatar_joint_count()). +/// +/// @return A pointer to joint position and rotation, or null in case of an +/// error. +VIRCADIA_CLIENT_DYN_API +const vircadia_vantage* vircadia_get_avatar_joint(int context_id, int avatar_index, int joint_index); + +/// @brief Gets the total count of the specified avatar's current pose joints. +/// +/// Individual joint flags can be accessed with vircadia_get_avatar_joint_flags(). +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return The count of joint flags, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_joint_flags_count(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's current pose joint flags. +/// +/// This joint flags defines the current pose of the avatar, together with +/// joint data (vircadia_get_avatar_joint()). +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// @param joint_index - The joint flags index in the internal buffer. Must be +/// in range (vircadia_get_avatar_joint_flags_count()). +/// +/// @return A pointer to joint flags, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const vircadia_joint_flags* vircadia_get_avatar_joint_flags(int context_id, int avatar_index, int joint_index); + +/// @brief Gets the specified avatar's grab joints data. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return A pointer to grab joint data, or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const vircadia_far_grab_joints* vircadia_get_avatar_grab_joints(int context_id, int avatar_index); + +/// @brief Gets the total count of the specified avatar's grab actions. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// +/// @return The count of grab actions, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_grabs_count(int context_id, int avatar_index); + +/// @brief Gets the specified avatar's grab action data. +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The avatar index in the internal buffer. Must be in +/// range (vircadia_get_avatar_count()). +/// @param grab_index - The grab action index in the internal buffer. Must be +/// in range (vircadia_get_avatar_grabs_count()). +/// +/// @return Grab action data or a negative error code. +VIRCADIA_CLIENT_DYN_API +vircadia_avatar_grab_result vircadia_get_avatar_grab(int context_id, int avatar_index, int grab_index); + +/// @brief Gets the total count of avatar disconnections. +/// +/// The list of disconnected avatars is updated with vircadia_update_avatars(). +/// Information of specific disconnection can be retrieved with +/// vircadia_get_avatar_disconnection_uuid(), +/// vircadia_get_avatar_disconnection_reason(), +/// +/// @param context_id - The id of the context (context.h). +/// +/// @return The count of disconnected avatars, or a negative error code. \n +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_disconnection_count(int context_id); + +/// @brief Gets the UUID of specified disconnected avatar. +/// +/// @param context_id - The id of the context (context.h). +/// @param disconnection_index - The avatar index in the internal buffer of +/// avatars disconnection. Must be in range +/// (vircadia_get_avatar_disconnection_count()). +/// +/// @return RFC4122 avatar identifier or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const uint8_t* vircadia_get_avatar_disconnection_uuid(int context_id, int disconnection_index); + +/// @brief Gets the reason of specified avatar's disconnection. +/// +/// @param context_id - The id of the context (context.h). +/// @param disconnection_index - The index in the internal buffer of +/// avatar disconnections. Must be in range +/// (vircadia_get_avatar_disconnection_count()). +/// +/// @return The reason code of avatar disconnection, or a negative error code. \n +/// +/// Reason codes: \n +/// vircadia_avatar_disconnection_unknown() \n +/// vircadia_avatar_disconnection_normal() \n +/// vircadia_avatar_disconnection_ignored() \n +/// vircadia_avatar_disconnection_they_entered_bubble() \n +/// vircadia_avatar_disconnection_you_entered_bubble() \n +/// +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_disconnection_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_get_avatar_disconnection_reason(int context_id, int disconnection_index); + +/// @brief Checks if given avatar data has been updated. +/// +/// This function checks whether an individual avatar's data has been updated +/// by the last call to vircadia_update_avatars(). +/// +/// @param context_id - The id of the context (context.h). +/// @param avatar_index - The index in the internal buffer of avatars. Must be +/// in range (vircadia_get_avatar_count()). +/// +/// @return 0 - avatar not updated, 1 - avatar updated, or a negative error code. \n +/// +/// Possible error codes: \n +/// vircadia_error_context_invalid() \n +/// vircadia_error_context_loss() \n +/// vircadia_error_avatars_disabled() \n +/// vircadia_error_avatar_invalid() +VIRCADIA_CLIENT_DYN_API +int vircadia_avatar_changed(int context_id, int avatar_index); + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/client.h b/libraries/vircadia-client/src/client.h index a414a8d2fb3..e593a051824 100644 --- a/libraries/vircadia-client/src/client.h +++ b/libraries/vircadia-client/src/client.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 31 March 2022. // 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 @@ -19,7 +20,12 @@ #include "error.h" #include "context.h" #include "node_list.h" +#include "node_types.h" #include "messages.h" #include "message_types.h" +#include "avatars.h" +#include "avatar_constants.h" +#include "audio.h" +#include "audio_constants.h" #endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/common.h b/libraries/vircadia-client/src/common.h index 31d9cc62aa2..0e1ec6a9ee5 100644 --- a/libraries/vircadia-client/src/common.h +++ b/libraries/vircadia-client/src/common.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 1 March 2022. // 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 diff --git a/libraries/vircadia-client/src/context.cpp b/libraries/vircadia-client/src/context.cpp index d82b43c72ad..d484bbc29a6 100644 --- a/libraries/vircadia-client/src/context.cpp +++ b/libraries/vircadia-client/src/context.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 15 March 2022. // 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 diff --git a/libraries/vircadia-client/src/context.h b/libraries/vircadia-client/src/context.h index 493db7b5b05..4b20d7af99d 100644 --- a/libraries/vircadia-client/src/context.h +++ b/libraries/vircadia-client/src/context.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 15 March 2022. // 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 diff --git a/libraries/vircadia-client/src/error.cpp b/libraries/vircadia-client/src/error.cpp index 070c7e40028..399f54067b9 100644 --- a/libraries/vircadia-client/src/error.cpp +++ b/libraries/vircadia-client/src/error.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 27 March 2022. // 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 @@ -50,3 +51,56 @@ VIRCADIA_CLIENT_DYN_API int vircadia_error_packet_write() { VIRCADIA_CLIENT_DYN_API int vircadia_error_argument_invalid() { return toInt(ErrorCode::ARGUMENT_INVALID); } + +VIRCADIA_CLIENT_DYN_API int vircadia_error_audio_disabled() { + return toInt(ErrorCode::AUDIO_DISABLED); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_audio_format_invalid() { + return toInt(ErrorCode::AUDIO_FORMAT_INVALID); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_audio_context_invalid() { + return toInt(ErrorCode::AUDIO_CONTEXT_INVALID); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_audio_codec_invalid() { + return toInt(ErrorCode::AUDIO_CODEC_INVALID); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatars_disabled() { + return toInt(ErrorCode::AVATARS_DISABLED); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_invalid() { + return toInt(ErrorCode::AVATAR_INVALID); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_attachment_invalid() { + return toInt(ErrorCode::AVATAR_ATTACHMENT_INVALID); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_joint_invalid() { + return toInt(ErrorCode::AVATAR_JOINT_INVALID); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_bone_invalid() { + return toInt(ErrorCode::AVATAR_BONE_INVALID); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_grab_invalid() { + return toInt(ErrorCode::AVATAR_GRAB_INVALID); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_entity_invalid() { + return toInt(ErrorCode::AVATAR_ENTITY_INVALID); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_view_invalid() { + return toInt(ErrorCode::AVATAR_VIEW_INVALID); +} + +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_disconnection_invalid() { + return toInt(ErrorCode::AVATAR_EPITAPH_INVALID); +} + diff --git a/libraries/vircadia-client/src/error.h b/libraries/vircadia-client/src/error.h index 4a519813115..65fa9f784a6 100644 --- a/libraries/vircadia-client/src/error.h +++ b/libraries/vircadia-client/src/error.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 27 March 2022. // 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 @@ -21,7 +22,7 @@ /// A limitation in the current library implantation, this error code is /// returned if user attempts to create the second context. /// -/// @return -1 +/// @return Unique negative error code. VIRCADIA_CLIENT_DYN_API int vircadia_error_context_exists(); /// @brief Context ID is invalid. @@ -29,7 +30,7 @@ VIRCADIA_CLIENT_DYN_API int vircadia_error_context_exists(); /// The user specified wrong context ID or the context with specified ID /// was destroyed. /// -/// @return -2 +/// @return Unique negative error code. VIRCADIA_CLIENT_DYN_API int vircadia_error_context_invalid(); /// @brief Context was lost. @@ -38,26 +39,26 @@ VIRCADIA_CLIENT_DYN_API int vircadia_error_context_invalid(); /// unrecoverable internal implantation errors. A lost context still /// must be destroyed, but cannot be otherwise used. /// -/// @return -3 +/// @return Unique negative error code. VIRCADIA_CLIENT_DYN_API int vircadia_error_context_loss(); /// @brief Node index is invalid. /// /// The Specified node index was out of range. /// -/// @return -4 +/// @return Unique negative error code. VIRCADIA_CLIENT_DYN_API int vircadia_error_node_invalid(); /// @brief Message index is invalid. /// /// Specified message index was out of range. /// -/// @return -5 +/// @return Unique negative error code. VIRCADIA_CLIENT_DYN_API int vircadia_error_message_invalid(); /// @brief Message type is invalid. /// -/// @return -6 +/// @return Unique negative error code. VIRCADIA_CLIENT_DYN_API int vircadia_error_message_type_invalid(); /// @brief Handling of messages of specified type is disabled. @@ -65,19 +66,117 @@ VIRCADIA_CLIENT_DYN_API int vircadia_error_message_type_invalid(); /// Use vircadia_enable_messages() to enable messages of specified /// type. /// -/// @return -7 +/// @return Unique negative error code. VIRCADIA_CLIENT_DYN_API int vircadia_error_message_type_disabled(); /// @brief Writing network packet failed. /// -/// @return -8 +/// @return Unique negative error code. VIRCADIA_CLIENT_DYN_API int vircadia_error_packet_write(); /// @brief Invalid function argument. /// /// One or more arguments passed to the function were not in a required range. /// -/// @return -9 +/// @return Unique negative error code. VIRCADIA_CLIENT_DYN_API int vircadia_error_argument_invalid(); +/// @brief Handling of avatars is disabled. +/// +/// Call vircadia_enable_avatars() first. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatars_disabled(); + +/// @brief Avatar index is invalid. +/// +/// Specified avatar index was out of range. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_invalid(); + +/// @brief Avatar attachment index is invalid. +/// +/// Specified avatar attachment index was out of range. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_attachment_invalid(); + +/// @brief Avatar joint index is invalid. +/// +/// Specified avatar joint index was out of range. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_joint_invalid(); + +/// @brief Avatar skeleton bone/joint index is invalid. +/// +/// Specified avatar skeleton bone/joint index was out of range. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_bone_invalid(); + +/// @brief Avatar grab index is invalid. +/// +/// Specified avatar grab index was out of range. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_grab_invalid(); + +/// @brief Avatar entity index is invalid. +/// +/// Specified avatar entity index was out of range. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_entity_invalid(); + +/// @brief Avatar view index is invalid. +/// +/// Specified avatar view index was out of range. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_view_invalid(); + +/// @brief Avatar disconnection index is invalid. +/// +/// Specified avatar disconnection index was out of range. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_avatar_disconnection_invalid(); + +/// @brief Audio Functionality is disabled. +/// +/// Use vircadia_enable_audio() to enable audio. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_audio_disabled(); + +/// @brief Invalid audio format specified. +/// +/// Valid audio format must have non-zero positive frequency and +/// channel count, and one of sample types defined in +/// audio_constants.h. Additionally when passed to +/// vircadia_set_audio_input_format(), the channel count must not be +/// greater than two. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_audio_format_invalid(); + +/// @brief Invalid audio context specified. +/// +/// Audio context must be retrieved by polling +/// vircadia_get_audio_input_context()/vircadia_get_audio_output_context(), +/// after calling +/// vircadia_set_audio_input_format()/vircadia_set_audio_output_format(). +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_audio_context_invalid(); + +/// @brief Invalid audio codec name specified. +/// +/// Valid codec names are defined in audio_constants.h. +/// +/// @return Unique negative error code. +VIRCADIA_CLIENT_DYN_API int vircadia_error_audio_codec_invalid(); + #endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/geometry.h b/libraries/vircadia-client/src/geometry.h new file mode 100644 index 00000000000..6ed32d6d503 --- /dev/null +++ b/libraries/vircadia-client/src/geometry.h @@ -0,0 +1,68 @@ +// +// geometry.h +// libraries/vircadia-client/src +// +// Created by Nshan G. on 23 July 2022. +// 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 +// + +/// @file + +#ifndef LIBRARIES_VIRCADIA_CLIENT_SRC_GEOMETRY_H +#define LIBRARIES_VIRCADIA_CLIENT_SRC_GEOMETRY_H + +/// @brief Defines the layout of vectors. +struct vircadia_vector { + /// @brief The x coordinate of the vector. + float x; + /// @brief The y coordinate of the vector. + float y; + /// @brief The z coordinate of the vector. + float z; +}; + +/// @brief Defines the layout of quaternions. +struct vircadia_quaternion { + /// @brief The x component of the quaternion. + float x; + /// @brief The y component of the quaternion. + float y; + /// @brief The z component of the quaternion. + float z; + /// @brief The w component of the quaternion. + float w; +}; + +/// @brief Defines the layout of bounding box data. +/// +/// Used in vircadia_set_my_avatar_bounding_box(), +/// vircadia_get_avatar_bounding_box() +struct vircadia_bounds { + /// @brief The dimensions/size of the bounding box. + vircadia_vector dimensions; + /// @brief The offset of the bounding box from avatar position. + vircadia_vector offset; +}; + +/// @brief Defines the layout of position-rotation pairs (vantage +/// points). +struct vircadia_vantage { + /// @brief The position component. + vircadia_vector position; + /// @brief The rotation component. + vircadia_quaternion rotation; +}; + +/// @brief Defines the layout linear transformation data. +struct vircadia_transform { + /// @brief The translation and rotation pair. + vircadia_vantage vantage; + /// @brief The scale. + float scale; +}; + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/internal/Audio.cpp b/libraries/vircadia-client/src/internal/Audio.cpp new file mode 100644 index 00000000000..c591fe786c2 --- /dev/null +++ b/libraries/vircadia-client/src/internal/Audio.cpp @@ -0,0 +1,139 @@ +// +// Audio.cpp +// libraries/vircadia-client/src/internal +// +// Created by Nshan G. on 4 July 2022. +// 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 +// + +#include "Audio.h" + +#include +#include +#include + +#include +#include +#include + +#include "audio/AudioClient.h" + +namespace vircadia::client +{ + + Audio::Audio() : + codecs{ + std::make_shared(), + std::make_shared(), + std::make_shared() + }, + allowedCodecs{codecs}, + selectedCodecName{""} + {} + + void Audio::enable() { + if (!isEnabled()) { + DependencyManager::set(allowedCodecs).data(); + } + } + + const std::vector>& Audio::getCodecs() { + return codecs; + } + + const std::string& Audio::getSelectedCodecName() { + selectedCodecName = DependencyManager::get()->getSelectedCodecName(); + return selectedCodecName; + } + + bool Audio::getIsMuted() const { + return DependencyManager::get()->getIsMuted(); + } + + bool Audio::getIsMutedByMixer() const { + return DependencyManager::get()->getIsMutedByMixer(); + } + + void Audio::setIsMuted(bool muted) { + DependencyManager::get()->setIsMuted(muted); + } + + void Audio::setVantage(const vircadia_vantage& vantage) { + DependencyManager::get()->setVantage(vantage); + } + + void Audio::setBounds(const vircadia_bounds& bounds) { + DependencyManager::get()->setBounds(bounds); + } + + void Audio::setInputEcho(bool enabled) { + DependencyManager::get()->setInputEcho(enabled); + } + + void Audio::setOutputBufferFrames(int frames) + { + DependencyManager::get()->setOutputBufferSize(frames); + } + + void Audio::setServerInjectorGain(float gain) { + DependencyManager::get()->setInjectorGain(gain); + } + + bool Audio::isEnabled() const { + return DependencyManager::isSet(); + } + + bool Audio::setCodecAllowed(std::string name, bool allow) { + auto matchName = [&name] (const auto& codecPtr) + { return codecPtr->getName().toStdString() == name; }; + + auto codec = std::find_if(codecs.begin(), codecs.end(), matchName); + if (codec == codecs.end()) { + return false; + } + + auto allowedCodec = std::find_if(allowedCodecs.begin(), allowedCodecs.end(), matchName); + auto isAllowed = allowedCodec != allowedCodecs.end(); + + if (allow != isAllowed) { + if (allow) { + allowedCodecs.push_back(*codec); + } else if (isAllowed) { + allowedCodecs.erase(allowedCodec); + } + + DependencyManager::get()->setSupportedCodecs(allowedCodecs); + } + + return true; + } + + void Audio::setInput(const AudioFormat& format) { + auto client = + DependencyManager::get(); + client->setInputFormat(format); + } + + void Audio::setOutput(const AudioFormat& format) { + DependencyManager::get()->setOutputFormat(format); + } + + AudioClient* Audio::getInputContext() { + return DependencyManager::get()->getInput(); + } + + AudioClient* Audio::getOutputContext() { + return DependencyManager::get()->getOutput(); + } + + void Audio::destroy() { + if (isEnabled()) { + DependencyManager::destroy(); + } + } + +} // namespace vircadia::client diff --git a/libraries/vircadia-client/src/internal/Audio.h b/libraries/vircadia-client/src/internal/Audio.h new file mode 100644 index 00000000000..f5dde791e5b --- /dev/null +++ b/libraries/vircadia-client/src/internal/Audio.h @@ -0,0 +1,66 @@ +// +// Audio.h +// libraries/vircadia-client/src/internal +// +// Created by Nshan G. on 4 July 2022. +// 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_VIRCADIA_CLIENT_SRC_INTERNAL_AUDIO_H +#define LIBRARIES_VIRCADIA_CLIENT_SRC_INTERNAL_AUDIO_H + +#include +#include +#include + +class Codec; +struct AudioFormat; +struct vircadia_vantage; +struct vircadia_bounds; + +namespace vircadia::client { + + class AudioClient; + + /// @private + class Audio { + public: + Audio(); + void enable(); + bool isEnabled() const; + const std::vector>& getCodecs(); + bool setCodecAllowed(std::string name, bool allow); + + const std::string& getSelectedCodecName(); + bool getIsMuted() const; + bool getIsMutedByMixer() const; + void setIsMuted(bool); + void setVantage(const vircadia_vantage&); + void setBounds(const vircadia_bounds&); + void setInputEcho(bool enabled); + void setOutputBufferFrames(int frames); + + void setServerInjectorGain(float gain); + + void setInput(const AudioFormat&); + void setOutput(const AudioFormat&); + + AudioClient* getInputContext(); + AudioClient* getOutputContext(); + + void destroy(); + private: + + std::vector> codecs; + std::vector> allowedCodecs; + + std::string selectedCodecName; + }; + +} // namespace vircadia::client + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/internal/Avatars.cpp b/libraries/vircadia-client/src/internal/Avatars.cpp new file mode 100644 index 00000000000..78869710f71 --- /dev/null +++ b/libraries/vircadia-client/src/internal/Avatars.cpp @@ -0,0 +1,66 @@ +// +// Avatars.cpp +// libraries/vircadia-client/src/internal +// +// Created by Nshan G. on 14 May 2022. +// 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 +// + +#include "Avatars.h" + +#include + +#include "avatars/AvatarManager.h" + +namespace vircadia::client +{ + + void Avatars::enable() { + if (!isEnabled()) { + DependencyManager::set(); + } + } + + void Avatars::update() { + assert(isEnabled()); + DependencyManager::get()->updateData(); + } + + void Avatars::updateManager() { + assert(isEnabled()); + DependencyManager::get()->update(); + } + + bool Avatars::isEnabled() const { + return DependencyManager::isSet(); + } + + void Avatars::destroy() { + DependencyManager::destroy(); + } + + AvatarData& Avatars::myAvatar() { + assert(isEnabled()); + return DependencyManager::get()->myAvatarDataIn; + } + + const std::vector& Avatars::all() const { + assert(isEnabled()); + return DependencyManager::get()->avatarDataOut; + } + + const std::vector>& Avatars::epitaphs() const { + assert(isEnabled()); + return DependencyManager::get()->epitaphsOut; + } + + std::vector& Avatars::views() { + assert(isEnabled()); + return DependencyManager::get()->viewsIn; + } + +} // namespace vircadia::client diff --git a/libraries/vircadia-client/src/internal/Avatars.h b/libraries/vircadia-client/src/internal/Avatars.h new file mode 100644 index 00000000000..57f6cbc7669 --- /dev/null +++ b/libraries/vircadia-client/src/internal/Avatars.h @@ -0,0 +1,53 @@ +// +// Avatars.h +// libraries/vircadia-client/src/internal +// +// Created by Nshan G. on 14 May 2022. +// 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_VIRCADIA_CLIENT_SRC_INTERNAL_AVATARS_H +#define LIBRARIES_VIRCADIA_CLIENT_SRC_INTERNAL_AVATARS_H + +#include +#include + +#include + +#include "Common.h" +#include "../avatars.h" + +struct ConicalViewFrustumData; + +namespace vircadia::client { + + struct AvatarData; + class AvatarManager; + + /// @private + class Avatars { + + public: + + void enable(); + void update(); + void updateManager(); + bool isEnabled() const; + int set(const AvatarData&); + + void destroy(); + + AvatarData& myAvatar(); + const std::vector& all() const; + const std::vector>& epitaphs() const; + std::vector& views(); + }; + + +} // namespace vircadia::client + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/internal/Common.cpp b/libraries/vircadia-client/src/internal/Common.cpp index 3b1a1b3d6a0..dbe52c9e3a6 100644 --- a/libraries/vircadia-client/src/internal/Common.cpp +++ b/libraries/vircadia-client/src/internal/Common.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 16 Apr 2022. // 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 @@ -24,4 +25,9 @@ namespace vircadia::client return to; } + QUuid qUuidfromBytes(const uint8_t* bytes) { + return QUuid::fromRfc4122(QByteArray( + reinterpret_cast(bytes), NUM_BYTES_RFC4122_UUID)); + } + } // namespace vircadia::client diff --git a/libraries/vircadia-client/src/internal/Common.h b/libraries/vircadia-client/src/internal/Common.h index 192afbf8e72..63fe3b81405 100644 --- a/libraries/vircadia-client/src/internal/Common.h +++ b/libraries/vircadia-client/src/internal/Common.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 16 Apr 2022. // 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 @@ -15,13 +16,16 @@ #include #include +#include + class QUuid; namespace vircadia::client { - using UUID = std::array; + using UUID = std::array; UUID toUUIDArray(const QUuid& from); + QUuid qUuidfromBytes(const uint8_t* bytes); } // namespace vircadia::client diff --git a/libraries/vircadia-client/src/internal/Context.cpp b/libraries/vircadia-client/src/internal/Context.cpp index 96c23af4fa9..79313b58e62 100644 --- a/libraries/vircadia-client/src/internal/Context.cpp +++ b/libraries/vircadia-client/src/internal/Context.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 27 March 2022. // 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 @@ -25,6 +26,7 @@ #include #include +#include "avatars/AvatarManager.h" #include "Error.h" namespace vircadia::client { @@ -33,7 +35,9 @@ namespace vircadia::client { argc(1), argvData("qt_is_such_a_joke"), argv(&argvData[0]), - messages_() + messages_(), + audio_(), + avatars_() { auto qtInitialization = qtInitialized.get_future(); appThread = std::thread{ [ this, nodeListParams, userAgent, info ] () { @@ -67,6 +71,14 @@ namespace vircadia::client { QTimer* domainCheckInTimer = new QTimer(nodeList.data()); QObject::connect(domainCheckInTimer, &QTimer::timeout, nodeList.data(), &NodeList::sendDomainServerCheckIn); domainCheckInTimer->start(DOMAIN_SERVER_CHECK_IN_MSECS); + QObject::connect(&qtApp, &QCoreApplication::aboutToQuit, [domainCheckInTimer](){ + domainCheckInTimer->stop(); + domainCheckInTimer->deleteLater(); + }); + + // you might think we could just do this in NodeList but we only want this connection for Interface + QObject::connect(&nodeList->getDomainHandler(), &DomainHandler::limitOfSilentDomainCheckInsReached, + nodeList.data(), [nodeList]() {nodeList->reset("Domain checkin limit"); }); // start the nodeThread so its event loop is running // (must happen after the checkin timer is created with the nodelist as it's parent) @@ -79,10 +91,24 @@ namespace vircadia::client { // TODO: account manager login - QObject::connect(&qtApp, &QCoreApplication::aboutToQuit, [this](){ + QTimer updateTimer; + QObject::connect(&updateTimer, &QTimer::timeout, [this]() { + if (avatars_.isEnabled()) { + auto nodeList = DependencyManager::get(); + auto avatarMixer = nodeList->soloNodeOfType(NodeType::AvatarMixer); + if (avatarMixer && avatarMixer->getActiveSocket()) { + avatars_.updateManager(); + } + } + }); + updateTimer.start(16); + + QObject::connect(&qtApp, &QCoreApplication::aboutToQuit, [this, &updateTimer](){ DependencyManager::prepareToExit(); { + updateTimer.stop(); + auto nodeList = DependencyManager::get(); // send the domain a disconnect packet, force stoppage of domain-server check-ins @@ -93,14 +119,24 @@ namespace vircadia::client { nodeList->getPacketReceiver().setShouldDropPackets(true); } + // TODO: a lot of objects destroyed below use + // deleteLater, and that doesn't seem to work + // reliably here (aboutToQuit signal), so maybe need + // to hand roll a quit event, to allow the event loop + // to clean things up, or not use deleteLater in this + // specific case + + avatars_.destroy(); + audio_.destroy(); + messages_.destroy(); + QThreadPool::globalInstance()->clear(); QThreadPool::globalInstance()->waitForDone(); - messages_.destroy(); - DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); + DependencyManager::destroy(); }); @@ -144,10 +180,16 @@ namespace vircadia::client { data.address = activeSocket != nullptr ? activeSocket->toString().toStdString() : ""; data.uuid = toUUIDArray(node->getUUID()); + data.inboundPPS = node->getInboundPPS(); + data.outboundPPS = node->getOutboundPPS(); + data.inboundKbps = node->getInboundKbps(); + data.outboundKbps = node->getOutboundKbps(); return data; }); }); + + sessionUUID = toUUIDArray(DependencyManager::get()->getSessionUUID()); } bool Context::isConnected() const { @@ -166,6 +208,26 @@ namespace vircadia::client { return messages_; } + Audio& Context::audio() { + return audio_; + } + + const Audio& Context::audio() const { + return audio_; + } + + Avatars& Context::avatars() { + return avatars_; + } + + const Avatars& Context::avatars() const { + return avatars_; + } + + const UUID& Context::getSessionUUID() const { + return sessionUUID; + } + std::list contexts; int checkContextValid(int id) { diff --git a/libraries/vircadia-client/src/internal/Context.h b/libraries/vircadia-client/src/internal/Context.h index 3fe0035cb37..7f8fdcbd00d 100644 --- a/libraries/vircadia-client/src/internal/Context.h +++ b/libraries/vircadia-client/src/internal/Context.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 27 March 2022. // 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 @@ -24,6 +25,8 @@ #include "../context.h" #include "Common.h" #include "Messages.h" +#include "Audio.h" +#include "Avatars.h" class QCoreApplication; @@ -35,6 +38,11 @@ namespace vircadia::client { bool active; std::string address; UUID uuid; + + float inboundPPS; + float outboundPPS; + float inboundKbps; + float outboundKbps; }; /// @private @@ -57,10 +65,19 @@ namespace vircadia::client { Messages& messages(); const Messages& messages() const; + Audio& audio(); + const Audio& audio() const; + + Avatars& avatars(); + const Avatars& avatars() const; + + const UUID& getSessionUUID() const; + private: std::thread appThread {}; std::atomic app {}; std::vector nodes {}; + UUID sessionUUID {}; std::promise qtInitialized {}; int argc; @@ -68,6 +85,8 @@ namespace vircadia::client { char* argv; Messages messages_; + Audio audio_; + Avatars avatars_; }; extern std::list contexts; diff --git a/libraries/vircadia-client/src/internal/DataTypeConversions.h b/libraries/vircadia-client/src/internal/DataTypeConversions.h new file mode 100644 index 00000000000..71f24b2740c --- /dev/null +++ b/libraries/vircadia-client/src/internal/DataTypeConversions.h @@ -0,0 +1,70 @@ +// +// DataTypeConversions.h +// libraries/vircadia-client/src/internal/avatars +// +// Created by Nshan G. on 23 July 2022. +// 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_VIRCADIA_CLIENT_SRC_INTERNAL_AVATARS_DATATYPECONVERSIONS_H +#define LIBRARIES_VIRCADIA_CLIENT_SRC_INTERNAL_AVATARS_DATATYPECONVERSIONS_H + +#include +#include +#include + +#include "../geometry.h" + +namespace vircadia::client +{ + + inline glm::vec3 glmVec3From(vircadia_vector v) { + return {v.x, v.y, v.z}; + } + + inline glm::quat glmQuatFrom(vircadia_quaternion q) { + return {q.w, q.x, q.y, q.z}; + } + + inline vircadia_vector vectorFrom(glm::vec3 v) { + return {v.x, v.y, v.z}; + } + + inline vircadia_quaternion quaternionFrom(glm::quat q) { + return {q.x, q.y, q.z, q.w}; + } + + template >* = nullptr> + vircadia_vector vectorFrom(PackedArray array) { + return {array.data[0], array.data[1], array.data[2]}; + } + + template >* = nullptr> + vircadia_quaternion quaternionFrom(PackedArray array) { + return {array.data[1], array.data[2], array.data[3], array.data[0]}; + } + + template + void serialize(vircadia_vector v, PackedArray& array) { + array.data[0] = v.x; + array.data[1] = v.y; + array.data[2] = v.z; + } + + template + void serialize(vircadia_quaternion q, PackedArray& array) { + array.data[0] = q.w; + array.data[1] = q.x; + array.data[2] = q.y; + array.data[3] = q.z; + } + +} // namespace vircadia::client + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/internal/Error.h b/libraries/vircadia-client/src/internal/Error.h index 94e380812d5..a799cf35c9f 100644 --- a/libraries/vircadia-client/src/internal/Error.h +++ b/libraries/vircadia-client/src/internal/Error.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 27 March 2022. // 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 @@ -34,8 +35,22 @@ enum class ErrorCode : int { PACKET_WRITE, - ARGUMENT_INVALID - + ARGUMENT_INVALID, + + AVATARS_DISABLED, + AVATAR_INVALID, + AVATAR_ATTACHMENT_INVALID, + AVATAR_JOINT_INVALID, + AVATAR_BONE_INVALID, + AVATAR_GRAB_INVALID, + AVATAR_ENTITY_INVALID, + AVATAR_VIEW_INVALID, + AVATAR_EPITAPH_INVALID, + + AUDIO_DISABLED, + AUDIO_FORMAT_INVALID, + AUDIO_CONTEXT_INVALID, + AUDIO_CODEC_INVALID }; inline int toInt(ErrorCode code) { @@ -51,6 +66,11 @@ bool isError(T* result) { return result == nullptr; } +template +auto makeError(int errorCode) { + return errorCode; +} + template auto chain(R result, F&& f) { if (isError(result)) { @@ -58,7 +78,7 @@ auto chain(R result, F&& f) { if constexpr (std::is_pointer_v) { return static_cast(nullptr); } else { - return result; + return makeError(result); } } else { return std::invoke(std::forward(f), result); @@ -74,7 +94,7 @@ auto chain(std::initializer_list result, F&& f) { if constexpr (std::is_pointer_v) { return static_cast(nullptr); } else { - return *error; + return makeError(*error); } } else { return std::invoke(std::forward(f), result); diff --git a/libraries/vircadia-client/src/internal/Messages.cpp b/libraries/vircadia-client/src/internal/Messages.cpp index e7bda00f619..aa1757070e2 100644 --- a/libraries/vircadia-client/src/internal/Messages.cpp +++ b/libraries/vircadia-client/src/internal/Messages.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 16 Apr 2022. // 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 @@ -133,6 +134,14 @@ namespace vircadia::client return ret; } + bool Messages::isAnyEnabled(std::bitset<8> type) const { + bool ret = false; + forAllSet(type, messageContexts, [&](auto& messagesContext) { + ret |= messagesContext.enabled; + }); + return ret; + } + int Messages::send(std::bitset<8> type, QString channel, QByteArray payload, bool localOnly) { auto client = DependencyManager::get(); diff --git a/libraries/vircadia-client/src/internal/Messages.h b/libraries/vircadia-client/src/internal/Messages.h index 41690de7cc8..a4da2ffb862 100644 --- a/libraries/vircadia-client/src/internal/Messages.h +++ b/libraries/vircadia-client/src/internal/Messages.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 16 Apr 2022. // 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 @@ -36,7 +37,7 @@ namespace vircadia::client { enum MessageType : uint8_t { MESSAGE_TYPE_TEXT = 1ul << MESSAGE_TYPE_TEXT_INDEX, MESSAGE_TYPE_DATA = 1ul << MESSAGE_TYPE_DATA_INDEX, - MESSAGE_TYPE_ANY = MESSAGE_TYPE_TEXT | MESSAGE_TYPE_DATA + MESSAGE_TYPE_ALL = MESSAGE_TYPE_TEXT | MESSAGE_TYPE_DATA }; /// @private @@ -67,6 +68,7 @@ namespace vircadia::client { void unsubscribe(QString channel) const; const std::vector& get(std::bitset<8> type) const; bool isEnabled(std::bitset<8> type) const; + bool isAnyEnabled(std::bitset<8> type) const; int send(std::bitset<8> type, QString channel, QByteArray payload, bool localOnly); void destroy(); diff --git a/libraries/vircadia-client/src/internal/audio/AudioClient.cpp b/libraries/vircadia-client/src/internal/audio/AudioClient.cpp new file mode 100644 index 00000000000..be20d045687 --- /dev/null +++ b/libraries/vircadia-client/src/internal/audio/AudioClient.cpp @@ -0,0 +1,182 @@ +// +// AudioClient.cpp +// libraries/vircadia-client/src/internal/audio +// +// Created by Nshan G. on 4 July 2022. +// 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 +// + +#include "AudioClient.h" + +#include + +#include "../DataTypeConversions.h" + +namespace vircadia::client +{ + + AudioClient::AudioClient(const std::vector>& supportedCodecs) : + AudioPacketHandler(), + position(), + orientation(), + codecs(supportedCodecs), + updateTimer(this), + codecsIn(codecs), + inputFormat{}, + outputFormat{}, + input(nullptr), + output(nullptr), + selectedCodecName(), + isMuted(false), + isMutedByMixer(false), + inputEcho(false), + vantage{{0.f, 0.f, 0.f}, {0.f,0.f,0.f,1.f}}, + bounds{{0.f, 0.f, 0.f}, {0.f, 0.f, 0.f}} + { + + setPositionGetter([this]() { return position; }); + setOrientationGetter([this]() { return orientation; }); + + setCustomDeleter([](Dependency* dependency){ + static_cast(dependency)->deleteLater(); + }); + + startThread(); + } + + void AudioClient::onStart() { + connect(&updateTimer, &QTimer::timeout, this, &AudioClient::update); + updateTimer.start(); + } + + void AudioClient::setInputFormat(AudioFormat format) { + std::scoped_lock lock(inout); + inputFormat = format; + input = nullptr; + } + + void AudioClient::setOutputFormat(AudioFormat format) { + std::scoped_lock lock(inout); + outputFormat = format; + output = nullptr; + } + + AudioClient* AudioClient::getInput() { + std::scoped_lock lock(inout); + return input; + } + + AudioClient* AudioClient::getOutput() { + std::scoped_lock lock(inout); + return output; + } + + void AudioClient::update() { + { + std::scoped_lock lock(inout); + + if (inputFormat != _inputFormat) { + cleanupInput(); + if (inputFormat.isValid()) { + setupInput(inputFormat); + } else if (!isDummyInput()) { + setupDummyInput(); + } + } + + if (_inputFormat.isValid()) { + input = this; + } + + if (outputFormat != _outputFormat) { + cleanupOutput(); + if (outputFormat.isValid()) { + setupOutput(outputFormat); + } + } + + if (_outputFormat.isValid()) { + output = this; + } + + selectedCodecName = _selectedCodecName.toStdString(); + + if (codecs != codecsIn) { + codecs = codecsIn; + negotiateAudioFormat(); + } + + position = glmVec3From(vantage.position); + orientation = glmQuatFrom(vantage.rotation); + avatarBoundingBoxCorner = glmVec3From(bounds.offset); + avatarBoundingBoxScale = glmVec3From(bounds.dimensions); + + _isMuted = isMuted || isMutedByMixer; + setServerEcho(inputEcho); + } + + if (_inputFormat.isValid()) { + sendInput(); + } + } + + void AudioClient::onMutedByMixer() { + std::scoped_lock lock(inout); + isMutedByMixer = true; + } + + void AudioClient::setSupportedCodecs(const std::vector>& supportedCodecs) { + std::scoped_lock lock(inout); + codecsIn = supportedCodecs; + } + + const std::vector>& AudioClient::getSupportedCodecs() const { + return codecs; + } + + AudioClient::AudioOutputIODevice& AudioClient::getOutputIODevice() { + return _audioOutputIODevice; + } + + std::string AudioClient::getSelectedCodecName() const { + std::scoped_lock lock(inout); + return _selectedCodecName.toStdString(); + } + + bool AudioClient::getIsMuted() const { + std::scoped_lock lock(inout); + return isMuted; + } + + bool AudioClient::getIsMutedByMixer() const { + std::scoped_lock lock(inout); + return isMutedByMixer; + } + + void AudioClient::setVantage(vircadia_vantage value) { + std::scoped_lock lock(inout); + vantage = value; + } + + void AudioClient::setBounds(vircadia_bounds value) { + std::scoped_lock lock(inout); + bounds = value; + } + + void AudioClient::setInputEcho(bool enabled) { + std::scoped_lock lock(inout); + inputEcho = enabled; + } + + void AudioClient::setIsMuted(bool muted) { + std::scoped_lock lock(inout); + isMuted = muted; + } + +} // namespace vircadia::client + +template class AudioPacketHandler; diff --git a/libraries/vircadia-client/src/internal/audio/AudioClient.h b/libraries/vircadia-client/src/internal/audio/AudioClient.h new file mode 100644 index 00000000000..de2fcb81a9d --- /dev/null +++ b/libraries/vircadia-client/src/internal/audio/AudioClient.h @@ -0,0 +1,87 @@ +// +// AudioClient.h +// libraries/vircadia-client/src/internal/audio +// +// Created by Nshan G. on 4 July 2022. +// 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_VIRCADIA_CLIENT_SRC_INTERNAL_AUDIO_AUDIOCLIENT_H +#define LIBRARIES_VIRCADIA_CLIENT_SRC_INTERNAL_AUDIO_AUDIOCLIENT_H + +#include + +#include + +#include "../../audio.h" + +namespace vircadia::client +{ + + /// @private + class AudioClient : public QObject, public Dependency, public AudioPacketHandler { + Q_OBJECT + public: + + AudioClient(const std::vector>& supportedCodecs); + void onStart(); + + void setSupportedCodecs(const std::vector>&); + const std::vector>& getSupportedCodecs() const; + + void setInputFormat(AudioFormat); + void setOutputFormat(AudioFormat); + + AudioClient* getInput(); + AudioClient* getOutput(); + + std::string getSelectedCodecName() const; + + bool getIsMuted() const; + bool getIsMutedByMixer() const; + + void setVantage(vircadia_vantage); + void setBounds(vircadia_bounds); + void setInputEcho(bool enabled); + void setIsMuted(bool muted); + + AudioOutputIODevice& getOutputIODevice(); + + private slots: + void update(); + + private: + + void onMutedByMixer(); + + friend class AudioPacketHandler; + + glm::vec3 position; + glm::quat orientation; + std::vector> codecs; + + QTimer updateTimer; + + std::vector> codecsIn; + AudioFormat inputFormat; + AudioFormat outputFormat; + AudioClient* input; + AudioClient* output; + std::string selectedCodecName; + bool isMuted; + bool isMutedByMixer; + bool inputEcho; + vircadia_vantage vantage; + vircadia_bounds bounds; + + + mutable std::mutex inout; + }; + +} // namespace vircadia::client + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/internal/avatars/AvatarData.cpp b/libraries/vircadia-client/src/internal/avatars/AvatarData.cpp new file mode 100644 index 00000000000..8eccddac7c6 --- /dev/null +++ b/libraries/vircadia-client/src/internal/avatars/AvatarData.cpp @@ -0,0 +1,491 @@ +// +// AvatarData.cpp +// libraries/vircadia-client/src/internal/avatars +// +// Created by Nshan G. on 9 May 2022. +// 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 +// + +#include "AvatarData.h" + +#include + +#include "../DataTypeConversions.h" + +using vircadia::client::operator==; +using vircadia::client::operator!=; + +namespace vircadia::client +{ + + QUuid Avatar::getSessionUUID() const { + return qUuidfromBytes(data.sessionUUID.data()); + } + + void Avatar::setSessionUUID(const QUuid& uuid) { + data.sessionUUID = toUUIDArray(uuid); + } + + QUuid Avatar::getSessionUUIDOut() const { + return nodeUUID; + } + + AvatarDataPacket::Identity Avatar::getIdentityDataOut() const { + auto identity = std::get(data.properties); + QVector attachments; + std::transform(identity.attachments.begin(), identity.attachments.end(), + std::back_inserter(attachments), [](const auto& x){ + return AttachmentData{ + QString(x.modelURL.c_str()), + QString(x.jointName.c_str()), + glmVec3From(x.transform.vantage.position), + glmQuatFrom(x.transform.vantage.rotation), + x.transform.scale, + x.isSoft + }; + }); + return { + std::move(attachments), + QString(identity.displayName.c_str()), + QString(identity.sessionDisplayName.c_str()), + identity.identityFlags + }; + } + + void Avatar::setIdentityDataIn(AvatarDataPacket::Identity identity) { + std::vector attachments; + std::transform(identity.attachmentData.begin(), identity.attachmentData.end(), + std::back_inserter(attachments), [](const auto& x) { + return AvatarData::Attachment{ + x.modelURL.toString().toStdString(), + x.jointName.toStdString(), + vectorFrom(x.translation), + quaternionFrom(x.rotation), + x.scale, + x.isSoft + }; + } + ); + data.setProperty({ + std::move(attachments), + identity.displayName.toStdString(), + identity.sessionDisplayName.toStdString(), + identity.identityFlags + }); + } + + bool Avatar::getIdentityDataChanged() const { + return data.changes.test(AvatarData::IdentityIndex); + } + + udt::SequenceNumber Avatar::getIdentitySequenceNumberOut() const { + return data.identitySequenceNumber; + } + + void Avatar::setIdentitySequenceNumberIn(udt::SequenceNumber number) { + data.identitySequenceNumber = number; + } + + void Avatar::pushIdentitySequenceNumber() { + ++data.identitySequenceNumber; + } + + void Avatar::onIdentityDataSent() { + data.changes.reset(AvatarData::IdentityIndex); + } + + bool Avatar::getSkeletonModelURLChanged() const { + return data.changes.test(AvatarData::SkeletonModelURLIndex); + } + + bool Avatar::getSkeletonDataChanged() const { + return data.changes.test(AvatarData::SkeletonDataIndex); + } + + bool Avatar::getGrabDataChanged() const { + return data.changes.test(AvatarData::GrabDataIndex); + } + + bool Avatar::getEntityDataChanged() const { + return data.changes.test(AvatarData::EntityDataIndex); + } + + void Avatar::onClientTraitsSent() { + data.changes.reset(AvatarData::SkeletonModelURLIndex); + data.changes.reset(AvatarData::SkeletonDataIndex); + data.changes.reset(AvatarData::GrabDataIndex); + data.changes.reset(AvatarData::EntityDataIndex); + } + + AvatarDataPacket::AvatarGlobalPosition Avatar::getGlobalPositionOut() const { + auto position = data.getProperty(); + AvatarDataPacket::AvatarGlobalPosition result; + serialize(position, result.globalPosition); + return result; + } + + void Avatar::setGlobalPositionIn(const AvatarDataPacket::AvatarGlobalPosition& position) { + data.setProperty( + vectorFrom(position.globalPosition)); + } + + AvatarDataPacket::AvatarBoundingBox Avatar::getBoundingBoxOut() const { + auto bounds = data.getProperty(); + AvatarDataPacket::AvatarBoundingBox result; + serialize(bounds.dimensions, result.avatarDimensions); + serialize(bounds.offset, result.boundOriginOffset); + return result; + } + + void Avatar::setBoundingBoxIn(const AvatarDataPacket::AvatarBoundingBox& bounds) { + data.setProperty({ + vectorFrom(bounds.avatarDimensions), + vectorFrom(bounds.boundOriginOffset), + }); + } + + glm::quat Avatar::getOrientationOut() const { + return glmQuatFrom(data.getProperty()); + } + + void Avatar::setOrientationIn(glm::quat orientation) { + data.setProperty( + quaternionFrom(orientation)); + } + + float Avatar::getScaleOut() const { + return data.getProperty(); + } + + void Avatar::setScaleIn(float scale) { + data.setProperty(scale); + } + + AvatarDataPacket::LookAtPosition Avatar::getLookAtPositionOut() const { + auto& position = data.getProperty(); + AvatarDataPacket::LookAtPosition result; + serialize(position, result.lookAtPosition); + return result; + } + + void Avatar::setLookAtPositionIn(const AvatarDataPacket::LookAtPosition& position) { + data.setProperty({ + vectorFrom(position.lookAtPosition) + }); + } + + float Avatar::getAudioLoudnessOut() const { + return data.getProperty(); + } + + void Avatar::setAudioLoudnessIn(float loudness) { + data.setProperty(loudness); + } + + Avatar::Transform Avatar::getSensorToWorldMatrixOut() const { + auto transform = data.getProperty(); + return { + glmVec3From(transform.vantage.position), + glmQuatFrom(transform.vantage.rotation), + glm::vec3(transform.scale) + }; + } + + void Avatar::setSensorToWorldMatrixIn(Transform transform) { + data.setProperty({ + { + vectorFrom(transform.translation), + quaternionFrom(transform.rotation) + }, + transform.scale.x + }); + } + + Avatar::AdditionalFlags Avatar::getAdditionalFlagsOut() const { + auto flags = data.getProperty(); + return { + KeyState(flags.key_state), + flags.hand_state, + bool(flags.has_head_scripted_blendshapes), + bool(flags.has_procedural_eye_movement), + bool(flags.has_audio_face_movement), + bool(flags.has_procedural_eye_face_movement), + bool(flags.has_procedural_blink_face_movement), + bool(flags.collides_with_avatars), + bool(flags.has_priority) + }; + }; + + void Avatar::setAdditionalFlagsIn(vircadia_avatar_additional_flags flags) { + data.setProperty(flags); + } + + Avatar::ParentInfo Avatar::getParentInfoOut() const { + auto parent = data.getProperty(); + UUID id; + std::copy_n(parent.uuid, id.size(), id.data()); + return { id, parent.joint_index}; + } + + void Avatar::setParentInfoIn(const uint8_t* uuid, uint16_t jointIndex) { + vircadia_avatar_parent_info info{}; + std::copy_n(uuid, UUID{}.size(), info.uuid); + info.joint_index = jointIndex; + data.setProperty(info); + } + + AvatarDataPacket::AvatarLocalPosition Avatar::getLocalPositionOut() const { + auto position = data.getProperty(); + AvatarDataPacket::AvatarLocalPosition result; + serialize(position, result.localPosition); + return result; + } + + void Avatar::setLocalPositionIn(const AvatarDataPacket::AvatarLocalPosition& position) { + data.setProperty( + vectorFrom(position.localPosition)); + } + + Avatar::HandControllers Avatar::getHandControllersOut() const { + auto controllers = data.getProperty(); + return { + { + glmVec3From(controllers.left.position), + glmQuatFrom(controllers.left.rotation), + }, + { + glmVec3From(controllers.right.position), + glmQuatFrom(controllers.right.rotation), + } + }; + } + + void Avatar::setHandControllersIn(const HandControllers& controllers) { + data.setProperty({ + { + vectorFrom(controllers.left.position), + quaternionFrom(controllers.left.orientation) + }, + { + vectorFrom(controllers.right.position), + quaternionFrom(controllers.right.orientation) + } + }); + } + + Avatar::FaceTrackerInfo Avatar::getFaceTrackerInfoOut() const { + auto& info = data.getProperty(); + return { + info.left_eye_blink, + info.right_eye_blink, + info.average_loundness, + info.brow_audio_lift, + info.blendshape_coefficients_size, + reinterpret_cast(info.blendshape_coefficients) + }; + } + + void Avatar::setFaceTrackerInfoIn(const FaceTrackerInfo& info) + { + auto [ + left_eye_blink, + right_eye_blink, + average_loundness, + brow_audio_lift, + blendshape_coefficients_size, + blendshape_coefficients + ] = info; + vircadia_avatar_face_tracker_info setInfo{}; + setInfo.left_eye_blink = left_eye_blink; + setInfo.right_eye_blink = right_eye_blink; + setInfo.average_loundness = average_loundness; + setInfo.brow_audio_lift = brow_audio_lift; + setInfo.blendshape_coefficients_size = blendshape_coefficients_size; + std::memcpy(setInfo.blendshape_coefficients, blendshape_coefficients, blendshape_coefficients_size * sizeof(float)); + data.setProperty(setInfo); + } + + int Avatar::getJointDataSizeOut() const { + return data.getProperty().size(); + } + + JointData Avatar::getJointDataOut(int index) const { + if (index < 0) { + return JointData{}; + } + + const auto& joints = data.getProperty(); + const auto& allFlags = data.getProperty(); + const auto& joint = size_t(index) < joints.size() ? joints[index] : vircadia_vantage{}; + const auto& flags = size_t(index) < allFlags.size() ? allFlags[index] : vircadia_joint_flags{}; + return { + glmQuatFrom(joint.rotation), + glmVec3From(joint.position), + bool(flags.rotation_is_default), + bool(flags.translation_is_default) + }; + } + + void Avatar::setJointDataSizeIn(int size) { + data.resizeProperty(size); + data.resizeProperty(size); + } + + void Avatar::setJointDataPositionIn(int index, glm::vec3 position) { + auto joint = data.getProperty()[index]; + joint.position = vectorFrom(position); + data.setProperty(index, joint); + } + + void Avatar::setJointDataRotationIn(int index, glm::quat rotation) { + auto joint = data.getProperty()[index]; + joint.rotation = quaternionFrom(rotation); + data.setProperty(index, joint); + } + + void Avatar::setJointDataPositionDefaultIn(int index, bool isDefault) { + auto flags = data.getProperty()[index]; + flags.translation_is_default = isDefault; + data.setProperty(index, flags); + } + + void Avatar::setJointDataRotationDefaultIn(int index, bool isDefault) { + auto flags = data.getProperty()[index]; + flags.rotation_is_default = isDefault; + data.setProperty(index, flags); + } + + AvatarDataPacket::FarGrabJoints Avatar::getFarGrabJointsOut() const { + auto joints = data.getProperty(); + AvatarDataPacket::FarGrabJoints result; + serialize(joints.left.position, result.leftFarGrabPosition); + serialize(joints.left.rotation, result.leftFarGrabRotation); + serialize(joints.right.position, result.rightFarGrabPosition); + serialize(joints.right.rotation, result.rightFarGrabRotation); + serialize(joints.mouse.position, result.mouseFarGrabPosition); + serialize(joints.mouse.rotation, result.mouseFarGrabRotation); + return result; + } + + void Avatar::setFarGrabJointsIn(const AvatarDataPacket::FarGrabJoints& joints) { + data.setProperty({ + { + vectorFrom(joints.leftFarGrabPosition), + quaternionFrom(joints.leftFarGrabRotation) + }, + { + vectorFrom(joints.rightFarGrabPosition), + quaternionFrom(joints.rightFarGrabRotation) + }, + { + vectorFrom(joints.mouseFarGrabPosition), + quaternionFrom(joints.mouseFarGrabRotation) + } + }); + } + + const char* Avatar::getSkeletonModelURLOut() const { + return data.getProperty().c_str(); + } + + void Avatar::setSkeletonModelURLIn(const QByteArray& encodedURL) { + data.setProperty(encodedURL.toStdString()); + } + + std::vector Avatar::getSkeletonDataOut() const { + auto bones = data.getProperty(); + std::vector result; + int stringStart = 0; + std::transform(bones.begin(), bones.end(), std::back_inserter(result), [&stringStart](auto& bone) { + AvatarSkeletonTrait::UnpackedJointData result{}; + result.boneType = bone.type; + result.defaultTranslation = glmVec3From(bone.defaultTransform.vantage.position); + result.defaultRotation = glmQuatFrom(bone.defaultTransform.vantage.rotation); + result.defaultScale = bone.defaultTransform.scale; + result.jointIndex = bone.index; + result.parentIndex = bone.parentIndex; + result.jointName = QString(bone.name.c_str()); + + // TODO: this logic belongs in AvatarDataStream + result.stringStart = stringStart; + result.stringLength = result.jointName.size(); + stringStart += result.stringLength; + + return result; + + }); + return result; + } + + void Avatar::setSkeletonDataIn(std::vector skeletonData) { + constexpr auto index = AvatarData::SkeletonDataIndex; + data.resizeProperty(skeletonData.size()); + for (std::size_t i = 0; i != skeletonData.size(); ++i) { + data.setProperty(i, AvatarData::Bone{ + AvatarSkeletonTrait::BoneType(skeletonData[i].boneType), + { + { + vectorFrom(skeletonData[i].defaultTranslation), + quaternionFrom(skeletonData[i].defaultRotation) + }, + skeletonData[i].defaultScale + }, + skeletonData[i].jointIndex, + skeletonData[i].parentIndex, + skeletonData[i].jointName.toStdString() + }); + } + } + + void Avatar::setGrabDataIn(const QMap* grabData) { + if (grabData == nullptr) { + grabData = &_avatarGrabData; + } + data.resizeProperty(grabData->size()); + int i = 0; + for (auto begin = grabData->begin(); begin != grabData->end(); ++begin) { + + Grab grab; + grab.fromByteArray(*begin); + + vircadia_avatar_grab converted{}; + auto targetID = toUUIDArray(grab.getTargetID()); + std::copy_n(targetID.begin(), targetID.size(), converted.target_id); + converted.joint_index = grab.getParentJointIndex(); + converted.offset.position = vectorFrom(grab.getPositionalOffset()); + converted.offset.rotation = quaternionFrom(grab.getRotationalOffset()); + + data.setProperty(i++, + std::pair{ toUUIDArray(begin.key()), converted }); + } + } + + const QMap& Avatar::getGrabData() const { + return _avatarGrabData; + } + + void Avatar::onParseError(std::string) { + //TODO keep a log of last N errors + } + + void Avatar::onPacketTooSmallError(std::string, int, int) { + //TODO keep a log of last N errors + } + + void Avatar::onGrabRemoved(QUuid) {}; + + ClientTraitsHandler* Avatar::getClientTraitsHandler() { + return clientTraitsHandler.get(); + } + + const ClientTraitsHandler* Avatar::getClientTraitsHandler() const { + return clientTraitsHandler.get(); + } + +} // namespace vircadia::client + +template class AvatarDataStream; diff --git a/libraries/vircadia-client/src/internal/avatars/AvatarData.h b/libraries/vircadia-client/src/internal/avatars/AvatarData.h new file mode 100644 index 00000000000..da3dfffef98 --- /dev/null +++ b/libraries/vircadia-client/src/internal/avatars/AvatarData.h @@ -0,0 +1,457 @@ +// +// AvatarData.h +// libraries/vircadia-client/src/internal/avatars +// +// Created by Nshan G. on 9 May 2022. +// 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_VIRCADIA_CLIENT_SRC_INTERNAL_AVATARS_AVATARDATA_H +#define LIBRARIES_VIRCADIA_CLIENT_SRC_INTERNAL_AVATARS_AVATARDATA_H + +#include +#include +#include + +#include + +#include + +#include "../Common.h" +#include "../../avatars.h" +#include "ClientTraitsHandler.h" +#include "../DataTypeConversions.h" + +namespace vircadia::client +{ + /// @private + template struct overloaded : Ts... { using Ts::operator()...; }; + template overloaded(Ts...) -> overloaded; + + inline bool operator==(const vircadia_vector& a, const vircadia_vector& b) { + return a.x == b.x && a.y == b.y && a.z == b.z; + } + inline bool operator!=(const vircadia_vector& a, const vircadia_vector& b) { + return !(a == b); + } + + inline bool operator==(const vircadia_quaternion& a, const vircadia_quaternion& b) { + return a.x == b.x && a.y == b.y && a.z == b.z && a.w == b.w; + } + inline bool operator!=(const vircadia_quaternion& a, const vircadia_quaternion& b) { + return !(a == b); + } + + inline bool operator==(const vircadia_bounds& a, const vircadia_bounds& b) { + return a.dimensions == b.dimensions && a.offset == b.offset; + } + inline bool operator!=(const vircadia_bounds& a, const vircadia_bounds& b) { + return !(a == b); + } + + inline bool operator==(const vircadia_vantage& a, const vircadia_vantage& b) { + return a.position == b.position && a.rotation == b.rotation; + } + inline bool operator!=(const vircadia_vantage& a, const vircadia_vantage& b) { + return !(a == b); + } + + inline bool operator==(const vircadia_transform& a, const vircadia_transform& b) { + return a.vantage == b.vantage && a.scale == b.scale; + } + inline bool operator!=(const vircadia_transform& a, const vircadia_transform& b) { + return !(a == b); + } + + inline bool operator==(const vircadia_avatar_additional_flags& a, const vircadia_avatar_additional_flags& b) { + return a.hand_state == b.hand_state && + a.key_state == b.key_state && + bool(a.has_head_scripted_blendshapes) == bool(b.has_head_scripted_blendshapes) && + bool(a.has_procedural_eye_movement) == bool(b.has_procedural_eye_movement) && + bool(a.has_audio_face_movement) == bool(b.has_audio_face_movement) && + bool(a.has_procedural_eye_face_movement) == bool(b.has_procedural_eye_face_movement) && + bool(a.has_procedural_blink_face_movement) == bool(b.has_procedural_blink_face_movement) && + bool(a.collides_with_avatars) == bool(b.collides_with_avatars) && + bool(a.has_priority) == bool(b.has_priority); + } + inline bool operator!=(const vircadia_avatar_additional_flags& a, const vircadia_avatar_additional_flags& b) { + return !(a == b); + } + + inline bool operator==(const vircadia_avatar_parent_info& a, const vircadia_avatar_parent_info& b) { + return a.joint_index == b.joint_index && + std::equal(a.uuid, a.uuid+16, b.uuid); + } + inline bool operator!=(const vircadia_avatar_parent_info& a, const vircadia_avatar_parent_info& b) { + return !(a == b); + } + + inline bool operator==(const vircadia_avatar_hand_controllers& a, const vircadia_avatar_hand_controllers& b) { + return a.left == b.left && a.right == b.right; + } + inline bool operator!=(const vircadia_avatar_hand_controllers& a, const vircadia_avatar_hand_controllers& b) { + return !(a == b); + } + + inline bool operator==(const vircadia_avatar_face_tracker_info& a, const vircadia_avatar_face_tracker_info& b) { + assert(a.blendshape_coefficients_size < 256); + assert(b.blendshape_coefficients_size < 256); + return a.left_eye_blink == b.left_eye_blink && + a.right_eye_blink == b.right_eye_blink && + a.average_loundness == b.average_loundness && + a.brow_audio_lift == b.brow_audio_lift && + a.blendshape_coefficients_size == b.blendshape_coefficients_size && + std::equal(a.blendshape_coefficients, a.blendshape_coefficients + a.blendshape_coefficients_size, b.blendshape_coefficients); + } + inline bool operator!=(const vircadia_avatar_face_tracker_info& a, const vircadia_avatar_face_tracker_info& b) { + return !(a == b); + } + + inline bool operator==(const vircadia_joint_flags& a, const vircadia_joint_flags& b) { + return bool(a.translation_is_default) == bool(b.translation_is_default) && + bool(a.rotation_is_default) == bool(b.rotation_is_default); + } + inline bool operator!=(const vircadia_joint_flags& a, const vircadia_joint_flags& b) { + return !(a == b); + } + + inline bool operator==(const vircadia_far_grab_joints& a, const vircadia_far_grab_joints& b) { + return a.left == b.left && a.right == b.right && a.mouse == b.mouse; + } + inline bool operator!=(const vircadia_far_grab_joints& a, const vircadia_far_grab_joints& b) { + return !(a == b); + } + + inline bool operator==(const vircadia_avatar_grab& a, const vircadia_avatar_grab& b) { + return a.joint_index == b.joint_index && + a.offset == b.offset && + std::equal(a.target_id, a.target_id + NUM_BYTES_RFC4122_UUID, b.target_id); + } + inline bool operator!=(const vircadia_avatar_grab& a, const vircadia_avatar_grab& b) { + return !(a == b); + } + + /// @private + struct AvatarData { + + struct Attachment { + std::string modelURL; + std::string jointName; + vircadia_transform transform; + bool isSoft; + + bool operator==(const Attachment& other) const { + return modelURL == other.modelURL && + jointName == other.jointName && + transform == other.transform && + isSoft == other.isSoft; + } + bool operator!=(const Attachment& other) const + { return !(*this == other); } + }; + + struct Identity { + std::vector attachments; + std::string displayName; + std::string sessionDisplayName; + AvatarDataPacket::IdentityFlags identityFlags; + + bool operator==(const Identity& other) const { + return attachments == other.attachments && + displayName == other.displayName && + sessionDisplayName == other.sessionDisplayName && + identityFlags == other.identityFlags; + } + bool operator!=(const Identity& other) const + { return !(*this == other); } + }; + + struct Bone { + AvatarSkeletonTrait::BoneType type; + vircadia_transform defaultTransform; + int index; + int parentIndex; + std::string name; + + bool operator==(const Bone& other) const { + return type == other.type && + defaultTransform == other.defaultTransform && + index == other.index && + parentIndex == other.parentIndex && + name == other.name; + } + bool operator!=(const Bone& other) const + { return !(*this == other); } + + operator AvatarSkeletonTrait::UnpackedJointData() { + return { + 0,0, + type, + glmVec3From(defaultTransform.vantage.position), + glmQuatFrom(defaultTransform.vantage.rotation), + defaultTransform.scale, + index, + parentIndex, + QString::fromUtf8(name.c_str()) + }; + } + }; + + enum PropertyIndex : size_t { + GlobalPositionIndex, + BoundingBoxIndex, + OrientationIndex, + ScaleIndex, + LookAtPositionIndex, + AudioLoudnessIndex, + SensorToWorldMatrixIndex, + AdditionalFlagsIndex, + ParentInfoIndex, + LocalPositionIndex, + HandControllersIndex, + FaceTrackerInfoIndex, + JointDataIndex, + JointDefaultPoseFlagsIndex, + GrabJointsIndex, + + IdentityIndex, + SkeletonModelURLIndex, + SkeletonDataIndex, + GrabDataIndex, + EntityDataIndex + }; + + UUID sessionUUID; + udt::SequenceNumber identitySequenceNumber { 0 }; + + using Properties = std::tuple< + vircadia_vector, // global position + vircadia_bounds, // bounding box + vircadia_quaternion, // orientation + float, // scale + vircadia_vector, // look at position + float, // audio loudness + vircadia_transform, // sensor to world matrix + vircadia_avatar_additional_flags, // additional flags + vircadia_avatar_parent_info, // parent info + vircadia_vector, // local position + vircadia_avatar_hand_controllers, // hand controllers + vircadia_avatar_face_tracker_info, // face tracker info + std::vector, // joint data + std::vector, // joint default pose flags + vircadia_far_grab_joints, // grab joints + + Identity, // identity + std::string, // skeleton model URL + std::vector, // skeleton data + std::vector>, + std::vector>> + >; + Properties properties; + std::bitset> changes; + + struct { + std::vector added; + std::vector removed; + } grabs; + + template , + std::enable_if_t< + std::is_invocable_v && + std::is_invocable_v + >* = nullptr + > + bool setProperty(Accessor&& accessor, Value value) { + const auto& current = accessor(std::get(properties)); + if (current != value) { + accessor(std::get(properties), std::move(value)); + changes.set(Index); + + // NOTE: grab joint data is hard coded in packet + // handling code to be sent and received together + // with joint data only... not sure why though, + // when something like joint default pose flags + // are handled completely separately + if (Index == AvatarData::GrabJointsIndex) { + changes.set(AvatarData::JointDataIndex); + } + + return true; + } + return false; + } + + template + bool setProperty(std::tuple_element_t value) { + auto noopAccessor = overloaded { + [](auto& x) -> decltype(auto) { + return (x); + }, + [](auto& x, auto&& v){ + x = std::forward(v); + } + }; + return setProperty(noopAccessor, value); + } + + template + bool setProperty(int element, Value value) { + return setProperty( overloaded { + [element](auto& x) -> decltype(auto) { + return (x[element]); + }, + [element](auto& x, auto&& v) { + x[element] = std::forward(v); + }, + }, value); + } + + template + bool resizeProperty(size_t size) { + return setProperty( overloaded { + [](auto&& x){ + return x.size(); + }, + [](auto& x, size_t size){ + x.resize(size); + }, + }, size); + } + + template + const auto& getProperty() const { + return std::get(properties); + } + + }; + + class AvatarManager; + + /// @private + class Avatar : public AvatarDataStream { + + public: + + QUuid getSessionUUID() const; + void setSessionUUID(const QUuid& uuid); + + private: + + QUuid getSessionUUIDOut() const; + + AvatarDataPacket::Identity getIdentityDataOut() const; + void setIdentityDataIn(AvatarDataPacket::Identity identity); + bool getIdentityDataChanged() const; + udt::SequenceNumber getIdentitySequenceNumberOut() const; + void setIdentitySequenceNumberIn(udt::SequenceNumber); + void pushIdentitySequenceNumber(); + void onIdentityDataSent(); + + bool getSkeletonModelURLChanged() const; + bool getSkeletonDataChanged() const; + bool getGrabDataChanged() const; + bool getEntityDataChanged() const; + void onClientTraitsSent(); + + AvatarDataPacket::AvatarGlobalPosition getGlobalPositionOut() const; + void setGlobalPositionIn(const AvatarDataPacket::AvatarGlobalPosition&); + + AvatarDataPacket::AvatarBoundingBox getBoundingBoxOut() const; + void setBoundingBoxIn(const AvatarDataPacket::AvatarBoundingBox&); + + glm::quat getOrientationOut() const; + void setOrientationIn(glm::quat); + + float getScaleOut() const; + void setScaleIn(float); + + AvatarDataPacket::LookAtPosition getLookAtPositionOut() const; + void setLookAtPositionIn(const AvatarDataPacket::LookAtPosition&); + + float getAudioLoudnessOut() const; + void setAudioLoudnessIn(float); + + struct Transform { + glm::vec3 translation; + glm::quat rotation; + glm::vec3 scale; + }; + + Transform getSensorToWorldMatrixOut() const; + void setSensorToWorldMatrixIn(Transform); + + using AdditionalFlags = std::tuple< + KeyState, uint8_t, bool, bool, bool, bool, bool, bool, bool + >; + + AdditionalFlags getAdditionalFlagsOut() const; + void setAdditionalFlagsIn(vircadia_avatar_additional_flags); + + using ParentInfo = std::tuple; + + ParentInfo getParentInfoOut() const; + void setParentInfoIn(const uint8_t* uuid, uint16_t jointIndex); + + void onParseError(std::string message); + void onPacketTooSmallError(std::string name, int sizeToRead, int sizeLeft); + + AvatarDataPacket::AvatarLocalPosition getLocalPositionOut() const; + void setLocalPositionIn(const AvatarDataPacket::AvatarLocalPosition&); + + struct HandControllers { + HandControllerVantage left; + HandControllerVantage right; + }; + + HandControllers getHandControllersOut() const; + void setHandControllersIn(const HandControllers&); + + using FaceTrackerInfo = std::tuple; + + FaceTrackerInfo getFaceTrackerInfoOut() const; + void setFaceTrackerInfoIn(const FaceTrackerInfo&); + + int getJointDataSizeOut() const; + JointData getJointDataOut(int index) const; + void setJointDataSizeIn(int size); + void setJointDataPositionIn(int index, glm::vec3); + void setJointDataRotationIn(int index, glm::quat); + void setJointDataPositionDefaultIn(int index, bool); + void setJointDataRotationDefaultIn(int index, bool); + + AvatarDataPacket::FarGrabJoints getFarGrabJointsOut() const; + void setFarGrabJointsIn(const AvatarDataPacket::FarGrabJoints&) ; + + void setSkeletonModelURLIn(const QByteArray& encodedURL); + const char* getSkeletonModelURLOut() const; + + std::vector getSkeletonDataOut() const; + void setSkeletonDataIn(std::vector); + + void setGrabDataIn(const QMap* grabData = nullptr); + const QMap& getGrabData() const; + + void onGrabRemoved(QUuid); + + ClientTraitsHandler* getClientTraitsHandler(); + const ClientTraitsHandler* getClientTraitsHandler() const; + + AvatarData data; + std::unique_ptr clientTraitsHandler; + + QUuid nodeUUID{}; + + friend class AvatarDataStream; + friend class AvatarManager; + }; + +} // namespace vircadia::client + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/internal/avatars/AvatarManager.cpp b/libraries/vircadia-client/src/internal/avatars/AvatarManager.cpp new file mode 100644 index 00000000000..2841b752382 --- /dev/null +++ b/libraries/vircadia-client/src/internal/avatars/AvatarManager.cpp @@ -0,0 +1,237 @@ +// +// AvatarManager.cpp +// libraries/vircadia-client/src/internal/avatars +// +// Created by Nshan G. on 9 May 2022. +// 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 +// + +#include "AvatarManager.h" + +#include + +#include +#include + +#include "../DataTypeConversions.h" + +namespace vircadia::client +{ + + AvatarPtr::AvatarPtr() = default; + + AvatarPtr::AvatarPtr(std::shared_ptr ptr, std::unique_lock lock) : + std::shared_ptr(ptr), lock(std::move(lock)) {} + + AvatarManager::AvatarManager() : + AvatarPacketHandler(), + myAvatar() + { + setCustomDeleter([](Dependency* dependency){ + static_cast(dependency)->deleteLater(); + }); + myAvatar.clientTraitsHandler.reset(new ClientTraitsHandler(&myAvatar)); + + moveToNewNamedThread(this, "Avatar Manager Thread"); + + // TODO: figure out if this is necessary for trait override packets + // moveToNewNamedThread(myAvatar.clientTraitsHandler.get(), "Avatar Client Traits Handler Thread"); + } + + template + auto firstEquals(T value) { + return [value] (const auto& pair) { return pair.first == value; }; + }; + + void AvatarManager::update() { + + udt::SequenceNumber identitySequenceNumber {}; + QUuid myUUID {}; + { + std::scoped_lock lock(myAvatarMutex); + identitySequenceNumber = myAvatar.data.identitySequenceNumber; + myUUID = myAvatar.nodeUUID; + } + + { + std::scoped_lock lock(avatarsMutex); + auto myAvatarReceived = std::find_if(avatars.begin(), avatars.end(), firstEquals(myUUID)); + + if (myAvatarReceived != avatars.end()) { + identitySequenceNumber = std::max(identitySequenceNumber, myAvatarReceived->second->data.identitySequenceNumber); + } + } + + { + std::scoped_lock lock(myAvatarMutex); + + myAvatar.data.identitySequenceNumber = identitySequenceNumber; + + for(auto&& grabData : myAvatar.data.grabs.added) { + myAvatar.grab( + qUuidfromBytes(grabData.target_id), + grabData.joint_index, + glmVec3From(grabData.offset.position), + glmQuatFrom(grabData.offset.rotation) + ); + } + + for(auto&& uuid : myAvatar.data.grabs.removed) { + myAvatar.releaseGrab(qUuidfromBytes(uuid.data())); + } + + myAvatar.data.grabs.added.clear(); + myAvatar.data.grabs.removed.clear(); + + AvatarDataPacket::SendStatus status; + status.itemFlags = myAvatar.data.changes.to_ullong(); + status.itemFlags &= AvatarDataPacket::ALL_HAS_FLAGS; + do { + myAvatar.sendAllPackets(Avatar::CullSmallData, status); + } while (!status); + myAvatar.data.changes &= ~AvatarDataPacket::ALL_HAS_FLAGS; + + identitySequenceNumber = myAvatar.data.identitySequenceNumber; + } + + { + std::scoped_lock lock(viewsMutex); + query(); + } + } + + void AvatarManager::updateData() { + + QMap myGrabData; + QUuid myId; + { + std::scoped_lock lock(myAvatarMutex); + auto previousChanges = myAvatar.data.changes; + auto identitySequenceNumber = myAvatar.data.identitySequenceNumber; + + myAvatarDataIn.grabs.added.insert(myAvatarDataIn.grabs.added.begin(), + myAvatar.data.grabs.added.begin(), myAvatar.data.grabs.added.end()); + myAvatarDataIn.grabs.removed.insert(myAvatarDataIn.grabs.removed.begin(), + myAvatar.data.grabs.removed.begin(), myAvatar.data.grabs.removed.end()); + + myAvatar.data = myAvatarDataIn; + + myAvatar.data.changes |= previousChanges; + myAvatar.data.identitySequenceNumber = identitySequenceNumber; + + myAvatarDataIn.changes.reset(); + myAvatarDataIn.grabs.added.clear(); + myAvatarDataIn.grabs.removed.clear(); + + myGrabData = myAvatar.getGrabData(); + myId = myAvatar.getSessionUUIDOut(); + } + + { + std::scoped_lock lock(avatarsMutex); + + avatarDataOut.resize(avatars.size()); + std::transform(avatars.begin(), avatars.end(), avatarDataOut.begin(), [&](auto& avatar){ + avatar.second->setGrabDataIn(avatar.second->getSessionUUID() == myId ? &myGrabData : nullptr); + auto data = avatar.second->data; + avatar.second->data.changes.reset(); + return data; + }); + + epitaphsOut.resize(epitaphs.size()); + std::transform(epitaphs.begin(), epitaphs.end(), epitaphsOut.begin(), [](auto& epitaph) { + return std::pair{toUUIDArray(epitaph.first), epitaph.second}; + }); + epitaphs.clear(); + } + + { + std::scoped_lock lock(viewsMutex); + views = viewsIn; + } + + } + + const std::vector& AvatarManager::getAvatarDataOut() const { + return avatarDataOut; + } + + AvatarPtr AvatarManager::newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer, bool& isNew) { + std::unique_lock lock(avatarsMutex); + auto avatar = std::find_if(avatars.begin(), avatars.end(), firstEquals(sessionUUID)); + isNew = avatar == avatars.end(); + if (isNew) { + avatars.emplace_back(sessionUUID, AvatarPtr(std::make_shared())); + avatar = std::prev(avatars.end()); + } + + return AvatarPtr(avatar->second, std::move(lock)); + } + + AvatarPtr AvatarManager::removeAvatar(const QUuid& sessionUUID, KillAvatarReason reason) { + std::scoped_lock lock(avatarsMutex); + + for(auto&& a : avatars) { + qDebug() << a.first; + } + + auto avatar = std::find_if(avatars.begin(), avatars.end(), firstEquals(sessionUUID)); + + if (avatar == avatars.end()) { + return AvatarPtr{}; + } + + AvatarPtr removed = std::move(avatar->second); + avatars.erase(avatar); + + epitaphs.emplace_back(sessionUUID, reason); + + return removed; + } + + void AvatarManager::clearOtherAvatars() { + std::scoped_lock lock(avatarsMutex); + AvatarPacketHandler::clearOtherAvatars(); + avatars.clear(); + epitaphs.clear(); + } + + const std::vector& AvatarManager::getViews() const { + return views; + } + + std::vector& AvatarManager::getLastQueriedViews() { + return lastQueriedViews; + } + + QUuid AvatarManager::mapIdentityUUID(const QUuid& uuid) { return uuid; } + void AvatarManager::onAvatarIdentityReceived(const QUuid& identityUUID, const QByteArray& data) {} + void AvatarManager::onAvatarDataReceived(const QUuid& sessionUUID, const QByteArray& data) {} + + void AvatarManager::onAvatarMixerActivated() { + std::scoped_lock lock(myAvatarMutex); + myAvatar.data.changes.set(AvatarData::IdentityIndex); + } + + void AvatarManager::onSessionUUIDChanged(const QUuid& uuid, const QUuid& old) { + std::scoped_lock lock(myAvatarMutex); + myAvatar.nodeUUID = uuid; + + // NOTE: UUID is sent with identity packet but not entirely + // sure if this is proper. There is also a sendUUID option + // in AvatarDataPacket::SendStatus, to send it with a data + // packet. + myAvatar.data.changes.set(AvatarData::IdentityIndex); + } + + void AvatarManager::onNodeIngored(const QUuid&, bool) {} + +} // namespace vircadia::client + +using namespace vircadia::client; + +template class AvatarPacketHandler; diff --git a/libraries/vircadia-client/src/internal/avatars/AvatarManager.h b/libraries/vircadia-client/src/internal/avatars/AvatarManager.h new file mode 100644 index 00000000000..dd8b6cdfa92 --- /dev/null +++ b/libraries/vircadia-client/src/internal/avatars/AvatarManager.h @@ -0,0 +1,89 @@ +// +// AvatarManager.h +// libraries/vircadia-client/src/internal/avatars +// +// Created by Nshan G. on 11 May 2022. +// 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_VIRCADIA_CLIENT_SRC_INTERNAL_AVATARS_AVATARMANAGER_H +#define LIBRARIES_VIRCADIA_CLIENT_SRC_INTERNAL_AVATARS_AVATARMANAGER_H + +#include +#include +#include + +#include + +#include +#include + +#include "AvatarData.h" + +namespace vircadia::client +{ + + /// @private + class AvatarPtr : public std::shared_ptr { + public: + AvatarPtr(); + explicit AvatarPtr(std::shared_ptr, std::unique_lock = {}); + private: + std::unique_lock lock; + }; + + /// @private + class AvatarManager : public QObject, public Dependency, public AvatarPacketHandler { + Q_OBJECT + public: + + AvatarManager(); + + void update(); + void updateData(); + const std::vector& getAvatarDataOut() const; + + AvatarData myAvatarDataIn; + std::vector avatarDataOut; + std::vector> epitaphsOut; + std::vector viewsIn; + + private: + friend class AvatarPacketHandler; + + QUuid mapIdentityUUID(const QUuid&); + AvatarPtr newOrExistingAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer, bool& isNew); + AvatarPtr removeAvatar(const QUuid& sessionUUID, KillAvatarReason); + void clearOtherAvatars(); + + const std::vector& getViews() const; + std::vector& getLastQueriedViews(); + + void onAvatarIdentityReceived(const QUuid& identityUUID, const QByteArray& data); + void onAvatarDataReceived(const QUuid& sessionUUID, const QByteArray& data); + void onAvatarMixerActivated(); + void onSessionUUIDChanged(const QUuid&, const QUuid& old); + void onNodeIngored(const QUuid&, bool ignored); + + + Avatar myAvatar; + std::vector> avatars; + std::vector> epitaphs; + + mutable std::mutex myAvatarMutex; + mutable std::mutex avatarsMutex; + + std::vector views; + std::vector lastQueriedViews; + + mutable std::mutex viewsMutex; + + }; + +} // namespace vircadia::client + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/internal/avatars/ClientTraitsHandler.cpp b/libraries/vircadia-client/src/internal/avatars/ClientTraitsHandler.cpp new file mode 100644 index 00000000000..3956a276821 --- /dev/null +++ b/libraries/vircadia-client/src/internal/avatars/ClientTraitsHandler.cpp @@ -0,0 +1,17 @@ +// +// ClientTraitsHandler.cpp +// libraries/vircadia-client/src/internal/avatars +// +// Created by Nshan G. on 20 May 2022. +// 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 +// + +#include + +#include "AvatarData.h" + +template class ClientTraitsHandler; diff --git a/libraries/vircadia-client/src/internal/avatars/ClientTraitsHandler.h b/libraries/vircadia-client/src/internal/avatars/ClientTraitsHandler.h new file mode 100644 index 00000000000..0f81e672f38 --- /dev/null +++ b/libraries/vircadia-client/src/internal/avatars/ClientTraitsHandler.h @@ -0,0 +1,40 @@ +// +// ClientTraitsHandler.h +// libraries/vircadia-client/src/internal/avatars +// +// Created by Nshan G. on 20 May 2022. +// 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_VIRCADIA_CLIENT_SRC_INTERNAL_AVATARS_CLIENTTRAITSHANDLER_H +#define LIBRARIES_VIRCADIA_CLIENT_SRC_INTERNAL_AVATARS_CLIENTTRAITSHANDLER_H + +#include + +#include + +namespace vircadia::client +{ + + class Avatar; + + class ClientTraitsHandler; + + using ClientTraitsHandlerBase = ::ClientTraitsHandler; + + /// @private + class ClientTraitsHandler : public QObject, public ClientTraitsHandlerBase { + Q_OBJECT + public: + using ClientTraitsHandlerBase::ClientTraitsHandler; + private: + friend ClientTraitsHandlerBase; + }; + +} // namespace vircadia::client + +#endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/message_types.cpp b/libraries/vircadia-client/src/message_types.cpp index bca98bf9389..9cd16d5e8c2 100644 --- a/libraries/vircadia-client/src/message_types.cpp +++ b/libraries/vircadia-client/src/message_types.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 29 March 2022. // 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 @@ -11,7 +12,7 @@ #include "message_types.h" -#include "internal/Context.h" +#include "internal/Messages.h" using namespace vircadia::client; @@ -23,6 +24,6 @@ VIRCADIA_CLIENT_DYN_API uint8_t vircadia_data_messages() { return MESSAGE_TYPE_DATA; } -VIRCADIA_CLIENT_DYN_API uint8_t vircadia_any_messages() { - return MESSAGE_TYPE_ANY; +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_all_messages() { + return MESSAGE_TYPE_ALL; } diff --git a/libraries/vircadia-client/src/message_types.h b/libraries/vircadia-client/src/message_types.h index 0c7764177e9..586b0bbdca1 100644 --- a/libraries/vircadia-client/src/message_types.h +++ b/libraries/vircadia-client/src/message_types.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 29 March 2022. // 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 @@ -28,9 +29,9 @@ VIRCADIA_CLIENT_DYN_API uint8_t vircadia_text_messages(); /// @return 0b10 VIRCADIA_CLIENT_DYN_API uint8_t vircadia_data_messages(); -/// @brief Flag value indicating any message type. +/// @brief Flag value that includes all message types. /// /// @return vircadia_text_messages() | vircadia_data_messages() -VIRCADIA_CLIENT_DYN_API uint8_t vircadia_any_messages(); +VIRCADIA_CLIENT_DYN_API uint8_t vircadia_all_messages(); #endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/messages.cpp b/libraries/vircadia-client/src/messages.cpp index 2044faba640..149b51572f0 100644 --- a/libraries/vircadia-client/src/messages.cpp +++ b/libraries/vircadia-client/src/messages.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 25 March 2022. // 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 @@ -17,7 +18,7 @@ using namespace vircadia::client; int validMessageType(uint8_t type) { - if ((type & ~MESSAGE_TYPE_ANY) != 0) + if ((type & ~MESSAGE_TYPE_ALL) != 0) return toInt(ErrorCode::MESSAGE_TYPE_INVALID); return 0; } @@ -73,19 +74,33 @@ int vircadia_enable_messages(int context_id, uint8_t type) { }); } +int anyTypesEnabled(const Context& context, uint8_t type) { + if (context.messages().isAnyEnabled(type)) { + return 0; + } else { + return toInt(ErrorCode::MESSAGE_TYPE_DISABLED); + } +} + VIRCADIA_CLIENT_DYN_API int vircadia_messages_subscribe(int context_id, const char* channel) { return chain(checkContextReady(context_id), [&](auto) { - std::next(std::begin(contexts), context_id)->messages().subscribe(channel); - return 0; + auto context = std::next(std::begin(contexts), context_id); + return chain(anyTypesEnabled(*context, MESSAGE_TYPE_ALL), [&](auto) { + context->messages().subscribe(channel); + return 0; + }); }); } VIRCADIA_CLIENT_DYN_API int vircadia_messages_unsubscribe(int context_id, const char* channel) { return chain(checkContextReady(context_id), [&](auto) { - std::next(std::begin(contexts), context_id)->messages().unsubscribe(channel); - return 0; + auto context = std::next(std::begin(contexts), context_id); + return chain(anyTypesEnabled(*context, MESSAGE_TYPE_ALL), [&](auto) { + context->messages().unsubscribe(channel); + return 0; + }); }); } diff --git a/libraries/vircadia-client/src/messages.h b/libraries/vircadia-client/src/messages.h index 533cd8a38b8..f9a72fbc289 100644 --- a/libraries/vircadia-client/src/messages.h +++ b/libraries/vircadia-client/src/messages.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 25 March 2022. // 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 @@ -49,7 +50,8 @@ int vircadia_enable_messages(int context_id, uint8_t types); /// @return 0 on success, or a negative error code. \n /// Possible error codes: \n /// vircadia_error_context_invalid() \n -/// vircadia_error_context_loss() +/// vircadia_error_context_loss() \n +/// vircadia_error_message_type_disabled() VIRCADIA_CLIENT_DYN_API int vircadia_messages_subscribe(int context_id, const char* channel); @@ -61,7 +63,8 @@ int vircadia_messages_subscribe(int context_id, const char* channel); /// @return 0 on success, or a negative error code. \n /// Possible error codes: \n /// vircadia_error_context_invalid() \n -/// vircadia_error_context_loss() +/// vircadia_error_context_loss() \n +/// vircadia_error_message_type_disabled() VIRCADIA_CLIENT_DYN_API int vircadia_messages_unsubscribe(int context_id, const char* channel); diff --git a/libraries/vircadia-client/src/node_list.cpp b/libraries/vircadia-client/src/node_list.cpp index 2375cfbb219..c22f0592fe3 100644 --- a/libraries/vircadia-client/src/node_list.cpp +++ b/libraries/vircadia-client/src/node_list.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 15 March 2022. // 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 @@ -78,3 +79,68 @@ int vircadia_node_type(int context_id, int index) { return node.type; }); } + +VIRCADIA_CLIENT_DYN_API +float vircadia_node_inbound_pps(int context_id, int index) { + float result; + int status = validateNodeIndex(context_id, index, [&](auto& node) -> int { + result = node.inboundPPS; + return 0; + }); + if (isError(status)) { + return status; + } else { + return result; + } +} + +VIRCADIA_CLIENT_DYN_API +float vircadia_node_outbound_pps(int context_id, int index) { + float result; + int status = validateNodeIndex(context_id, index, [&](auto& node) -> int { + result = node.outboundPPS; + return 0; + }); + if (isError(status)) { + return status; + } else { + return result; + } +} + +VIRCADIA_CLIENT_DYN_API +float vircadia_node_inbound_kbps(int context_id, int index) { + float result; + int status = validateNodeIndex(context_id, index, [&](auto& node) -> int { + result = node.inboundKbps; + return 0; + }); + if (isError(status)) { + return status; + } else { + return result; + } +} + +VIRCADIA_CLIENT_DYN_API +float vircadia_node_outbound_kbps(int context_id, int index) { + float result; + int status = validateNodeIndex(context_id, index, [&](auto& node) -> int { + result = node.outboundKbps; + return 0; + }); + if (isError(status)) { + return status; + } else { + return result; + } +} + +VIRCADIA_CLIENT_DYN_API +const uint8_t* vircadia_client_get_session_uuid(int id) { + return chain(checkContextReady(id), [&](auto) { + return std::next(std::begin(contexts), id)->getSessionUUID().data(); + }); +} + +// TODO: APIs for permissions, kicking, muting diff --git a/libraries/vircadia-client/src/node_list.h b/libraries/vircadia-client/src/node_list.h index 407bc035451..6e0b1f751da 100644 --- a/libraries/vircadia-client/src/node_list.h +++ b/libraries/vircadia-client/src/node_list.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 31 March 2022. // 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 @@ -113,4 +114,26 @@ int vircadia_node_active(int context_id, int index); VIRCADIA_CLIENT_DYN_API int vircadia_node_type(int context_id, int index); +VIRCADIA_CLIENT_DYN_API +float vircadia_node_inbound_pps(int context_id, int index); + +VIRCADIA_CLIENT_DYN_API +float vircadia_node_outbound_pps(int context_id, int index); + +VIRCADIA_CLIENT_DYN_API +float vircadia_node_inbound_kbps(int context_id, int index); + +VIRCADIA_CLIENT_DYN_API +float vircadia_node_outbound_kbps(int context_id, int index); + +/// @brief Get the session UUID of the client node. +/// +/// The session UUID only changes with an explicit call to +/// vircadia_update_nodes(). +/// +/// @param context_id The ID of the context to use (context.h). +/// @return Pointer to 16 byte UUID or null in case of an error. +VIRCADIA_CLIENT_DYN_API +const uint8_t* vircadia_client_get_session_uuid(int context_id); + #endif /* end of include guard */ diff --git a/libraries/vircadia-client/src/node_types.cpp b/libraries/vircadia-client/src/node_types.cpp index 1e326d449e2..05bab3e19d0 100644 --- a/libraries/vircadia-client/src/node_types.cpp +++ b/libraries/vircadia-client/src/node_types.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 13 Apr 2022. // 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 diff --git a/libraries/vircadia-client/src/node_types.h b/libraries/vircadia-client/src/node_types.h index e6ff400a9ea..255ede0bf08 100644 --- a/libraries/vircadia-client/src/node_types.h +++ b/libraries/vircadia-client/src/node_types.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 13 Apr 2022. // 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 diff --git a/libraries/vircadia-client/src/version.cpp b/libraries/vircadia-client/src/version.cpp index b57c1089222..254bc652132 100644 --- a/libraries/vircadia-client/src/version.cpp +++ b/libraries/vircadia-client/src/version.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 18 Feb 2022. // 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 diff --git a/libraries/vircadia-client/src/version.h b/libraries/vircadia-client/src/version.h index 3dd9f87ca30..76054e057a2 100644 --- a/libraries/vircadia-client/src/version.h +++ b/libraries/vircadia-client/src/version.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 18 Feb 2022. // 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 diff --git a/libraries/vircadia-client/tests/CMakeLists.txt b/libraries/vircadia-client/tests/CMakeLists.txt index 880ec9649c7..a3a3c13a98f 100644 --- a/libraries/vircadia-client/tests/CMakeLists.txt +++ b/libraries/vircadia-client/tests/CMakeLists.txt @@ -1,7 +1,13 @@ cmake_minimum_required(VERSION 3.5) + find_package(Catch2 REQUIRED) -add_executable(vircadia-client-tests version.cpp client.cpp messages.cpp) +add_executable(vircadia-client-tests version.cpp client.cpp messages.cpp avatars.cpp avatars_receiver.cpp audio.cpp) +set_target_properties(vircadia-client-tests PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED ON + CXX_EXTENSIONS OFF +) target_link_libraries(vircadia-client-tests PRIVATE vircadia-client Catch2::Catch2WithMain) include(CTest) diff --git a/libraries/vircadia-client/tests/audio.cpp b/libraries/vircadia-client/tests/audio.cpp new file mode 100644 index 00000000000..bedc9fdad7c --- /dev/null +++ b/libraries/vircadia-client/tests/audio.cpp @@ -0,0 +1,118 @@ +// +// audio.cpp +// libraries/vircadia-client/tests +// +// Created by Nshan G. on 9 July 2022. +// 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 +// + +#include "../src/context.h" +#include "../src/node_list.h" +#include "../src/node_types.h" +#include "../src/audio.h" +#include "../src/audio_constants.h" +#include "../src/error.h" + +#include +#include +#include +#include + +#include + +#include "common.h" + +using namespace std::literals; + +TEST_CASE("Client API audio functionality.", "[client-api-audio]") { + + const float pi = std::acos(-1); + + const int context = vircadia_create_context(vircadia_context_defaults()); + vircadia_connect(context, "localhost"); + + { + std::thread input_thread{}; + std::thread output_thread{}; + + auto cleanup = defer([context, &input_thread, &output_thread](){ + if (input_thread.joinable()) { + input_thread.join(); + } + if (output_thread.joinable()) { + output_thread.join(); + } + REQUIRE(vircadia_destroy_context(context) == 0); + }); + + + bool input_started = false; + uint8_t* input = nullptr; + bool output_started = false; + uint8_t* output = nullptr; + std::vector output_data; + + REQUIRE(vircadia_enable_audio(context) >= 0); + REQUIRE(vircadia_set_audio_input_echo(context, 1) >= 0); + REQUIRE(vircadia_set_audio_input_format(context, vircadia_audio_format{ + vircadia_audio_sample_type_float(), 48000, 1}) >= 0); + REQUIRE(vircadia_set_audio_output_format(context, vircadia_audio_format{ + vircadia_audio_sample_type_float(), 48000, 2}) >= 0); + + for (int i = 0; i < 300; ++i) { + int status = vircadia_connection_status(context); + if (status == 1) { + + if (input == nullptr) { + input = vircadia_get_audio_input_context(context); + } else if (!input_started) { + input_thread = std::thread([input, pi, current_sample = 0]() mutable { + constexpr int sample_size = 480; + static float data[sample_size]; + while (current_sample != 48000) { + for (int i = 0; i < sample_size; ++i) { + data[i] = std::sin(148 * 2 * pi * ((current_sample%48000)/48000.f)); + ++current_sample; + } + REQUIRE(vircadia_set_audio_input_data(input, + reinterpret_cast(data), sample_size * sizeof(float)) >= 0); + std::this_thread::sleep_for(10ms); + } + }); + input_started = true; + } + + if (output == nullptr) { + output = vircadia_get_audio_output_context(context); + } else if(!output_started) { + output_thread = std::thread([output, &output_data]() { + constexpr int sample_size = 480 * 2; + while (output_data.size() != 48000 * 2) { + output_data.resize(output_data.size() + sample_size); + REQUIRE(vircadia_get_audio_output_data(output, + reinterpret_cast(output_data.data() + output_data.size() - sample_size), + sample_size * sizeof(float)) == sample_size * sizeof(float)); + std::this_thread::sleep_for(10ms); + } + }); + output_started = true; + } + + } + + std::this_thread::sleep_for(10ms); + } + + if (!output_data.empty()) { + // TODO: check this as data arrives and finish the test once satisfied + REQUIRE(std::count_if(output_data.begin(), output_data.end(), + [](auto x) { return std::abs(x) > 0.5f; }) > 5000); + } + + } + +} diff --git a/libraries/vircadia-client/tests/avatars.cpp b/libraries/vircadia-client/tests/avatars.cpp new file mode 100644 index 00000000000..a78b5aa5224 --- /dev/null +++ b/libraries/vircadia-client/tests/avatars.cpp @@ -0,0 +1,202 @@ +// +// avatars.cpp +// libraries/vircadia-client/tests +// +// Created by Nshan G. on 16 May 2022. +// 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 +// + +#include "../src/context.h" +#include "../src/node_list.h" +#include "../src/node_types.h" +#include "../src/avatars.h" +#include "../src/error.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "common.h" + +using namespace std::literals; + +const float pi = std::acos(-1); + +vircadia_quaternion axis_angle(vircadia_vector axis, float angle) { + const float s = std::sin(angle/2); + return { + s * axis.x, + s * axis.y, + s * axis.z, + std::cos(angle/2) + }; +} + +TEST_CASE("Client API avatar functionality - sender.", "[client-api-avatars-sender]") { + + // TODO: this will not work if multiple tests are ran against a single + // server, should use unique id per test run instead + const auto name_prefix = "Client Unit Test Avatar"s; + const auto send_string = "(sending test data)"s; + + const auto avatar_urls = array{ + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Kim/fbx/Kim.fst", + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Mason/fbx/Mason.fst", + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Mike/fbx/Mike.fst", + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Sean/fbx/Sean.fst", + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Summer/fbx/Summer.fst", + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Tanya/fbx/Tanya.fst" + }; + + unsigned random = std::chrono::steady_clock::now().time_since_epoch().count() % avatar_urls.size(); + + const int context = vircadia_create_context(vircadia_context_defaults()); + vircadia_connect(context, "localhost"); + + vircadia_vector global_position {0,0,0}; + vircadia_bounds bounding_box {{1,1,1}, {0,0,0}}; + float rotation = 0; + float scale = 1; + + const uint8_t* session_uuid = nullptr; + + { + auto cleanup = defer([context](){ + REQUIRE(vircadia_destroy_context(context) == 0); + }); + + REQUIRE(vircadia_set_my_avatar_display_name(context, name_prefix.c_str()) == vircadia_error_avatars_disabled()); + REQUIRE(vircadia_update_avatars(context) == vircadia_error_avatars_disabled()); + + REQUIRE(vircadia_enable_avatars(context) == 0); + REQUIRE(vircadia_set_my_avatar_display_name(context, name_prefix.c_str()) == 0); + REQUIRE(vircadia_set_my_avatar_skeleton_model_url(context, avatar_urls[random]) == 0); + REQUIRE(vircadia_set_my_avatar_bounding_box(context, bounding_box) == 0); + REQUIRE(vircadia_set_my_avatar_look_at_snapping(context, 1) == 0); + + const uint8_t* copying = nullptr; + const uint8_t* receiver = nullptr; + + for (int i = 0; i < 1000; ++i) { + + REQUIRE(vircadia_update_nodes(context) == 0); + session_uuid = vircadia_client_get_session_uuid(context); + REQUIRE(session_uuid != nullptr); + + REQUIRE(vircadia_update_avatars(context) == 0); + + auto avatar_count = vircadia_get_avatar_count(context); + REQUIRE(avatar_count >= 0); + + bool receiver_active = false; + + for (int avatar = 0; avatar != avatar_count; ++avatar) { + const uint8_t* uuid = vircadia_get_avatar_uuid(context, avatar); + REQUIRE(uuid != nullptr); + auto display_name = vircadia_get_avatar_display_name(context, avatar); + REQUIRE(display_name != nullptr); + + bool is_me = std::equal(uuid, uuid + 16, session_uuid); + if (is_me) { + continue; + } + + bool starts_with_prefix = std::search(display_name, display_name + std::strlen(display_name), + name_prefix.begin(), name_prefix.end()) == display_name; + + if (copying != nullptr) { + // for manual testing, replicates another avatar + if (std::equal(copying, copying + 16, uuid)) { + auto joint_count = vircadia_get_avatar_joint_count(context, avatar); + REQUIRE(joint_count >= 0); + REQUIRE(vircadia_set_my_avatar_joint_count(context, joint_count) == 0); + for (int j = 0; j != joint_count; ++j) { + auto joint = vircadia_get_avatar_joint(context, avatar, j); + REQUIRE(joint != nullptr); + REQUIRE(vircadia_set_my_avatar_joint(context, j, *joint) == 0); + } + + REQUIRE(vircadia_set_my_avatar_joint_flags_count(context, joint_count) == 0); + for (int j = 0; j != joint_count; ++j) { + auto flags = vircadia_get_avatar_joint_flags(context, avatar, j); + REQUIRE(flags != nullptr); + REQUIRE(vircadia_set_my_avatar_joint_flags(context, j, *flags) == 0); + } + + auto global_position = vircadia_get_avatar_global_position(context, avatar); + REQUIRE(global_position != nullptr); + auto offset_position = *global_position; + offset_position.x += 1; + REQUIRE(vircadia_set_my_avatar_global_position(context, offset_position) == 0); + + auto orientation = vircadia_get_avatar_orientation(context, avatar); + REQUIRE(orientation != nullptr); + REQUIRE(vircadia_set_my_avatar_orientation(context, *orientation) == 0); + + auto scale = vircadia_get_avatar_scale(context, avatar); + REQUIRE(scale != nullptr); + REQUIRE(vircadia_set_my_avatar_scale(context, *scale) == 0); + } + } else if (receiver != nullptr) { + if (std::equal(receiver, receiver + 16, uuid)) { + REQUIRE(vircadia_set_my_avatar_global_position(context, global_position) == 0); + global_position.x += 0.01f; + global_position.y += 0.01f; + global_position.z += 0.01f; + + REQUIRE(vircadia_set_my_avatar_orientation(context, axis_angle({0,1,0},rotation)) == 0); + rotation += pi/250; + + REQUIRE(vircadia_set_my_avatar_scale(context, scale) == 0); + scale += 0.001f; + + auto joint_count = i / 10; + REQUIRE(vircadia_set_my_avatar_joint_count(context, joint_count) == 0); + for (int j = 0; j != joint_count; ++j) { + auto joint = vircadia_vantage{ + {i/1000.f, i/1000.f, i/1000.f}, + axis_angle({0,0,1}, pi*i/100.f) + }; + REQUIRE(vircadia_set_my_avatar_joint(context, j, joint) == 0); + } + receiver_active = true; + } + } else if (starts_with_prefix) { + const auto receiver_name = name_prefix + " Receiver"; + bool starts_with_reveiver = std::search(display_name, display_name + std::strlen(display_name), + receiver_name.begin(), receiver_name.end()) == display_name; + if (starts_with_reveiver) { + receiver = uuid; + receiver_active = true; + REQUIRE(vircadia_set_my_avatar_display_name(context, + (name_prefix + " " + send_string).c_str()) == 0); + } + } else if (i > 100) { + REQUIRE(vircadia_set_my_avatar_display_name(context, + (name_prefix + " (copying "s + display_name + ")"s).c_str()) == 0); + auto skeleton_model_url = vircadia_get_avatar_skeleton_model_url(context, avatar); + REQUIRE(skeleton_model_url != nullptr); + REQUIRE(vircadia_set_my_avatar_skeleton_model_url(context, skeleton_model_url) == 0); + + copying = uuid; + } + } + + if (receiver != nullptr && !receiver_active) { + break; + } + + std::this_thread::sleep_for(10ms); + } + } + +} diff --git a/libraries/vircadia-client/tests/avatars_receiver.cpp b/libraries/vircadia-client/tests/avatars_receiver.cpp new file mode 100644 index 00000000000..8b1b13676e6 --- /dev/null +++ b/libraries/vircadia-client/tests/avatars_receiver.cpp @@ -0,0 +1,205 @@ +// +// avatars_control.cpp +// libraries/vircadia-client/tests +// +// Created by Nshan G. on 3 June 2022. +// 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 +// + +#include "../src/context.h" +#include "../src/node_list.h" +#include "../src/node_types.h" +#include "../src/avatars.h" +#include "../src/error.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "common.h" + +#include + +bool operator<(vircadia_vector a, vircadia_vector b) { + return a.x < b.x && a.y < b.y && a.z < b.z; +} + +bool operator!=(vircadia_quaternion a, vircadia_quaternion b) { + return a.x != b.x || a.y != b.y || a.z != b.z || a.w != b.y; +} + +using namespace std::literals; + +TEST_CASE("Client API avatar functionality - receiver.", "[client-api-avatars-receiver]") { + + // TODO: this will not work if multiple tests are ran against a single + // server, should use unique id per test run instead + const auto name_prefix = "Client Unit Test Avatar"s; + const auto send_string = "(sending test data)"s; + + const auto avatar_urls = array{ + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Kim/fbx/Kim.fst", + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Mason/fbx/Mason.fst", + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Mike/fbx/Mike.fst", + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Sean/fbx/Sean.fst", + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Summer/fbx/Summer.fst", + "https://cdn-1.vircadia.com/us-e-1/Bazaar/Avatars/Tanya/fbx/Tanya.fst" + }; + + const int context = vircadia_create_context(vircadia_context_defaults()); + vircadia_connect(context, "localhost"); + + std::optional last_global_position {}; + std::optional last_orientation {}; + std::optional last_scale{}; + std::optional last_joint_count{}; + std::optional last_joint_value{}; + bool position_increased = false; + bool orientation_changed = false; + bool scale_increased = false; + bool joint_count_increased = false; + bool joint_position_increased = false; + bool joint_rotation_changed = false; + + const uint8_t* sender = nullptr; + const uint8_t* session_uuid = nullptr; + + { + auto cleanup = defer([context](){ + REQUIRE(vircadia_destroy_context(context) == 0); + }); + + REQUIRE(vircadia_enable_avatars(context) == 0); + REQUIRE(vircadia_set_my_avatar_display_name(context, (name_prefix + " Receiver").c_str()) == 0); + + + for (int i = 0; i < 1000; ++i) { + + REQUIRE(vircadia_update_nodes(context) == 0); + session_uuid = vircadia_client_get_session_uuid(context); + REQUIRE(session_uuid != nullptr); + + REQUIRE(vircadia_update_avatars(context) == 0); + + auto avatar_count = vircadia_get_avatar_count(context); + REQUIRE(avatar_count >= 0); + + for (int avatar = 0; avatar != avatar_count; ++avatar) { + const uint8_t* uuid = vircadia_get_avatar_uuid(context, avatar); + REQUIRE(uuid != nullptr); + auto display_name = vircadia_get_avatar_display_name(context, avatar); + REQUIRE(display_name != nullptr); + + if (sender != nullptr) { + if (std::equal(uuid, uuid + 16, sender)) { + const auto global_position = vircadia_get_avatar_global_position(context, avatar); + REQUIRE(global_position != nullptr); + if (!last_global_position) { + last_global_position = *global_position; + } else if(*last_global_position < *global_position) { + position_increased = true; + } + + const auto orientation = vircadia_get_avatar_orientation(context, avatar); + REQUIRE(orientation != nullptr); + if (!last_orientation) { + last_orientation = *orientation; + } else if(*last_orientation != *orientation) { + orientation_changed = true; + } + + const auto scale = vircadia_get_avatar_scale(context, avatar); + REQUIRE(scale != nullptr); + if (!last_scale) { + last_scale = *scale; + } else if(*last_scale < *scale) { + scale_increased = true; + } + + const auto joint_count = vircadia_get_avatar_joint_count(context, avatar); + REQUIRE(joint_count >= 0); + if (!last_joint_count) { + last_joint_count = joint_count; + } else if(*last_joint_count < joint_count) { + joint_count_increased = true; + } + + if (joint_count > 0) { + const auto joint = vircadia_get_avatar_joint(context, avatar, 0); + REQUIRE(joint != nullptr); + if (!last_joint_value) { + last_joint_value = *joint; + } else { + if (last_joint_value->position < joint->position) { + joint_position_increased = true; + } + if (last_joint_value->rotation != joint->rotation) { + joint_rotation_changed = true; + } + } + } + + } + } else { + bool is_me = std::equal(uuid, uuid + 16, session_uuid); + auto display_name_end = display_name + std::strlen(display_name); + bool starts_with_prefix = std::search(display_name, display_name_end, + name_prefix.begin(), name_prefix.end()) == display_name; + if (!is_me && starts_with_prefix) { + bool sending = std::search(display_name, display_name_end, + send_string.begin(), send_string.end()) != display_name_end; + if (sending) { + sender = uuid; + + const auto bounding_box = vircadia_get_avatar_bounding_box(context, avatar); + REQUIRE(bounding_box != nullptr); + REQUIRE(bounding_box->dimensions.x == 1); + REQUIRE(bounding_box->dimensions.y == 1); + REQUIRE(bounding_box->dimensions.z == 1); + REQUIRE(bounding_box->offset.x == 0); + REQUIRE(bounding_box->offset.y == 0); + REQUIRE(bounding_box->offset.z == 0); + + const char* model = vircadia_get_avatar_skeleton_model_url(context, avatar); + REQUIRE(model != nullptr); + REQUIRE(std::any_of(avatar_urls.begin(), avatar_urls.end(), + [model = std::string(model)](auto url) { return model == url; })); + + auto look_at_snapping_enabled = vircadia_get_avatar_look_at_snapping(context, avatar); + REQUIRE(look_at_snapping_enabled >= 0); + REQUIRE(look_at_snapping_enabled == 1); + + } + } + } + } + + if (position_increased && orientation_changed && scale_increased && + joint_count_increased && joint_position_increased && joint_rotation_changed) { + break; + } + + std::this_thread::sleep_for(10ms); + } + + if(sender != nullptr) { + REQUIRE(position_increased); + REQUIRE(orientation_changed); + REQUIRE(scale_increased); + REQUIRE(joint_count_increased); + REQUIRE(joint_position_increased); + REQUIRE(joint_rotation_changed); + } + } + +} diff --git a/libraries/vircadia-client/tests/client.cpp b/libraries/vircadia-client/tests/client.cpp index 7f4af4bdf87..1f75c39fb1f 100644 --- a/libraries/vircadia-client/tests/client.cpp +++ b/libraries/vircadia-client/tests/client.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 17 Mar 2022. // 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 diff --git a/libraries/vircadia-client/tests/common.h b/libraries/vircadia-client/tests/common.h index a2b79771dd3..1119e299537 100644 --- a/libraries/vircadia-client/tests/common.h +++ b/libraries/vircadia-client/tests/common.h @@ -4,6 +4,7 @@ // // Created by Nshan G. on 13 Apr 2022. // 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 @@ -13,6 +14,7 @@ #define LIBRARIES_VIRCADIA_CLIENT_TESTS_COMMON_H #include +#include using deferred = std::shared_ptr; @@ -22,4 +24,11 @@ auto defer(Cleanup cleanup) { [cleanup = std::move(cleanup)] (void*) { cleanup(); }); } +// cause windows and mac just can't +// https://github.com/vircadia/vircadia/runs/6489707252?check_suite_focus=true +template +struct array : std::array {}; +template +array(First, Rest...) -> array; + #endif /* end of include guard */ diff --git a/libraries/vircadia-client/tests/messages.cpp b/libraries/vircadia-client/tests/messages.cpp index 85caf3219fd..9c3384fecb1 100644 --- a/libraries/vircadia-client/tests/messages.cpp +++ b/libraries/vircadia-client/tests/messages.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 30 March 2022. // 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 diff --git a/libraries/vircadia-client/tests/version.cpp b/libraries/vircadia-client/tests/version.cpp index 561cb216b7f..f2520349631 100644 --- a/libraries/vircadia-client/tests/version.cpp +++ b/libraries/vircadia-client/tests/version.cpp @@ -4,6 +4,7 @@ // // Created by Nshan G. on 2 Mar 2022. // 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 diff --git a/plugins/opusCodec/CMakeLists.txt b/plugins/opusCodec/CMakeLists.txt index 583aff85d66..617f4c57433 100644 --- a/plugins/opusCodec/CMakeLists.txt +++ b/plugins/opusCodec/CMakeLists.txt @@ -8,7 +8,7 @@ set(TARGET_NAME opusCodec) setup_hifi_client_server_plugin() -link_hifi_libraries(shared audio plugins) +link_hifi_libraries(shared audio plugins opus-codec) target_opus() if (BUILD_SERVER) diff --git a/plugins/opusCodec/src/OpusCodecManager.cpp b/plugins/opusCodec/src/OpusCodecManager.cpp index 1e3d73a2295..d2123ba7589 100644 --- a/plugins/opusCodec/src/OpusCodecManager.cpp +++ b/plugins/opusCodec/src/OpusCodecManager.cpp @@ -1,5 +1,5 @@ // -// opusCodec.cpp +// OpusCodecManager.cpp // plugins/opusCodec/src // // Created by Michael Bailey on 12/20/2019 @@ -14,11 +14,8 @@ #include #include - -#include "OpusEncoder.h" -#include "OpusDecoder.h" - -const char* AthenaOpusCodec::NAME { "opus" }; +#include +#include void AthenaOpusCodec::init() { } @@ -40,19 +37,23 @@ bool AthenaOpusCodec::isSupported() const { return true; } +const QString AthenaOpusCodec::getName() const { + return OpusCodec::getName(); +} + Encoder* AthenaOpusCodec::createEncoder(int sampleRate, int numChannels) { - return new AthenaOpusEncoder(sampleRate, numChannels); + return OpusCodec::createEncoder(sampleRate, numChannels); } Decoder* AthenaOpusCodec::createDecoder(int sampleRate, int numChannels) { - return new AthenaOpusDecoder(sampleRate, numChannels); + return OpusCodec::createDecoder(sampleRate, numChannels); } void AthenaOpusCodec::releaseEncoder(Encoder* encoder) { - delete encoder; + return OpusCodec::releaseEncoder(encoder); } void AthenaOpusCodec::releaseDecoder(Decoder* decoder) { - delete decoder; + return OpusCodec::releaseDecoder(decoder); } diff --git a/plugins/opusCodec/src/OpusCodecManager.h b/plugins/opusCodec/src/OpusCodecManager.h index be6a6b2ff0e..597bd5bf2bb 100644 --- a/plugins/opusCodec/src/OpusCodecManager.h +++ b/plugins/opusCodec/src/OpusCodecManager.h @@ -13,14 +13,15 @@ #define hifi__OpusCodecManager_h #include +#include -class AthenaOpusCodec : public CodecPlugin { +class AthenaOpusCodec : public CodecPlugin, public OpusCodec { Q_OBJECT public: // Plugin functions bool isSupported() const override; - const QString getName() const override { return NAME; } + const QString getName() const override; void init() override; void deinit() override; @@ -34,9 +35,6 @@ class AthenaOpusCodec : public CodecPlugin { virtual Decoder* createDecoder(int sampleRate, int numChannels) override; virtual void releaseEncoder(Encoder* encoder) override; virtual void releaseDecoder(Decoder* decoder) override; - -private: - static const char* NAME; }; #endif // hifi__opusCodecManager_h diff --git a/plugins/pcmCodec/CMakeLists.txt b/plugins/pcmCodec/CMakeLists.txt index 34e49d908ba..029fb80e4ed 100644 --- a/plugins/pcmCodec/CMakeLists.txt +++ b/plugins/pcmCodec/CMakeLists.txt @@ -8,7 +8,7 @@ set(TARGET_NAME pcmCodec) setup_hifi_client_server_plugin() -link_hifi_libraries(shared audio plugins) +link_hifi_libraries(shared audio plugins pcm-codec) if (BUILD_SERVER) install_beside_console() diff --git a/plugins/pcmCodec/src/PCMCodecManager.cpp b/plugins/pcmCodec/src/PCMCodecManager.cpp index 04adb367afe..59ca714bc4c 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.cpp +++ b/plugins/pcmCodec/src/PCMCodecManager.cpp @@ -15,80 +15,84 @@ #include -const char* PCMCodec::NAME { "pcm" }; - -void PCMCodec::init() { +void PCMCodecManager::init() { } -void PCMCodec::deinit() { +void PCMCodecManager::deinit() { } -bool PCMCodec::activate() { +bool PCMCodecManager::activate() { CodecPlugin::activate(); return true; } -void PCMCodec::deactivate() { +void PCMCodecManager::deactivate() { CodecPlugin::deactivate(); } -bool PCMCodec::isSupported() const { +bool PCMCodecManager::isSupported() const { return true; } +const QString PCMCodecManager::getName() const { + return PCMCodec::getName(); +} -Encoder* PCMCodec::createEncoder(int sampleRate, int numChannels) { - return this; -} -Decoder* PCMCodec::createDecoder(int sampleRate, int numChannels) { - return this; +Encoder* PCMCodecManager::createEncoder(int sampleRate, int numChannels) { + return PCMCodec::createEncoder(sampleRate, numChannels); } -void PCMCodec::releaseEncoder(Encoder* encoder) { - // do nothing +Decoder* PCMCodecManager::createDecoder(int sampleRate, int numChannels) { + return PCMCodec::createDecoder(sampleRate, numChannels); } -void PCMCodec::releaseDecoder(Decoder* decoder) { - // do nothing +void PCMCodecManager::releaseEncoder(Encoder* encoder) { + PCMCodec::releaseEncoder(encoder); } -const char* zLibCodec::NAME { "zlib" }; +void PCMCodecManager::releaseDecoder(Decoder* decoder) { + PCMCodec::releaseDecoder(decoder); +} -void zLibCodec::init() { +void zLibCodecManager::init() { } -void zLibCodec::deinit() { +void zLibCodecManager::deinit() { } -bool zLibCodec::activate() { +bool zLibCodecManager::activate() { CodecPlugin::activate(); return true; } -void zLibCodec::deactivate() { +void zLibCodecManager::deactivate() { CodecPlugin::deactivate(); } -bool zLibCodec::isSupported() const { +bool zLibCodecManager::isSupported() const { return true; } -Encoder* zLibCodec::createEncoder(int sampleRate, int numChannels) { - return this; +const QString zLibCodecManager::getName() const { + return zLibCodec::getName(); +} + +Encoder* zLibCodecManager::createEncoder(int sampleRate, int numChannels) { + return zLibCodec::createEncoder(sampleRate, numChannels); } -Decoder* zLibCodec::createDecoder(int sampleRate, int numChannels) { - return this; +Decoder* zLibCodecManager::createDecoder(int sampleRate, int numChannels) { + return zLibCodec::createDecoder(sampleRate, numChannels); } -void zLibCodec::releaseEncoder(Encoder* encoder) { - // do nothing... it wasn't allocated +void zLibCodecManager::releaseEncoder(Encoder* encoder) { + return zLibCodec::releaseEncoder(encoder); } -void zLibCodec::releaseDecoder(Decoder* decoder) { - // do nothing... it wasn't allocated +void zLibCodecManager::releaseDecoder(Decoder* decoder) { + return zLibCodec::releaseDecoder(decoder); } diff --git a/plugins/pcmCodec/src/PCMCodecManager.h b/plugins/pcmCodec/src/PCMCodecManager.h index 178f7cbd9b7..b673eb2c587 100644 --- a/plugins/pcmCodec/src/PCMCodecManager.h +++ b/plugins/pcmCodec/src/PCMCodecManager.h @@ -13,15 +13,15 @@ #define hifi__PCMCodecManager_h #include -#include +#include -class PCMCodec : public CodecPlugin, public Encoder, public Decoder { +class PCMCodecManager : public CodecPlugin, public PCMCodec { Q_OBJECT public: // Plugin functions bool isSupported() const override; - const QString getName() const override { return NAME; } + const QString getName() const override; void init() override; void deinit() override; @@ -35,31 +35,15 @@ class PCMCodec : public CodecPlugin, public Encoder, public Decoder { virtual Decoder* createDecoder(int sampleRate, int numChannels) override; virtual void releaseEncoder(Encoder* encoder) override; virtual void releaseDecoder(Decoder* decoder) override; - - virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { - encodedBuffer = decodedBuffer; - } - - virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { - decodedBuffer = encodedBuffer; - } - - virtual void lostFrame(QByteArray& decodedBuffer) override { - decodedBuffer.resize(AudioConstants::NETWORK_FRAME_BYTES_STEREO); - memset(decodedBuffer.data(), 0, decodedBuffer.size()); - } - -private: - static const char* NAME; }; -class zLibCodec : public CodecPlugin, public Encoder, public Decoder { +class zLibCodecManager : public CodecPlugin, public zLibCodec { Q_OBJECT public: // Plugin functions bool isSupported() const override; - const QString getName() const override { return NAME; } + const QString getName() const override; void init() override; void deinit() override; @@ -73,22 +57,6 @@ class zLibCodec : public CodecPlugin, public Encoder, public Decoder { virtual Decoder* createDecoder(int sampleRate, int numChannels) override; virtual void releaseEncoder(Encoder* encoder) override; virtual void releaseDecoder(Decoder* decoder) override; - - virtual void encode(const QByteArray& decodedBuffer, QByteArray& encodedBuffer) override { - encodedBuffer = qCompress(decodedBuffer); - } - - virtual void decode(const QByteArray& encodedBuffer, QByteArray& decodedBuffer) override { - decodedBuffer = qUncompress(encodedBuffer); - } - - virtual void lostFrame(QByteArray& decodedBuffer) override { - decodedBuffer.resize(AudioConstants::NETWORK_FRAME_BYTES_STEREO); - memset(decodedBuffer.data(), 0, decodedBuffer.size()); - } - -private: - static const char* NAME; }; #endif // hifi__PCMCodecManager_h diff --git a/plugins/pcmCodec/src/PCMCodecProvider.cpp b/plugins/pcmCodec/src/PCMCodecProvider.cpp index dded40bfc98..9d65c0f2ca7 100644 --- a/plugins/pcmCodec/src/PCMCodecProvider.cpp +++ b/plugins/pcmCodec/src/PCMCodecProvider.cpp @@ -30,12 +30,12 @@ class PCMCodecProvider : public QObject, public CodecProvider { static std::once_flag once; std::call_once(once, [&] { - CodecPluginPointer pcmCodec(std::make_shared()); + CodecPluginPointer pcmCodec(std::make_shared()); if (pcmCodec->isSupported()) { _codecPlugins.push_back(pcmCodec); } - CodecPluginPointer zlibCodec(std::make_shared()); + CodecPluginPointer zlibCodec(std::make_shared()); if (zlibCodec->isSupported()) { _codecPlugins.push_back(zlibCodec); } diff --git a/tools/qt-builder/README.md b/tools/qt-builder/README.md index fbf868abe31..dd5933e3151 100644 --- a/tools/qt-builder/README.md +++ b/tools/qt-builder/README.md @@ -192,9 +192,12 @@ Using 7-Zip: * `7z a -ttar qt5-install-5.15.2-windows.tar qt5-install` * `7z a -tgzip qt5-install-5.15.2-windows.tar.gz qt5-install-5.15.2-windows.tar` -Upload qt5-install-5.15.2-windows.tar.gz to the Amazon S3 vircadia-public bucket, under the dependencies/vckpg directory. -Update hifi_vcpkg.py to use this new URL. Additionally, you should make a small change to any file in the vircadia/cmake/ports -directory to force the re-download of the qt-install.tar.gz during the build process for Vircadia. +Upload `qt5-install-5.15.2-windows.tar.gz` to Vircadia IPFS gateway server +(ipfs.vircadia.dev), add/pin it there with IPFS, add a location entry with +file's CID in `/etc/nginx/sites-available/default`, similarly in [IPFS hashes +JSON](https://github.com/vircadia/vircadia-ipfs-repo/blob/master/hashes.json) +and update `cmake/ExternalAssetsConfig.cmake` to use the new CID and SHA512/MD5 +checksums. #### Preparing Symbols @@ -281,7 +284,13 @@ find . -name \*.prl -exec sed -i -e '/^QMAKE_PRL_BUILD_DIR/d' {} \; ```bash tar -Jcvf qt5-install-5.15.2-ubuntu-18.04-amd64.tar.xz qt5-install ``` -2. Upload qt5-install-5.15.2-ubuntu-18.04-amd64.tar.xz to https://athena-public.s3.amazonaws.com/dependencies/vcpkg/ +2. Upload `qt5-install-5.15.2-ubuntu-18.04-amd64.tar.xz` to Vircadia IPFS + gateway server (ipfs.vircadia.dev), add/pin it with IPFS, add a location + entry with files CID in `/etc/nginx/sites-available/default`, similarly in + [IPFS hashes + JSON](https://github.com/vircadia/vircadia-ipfs-repo/blob/master/hashes.json) + and update `cmake/ExternalAssetsConfig.cmake` to use the new CID and + SHA512/MD5 checksums. @@ -347,7 +356,13 @@ Add a *qt.conf* file. ```bash tar -Jcvf qt5-install-5.15.2-qtwebengine-5.15.7-macOSXSDK10.14-macos.tar.xz qt5-install ``` -Upload qt5-install-5.15.2-qtwebengine-5.15.7-macOSXSDK10.14-macos.tar.xz to our Amazon S3 vircadia-public bucket, under the dependencies/vckpg directory +Upload `qt5-install-5.15.2-qtwebengine-5.15.7-macOSXSDK10.14-macos.tar.xz` to +Vircadia IPFS gateway server (ipfs.vircadia.dev), add/pin it there with IPFS, +add a location entry with file's CID in `/etc/nginx/sites-available/default`, +similarly in [IPFS hashes +JSON](https://github.com/vircadia/vircadia-ipfs-repo/blob/master/hashes.json) +and update `cmake/ExternalAssetsConfig.cmake` to use the new CID and SHA512/MD5 +checksums. #### Creating symbols (optional) Run `python3 prepare-mac-symbols-for-backtrace.py qt5-install` to scan the qt5-build directory for any dylibs and execute dsymutil to create dSYM bundles. After running this command the backtrace directory will be created. Zip this directory up, but make sure that all dylibs and dSYM fiels are in the root of the zip file, not under a sub-directory. This file can then be uploaded to backtrace or other crash log handling tool.