mirror of
https://github.com/Armored-Dragon/overte.git
synced 2025-03-11 16:13:16 +01:00
807 lines
31 KiB
C++
807 lines
31 KiB
C++
//
|
|
// AudioMixerSlave.cpp
|
|
// assignment-client/src/audio
|
|
//
|
|
// Created by Zach Pomerantz on 11/22/16.
|
|
// 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
|
|
//
|
|
|
|
#include "AudioMixerSlave.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include <glm/glm.hpp>
|
|
#include <glm/gtx/norm.hpp>
|
|
#include <glm/gtx/vector_angle.hpp>
|
|
|
|
#include <LogHandler.h>
|
|
#include <NetworkAccessManager.h>
|
|
#include <NodeList.h>
|
|
#include <Node.h>
|
|
#include <OctreeConstants.h>
|
|
#include <plugins/PluginManager.h>
|
|
#include <plugins/CodecPlugin.h>
|
|
#include <udt/PacketHeaders.h>
|
|
#include <SharedUtil.h>
|
|
#include <StDev.h>
|
|
#include <UUID.h>
|
|
|
|
#include "AudioRingBuffer.h"
|
|
#include "AudioMixer.h"
|
|
#include "AudioMixerClientData.h"
|
|
#include "AvatarAudioStream.h"
|
|
#include "InjectedAudioStream.h"
|
|
#include "AudioHelpers.h"
|
|
|
|
using namespace std;
|
|
using AudioStreamVector = AudioMixerClientData::AudioStreamVector;
|
|
using MixableStream = AudioMixerClientData::MixableStream;
|
|
using MixableStreamsVector = AudioMixerClientData::MixableStreamsVector;
|
|
|
|
// packet helpers
|
|
std::unique_ptr<NLPacket> createAudioPacket(PacketType type, int size, quint16 sequence, QString codec);
|
|
void sendMixPacket(const SharedNodePointer& node, AudioMixerClientData& data, QByteArray& buffer);
|
|
void sendSilentPacket(const SharedNodePointer& node, AudioMixerClientData& data);
|
|
void sendMutePacket(const SharedNodePointer& node, AudioMixerClientData&);
|
|
void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& data);
|
|
|
|
// mix helpers
|
|
inline float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd);
|
|
inline float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream,
|
|
const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho);
|
|
inline float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
|
|
const glm::vec3& relativePosition);
|
|
|
|
void AudioMixerSlave::processPackets(const SharedNodePointer& node) {
|
|
AudioMixerClientData* data = (AudioMixerClientData*)node->getLinkedData();
|
|
if (data) {
|
|
// process packets and collect the number of streams available for this frame
|
|
stats.sumStreams += data->processPackets(_sharedData.addedStreams);
|
|
}
|
|
}
|
|
|
|
void AudioMixerSlave::configureMix(ConstIter begin, ConstIter end, unsigned int frame, int numToRetain) {
|
|
_begin = begin;
|
|
_end = end;
|
|
_frame = frame;
|
|
_numToRetain = numToRetain;
|
|
}
|
|
|
|
void AudioMixerSlave::mix(const SharedNodePointer& node) {
|
|
// check that the node is valid
|
|
AudioMixerClientData* data = (AudioMixerClientData*)node->getLinkedData();
|
|
if (data == nullptr) {
|
|
return;
|
|
}
|
|
|
|
if (node->isUpstream()) {
|
|
return;
|
|
}
|
|
|
|
// check that the stream is valid
|
|
auto avatarStream = data->getAvatarAudioStream();
|
|
if (avatarStream == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// send mute packet, if necessary
|
|
if (AudioMixer::shouldMute(avatarStream->getQuietestFrameLoudness()) || data->shouldMuteClient()) {
|
|
sendMutePacket(node, *data);
|
|
}
|
|
|
|
// send audio packets, if necessary
|
|
if (node->getType() == NodeType::Agent && node->getActiveSocket()) {
|
|
++stats.sumListeners;
|
|
|
|
// mix the audio
|
|
bool mixHasAudio = prepareMix(node);
|
|
|
|
// send audio packet
|
|
if (mixHasAudio || data->shouldFlushEncoder()) {
|
|
QByteArray encodedBuffer;
|
|
if (mixHasAudio) {
|
|
// encode the audio
|
|
QByteArray decodedBuffer(reinterpret_cast<char*>(_bufferSamples), AudioConstants::NETWORK_FRAME_BYTES_STEREO);
|
|
data->encode(decodedBuffer, encodedBuffer);
|
|
} else {
|
|
// time to flush (resets shouldFlush until the next encode)
|
|
data->encodeFrameOfZeros(encodedBuffer);
|
|
}
|
|
|
|
sendMixPacket(node, *data, encodedBuffer);
|
|
} else {
|
|
++stats.sumListenersSilent;
|
|
sendSilentPacket(node, *data);
|
|
}
|
|
|
|
// send environment packet
|
|
sendEnvironmentPacket(node, *data);
|
|
|
|
// send stats packet (about every second)
|
|
const unsigned int NUM_FRAMES_PER_SEC = (int)ceil(AudioConstants::NETWORK_FRAMES_PER_SEC);
|
|
if (data->shouldSendStats(_frame % NUM_FRAMES_PER_SEC)) {
|
|
data->sendAudioStreamStatsPackets(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
template <class Container, class Predicate>
|
|
void erase_if(Container& cont, Predicate&& pred) {
|
|
auto it = remove_if(begin(cont), end(cont), std::forward<Predicate>(pred));
|
|
cont.erase(it, end(cont));
|
|
}
|
|
|
|
template <class Container>
|
|
bool contains(const Container& cont, typename Container::value_type value) {
|
|
return std::any_of(begin(cont), end(cont), [&value](const auto& element) {
|
|
return value == element;
|
|
});
|
|
}
|
|
|
|
// This class lets you do an erase if in several segments
|
|
// that use different predicates
|
|
template <class Container>
|
|
class SegmentedEraseIf {
|
|
public:
|
|
using iterator = typename Container::iterator;
|
|
|
|
SegmentedEraseIf(Container& cont) : _cont(cont) {
|
|
_first = begin(_cont);
|
|
_it = _first;
|
|
}
|
|
~SegmentedEraseIf() {
|
|
assert(_it == end(_cont));
|
|
_cont.erase(_first, _it);
|
|
}
|
|
|
|
template <class Predicate>
|
|
void iterateTo(iterator last, Predicate pred) {
|
|
while (_it != last) {
|
|
if (!pred(*_it)) {
|
|
if (_first != _it) {
|
|
*_first = move(*_it);
|
|
}
|
|
++_first;
|
|
}
|
|
++_it;
|
|
}
|
|
}
|
|
|
|
private:
|
|
iterator _first;
|
|
iterator _it;
|
|
Container& _cont;
|
|
};
|
|
|
|
|
|
void AudioMixerSlave::addStreams(Node& listener, AudioMixerClientData& listenerData) {
|
|
auto& ignoredNodeIDs = listener.getIgnoredNodeIDs();
|
|
auto& ignoringNodeIDs = listenerData.getIgnoringNodeIDs();
|
|
|
|
auto& streams = listenerData.getStreams();
|
|
|
|
// add data for newly created streams to our vector
|
|
if (!listenerData.getHasReceivedFirstMix()) {
|
|
// when this listener is new, we need to fill its added streams object with all available streams
|
|
std::for_each(_begin, _end, [&](const SharedNodePointer& node) {
|
|
AudioMixerClientData* nodeData = static_cast<AudioMixerClientData*>(node->getLinkedData());
|
|
if (nodeData) {
|
|
for (auto& stream : nodeData->getAudioStreams()) {
|
|
bool ignoredByListener = contains(ignoredNodeIDs, node->getUUID());
|
|
bool ignoringListener = contains(ignoringNodeIDs, node->getUUID());
|
|
|
|
if (ignoredByListener || ignoringListener) {
|
|
streams.skipped.emplace_back(node->getUUID(), node->getLocalID(),
|
|
stream->getStreamIdentifier(), stream.get());
|
|
|
|
// pre-populate ignored and ignoring flags for this stream
|
|
streams.skipped.back().ignoredByListener = ignoredByListener;
|
|
streams.skipped.back().ignoringListener = ignoringListener;
|
|
} else {
|
|
streams.active.emplace_back(node->getUUID(), node->getLocalID(),
|
|
stream->getStreamIdentifier(), stream.get());
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// flag this listener as having received their first mix so we know we don't need to enumerate all nodes again
|
|
listenerData.setHasReceivedFirstMix(true);
|
|
} else {
|
|
for (const auto& newStream : _sharedData.addedStreams) {
|
|
bool ignoredByListener = contains(ignoredNodeIDs, newStream.nodeIDStreamID.nodeID);
|
|
bool ignoringListener = contains(ignoringNodeIDs, newStream.nodeIDStreamID.nodeID);
|
|
|
|
if (ignoredByListener || ignoringListener) {
|
|
streams.skipped.emplace_back(newStream.nodeIDStreamID, newStream.positionalStream);
|
|
|
|
// pre-populate ignored and ignoring flags for this stream
|
|
streams.skipped.back().ignoredByListener = ignoredByListener;
|
|
streams.skipped.back().ignoringListener = ignoringListener;
|
|
} else {
|
|
streams.active.emplace_back(newStream.nodeIDStreamID, newStream.positionalStream);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool shouldBeRemoved(const MixableStream& stream, const AudioMixerSlave::SharedData& sharedData) {
|
|
return (contains(sharedData.removedNodes, stream.nodeStreamID.nodeLocalID) ||
|
|
contains(sharedData.removedStreams, stream.nodeStreamID));
|
|
};
|
|
|
|
bool shouldBeInactive(MixableStream& stream) {
|
|
return (!stream.positionalStream->lastPopSucceeded() ||
|
|
stream.positionalStream->getLastPopOutputLoudness() == 0.0f);
|
|
};
|
|
|
|
bool shouldBeSkipped(MixableStream& stream, const Node& listener,
|
|
const AvatarAudioStream& listenerAudioStream,
|
|
const AudioMixerClientData& listenerData) {
|
|
|
|
if (stream.nodeStreamID.nodeLocalID == listener.getLocalID()) {
|
|
return !stream.positionalStream->shouldLoopbackForNode();
|
|
}
|
|
|
|
// grab the unprocessed ignores and unignores from and for this listener
|
|
const auto& nodesIgnoredByListener = listenerData.getNewIgnoredNodeIDs();
|
|
const auto& nodesUnignoredByListener = listenerData.getNewUnignoredNodeIDs();
|
|
const auto& nodesIgnoringListener = listenerData.getNewIgnoringNodeIDs();
|
|
const auto& nodesUnignoringListener = listenerData.getNewUnignoringNodeIDs();
|
|
|
|
// this stream was previously not ignored by the listener and we have some newly ignored streams
|
|
// check now if it is one of the ignored streams and flag it as such
|
|
if (stream.ignoredByListener) {
|
|
stream.ignoredByListener = !contains(nodesUnignoredByListener, stream.nodeStreamID.nodeID);
|
|
} else {
|
|
stream.ignoredByListener = contains(nodesIgnoredByListener, stream.nodeStreamID.nodeID);
|
|
}
|
|
|
|
if (stream.ignoringListener) {
|
|
stream.ignoringListener = !contains(nodesUnignoringListener, stream.nodeStreamID.nodeID);
|
|
} else {
|
|
stream.ignoringListener = contains(nodesIgnoringListener, stream.nodeStreamID.nodeID);
|
|
}
|
|
|
|
bool listenerIsAdmin = listenerData.getRequestsDomainListData() && listener.getCanKick();
|
|
if (stream.ignoredByListener || (stream.ignoringListener && !listenerIsAdmin)) {
|
|
return true;
|
|
}
|
|
|
|
if (!listenerData.getSoloedNodes().empty()) {
|
|
return !contains(listenerData.getSoloedNodes(), stream.nodeStreamID.nodeID);
|
|
}
|
|
|
|
bool shouldCheckIgnoreBox = (listenerAudioStream.isIgnoreBoxEnabled() ||
|
|
stream.positionalStream->isIgnoreBoxEnabled());
|
|
if (shouldCheckIgnoreBox &&
|
|
listenerAudioStream.getIgnoreBox().touches(stream.positionalStream->getIgnoreBox())) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
float approximateVolume(const MixableStream& stream, const AvatarAudioStream* listenerAudioStream) {
|
|
if (stream.positionalStream->getLastPopOutputTrailingLoudness() == 0.0f) {
|
|
return 0.0f;
|
|
}
|
|
|
|
if (stream.positionalStream == listenerAudioStream) {
|
|
return 1.0f;
|
|
}
|
|
|
|
// approximate the gain
|
|
float gain = approximateGain(*listenerAudioStream, *(stream.positionalStream));
|
|
|
|
// for avatar streams, modify by the set gain adjustment
|
|
if (stream.nodeStreamID.streamID.isNull()) {
|
|
gain *= stream.hrtf->getGainAdjustment();
|
|
}
|
|
|
|
return stream.positionalStream->getLastPopOutputTrailingLoudness() * gain;
|
|
};
|
|
|
|
bool AudioMixerSlave::prepareMix(const SharedNodePointer& listener) {
|
|
AvatarAudioStream* listenerAudioStream = static_cast<AudioMixerClientData*>(listener->getLinkedData())->getAvatarAudioStream();
|
|
AudioMixerClientData* listenerData = static_cast<AudioMixerClientData*>(listener->getLinkedData());
|
|
|
|
// zero out the mix for this listener
|
|
memset(_mixSamples, 0, sizeof(_mixSamples));
|
|
|
|
bool isThrottling = _numToRetain != -1;
|
|
bool isSoloing = !listenerData->getSoloedNodes().empty();
|
|
|
|
auto& streams = listenerData->getStreams();
|
|
|
|
addStreams(*listener, *listenerData);
|
|
|
|
// Process skipped streams
|
|
erase_if(streams.skipped, [&](MixableStream& stream) {
|
|
if (shouldBeRemoved(stream, _sharedData)) {
|
|
return true;
|
|
}
|
|
|
|
if (!shouldBeSkipped(stream, *listener, *listenerAudioStream, *listenerData)) {
|
|
if (shouldBeInactive(stream)) {
|
|
streams.inactive.push_back(move(stream));
|
|
++stats.skippedToInactive;
|
|
} else {
|
|
streams.active.push_back(move(stream));
|
|
++stats.skippedToActive;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (!isThrottling) {
|
|
updateHRTFParameters(stream, *listenerAudioStream,
|
|
listenerData->getMasterAvatarGain());
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Process inactive streams
|
|
erase_if(streams.inactive, [&](MixableStream& stream) {
|
|
if (shouldBeRemoved(stream, _sharedData)) {
|
|
return true;
|
|
}
|
|
|
|
if (shouldBeSkipped(stream, *listener, *listenerAudioStream, *listenerData)) {
|
|
streams.skipped.push_back(move(stream));
|
|
++stats.inactiveToSkipped;
|
|
return true;
|
|
}
|
|
|
|
if (!shouldBeInactive(stream)) {
|
|
streams.active.push_back(move(stream));
|
|
++stats.inactiveToActive;
|
|
return true;
|
|
}
|
|
|
|
if (!isThrottling) {
|
|
updateHRTFParameters(stream, *listenerAudioStream,
|
|
listenerData->getMasterAvatarGain());
|
|
}
|
|
return false;
|
|
});
|
|
|
|
// Process active streams
|
|
erase_if(streams.active, [&](MixableStream& stream) {
|
|
if (shouldBeRemoved(stream, _sharedData)) {
|
|
return true;
|
|
}
|
|
|
|
if (isThrottling) {
|
|
// we're throttling, so we need to update the approximate volume for any un-skipped streams
|
|
// unless this is simply for an echo (in which case the approx volume is 1.0)
|
|
stream.approximateVolume = approximateVolume(stream, listenerAudioStream);
|
|
} else {
|
|
if (shouldBeSkipped(stream, *listener, *listenerAudioStream, *listenerData)) {
|
|
addStream(stream, *listenerAudioStream, 0.0f, isSoloing);
|
|
streams.skipped.push_back(move(stream));
|
|
++stats.activeToSkipped;
|
|
return true;
|
|
}
|
|
|
|
addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(),
|
|
isSoloing);
|
|
|
|
if (shouldBeInactive(stream)) {
|
|
// To reduce artifacts we still call render to flush the HRTF for every silent
|
|
// sources on the first frame where the source becomes silent
|
|
// this ensures the correct tail from last mixed block
|
|
streams.inactive.push_back(move(stream));
|
|
++stats.activeToInactive;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
if (isThrottling) {
|
|
// since we're throttling, we need to partition the mixable into throttled and unthrottled streams
|
|
int numToRetain = min(_numToRetain, (int)streams.active.size()); // Make sure we don't overflow
|
|
auto throttlePoint = begin(streams.active) + numToRetain;
|
|
|
|
std::nth_element(streams.active.begin(), throttlePoint, streams.active.end(),
|
|
[](const auto& a, const auto& b)
|
|
{
|
|
return a.approximateVolume > b.approximateVolume;
|
|
});
|
|
|
|
SegmentedEraseIf<MixableStreamsVector> erase(streams.active);
|
|
erase.iterateTo(throttlePoint, [&](MixableStream& stream) {
|
|
if (shouldBeSkipped(stream, *listener, *listenerAudioStream, *listenerData)) {
|
|
resetHRTFState(stream);
|
|
streams.skipped.push_back(move(stream));
|
|
++stats.activeToSkipped;
|
|
return true;
|
|
}
|
|
|
|
addStream(stream, *listenerAudioStream, listenerData->getMasterAvatarGain(),
|
|
isSoloing);
|
|
|
|
if (shouldBeInactive(stream)) {
|
|
// To reduce artifacts we still call render to flush the HRTF for every silent
|
|
// sources on the first frame where the source becomes silent
|
|
// this ensures the correct tail from last mixed block
|
|
streams.inactive.push_back(move(stream));
|
|
++stats.activeToInactive;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
erase.iterateTo(end(streams.active), [&](MixableStream& stream) {
|
|
// To reduce artifacts we reset the HRTF state for every throttled
|
|
// sources on the first frame where the source becomes throttled
|
|
// this ensures at least remove the tail from last mixed block
|
|
// preventing excessive artifacts on the next first block
|
|
resetHRTFState(stream);
|
|
|
|
if (shouldBeSkipped(stream, *listener, *listenerAudioStream, *listenerData)) {
|
|
streams.skipped.push_back(move(stream));
|
|
++stats.activeToSkipped;
|
|
return true;
|
|
}
|
|
|
|
if (shouldBeInactive(stream)) {
|
|
streams.inactive.push_back(move(stream));
|
|
++stats.activeToInactive;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
}
|
|
|
|
stats.skipped += (int)streams.skipped.size();
|
|
stats.inactive += (int)streams.inactive.size();
|
|
stats.active += (int)streams.active.size();
|
|
|
|
// clear the newly ignored, un-ignored, ignoring, and un-ignoring streams now that we've processed them
|
|
listenerData->clearStagedIgnoreChanges();
|
|
|
|
#ifdef HIFI_AUDIO_MIXER_DEBUG
|
|
auto mixEnd = p_high_resolution_clock::now();
|
|
auto mixTime = std::chrono::duration_cast<std::chrono::nanoseconds>(mixEnd - mixStart);
|
|
stats.mixTime += mixTime.count();
|
|
#endif
|
|
|
|
// check for silent audio before limiting
|
|
// limiting uses a dither and can only guarantee abs(sample) <= 1
|
|
bool hasAudio = false;
|
|
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; ++i) {
|
|
if (_mixSamples[i] != 0.0f) {
|
|
hasAudio = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// use the per listener AudioLimiter to render the mixed data
|
|
listenerData->audioLimiter.render(_mixSamples, _bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
|
|
|
return hasAudio;
|
|
}
|
|
|
|
void AudioMixerSlave::addStream(AudioMixerClientData::MixableStream& mixableStream,
|
|
AvatarAudioStream& listeningNodeStream,
|
|
float masterListenerGain, bool isSoloing) {
|
|
++stats.totalMixes;
|
|
|
|
auto streamToAdd = mixableStream.positionalStream;
|
|
|
|
// check if this is a server echo of a source back to itself
|
|
bool isEcho = (streamToAdd == &listeningNodeStream);
|
|
|
|
glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition();
|
|
|
|
float distance = glm::max(glm::length(relativePosition), EPSILON);
|
|
float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition);
|
|
|
|
float gain = 1.0f;
|
|
if (!isSoloing) {
|
|
gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho);
|
|
}
|
|
|
|
const int HRTF_DATASET_INDEX = 1;
|
|
|
|
if (!streamToAdd->lastPopSucceeded()) {
|
|
bool forceSilentBlock = true;
|
|
|
|
if (!streamToAdd->getLastPopOutput().isNull()) {
|
|
bool isInjector = dynamic_cast<const InjectedAudioStream*>(streamToAdd);
|
|
|
|
// in an injector, just go silent - the injector has likely ended
|
|
// in other inputs (microphone, &c.), repeat with fade to avoid the harsh jump to silence
|
|
if (!isInjector) {
|
|
// calculate its fade factor, which depends on how many times it's already been repeated.
|
|
float fadeFactor = calculateRepeatedFrameFadeFactor(streamToAdd->getConsecutiveNotMixedCount() - 1);
|
|
if (fadeFactor > 0.0f) {
|
|
// apply the fadeFactor to the gain
|
|
gain *= fadeFactor;
|
|
forceSilentBlock = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (forceSilentBlock) {
|
|
// call renderSilent with a forced silent block to reduce artifacts
|
|
// (this is not done for stereo streams since they do not go through the HRTF)
|
|
if (!streamToAdd->isStereo() && !isEcho) {
|
|
static int16_t silentMonoBlock[AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL] = {};
|
|
mixableStream.hrtf->render(silentMonoBlock, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
|
|
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
|
|
|
++stats.hrtfRenders;
|
|
}
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
// grab the stream from the ring buffer
|
|
AudioRingBuffer::ConstIterator streamPopOutput = streamToAdd->getLastPopOutput();
|
|
|
|
// stereo sources are not passed through HRTF
|
|
if (streamToAdd->isStereo()) {
|
|
|
|
// apply the avatar gain adjustment
|
|
gain *= mixableStream.hrtf->getGainAdjustment();
|
|
|
|
const float scale = 1 / 32768.0f; // int16_t to float
|
|
|
|
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) {
|
|
_mixSamples[2*i+0] += (float)streamPopOutput[2*i+0] * gain * scale;
|
|
_mixSamples[2*i+1] += (float)streamPopOutput[2*i+1] * gain * scale;
|
|
}
|
|
|
|
++stats.manualStereoMixes;
|
|
} else if (isEcho) {
|
|
// echo sources are not passed through HRTF
|
|
|
|
const float scale = 1/32768.0f; // int16_t to float
|
|
|
|
for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; i++) {
|
|
float sample = (float)streamPopOutput[i] * gain * scale;
|
|
_mixSamples[2*i+0] += sample;
|
|
_mixSamples[2*i+1] += sample;
|
|
}
|
|
|
|
++stats.manualEchoMixes;
|
|
} else {
|
|
streamPopOutput.readSamples(_bufferSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
|
|
|
mixableStream.hrtf->render(_bufferSamples, _mixSamples, HRTF_DATASET_INDEX, azimuth, distance, gain,
|
|
AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL);
|
|
|
|
++stats.hrtfRenders;
|
|
}
|
|
}
|
|
|
|
void AudioMixerSlave::updateHRTFParameters(AudioMixerClientData::MixableStream& mixableStream,
|
|
AvatarAudioStream& listeningNodeStream,
|
|
float masterListenerGain) {
|
|
auto streamToAdd = mixableStream.positionalStream;
|
|
|
|
// check if this is a server echo of a source back to itself
|
|
bool isEcho = (streamToAdd == &listeningNodeStream);
|
|
|
|
glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream.getPosition();
|
|
|
|
float distance = glm::max(glm::length(relativePosition), EPSILON);
|
|
float gain = computeGain(masterListenerGain, listeningNodeStream, *streamToAdd, relativePosition, distance, isEcho);
|
|
float azimuth = isEcho ? 0.0f : computeAzimuth(listeningNodeStream, listeningNodeStream, relativePosition);
|
|
|
|
mixableStream.hrtf->setParameterHistory(azimuth, distance, gain);
|
|
|
|
++stats.hrtfUpdates;
|
|
}
|
|
|
|
void AudioMixerSlave::resetHRTFState(AudioMixerClientData::MixableStream& mixableStream) {
|
|
mixableStream.hrtf->reset();
|
|
++stats.hrtfResets;
|
|
}
|
|
|
|
std::unique_ptr<NLPacket> createAudioPacket(PacketType type, int size, quint16 sequence, QString codec) {
|
|
auto audioPacket = NLPacket::create(type, size);
|
|
audioPacket->writePrimitive(sequence);
|
|
audioPacket->writeString(codec);
|
|
return audioPacket;
|
|
}
|
|
|
|
void sendMixPacket(const SharedNodePointer& node, AudioMixerClientData& data, QByteArray& buffer) {
|
|
const int MIX_PACKET_SIZE =
|
|
sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE + AudioConstants::NETWORK_FRAME_BYTES_STEREO;
|
|
quint16 sequence = data.getOutgoingSequenceNumber();
|
|
QString codec = data.getCodecName();
|
|
auto mixPacket = createAudioPacket(PacketType::MixedAudio, MIX_PACKET_SIZE, sequence, codec);
|
|
|
|
// pack samples
|
|
mixPacket->write(buffer.constData(), buffer.size());
|
|
|
|
// send packet
|
|
DependencyManager::get<NodeList>()->sendPacket(std::move(mixPacket), *node);
|
|
data.incrementOutgoingMixedAudioSequenceNumber();
|
|
}
|
|
|
|
void sendSilentPacket(const SharedNodePointer& node, AudioMixerClientData& data) {
|
|
const int SILENT_PACKET_SIZE =
|
|
sizeof(quint16) + AudioConstants::MAX_CODEC_NAME_LENGTH_ON_WIRE + sizeof(quint16);
|
|
quint16 sequence = data.getOutgoingSequenceNumber();
|
|
QString codec = data.getCodecName();
|
|
auto mixPacket = createAudioPacket(PacketType::SilentAudioFrame, SILENT_PACKET_SIZE, sequence, codec);
|
|
|
|
// pack number of samples
|
|
mixPacket->writePrimitive(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
|
|
|
|
// send packet
|
|
DependencyManager::get<NodeList>()->sendPacket(std::move(mixPacket), *node);
|
|
data.incrementOutgoingMixedAudioSequenceNumber();
|
|
}
|
|
|
|
void sendMutePacket(const SharedNodePointer& node, AudioMixerClientData& data) {
|
|
auto mutePacket = NLPacket::create(PacketType::NoisyMute, 0);
|
|
DependencyManager::get<NodeList>()->sendPacket(std::move(mutePacket), *node);
|
|
|
|
// probably now we just reset the flag, once should do it (?)
|
|
data.setShouldMuteClient(false);
|
|
}
|
|
|
|
void sendEnvironmentPacket(const SharedNodePointer& node, AudioMixerClientData& data) {
|
|
bool hasReverb = false;
|
|
float reverbTime, wetLevel;
|
|
|
|
auto& reverbSettings = AudioMixer::getReverbSettings();
|
|
auto& audioZones = AudioMixer::getAudioZones();
|
|
|
|
AvatarAudioStream* stream = data.getAvatarAudioStream();
|
|
glm::vec3 streamPosition = stream->getPosition();
|
|
|
|
// find reverb properties
|
|
for (const auto& settings : reverbSettings) {
|
|
AABox box = audioZones[settings.zone].area;
|
|
if (box.contains(streamPosition)) {
|
|
hasReverb = true;
|
|
reverbTime = settings.reverbTime;
|
|
wetLevel = settings.wetLevel;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// check if data changed
|
|
bool dataChanged = (stream->hasReverb() != hasReverb) ||
|
|
(stream->hasReverb() && (stream->getRevebTime() != reverbTime || stream->getWetLevel() != wetLevel));
|
|
if (dataChanged) {
|
|
// update stream
|
|
if (hasReverb) {
|
|
stream->setReverb(reverbTime, wetLevel);
|
|
} else {
|
|
stream->clearReverb();
|
|
}
|
|
}
|
|
|
|
// send packet at change or every so often
|
|
float CHANCE_OF_SEND = 0.01f;
|
|
bool sendData = dataChanged || (randFloat() < CHANCE_OF_SEND);
|
|
|
|
if (sendData) {
|
|
// size the packet
|
|
unsigned char bitset = 0;
|
|
int packetSize = sizeof(bitset);
|
|
if (hasReverb) {
|
|
packetSize += sizeof(reverbTime) + sizeof(wetLevel);
|
|
}
|
|
|
|
// write the packet
|
|
auto envPacket = NLPacket::create(PacketType::AudioEnvironment, packetSize);
|
|
if (hasReverb) {
|
|
setAtBit(bitset, HAS_REVERB_BIT);
|
|
}
|
|
envPacket->writePrimitive(bitset);
|
|
if (hasReverb) {
|
|
envPacket->writePrimitive(reverbTime);
|
|
envPacket->writePrimitive(wetLevel);
|
|
}
|
|
|
|
// send the packet
|
|
DependencyManager::get<NodeList>()->sendPacket(std::move(envPacket), *node);
|
|
}
|
|
}
|
|
|
|
float approximateGain(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd) {
|
|
float gain = 1.0f;
|
|
|
|
// injector: apply attenuation
|
|
if (streamToAdd.getType() == PositionalAudioStream::Injector) {
|
|
gain *= reinterpret_cast<const InjectedAudioStream*>(&streamToAdd)->getAttenuationRatio();
|
|
}
|
|
|
|
// avatar: skip attenuation - it is too costly to approximate
|
|
|
|
// distance attenuation: approximate, ignore zone-specific attenuations
|
|
glm::vec3 relativePosition = streamToAdd.getPosition() - listeningNodeStream.getPosition();
|
|
float distance = glm::length(relativePosition);
|
|
return gain / distance;
|
|
|
|
// avatar: skip master gain - it is constant for all streams
|
|
}
|
|
|
|
float computeGain(float masterListenerGain, const AvatarAudioStream& listeningNodeStream,
|
|
const PositionalAudioStream& streamToAdd, const glm::vec3& relativePosition, float distance, bool isEcho) {
|
|
float gain = 1.0f;
|
|
|
|
// injector: apply attenuation
|
|
if (streamToAdd.getType() == PositionalAudioStream::Injector) {
|
|
gain *= reinterpret_cast<const InjectedAudioStream*>(&streamToAdd)->getAttenuationRatio();
|
|
|
|
// avatar: apply fixed off-axis attenuation to make them quieter as they turn away
|
|
} else if (!isEcho && (streamToAdd.getType() == PositionalAudioStream::Microphone)) {
|
|
glm::vec3 rotatedListenerPosition = glm::inverse(streamToAdd.getOrientation()) * relativePosition;
|
|
|
|
// source directivity is based on angle of emission, in local coordinates
|
|
glm::vec3 direction = glm::normalize(rotatedListenerPosition);
|
|
float angleOfDelivery = fastAcosf(glm::clamp(-direction.z, -1.0f, 1.0f)); // UNIT_NEG_Z is "forward"
|
|
|
|
const float MAX_OFF_AXIS_ATTENUATION = 0.2f;
|
|
const float OFF_AXIS_ATTENUATION_STEP = (1 - MAX_OFF_AXIS_ATTENUATION) / 2.0f;
|
|
float offAxisCoefficient = MAX_OFF_AXIS_ATTENUATION + (angleOfDelivery * (OFF_AXIS_ATTENUATION_STEP / PI_OVER_TWO));
|
|
|
|
gain *= offAxisCoefficient;
|
|
|
|
// apply master gain, only to avatars
|
|
gain *= masterListenerGain;
|
|
}
|
|
|
|
auto& audioZones = AudioMixer::getAudioZones();
|
|
auto& zoneSettings = AudioMixer::getZoneSettings();
|
|
|
|
// find distance attenuation coefficient
|
|
float attenuationPerDoublingInDistance = AudioMixer::getAttenuationPerDoublingInDistance();
|
|
for (const auto& settings : zoneSettings) {
|
|
if (audioZones[settings.source].area.contains(streamToAdd.getPosition()) &&
|
|
audioZones[settings.listener].area.contains(listeningNodeStream.getPosition())) {
|
|
attenuationPerDoublingInDistance = settings.coefficient;
|
|
break;
|
|
}
|
|
}
|
|
// translate the zone setting to gain per log2(distance)
|
|
float g = glm::clamp(1.0f - attenuationPerDoublingInDistance, EPSILON, 1.0f);
|
|
|
|
// calculate the attenuation using the distance to this node
|
|
// reference attenuation of 0dB at distance = 1.0m
|
|
gain *= fastExp2f(fastLog2f(g) * fastLog2f(std::max(distance, HRTF_NEARFIELD_MIN)));
|
|
gain = std::min(gain, 1.0f / HRTF_NEARFIELD_MIN);
|
|
|
|
return gain;
|
|
}
|
|
|
|
float computeAzimuth(const AvatarAudioStream& listeningNodeStream, const PositionalAudioStream& streamToAdd,
|
|
const glm::vec3& relativePosition) {
|
|
glm::quat inverseOrientation = glm::inverse(listeningNodeStream.getOrientation());
|
|
|
|
glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition;
|
|
|
|
// project the rotated source position vector onto the XZ plane
|
|
rotatedSourcePosition.y = 0.0f;
|
|
|
|
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;
|
|
}
|
|
}
|