diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf old mode 100644 new mode 100755 index c139a196d0..138d7f3dda Binary files a/interface/resources/fonts/hifi-glyphs.ttf and b/interface/resources/fonts/hifi-glyphs.ttf differ diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 846f1bec3c..020a85b46d 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -31,6 +31,7 @@ Item { property real displayNameTextPixelSize: 18 property int usernameTextHeight: 12 property real audioLevel: 0.0 + property real avgAudioLevel: 0.0 property bool isMyCard: false property bool selected: false property bool isAdmin: false @@ -55,7 +56,7 @@ Item { id: textContainer // Size width: parent.width - /*avatarImage.width - parent.spacing - */parent.anchors.leftMargin - parent.anchors.rightMargin - height: childrenRect.height + height: selected || isMyCard ? childrenRect.height : childrenRect.height - 15 anchors.verticalCenter: parent.verticalCenter // DisplayName field for my card @@ -273,6 +274,7 @@ Item { // Style radius: 4 color: "#c5c5c5" + visible: isMyCard || selected // Rectangle for the zero-gain point on the VU meter Rectangle { id: vuMeterZeroGain @@ -303,6 +305,7 @@ Item { id: vuMeterBase // Anchors anchors.fill: parent + visible: isMyCard || selected // Style color: parent.color radius: parent.radius @@ -310,6 +313,7 @@ Item { // Rectangle for the VU meter audio level Rectangle { id: vuMeterLevel + visible: isMyCard || selected // Size width: (thisNameCard.audioLevel) * parent.width // Style diff --git a/interface/resources/qml/hifi/Pal.qml b/interface/resources/qml/hifi/Pal.qml index 7ff4e8a4b1..3bad7ee647 100644 --- a/interface/resources/qml/hifi/Pal.qml +++ b/interface/resources/qml/hifi/Pal.qml @@ -13,6 +13,7 @@ import QtQuick 2.5 import QtQuick.Controls 1.4 +import QtGraphicalEffects 1.0 import Qt.labs.settings 1.0 import "../styles-uit" import "../controls-uit" as HifiControls @@ -33,7 +34,7 @@ Rectangle { property int actionButtonAllowance: actionButtonWidth * 2 property int minNameCardWidth: palContainer.width - (actionButtonAllowance * 2) - 4 - hifi.dimensions.scrollbarBackgroundWidth property int nameCardWidth: minNameCardWidth + (iAmAdmin ? 0 : actionButtonAllowance) - property var myData: ({displayName: "", userName: "", audioLevel: 0.0, admin: true}) // valid dummy until set + property var myData: ({displayName: "", userName: "", audioLevel: 0.0, avgAudioLevel: 0.0, admin: true}) // valid dummy until set 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 @@ -57,6 +58,8 @@ Rectangle { category: "pal" property bool filtered: false property int nearDistance: 30 + property int sortIndicatorColumn: 1 + property int sortIndicatorOrder: Qt.AscendingOrder } function refreshWithFilter() { // We should just be able to set settings.filtered to filter.checked, but see #3249, so send to .js for saving. @@ -96,6 +99,7 @@ Rectangle { displayName: myData.displayName userName: myData.userName audioLevel: myData.audioLevel + avgAudioLevel: myData.avgAudioLevel isMyCard: true // Size width: minNameCardWidth @@ -190,8 +194,24 @@ Rectangle { centerHeaderText: true sortIndicatorVisible: true headerVisible: true - onSortIndicatorColumnChanged: sortModel() - onSortIndicatorOrderChanged: sortModel() + sortIndicatorColumn: settings.sortIndicatorColumn + sortIndicatorOrder: settings.sortIndicatorOrder + onSortIndicatorColumnChanged: { + settings.sortIndicatorColumn = sortIndicatorColumn + sortModel() + } + onSortIndicatorOrderChanged: { + settings.sortIndicatorOrder = sortIndicatorOrder + sortModel() + } + + TableViewColumn { + role: "avgAudioLevel" + title: "LOUD" + width: actionButtonWidth + movable: false + resizable: false + } TableViewColumn { id: displayNameHeader @@ -201,13 +221,6 @@ Rectangle { movable: false resizable: false } - TableViewColumn { - role: "personalMute" - title: "MUTE" - width: actionButtonWidth - movable: false - resizable: false - } TableViewColumn { role: "ignore" title: "IGNORE" @@ -238,7 +251,7 @@ Rectangle { // This Rectangle refers to each Row in the table. rowDelegate: Rectangle { // The only way I know to specify a row height. // Size - height: rowHeight + height: styleData.selected ? rowHeight : rowHeight - 15 color: styleData.selected ? hifi.colors.orangeHighlight : styleData.alternate ? hifi.colors.tableRowLightEven : hifi.colors.tableRowLightOdd @@ -249,6 +262,8 @@ Rectangle { id: itemCell property bool isCheckBox: styleData.role === "personalMute" || styleData.role === "ignore" property bool isButton: styleData.role === "mute" || styleData.role === "kick" + property bool isAvgAudio: styleData.role === "avgAudioLevel" + // This NameCard refers to the cell that contains an avatar's // DisplayName and UserName NameCard { @@ -257,7 +272,8 @@ Rectangle { displayName: styleData.value userName: model ? model.userName : "" audioLevel: model ? model.audioLevel : 0.0 - visible: !isCheckBox && !isButton + avgAudioLevel: model ? model.avgAudioLevel : 0.0 + visible: !isCheckBox && !isButton && !isAvgAudio uuid: model ? model.sessionId : "" selected: styleData.selected isAdmin: model && model.admin @@ -267,6 +283,30 @@ Rectangle { // Anchors anchors.left: parent.left } + HifiControls.GlyphButton { + function getGlyph() { + var fileName = "vol_"; + if (model["personalMute"]) { + fileName += "x_"; + } + fileName += (4.0*(model ? model.avgAudioLevel : 0.0)).toFixed(0); + return hifi.glyphs[fileName]; + } + id: avgAudioVolume + visible: isAvgAudio + glyph: getGlyph() + width: 32 + size: height + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + var newValue = !model["personalMute"]; + userModel.setProperty(model.userIndex, "personalMute", newValue) + userModelData[model.userIndex]["personalMute"] = newValue // Defensive programming + Users["personalMute"](model.sessionId, newValue) + UserActivityLogger["palAction"](newValue ? "personalMute" : "un-personalMute", model.sessionId) + } + } // This CheckBox belongs in the columns that contain the stateful action buttons ("Mute" & "Ignore" for now) // KNOWN BUG with the Checkboxes: When clicking in the center of the sorting header, the checkbox @@ -311,7 +351,7 @@ Rectangle { visible: isButton anchors.centerIn: parent width: 32 - height: 24 + height: 32 onClicked: { Users[styleData.role](model.sessionId) UserActivityLogger["palAction"](styleData.role, model.sessionId) @@ -363,7 +403,7 @@ Rectangle { anchors.left: table.left anchors.top: table.top anchors.topMargin: 1 - anchors.leftMargin: nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6 + anchors.leftMargin: actionButtonWidth + nameCardWidth/2 + displayNameHeaderMetrics.width/2 + 6 RalewayRegular { id: helpText text: "[?]" @@ -537,16 +577,21 @@ Rectangle { break; case 'updateAudioLevel': for (var userId in message.params) { - var audioLevel = message.params[userId]; + var audioLevel = message.params[userId][0]; + var avgAudioLevel = message.params[userId][1]; // If the userId is 0, we're updating "myData". if (userId == 0) { myData.audioLevel = audioLevel; myCard.audioLevel = audioLevel; // Defensive programming + myData.avgAudioLevel = avgAudioLevel; + myCard.avgAudioLevel = avgAudioLevel; } else { var userIndex = findSessionIndex(userId); if (userIndex != -1) { userModel.setProperty(userIndex, "audioLevel", audioLevel); userModelData[userIndex].audioLevel = audioLevel; // Defensive programming + userModel.setProperty(userIndex, "avgAudioLevel", avgAudioLevel); + userModelData[userIndex].avgAudioLevel = avgAudioLevel; } } } diff --git a/interface/resources/qml/styles-uit/HifiConstants.qml b/interface/resources/qml/styles-uit/HifiConstants.qml index e261e2198f..031e80283e 100644 --- a/interface/resources/qml/styles-uit/HifiConstants.qml +++ b/interface/resources/qml/styles-uit/HifiConstants.qml @@ -318,5 +318,15 @@ Item { readonly property string deg: "\\" readonly property string px: "|" readonly property string editPencil: "\ue00d" + readonly property string vol_0: "\ue00e" + readonly property string vol_1: "\ue00f" + readonly property string vol_2: "\ue010" + readonly property string vol_3: "\ue011" + readonly property string vol_4: "\ue012" + readonly property string vol_x_0: "\ue013" + readonly property string vol_x_1: "\ue014" + readonly property string vol_x_2: "\ue015" + readonly property string vol_x_3: "\ue016" + readonly property string vol_x_4: "\ue017" } } diff --git a/scripts/system/pal.js b/scripts/system/pal.js index 106f226a33..70b2739c96 100644 --- a/scripts/system/pal.js +++ b/scripts/system/pal.js @@ -206,6 +206,17 @@ HighlightedEntity.updateOverlays = function updateHighlightedEntities() { }); }; +/* this contains current gain for a given node (by session id). More efficient than + * querying it, plus there isn't a getGain function so why write one */ +var sessionGains = {}; +function convertDbToLinear(decibels) { + // +20db = 10x, 0dB = 1x, -10dB = 0.1x, etc... + // but, your perception is that something 2x as loud is +10db + // so we go from -60 to +20 or 1/64x to 4x. For now, we can + // maybe scale the signal this way?? + return Math.pow(2, decibels/10.0); +} + function fromQml(message) { // messages are {method, params}, like json-rpc. See also sendToQml. var data; switch (message.method) { @@ -311,6 +322,7 @@ function populateUserList(selectData) { userName: '', sessionId: id || '', audioLevel: 0.0, + avgAudioLevel: 0.0, admin: false, personalMute: !!id && Users.getPersonalMuteStatus(id), // expects proper boolean, not null ignore: !!id && Users.getIgnoreStatus(id) // ditto @@ -604,41 +616,54 @@ function receiveMessage(channel, messageString, senderID) { } } - var AVERAGING_RATIO = 0.05; var LOUDNESS_FLOOR = 11.0; var LOUDNESS_SCALE = 2.8 / 5.0; var LOG2 = Math.log(2.0); +var AUDIO_PEAK_DECAY = 0.02; var myData = {}; // we're not includied in ExtendedOverlay.get. +function scaleAudio(val) { + var audioLevel = 0.0; + if (val <= LOUDNESS_FLOOR) { + audioLevel = val / LOUDNESS_FLOOR * LOUDNESS_SCALE; + } else { + audioLevel = (val -(LOUDNESS_FLOOR -1 )) * LOUDNESS_SCALE; + } + if (audioLevel > 1.0) { + audioLevel = 1; + } + return audioLevel; +} + function getAudioLevel(id) { // the VU meter should work similarly to the one in AvatarInputs: log scale, exponentially averaged // But of course it gets the data at a different rate, so we tweak the averaging ratio and frequency // of updating (the latter for efficiency too). var avatar = AvatarList.getAvatar(id); var audioLevel = 0.0; + var avgAudioLevel = 0.0; var data = id ? ExtendedOverlay.get(id) : myData; - if (!data) { - return audioLevel; - } + if (data) { - // we will do exponential moving average by taking some the last loudness and averaging - data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness); + // we will do exponential moving average by taking some the last loudness and averaging + data.accumulatedLevel = AVERAGING_RATIO * (data.accumulatedLevel || 0) + (1 - AVERAGING_RATIO) * (avatar.audioLoudness); - // add 1 to insure we don't go log() and hit -infinity. Math.log is - // natural log, so to get log base 2, just divide by ln(2). - var logLevel = Math.log(data.accumulatedLevel + 1) / LOG2; + // add 1 to insure we don't go log() and hit -infinity. Math.log is + // natural log, so to get log base 2, just divide by ln(2). + audioLevel = scaleAudio(Math.log(data.accumulatedLevel + 1) / LOG2); - if (logLevel <= LOUDNESS_FLOOR) { - audioLevel = logLevel / LOUDNESS_FLOOR * LOUDNESS_SCALE; - } else { - audioLevel = (logLevel - (LOUDNESS_FLOOR - 1.0)) * LOUDNESS_SCALE; + // decay avgAudioLevel + avgAudioLevel = Math.max((1-AUDIO_PEAK_DECAY) * (data.avgAudioLevel || 0), audioLevel); + + data.avgAudioLevel = avgAudioLevel; + data.audioLevel = audioLevel; + + // now scale for the gain. Also, asked to boost the low end, so one simple way is + // to take sqrt of the value. Lets try that, see how it feels. + avgAudioLevel = Math.min(1.0, Math.sqrt(avgAudioLevel *(sessionGains[id] || 0.75))); } - if (audioLevel > 1.0) { - audioLevel = 1; - } - data.audioLevel = audioLevel; - return audioLevel; + return [audioLevel, avgAudioLevel]; } function createAudioInterval(interval) {