Merge pull request #9352 from zfox23/PerAvatarVolume

PAL: Per-Avatar Volume Slider; visual tweaks
This commit is contained in:
Zach Fox 2017-01-16 10:34:31 -08:00 committed by GitHub
commit d2c7342a62
11 changed files with 195 additions and 29 deletions

View file

@ -28,6 +28,7 @@
#include <StDev.h>
#include <UUID.h>
#include "AudioHelpers.h"
#include "AudioRingBuffer.h"
#include "AudioMixerClientData.h"
#include "AvatarAudioStream.h"
@ -68,7 +69,8 @@ AudioMixer::AudioMixer(ReceivedMessage& message) :
packetReceiver.registerListener(PacketType::KillAvatar, this, "handleKillAvatarPacket");
packetReceiver.registerListener(PacketType::NodeMuteRequest, this, "handleNodeMuteRequestPacket");
packetReceiver.registerListener(PacketType::RadiusIgnoreRequest, this, "handleRadiusIgnoreRequestPacket");
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
packetReceiver.registerListener(PacketType::RequestsDomainListData, this, "handleRequestsDomainListDataPacket");
packetReceiver.registerListener(PacketType::PerAvatarGainSet, this, "handlePerAvatarGainSetDataPacket");
connect(nodeList.data(), &NodeList::nodeKilled, this, &AudioMixer::handleNodeKilled);
}
@ -186,7 +188,8 @@ void AudioMixer::handleNodeKilled(SharedNodePointer killedNode) {
nodeList->eachNode([&killedNode](const SharedNodePointer& node) {
auto clientData = dynamic_cast<AudioMixerClientData*>(node->getLinkedData());
if (clientData) {
clientData->removeHRTFsForNode(killedNode->getUUID());
QUuid killedUUID = killedNode->getUUID();
clientData->removeHRTFsForNode(killedUUID);
}
});
}
@ -240,6 +243,20 @@ void AudioMixer::handleNodeIgnoreRequestPacket(QSharedPointer<ReceivedMessage> p
sendingNode->parseIgnoreRequestMessage(packet);
}
void AudioMixer::handlePerAvatarGainSetDataPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
auto clientData = dynamic_cast<AudioMixerClientData*>(sendingNode->getLinkedData());
if (clientData) {
QUuid listeningNodeUUID = sendingNode->getUUID();
// parse the UUID from the packet
QUuid audioSourceUUID = QUuid::fromRfc4122(packet->readWithoutCopy(NUM_BYTES_RFC4122_UUID));
uint8_t packedGain;
packet->readPrimitive(&packedGain);
float gain = unpackFloatGainFromByte(packedGain);
clientData->hrtfForStream(audioSourceUUID, QUuid()).setGainAdjustment(gain);
qDebug() << "Setting gain adjustment for hrtf[" << listeningNodeUUID << "][" << audioSourceUUID << "] to " << gain;
}
}
void AudioMixer::handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode) {
sendingNode->parseIgnoreRadiusRequestMessage(packet);
}

View file

@ -66,6 +66,7 @@ private slots:
void handleRadiusIgnoreRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleKillAvatarPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handleNodeMuteRequestPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void handlePerAvatarGainSetDataPacket(QSharedPointer<ReceivedMessage> packet, SharedNodePointer sendingNode);
void start();
void removeHRTFsForFinishedInjector(const QUuid& streamID);

View file

