diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 51c4e25410..7ba9242306 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -93,6 +93,7 @@ AudioMixer::AudioMixer(ReceivedMessage& message) : this, "handleNodeAudioPacket"); packetReceiver.registerListener(PacketType::MuteEnvironment, this, "handleMuteEnvironmentPacket"); packetReceiver.registerListener(PacketType::NegotiateAudioFormat, this, "handleNegotiateAudioFormat"); + packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled); } @@ -324,7 +325,8 @@ bool AudioMixer::prepareMixForListeningNode(Node* node) { // loop through all other nodes that have sufficient audio to mix DependencyManager::get()->eachNode([&](const SharedNodePointer& otherNode){ - if (otherNode->getLinkedData()) { + // make sure that we have audio data for this other node and that it isn't being ignored by our listening node + if (otherNode->getLinkedData() && !node->isIgnoringNodeWithID(otherNode->getUUID())) { AudioMixerClientData* otherNodeClientData = (AudioMixerClientData*) otherNode->getLinkedData(); // enumerate the ARBs attached to the otherNode and add all that should be added to mix @@ -554,6 +556,10 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) { }); } +void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode) { + sendingNode->parseIgnoreRequestMessage(packet); +} + void AudioMixer::removeHRTFsForFinishedInjector(const QUuid& streamID) { auto injectorClientData = qobject_cast(sender()); if (injectorClientData) { diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 4b2a27120d..9b26989847 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -47,6 +47,7 @@ private slots: void handleMuteEnvironmentPacket(QSharedPointer packet, SharedNodePointer sendingNode); void handleNegotiateAudioFormat(QSharedPointer message, SharedNodePointer sendingNode); void handleNodeKilled(SharedNodePointer killedNode); + void handleNodeIgnoreRequestPacket(QSharedPointer packet, SharedNodePointer sendingNode); void removeHRTFsForFinishedInjector(const QUuid& streamID); diff --git a/assignment-client/src/avatars/AvatarMixer.cpp b/assignment-client/src/avatars/AvatarMixer.cpp index 46599396ca..65989b389e 100644 --- a/assignment-client/src/avatars/AvatarMixer.cpp +++ b/assignment-client/src/avatars/AvatarMixer.cpp @@ -45,6 +45,7 @@ AvatarMixer::AvatarMixer(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::AvatarData, this, "handleAvatarDataPacket"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "handleAvatarIdentityPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket"); + packetReceiver.registerListener(PacketType::NodeIgnoreRequest, this, "handleNodeIgnoreRequestPacket"); auto nodeList = DependencyManager::get(); connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &AvatarMixer::handlePacketVersionMismatch); @@ -227,14 +228,15 @@ void AvatarMixer::broadcastAvatarData() { // send back a packet with other active node data to this node nodeList->eachMatchingNode( [&](const SharedNodePointer& otherNode)->bool { - if (!otherNode->getLinkedData()) { + // make sure we have data for this avatar, that it isn't the same node, + // and isn't an avatar that the viewing node has ignored + if (!otherNode->getLinkedData() + || otherNode->getUUID() == node->getUUID() + || node->isIgnoringNodeWithID(otherNode->getUUID())) { return false; + } else { + return true; } - if (otherNode->getUUID() == node->getUUID()) { - return false; - } - - return true; }, [&](const SharedNodePointer& otherNode) { ++numOtherAvatars; @@ -431,6 +433,10 @@ void AvatarMixer::handleKillAvatarPacket(QSharedPointer message DependencyManager::get()->processKillNode(*message); } +void AvatarMixer::handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode) { + senderNode->parseIgnoreRequestMessage(message); +} + void AvatarMixer::sendStatsPacket() { QJsonObject statsObject; statsObject["average_listeners_last_second"] = (float) _sumListeners / (float) _numStatFrames; diff --git a/assignment-client/src/avatars/AvatarMixer.h b/assignment-client/src/avatars/AvatarMixer.h index d1a9249c83..00cf457d40 100644 --- a/assignment-client/src/avatars/AvatarMixer.h +++ b/assignment-client/src/avatars/AvatarMixer.h @@ -37,9 +37,11 @@ private slots: void handleAvatarDataPacket(QSharedPointer message, SharedNodePointer senderNode); void handleAvatarIdentityPacket(QSharedPointer message, SharedNodePointer senderNode); void handleKillAvatarPacket(QSharedPointer message); + void handleNodeIgnoreRequestPacket(QSharedPointer message, SharedNodePointer senderNode); void domainSettingsRequestComplete(); void handlePacketVersionMismatch(PacketType type, const HifiSockAddr& senderSockAddr, const QUuid& senderUUID); + private: void broadcastAvatarData(); void parseDomainServerSettings(const QJsonObject& domainSettings); diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 48bbccc3b6..0e0f56438e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -98,6 +98,7 @@ #include #include #include +#include #include #include #include @@ -440,6 +441,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -4755,6 +4757,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Reticle", getApplicationCompositor().getReticleInterface()); scriptEngine->registerGlobalObject("UserActivityLogger", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Users", DependencyManager::get().data()); } bool Application::canAcceptURL(const QString& urlString) const { diff --git a/interface/src/avatar/AvatarManager.cpp b/interface/src/avatar/AvatarManager.cpp index 31a77df0cf..bd76d2bd81 100644 --- a/interface/src/avatar/AvatarManager.cpp +++ b/interface/src/avatar/AvatarManager.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include "Application.h" @@ -69,10 +70,15 @@ AvatarManager::AvatarManager(QObject* parent) : // register a meta type for the weak pointer we'll use for the owning avatar mixer for each avatar qRegisterMetaType >("NodeWeakPointer"); - auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); + auto nodeList = DependencyManager::get(); + auto& packetReceiver = nodeList->getPacketReceiver(); packetReceiver.registerListener(PacketType::BulkAvatarData, this, "processAvatarDataPacket"); packetReceiver.registerListener(PacketType::KillAvatar, this, "processKillAvatar"); packetReceiver.registerListener(PacketType::AvatarIdentity, this, "processAvatarIdentityPacket"); + + // when we hear that the user has ignored an avatar by session UUID + // immediately remove that avatar instead of waiting for the absence of packets from avatar mixer + connect(nodeList.data(), &NodeList::ignoredNode, this, &AvatarManager::removeAvatar); } AvatarManager::~AvatarManager() { @@ -85,7 +91,8 @@ void AvatarManager::init() { _avatarHash.insert(MY_AVATAR_KEY, _myAvatar); } - connect(DependencyManager::get().data(), &SceneScriptingInterface::shouldRenderAvatarsChanged, this, &AvatarManager::updateAvatarRenderStatus, Qt::QueuedConnection); + connect(DependencyManager::get().data(), &SceneScriptingInterface::shouldRenderAvatarsChanged, + this, &AvatarManager::updateAvatarRenderStatus, Qt::QueuedConnection); render::ScenePointer scene = qApp->getMain3DScene(); render::PendingChanges pendingChanges; diff --git a/interface/src/avatar/AvatarManager.h b/interface/src/avatar/AvatarManager.h index c49d566aa8..f09aa9791c 100644 --- a/interface/src/avatar/AvatarManager.h +++ b/interface/src/avatar/AvatarManager.h @@ -78,6 +78,9 @@ public slots: void setShouldShowReceiveStats(bool shouldShowReceiveStats) { _shouldShowReceiveStats = shouldShowReceiveStats; } void updateAvatarRenderStatus(bool shouldRenderAvatars); +private slots: + virtual void removeAvatar(const QUuid& sessionUUID) override; + private: explicit AvatarManager(QObject* parent = 0); explicit AvatarManager(const AvatarManager& other); @@ -88,7 +91,6 @@ private: virtual AvatarSharedPointer newSharedAvatar() override; virtual AvatarSharedPointer addAvatar(const QUuid& sessionUUID, const QWeakPointer& mixerWeakPointer) override; - virtual void removeAvatar(const QUuid& sessionUUID) override; virtual void handleRemovedAvatar(const AvatarSharedPointer& removedAvatar) override; QVector _avatarFades; diff --git a/interface/src/scripting/ToolbarScriptingInterface.cpp b/interface/src/scripting/ToolbarScriptingInterface.cpp index 82332b3187..175a7fd539 100644 --- a/interface/src/scripting/ToolbarScriptingInterface.cpp +++ b/interface/src/scripting/ToolbarScriptingInterface.cpp @@ -14,7 +14,7 @@ class QmlWrapper : public QObject { Q_OBJECT public: QmlWrapper(QObject* qmlObject, QObject* parent = nullptr) - : QObject(parent), _qmlObject(qmlObject) { + : QObject(parent), _qmlObject(qmlObject) { } Q_INVOKABLE void writeProperty(QString propertyName, QVariant propertyValue) { @@ -91,6 +91,10 @@ public: return new ToolbarButtonProxy(rawButton, this); } + + Q_INVOKABLE void removeButton(const QVariant& name) { + QMetaObject::invokeMethod(_qmlObject, "removeButton", Qt::AutoConnection, Q_ARG(QVariant, name)); + } }; @@ -112,4 +116,4 @@ QObject* ToolbarScriptingInterface::getToolbar(const QString& toolbarId) { } -#include "ToolbarScriptingInterface.moc" \ No newline at end of file +#include "ToolbarScriptingInterface.moc" diff --git a/interface/src/ui/overlays/Base3DOverlay.h b/interface/src/ui/overlays/Base3DOverlay.h index 41e7e517b7..e602dec48c 100644 --- a/interface/src/ui/overlays/Base3DOverlay.h +++ b/interface/src/ui/overlays/Base3DOverlay.h @@ -17,7 +17,7 @@ class Base3DOverlay : public Overlay { Q_OBJECT - + public: Base3DOverlay(); Base3DOverlay(const Base3DOverlay* base3DOverlay); @@ -27,10 +27,10 @@ public: const glm::vec3& getPosition() const { return _transform.getTranslation(); } const glm::quat& getRotation() const { return _transform.getRotation(); } const glm::vec3& getScale() const { return _transform.getScale(); } - + // TODO: consider implementing registration points in this class const glm::vec3& getCenter() const { return getPosition(); } - + float getLineWidth() const { return _lineWidth; } bool getIsSolid() const { return _isSolid; } bool getIsDashedLine() const { return _isDashedLine; } @@ -43,7 +43,7 @@ public: void setRotation(const glm::quat& value) { _transform.setRotation(value); } void setScale(float value) { _transform.setScale(value); } void setScale(const glm::vec3& value) { _transform.setScale(value); } - + void setLineWidth(float lineWidth) { _lineWidth = lineWidth; } void setIsSolid(bool isSolid) { _isSolid = isSolid; } void setIsDashedLine(bool isDashedLine) { _isDashedLine = isDashedLine; } @@ -55,22 +55,22 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, + virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal); - virtual bool findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, + virtual bool findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal, QString& extraInfo) { return findRayIntersection(origin, direction, distance, face, surfaceNormal); } protected: Transform _transform; - + float _lineWidth; bool _isSolid; bool _isDashedLine; bool _ignoreRayIntersection; bool _drawInFront; }; - + #endif // hifi_Base3DOverlay_h diff --git a/interface/src/ui/overlays/ModelOverlay.cpp b/interface/src/ui/overlays/ModelOverlay.cpp index a857dc39e0..9c203c0129 100644 --- a/interface/src/ui/overlays/ModelOverlay.cpp +++ b/interface/src/ui/overlays/ModelOverlay.cpp @@ -19,8 +19,7 @@ QString const ModelOverlay::TYPE = "model"; ModelOverlay::ModelOverlay() : _model(std::make_shared(std::make_shared())), - _modelTextures(QVariantMap()), - _updateModel(false) + _modelTextures(QVariantMap()) { _model->init(); _isLoaded = false; @@ -44,7 +43,11 @@ void ModelOverlay::update(float deltatime) { if (_updateModel) { _updateModel = false; _model->setSnapModelToCenter(true); - _model->setScaleToFit(true, getDimensions()); + if (_scaleToFit) { + _model->setScaleToFit(true, getScale() * getDimensions()); + } else { + _model->setScale(getScale()); + } _model->setRotation(getRotation()); _model->setTranslation(getPosition()); _model->setURL(_url); @@ -84,16 +87,31 @@ void ModelOverlay::render(RenderArgs* args) { } void ModelOverlay::setProperties(const QVariantMap& properties) { - auto position = getPosition(); - auto rotation = getRotation(); + auto origPosition = getPosition(); + auto origRotation = getRotation(); + auto origDimensions = getDimensions(); + auto origScale = getScale(); - Volume3DOverlay::setProperties(properties); + Base3DOverlay::setProperties(properties); - if (position != getPosition() || rotation != getRotation()) { - _updateModel = true; + auto scale = properties["scale"]; + if (scale.isValid()) { + setScale(vec3FromVariant(scale)); } - _updateModel = true; + auto dimensions = properties["dimensions"]; + if (dimensions.isValid()) { + _scaleToFit = true; + setDimensions(vec3FromVariant(dimensions)); + } else if (scale.isValid()) { + // if "scale" property is set but "dimentions" is not. + // do NOT scale to fit. + _scaleToFit = false; + } + + if (origPosition != getPosition() || origRotation != getRotation() || origDimensions != getDimensions() || origScale != getScale()) { + _updateModel = true; + } auto urlValue = properties["url"]; if (urlValue.isValid() && urlValue.canConvert()) { @@ -101,15 +119,15 @@ void ModelOverlay::setProperties(const QVariantMap& properties) { _updateModel = true; _isLoaded = false; } - + auto texturesValue = properties["textures"]; if (texturesValue.isValid() && texturesValue.canConvert(QVariant::Map)) { QVariantMap textureMap = texturesValue.toMap(); foreach(const QString& key, textureMap.keys()) { - + QUrl newTextureURL = textureMap[key].toUrl(); qDebug() << "Updating texture named" << key << "to texture at URL" << newTextureURL; - + QMetaObject::invokeMethod(_model.get(), "setTextureWithNameToURL", Qt::AutoConnection, Q_ARG(const QString&, key), Q_ARG(const QUrl&, newTextureURL)); @@ -123,8 +141,11 @@ QVariant ModelOverlay::getProperty(const QString& property) { if (property == "url") { return _url.toString(); } - if (property == "dimensions" || property == "scale" || property == "size") { - return vec3toVariant(_model->getScaleToFitDimensions()); + if (property == "dimensions" || property == "size") { + return vec3toVariant(getDimensions()); + } + if (property == "scale") { + return vec3toVariant(getScale()); } if (property == "textures") { if (_modelTextures.size() > 0) { @@ -143,14 +164,14 @@ QVariant ModelOverlay::getProperty(const QString& property) { bool ModelOverlay::findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal) { - + QString subMeshNameTemp; return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, surfaceNormal, subMeshNameTemp); } bool ModelOverlay::findRayIntersectionExtraInfo(const glm::vec3& origin, const glm::vec3& direction, float& distance, BoxFace& face, glm::vec3& surfaceNormal, QString& extraInfo) { - + return _model->findRayIntersectionAgainstSubMeshes(origin, direction, distance, face, surfaceNormal, extraInfo); } diff --git a/interface/src/ui/overlays/ModelOverlay.h b/interface/src/ui/overlays/ModelOverlay.h index dc4b4a853b..091cab44c9 100644 --- a/interface/src/ui/overlays/ModelOverlay.h +++ b/interface/src/ui/overlays/ModelOverlay.h @@ -43,9 +43,10 @@ private: ModelPointer _model; QVariantMap _modelTextures; - + QUrl _url; - bool _updateModel; + bool _updateModel = { false }; + bool _scaleToFit = { false }; }; #endif // hifi_ModelOverlay_h diff --git a/interface/src/ui/overlays/Volume3DOverlay.cpp b/interface/src/ui/overlays/Volume3DOverlay.cpp index c8078d35c6..563198c976 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.cpp +++ b/interface/src/ui/overlays/Volume3DOverlay.cpp @@ -22,7 +22,7 @@ AABox Volume3DOverlay::getBounds() const { auto extents = Extents{_localBoundingBox}; extents.rotate(getRotation()); extents.shiftBy(getPosition()); - + return AABox(extents); } @@ -31,7 +31,7 @@ void Volume3DOverlay::setProperties(const QVariantMap& properties) { auto dimensions = properties["dimensions"]; - // if "dimensions" property was not there, check to see if they included aliases: scale + // if "dimensions" property was not there, check to see if they included aliases: scale, size if (!dimensions.isValid()) { dimensions = properties["scale"]; if (!dimensions.isValid()) { @@ -57,7 +57,7 @@ bool Volume3DOverlay::findRayIntersection(const glm::vec3& origin, const glm::ve // extents is the entity relative, scaled, centered extents of the entity glm::mat4 worldToEntityMatrix; _transform.getInverseMatrix(worldToEntityMatrix); - + glm::vec3 overlayFrameOrigin = glm::vec3(worldToEntityMatrix * glm::vec4(origin, 1.0f)); glm::vec3 overlayFrameDirection = glm::vec3(worldToEntityMatrix * glm::vec4(direction, 0.0f)); diff --git a/interface/src/ui/overlays/Volume3DOverlay.h b/interface/src/ui/overlays/Volume3DOverlay.h index 4d087615d2..04b694b2f8 100644 --- a/interface/src/ui/overlays/Volume3DOverlay.h +++ b/interface/src/ui/overlays/Volume3DOverlay.h @@ -15,13 +15,13 @@ class Volume3DOverlay : public Base3DOverlay { Q_OBJECT - + public: Volume3DOverlay() {} Volume3DOverlay(const Volume3DOverlay* volume3DOverlay); - + virtual AABox getBounds() const override; - + const glm::vec3& getDimensions() const { return _localBoundingBox.getDimensions(); } void setDimensions(float value) { _localBoundingBox.setBox(glm::vec3(-value / 2.0f), value); } void setDimensions(const glm::vec3& value) { _localBoundingBox.setBox(-value / 2.0f, value); } @@ -29,13 +29,13 @@ public: void setProperties(const QVariantMap& properties) override; QVariant getProperty(const QString& property) override; - virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, - BoxFace& face, glm::vec3& surfaceNormal) override; - + virtual bool findRayIntersection(const glm::vec3& origin, const glm::vec3& direction, float& distance, + BoxFace& face, glm::vec3& surfaceNormal) override; + protected: // Centered local bounding box AABox _localBoundingBox{ vec3(0.0f), 1.0f }; }; - + #endif // hifi_Volume3DOverlay_h diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 0cd01bb579..dd72125d93 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -14,6 +14,8 @@ #include #include +#include +#include #ifdef __APPLE__ #include @@ -43,8 +45,6 @@ #include #include -#include "AudioInjector.h" -#include "AudioConstants.h" #include "PositionalAudioStream.h" #include "AudioClientLogging.h" @@ -104,6 +104,7 @@ AudioClient::AudioClient() : _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), + _audioLimiter(AudioConstants::SAMPLE_RATE, AudioConstants::STEREO), _outgoingAvatarAudioSequenceNumber(0), _audioOutputIODevice(_receivedAudioStream, this), _stats(&_receivedAudioStream), @@ -534,14 +535,18 @@ void AudioClient::handleSelectedAudioFormat(QSharedPointer mess _selectedCodecName = message->readString(); qDebug() << "Selected Codec:" << _selectedCodecName; + + // release any old codec encoder/decoder first... + if (_codec && _encoder) { + _codec->releaseEncoder(_encoder); + _encoder = nullptr; + _codec = nullptr; + } + _receivedAudioStream.cleanupCodec(); + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); for (auto& plugin : codecPlugins) { if (_selectedCodecName == plugin->getName()) { - // release any old codec encoder/decoder first... - if (_codec && _encoder) { - _codec->releaseEncoder(_encoder); - _encoder = nullptr; - } _codec = plugin; _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); @@ -821,7 +826,6 @@ void AudioClient::handleAudioInput() { audioTransform.setRotation(_orientationGetter()); // FIXME find a way to properly handle both playback audio and user audio concurrently - // TODO - codec encode goes here QByteArray decocedBuffer(reinterpret_cast(networkAudioSamples), numNetworkBytes); QByteArray encodedBuffer; if (_encoder) { @@ -840,7 +844,6 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { audioTransform.setTranslation(_positionGetter()); audioTransform.setRotation(_orientationGetter()); - // TODO - codec encode goes here QByteArray encodedBuffer; if (_encoder) { _encoder->encode(audio, encodedBuffer); @@ -852,6 +855,74 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { emitAudioPacket(encodedBuffer.data(), encodedBuffer.size(), _outgoingAvatarAudioSequenceNumber, audioTransform, PacketType::MicrophoneAudioWithEcho); } +void AudioClient::mixLocalAudioInjectors(int16_t* inputBuffer) { + + memset(_hrtfBuffer, 0, sizeof(_hrtfBuffer)); + QVector injectorsToRemove; + static const float INT16_TO_FLOAT_SCALE_FACTOR = 1/32768.0f; + + bool injectorsHaveData = false; + + for (AudioInjector* injector : getActiveLocalAudioInjectors()) { + if (injector->getLocalBuffer()) { + + qint64 samplesToRead = injector->isStereo() ? + AudioConstants::NETWORK_FRAME_BYTES_STEREO : + AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; + + // get one frame from the injector (mono or stereo) + if (0 < injector->getLocalBuffer()->readData((char*)_scratchBuffer, samplesToRead)) { + + injectorsHaveData = true; + + if (injector->isStereo() ) { + for(int i=0; igetPosition() - _positionGetter(); + float gain = gainForSource(relativePosition, injector->getVolume()); + float azimuth = azimuthForSource(relativePosition); + + + injector->getLocalHRTF().render(_scratchBuffer, _hrtfBuffer, 1, azimuth, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } + + } else { + + qDebug() << "injector has no more data, marking finished for removal"; + injector->finish(); + injectorsToRemove.append(injector); + } + + } else { + + qDebug() << "injector has no local buffer, marking as finished for removal"; + injector->finish(); + injectorsToRemove.append(injector); + } + } + + if(injectorsHaveData) { + + // mix network into the hrtfBuffer + for(int i=0; i(decodedBuffer.data()); + const int16_t* decodedSamples; int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); + QByteArray decodedBufferCopy = decodedBuffer; + assert(decodedBuffer.size() == AudioConstants::NETWORK_FRAME_BYTES_STEREO); + + if(getActiveLocalAudioInjectors().size() > 0) { + mixLocalAudioInjectors((int16_t*)decodedBufferCopy.data()); + decodedSamples = reinterpret_cast(decodedBufferCopy.data()); + } else { + decodedSamples = reinterpret_cast(decodedBuffer.data()); + } // copy the packet from the RB to the output possibleResampling(_networkToOutputResampler, decodedSamples, outputSamples, @@ -874,6 +954,7 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA if (hasReverb) { assert(_outputFormat.channelCount() == 2); updateReverbOptions(); + qDebug() << "handling reverb"; _listenerReverb.render(outputSamples, outputSamples, numDeviceOutputSamples/2); } } @@ -923,36 +1004,25 @@ void AudioClient::setIsStereoInput(bool isStereoInput) { bool AudioClient::outputLocalInjector(bool isStereo, AudioInjector* injector) { - if (injector->getLocalBuffer()) { - QAudioFormat localFormat = _desiredOutputFormat; - localFormat.setChannelCount(isStereo ? 2 : 1); + if (injector->getLocalBuffer() && _audioInput ) { + // just add it to the vector of active local injectors, if + // not already there. + // Since this is invoked with invokeMethod, there _should_ be + // no reason to lock access to the vector of injectors. + if (!_activeLocalAudioInjectors.contains(injector)) { + qDebug() << "adding new injector"; + + _activeLocalAudioInjectors.append(injector); + } else { + qDebug() << "injector exists in active list already"; + } + + return true; - QAudioOutput* localOutput = new QAudioOutput(getNamedAudioDeviceForMode(QAudio::AudioOutput, _outputAudioDeviceName), - localFormat, - injector->getLocalBuffer()); - - // move the localOutput to the same thread as the local injector buffer - localOutput->moveToThread(injector->getLocalBuffer()->thread()); - - // have it be stopped when that local buffer is about to close - // We don't want to stop this localOutput and injector whenever this AudioClient singleton goes idle, - // only when the localOutput does. But the connection is to localOutput, so that it happens on the right thread. - connect(localOutput, &QAudioOutput::stateChanged, localOutput, [=](QAudio::State state) { - if (state == QAudio::IdleState) { - localOutput->stop(); - injector->stop(); - } - }); - - connect(injector->getLocalBuffer(), &QIODevice::aboutToClose, localOutput, &QAudioOutput::stop); - - qCDebug(audioclient) << "Starting QAudioOutput for local injector" << localOutput; - - localOutput->start(injector->getLocalBuffer()); - return localOutput->state() == QAudio::ActiveState; + } else { + // no local buffer or audio + return false; } - - return false; } void AudioClient::outputFormatChanged() { @@ -1205,18 +1275,79 @@ float AudioClient::getAudioOutputMsecsUnplayed() const { return msecsAudioOutputUnplayed; } + +float AudioClient::azimuthForSource(const glm::vec3& relativePosition) { + // copied from AudioMixer, more or less + glm::quat inverseOrientation = glm::inverse(_orientationGetter()); + + // compute sample delay for the 2 ears to create phase panning + glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; + + // project the rotated source position vector onto x-y plane + rotatedSourcePosition.y = 0.0f; + + static const float SOURCE_DISTANCE_THRESHOLD = 1e-30f; + + if (glm::length2(rotatedSourcePosition) > SOURCE_DISTANCE_THRESHOLD) { + + // produce an oriented angle about the y-axis + return glm::orientedAngle(glm::vec3(0.0f, 0.0f, -1.0f), glm::normalize(rotatedSourcePosition), glm::vec3(0.0f, -1.0f, 0.0f)); + } else { + + // no azimuth if they are in same spot + return 0.0f; + } +} + +float AudioClient::gainForSource(const glm::vec3& relativePosition, float volume) { + // TODO: put these in a place where we can share with AudioMixer! + const float DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE = 0.18f; + const float ATTENUATION_BEGINS_AT_DISTANCE = 1.0f; + + + //qDebug() << "initial gain is " << volume; + + // I'm assuming that the AudioMixer's getting of the stream's attenuation + // factor is basically same as getting volume + float gain = volume; + float distanceBetween = glm::length(relativePosition); + if (distanceBetween < EPSILON ) { + distanceBetween = EPSILON; + } + + // audio mixer has notion of zones. Unsure how to map that across here... + + // attenuate based on distance now + if (distanceBetween >= ATTENUATION_BEGINS_AT_DISTANCE) { + float distanceCoefficient = 1.0f - (logf(distanceBetween/ATTENUATION_BEGINS_AT_DISTANCE) / logf(2.0f) + * DEFAULT_ATTENUATION_PER_DOUBLING_IN_DISTANCE); + if (distanceCoefficient < 0.0f) { + distanceCoefficient = 0.0f; + } + + gain *= distanceCoefficient; + } + + //qDebug() << "calculated gain as " << gain; + + return gain; +} + qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { auto samplesRequested = maxSize / sizeof(int16_t); int samplesPopped; int bytesWritten; - + if ((samplesPopped = _receivedAudioStream.popSamples((int)samplesRequested, false)) > 0) { AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); lastPopOutput.readSamples((int16_t*)data, samplesPopped); bytesWritten = samplesPopped * sizeof(int16_t); } else { + // nothing on network, don't grab anything from injectors, and just + // return 0s memset(data, 0, maxSize); bytesWritten = maxSize; + } int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index bd0afe453d..df7b10ab04 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -37,13 +37,17 @@ #include #include #include +#include +#include +#include +#include +#include +#include #include #include "AudioIOStats.h" #include "AudioNoiseGate.h" -#include "AudioSRC.h" -#include "AudioReverb.h" #ifdef _WIN32 #pragma warning( push ) @@ -88,7 +92,6 @@ public: void stop() { close(); } qint64 readData(char * data, qint64 maxSize); qint64 writeData(const char * data, qint64 maxSize) { return 0; } - int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; } private: MixedProcessedAudioStream& _receivedAudioStream; @@ -128,6 +131,8 @@ public: void setPositionGetter(AudioPositionGetter positionGetter) { _positionGetter = positionGetter; } void setOrientationGetter(AudioOrientationGetter orientationGetter) { _orientationGetter = orientationGetter; } + + QVector& getActiveLocalAudioInjectors() { return _activeLocalAudioInjectors; } static const float CALLBACK_ACCELERATOR_RATIO; @@ -210,6 +215,9 @@ protected: private: void outputFormatChanged(); + void mixLocalAudioInjectors(int16_t* inputBuffer); + float azimuthForSource(const glm::vec3& relativePosition); + float gainForSource(const glm::vec3& relativePosition, float volume); QByteArray firstInputFrame; QAudioInput* _audioInput; @@ -266,6 +274,11 @@ private: AudioSRC* _inputToNetworkResampler; AudioSRC* _networkToOutputResampler; + // for local hrtf-ing + float _hrtfBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; + int16_t _scratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; + AudioLimiter _audioLimiter; + // Adds Reverb void configureReverb(); void updateReverbOptions(); @@ -296,6 +309,8 @@ private: void checkDevices(); bool _hasReceivedFirstPacket = false; + + QVector _activeLocalAudioInjectors; CodecPluginPointer _codec; QString _selectedCodecName; diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index fee33dcb92..08143b8491 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -26,6 +26,8 @@ #include "AudioInjector.h" +int audioInjectorPtrMetaTypeId = qRegisterMetaType(); + AudioInjector::AudioInjector(QObject* parent) : QObject(parent) { @@ -41,11 +43,20 @@ AudioInjector::AudioInjector(const Sound& sound, const AudioInjectorOptions& inj AudioInjector::AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions) : _audioData(audioData), - _options(injectorOptions) + _options(injectorOptions) { } +void AudioInjector::setOptions(const AudioInjectorOptions& options) { + // since options.stereo is computed from the audio stream, + // we need to copy it from existing options just in case. + bool currentlyStereo = _options.stereo; + _options = options; + _options.stereo = currentlyStereo; +} + + void AudioInjector::finish() { bool shouldDelete = (_state == State::NotFinishedWithPendingDelete); _state = State::Finished; @@ -115,11 +126,11 @@ void AudioInjector::restart() { _hasSetup = false; _shouldStop = false; _state = State::NotFinished; - + // call inject audio to start injection over again setupInjection(); - // if we're a local injector call inject locally to start injecting again + // if we're a local injector, just inject again if (_options.localOnly) { injectLocally(); } else { @@ -145,7 +156,8 @@ bool AudioInjector::injectLocally() { // give our current send position to the local buffer _localBuffer->setCurrentOffset(_currentSendOffset); - success = _localAudioInterface->outputLocalInjector(_options.stereo, this); + // call this function on the AudioClient's thread + success = QMetaObject::invokeMethod(_localAudioInterface, "outputLocalInjector", Q_ARG(bool, _options.stereo), Q_ARG(AudioInjector*, this)); if (!success) { qCDebug(audio) << "AudioInjector::injectLocally could not output locally via _localAudioInterface"; diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index c90256429d..1af733ace6 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -26,6 +26,7 @@ #include "AudioInjectorLocalBuffer.h" #include "AudioInjectorOptions.h" +#include "AudioHRTF.h" #include "Sound.h" class AbstractAudioInterface; @@ -54,8 +55,12 @@ public: void setCurrentSendOffset(int currentSendOffset) { _currentSendOffset = currentSendOffset; } AudioInjectorLocalBuffer* getLocalBuffer() const { return _localBuffer; } + AudioHRTF& getLocalHRTF() { return _localHRTF; } + bool isLocalOnly() const { return _options.localOnly; } - + float getVolume() const { return _options.volume; } + glm::vec3 getPosition() const { return _options.position; } + bool isStereo() const { return _options.stereo; } void setLocalAudioInterface(AbstractAudioInterface* localAudioInterface) { _localAudioInterface = localAudioInterface; } static AudioInjector* playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options, AbstractAudioInterface* localInterface); @@ -70,18 +75,16 @@ public slots: void stopAndDeleteLater(); const AudioInjectorOptions& getOptions() const { return _options; } - void setOptions(const AudioInjectorOptions& options) { _options = options; } + void setOptions(const AudioInjectorOptions& options); float getLoudness() const { return _loudness; } bool isPlaying() const { return _state == State::NotFinished || _state == State::NotFinishedWithPendingDelete; } + void finish(); signals: void finished(); void restarting(); -private slots: - void finish(); - private: void setupInjection(); int64_t injectNextFrame(); @@ -103,8 +106,9 @@ private: std::unique_ptr _frameTimer { nullptr }; quint16 _outgoingSequenceNumber { 0 }; + // when the injector is local, we need this + AudioHRTF _localHRTF; friend class AudioInjectorManager; }; - #endif // hifi_AudioInjector_h diff --git a/libraries/avatars/src/AvatarHashMap.cpp b/libraries/avatars/src/AvatarHashMap.cpp index d153cfd977..48b701d142 100644 --- a/libraries/avatars/src/AvatarHashMap.cpp +++ b/libraries/avatars/src/AvatarHashMap.cpp @@ -19,7 +19,9 @@ #include "AvatarHashMap.h" AvatarHashMap::AvatarHashMap() { - connect(DependencyManager::get().data(), &NodeList::uuidChanged, this, &AvatarHashMap::sessionUUIDChanged); + auto nodeList = DependencyManager::get(); + + connect(nodeList.data(), &NodeList::uuidChanged, this, &AvatarHashMap::sessionUUIDChanged); } QVector AvatarHashMap::getAvatarIdentifiers() { @@ -105,7 +107,10 @@ void AvatarHashMap::processAvatarDataPacket(QSharedPointer mess QByteArray byteArray = message->readWithoutCopy(message->getBytesLeftToRead()); - if (sessionUUID != _lastOwnerSessionUUID) { + // make sure this isn't our own avatar data or for a previously ignored node + auto nodeList = DependencyManager::get(); + + if (sessionUUID != _lastOwnerSessionUUID && !nodeList->isIgnoringNode(sessionUUID)) { auto avatar = newOrExistingAvatar(sessionUUID, sendingNode); // have the matching (or new) avatar parse the data from the packet @@ -124,9 +129,13 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer AvatarData::Identity identity; AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity); - // mesh URL for a UUID, find avatar in our list - auto avatar = newOrExistingAvatar(identity.uuid, sendingNode); - avatar->processAvatarIdentity(identity); + // make sure this isn't for an ignored avatar + auto nodeList = DependencyManager::get(); + if (!nodeList->isIgnoringNode(identity.uuid)) { + // mesh URL for a UUID, find avatar in our list + auto avatar = newOrExistingAvatar(identity.uuid, sendingNode); + avatar->processAvatarIdentity(identity); + } } void AvatarHashMap::processKillAvatar(QSharedPointer message, SharedNodePointer sendingNode) { diff --git a/libraries/avatars/src/AvatarHashMap.h b/libraries/avatars/src/AvatarHashMap.h index 9d3ebb60f5..a59cc4fa96 100644 --- a/libraries/avatars/src/AvatarHashMap.h +++ b/libraries/avatars/src/AvatarHashMap.h @@ -19,12 +19,14 @@ #include #include +#include + #include #include #include #include "AvatarData.h" -#include + class AvatarHashMap : public QObject, public Dependency { Q_OBJECT diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index 89da59a8f5..7611407de1 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -429,12 +429,8 @@ qint64 LimitedNodeList::sendPacket(std::unique_ptr packet, const Node& } int LimitedNodeList::updateNodeWithDataFromPacket(QSharedPointer message, SharedNodePointer sendingNode) { - QMutexLocker locker(&sendingNode->getMutex()); - NodeData* linkedData = sendingNode->getLinkedData(); - if (!linkedData && linkedDataCreateCallback) { - linkedDataCreateCallback(sendingNode.data()); - } + NodeData* linkedData = getOrCreateLinkedData(sendingNode); if (linkedData) { QMutexLocker linkedDataLocker(&linkedData->getMutex()); @@ -444,6 +440,17 @@ int LimitedNodeList::updateNodeWithDataFromPacket(QSharedPointergetMutex()); + + NodeData* linkedData = node->getLinkedData(); + if (!linkedData && linkedDataCreateCallback) { + linkedDataCreateCallback(node.data()); + } + + return node->getLinkedData(); +} + SharedNodePointer LimitedNodeList::nodeWithUUID(const QUuid& nodeUUID) { QReadLocker readLocker(&_nodeMutex); diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index a90f97da3e..789a7bbcda 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -149,6 +149,7 @@ public: void processKillNode(ReceivedMessage& message); int updateNodeWithDataFromPacket(QSharedPointer packet, SharedNodePointer matchingNode); + NodeData* getOrCreateLinkedData(SharedNodePointer node); unsigned int broadcastToNodes(std::unique_ptr packet, const NodeSet& destinationNodeTypes); SharedNodePointer soloNodeOfType(NodeType_t nodeType); diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp index 7201b2fd9a..406498b025 100644 --- a/libraries/networking/src/Node.cpp +++ b/libraries/networking/src/Node.cpp @@ -12,15 +12,17 @@ #include #include -#include - -#include "Node.h" -#include "SharedUtil.h" -#include "NodePermissions.h" - #include #include +#include + +#include "NetworkLogging.h" +#include "NodePermissions.h" +#include "SharedUtil.h" + +#include "Node.h" + const QString UNKNOWN_NodeType_t_NAME = "Unknown"; int NodePtrMetaTypeId = qRegisterMetaType("Node*"); @@ -78,6 +80,27 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) { _clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile(); } +void Node::parseIgnoreRequestMessage(QSharedPointer message) { + while (message->getBytesLeftToRead()) { + // parse out the UUID being ignored from the packet + QUuid ignoredUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + + addIgnoredNode(ignoredUUID); + } +} + +void Node::addIgnoredNode(const QUuid& otherNodeID) { + if (!otherNodeID.isNull() && otherNodeID != _uuid) { + qCDebug(networking) << "Adding" << uuidStringWithoutCurlyBraces(otherNodeID) << "to ignore set for" + << uuidStringWithoutCurlyBraces(_uuid); + + // add the session UUID to the set of ignored ones for this listening node + _ignoredNodeIDSet.insert(otherNodeID); + } else { + qCWarning(networking) << "Node::addIgnoredNode called with null ID or ID of ignoring node."; + } +} + QDataStream& operator<<(QDataStream& out, const Node& node) { out << node._type; out << node._uuid; diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 08eb9829f8..270814c35c 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -21,6 +21,10 @@ #include #include +#include + +#include + #include "HifiSockAddr.h" #include "NetworkPeer.h" #include "NodeData.h" @@ -66,6 +70,10 @@ public: bool getCanRezTmp() const { return _permissions.can(NodePermissions::Permission::canRezTemporaryEntities); } bool getCanWriteToAssetServer() const { return _permissions.can(NodePermissions::Permission::canWriteToAssetServer); } + void parseIgnoreRequestMessage(QSharedPointer message); + void addIgnoredNode(const QUuid& otherNodeID); + bool isIgnoringNodeWithID(const QUuid& nodeID) const { return _ignoredNodeIDSet.find(nodeID) != _ignoredNodeIDSet.cend(); } + friend QDataStream& operator<<(QDataStream& out, const Node& node); friend QDataStream& operator>>(QDataStream& in, Node& node); @@ -84,6 +92,7 @@ private: QMutex _mutex; MovingPercentile _clockSkewMovingPercentile; NodePermissions _permissions; + tbb::concurrent_unordered_set _ignoredNodeIDSet; }; Q_DECLARE_METATYPE(Node*) diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index d3fc93b991..a73537aad0 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -93,6 +93,9 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned // anytime we get a new node we will want to attempt to punch to it connect(this, &LimitedNodeList::nodeAdded, this, &NodeList::startNodeHolePunch); + + // anytime we get a new node we may need to re-send our set of ignored node IDs to it + connect(this, &LimitedNodeList::nodeActivated, this, &NodeList::maybeSendIgnoreSetToNode); // setup our timer to send keepalive pings (it's started and stopped on domain connect/disconnect) _keepAlivePingTimer.setInterval(KEEPALIVE_PING_INTERVAL_MS); @@ -215,6 +218,11 @@ void NodeList::reset() { _numNoReplyDomainCheckIns = 0; + // lock and clear our set of ignored IDs + _ignoredSetLock.lockForWrite(); + _ignoredNodeIDs.clear(); + _ignoredSetLock.unlock(); + // refresh the owner UUID to the NULL UUID setSessionUUID(QUuid()); @@ -692,3 +700,67 @@ void NodeList::sendKeepAlivePings() { sendPacket(constructPingPacket(), *node); }); } + +void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) { + // enumerate the nodes to send a reliable ignore packet to each that can leverage it + + if (!nodeID.isNull() && _sessionUUID != nodeID) { + eachMatchingNode([&nodeID](const SharedNodePointer& node)->bool { + if (node->getType() == NodeType::AudioMixer || node->getType() == NodeType::AvatarMixer) { + return true; + } else { + return false; + } + }, [&nodeID, this](const SharedNodePointer& destinationNode) { + // create a reliable NLPacket with space for the ignore UUID + auto ignorePacket = NLPacket::create(PacketType::NodeIgnoreRequest, NUM_BYTES_RFC4122_UUID, true); + + // write the node ID to the packet + ignorePacket->write(nodeID.toRfc4122()); + + qDebug() << "Sending packet to ignore node" << uuidStringWithoutCurlyBraces(nodeID); + + // send off this ignore packet reliably to the matching node + sendPacket(std::move(ignorePacket), *destinationNode); + }); + + QReadLocker setLocker { &_ignoredSetLock }; + + // add this nodeID to our set of ignored IDs + _ignoredNodeIDs.insert(nodeID); + + emit ignoredNode(nodeID); + + } else { + qWarning() << "UsersScriptingInterface::ignore called with an invalid ID or an ID which matches the current session ID."; + } +} + +bool NodeList::isIgnoringNode(const QUuid& nodeID) const { + QReadLocker setLocker { &_ignoredSetLock }; + return _ignoredNodeIDs.find(nodeID) != _ignoredNodeIDs.cend(); +} + +void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { + if (newNode->getType() == NodeType::AudioMixer || newNode->getType() == NodeType::AvatarMixer) { + // this is a mixer that we just added - it's unlikely it knows who we were previously ignoring in this session, + // so send that list along now (assuming it isn't empty) + + QReadLocker setLocker { &_ignoredSetLock }; + + if (_ignoredNodeIDs.size() > 0) { + // setup a packet list so we can send the stream of ignore IDs + auto ignorePacketList = NLPacketList::create(PacketType::NodeIgnoreRequest, QByteArray(), true); + + // enumerate the ignored IDs and write them to the packet list + auto it = _ignoredNodeIDs.cbegin(); + while (it != _ignoredNodeIDs.end()) { + ignorePacketList->write(it->toRfc4122()); + ++it; + } + + // send this NLPacketList to the new node + sendPacketList(std::move(ignorePacketList), *newNode); + } + } +} diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index 3fbc86c736..ff994ce612 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -20,6 +20,8 @@ #include // not on windows, not needed for mac or windows #endif +#include + #include #include #include @@ -68,6 +70,9 @@ public: void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; } + void ignoreNodeBySessionID(const QUuid& nodeID); + bool isIgnoringNode(const QUuid& nodeID) const; + public slots: void reset(); void sendDomainServerCheckIn(); @@ -92,6 +97,8 @@ public slots: signals: void limitOfSilentDomainCheckInsReached(); void receivedDomainServerList(); + void ignoredNode(const QUuid& nodeID); + private slots: void stopKeepalivePingTimer(); void sendPendingDSPathQuery(); @@ -103,6 +110,8 @@ private slots: void pingPunchForDomainServer(); void sendKeepAlivePings(); + + void maybeSendIgnoreSetToNode(SharedNodePointer node); private: NodeList() : LimitedNodeList(0, 0) { assert(false); } // Not implemented, needed for DependencyManager templates compile @@ -129,6 +138,9 @@ private: bool _isShuttingDown { false }; QTimer _keepAlivePingTimer; + mutable QReadWriteLock _ignoredSetLock; + tbb::concurrent_unordered_set _ignoredNodeIDs; + #if (PR_BUILD || DEV_BUILD) bool _shouldSendNewerVersion { false }; #endif diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index 6359ad0aff..fca006ae87 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -40,8 +40,6 @@ const QSet NON_SOURCED_PACKETS = QSet() << PacketType::ICEServerHeartbeatDenied << PacketType::AssignmentClientStatus << PacketType::StopNode << PacketType::DomainServerRemovedNode; -const QSet RELIABLE_PACKETS = QSet(); - PacketVersion versionForPacketType(PacketType packetType) { switch (packetType) { case PacketType::DomainList: @@ -62,6 +60,8 @@ PacketVersion versionForPacketType(PacketType packetType) { case PacketType::AssetUpload: // Removal of extension from Asset requests return 18; + case PacketType::NodeIgnoreRequest: + return 18; // Introduction of node ignore request (which replaced an unused packet tpye) case PacketType::DomainConnectionDenied: return static_cast(DomainConnectionDeniedVersion::IncludesReasonCode); diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 296ebb8e68..9581f3ca20 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -61,7 +61,7 @@ public: AssignmentClientStatus, NoisyMute, AvatarIdentity, - TYPE_UNUSED_1, + NodeIgnoreRequest, DomainConnectRequest, DomainServerRequireDTLS, NodeJsonStats, @@ -109,7 +109,6 @@ typedef char PacketVersion; extern const QSet NON_VERIFIED_PACKETS; extern const QSet NON_SOURCED_PACKETS; -extern const QSet RELIABLE_PACKETS; PacketVersion versionForPacketType(PacketType packetType); QByteArray protocolVersionsSignature(); /// returns a unqiue signature for all the current protocols diff --git a/libraries/networking/src/udt/PacketQueue.cpp b/libraries/networking/src/udt/PacketQueue.cpp index 4b8c0b187c..3684e5ba07 100644 --- a/libraries/networking/src/udt/PacketQueue.cpp +++ b/libraries/networking/src/udt/PacketQueue.cpp @@ -65,7 +65,9 @@ void PacketQueue::queuePacket(PacketPointer packet) { } void PacketQueue::queuePacketList(PacketListPointer packetList) { - packetList->preparePackets(getNextMessageNumber()); + if (packetList->isOrdered()) { + packetList->preparePackets(getNextMessageNumber()); + } LockGuard locker(_packetsLock); _channels.push_back(std::move(packetList->_packets)); diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp new file mode 100644 index 0000000000..ff7ccb0164 --- /dev/null +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -0,0 +1,19 @@ +// +// UsersScriptingInterface.cpp +// libraries/script-engine/src +// +// Created by Stephen Birarda on 2016-07-11. +// 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 "UsersScriptingInterface.h" + +#include + +void UsersScriptingInterface::ignore(const QUuid& nodeID) { + // ask the NodeList to ignore this user (based on the session ID of their node) + DependencyManager::get()->ignoreNodeBySessionID(nodeID); +} diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h new file mode 100644 index 0000000000..0dc62c088c --- /dev/null +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -0,0 +1,28 @@ +// +// UsersScriptingInterface.h +// libraries/script-engine/src +// +// Created by Stephen Birarda on 2016-07-11. +// 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 +// + +#pragma once + +#ifndef hifi_UsersScriptingInterface_h +#define hifi_UsersScriptingInterface_h + +#include + +class UsersScriptingInterface : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + +public slots: + void ignore(const QUuid& nodeID); +}; + + +#endif // hifi_UsersScriptingInterface_h diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 711e64f938..6880de99b5 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -17,6 +17,7 @@ Script.load("system/goto.js"); Script.load("system/hmd.js"); Script.load("system/examples.js"); Script.load("system/edit.js"); +Script.load("system/ignore.js"); Script.load("system/selectAudioDevice.js"); Script.load("system/notifications.js"); Script.load("system/controllers/handControllerGrab.js"); diff --git a/scripts/system/assets/images/ignore-target-01.svg b/scripts/system/assets/images/ignore-target-01.svg new file mode 100644 index 0000000000..98cee89ca1 --- /dev/null +++ b/scripts/system/assets/images/ignore-target-01.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/tools/ignore.svg b/scripts/system/assets/images/tools/ignore.svg new file mode 100644 index 0000000000..f315c5f249 --- /dev/null +++ b/scripts/system/assets/images/tools/ignore.svg @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/ignore.js b/scripts/system/ignore.js new file mode 100644 index 0000000000..39405a2e57 --- /dev/null +++ b/scripts/system/ignore.js @@ -0,0 +1,210 @@ +// +// ignore.js +// scripts/system/ +// +// Created by Stephen Birarda on 07/11/2016 +// 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 +// + +// grab the toolbar +var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); + +// setup the ignore button and add it to the toolbar +var button = toolbar.addButton({ + objectName: 'ignore', + imageURL: Script.resolvePath("assets/images/tools/ignore.svg"), + visible: true, + buttonState: 1, + alpha: 0.9 +}); + +var isShowingOverlays = false; +var ignoreOverlays = {}; + +function removeOverlays() { + // enumerate the overlays and remove them + var ignoreOverlayKeys = Object.keys(ignoreOverlays); + + for (i = 0; i < ignoreOverlayKeys.length; ++i) { + var avatarID = ignoreOverlayKeys[i]; + Overlays.deleteOverlay(ignoreOverlays[avatarID]); + } + + ignoreOverlays = {}; +} + +// handle clicks on the toolbar button +function buttonClicked(){ + if (isShowingOverlays) { + removeOverlays(); + isShowingOverlays = false; + } else { + isShowingOverlays = true; + } + + button.writeProperty('buttonState', isShowingOverlays ? 0 : 1); +} + +button.clicked.connect(buttonClicked); + +function updateOverlays() { + if (isShowingOverlays) { + + var identifiers = AvatarList.getAvatarIdentifiers(); + + for (i = 0; i < identifiers.length; ++i) { + var avatarID = identifiers[i]; + + if (avatarID === null) { + // this is our avatar, skip it + continue; + } + + // get the position for this avatar + var avatar = AvatarList.getAvatar(avatarID); + var avatarPosition = avatar && avatar.position; + + if (!avatarPosition) { + // we don't have a valid position for this avatar, skip it + continue; + } + + // setup a position for the overlay that is just above this avatar's head + var overlayPosition = avatar.getJointPosition("Head"); + overlayPosition.y += 0.45; + + if (avatarID in ignoreOverlays) { + // keep the overlay above the current position of this avatar + Overlays.editOverlay(ignoreOverlays[avatarID], { + position: overlayPosition + }); + } else { + // add the overlay above this avatar + var newOverlay = Overlays.addOverlay("image3d", { + url: Script.resolvePath("assets/images/ignore-target-01.svg"), + position: overlayPosition, + size: 0.4, + scale: 0.4, + color: { red: 255, green: 255, blue: 255}, + alpha: 1, + solid: true, + isFacingAvatar: true, + drawInFront: true + }); + + // push this overlay to our array of overlays + ignoreOverlays[avatarID] = newOverlay; + } + } + } +} + +Script.update.connect(updateOverlays); + +AvatarList.avatarRemovedEvent.connect(function(avatarID){ + if (isShowingOverlays) { + // we are currently showing overlays and an avatar just went away + + // first remove the rendered overlay + Overlays.deleteOverlay(ignoreOverlays[avatarID]); + + // delete the saved ID of the overlay from our ignored overlays object + delete ignoreOverlays[avatarID]; + } +}); + +function handleSelectedOverlay(clickedOverlay) { + // see this is one of our ignore overlays + + var ignoreOverlayKeys = Object.keys(ignoreOverlays) + for (i = 0; i < ignoreOverlayKeys.length; ++i) { + var avatarID = ignoreOverlayKeys[i]; + var ignoreOverlay = ignoreOverlays[avatarID]; + + if (clickedOverlay.overlayID == ignoreOverlay) { + // matched to an overlay, ask for the matching avatar to be ignored + Users.ignore(avatarID); + + // cleanup of the overlay is handled by the connection to avatarRemovedEvent + } + } +} + +Controller.mousePressEvent.connect(function(event){ + if (isShowingOverlays) { + // handle click events so we can detect when our overlays are clicked + + if (!event.isLeftButton) { + // if another mouse button than left is pressed ignore it + return false; + } + + // compute the pick ray from the event + var pickRay = Camera.computePickRay(event.x, event.y); + + // grab the clicked overlay for the given pick ray + var clickedOverlay = Overlays.findRayIntersection(pickRay); + if (clickedOverlay.intersects) { + handleSelectedOverlay(clickedOverlay); + } + } +}); + +// We get mouseMoveEvents from the handControllers, via handControllerPointer. +// But we dont' get mousePressEvents. +var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); + +var TRIGGER_GRAB_VALUE = 0.85; // From handControllerGrab/Pointer.js. Should refactor. +var TRIGGER_ON_VALUE = 0.4; +var TRIGGER_OFF_VALUE = 0.15; +var triggered = false; +var activeHand = Controller.Standard.RightHand; + +function controllerComputePickRay() { + var controllerPose = Controller.getPoseValue(activeHand); + if (controllerPose.valid && triggered) { + var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), + MyAvatar.position); + // This gets point direction right, but if you want general quaternion it would be more complicated: + var controllerDirection = Quat.getUp(Quat.multiply(MyAvatar.orientation, controllerPose.rotation)); + return { origin: controllerPosition, direction: controllerDirection }; + } +} + +function makeTriggerHandler(hand) { + return function (value) { + if (isShowingOverlays) { + if (!triggered && (value > TRIGGER_GRAB_VALUE)) { // should we smooth? + triggered = true; + if (activeHand !== hand) { + // No switching while the other is already triggered, so no need to release. + activeHand = (activeHand === Controller.Standard.RightHand) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; + } + + var pickRay = controllerComputePickRay(); + if (pickRay) { + var overlayIntersection = Overlays.findRayIntersection(pickRay); + if (overlayIntersection.intersects) { + handleSelectedOverlay(overlayIntersection); + } + } + } else if (triggered && (value < TRIGGER_OFF_VALUE)) { + triggered = false; + } + } + }; +} +triggerMapping.from(Controller.Standard.RT).peek().to(makeTriggerHandler(Controller.Standard.RightHand)); +triggerMapping.from(Controller.Standard.LT).peek().to(makeTriggerHandler(Controller.Standard.LeftHand)); + +triggerMapping.enable(); + +// cleanup the toolbar button and overlays when script is stopped +Script.scriptEnding.connect(function() { + toolbar.removeButton('ignore'); + removeOverlays(); + triggerMapping.disable(); +});