@ -10,35 +10,35 @@
//
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtGraphicalEffects 1.0
import "../styles-uit"
Row {
Item {
id: thisNameCard
// Spacing
spacing: 10
// Anchors
anchors.top: parent.top
anchors {
topMargin: (parent.height - contentHeight)/2
bottomMargin: (parent.height - contentHeight)/2
verticalCenter: parent.verticalCenter
leftMargin: 10
rightMargin: 10
}
// Properties
property int contentHeight: 50
property string uuid: ""
property string displayName: ""
property string userName: ""
property int displayTextHeight: 18
property int usernameTextHeight: 12
property real audioLevel: 0.0
property bool isMyCard: false
property bool selected: false
/* User image commented out for now - will probably be re-introduced later.
Column {
id: avatarImage
// Size
height: contentHeight
height: parent.height
width: height
Image {
id: userImage
@ -49,12 +49,12 @@ Row {
}
}
*/
Column {
Item {
id: textContainer
// Size
width: parent.width - /*avatarImage.width - */parent.anchors.leftMargin - parent.anchors.rightMargin - parent.spacing
height: contentHeight
width: parent.width - /*avatarImage.width - parent.spacing - */parent.anchors.leftMargin - parent.anchors.rightMargin
height: childrenRect.height
anchors.verticalCenter: parent.verticalCenter
// DisplayName Text
FiraSansSemiBold {
id: displayNameText
@ -63,6 +63,8 @@ Row {
elide: Text.ElideRight
// Size
width: parent.width
// Anchors
anchors.top: parent.top
// Text Size
size: thisNameCard.displayTextHeight
// Text Positioning
@ -80,6 +82,8 @@ Row {
visible: thisNameCard.displayName
// Size
width: parent.width
// Anchors
anchors.top: displayNameText.bottom
// Text Size
size: thisNameCard.usernameTextHeight
// Text Positioning
@ -90,25 +94,56 @@ Row {
// Spacer
Item {
id: spacer
height: 4
width: parent.width
// Anchors
anchors.top: userNameText.bottom
}
// VU Meter
Rectangle { // CHANGEME to the appropriate type!
Rectangle {
id: nameCardVUMeter
// Size
width: parent.width
width: ((gainSlider.value - gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue)) * parent.width
height: 8
// Anchors
anchors.top: spacer.bottom
// Style
radius: 4
color: "#c5c5c5"
// Rectangle for the zero-gain point on the VU meter
Rectangle {
id: vuMeterZeroGain
visible: gainSlider.visible
// Size
width: 4
height: 18
// Style
color: hifi.colors.darkGray
// Anchors
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: (-gainSlider.minimumValue)/(gainSlider.maximumValue - gainSlider.minimumValue) * gainSlider.width - 4
}
// Rectangle for the VU meter line
Rectangle {
id: vuMeterLine
width: gainSlider.width
visible: gainSlider.visible
// Style
color: vuMeterBase.color
radius: nameCardVUMeter.radius
height: nameCardVUMeter.height / 2
anchors.verticalCenter: nameCardVUMeter.verticalCenter
}
// Rectangle for the VU meter base
Rectangle {
id: vuMeterBase
// Anchors
anchors.fill: parent
// Style
color: "#c5c5c5"
color: parent.color
radius: parent.radius
}
// Rectangle for the VU meter audio level
@ -117,7 +152,7 @@ Row {
// Size
width: (thisNameCard.audioLevel) * parent.width
// Style
color: "#c5c5c5"
color: parent.color
radius: parent.radius
// Anchors
anchors.bottom: parent.bottom
@ -138,5 +173,66 @@ Row {
}
}
}
// Per-Avatar Gain Slider
Slider {
id: gainSlider
// Size
width: parent.width
height: 14
// Anchors
anchors.verticalCenter: nameCardVUMeter.verticalCenter
// Properties
visible: !isMyCard && selected
value: pal.gainSliderValueDB[uuid] ? pal.gainSliderValueDB[uuid] : 0.0
minimumValue: -60.0
maximumValue: 20.0
stepSize: 5
updateValueWhileDragging: true
onValueChanged: updateGainFromQML(uuid, value)
MouseArea {
anchors.fill: parent
onWheel: {
// Do nothing.
}
onDoubleClicked: {
gainSlider.value = 0.0
}
onPressed: {
// Pass through to Slider
mouse.accepted = false
}
onReleased: {
// Pass through to Slider
mouse.accepted = false
}
}
style: SliderStyle {
groove: Rectangle {
color: "#c5c5c5"
implicitWidth: gainSlider.width
implicitHeight: 4
radius: 2
opacity: 0
}
handle: Rectangle {
anchors.centerIn: parent
color: (control.pressed || control.hovered) ? "#00b4ef" : "#8F8F8F"
implicitWidth: 10
implicitHeight: 16
}
}
}
}
function updateGainFromQML(avatarUuid, sliderValue) {
if (pal.gainSliderValueDB[avatarUuid] !== sliderValue) {
pal.gainSliderValueDB[avatarUuid] = sliderValue;
var data = {
sessionId: avatarUuid,
gain: sliderValue
};
pal.sendToScript({method: 'updateGain', params: data});
}
}
}

View file

@ -24,7 +24,7 @@ Rectangle {
// Style
color: "#E3E3E3"
// Properties
property int myCardHeight: 70
property int myCardHeight: 90
property int rowHeight: 70
property int actionButtonWidth: 75
property int nameCardWidth: palContainer.width - actionButtonWidth*(iAmAdmin ? 4 : 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth
@ -32,6 +32,9 @@ Rectangle {
property var ignored: ({}); // Keep a local list of ignored avatars & their data. Necessary because HashMap is slow to respond after ignoring.
property var userModelData: [] // This simple list is essentially a mirror of the userModel listModel without all the extra complexities.
property bool iAmAdmin: false
// Keep a local list of per-avatar gainSliderValueDBs. Far faster than fetching this data from the server.
// NOTE: if another script modifies the per-avatar gain, this value won't be accurate!
property var gainSliderValueDB: ({});
// This is the container for the PAL
Rectangle {
@ -51,7 +54,7 @@ Rectangle {
id: myInfo
// Size
width: palContainer.width
height: myCardHeight + 20
height: myCardHeight
// Style
color: pal.color
// Anchors
@ -65,6 +68,7 @@ Rectangle {
displayName: myData.displayName
userName: myData.userName
audioLevel: myData.audioLevel
isMyCard: true
// Size
width: nameCardWidth
height: parent.height
@ -206,6 +210,8 @@ Rectangle {
userName: model && model.userName
audioLevel: model && model.audioLevel
visible: !isCheckBox && !isButton
uuid: model && model.sessionId
selected: styleData.selected
// Size
width: nameCardWidth
height: parent.height
@ -492,8 +498,9 @@ Rectangle {
}
}
break;
case 'clearIgnored':
case 'clearLocalQMLData':
ignored = {};
gainSliderValueDB = {};
break;
default:
console.log('Unrecognized message:', JSON.stringify(message));

View file

@ -45,7 +45,7 @@ public:
void renderSilent(int16_t* input, float* output, int index, float azimuth, float distance, float gain, int numFrames);
//
// HRTF local gain adjustment
// HRTF local gain adjustment in amplitude (1.0 == unity)
//
void setGainAdjustment(float gain) { _gainAdjust = HRTF_GAIN * gain; };

View file

@ -26,6 +26,7 @@
#include "AccountManager.h"
#include "AddressManager.h"
#include "Assignment.h"
#include "AudioHelpers.h"
#include "HifiSockAddr.h"
#include "FingerprintUtils.h"
@ -951,6 +952,30 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) {
}
}
void NodeList::setAvatarGain(const QUuid& nodeID, float gain) {
// cannot set gain of yourself or nobody
if (!nodeID.isNull() && _sessionUUID != nodeID) {
auto audioMixer = soloNodeOfType(NodeType::AudioMixer);
if (audioMixer) {
// setup the packet
auto setAvatarGainPacket = NLPacket::create(PacketType::PerAvatarGainSet, NUM_BYTES_RFC4122_UUID + sizeof(float), true);
// write the node ID to the packet
setAvatarGainPacket->write(nodeID.toRfc4122());
// We need to convert the gain in dB (from the script) to an amplitude before packing it.
setAvatarGainPacket->writePrimitive(packFloatGainToByte(fastExp2f(gain / 6.0206f)));
qCDebug(networking) << "Sending Set Avatar Gain packet UUID: " << uuidStringWithoutCurlyBraces(nodeID) << "Gain:" << gain;
sendPacket(std::move(setAvatarGainPacket), *audioMixer);
} else {
qWarning() << "Couldn't find audio mixer to send set gain request";
}
} else {
qWarning() << "NodeList::setAvatarGain called with an invalid ID or an ID which matches the current session ID:" << nodeID;
}
}
void NodeList::kickNodeBySessionID(const QUuid& nodeID) {
// send a request to domain-server to kick the node with the given session ID
// the domain-server will handle the persistence of the kick (via username or IP)

View file

@ -82,6 +82,7 @@ public:
bool isIgnoringNode(const QUuid& nodeID) const;
void personalMuteNodeBySessionID(const QUuid& nodeID, bool muteEnabled);
bool isPersonalMutingNode(const QUuid& nodeID) const;
void setAvatarGain(const QUuid& nodeID, float gain);
void kickNodeBySessionID(const QUuid& nodeID);
void muteNodeBySessionID(const QUuid& nodeID);

View file

@ -106,7 +106,8 @@ public:
ViewFrustum,
RequestsDomainListData,
ExitingSpaceBubble,
LAST_PACKET_TYPE = ExitingSpaceBubble
PerAvatarGainSet,
LAST_PACKET_TYPE = PerAvatarGainSet
};
};

View file

@ -42,6 +42,11 @@ bool UsersScriptingInterface::getPersonalMuteStatus(const QUuid& nodeID) {
return DependencyManager::get<NodeList>()->isPersonalMutingNode(nodeID);
}
void UsersScriptingInterface::setAvatarGain(const QUuid& nodeID, float gain) {
// ask the NodeList to set the gain of the specified avatar
DependencyManager::get<NodeList>()->setAvatarGain(nodeID, gain);
}
void UsersScriptingInterface::kick(const QUuid& nodeID) {
// ask the NodeList to kick the user with the given session ID
DependencyManager::get<NodeList>()->kickNodeBySessionID(nodeID);

View file

@ -61,6 +61,15 @@ public slots:
*/
bool getPersonalMuteStatus(const QUuid& nodeID);
/**jsdoc
* Sets an avatar's gain for you and you only.
* Units are Decibels (dB)
* @function Users.setAvatarGain
* @param {nodeID} nodeID The node or session ID of the user whose gain you want to modify.
* @param {float} gain The gain of the avatar you'd like to set. Units are dB.
*/
void setAvatarGain(const QUuid& nodeID, float gain);
/**jsdoc
* Kick another user.
* @function Users.kick

View file

@ -233,6 +233,10 @@ pal.fromQml.connect(function (message) { // messages are {method, params}, like
removeOverlays();
populateUserList();
break;
case 'updateGain':
data = message.params;
Users.setAvatarGain(data['sessionId'], data['gain']);
break;
default:
print('Unrecognized message from Pal.qml:', JSON.stringify(message));
}
@ -582,14 +586,14 @@ button.clicked.connect(onClicked);
pal.visibleChanged.connect(onVisibleChanged);
pal.closed.connect(off);
Users.usernameFromIDReply.connect(usernameFromIDReply);
function clearIgnoredInQMLAndClosePAL() {
pal.sendToQml({ method: 'clearIgnored' });
function clearLocalQMLDataAndClosePAL() {
pal.sendToQml({ method: 'clearLocalQMLData' });
if (pal.visible) {
onClicked(); // Close the PAL
}
}
Window.domainChanged.connect(clearIgnoredInQMLAndClosePAL);
Window.domainConnectionRefused.connect(clearIgnoredInQMLAndClosePAL);
Window.domainChanged.connect(clearLocalQMLDataAndClosePAL);
Window.domainConnectionRefused.connect(clearLocalQMLDataAndClosePAL);
//
// Cleanup.
@ -600,8 +604,8 @@ Script.scriptEnding.connect(function () {
pal.visibleChanged.disconnect(onVisibleChanged);
pal.closed.disconnect(off);
Users.usernameFromIDReply.disconnect(usernameFromIDReply);
Window.domainChanged.disconnect(clearIgnoredInQMLAndClosePAL);
Window.domainConnectionRefused.disconnect(clearIgnoredInQMLAndClosePAL);
Window.domainChanged.disconnect(clearLocalQMLDataAndClosePAL);
Window.domainConnectionRefused.disconnect(clearLocalQMLDataAndClosePAL);
Messages.unsubscribe(CHANNEL);
Messages.messageReceived.disconnect(receiveMessage);
off();