From 7780db813da4196f6fc4ce555172acacb70793ff Mon Sep 17 00:00:00 2001 From: Oren Hurvitz Date: Mon, 16 Apr 2018 15:26:06 +0300 Subject: [PATCH 01/27] Save the "Mute Microphone" setting --- interface/src/scripting/Audio.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 2c4c29ff65..b4e3d7913b 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -25,6 +25,8 @@ QString Audio::DESKTOP { "Desktop" }; QString Audio::HMD { "VR" }; Setting::Handle enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true }; +Setting::Handle mutedSetting { QStringList{ Audio::AUDIO, "MuteMicrophone" }, false }; + float Audio::loudnessToLevel(float loudness) { float level = loudness * (1/32768.0f); // level in [0, 1] @@ -42,6 +44,7 @@ Audio::Audio() : _devices(_contextIsHMD) { connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged); enableNoiseReduction(enableNoiseReductionSetting.get()); onContextChanged(); + setMuted(mutedSetting.get()); } bool Audio::startRecording(const QString& filepath) { @@ -89,6 +92,15 @@ bool Audio::noiseReductionEnabled() const { }); } +void Audio::onMutedChanged() { + bool isMuted = DependencyManager::get()->isMuted(); + if (_isMuted != isMuted) { + _isMuted = isMuted; + mutedSetting.set(_isMuted); + emit mutedChanged(_isMuted); + } +} + void Audio::enableNoiseReduction(bool enable) { bool changed = false; withWriteLock([&] { From d54d0280c2469b2cec8ead405fe754ced5c4604f Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sun, 17 Feb 2019 14:21:23 -0800 Subject: [PATCH 02/27] rework audioMuteOverlay.js --- interface/src/scripting/Audio.cpp | 10 +-- scripts/defaultScripts.js | 3 +- scripts/system/audioMuteOverlay.js | 140 +++++++++++++---------------- 3 files changed, 63 insertions(+), 90 deletions(-) diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index b4e3d7913b..bb40f69b0b 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -76,6 +76,7 @@ void Audio::setMuted(bool isMuted) { withWriteLock([&] { if (_isMuted != isMuted) { _isMuted = isMuted; + mutedSetting.set(_isMuted); auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); changed = true; @@ -92,15 +93,6 @@ bool Audio::noiseReductionEnabled() const { }); } -void Audio::onMutedChanged() { - bool isMuted = DependencyManager::get()->isMuted(); - if (_isMuted != isMuted) { - _isMuted = isMuted; - mutedSetting.set(_isMuted); - emit mutedChanged(_isMuted); - } -} - void Audio::enableNoiseReduction(bool enable) { bool changed = false; withWriteLock([&] { diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index bd7e79dffc..e392680df9 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,7 +32,8 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js" + "system/miniTablet.js", + "system/audioMuteOverlay.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index 731d62017d..14ac96c8c6 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -1,104 +1,84 @@ -"use strict"; -/* jslint vars: true, plusplus: true, forin: true*/ -/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ -/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // audioMuteOverlay.js // // client script that creates an overlay to provide mute feedback // // Created by Triplelexx on 17/03/09 +// Reworked by Seth Alves on 2019-2-17 // Copyright 2017 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 // +"use strict"; + +/* global Audio, Script, Overlays, Quat, MyAvatar */ + (function() { // BEGIN LOCAL_SCOPE - var utilsPath = Script.resolvePath('../developer/libraries/utils.js'); - Script.include(utilsPath); - var TWEEN_SPEED = 0.025; - var MIX_AMOUNT = 0.25; + var lastInputLoudness = 0.0; + var sampleRate = 8.0; // Hz + var attackTC = Math.exp(-1.0 / (sampleRate * 0.500)) // 500 milliseconds attack + var releaseTC = Math.exp(-1.0 / (sampleRate * 1.000)) // 1000 milliseconds release + var holdReset = 2.0 * sampleRate; // 2 seconds hold + var holdCount = 0; + var warningOverlayID = null; - var overlayPosition = Vec3.ZERO; - var tweenPosition = 0; - var startColor = { - red: 170, - green: 170, - blue: 170 - }; - var endColor = { - red: 255, - green: 0, - blue: 0 - }; - var overlayID; + function showWarning() { + if (warningOverlayID) { + return; + } + warningOverlayID = Overlays.addOverlay("text3d", { + name: "Muted-Warning", + localPosition: { x: 0.2, y: -0.35, z: -1.0 }, + localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }), + text: "Warning: you are muted", + textAlpha: 1, + color: { red: 226, green: 51, blue: 77 }, + backgroundAlpha: 0, + lineHeight: 0.042, + visible: true, + ignoreRayIntersection: true, + drawInFront: true, + grabbable: false, + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX") + }); + }; - Script.update.connect(update); - Script.scriptEnding.connect(cleanup); + function hideWarning() { + if (!warningOverlayID) { + return; + } + Overlays.deleteOverlay(warningOverlayID); + warningOverlayID = null; + } - function update(dt) { - if (!Audio.muted) { - if (hasOverlay()) { - deleteOverlay(); - } - } else if (!hasOverlay()) { - createOverlay(); - } else { - updateOverlay(); - } - } + function cleanup() { + Overlays.deleteOverlay(warningOverlayID); + } - function getOffsetPosition() { - return Vec3.sum(Camera.position, Quat.getFront(Camera.orientation)); - } + Script.scriptEnding.connect(cleanup); - function createOverlay() { - overlayPosition = getOffsetPosition(); - overlayID = Overlays.addOverlay("sphere", { - position: overlayPosition, - rotation: Camera.orientation, - alpha: 0.9, - dimensions: 0.1, - solid: true, - ignoreRayIntersection: true - }); - } + Script.setInterval(function() { - function hasOverlay() { - return Overlays.getProperty(overlayID, "position") !== undefined; - } + var inputLoudness = Audio.inputLevel; + var tc = (inputLoudness > lastInputLoudness) ? attackTC : releaseTC; + inputLoudness += tc * (lastInputLoudness - inputLoudness); + lastInputLoudness = inputLoudness; - function updateOverlay() { - // increase by TWEEN_SPEED until completion - if (tweenPosition < 1) { - tweenPosition += TWEEN_SPEED; - } else { - // after tween completion reset to zero and flip values to ping pong - tweenPosition = 0; - for (var component in startColor) { - var storedColor = startColor[component]; - startColor[component] = endColor[component]; - endColor[component] = storedColor; - } - } - // mix previous position with new and mix colors - overlayPosition = Vec3.mix(overlayPosition, getOffsetPosition(), MIX_AMOUNT); - Overlays.editOverlay(overlayID, { - color: colorMix(startColor, endColor, easeIn(tweenPosition)), - position: overlayPosition, - rotation: Camera.orientation - }); - } + if (Audio.muted && inputLoudness > 0.3) { + holdCount = holdReset; + } else { + holdCount = Math.max(holdCount - 1, 0); + } - function deleteOverlay() { - Overlays.deleteOverlay(overlayID); - } + if (holdCount > 0) { + showWarning(); + } else { + hideWarning(); + } + }, 1000.0 / sampleRate); - function cleanup() { - deleteOverlay(); - Audio.muted.disconnect(onMuteToggled); - Script.update.disconnect(update); - } }()); // END LOCAL_SCOPE From 3ecd86caee33cb72aaadbf2fc3fe90cde635a5c6 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Tue, 19 Feb 2019 09:32:41 -0800 Subject: [PATCH 03/27] add a way to disble muted warning from audio panel. fix positioning of warning. hide warning when removing timer. --- interface/resources/qml/hifi/audio/Audio.qml | 13 ++ interface/src/scripting/Audio.cpp | 25 ++++ interface/src/scripting/Audio.h | 14 +- libraries/audio-client/src/AudioClient.cpp | 8 + libraries/audio-client/src/AudioClient.h | 5 + scripts/system/audioMuteOverlay.js | 146 ++++++++++++------- 6 files changed, 155 insertions(+), 56 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index c8dd83cd62..34ae64aee8 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -159,6 +159,19 @@ Rectangle { onXChanged: rightMostInputLevelPos = x + width } } + + RowLayout { + spacing: muteMic.spacing*2; + AudioControls.CheckBox { + spacing: muteMic.spacing + text: qsTr("Warn when muted"); + checked: AudioScriptingInterface.warnWhenMuted; + onClicked: { + AudioScriptingInterface.warnWhenMuted = checked; + checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding + } + } + } } Separator {} diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index bb40f69b0b..4a4b3c146b 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -25,6 +25,7 @@ QString Audio::DESKTOP { "Desktop" }; QString Audio::HMD { "VR" }; Setting::Handle enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true }; +Setting::Handle enableWarnWhenMutedSetting { QStringList { Audio::AUDIO, "WarnWhenMuted" }, true }; Setting::Handle mutedSetting { QStringList{ Audio::AUDIO, "MuteMicrophone" }, false }; @@ -39,10 +40,12 @@ Audio::Audio() : _devices(_contextIsHMD) { auto client = DependencyManager::get().data(); connect(client, &AudioClient::muteToggled, this, &Audio::setMuted); connect(client, &AudioClient::noiseReductionChanged, this, &Audio::enableNoiseReduction); + connect(client, &AudioClient::warnWhenMutedChanged, this, &Audio::enableWarnWhenMuted); connect(client, &AudioClient::inputLoudnessChanged, this, &Audio::onInputLoudnessChanged); connect(client, &AudioClient::inputVolumeChanged, this, &Audio::setInputVolume); connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged); enableNoiseReduction(enableNoiseReductionSetting.get()); + enableWarnWhenMuted(enableWarnWhenMutedSetting.get()); onContextChanged(); setMuted(mutedSetting.get()); } @@ -109,6 +112,28 @@ void Audio::enableNoiseReduction(bool enable) { } } +bool Audio::warnWhenMutedEnabled() const { + return resultWithReadLock([&] { + return _enableWarnWhenMuted; + }); +} + +void Audio::enableWarnWhenMuted(bool enable) { + bool changed = false; + withWriteLock([&] { + if (_enableWarnWhenMuted != enable) { + _enableWarnWhenMuted = enable; + auto client = DependencyManager::get().data(); + QMetaObject::invokeMethod(client, "setWarnWhenMuted", Q_ARG(bool, enable), Q_ARG(bool, false)); + enableWarnWhenMutedSetting.set(enable); + changed = true; + } + }); + if (changed) { + emit warnWhenMutedChanged(enable); + } +} + float Audio::getInputVolume() const { return resultWithReadLock([&] { return _inputVolume; diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index e4dcba9130..7e216eb0b2 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -58,6 +58,7 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged) Q_PROPERTY(bool noiseReduction READ noiseReductionEnabled WRITE enableNoiseReduction NOTIFY noiseReductionChanged) + Q_PROPERTY(bool warnWhenMuted READ warnWhenMutedEnabled WRITE enableWarnWhenMuted NOTIFY warnWhenMutedChanged) Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY inputVolumeChanged) Q_PROPERTY(float inputLevel READ getInputLevel NOTIFY inputLevelChanged) Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) @@ -75,6 +76,7 @@ public: bool isMuted() const; bool noiseReductionEnabled() const; + bool warnWhenMutedEnabled() const; float getInputVolume() const; float getInputLevel() const; bool isClipping() const; @@ -192,7 +194,7 @@ signals: * }); */ void mutedChanged(bool isMuted); - + /**jsdoc * Triggered when the audio input noise reduction is enabled or disabled. * @function Audio.noiseReductionChanged @@ -201,6 +203,14 @@ signals: */ void noiseReductionChanged(bool isEnabled); + /**jsdoc + * Triggered when "warn when muted" is enabled or disabled. + * @function Audio.warnWhenMutedChanged + * @param {boolean} isEnabled - true if "warn when muted" is enabled, otherwise false. + * @returns {Signal} + */ + void warnWhenMutedChanged(bool isEnabled); + /**jsdoc * Triggered when the input audio volume changes. * @function Audio.inputVolumeChanged @@ -248,6 +258,7 @@ public slots: private slots: void setMuted(bool muted); void enableNoiseReduction(bool enable); + void enableWarnWhenMuted(bool enable); void setInputVolume(float volume); void onInputLoudnessChanged(float loudness, bool isClipping); @@ -262,6 +273,7 @@ private: bool _isClipping { false }; bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. + bool _enableWarnWhenMuted { true }; bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 8c50a195ee..1c10d24f23 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1531,6 +1531,14 @@ void AudioClient::setNoiseReduction(bool enable, bool emitSignal) { } } +void AudioClient::setWarnWhenMuted(bool enable, bool emitSignal) { + if (_warnWhenMuted != enable) { + _warnWhenMuted = enable; + if (emitSignal) { + emit warnWhenMutedChanged(_warnWhenMuted); + } + } +} bool AudioClient::setIsStereoInput(bool isStereoInput) { bool stereoInputChanged = false; diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 29036b7c71..6d3483b0f8 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -210,6 +210,9 @@ public slots: void setNoiseReduction(bool isNoiseGateEnabled, bool emitSignal = true); bool isNoiseReductionEnabled() const { return _isNoiseGateEnabled; } + void setWarnWhenMuted(bool isNoiseGateEnabled, bool emitSignal = true); + bool isWarnWhenMutedEnabled() const { return _warnWhenMuted; } + bool getLocalEcho() { return _shouldEchoLocally; } void setLocalEcho(bool localEcho) { _shouldEchoLocally = localEcho; } void toggleLocalEcho() { _shouldEchoLocally = !_shouldEchoLocally; } @@ -246,6 +249,7 @@ signals: void inputVolumeChanged(float volume); void muteToggled(bool muted); void noiseReductionChanged(bool noiseReductionEnabled); + void warnWhenMutedChanged(bool warnWhenMutedEnabled); void mutedByMixer(); void inputReceived(const QByteArray& inputSamples); void inputLoudnessChanged(float loudness, bool isClipping); @@ -365,6 +369,7 @@ private: bool _shouldEchoLocally; bool _shouldEchoToServer; bool _isNoiseGateEnabled; + bool _warnWhenMuted; bool _reverb; AudioEffectOptions _scriptReverbOptions; diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index 14ac96c8c6..d759b7d885 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -17,68 +17,104 @@ (function() { // BEGIN LOCAL_SCOPE - var lastInputLoudness = 0.0; - var sampleRate = 8.0; // Hz - var attackTC = Math.exp(-1.0 / (sampleRate * 0.500)) // 500 milliseconds attack - var releaseTC = Math.exp(-1.0 / (sampleRate * 1.000)) // 1000 milliseconds release - var holdReset = 2.0 * sampleRate; // 2 seconds hold - var holdCount = 0; - var warningOverlayID = null; + var lastInputLoudness = 0.0; + var sampleRate = 8.0; // Hz + var attackTC = Math.exp(-1.0 / (sampleRate * 0.500)); // 500 milliseconds attack + var releaseTC = Math.exp(-1.0 / (sampleRate * 1.000)); // 1000 milliseconds release + var holdReset = 2.0 * sampleRate; // 2 seconds hold + var holdCount = 0; + var warningOverlayID = null; + var pollInterval = null; + var warningText = "Muted"; + var textDimensions = { x: 100, y: 50 }; - function showWarning() { - if (warningOverlayID) { - return; - } - warningOverlayID = Overlays.addOverlay("text3d", { - name: "Muted-Warning", - localPosition: { x: 0.2, y: -0.35, z: -1.0 }, - localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }), - text: "Warning: you are muted", - textAlpha: 1, - color: { red: 226, green: 51, blue: 77 }, - backgroundAlpha: 0, - lineHeight: 0.042, - visible: true, - ignoreRayIntersection: true, - drawInFront: true, - grabbable: false, - parentID: MyAvatar.SELF_ID, - parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX") - }); - }; + function showWarning() { + if (warningOverlayID) { + return; + } - function hideWarning() { - if (!warningOverlayID) { - return; - } - Overlays.deleteOverlay(warningOverlayID); - warningOverlayID = null; - } + var windowWidth; + var windowHeight; + if (HMD.active) { + var viewportDimension = Controller.getViewportDimensions(); + windowWidth = viewportDimension.x; + windowHeight = viewportDimension.y; + } else { + windowWidth = Window.innerWidth; + windowHeight = Window.innerHeight; + } - function cleanup() { - Overlays.deleteOverlay(warningOverlayID); - } + warningOverlayID = Overlays.addOverlay("text", { + name: "Muted-Warning", + font: { size: 36 }, + text: warningText, + x: windowWidth / 2 - textDimensions.x / 2, + y: windowHeight / 2 - textDimensions.y / 2, + width: textDimensions.x, + height: textDimensions.y, + textColor: { red: 226, green: 51, blue: 77 }, + backgroundAlpha: 0, + visible: true + }); + } - Script.scriptEnding.connect(cleanup); + function hideWarning() { + if (!warningOverlayID) { + return; + } + Overlays.deleteOverlay(warningOverlayID); + warningOverlayID = null; + } - Script.setInterval(function() { + function startPoll() { + if (pollInterval) { + return; + } + pollInterval = Script.setInterval(function() { + var inputLoudness = Audio.inputLevel; + var tc = (inputLoudness > lastInputLoudness) ? attackTC : releaseTC; + inputLoudness += tc * (lastInputLoudness - inputLoudness); + lastInputLoudness = inputLoudness; - var inputLoudness = Audio.inputLevel; - var tc = (inputLoudness > lastInputLoudness) ? attackTC : releaseTC; - inputLoudness += tc * (lastInputLoudness - inputLoudness); - lastInputLoudness = inputLoudness; + if (inputLoudness > 0.1) { + holdCount = holdReset; + } else { + holdCount = Math.max(holdCount - 1, 0); + } - if (Audio.muted && inputLoudness > 0.3) { - holdCount = holdReset; - } else { - holdCount = Math.max(holdCount - 1, 0); - } + if (holdCount > 0) { + showWarning(); + } else { + hideWarning(); + } + }, 1000.0 / sampleRate); + } - if (holdCount > 0) { - showWarning(); - } else { - hideWarning(); - } - }, 1000.0 / sampleRate); + function stopPoll() { + if (!pollInterval) { + return; + } + Script.clearInterval(pollInterval); + pollInterval = null; + hideWarning(); + } + + function startOrStopPoll() { + if (Audio.warnWhenMuted && Audio.muted) { + startPoll(); + } else { + stopPoll(); + } + } + + function cleanup() { + stopPoll(); + } + + Script.scriptEnding.connect(cleanup); + + startOrStopPoll(); + Audio.mutedChanged.connect(startOrStopPoll); + Audio.warnWhenMutedChanged.connect(startOrStopPoll); }()); // END LOCAL_SCOPE From f83f2b8226b17f2c7446534cd155b917d01a0cc1 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 20 Feb 2019 12:53:43 -0800 Subject: [PATCH 04/27] keep muted warning in center of view for HMD --- scripts/system/audioMuteOverlay.js | 51 ++++++++++++++++++------------ 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index d759b7d885..65793d1d87 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -26,36 +26,45 @@ var warningOverlayID = null; var pollInterval = null; var warningText = "Muted"; - var textDimensions = { x: 100, y: 50 }; function showWarning() { if (warningOverlayID) { return; } - var windowWidth; - var windowHeight; if (HMD.active) { - var viewportDimension = Controller.getViewportDimensions(); - windowWidth = viewportDimension.x; - windowHeight = viewportDimension.y; + warningOverlayID = Overlays.addOverlay("text3d", { + name: "Muted-Warning", + localPosition: { x: 0, y: 0, z: -1.0 }, + localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }), + text: warningText, + textAlpha: 1, + textColor: { red: 226, green: 51, blue: 77 }, + backgroundAlpha: 0, + lineHeight: 0.042, + dimensions: { x: 0.11, y: 0.05 }, + visible: true, + ignoreRayIntersection: true, + drawInFront: true, + grabbable: false, + parentID: MyAvatar.SELF_ID, + parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX") + }); } else { - windowWidth = Window.innerWidth; - windowHeight = Window.innerHeight; + var textDimensions = { x: 100, y: 50 }; + warningOverlayID = Overlays.addOverlay("text", { + name: "Muted-Warning", + font: { size: 36 }, + text: warningText, + x: Window.innerWidth / 2 - textDimensions.x / 2, + y: Window.innerHeight / 2 - textDimensions.y / 2, + width: textDimensions.x, + height: textDimensions.y, + textColor: { red: 226, green: 51, blue: 77 }, + backgroundAlpha: 0, + visible: true + }); } - - warningOverlayID = Overlays.addOverlay("text", { - name: "Muted-Warning", - font: { size: 36 }, - text: warningText, - x: windowWidth / 2 - textDimensions.x / 2, - y: windowHeight / 2 - textDimensions.y / 2, - width: textDimensions.x, - height: textDimensions.y, - textColor: { red: 226, green: 51, blue: 77 }, - backgroundAlpha: 0, - visible: true - }); } function hideWarning() { From 216b53dcb0975aba559e410393244ce44b0cc691 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Wed, 20 Feb 2019 13:07:24 -0800 Subject: [PATCH 05/27] attempt to take background noise into account when triggering mute warning --- scripts/system/audioMuteOverlay.js | 33 ++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index 65793d1d87..96f6d636dc 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -13,14 +13,22 @@ "use strict"; -/* global Audio, Script, Overlays, Quat, MyAvatar */ +/* global Audio, Script, Overlays, Quat, MyAvatar, HMD */ (function() { // BEGIN LOCAL_SCOPE - var lastInputLoudness = 0.0; + var lastShortTermInputLoudness = 0.0; + var lastLongTermInputLoudness = 0.0; var sampleRate = 8.0; // Hz - var attackTC = Math.exp(-1.0 / (sampleRate * 0.500)); // 500 milliseconds attack - var releaseTC = Math.exp(-1.0 / (sampleRate * 1.000)); // 1000 milliseconds release + + var shortTermAttackTC = Math.exp(-1.0 / (sampleRate * 0.500)); // 500 milliseconds attack + var shortTermReleaseTC = Math.exp(-1.0 / (sampleRate * 1.000)); // 1000 milliseconds release + + var longTermAttackTC = Math.exp(-1.0 / (sampleRate * 5.0)); // 5 second attack + var longTermReleaseTC = Math.exp(-1.0 / (sampleRate * 10.0)); // 10 seconds release + + var activationThreshold = 0.05; // how much louder short-term needs to be than long-term to trigger warning + var holdReset = 2.0 * sampleRate; // 2 seconds hold var holdCount = 0; var warningOverlayID = null; @@ -80,12 +88,19 @@ return; } pollInterval = Script.setInterval(function() { - var inputLoudness = Audio.inputLevel; - var tc = (inputLoudness > lastInputLoudness) ? attackTC : releaseTC; - inputLoudness += tc * (lastInputLoudness - inputLoudness); - lastInputLoudness = inputLoudness; + var shortTermInputLoudness = Audio.inputLevel; + var longTermInputLoudness = shortTermInputLoudness; - if (inputLoudness > 0.1) { + var shortTc = (shortTermInputLoudness > lastShortTermInputLoudness) ? shortTermAttackTC : shortTermReleaseTC; + var longTc = (longTermInputLoudness > lastLongTermInputLoudness) ? longTermAttackTC : longTermReleaseTC; + + shortTermInputLoudness += shortTc * (lastShortTermInputLoudness - shortTermInputLoudness); + longTermInputLoudness += longTc * (lastLongTermInputLoudness - longTermInputLoudness); + + lastShortTermInputLoudness = shortTermInputLoudness; + lastLongTermInputLoudness = longTermInputLoudness; + + if (shortTermInputLoudness > lastLongTermInputLoudness + activationThreshold) { holdCount = holdReset; } else { holdCount = Math.max(holdCount - 1, 0); From 6ee236e783e8a1faf2075b0cf462f11770f6a486 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Thu, 21 Feb 2019 14:04:08 -0800 Subject: [PATCH 06/27] added a button to enable server loopback of audio --- interface/resources/qml/hifi/audio/Audio.qml | 7 ++ .../qml/hifi/audio/LoopbackAudio.qml | 75 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 interface/resources/qml/hifi/audio/LoopbackAudio.qml diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 34ae64aee8..e340ec5003 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -313,5 +313,12 @@ Rectangle { (bar.currentIndex === 0 && !isVR); anchors { left: parent.left; leftMargin: margins.paddings } } + LoopbackAudio { + x: margins.paddings + + visible: (bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR); + anchors { left: parent.left; leftMargin: margins.paddings } + } } } diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml new file mode 100644 index 0000000000..6d5f8d88fd --- /dev/null +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -0,0 +1,75 @@ +// +// LoopbackAudio.qml +// qml/hifi/audio +// +// Created by Seth Alves on 2019-2-18 +// Copyright 2019 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 +// + +import QtQuick 2.7 +import QtQuick.Controls 2.2 +import QtQuick.Layouts 1.3 + +import stylesUit 1.0 +import controlsUit 1.0 as HifiControls + +RowLayout { + property bool audioLoopedBack: false; + function startAudioLoopback() { + if (!audioLoopedBack) { + audioLoopedBack = true; + AudioScope.setServerEcho(true); + } + } + function stopAudioLoopback () { + if (audioLoopedBack) { + audioLoopedBack = false; + AudioScope.setServerEcho(false); + } + } + + Component.onDestruction: stopAudioLoopback(); + onVisibleChanged: { + if (!visible) { + stopAudioLoopback(); + } + } + + HifiConstants { id: hifi; } + + Button { + id: control + background: Rectangle { + implicitWidth: 20; + implicitHeight: 20; + radius: hifi.buttons.radius; + gradient: Gradient { + GradientStop { + position: 0.2; + color: audioLoopedBack ? hifi.buttons.colorStart[hifi.buttons.blue] : hifi.buttons.colorStart[hifi.buttons.black]; + } + GradientStop { + position: 1.0; + color: audioLoopedBack ? hifi.buttons.colorFinish[hifi.buttons.blue] : hifi.buttons.colorFinish[hifi.buttons.black]; + } + } + } + contentItem: HiFiGlyphs { + size: 14; + color: (control.pressed || control.hovered) ? (audioLoopedBack ? "black" : hifi.colors.primaryHighlight) : "white"; + text: audioLoopedBack ? hifi.glyphs.stop_square : hifi.glyphs.playback_play; + } + + onClicked: audioLoopedBack ? stopAudioLoopback() : startAudioLoopback(); + } + + RalewayRegular { + Layout.leftMargin: 2; + size: 14; + color: "white"; + text: audioLoopedBack ? qsTr("Disable Audio Loopback") : qsTr("Enable Audio Loopback"); + } +} From d5e8cba1eec07a12c6cf6a6f8e1919a015b541fb Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Fri, 22 Feb 2019 09:33:23 -0800 Subject: [PATCH 07/27] make server-audio-loopback button work in HMDs --- interface/resources/qml/hifi/audio/LoopbackAudio.qml | 4 ++-- libraries/audio-client/src/AudioClient.h | 12 ++++++------ libraries/audio/src/AbstractAudioInterface.h | 9 ++++++++- .../script-engine/src/AudioScriptingInterface.cpp | 12 ++++++++++++ .../script-engine/src/AudioScriptingInterface.h | 12 ++++++++++++ 5 files changed, 40 insertions(+), 9 deletions(-) diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 6d5f8d88fd..2f0dbe5950 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -21,13 +21,13 @@ RowLayout { function startAudioLoopback() { if (!audioLoopedBack) { audioLoopedBack = true; - AudioScope.setServerEcho(true); + AudioScriptingInterface.setServerEcho(true); } } function stopAudioLoopback () { if (audioLoopedBack) { audioLoopedBack = false; - AudioScope.setServerEcho(false); + AudioScriptingInterface.setServerEcho(false); } } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 6d3483b0f8..b9648219a5 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -213,13 +213,13 @@ public slots: void setWarnWhenMuted(bool isNoiseGateEnabled, bool emitSignal = true); bool isWarnWhenMutedEnabled() const { return _warnWhenMuted; } - bool getLocalEcho() { return _shouldEchoLocally; } - void setLocalEcho(bool localEcho) { _shouldEchoLocally = localEcho; } - void toggleLocalEcho() { _shouldEchoLocally = !_shouldEchoLocally; } + virtual bool getLocalEcho() override { return _shouldEchoLocally; } + virtual void setLocalEcho(bool localEcho) override { _shouldEchoLocally = localEcho; } + virtual void toggleLocalEcho() override { _shouldEchoLocally = !_shouldEchoLocally; } - bool getServerEcho() { return _shouldEchoToServer; } - void setServerEcho(bool serverEcho) { _shouldEchoToServer = serverEcho; } - void toggleServerEcho() { _shouldEchoToServer = !_shouldEchoToServer; } + virtual bool getServerEcho() override { return _shouldEchoToServer; } + virtual void setServerEcho(bool serverEcho) override { _shouldEchoToServer = serverEcho; } + virtual void toggleServerEcho() override { _shouldEchoToServer = !_shouldEchoToServer; } void processReceivedSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer); void sendMuteEnvironmentPacket(); diff --git a/libraries/audio/src/AbstractAudioInterface.h b/libraries/audio/src/AbstractAudioInterface.h index 0f075ab224..e9e40e95f9 100644 --- a/libraries/audio/src/AbstractAudioInterface.h +++ b/libraries/audio/src/AbstractAudioInterface.h @@ -45,9 +45,16 @@ public slots: virtual bool shouldLoopbackInjectors() { return false; } virtual bool setIsStereoInput(bool stereo) = 0; - virtual bool isStereoInput() = 0; + virtual bool getLocalEcho() = 0; + virtual void setLocalEcho(bool localEcho) = 0; + virtual void toggleLocalEcho() = 0; + + virtual bool getServerEcho() = 0; + virtual void setServerEcho(bool serverEcho) = 0; + virtual void toggleServerEcho() = 0; + signals: void isStereoInputChanged(bool isStereo); }; diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index 8e54d2d5de..b12b55c3f7 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -88,3 +88,15 @@ bool AudioScriptingInterface::isStereoInput() { } return stereoEnabled; } + +void AudioScriptingInterface::setServerEcho(bool serverEcho) { + if (_localAudioInterface) { + QMetaObject::invokeMethod(_localAudioInterface, "setServerEcho", Q_ARG(bool, serverEcho)); + } +} + +void AudioScriptingInterface::setLocalEcho(bool localEcho) { + if (_localAudioInterface) { + QMetaObject::invokeMethod(_localAudioInterface, "setLocalEcho", Q_ARG(bool, localEcho)); + } +} diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index d2f886d2dd..23cc02248d 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -66,6 +66,18 @@ public: _localAudioInterface->getAudioSolo().reset(); } + /**jsdoc + * @function Audio.setServerEcho + * @parm {boolean} serverEcho + */ + Q_INVOKABLE void setServerEcho(bool serverEcho); + + /**jsdoc + * @function Audio.setLocalEcho + * @parm {boolean} localEcho + */ + Q_INVOKABLE void setLocalEcho(bool localEcho); + protected: AudioScriptingInterface() = default; From 96142160066eea2064789078457015d193390536 Mon Sep 17 00:00:00 2001 From: Seth Alves Date: Sat, 23 Feb 2019 14:23:09 -0800 Subject: [PATCH 08/27] don't disable server echo when audio qml page is closed --- .../qml/hifi/audio/LoopbackAudio.qml | 9 +----- .../src/AudioScriptingInterface.cpp | 28 +++++++++++++++++++ .../src/AudioScriptingInterface.h | 21 ++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 2f0dbe5950..3ecf09c948 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -17,7 +17,7 @@ import stylesUit 1.0 import controlsUit 1.0 as HifiControls RowLayout { - property bool audioLoopedBack: false; + property bool audioLoopedBack: AudioScriptingInterface.getServerEcho(); function startAudioLoopback() { if (!audioLoopedBack) { audioLoopedBack = true; @@ -31,13 +31,6 @@ RowLayout { } } - Component.onDestruction: stopAudioLoopback(); - onVisibleChanged: { - if (!visible) { - stopAudioLoopback(); - } - } - HifiConstants { id: hifi; } Button { diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index b12b55c3f7..65d71e46e6 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -89,14 +89,42 @@ bool AudioScriptingInterface::isStereoInput() { return stereoEnabled; } +bool AudioScriptingInterface::getServerEcho() { + bool serverEchoEnabled = false; + if (_localAudioInterface) { + serverEchoEnabled = _localAudioInterface->getServerEcho(); + } + return serverEchoEnabled; +} + void AudioScriptingInterface::setServerEcho(bool serverEcho) { if (_localAudioInterface) { QMetaObject::invokeMethod(_localAudioInterface, "setServerEcho", Q_ARG(bool, serverEcho)); } } +void AudioScriptingInterface::toggleServerEcho() { + if (_localAudioInterface) { + QMetaObject::invokeMethod(_localAudioInterface, "toggleServerEcho"); + } +} + +bool AudioScriptingInterface::getLocalEcho() { + bool localEchoEnabled = false; + if (_localAudioInterface) { + localEchoEnabled = _localAudioInterface->getLocalEcho(); + } + return localEchoEnabled; +} + void AudioScriptingInterface::setLocalEcho(bool localEcho) { if (_localAudioInterface) { QMetaObject::invokeMethod(_localAudioInterface, "setLocalEcho", Q_ARG(bool, localEcho)); } } + +void AudioScriptingInterface::toggleLocalEcho() { + if (_localAudioInterface) { + QMetaObject::invokeMethod(_localAudioInterface, "toggleLocalEcho"); + } +} diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index 23cc02248d..a6801dcdcb 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -66,18 +66,39 @@ public: _localAudioInterface->getAudioSolo().reset(); } + /**jsdoc + * @function Audio.getServerEcho + */ + Q_INVOKABLE bool getServerEcho(); + /**jsdoc * @function Audio.setServerEcho * @parm {boolean} serverEcho */ Q_INVOKABLE void setServerEcho(bool serverEcho); + /**jsdoc + * @function Audio.toggleServerEcho + */ + Q_INVOKABLE void toggleServerEcho(); + + /**jsdoc + * @function Audio.getLocalEcho + */ + Q_INVOKABLE bool getLocalEcho(); + /**jsdoc * @function Audio.setLocalEcho * @parm {boolean} localEcho */ Q_INVOKABLE void setLocalEcho(bool localEcho); + /**jsdoc + * @function Audio.toggleLocalEcho + */ + Q_INVOKABLE void toggleLocalEcho(); + + protected: AudioScriptingInterface() = default; From 4187104b1788955f050a20304bb1ccce737fd370 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 4 Mar 2019 14:12:09 -0800 Subject: [PATCH 09/27] culling mute state --- interface/resources/qml/hifi/audio/Audio.qml | 13 -- interface/src/scripting/Audio.cpp | 29 --- interface/src/scripting/Audio.h | 12 -- libraries/audio-client/src/AudioClient.cpp | 9 - libraries/audio-client/src/AudioClient.h | 5 - scripts/system/audioMuteOverlay.js | 202 ++++++++----------- 6 files changed, 81 insertions(+), 189 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index e340ec5003..aa64af22a3 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -159,19 +159,6 @@ Rectangle { onXChanged: rightMostInputLevelPos = x + width } } - - RowLayout { - spacing: muteMic.spacing*2; - AudioControls.CheckBox { - spacing: muteMic.spacing - text: qsTr("Warn when muted"); - checked: AudioScriptingInterface.warnWhenMuted; - onClicked: { - AudioScriptingInterface.warnWhenMuted = checked; - checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding - } - } - } } Separator {} diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 4a4b3c146b..2c4c29ff65 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -25,9 +25,6 @@ QString Audio::DESKTOP { "Desktop" }; QString Audio::HMD { "VR" }; Setting::Handle enableNoiseReductionSetting { QStringList { Audio::AUDIO, "NoiseReduction" }, true }; -Setting::Handle enableWarnWhenMutedSetting { QStringList { Audio::AUDIO, "WarnWhenMuted" }, true }; -Setting::Handle mutedSetting { QStringList{ Audio::AUDIO, "MuteMicrophone" }, false }; - float Audio::loudnessToLevel(float loudness) { float level = loudness * (1/32768.0f); // level in [0, 1] @@ -40,14 +37,11 @@ Audio::Audio() : _devices(_contextIsHMD) { auto client = DependencyManager::get().data(); connect(client, &AudioClient::muteToggled, this, &Audio::setMuted); connect(client, &AudioClient::noiseReductionChanged, this, &Audio::enableNoiseReduction); - connect(client, &AudioClient::warnWhenMutedChanged, this, &Audio::enableWarnWhenMuted); connect(client, &AudioClient::inputLoudnessChanged, this, &Audio::onInputLoudnessChanged); connect(client, &AudioClient::inputVolumeChanged, this, &Audio::setInputVolume); connect(this, &Audio::contextChanged, &_devices, &AudioDevices::onContextChanged); enableNoiseReduction(enableNoiseReductionSetting.get()); - enableWarnWhenMuted(enableWarnWhenMutedSetting.get()); onContextChanged(); - setMuted(mutedSetting.get()); } bool Audio::startRecording(const QString& filepath) { @@ -79,7 +73,6 @@ void Audio::setMuted(bool isMuted) { withWriteLock([&] { if (_isMuted != isMuted) { _isMuted = isMuted; - mutedSetting.set(_isMuted); auto client = DependencyManager::get().data(); QMetaObject::invokeMethod(client, "setMuted", Q_ARG(bool, isMuted), Q_ARG(bool, false)); changed = true; @@ -112,28 +105,6 @@ void Audio::enableNoiseReduction(bool enable) { } } -bool Audio::warnWhenMutedEnabled() const { - return resultWithReadLock([&] { - return _enableWarnWhenMuted; - }); -} - -void Audio::enableWarnWhenMuted(bool enable) { - bool changed = false; - withWriteLock([&] { - if (_enableWarnWhenMuted != enable) { - _enableWarnWhenMuted = enable; - auto client = DependencyManager::get().data(); - QMetaObject::invokeMethod(client, "setWarnWhenMuted", Q_ARG(bool, enable), Q_ARG(bool, false)); - enableWarnWhenMutedSetting.set(enable); - changed = true; - } - }); - if (changed) { - emit warnWhenMutedChanged(enable); - } -} - float Audio::getInputVolume() const { return resultWithReadLock([&] { return _inputVolume; diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index 7e216eb0b2..fcf3c181da 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -58,7 +58,6 @@ class Audio : public AudioScriptingInterface, protected ReadWriteLockable { Q_PROPERTY(bool muted READ isMuted WRITE setMuted NOTIFY mutedChanged) Q_PROPERTY(bool noiseReduction READ noiseReductionEnabled WRITE enableNoiseReduction NOTIFY noiseReductionChanged) - Q_PROPERTY(bool warnWhenMuted READ warnWhenMutedEnabled WRITE enableWarnWhenMuted NOTIFY warnWhenMutedChanged) Q_PROPERTY(float inputVolume READ getInputVolume WRITE setInputVolume NOTIFY inputVolumeChanged) Q_PROPERTY(float inputLevel READ getInputLevel NOTIFY inputLevelChanged) Q_PROPERTY(bool clipping READ isClipping NOTIFY clippingChanged) @@ -76,7 +75,6 @@ public: bool isMuted() const; bool noiseReductionEnabled() const; - bool warnWhenMutedEnabled() const; float getInputVolume() const; float getInputLevel() const; bool isClipping() const; @@ -203,14 +201,6 @@ signals: */ void noiseReductionChanged(bool isEnabled); - /**jsdoc - * Triggered when "warn when muted" is enabled or disabled. - * @function Audio.warnWhenMutedChanged - * @param {boolean} isEnabled - true if "warn when muted" is enabled, otherwise false. - * @returns {Signal} - */ - void warnWhenMutedChanged(bool isEnabled); - /**jsdoc * Triggered when the input audio volume changes. * @function Audio.inputVolumeChanged @@ -258,7 +248,6 @@ public slots: private slots: void setMuted(bool muted); void enableNoiseReduction(bool enable); - void enableWarnWhenMuted(bool enable); void setInputVolume(float volume); void onInputLoudnessChanged(float loudness, bool isClipping); @@ -273,7 +262,6 @@ private: bool _isClipping { false }; bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. - bool _enableWarnWhenMuted { true }; bool _contextIsHMD { false }; AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 1c10d24f23..b2e6167ffa 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1531,15 +1531,6 @@ void AudioClient::setNoiseReduction(bool enable, bool emitSignal) { } } -void AudioClient::setWarnWhenMuted(bool enable, bool emitSignal) { - if (_warnWhenMuted != enable) { - _warnWhenMuted = enable; - if (emitSignal) { - emit warnWhenMutedChanged(_warnWhenMuted); - } - } -} - bool AudioClient::setIsStereoInput(bool isStereoInput) { bool stereoInputChanged = false; if (isStereoInput != _isStereoInput && _inputDeviceInfo.supportedChannelCounts().contains(2)) { diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index b9648219a5..87e0f68e72 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -210,9 +210,6 @@ public slots: void setNoiseReduction(bool isNoiseGateEnabled, bool emitSignal = true); bool isNoiseReductionEnabled() const { return _isNoiseGateEnabled; } - void setWarnWhenMuted(bool isNoiseGateEnabled, bool emitSignal = true); - bool isWarnWhenMutedEnabled() const { return _warnWhenMuted; } - virtual bool getLocalEcho() override { return _shouldEchoLocally; } virtual void setLocalEcho(bool localEcho) override { _shouldEchoLocally = localEcho; } virtual void toggleLocalEcho() override { _shouldEchoLocally = !_shouldEchoLocally; } @@ -249,7 +246,6 @@ signals: void inputVolumeChanged(float volume); void muteToggled(bool muted); void noiseReductionChanged(bool noiseReductionEnabled); - void warnWhenMutedChanged(bool warnWhenMutedEnabled); void mutedByMixer(); void inputReceived(const QByteArray& inputSamples); void inputLoudnessChanged(float loudness, bool isClipping); @@ -369,7 +365,6 @@ private: bool _shouldEchoLocally; bool _shouldEchoToServer; bool _isNoiseGateEnabled; - bool _warnWhenMuted; bool _reverb; AudioEffectOptions _scriptReverbOptions; diff --git a/scripts/system/audioMuteOverlay.js b/scripts/system/audioMuteOverlay.js index 96f6d636dc..c597f75bca 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -1,144 +1,104 @@ +"use strict"; +/* jslint vars: true, plusplus: true, forin: true*/ +/* globals Tablet, Script, AvatarList, Users, Entities, MyAvatar, Camera, Overlays, Vec3, Quat, Controller, print, getControllerWorldLocation */ +/* eslint indent: ["error", 4, { "outerIIFEBody": 0 }] */ // // audioMuteOverlay.js // // client script that creates an overlay to provide mute feedback // // Created by Triplelexx on 17/03/09 -// Reworked by Seth Alves on 2019-2-17 // Copyright 2017 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 // -"use strict"; +(function () { // BEGIN LOCAL_SCOPE + var utilsPath = Script.resolvePath('../developer/libraries/utils.js'); + Script.include(utilsPath); -/* global Audio, Script, Overlays, Quat, MyAvatar, HMD */ + var TWEEN_SPEED = 0.025; + var MIX_AMOUNT = 0.25; -(function() { // BEGIN LOCAL_SCOPE + var overlayPosition = Vec3.ZERO; + var tweenPosition = 0; + var startColor = { + red: 170, + green: 170, + blue: 170 + }; + var endColor = { + red: 255, + green: 0, + blue: 0 + }; + var overlayID; - var lastShortTermInputLoudness = 0.0; - var lastLongTermInputLoudness = 0.0; - var sampleRate = 8.0; // Hz + Script.update.connect(update); + Script.scriptEnding.connect(cleanup); - var shortTermAttackTC = Math.exp(-1.0 / (sampleRate * 0.500)); // 500 milliseconds attack - var shortTermReleaseTC = Math.exp(-1.0 / (sampleRate * 1.000)); // 1000 milliseconds release - - var longTermAttackTC = Math.exp(-1.0 / (sampleRate * 5.0)); // 5 second attack - var longTermReleaseTC = Math.exp(-1.0 / (sampleRate * 10.0)); // 10 seconds release - - var activationThreshold = 0.05; // how much louder short-term needs to be than long-term to trigger warning - - var holdReset = 2.0 * sampleRate; // 2 seconds hold - var holdCount = 0; - var warningOverlayID = null; - var pollInterval = null; - var warningText = "Muted"; - - function showWarning() { - if (warningOverlayID) { - return; - } - - if (HMD.active) { - warningOverlayID = Overlays.addOverlay("text3d", { - name: "Muted-Warning", - localPosition: { x: 0, y: 0, z: -1.0 }, - localOrientation: Quat.fromVec3Degrees({ x: 0.0, y: 0.0, z: 0.0, w: 1.0 }), - text: warningText, - textAlpha: 1, - textColor: { red: 226, green: 51, blue: 77 }, - backgroundAlpha: 0, - lineHeight: 0.042, - dimensions: { x: 0.11, y: 0.05 }, - visible: true, - ignoreRayIntersection: true, - drawInFront: true, - grabbable: false, - parentID: MyAvatar.SELF_ID, - parentJointIndex: MyAvatar.getJointIndex("_CAMERA_MATRIX") - }); - } else { - var textDimensions = { x: 100, y: 50 }; - warningOverlayID = Overlays.addOverlay("text", { - name: "Muted-Warning", - font: { size: 36 }, - text: warningText, - x: Window.innerWidth / 2 - textDimensions.x / 2, - y: Window.innerHeight / 2 - textDimensions.y / 2, - width: textDimensions.x, - height: textDimensions.y, - textColor: { red: 226, green: 51, blue: 77 }, - backgroundAlpha: 0, - visible: true - }); - } - } - - function hideWarning() { - if (!warningOverlayID) { - return; - } - Overlays.deleteOverlay(warningOverlayID); - warningOverlayID = null; - } - - function startPoll() { - if (pollInterval) { - return; - } - pollInterval = Script.setInterval(function() { - var shortTermInputLoudness = Audio.inputLevel; - var longTermInputLoudness = shortTermInputLoudness; - - var shortTc = (shortTermInputLoudness > lastShortTermInputLoudness) ? shortTermAttackTC : shortTermReleaseTC; - var longTc = (longTermInputLoudness > lastLongTermInputLoudness) ? longTermAttackTC : longTermReleaseTC; - - shortTermInputLoudness += shortTc * (lastShortTermInputLoudness - shortTermInputLoudness); - longTermInputLoudness += longTc * (lastLongTermInputLoudness - longTermInputLoudness); - - lastShortTermInputLoudness = shortTermInputLoudness; - lastLongTermInputLoudness = longTermInputLoudness; - - if (shortTermInputLoudness > lastLongTermInputLoudness + activationThreshold) { - holdCount = holdReset; - } else { - holdCount = Math.max(holdCount - 1, 0); + function update(dt) { + if (!Audio.muted) { + if (hasOverlay()) { + deleteOverlay(); } - - if (holdCount > 0) { - showWarning(); - } else { - hideWarning(); - } - }, 1000.0 / sampleRate); - } - - function stopPoll() { - if (!pollInterval) { - return; - } - Script.clearInterval(pollInterval); - pollInterval = null; - hideWarning(); - } - - function startOrStopPoll() { - if (Audio.warnWhenMuted && Audio.muted) { - startPoll(); + } else if (!hasOverlay()) { + createOverlay(); } else { - stopPoll(); + updateOverlay(); } } + function getOffsetPosition() { + return Vec3.sum(Camera.position, Quat.getFront(Camera.orientation)); + } + + function createOverlay() { + overlayPosition = getOffsetPosition(); + overlayID = Overlays.addOverlay("sphere", { + position: overlayPosition, + rotation: Camera.orientation, + alpha: 0.9, + dimensions: 0.1, + solid: true, + ignoreRayIntersection: true + }); + } + + function hasOverlay() { + return Overlays.getProperty(overlayID, "position") !== undefined; + } + + function updateOverlay() { + // increase by TWEEN_SPEED until completion + if (tweenPosition < 1) { + tweenPosition += TWEEN_SPEED; + } else { + // after tween completion reset to zero and flip values to ping pong + tweenPosition = 0; + for (var component in startColor) { + var storedColor = startColor[component]; + startColor[component] = endColor[component]; + endColor[component] = storedColor; + } + } + // mix previous position with new and mix colors + overlayPosition = Vec3.mix(overlayPosition, getOffsetPosition(), MIX_AMOUNT); + Overlays.editOverlay(overlayID, { + color: colorMix(startColor, endColor, easeIn(tweenPosition)), + position: overlayPosition, + rotation: Camera.orientation + }); + } + + function deleteOverlay() { + Overlays.deleteOverlay(overlayID); + } + function cleanup() { - stopPoll(); + deleteOverlay(); + Audio.muted.disconnect(onMuteToggled); + Script.update.disconnect(update); } - - Script.scriptEnding.connect(cleanup); - - startOrStopPoll(); - Audio.mutedChanged.connect(startOrStopPoll); - Audio.warnWhenMutedChanged.connect(startOrStopPoll); - -}()); // END LOCAL_SCOPE +}()); // END LOCAL_SCOPE \ No newline at end of file From 19ac8cae316d620427a8e1847d964526e526548a Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 4 Mar 2019 14:18:10 -0800 Subject: [PATCH 10/27] culling mute state --- scripts/defaultScripts.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index e392680df9..bd7e79dffc 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -32,8 +32,7 @@ var DEFAULT_SCRIPTS_COMBINED = [ "system/firstPersonHMD.js", "system/tablet-ui/tabletUI.js", "system/emote.js", - "system/miniTablet.js", - "system/audioMuteOverlay.js" + "system/miniTablet.js" ]; var DEFAULT_SCRIPTS_SEPARATE = [ "system/controllers/controllerScripts.js", From a68aaaa7a46cf8ab6a4bf090a50bd3ecccc20c48 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 4 Mar 2019 16:06:50 -0800 Subject: [PATCH 11/27] adding timer and ui color changes --- interface/resources/qml/hifi/audio/Audio.qml | 33 ++++++++----------- .../qml/hifi/audio/LoopbackAudio.qml | 32 ++++++++++++++---- .../qml/hifi/audio/PlaySampleSound.qml | 6 ++-- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index aa64af22a3..bcbd253e24 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -98,13 +98,6 @@ Rectangle { Separator { } - RalewayRegular { - x: margins.paddings + muteMic.boxSize + muteMic.spacing; - size: 16; - color: "white"; - text: qsTr("Input Device Settings") - } - ColumnLayout { x: margins.paddings; spacing: 16; @@ -171,7 +164,7 @@ Rectangle { HiFiGlyphs { width: margins.sizeCheckBox text: hifi.glyphs.mic; - color: hifi.colors.primaryHighlight; + color: hifi.colors.white; anchors.left: parent.left anchors.leftMargin: -size/4 //the glyph has empty space at left about 25% anchors.verticalCenter: parent.verticalCenter; @@ -183,8 +176,8 @@ Rectangle { anchors.left: parent.left anchors.leftMargin: margins.sizeCheckBox size: 16; - color: hifi.colors.lightGrayText; - text: qsTr("CHOOSE INPUT DEVICE"); + color: hifi.colors.white; + text: qsTr("Choose input device"); } } @@ -233,6 +226,13 @@ Rectangle { } } } + LoopbackAudio { + x: margins.paddings + + visible: (bar.currentIndex === 1 && isVR) || + (bar.currentIndex === 0 && !isVR); + anchors { left: parent.left; leftMargin: margins.paddings } + } Separator {} @@ -247,7 +247,7 @@ Rectangle { anchors.verticalCenter: parent.verticalCenter; width: margins.sizeCheckBox text: hifi.glyphs.unmuted; - color: hifi.colors.primaryHighlight; + color: hifi.colors.white; size: 36; } @@ -257,8 +257,8 @@ Rectangle { anchors.leftMargin: margins.sizeCheckBox anchors.verticalCenter: parent.verticalCenter; size: 16; - color: hifi.colors.lightGrayText; - text: qsTr("CHOOSE OUTPUT DEVICE"); + color: hifi.colors.white; + text: qsTr("Choose output device"); } } @@ -300,12 +300,5 @@ Rectangle { (bar.currentIndex === 0 && !isVR); anchors { left: parent.left; leftMargin: margins.paddings } } - LoopbackAudio { - x: margins.paddings - - visible: (bar.currentIndex === 1 && isVR) || - (bar.currentIndex === 0 && !isVR); - anchors { left: parent.left; leftMargin: margins.paddings } - } } } diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index 3ecf09c948..ed416656b0 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -24,7 +24,7 @@ RowLayout { AudioScriptingInterface.setServerEcho(true); } } - function stopAudioLoopback () { + function stopAudioLoopback() { if (audioLoopedBack) { audioLoopedBack = false; AudioScriptingInterface.setServerEcho(false); @@ -33,6 +33,16 @@ RowLayout { HifiConstants { id: hifi; } + Timer { + id: loopbackTimer + interval: 8000; + running: false; + repeat: false; + onTriggered: { + stopAudioLoopback(); + } + } + Button { id: control background: Rectangle { @@ -42,27 +52,35 @@ RowLayout { gradient: Gradient { GradientStop { position: 0.2; - color: audioLoopedBack ? hifi.buttons.colorStart[hifi.buttons.blue] : hifi.buttons.colorStart[hifi.buttons.black]; + color: audioLoopedBack ? hifi.buttons.colorStart[hifi.buttons.blue] : "#FFFFFF"; } GradientStop { position: 1.0; - color: audioLoopedBack ? hifi.buttons.colorFinish[hifi.buttons.blue] : hifi.buttons.colorFinish[hifi.buttons.black]; + color: audioLoopedBack ? hifi.buttons.colorFinish[hifi.buttons.blue] : "#AFAFAF"; } } } contentItem: HiFiGlyphs { size: 14; - color: (control.pressed || control.hovered) ? (audioLoopedBack ? "black" : hifi.colors.primaryHighlight) : "white"; - text: audioLoopedBack ? hifi.glyphs.stop_square : hifi.glyphs.playback_play; + color: (control.pressed || control.hovered) ? (audioLoopedBack ? "black" : hifi.colors.primaryHighlight) : "#404040"; + text: audioLoopedBack ? hifi.glyphs.stop_square : hifi.glyphs.mic; } - onClicked: audioLoopedBack ? stopAudioLoopback() : startAudioLoopback(); + onClicked: { + if (audioLoopedBack) { + loopbackTimer.stop(); + stopAudioLoopback(); + } else { + loopbackTimer.restart(); + startAudioLoopback(); + } + } } RalewayRegular { Layout.leftMargin: 2; size: 14; color: "white"; - text: audioLoopedBack ? qsTr("Disable Audio Loopback") : qsTr("Enable Audio Loopback"); + text: audioLoopedBack ? qsTr("Stop testing your voice") : qsTr("Test your voice"); } } diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index cfe55af9c4..9889d2c6ca 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -64,11 +64,11 @@ RowLayout { gradient: Gradient { GradientStop { position: 0.2; - color: isPlaying ? hifi.buttons.colorStart[hifi.buttons.blue] : hifi.buttons.colorStart[hifi.buttons.black]; + color: isPlaying ? hifi.buttons.colorStart[hifi.buttons.blue] : "#FFFFFF"; } GradientStop { position: 1.0; - color: isPlaying ? hifi.buttons.colorFinish[hifi.buttons.blue] : hifi.buttons.colorFinish[hifi.buttons.black]; + color: isPlaying ? hifi.buttons.colorFinish[hifi.buttons.blue] : "#AFAFAF"; } } } @@ -77,7 +77,7 @@ RowLayout { // x: isPlaying ? 0 : 1; // y: 1; size: 14; - color: (control.pressed || control.hovered) ? (isPlaying ? "black" : hifi.colors.primaryHighlight) : "white"; + color: (control.pressed || control.hovered) ? (isPlaying ? "black" : hifi.colors.primaryHighlight) : "#404040"; text: isPlaying ? hifi.glyphs.stop_square : hifi.glyphs.playback_play; } From 0f2792930a3d4ba0edda89e8d0330081d16578b5 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 4 Mar 2019 19:28:12 -0800 Subject: [PATCH 12/27] adding switches and buttons to testing audio --- .../resources/qml/controlsUit/Switch.qml | 3 +- interface/resources/qml/hifi/audio/Audio.qml | 81 +++++++++++-------- .../qml/hifi/audio/LoopbackAudio.qml | 31 ++----- .../qml/hifi/audio/PlaySampleSound.qml | 35 ++------ 4 files changed, 62 insertions(+), 88 deletions(-) diff --git a/interface/resources/qml/controlsUit/Switch.qml b/interface/resources/qml/controlsUit/Switch.qml index 0961ef2500..0de95a7e70 100644 --- a/interface/resources/qml/controlsUit/Switch.qml +++ b/interface/resources/qml/controlsUit/Switch.qml @@ -27,6 +27,7 @@ Item { property string labelGlyphOnText: ""; property int labelGlyphOnSize: 32; property alias checked: originalSwitch.checked; + property string backgroundOnColor: "#252525"; signal onCheckedChanged; signal clicked; @@ -54,7 +55,7 @@ Item { } background: Rectangle { - color: "#252525"; + color: checked ? backgroundOnColor : "#252525"; implicitWidth: rootSwitch.switchWidth; implicitHeight: rootSwitch.height; radius: rootSwitch.switchRadius; diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index bcbd253e24..fc0c4d2d43 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -16,7 +16,7 @@ import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import stylesUit 1.0 -import controlsUit 1.0 as HifiControls +import controlsUit 1.0 as HifiControlsUit import "../../windows" import "./" as AudioControls @@ -27,6 +27,8 @@ Rectangle { property var eventBridge; property string title: "Audio Settings" + property int switchHeight: 16 + property int switchWidth: 40 signal sendToScript(var message); color: hifi.colors.baseGray; @@ -38,7 +40,7 @@ Rectangle { property bool isVR: AudioScriptingInterface.context === "VR" - property real rightMostInputLevelPos: 0 + property real rightMostInputLevelPos: 450 //placeholder for control sizes and paddings //recalculates dynamically in case of UI size is changed QtObject { @@ -98,58 +100,68 @@ Rectangle { Separator { } - ColumnLayout { - x: margins.paddings; - spacing: 16; + RowLayout { + x: 2 * margins.paddings; + spacing: columnOne.width; width: parent.width; // mute is in its own row - RowLayout { - spacing: (margins.sizeCheckBox - 10.5) * 3; - AudioControls.CheckBox { - id: muteMic - text: qsTr("Mute microphone"); - spacing: margins.sizeCheckBox - boxSize - isRedCheck: true; + ColumnLayout { + id: columnOne + spacing: 12; + x: margins.paddings + HifiControlsUit.Switch { + id: muteMic; + height: root.switchHeight; + switchWidth: root.switchWidth; + labelTextOn: "Mute microphone"; + backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.muted; - onClicked: { + onCheckedChanged: { AudioScriptingInterface.muted = checked; checked = Qt.binding(function() { return AudioScriptingInterface.muted; }); // restore binding } } - AudioControls.CheckBox { - id: stereoMic - spacing: muteMic.spacing; - text: qsTr("Enable stereo input"); + HifiControlsUit.Switch { + id: stereoInput; + height: root.switchHeight; + switchWidth: root.switchWidth; + labelTextOn: qsTr("Stereo input"); + backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.isStereoInput; - onClicked: { + onCheckedChanged: { AudioScriptingInterface.isStereoInput = checked; checked = Qt.binding(function() { return AudioScriptingInterface.isStereoInput; }); // restore binding } } } - RowLayout { - spacing: muteMic.spacing*2; //make it visually distinguish - AudioControls.CheckBox { - spacing: muteMic.spacing - text: qsTr("Enable noise reduction"); + ColumnLayout { + spacing: 12; + HifiControlsUit.Switch { + height: root.switchHeight; + switchWidth: root.switchWidth; + labelTextOn: "Noise Reduction"; + backgroundOnColor: "#E3E3E3"; checked: AudioScriptingInterface.noiseReduction; - onClicked: { + onCheckedChanged: { AudioScriptingInterface.noiseReduction = checked; checked = Qt.binding(function() { return AudioScriptingInterface.noiseReduction; }); // restore binding } } - AudioControls.CheckBox { - spacing: muteMic.spacing - text: qsTr("Show audio level meter"); + + HifiControlsUit.Switch { + id: audioLevelSwitch + height: root.switchHeight; + switchWidth: root.switchWidth; + labelTextOn: qsTr("Audio Level Meter"); + backgroundOnColor: "#E3E3E3"; checked: AvatarInputs.showAudioTools; - onClicked: { + onCheckedChanged: { AvatarInputs.showAudioTools = checked; checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding } - onXChanged: rightMostInputLevelPos = x + width } } } @@ -203,7 +215,7 @@ Rectangle { width: parent.width - inputLevel.width clip: true checkable: !checked - checked: bar.currentIndex === 0 ? selectedDesktop : selectedHMD; + checked: bar.currentIndex === 0 ? selectedDesktop : selectedHMD; boxSize: margins.sizeCheckBox / 2 isRound: true text: devicename @@ -215,7 +227,7 @@ Rectangle { } } } - InputPeak { + AudioControls.InputPeak { id: inputLevel anchors.right: parent.right peak: model.peak; @@ -225,8 +237,11 @@ Rectangle { AudioScriptingInterface.devices.input.peakValuesAvailable; } } + Component.onCompleted: { + console.log("width " + rightMostInputLevelPos); + } } - LoopbackAudio { + AudioControls.LoopbackAudio { x: margins.paddings visible: (bar.currentIndex === 1 && isVR) || @@ -293,7 +308,7 @@ Rectangle { } } } - PlaySampleSound { + AudioControls.PlaySampleSound { x: margins.paddings visible: (bar.currentIndex === 1 && isVR) || diff --git a/interface/resources/qml/hifi/audio/LoopbackAudio.qml b/interface/resources/qml/hifi/audio/LoopbackAudio.qml index ed416656b0..8ec0ffc496 100644 --- a/interface/resources/qml/hifi/audio/LoopbackAudio.qml +++ b/interface/resources/qml/hifi/audio/LoopbackAudio.qml @@ -14,7 +14,7 @@ import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import stylesUit 1.0 -import controlsUit 1.0 as HifiControls +import controlsUit 1.0 as HifiControlsUit RowLayout { property bool audioLoopedBack: AudioScriptingInterface.getServerEcho(); @@ -43,29 +43,9 @@ RowLayout { } } - Button { - id: control - background: Rectangle { - implicitWidth: 20; - implicitHeight: 20; - radius: hifi.buttons.radius; - gradient: Gradient { - GradientStop { - position: 0.2; - color: audioLoopedBack ? hifi.buttons.colorStart[hifi.buttons.blue] : "#FFFFFF"; - } - GradientStop { - position: 1.0; - color: audioLoopedBack ? hifi.buttons.colorFinish[hifi.buttons.blue] : "#AFAFAF"; - } - } - } - contentItem: HiFiGlyphs { - size: 14; - color: (control.pressed || control.hovered) ? (audioLoopedBack ? "black" : hifi.colors.primaryHighlight) : "#404040"; - text: audioLoopedBack ? hifi.glyphs.stop_square : hifi.glyphs.mic; - } - + HifiControlsUit.Button { + text: audioLoopedBack ? qsTr("STOP TESTING YOUR VOICE") : qsTr("TEST YOUR VOICE"); + color: audioLoopedBack ? hifi.buttons.red : hifi.buttons.blue; onClicked: { if (audioLoopedBack) { loopbackTimer.stop(); @@ -81,6 +61,7 @@ RowLayout { Layout.leftMargin: 2; size: 14; color: "white"; - text: audioLoopedBack ? qsTr("Stop testing your voice") : qsTr("Test your voice"); + font.italic: true + text: audioLoopedBack ? qsTr("Speak in your input") : ""; } } diff --git a/interface/resources/qml/hifi/audio/PlaySampleSound.qml b/interface/resources/qml/hifi/audio/PlaySampleSound.qml index 9889d2c6ca..b9d9727dab 100644 --- a/interface/resources/qml/hifi/audio/PlaySampleSound.qml +++ b/interface/resources/qml/hifi/audio/PlaySampleSound.qml @@ -14,7 +14,7 @@ import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 import stylesUit 1.0 -import controlsUit 1.0 as HifiControls +import controlsUit 1.0 as HifiControlsUit RowLayout { property var sound: null; @@ -55,32 +55,9 @@ RowLayout { HifiConstants { id: hifi; } - Button { - id: control - background: Rectangle { - implicitWidth: 20; - implicitHeight: 20; - radius: hifi.buttons.radius; - gradient: Gradient { - GradientStop { - position: 0.2; - color: isPlaying ? hifi.buttons.colorStart[hifi.buttons.blue] : "#FFFFFF"; - } - GradientStop { - position: 1.0; - color: isPlaying ? hifi.buttons.colorFinish[hifi.buttons.blue] : "#AFAFAF"; - } - } - } - contentItem: HiFiGlyphs { - // absolutely position due to asymmetry in glyph -// x: isPlaying ? 0 : 1; -// y: 1; - size: 14; - color: (control.pressed || control.hovered) ? (isPlaying ? "black" : hifi.colors.primaryHighlight) : "#404040"; - text: isPlaying ? hifi.glyphs.stop_square : hifi.glyphs.playback_play; - } - + HifiControlsUit.Button { + text: isPlaying ? qsTr("STOP TESTING YOUR SOUND") : qsTr("TEST YOUR SOUND"); + color: isPlaying ? hifi.buttons.red : hifi.buttons.blue; onClicked: isPlaying ? stopSound() : playSound(); } @@ -88,7 +65,7 @@ RowLayout { Layout.leftMargin: 2; size: 14; color: "white"; - text: isPlaying ? qsTr("Stop sample sound") : qsTr("Play sample sound"); + font.italic: true + text: isPlaying ? qsTr("Listen to your output") : ""; } - } From a00d6ec9f6565e77db30a32211ed5eaa285aba50 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Mon, 4 Mar 2019 19:42:30 -0800 Subject: [PATCH 13/27] removing debug statement --- interface/resources/qml/controlsUit/Switch.qml | 4 ++-- interface/resources/qml/hifi/audio/Audio.qml | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/interface/resources/qml/controlsUit/Switch.qml b/interface/resources/qml/controlsUit/Switch.qml index 0de95a7e70..4e1c21c456 100644 --- a/interface/resources/qml/controlsUit/Switch.qml +++ b/interface/resources/qml/controlsUit/Switch.qml @@ -41,10 +41,10 @@ Item { onClicked: rootSwitch.clicked(); hoverEnabled: true - topPadding: 3; + topPadding: 1; leftPadding: 3; rightPadding: 3; - bottomPadding: 3; + bottomPadding: 1; onHoveredChanged: { if (hovered) { diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index fc0c4d2d43..1869fb9b3e 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -108,7 +108,7 @@ Rectangle { // mute is in its own row ColumnLayout { id: columnOne - spacing: 12; + spacing: 24; x: margins.paddings HifiControlsUit.Switch { id: muteMic; @@ -138,7 +138,7 @@ Rectangle { } ColumnLayout { - spacing: 12; + spacing: 24; HifiControlsUit.Switch { height: root.switchHeight; switchWidth: root.switchWidth; @@ -237,9 +237,6 @@ Rectangle { AudioScriptingInterface.devices.input.peakValuesAvailable; } } - Component.onCompleted: { - console.log("width " + rightMostInputLevelPos); - } } AudioControls.LoopbackAudio { x: margins.paddings From f1cf11de8546cf367671f6ae107efc1401e6c1e0 Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 5 Mar 2019 10:53:55 -0800 Subject: [PATCH 14/27] moving gain slider to audio settings --- interface/resources/qml/hifi/NameCard.qml | 28 +++----- interface/resources/qml/hifi/audio/Audio.qml | 75 ++++++++++++++++++-- 2 files changed, 77 insertions(+), 26 deletions(-) diff --git a/interface/resources/qml/hifi/NameCard.qml b/interface/resources/qml/hifi/NameCard.qml index 242ca5ab57..646fc881e1 100644 --- a/interface/resources/qml/hifi/NameCard.qml +++ b/interface/resources/qml/hifi/NameCard.qml @@ -376,7 +376,7 @@ Item { } FiraSansRegular { id: nameCardConnectionInfoText - visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && !isMyCard + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" width: parent.width height: displayNameTextPixelSize size: displayNameTextPixelSize - 4 @@ -412,7 +412,7 @@ Item { } FiraSansRegular { id: nameCardRemoveConnectionText - visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && !isMyCard + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" width: parent.width height: displayNameTextPixelSize size: displayNameTextPixelSize - 4 @@ -425,7 +425,7 @@ Item { } HifiControls.Button { id: visitConnectionButton - visible: selected && !isMyCard && pal.activeTab == "connectionsTab" && !isMyCard + visible: selected && !isMyCard && pal.activeTab == "connectionsTab" text: "Visit" enabled: thisNameCard.placeName !== "" anchors.verticalCenter: nameCardRemoveConnectionImage.verticalCenter @@ -450,7 +450,7 @@ Item { // Style radius: 4 color: "#c5c5c5" - visible: (isMyCard || (selected && pal.activeTab == "nearbyTab")) && isPresent + visible: (!isMyCard && (selected && pal.activeTab == "nearbyTab")) && isPresent // Rectangle for the zero-gain point on the VU meter Rectangle { id: vuMeterZeroGain @@ -481,7 +481,7 @@ Item { id: vuMeterBase // Anchors anchors.fill: parent - visible: isMyCard || selected + visible: !isMyCard && selected // Style color: parent.color radius: parent.radius @@ -489,7 +489,7 @@ Item { // Rectangle for the VU meter audio level Rectangle { id: vuMeterLevel - visible: isMyCard || selected + visible: !isMyCard && selected // Size width: (thisNameCard.audioLevel) * parent.width // Style @@ -525,7 +525,7 @@ Item { anchors.verticalCenter: nameCardVUMeter.verticalCenter; anchors.left: nameCardVUMeter.left; // Properties - visible: (isMyCard || (selected && pal.activeTab == "nearbyTab")) && isPresent; + visible: (!isMyCard && (selected && pal.activeTab == "nearbyTab")) && isPresent; minimumValue: -60.0 maximumValue: 20.0 stepSize: 5 @@ -572,19 +572,7 @@ Item { implicitHeight: 16 } } - RalewayRegular { - // The slider for my card is special, it controls the master gain - id: gainSliderText; - visible: isMyCard; - text: "master volume"; - size: hifi.fontSizes.tabularData; - anchors.left: parent.right; - anchors.leftMargin: 8; - color: hifi.colors.baseGrayHighlight; - horizontalAlignment: Text.AlignLeft; - verticalAlignment: Text.AlignTop; - } - } + } function updateGainFromQML(avatarUuid, sliderValue, isReleased) { if (Users.getAvatarGain(avatarUuid) != sliderValue) { diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index 1869fb9b3e..efbf663838 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -11,7 +11,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // -import QtQuick 2.5 +import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.3 @@ -26,6 +26,8 @@ Rectangle { HifiConstants { id: hifi; } property var eventBridge; + // leave as blank, this is user's volume for the avatar mixer + property var myAvatarUuid: "" property string title: "Audio Settings" property int switchHeight: 16 property int switchWidth: 40 @@ -82,16 +84,16 @@ Rectangle { }); } - function disablePeakValues() { - root.showPeaks = false; - AudioScriptingInterface.devices.input.peakValuesEnabled = false; + function updateMyAvatarGainFromQML(sliderValue, isReleased) { + if (Users.getAvatarGain(myAvatarUuid) != sliderValue) { + Users.setAvatarGain(myAvatarUuid, sliderValue); + } } Component.onCompleted: enablePeakValues(); - Component.onDestruction: disablePeakValues(); - onVisibleChanged: visible ? enablePeakValues() : disablePeakValues(); Column { + id: column spacing: 12; anchors.top: bar.bottom anchors.bottom: parent.bottom @@ -305,6 +307,67 @@ Rectangle { } } } + + Item { + id: gainContainer + x: margins.paddings; + width: parent.width - margins.paddings*2 + height: gainSliderTextMetrics.height + + HifiControlsUit.Slider { + id: gainSlider + anchors.right: parent.right + height: parent.height + width: 200 + minimumValue: -60.0 + maximumValue: 20.0 + stepSize: 5 + value: Users.getAvatarGain(myAvatarUuid) + onValueChanged: { + updateMyAvatarGainFromQML(value, false); + } + onPressedChanged: { + if (!pressed) { + updateMyAvatarGainFromQML(value, false); + } + } + + MouseArea { + anchors.fill: parent + onWheel: { + // Do nothing. + } + onDoubleClicked: { + gainSlider.value = 0.0 + } + onPressed: { + // Pass through to Slider + mouse.accepted = false + } + onReleased: { + // the above mouse.accepted seems to make this + // never get called, nonetheless... + mouse.accepted = false + } + } + } + TextMetrics { + id: gainSliderTextMetrics + text: gainSliderText.text + font: gainSliderText.font + } + RalewayRegular { + // The slider for my card is special, it controls the master gain + id: gainSliderText; + text: "Avatar volume"; + size: 16; + anchors.left: parent.left; + color: hifi.colors.white; + horizontalAlignment: Text.AlignLeft; + verticalAlignment: Text.AlignTop; + } + } + AudioControls.PlaySampleSound { x: margins.paddings From 2020b757a3b536e070a0a8c9cb1dd25344a88c7d Mon Sep 17 00:00:00 2001 From: Wayne Chen Date: Tue, 5 Mar 2019 10:58:26 -0800 Subject: [PATCH 15/27] adding mic glyph --- .../hifi-glyphs-1.34/fonts/hifi-glyphs.woff | Bin 21976 -> 0 bytes .../fonts/hifi-glyphs.eot | Bin 34130 -> 34726 bytes .../fonts/hifi-glyphs.svg | 9 ++- .../fonts/hifi-glyphs.ttf | Bin 33952 -> 34548 bytes .../hifi-glyphs-1.38/fonts/hifi-glyphs.woff | Bin 0 -> 22380 bytes .../icons-reference.html | 76 ++++++++++++------ .../styles.css | 27 ++++--- interface/resources/fonts/hifi-glyphs.ttf | Bin 34396 -> 34548 bytes 8 files changed, 74 insertions(+), 38 deletions(-) delete mode 100644 interface/resources/fonts/hifi-glyphs-1.34/fonts/hifi-glyphs.woff rename interface/resources/fonts/{hifi-glyphs-1.34 => hifi-glyphs-1.38}/fonts/hifi-glyphs.eot (83%) rename interface/resources/fonts/{hifi-glyphs-1.34 => hifi-glyphs-1.38}/fonts/hifi-glyphs.svg (96%) rename interface/resources/fonts/{hifi-glyphs-1.34 => hifi-glyphs-1.38}/fonts/hifi-glyphs.ttf (83%) create mode 100644 interface/resources/fonts/hifi-glyphs-1.38/fonts/hifi-glyphs.woff rename interface/resources/fonts/{hifi-glyphs-1.34 => hifi-glyphs-1.38}/icons-reference.html (97%) rename interface/resources/fonts/{hifi-glyphs-1.34 => hifi-glyphs-1.38}/styles.css (97%) diff --git a/interface/resources/fonts/hifi-glyphs-1.34/fonts/hifi-glyphs.woff b/interface/resources/fonts/hifi-glyphs-1.34/fonts/hifi-glyphs.woff deleted file mode 100644 index 6d469701366c67d5aa8123d2be5b314f3f27b65a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21976 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 diff --git a/interface/resources/fonts/hifi-glyphs-1.34/fonts/hifi-glyphs.eot b/interface/resources/fonts/hifi-glyphs-1.38/fonts/hifi-glyphs.eot similarity index 83% rename from interface/resources/fonts/hifi-glyphs-1.34/fonts/hifi-glyphs.eot rename to interface/resources/fonts/hifi-glyphs-1.38/fonts/hifi-glyphs.eot index 6cdc2487a68d6377f9fdd2fe51ca5c1b9a27b7ad..88936e6c5128623e596c932f5a46a40cd4842a23 100644 GIT binary patch delta 2035 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 diff --git a/interface/resources/fonts/hifi-glyphs-1.34/fonts/hifi-glyphs.svg b/interface/resources/fonts/hifi-glyphs-1.38/fonts/hifi-glyphs.svg similarity index 96% rename from interface/resources/fonts/hifi-glyphs-1.34/fonts/hifi-glyphs.svg rename to interface/resources/fonts/hifi-glyphs-1.38/fonts/hifi-glyphs.svg index de91dcae71..a3a1aac8a9 100644 --- a/interface/resources/fonts/hifi-glyphs-1.34/fonts/hifi-glyphs.svg +++ b/interface/resources/fonts/hifi-glyphs-1.38/fonts/hifi-glyphs.svg @@ -14,7 +14,6 @@ - @@ -97,7 +96,6 @@ - @@ -137,7 +135,6 @@ - @@ -156,4 +153,10 @@ + + + + + + diff --git a/interface/resources/fonts/hifi-glyphs-1.34/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs-1.38/fonts/hifi-glyphs.ttf similarity index 83% rename from interface/resources/fonts/hifi-glyphs-1.34/fonts/hifi-glyphs.ttf rename to interface/resources/fonts/hifi-glyphs-1.38/fonts/hifi-glyphs.ttf index c27553333bda29cdfd06e4c1ebdd964b83ff6175..dae388d2eb6798d7d0d0f4135609908d83141764 100644 GIT binary patch delta 2032 zcmY+FYfM|`8OQ&xZH|wx*v3A-U=w3};n*18?}l7T2oMOQaadat5@HC1*uhDhl%#-Z zl9Dx{3FS02O|vPR&{nZDOGmUtQ>C?2R_#NlRcOk(Et8_DtEPR3I+dYynw04n)~QE2 z|M!26p67j^-}B*lZ@ns5eN9jT0ieJ%gy?B)&2&x9%-#oPJ_TUj(Xzc2zXk=&+y`Lo z-s1~)zu!3pkmmsOX7dAu`8!v80l^o5;N0-Q$->j85CSuI0no#_Gl&1Ae)1(i-3DG< z92puI)C{}-3g8id=*Y-WX+Y#Bj{%|uU?0gB$7?&Q&j92uK$>%7*@3U#(|#YQC;}v& z9~dv-E7H$ZL0Lqrbp$e^))#MI*iXAoLS5kwI~97z^q7gf>7oFIQ zT?m(8h7clDfPxq_BzOx_$RLLTN>oAxH8iNgMQEV|13e5dVmErR2N^tzZ{aySj~CE~ zH?a@<(T@Qf!XSn)j1e5cD2^hBJdR-u1sul-oWv=d#)}xo8JtDnB}`xvQ&>O=(|8%@ zZ~^D>3TE(ayoOhC370X8E8y@3uHgo*bNwP$e2S5~IB;-F zIuQ&7y4%`&Ox2dxug+x;*KD%Z+Uo2Mr>}mVb^9BtZr&9(3q=)_n3lXHmB|&# zN|jnuby2Hh^akVZ-aQ%aAEJr*3Kf^HP?ie9N1{Q|QiYPDsgJ~?;yZK)y(EF8Q!*=g zC{;@R(hljY^uF{H>Az%+vQgO&Lb5rxEDpNIA z^|3asUC{nbcUJcSvyXYetm)JGOZpXq$xtxdFsv9g#wp`%<9*`;;}a88GPRiIO`lgY z)os<+&HF4;%Ser;=JcjFtd#Xu?P1%)x@kM?yBw5b%5llD;P|Cu&3VQ7iOc4ib$wRf zR{t?O?%w7uxZm->Gu=?r@NC0W!_U1fK7sFw@1}3bPy56E8UJ4b+Q3NQgP=8dD0nY) zAxwpDNA^WNkEWw_OR?tIt=QwZHQpJ|#eWiCNwg#$BsIywM(#lg6GDK)=8S}6(NIv&=(J)=XrpvmCK!!`T~0Cg zi(x|x5$qg|#M-;U;Yh4KmC2mWxY@lT7pK_-`Tmj!eb`JclMhi14^n6ch|?7bJ3YiErYwZwMXP60Bp7WX zVOKa7O_89Uv5<0vUQ2n1&Dli4{BK6*vhm?es2uDhV`5dDrC5tus<2orY>tipUZXKm zMvX=&&WVM~_L4DYwA*4_j@H*L&E!Nu|-KOi@BE z8|-$2hZng*663RC5(Pg^c#!{)51~LDZ9wcBrP%)mw86EG6f2dXc;K(OLJ_g&G*)`f!1dU<@yBaMXby*|iyz1;L@kRcwu zTBX;k)cT&dyOEa5>GQNqMmM_Sa$4=%=98-Aq|c{f7?sb(Hu7tuFkYXg?J|{Iu9Dfs zX*NmAh&Ep1cGtvcnH1n!y7zOn6Hb>C7uQENM3CX54POBn{=VTU$cU0VHc{*9M1mjd zA_+ubaA(6;fWgX!r@&y1tDMwxznM5t8G#oecoBpj0VKH1lU6$74S9q9K!SU3(#c(& zY~%hgX(exS|DL=qf{pCt7N*)L{D>6DaqhFJR*GCB3*=4CUFujKFRcoekDp5t?#Jhs z!iwz3Q1+-dd*nnmH#A6{7&`0q2dLaw_UO delta 1476 zcmX|>TTGK@7{`Ba`?VZOKcJKr%Bd7=5n5?$Ddp%1@q~aUJPg6g!KxsLh|U8ASDAut zj?V*G7Ncf|M5kM1G0qs{#b$Ql7GoA;vRybYj9H9vi+bheQY3qm_y7Eo_sNszfAN1` z9#@?^stO|jJwvEztgT(W{6K%F0U{9~b5YI0T7EH*5hDb9IyQS;>&X~s0T+mLy?jxa&V0-c!X%6r^;17j5!a>O` z9)U0d`JwLqy}7NHRuJEU2z2$d1;4qi^ns=wgtt4mx0lzVCHfX!Xh*QSV_T=;XE5K)y$!u`P8tGTAVDSfJH1O$Y$nIMLDza;iZu!EM)~5WRlHE#!=`XL=IMRv5`kU zGqB^p%}h>FND&@f%%Ye&npnD%P={4ZKG)8)>1HO|;QYCtKLcHn!76H#_K|m!0&ni+*;qhrPVdKK65f zqJs<)W{ATKbBGU!@F61{;V5IA;26jGh>!V%)0|PdBu9(CG*DJCP`RLbPW}9vg|*IQ z1&bC3H_xjopY8KDE?K%FBQyKtxY7~IvF6(H@@Lo`?wO|wi#)Db#dS@~=QfmNsU?jz zCN?f!m!OvuladW7#?e%hIW66?a_#Ch%3so`a!@0O52^1+Ug?GgO}n-wCNt)CtQ31L zj<}|{3vrL*o$>AQBk||rAM0eDU01J*=x*!YBplPb_1EMY`9xx2(#zyf@&kk4u+0!L z{GH-WxtQ|AXfuu*pQTo(-ZPm@SIri4oq62+D6Jr^H|9&^QX1h`a1V!o-^-3ep7xV|Ne|oyUH%xE%sXb zK8L}v+VN#UQbDNT?##xSZwl`ezASPUMVylJg7bk(<*Ii5R6OD~xUKG4?qlxjo_J5E zXTtN$TjTxQ`($=tcBjwpyI#`b&yoET{^zC5r7r?21LJ}Ffq%=g%i7B#Wv|Nz%71{# zueIy7VKrj}#U62)L~@u1!e=iMHf@$Lnkt0XU*a%oi-gTyC4Bx0;Wee2efB(Cs!^L~ zvzPdyWHP5lONH>3_=-eHwGh>|bZxqAO45yTjBK2el`$H9somk%CsaBcW%-6)-{`6; z6n)Xotx-`$8$(gqq>0trOH1u~y=#HSChPUGO*4N|rN1;!d3CUP@^XDBOWD1@c`~)@ zrmUv4HJF0b1YV7gaUz7H=}-iB>rLa_}^NMac_<<`(* zt>_gyxuhfv*DC9VmrCmHt!Z+0015U z001BW#sCCQQ!g?A08?lH002q=003Z7X-+g_ZDDW#08_L800Dsj00M$x=wFFwWnp9h z097~u001rk001@;XDa1rXk}pl099xJ0015U001NeFah3ZZFG1509A+p008U&00A~1 zARAY0VR&!=09JGW000I6000I6nNR?3VQpmq09Jee007+p00FRTDS@qxaFs_v}J2wtd62kbs5V9KxB@_dug%YX(Qvw)5H!iX) zOS0ALN?L8-X?Ob07OlE0$=!0pHU`rY3`r;tFoBSSB#%H6(qCRO`u>ymzW=#*R<<#T%~ z=Ki@-Imw4ls=y~z1{Rck@mnYVZ;X!>PGZ;LxhLjq<}`o(XHP3D``l^l;O9>(`_gGm zFP&UgAl+1!eWr{nJFD!{va8E(F1w>_S=s$%50|YhTT@n7w!W;TtfMSk7As4aWy*48 zlV!7I&y;<4A&|_&)19*LRukM&F&j2Yjo1&AuLApD*dte70|sZ=dg=?`hu= z->bf(z90Mk)%QE!pMC$k{G{@e%LC=-mtS6fL;20+%gXOBf2e$Uc}@BH^0xB6@?<$K z*URnlLitGfbotiu{pAPCpDKT*{JHWM%3m&jr~K#TzbyZK`Jc<(ii(O)SA4eOOBH8S zoL#ZF;);rEDsHH_rQ(i?dnz8RSW&UIVnaoHMSn%CLa5LyvK5mRJ1P!UJXi7EikBIej zl{1ywEB93%sC=&S<;u6~o7P^kc2P%7du?s=hT7gWE!FL7>#LjBE~;s1?rd*qY^-hX zsIRSF+gjh!T-&jzsiCH>x~aCcdhPLxU9F8R)oY=PhStu`+E8ar*P7an*81A^+CI45 z*icvB*;Bg){uf~r+um8bwzZ)d{*V1^ zukC2)t6fB3YJ}#bZdZHlqS{a^TxqW*Loe!Rs%~ubdZERphUSJQXw_65`q=lePS-Yc z%y+b=tFyDExzyj^U#|faRJV52ded9f(b>{kDtcqj|LAHim6~gNC~L24Y42*HrN)-J z77wo0#_AdXrhUVjmQYPgQ&U%SLuYSYd-a;q^g3#*+iU7i0OAv)dzh|iSzBA(UENvT zUftf_($fKrz3*=C8+vX9taW%(IyUB_#WfA>HI22Dx~(k@&7HOFt&LqBi#po6s$r}a z@4FfZ2Gg!3x4rWEu8z)zb-mt?#@cnAUa6fBc1?AAhu7_*j>hVa`i|b_8epTwhUN{8 zEj87h4K2+z04E^G`)XX*(zv#^eNkgwsnF5XSleCO2vcilUe{6$?ABS|(o|auGimRr z?d)u5uIuP*hn`#3tOvGPRM+0p)mqck)X`nHwzjS{)CAx+Eg~$pwz{UK7TVK6TdUhU zfmdofx?4iEjUC+$9Sv(5YnuRbjUMV2T|$wyXl=9i-PPLJ&{PW}HFrSU+UA8U}Rw1MXhkLp^;weZfRU}G5J_bJ}x02my(am$VbS#74mL|yxSqKLTJ%t9re|e zeLHLFYil-iG&TShI$B#A3C?OeTDsb6YJp_EYpQEDEFuNMaU~L!AO}#$q9M%}Ev^mK z072_&>(|u~ye(SX+TH-wY8L?&pi);maM+^OhMFE=zuL}@T40Fkj*gy|_O;>(sUXC4pW!oomM;V1JnPH(^1gy^ z&{y;g`G$QXzER(pZ`?QGoAhn;P5GvMGa&Le`?mPD`nLJD`*!$t`euE*e7k*ne0xC^ zJnq}?I{*sd3Ev^#lfI{Xhe0(w<9pWkobP$xw|(F7y#R{hMc+%l@A|&y`@ZjG-z%Ut zUMu^G?_Yec``+-q2`c0*-w%9m``+>W(D$zIN1#xC;(O2cQ{T^gKL^$FzV8>l4}2f` ze(C#_@7KPMe82Ji7Szq}eSh%%(f23czk%ZU_p)B!UwnV{IlejH|MLAmKG*j*-+%c2 z6BJTexv#vuyrR6aoGJI0v*oD#Q{@ZFKMjiNGv!s~pDq7f`R74peWCo6a;|(~`9GEa zbNLs`Pc8pa`DtZ?M0=KlcKns^-^<^r_;%%C<`QPo-^5O#%Rc4*)ZC{BPD-7$`7=kV zl+S+Qv(J3)y3hT~=P&$>S%#^@ZRU8o%(vQ+~7Xihuh2KYjSmi7)=; z%fAo&IJo00O<(!WS6)6nf5ypYeC>>FU%l|m%g_AJvtB%hJ7>?i!E=9c-lFqH&O2~^ z@O+hFxOTWDIyrth*djHanr9WEwo7+Bp`>A(be)m0huef{tJ!jl=-aS8AmS6VUd;9MD z{C&@T>xOUbz5m?@UVrG0hu?eTSC5V??|N)t#T_e#R(!bPuPZNIxohQbSM99!S3kAp zk~MZs=h~as?qB=As^&iL1i5GT?|&wEkbQRb{s#lD=6`V2>IZ`>*#{3)KO1<}KZf%o zxuRj3SzXm7E3T)r{rSETjP8Gjll@^F561`633y|4n}6+wZM&hTJ=?bK2{@X6&$h<3 z&{OS(4Qm6p_yrs<@>aYUFZN?pH5Z&)$Zel#Ss!!}yS}w~eZc)(>yQ1G$tRO(J{5HD z_NP)tCSw|z;E!h9FZkEDZr&br5W9V5)AoS#l6wmWKSlc`);t?ZJ20?X5E~ z@z?wtT3go#uVdHGv~CMH*H=2P`A3W45M1dE_lHP@-f(dg>W>zSFhBKcT>G{u_Qr+( zr%$J)orB@GE$sCl8+zDZ<(%$}a~5FLN}H*m=qEEwn!FXcjT-9A2g+#B5{qmLaM zoggQ1!q@_gotPiHYVPaKAGxXVP>$l zufpA{`CQZCVZeM zvWgYtoX_rT+0qa=-4Aw0eej#EkBeE!Twv5G3;AQIWGv|X=PlgpY%Y0QM=ASIpIACbjk(#7hY=kLb7xyZO+iI!v{r`4|{wYV9}_KtTxj>qwkRT!M! zEIch+$Vs@bxyHq;7G^Nky9?dJ&C}s67)}0>b9$T+cRee!7%NdZEJuXicr2NW^AX&E z`?B$oWKk|+jO0A?Dxf$+>18H^?p=f~gkYJVbJyK)!}`RH6uEH^X+fmR{0$H<(4xv| zbx!3J5sQ*6OYnzLzaM^SvWi;Yy%D#5gMrJU=v|gn83-k7ii+NI1{g(ERSl!8U&E@T zNNBCQkb4eyyv0B*1*@`zp-<#=M>soX^y5zCvaA4qAU)_1I+6=aI?W4dA;HJQDDJ}% zGoFpx!XO^TqgHV+H<%qYrvN_+))`zBbG#*3NiBvWct99PN;13@Ng+@nevkpkbWI(x zM+|6=i|K3>UFLq8!wEHI2s&>`HXg(|J!5BVy?}=i&W2>BM;;K8e2N#6IEI|N&)_hd zH8a^Dp1{r)c>-nmOgw_;Db=$wr|ycoGJbaCs}bw4UUl<9oC*@3|q z;CHqf6Y?|NPp;clyZh0xhXD$pAVyuOrt7i$DHKVwDJ2arlx6V?*7JpZ=7?F$GwaTBo41Vavi8MN1F2qq&A|Psd-1*aeye)0&loUcmQ^#lZc7u|+&tU6JC)9+ z2l*ksD2$1tXi8?X4-7R7CZ3U>8QZ%DERoO8Ar8bX4(1AkFy8SdW-{0mv=mzHKFyu6;O2#2U9e=K_x(sWXJSLubwgL7 zfKAa9bl?g#jnkNy2xE#+M3~poa9uDY6Q=$v?#u;mRGzWm%}Vdv+4Ck`xq0DP(69IH zywt@(Z<-E&Dip9O8KmR1#zV(xk#sEaq~o;8la66v=-3_R&VnAl3O#$@RnF5+o}1_$ z?(XjE>*^lv8y_DT9uKSqA^76qr(S&N(Wmad=g~*+zUQe&Ukp0;RUVwJTe+%^yk`#_ zoSi)obS^p%Sa(<%fwhCkVy997Kg1F#kR(#TeQ6;`2M_{QGWalPF$Sc0l8S+046S6~ zQqX2P)>KW)=L`9K-Y~Jvn`t9~(`i&!$M9*KFnCSWC171Q<62N#2Pr`6<_o5Q?X=00 z+I$rLB2lkCDm8G<*f?5dzoB&C{{aYo09cn0~hLy6s8lZvZ zYxtbk$gqm4c>|)lSte}~5P2Ra%ru&HSMDqNx+D1Ab%}4T4 zRBBK)cgNhR+}R7_iOb`RrRFvVq9Y)gPN(54z#C^#jiVK6a?6F8xPtp%!8%3zek82}2UGK?UQ zyTIzIs0tcj%0PA{VO6j`G+i+;%s|#e4enwdy*k4|p8}smJEB+SZ5Fx{GuXnG3hscZ zS=h!|(SqU4Gz^b=8aZr2S0(m@&LmyX1n3HwQxYh9$~;Cq78F4TLh0E2Z=s>edDvOc zZQXGG`HU#?A}^yQ;WTFSX?;qYQfwXc(AF(mwr<^uCc3Y_dNjm1HO@Y6WBX*sWJf+n z{&WCGFwlM9VPLgjL5Rj;v3ML!y0>$$jJ@(e>Q*@^CFP_HG6>^=!GXc>prlHgq{Olj z9E+ol4ki&-!r2&lyqW=NAS+TR8IpU975T=;^(S#gwbYCPs!Y=QhWdy5hR`F2A3prZ z)BQv8RB^aCyipw)0CbD0g#LK;^Us4wCK`=ExUn4x!u|J8b^;@5U1=X4| zlXOABDhfJ}uHYi@HxRLi!IVaO_AqHuBpAGT?jdeuFgg%)zr*5iC>}|p#SKhC5O7jK z7`tdUtUv{(79c82REVUP6%lMeQAS^@WqJql#h~+T78i@TVFP`*lPOw2X%RW7mH~|` zYoKHmMTLcExIgFK-@z12O(Rwmv{4i+E9jC2f;%Q5@&cBS`)w9%S0&)gJr1hl+p==; z3AbfLz=R&5^uZ1$Z|X$LS2@`U?g&0Iaq!@Q0~@Il>3Ct){>2}<=itQ`uUg%4F9v9J=p`lI_r?z{!gs;|?}tBLvI2`+b)KS{)sl_FM3Zj^fwADIL&_8$Y|z~T=G z7>ie}ItIpS0!HT%3dS?|=@0>9>=FC`ez506FuD)pXYq66B``*x#%RiYjC=KV#!BH> z3jSg^Wue<%x&3y=E2k`+vn=$=E4RPOcxB7VVJn4Rx$V_g8L#ZM1;U>>_s`t!b)Y^U zgkc}9T{l1MuI{H{*k{L|-o1-V9!7nJVhXKv*K*L$t4tWYC9iAH*Gq=0UJ<*F;e z&y8b{2qw;&7JBa|cf8C*^LRX;pPt@hZwI@F{Zn#^;RnrPK2yjHNwB6IFo&dJWX2O}=D8(it0NYMqoiU?oL>hoWip~-)1tcJZ0CHI9 z^*4YpI7f&FBm^_<`kOyq!CB%g`9zf^?h>ZTNjmp%V4Q(4KrbG-=Ou>MK{^YdE|P|T z_OGh0W}uuhc#tbpXE^`rm|Rx25^{oHxo-VR{1{%l2Oq*y_QcRoAwQfM(-39_UDk>C zRSb~p+Po~*M6iY!utGpffDsP%ENW?CJedsAUJ7n1^L)tJlBZ9`>&WZouF+q*77B!mAFfTE%$fq=oaqG5hzgKEQZo%gPfLKCtS* z0mdss+h`J4*tyWz$;H9>h-Z>nkaGTPCX>x3GhhHe>aTJZJ0h1zf}?AJnf%V%aM!Z! zOe!9@{s(N`cZ6hE4>pd)sfPG6`s%2wZ<18;9?N1>?VSy+3ZHV47Ao@HX@`?5yO@U*MeQ z2DqU@VJPSI17+0|}BC%r!9Hbpts|!7%#AgDmq?AewpZlbwHZa4_)R z1%KMGEaH8;XMwBUmy3>pJ!nY4jdgw{p(V^jHZ<0?504SIu`sn+I85BeuzLtRZV*gn z3bf9oTkV%wC5r3%djZu#1b5*+J29Lb^gP6ZJBGD>&%^8)ZkZY&L4%Z&<2FD0l+0?=qf|3$bSfWd(wT0Ls5-c1nsATEjjBdpxAZ;3!P8i2=%S6{E zjHdC#s5-6K87nKJ@4Ahg7ycmL{8`}5_uG*%5zKT6D8J6HrnI;b2XDSamV9BlM3#OR zJR%xH;Q?J6+!`Q@O2`sQr4m4vPTZdX$K4}K-W{WUzpVG<2~~*S4^+u>7Ffh!R>0Un zzh|oxS3Y>}@f&9z$Nj&ZaM`Ume!a@6b-v5#l1k?ecAbc>S;|NxP=+P)Qg{uR*#t4o zVw3nlh6esCSQFoH57OyFFZ0vS(dkpJgy{oU(&=}WrcZ(yPsthN^tn&6Bv46%Mb3n~ zW`6p>m8I$1@!@1q9)#%^$n^V?!8x+UU}7RORo$z;TF4d2wl@N^DYw~OHbmB=qInf_FiDO zt8iHmd_kPy&6I|mau)1-F!oV4m5GChj0eCOPJ>m5AHH!r-p3Fx0B2A(X(d1ole;E3 z%gC)@!Mjc46pGu)ED_uR+M|l_x8GWiS1?IQNRhjllpUy=TQIi(Ec=mac_m)iw`RkJ z#tp0cpopu7H*DLmweN9!5FZ?VeA_m(ZQJ9+Pr$Fo`?hUBeQp~!GhN(;_uxI@+15=h zo7NR;@mgFLZfR+43D-ffwzzJl6_)L6cn{uS^atJz~VZk%v)8G>Y+|Pi%7R`WjQl(LnJ2Yru$i%mY4vB9s$e<)Z zWoDA#KGJXjjQ@%10N5w0M>47T>dH}09l3n3+zUp>slVeIxAi@)JfZ9#-n(VXrkNc> zyL1gaNG*kAe^N{XA9uAU8~IiwgB=g1x{Qbe5O`XIPw*t*FNjtATZP1qH1ITy1iF#M zUQGog=JOhnyR3qS<0D4L=x8hFIEn$fblX{&jqAQ!|znR+^;0aQl{OQL-N5+cWB z3jn&0iBTygrez>0^ecmt-OAvUOqdyyxSWUur1o*VOm+-XjfpUrog{=$DnmbH%1{On zV6M!FRm>IOFFRz7AVmcgs>%UUAuu2?Q4z)x(6#SzHdz|h z!lnX#JFG6a%qS)XWL{W!6fJTIJHQEpYj-uwq>)Owf>29Ad*j@{u!MS&g8GzZRVKY- zIY?b}ihr3gdoo?ckeMn706IYqL)=k-u-mzcTckTi*}Hxs2Y%2TgXZo$&^7+6M#uoGIu4Yu;#S2X=DnK zAS**jjFBia4bi5SXd_C5IH6zaRYEwW38s)u7Wg57Fd8`1m?hYwko!xEg?1^u;Suc7 z0wV#Ag3c}bID+p$aeHyw;5I#@*w_{~wT!p$>(hM=gN;_7-J9vN!e%(rk7k@tap3)g z!;$<@Kq=<)MU0C1NEnpc8*g#NZs+9p!xM}tde10otoU_`f4+4TjRl(XpPLZ`h zU0t-O?gU%>v15h9uk# z?5)y`7Zq!=YJt!KwL)Ubkx51(;fVRLv+SpKa&=uq@D(4AnBlj7sk1XK75b+C5*o}F?eVj<6POrsl-1cojkYeDCxvj z^y8f@UEWSQk)ab66a(pm`&cIjw=wQI=Q`301}|^wI4HE24C&?Nr391|=_RF#rCws( zM8DU|cPEMmw}RZ>?!3WWxL`#OM+Cn%A7lf?B4|WU@xa|AxJ(rfPh!A=CX}?*6V=CB z60`QdXbD%KrD%v=%b1QQjgs+_`yC)n1XI_~o55}0S7^IImkLuA|0U5rb%7j;>}#AS(C+&C_t z;0E=%M_iq5N?V3$1)Odt>#o5pi1B%=n>a%##e>BMDiI%qxgd&go_a5lh5 zQPYx`M(4T~Bc-*pLAKk7_^)#8_qmG~IR8?)c)@QMbC)c*ZI-)qLDih?*c^}<>>+gH zQAX4Sg_2q(FAR19$=05xsR20~{e!u2CotgW5u~ z&cQmC)FAOqS9mxP_s(Hj{Oe5Qkd{e+Ib#SrY2OfIrypl4L)9Gvyj8ekEEf&~!% zJ6P~JzFGFfX8%KweXeiT|9^o;XkW+vUrZX{uk-&Gl(3yd2@T$qkb8=stb0)u=0Suh zp?zyExPT$PG8W0sqAH>7>lsxdtCq-RSlb_cv~LYe!)QQS5ue=n8;7KMdiHHT!;D8b(D+qV`W8KKPo;|Q}cYZtBj+2JcL;Of;G?7mg#34M5 zH{08W(cexv%;BtP6TcIh<2X);Bnl%WpqNsjZ#}JpO@rM3U8HK zEcA;5{6IRAjtNOVA*A31&J*#k+oWtOINGK=T z;Ko~`EfmryBQS+jQ5?pb@N9111Tn}}1Czrk*0R|Yl_wb#+vf&Q|(jH zEqE61%x{^3zb%DXjCSHJ5xCx7Xu|bqb|Ib+#uMYovFLEP7%ukZdota2x84P=f49(+ z=t=g1QHBP>(Y{1aqFd<3a7XX9d+eTk?_jt%FcKY0j3vf}NsK1&xIS)=*~9tbV6m7V z$&A@!`Z%5hp1pIfh0B8ZXC$gTEA zaa>Pjkz4MZ&G42)T4pty#4Bt)i}RS|F`)hTV#j81R`+fIU}#rm5AID33=D)*eYgj? z7qd|_sz)$fO~TcIR4)|!jc_)CK#-lySP46o<#T*i%)?5n_7`wY&t|fgnbq=W`Y((* zrcU4q^j#K@q>Ir)q!2cH$$G+RDKgxf=_V{IC4_LH-wF{s6l_=-eUHWCso{a*K(W{A z0g?N+v$=%hv;A+mJ}&O~vi_q`pjWEqPH|>S0z#(30iFE^Klx35(cis(Dwc@b1Kp!@ z&SuGPS@Ogl>SgItmKC~;(I3zD6G=!GH~mc)ic(rAD*EFeS@N4)pbKZ0Th8Gq4<=V4 z5fh?-`UIdprYG!#&F63)ISCdQjI0fRIfI}m;m_k754RIW97lj9F9Y5E>A5?(k6m`5 z+5f29qboSl(PeYDa+fb~4!K|A;>lDZ;70t3q?HX0u_P=Xa3X#Znh$2&d$=qqfJ@Vl zF#~nu0e6=_eynab;N*YC^<&n%_2|cMkv#xtO~$*0yJCTJ^z72-*Gr%Ckhm9A##3HH z5@n*}C*#3>mO}Tmdm)##P5>iC#@g*a)&$0Srt-vJ&T?-89$<;Uu^Asd%j*exrk#9p z-tKD7{`iCwpB%|~-kUIOzx?Cvm-}h&M2w#@uPHpm`nR+FAJabTJmoWQF3g>M+(IFW zz`3*XW9odqdC3$&#Kt!@ z2T5|oY0f6@+c8Fcxc8x&wM&`(7Q?!Y_T zhQ`Qha}D}dj**@od1mjP*IxUp@%H`b@x7z&scv9&_bl_k&L!!`RzTMs&Q1Q2SfUS1-fp zjt8E7<>lv|dpY<#K2%$H7&$BbswX-~L|j7dj}~*gw`|$HyJ<`9y2hs3+O18yz$IVl zoW^zaFyK}35gfsB16E;%&*B2Cz?^NPAN+vXv}w{D?#svWIG;02%_IpJV8a`sk#4I^ zY!^Es9i8a<>zPm}-icdrd$w~lG#Z+WPlF<#%#Mzti7_Ty!1;I%WV@QeDKOP|9$Xyf z5EmNj%C;-*N>{QMBz-#*i{NN3X7DBmbqkxSVW8T8&e_HLnX7)$|;^qdv}M`)|pT5RD^BM>KKaGEZ2=4^k&dS+Mn`U>mY_6+sZmzG}+_E!Bww+b( z{{8rJaEZGdjeC9-{#Pijn&UXP+|4>ach`-BFI8X)t-8}LIJ#|eBaM8Q^>_4LD$Dd zVzEfTz1bg$VzmILz}+M-pbZ;d!^{xQgTMw1jXBL+_Vqd}^)b3k z6Ni%DV#MBDI!9uAilrf~m?Vj6vZh$5_-hWvG?E1Wf}E0xhe`NFhkGw?<|aGxO>!gN z5N+!0?CNZat_L=6%6Cq7PJnH*9dFBTnVdwElUu+n+=jPACp*!|6&#+4P4|q2#@cc% zxCOVxLOne_u{PkemR#FdXtZZKwh5z6cse&aHa3!*!J7c;zd66>u3Ru&dDVia?&n%Z zSvAjqp`m~cFsO@>pum!BwWOGo;zCqJTGJTD!;GzE)QpzXO$!ergWVX-wgh@XgZHG+ zIHdq@oO_a6BLlQ?OJ88M^U zCWzZpohop`43!2)a?V$|&3JQe0{-%w@D@P-=C1Ko2bSPftGc_R&A16D)iU0NCb~Aq zNXVkDatco+#(GA2v+cMIx5YbpLu+gA!nM15M`Ihw8p}ho_U()YNYX@I1ayi3>AyRh zxvZUt2W6rsVS*wtaukGFQh;JYOz zj+W$;VuBU7Yvjj3`Jl6q22keH@MwoOWf#dmX)Bcl_Oy(67nq2A7}(a;1)$raA? zoR_-gIkxk*DomVsYskII^V;N38l#@^dDOY;A2*j}`B-zZMe=`Yjy36j*jy!1d@y~h zIpQJ9Bo+(~A`(S60es2;G=wT7zePvRM#pB|`)9eq9gIS3Xa#-Xr^$IT8f;*kFDJN< z8S9tgo+la6;?1OyGQiKo=pug#C#4jh{yOJW^o&K9xUZSJOm)kP#a!VO3rW9+!J$Wb56NizV2`3V9>kpac~`YPUXAi%Ga?D z6xKNh{p*f$yXw~Y|JE`~zjuLCvTk;lf9~vs(29hjNNpO>I(iemwdTb%Nn z9NTGO-JgJrbk1?^bkAWO-%Xr*=PuIwQ9m&h4G9cP;+L(n=80rjeA zT#3jW5;8&e7N241&KOB?r$DPT&64a}mB6OX7gC5t21_=H?;XPU1qPEy4!mVeDkZ;) zDwz);hagF&0U?R{bQ1jy(8}{Y(l=!=mF!~B{B9y8L(VhY)eEYe<<2^;=zO_<8TQKt ziI|Dnv9KP=ohac-K8CB5LNnh<O83L(DwuWSaUm%<6Im7TK#tj7( zZO?P-{g@IPD9@BgNl!6=CX&b|w@P9Z@9yFvG7D(cJgCwnk4+{RB|$-g*iWEUO)JOH z3Tz02S>VBt+-DUKgOmY3*@%*1Itc=)M0ATtE)k9-d=3MlM2y5NNzy(ru|)`@7%K6D zh%2PR4~4|JO!OfeUdee_nwMlBrxKNoukBRwD~ep;S_6 z0uJ!n8s|AK?4COGZR}TIZon|dL$?~4pK4IZO!6W-Z0op^AJQM`Z)71=Zk!Q12}3CX457Y=fSFA3~qc@RBKE73nyp_$QO zt{xYSo^X;pF++~vP*4iS0EP!C0$4XZ!hrzrVoa*Hwdn=JYn*@M1Y$yg zWW?kQlu8fE4fqwB)J4SQnn8@WF{L(YsSl+TzViB}+Lxq9c-y*w2AD?u;8xa)`FnU) z|E?9%M3N0c#}ROS5#yZ<-7F?__xRqEO_=wHDSK)f(O_E%r9_D+vUhNd9@X-+10jE! z$OQ>Gl^1ao%VbP?mWzT(XDDEbXo;STv?wbn^Lm_C_hwYuA)rb@LIXWGJ%zE^i*rS$ zL`kw$?=cfC0V}zcy_{h%^Q>D+lRHj3$`0fN8<{?VhN8{m1usQFEqyCq+PtF3a*(DP z0J*&myp4ln4OPVR4AD#8!l76uP*KQ^u^a);n_LMWiibGDfudKUqa$2eA0C$y45IN6 zw^8>~*VQ{B07JqYJ!M?lMk$3t#gfP)=MmDHfV?dqn)eJ+g$Ndu2HQ?LJ`?YR0)U~G z4*02PD9`ol{`nk~W`>u;N81{n!m~hG0tIbh zybjt7R2wKcp6Fh3RF3T9iU(6kJTs&XMW($RQ z7Vg9&0k>kecQFSSW7$YFMlN3W0T<213i%xL5z9qs{KJWMLibg3x6XZ*GrVI$Ut@{U z!;9$JmH1Ky^CB-u=!!=fFRVtfrlKpJVnEGM)AASB9t1CwmVXBpn+8)uKYNNHXXSJW zy}RO_gY)I@yI}c*|JWdCttV*Eg)QqX2&BOS$W%c+PM*13e zqyfLOYzNbJh=Xf8pyic2IDFOXYhK*3Y)4}+Lsu?QROC30Nn39n!LKpDb-%+MfhsNi z$EpxptyE>x5P;EuuV#p&jyTM|i4V_gp4>WJoW_c7VVmR`Ec?LyqULhyor?e)DV)&3 z?2w1rwzltUc)IU-jNGp|3^z9M)PWyM2l3v>j`qR0ov>2rB+w?_(7Gn_7|97*iPshy zW`G#El!aOq&gl`j>F)8iw(jn>_VMoN>GAQYK-FAgF373C0)Te+R`;_jFLq8_!gvIs zTmcm56p1$@5YP4c5Bl#O4l@R?qpd6d@QU4iN>Vq z1)luD;EH3#ulUI-3t)xa)Bl~WB9EMg-!X=2(g?JY7J0OC$p=FWm@T5pBlj`)Q`{e% zOWoVAT+_|8jc{A?OxWzS`%P58mfH!FY~3AhW)3dqrdp<(M|+}M`kv@Lh@9))1ovd& zjOUqk+qq)w$&cL69AT`!@rSV*o!Pb67(h;$a}wt;@7~hS{C{LW0Lfdp{m);HDfMT$ z)%X9L6z#cFxuZvx-E`A3@;-9(=#e8wgD<*Qa5+1h%_Zzua1~bGWJoDa;y}_x1Rn`2 znW5rA#pwIsoBhf8Qz=r$yJg(@zxfR-{hGVrHy^R&kYv~FMnZp9sT+awxcr#9tbtvSD*JBJ(V9`7FO2Fk*}4#0?d-*Ep>-w?^t5Crmg zheQIkcXAV5q$Vs2P!nWNGcxeN1D9Ub-A%j_LGI?eg&qM>GfmbfwQ+4i$>^qGOicdy z-=2C3O;0nPYDVrQZa+6X^8UXbK1?=tEsHnF5hmo`;@-kk&3(q10h0n}c!LL$@I1Qj zK879}f(sc8j{cs7UcswGPCj}Sy-KL|dmO$8uf+ENf20#JK`cGxBBk+G6!;^S_Y58s z^Lx>>5`P?JK^+kO$df!^;187?$Ia#KY#wKTKg3(9_%`Aa z4gzmcgCmomykRF8aJk7T3*e9{$LIVzckO~#2D$4NeC)?t?kU{$3rPIm;%->rKJ>f) z@`lsrF63@p;9ji$H|{$(x_5IoEg;7m9_E(7x8l6O-MqlQoBr_He(UK^UUASu=-`Hb ze0|vcCiLju>qL*={*v=W&Ux4g_{kG4QmN8TF}@1(?ztE{=NtG{%+RCVD!7$2bAo6{ zk{=4>a8L6)D>rfqFi>#$^5u^%UvBUw+2wG~a?ZkcOk(`k(=qMv2y#1@7@|)}4xd5Y1_kQkHxPR9i%WbG<6G_X?WHNTp=~`%K z$brCABKWm!{yW`=xb=6@+xGF>33~hH?f%S1T%&tM>9%PHe-A>n&QGmC@BxXwM$1XO zk@))}P28LHQ_~CVKd_W^^dt%OkZ>V+W8|EC4%aZ>UZ&Znq`e}RTwsiLkoMF!2JcuU z?ZKe*4(&qwbIzy3i}R-1@#gR*p}biy;HXi5_E>X`T5u#Df8GZi2p3``kO&lYq)9F+ zaa*Yg?1>{0L49y7WD@lv(GXdnVihQL|13wsN#3JfsORkE@lab?ho&@N9z0_(J?Yaf zH1fz7FAr9fy+ojrAp(^(a2*vTNORgft5`+h?{hd~nzllYC_xo+B1b?;HUTC@03e~^ z3K}V$_=wZ2oN5XgK>>{+kT5&hrVtWJO66_3>yk6F=*UkvP%;Wh5W(>@ACJdk$(WE% z3P~fSi&?|W4(4FJqd$L>%jg-&Na|@RmI5RTlBxo#Mfk*$Km-_&45RtMN2H&N6jHJ=kc} znmXus-f5yBi<%;P!9xrxO_iz30a&1s0I-u3G9_XYgC3TXB4HRWKtdw}#8m<4$$S$D zNn(IKB+`OBatw{I9*I{0iYdpSC|mLiDVFj4$mA40eSV1y7$K(+ zfICzg9P#`t=ujnOpgaT2q>JF<<>STlkZ8&o*;Z_9D7ISE^TuG7xJxon9MDDs{*=Gr zO=3-6_CA3eFzUI_*+H|Q52-fzF&4r_Y*MR`p8e1OOk2u`8H^>MDZP%pI0{PJjOVQZ z!3>PO_=^|ArD%sMx+p1#25gmRCTxV2I=wEjAq;#ChJs+JSxd_rz}OnvJNTrrw|{4N zra8NA;+lsSU(>lJw$fTZ(=^$;N87FMo!+%4kH)P@X*4k!AMCSw%x~89|K?@OZU-AW;Yrf^$t;xJtD%qK|7rlJQ!xCyCzy&ySmzf z33^~>9s>7vg)kgB$`6Ym!^~G}o#>hg6!DmC>4?UQFhk_6jt}51T|7kYT5&+Vs zy+ml9GItq=-J_1}T*i>>sZ%(|cHsra?9A(&?JP8pz#tp(ggkawdQ#o26y%Jl=P1!} zHNIQv$)ZRhBE*z1peT$ZIDrS^vT$drULxrQ$@vhzo-%=FhVdb}h;#Bbz&pAf=9BqG z#`Ph~ZxJ4}FczJKY8Kh6q z8z+BYf`K2^J=QbcJ&rw*KuunF#Xn3n%(hed)&r9x+!=`&TaL72k@xa8s*)0c(NdDapMY)g}R7R9h zkYFD=D-_(|UM=JMm_2wGU>&8_Gf`ZxtOJ`wWaE5Ph=Me7EGC3|aYRCjt20q8YQ#;r z+RG$`l$heXBYmN0XQ-ZEldsLKo!+oBG#lE-@6HeE1v8&P&u!$OfoISK{4_4e8Eqzt@4V8th#A&v$MrSTn)YS60^VQnZcUamK^*R>BkxMejg z)~C7ktu5;V^-AkB-l>pB{$aFpy0sx#HRl75Wn^%mH|W;0(|&nARKI>Dn4*^%PlNZ^QD%VniZLXM%eY%~#%1}^YN6WM%l4~xkGNEf&0 zGvQP;#BQ3L+_Y)3vnBBGCN3O__6Pf)A7Y21`7ki*Qygwwdibt&3~!MWo9Mm6%<>Dl zHpf5Bk|#=>$*Jkl`|y?c@}7t3R&_EQ@9zOBhy0=b;%M+$Hl&7!@F@C-#iPZcQ5C(w zjt-G0{`L65^As)K;*EqBNA7ps2e>qtr-7gQ!NW0wAF<%Tqyv}x(>$LJI=kIlIMocXkD}}l9mL4EGt-r;->JX@a6$T^|)eZvKc!wGB{EkDVij2 z4LXzsA&tVlOmBBzS8q3o7b@~#bFrn^VkAH}nS?PEed2o)-yQ!R%8Xm^Qo%kKbX4_c zXN;>Ki-mUf)c53ic8=ki35@sd?8%MQkL?_bjn?-t`?^@?v^AfoHTie7I$7t-pRDEe zm8Z3$qYboH)!e5X_2ZASJofm?zrW&WGB-WKk|*Ys9%MPqwe=l;qMkERppUVPE&Rt1 zvrKr8v4mb`XfMYfhuP}&k|{mU@_cSPp4lXCM{lz`+Po)Pu5SZAzn)z`-LgI4+(4da zIpRIhG86`GKmJ6^k>b!uz)`#>THdsoUA}v98h;1!IrUTCR{nLfQR8aW<|3dx%Z;TW=Fv__@+=?e! zXP9s?s)phN}0kF;+6-o>$~(4*GCk*L_%?H`6a&~83Dh5&!FRXS95Zg*q!d) z$QM%?p*Wq{jJM#ei7nyjxDZZv^CbIT=^|FK;DyiO#WSHlaU*&`xAm@>rm=?XQ2%ge zp$Vlg;P9j{ksj|ByQEGTxhJucctRQztZ{t;Pofp>b=)p{`^3hrNo~OBF~SW!9esVM z$+5XHJgN?>{j;5ueS^sTVdYR^Av_9ewpEw3x$P zdY9QVrcEf5;3}TPcH%CzM@#nzUAPlH_7~0;3c`p`*g7&<9BPO+^>nqMz3y@@B!u~d zuyMnVuHEtep{}X$Ec!`?3&|n5R~oLH>==rma{%jjLLWEBy7aIa*TZeeCftOZt(L*| ztTALv8tE}%9FHT}Af}Z<_d|3rcJBBqXD4tZnE+`brAvok&^13~cyj*QMAyB`q{#zl z;15h`9=&rdsBBAu(izS_f!D>Ss8noeA<_}bh8ft@%93x3$j+G=Dnl)UPX}7rce^-z zkesS84Z%nUSNhWeG}Z+(=zepdi9xV|^}!oFMr-^e^9AKE!|t%R(etmxKk!M(oU`tJt=79iCTi7q`f09I*CaED-$mr z{PYg@QgRNHoGsPK6Y=0}kT01&6E97V!3Y8`Ad+~?^P+&k)huNNIg6deWK^>4Oj{Y` z)6@)TtO+_O3li$4jd}WTM_62jsCdabRdY8uKjSo5aiRb&XsWyWkz|-e9h34JX_dTM zLLL4TKrW|2$tj{32NRRTs)#Q}GDfp7O(m%aGB1|4n2~#*KS`dQri&Uj)ePP0*S$|I zBU_T4o=Q)rHlp-YYC1hd(nNGg11fCrCsJm{woE(tq#wvHs7WvY1>lf~*{deOZ(wb5 z9GM)?kh6G3-Y8GPd!sxf(*s&w@`jAy1H4PL((UAMF*4Y%p#DfW+*gPU4HX85g6=H} zhsm>J$n{8HxG*qO94rihwegU18y8PUr8MytVU`GstosZsYr0`4;pc~KZs?KCDqtWI zRUwT@ut%il=jkD08~S_nk>k4v-Q9;B#Y4=)wTb;TT` zYp^m&y2eM&d))h9Z*{MBpLVWp{pn8>f6tJ#Wq8ca7R&<8wgH27Y_vFm$MML388TFo zwt)(`khbH)A*m_U=j|Dzm4`Wrk9NhorZN+@K_VC|O-CR{=}304_k2itqKEDa@idqq zqg8vjC>X^-Z?hTLU*+vkwlllY9#swK#bEOZI~w>#4VO$P5j$Dh)K~n5G?w0&ifv4c zNTePs=nSuWsUvB!75S<=!#Q7Z%iXV#)V8a+ZSI#hu+Fr5HMv?rt_JAUzy`2nKIdG= z5nUdPvY~$RlPj#4^_ z(>hu|gIzzvQ|oY*AEb*B%(KKM2)K%~h$9vPT=2})RsPW-FhsD+Zku6%gJgr((>R&t z(F2b$UZw?!m53^O;1I}coH0z@uoaNh+gq{2GDN}=Sb0I@0d(@P@<7#lP7k+qf%C0H z+~G$^nsGHI(!%`6t4`!6J-Dw4l`caYm4SPyw16vqbHYv z#{<~C5I_b6A`=-aCqV8!u)Or-9kB%RlT2F2HV8^HgG~Z5*g~6~Te;g7{IJq{S2+ir zKXD{anjD`ypCwt+X^|$j(XBk_<$@nEf_Kc&Ah}!U*KaVUZh`Otq3Ux6IS`ErIg$S6 zLyz5wm*V@M$FJd?g>94L6XRR+yH#Q~r7WB!Hf9c+X>?!}-FXD*<7G{(Uc~^j$TZri zfcmg-&NNYRh-sGo-_EWrHj3*EuMIPMnn^>|bQQHk>sD2(C@s9C>ID(elvc#FN&#L*rKL8(L@p!{5(9#$g!G}5rlps(k&2p! zKJ;NCt(5lvXKakyst-LY@2<}|Gdpu;=A8dO|Mz{TTZu$_Lg`Kj!;W+NF2V(vozc&0 zwpp~YjMSHZ8qgQuXBiwY*jLwcZe+zHO%l3*08Y-6elT!w%op=Uwvc9jqmaqx(Syfr zT|^j>6g2I-$E38EBL+Mg>Lp~v~jN@@iL zV4zSTrOQfz^d6w1Vt%3!#~!u*o%(tl7Ssw=jTNIF#4XE4Wv;B6N+D4YA)zKzQBm+s zO%N4Km+()Q4aHKKm9jG>{DEqL_Ft$F%(dji(cn{Rk-^Ad*z88rc9QRguoA(fgwY6t z`se=G&XtXdj)!ByFaFBGlsz_rY22QIDHxYVLL)tP7aW1ZQdbDmG3f}fCpL1K5Z(f@ zSiCnG#+x8gjFn<#rJ5dQJ4RqP2i;~Ee=|~3$nu-yN1N^B^Fwqy+ki`8nSVho`l`^ZNdoqq;Sch>g9Uf3Jn-_aR~ z_sStPkr6WqV4YB31)ac&T0#?Zp+c_}sdP+)XW>j3#J(K9g1 z;OxN6B&IV1XMr87f0O3H(EYsaY9*shRo__O>pp#paK=2&b4IFL?*fw+xFZ=g(JQ}4m-Y&RybpUTG;A_qil~KI_2&#rf&Ht zwv~@R3f)$y79N&HY0&kb>g*=|YvI2mwNYylSdpKEQJJ_{~p|b**>* z=Z`NQ`U9;Zb@$$Hl?J{Ox~%ZOx5`2@I6ky0;Z2U(W~5eI-ZrfM$@&rO9PqdQ&$D{O zx$OKsu>LikB&0`&`g(%vp9=hT7vIy{cf6-}=tR(Y>SCZI8IpwEOK_WiUu(ziJuRot z1+Vx^NlOY!nnY^oTl}zXZuh**T6ei?B_u4_ZgL*uiIZUymSWc#e%_v&gL%9|KI_B+ z6Y1gO(8-?hAMWZt9IW^8N4iE&HQn~Z#K_4poxSaUcmB#R-<^MPQ?S13R-onJp_ZoS zoYgn{uN?YG3wEW}XCUzNn@-PP#%B27Lk*wa{qQG79)9R?-|9zwPtr;R5&FYg=o&G~Nl{?O3S@L0o|h7Aqd8(JHB8&@~3Y3yiBH;y%4Zv0c@$1C1n@sE|< z%J!A#R$g2AG4}!|aW}a4xDWZS@h|gH-pWpRYl?VNF@h&EcYg^ES2s=&ZMjUId|9$Z zQIf=eSzGg{+C_rX_C5>6A%bWx|q};Im6&f&z)$8LrCRYM#`&UHNbUFMrAQF1;r?BP_{7Zd%e# z5$@gj^}Tx=?yOzAVZ++BcfNhENw|Bf`CIFrZC~>#{?Bv!0L<;?YTV3d*yZFc>%mFjRVW^<8a+_&R6GgTnH$RbD=}5`X)+8Cn_!<)vY81Wqzx8Cm&P%Xr9GrpRuYvQTDHnMDL@ zDx>Y>r7~-gTrJc$^JKj>)tmhktqwYyd5Y~dIn90o(FTQ*B`2uVA9)Yp z>rp;57iFSf^56Xv20U%{64}51I9ZXpw+9lb{58xoHf87tEWIU2`Z&hp&si0Wl z6QPsQ0ndkK5m!c^7NXJma^5Cq)4t}q>?5uKP?au zhX}!M@nGQ#r_itDas<%Iu*pMQ-o-hxN{sVPixts80jJCf9nsEkI686q^uR=|HqhA- z6tWZQxIETh%9&=tHmYe|G(&~s`5t|rdLY)K#$svFVyQyL$lH2ZFXzg+3d#C0A-hNZ zab$m5Qc{ZNPHQPbr5E}O8a_sYXN}n`8XPmq&*IZ+DQDr+UZCkQTK6KKvX7aa`Y}z! zSAvo0ucfd%0h*|~^g+$}@-xn(swL`DN>cmdMfql(kJJM%tykrI+*X;(HvlG>iWZY) zMTvo8vaMIQZ{fV}G<|AW?9FRiuW_;oRf4xv!Bp7QEmyB{-glbXlV$eeR!*>`VlVVS zPpVgv*v?CwP$}6}w5{VYG|FQosoEsCH=;Im>eSTKDbp-zcG^}8iHww0Y5J+HwXL0)Z29TieFOW&WEC2uic${NkU|?hbf-|;@&V%S1 zOG>`LXaG|`2t)t?c${NkWME+617ZmV5MW|p1i~3W%mU^y000Wo0LuUXc${NkW@2ER zz`)AD!RW)7#=yYf4yC^`NHVlAFfcK&ax%aGqW}W}1f;p9FgP$MKConb@c#jW9urgz zn3&4g&7c5;i~#A{3o`%!c$~G6xcgH(^jcJ_^Gh%DtH%~ zNfz1Ukjo-^ipi&dYKkyYf{6qwiKUb>dg#SK9y-!UCzV>tsi2M^f(hl8Yi<}|SP+Dx zB7#WNL=jC4$;5F-JQ`9+Bnd55G-0NY8ZrnGEHXha$59YD@hZrjalsWg-0{E@FT6R% z2VeZ~CxAdKG}AyM^|a7R8|`%POebA*(?>sp3^BqeV~jJwBvVW?!z^>mv%nI|tgy-& z>uj*e7TY|rgOy$OIAEVcjyU0xQ_eW&f-CNM;E^|8#OoCH_x);_S=l+ci~8dHg6bk; zi7BBnwzRCL*N~@6OHZvWuc!+O4!ym;85j-=S4BjsqoQMy`3wZzlc${NkWME)o00KQGhX1$! z-)2%}U}QiAOaNR31QGxMc$__t%`3xU0LMS=KiDqP?BHefb7b_()`-?bn2YT&2RR@I zH#Y}DUZRHWAj{1`*@5Jwc2*8I5aN?7+K0Q6(diuUR5g-CRWJpqEfRGe{bTdc? zL#i1GRGq|0Gt4Acd~(k-hnz6Sk&OP)&zc!Uoqe>Z^P+2!k`*_ez=jx8s!$65Bw1q2 zwv_ibAL9S*X^^Dt+_9e>*c@s0N`lktXo-Z1@-&k>F7qi?^mI}+ivT`_k002+`0GR*) zc$|%oJr06E6odys5`(ccmRlG*2&}Qt15jCe1AaC!V1R}bcmt2&F+70B@Br2h_)Ijh zV3U{qc6Ro?SpZX9V4+(UJS>DqaG`~5tZ)`~=(!1x$q!){o;9P>awb&f{i}{g?7tMY zvBy@}q30?*Cf|i!@)J1>QkIVlN3=Lmse#<2#?OxJDd*wm<|D=^QK0Gfik1Y5-v|(L z-?9ETe&21Vu34Jyh(QsTMH81*mM9&BYNx)&^R)5Tz78Y$hNL|(N=4Q50Tl;yp>Rm5 j+LVQgf^blu7y8;nlmGw#c${NkW

HiFi Glyphs

-

This font was created for use in High Fidelity

+

This font was created withFontastic

CSS mapping

  • @@ -43,10 +43,6 @@
  • -
  • -
    - -
  • @@ -375,10 +371,6 @@
  • -
  • -
    - -
  • @@ -535,10 +527,6 @@
  • -
  • -
    - -
  • @@ -611,6 +599,30 @@
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +

Character mapping

    @@ -642,10 +654,6 @@
    -
  • -
    - -
  • @@ -974,10 +982,6 @@
  • -
  • -
    - -
  • @@ -1134,10 +1138,6 @@
  • -
  • -
    - -
  • @@ -1210,6 +1210,30 @@
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
  • +
  • +
    + +
- + \ No newline at end of file diff --git a/interface/resources/fonts/hifi-glyphs-1.34/styles.css b/interface/resources/fonts/hifi-glyphs-1.38/styles.css similarity index 97% rename from interface/resources/fonts/hifi-glyphs-1.34/styles.css rename to interface/resources/fonts/hifi-glyphs-1.38/styles.css index 59e38d455c..f6a096189b 100644 --- a/interface/resources/fonts/hifi-glyphs-1.34/styles.css +++ b/interface/resources/fonts/hifi-glyphs-1.38/styles.css @@ -59,9 +59,6 @@ .icon-headphones:before { content: "\68"; } -.icon-mic:before { - content: "\69"; -} .icon-upload:before { content: "\6a"; } @@ -308,9 +305,6 @@ .icon-voxels:before { content: "\e005"; } -.icon-lock:before { - content: "\e006"; -} .icon-visible:before { content: "\e007"; } @@ -428,9 +422,6 @@ .icon-password:before { content: "\e029"; } -.icon-rez:before { - content: "\e025"; -} .icon-keyboard-collapse:before { content: "\e02b"; } @@ -485,3 +476,21 @@ .icon-oculus:before { content: "\e036"; } +.icon-check-circled:before { + content: "\e037"; +} +.icon-rez-01:before { + content: "\e025"; +} +.icon-locked:before { + content: "\e006"; +} +.icon-unlocked:before { + content: "\e039"; +} +.icon-mic:before { + content: "\69"; +} +.icon-92-lock-01:before { + content: "\e038"; +} diff --git a/interface/resources/fonts/hifi-glyphs.ttf b/interface/resources/fonts/hifi-glyphs.ttf index aaeb1d2ace9c0f76f01de9035c05cf1af5f44df3..dae388d2eb6798d7d0d0f4135609908d83141764 100644 GIT binary patch delta 1790 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 From 72ec0ea29ad70609972b7e5657aadb96f5614b6a Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Mar 2019 11:16:03 -0800 Subject: [PATCH 16/27] Update Oculus Mobile SDK to latest version --- cmake/macros/TargetOculusMobile.cmake | 2 +- hifi_android.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cmake/macros/TargetOculusMobile.cmake b/cmake/macros/TargetOculusMobile.cmake index 3eaa008b14..f5229845a9 100644 --- a/cmake/macros/TargetOculusMobile.cmake +++ b/cmake/macros/TargetOculusMobile.cmake @@ -1,6 +1,6 @@ macro(target_oculus_mobile) - set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/oculus/VrApi) + set(INSTALL_DIR ${HIFI_ANDROID_PRECOMPILED}/oculus_1.22/VrApi) # Mobile SDK set(OVR_MOBILE_INCLUDE_DIRS ${INSTALL_DIR}/Include) diff --git a/hifi_android.py b/hifi_android.py index b8a606a82f..42b472e960 100644 --- a/hifi_android.py +++ b/hifi_android.py @@ -45,10 +45,10 @@ ANDROID_PACKAGES = { 'sharedLibFolder': 'lib', 'includeLibs': ['libnvtt.so', 'libnvmath.so', 'libnvimage.so', 'libnvcore.so'] }, - 'oculus': { - 'file': 'ovr_sdk_mobile_1.19.0.zip', - 'versionId': 's_RN1vlEvUi3pnT7WPxUC4pQ0RJBs27y', - 'checksum': '98f0afb62861f1f02dd8110b31ed30eb', + 'oculus_1.22': { + 'file': 'ovr_sdk_mobile_1.22.zip', + 'versionId': 'InhomR5gwkzyiLAelH3X9k4nvV3iIpA_', + 'checksum': '1ac3c5b0521e5406f287f351015daff8', 'sharedLibFolder': 'VrApi/Libs/Android/arm64-v8a/Release', 'includeLibs': ['libvrapi.so'] }, From 05ba515c73491ebc9660e34bbb69bfed49ad10a5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Mar 2019 13:13:45 -0800 Subject: [PATCH 17/27] Support KHR_no_error in the VR context --- libraries/oculusMobile/src/ovr/GLContext.cpp | 11 +++++------ libraries/oculusMobile/src/ovr/GLContext.h | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/libraries/oculusMobile/src/ovr/GLContext.cpp b/libraries/oculusMobile/src/ovr/GLContext.cpp index 449ba67084..8d81df42e5 100644 --- a/libraries/oculusMobile/src/ovr/GLContext.cpp +++ b/libraries/oculusMobile/src/ovr/GLContext.cpp @@ -13,10 +13,7 @@ #include #include - -#if !defined(EGL_OPENGL_ES3_BIT_KHR) -#define EGL_OPENGL_ES3_BIT_KHR 0x0040 -#endif +#include using namespace ovr; @@ -129,7 +126,7 @@ void GLContext::doneCurrent() { eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); } -bool GLContext::create(EGLDisplay display, EGLContext shareContext) { +bool GLContext::create(EGLDisplay display, EGLContext shareContext, bool noError) { this->display = display; auto config = findConfig(display); @@ -139,7 +136,9 @@ bool GLContext::create(EGLDisplay display, EGLContext shareContext) { return false; } - EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; + EGLint contextAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, + noError ? EGL_CONTEXT_OPENGL_NO_ERROR_KHR : EGL_NONE, EGL_TRUE, + EGL_NONE }; context = eglCreateContext(display, config, shareContext, contextAttribs); if (context == EGL_NO_CONTEXT) { diff --git a/libraries/oculusMobile/src/ovr/GLContext.h b/libraries/oculusMobile/src/ovr/GLContext.h index 04f96e8d47..5ccbf20c42 100644 --- a/libraries/oculusMobile/src/ovr/GLContext.h +++ b/libraries/oculusMobile/src/ovr/GLContext.h @@ -25,7 +25,7 @@ struct GLContext { static EGLConfig findConfig(EGLDisplay display); bool makeCurrent(); void doneCurrent(); - bool create(EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY), EGLContext shareContext = EGL_NO_CONTEXT); + bool create(EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY), EGLContext shareContext = EGL_NO_CONTEXT, bool noError = false); void destroy(); operator bool() const { return context != EGL_NO_CONTEXT; } static void initModule(); From b515a0cceb3633d4872911cf10ba5cf274279df1 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Mar 2019 13:15:12 -0800 Subject: [PATCH 18/27] Support KHR_no_error in the VR wrapper API --- libraries/oculusMobile/src/ovr/VrHandler.cpp | 25 +++++++++++++------- libraries/oculusMobile/src/ovr/VrHandler.h | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/libraries/oculusMobile/src/ovr/VrHandler.cpp b/libraries/oculusMobile/src/ovr/VrHandler.cpp index 3fe3901517..6cc2ec9526 100644 --- a/libraries/oculusMobile/src/ovr/VrHandler.cpp +++ b/libraries/oculusMobile/src/ovr/VrHandler.cpp @@ -42,6 +42,8 @@ struct VrSurface : public TaskQueue { uint32_t readFbo{0}; std::atomic presentIndex{1}; double displayTime{0}; + // Not currently set by anything + bool noErrorContext { false }; static constexpr float EYE_BUFFER_SCALE = 1.0f; @@ -71,7 +73,7 @@ struct VrSurface : public TaskQueue { EGLContext currentContext = eglGetCurrentContext(); EGLDisplay currentDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); - vrglContext.create(currentDisplay, currentContext); + vrglContext.create(currentDisplay, currentContext, noErrorContext); vrglContext.makeCurrent(); glm::uvec2 eyeTargetSize; @@ -91,14 +93,17 @@ struct VrSurface : public TaskQueue { } void shutdown() { + noErrorContext = false; + // Destroy the context? } - void setHandler(VrHandler *newHandler) { + void setHandler(VrHandler *newHandler, bool noError) { withLock([&] { isRenderThread = newHandler != nullptr; if (handler != newHandler) { shutdown(); handler = newHandler; + noErrorContext = noError; init(); if (handler) { updateVrMode(); @@ -142,7 +147,6 @@ struct VrSurface : public TaskQueue { __android_log_write(ANDROID_LOG_WARN, "QQQ_OVR", "vrapi_LeaveVrMode"); vrapi_SetClockLevels(session, 1, 1); vrapi_SetExtraLatencyMode(session, VRAPI_EXTRA_LATENCY_MODE_OFF); - vrapi_SetDisplayRefreshRate(session, 60); vrapi_LeaveVrMode(session); session = nullptr; @@ -153,16 +157,19 @@ struct VrSurface : public TaskQueue { ovrJava java{ vm, env, oculusActivity }; ovrModeParms modeParms = vrapi_DefaultModeParms(&java); modeParms.Flags |= VRAPI_MODE_FLAG_NATIVE_WINDOW; + if (noErrorContext) { + modeParms.Flags |= VRAPI_MODE_FLAG_CREATE_CONTEXT_NO_ERROR; + } modeParms.Display = (unsigned long long) vrglContext.display; modeParms.ShareContext = (unsigned long long) vrglContext.context; modeParms.WindowSurface = (unsigned long long) nativeWindow; session = vrapi_EnterVrMode(&modeParms); - ovrPosef trackingTransform = vrapi_GetTrackingTransform( session, VRAPI_TRACKING_TRANSFORM_SYSTEM_CENTER_EYE_LEVEL); - vrapi_SetTrackingTransform( session, trackingTransform ); - vrapi_SetPerfThread(session, VRAPI_PERF_THREAD_TYPE_RENDERER, pthread_self()); + vrapi_SetTrackingSpace( session, VRAPI_TRACKING_SPACE_LOCAL); + vrapi_SetPerfThread(session, VRAPI_PERF_THREAD_TYPE_RENDERER, gettid()); vrapi_SetClockLevels(session, 2, 4); vrapi_SetExtraLatencyMode(session, VRAPI_EXTRA_LATENCY_MODE_DYNAMIC); - vrapi_SetDisplayRefreshRate(session, 72); + // Generates a warning on the quest: "vrapi_SetDisplayRefreshRate: Dynamic Display Refresh Rate not supported" + // vrapi_SetDisplayRefreshRate(session, 72); }); } } @@ -227,8 +234,8 @@ bool VrHandler::vrActive() const { return SURFACE.session != nullptr; } -void VrHandler::setHandler(VrHandler* handler) { - SURFACE.setHandler(handler); +void VrHandler::setHandler(VrHandler* handler, bool noError) { + SURFACE.setHandler(handler, noError); } void VrHandler::pollTask() { diff --git a/libraries/oculusMobile/src/ovr/VrHandler.h b/libraries/oculusMobile/src/ovr/VrHandler.h index 3c36ee8626..46e99f7476 100644 --- a/libraries/oculusMobile/src/ovr/VrHandler.h +++ b/libraries/oculusMobile/src/ovr/VrHandler.h @@ -21,7 +21,7 @@ public: using HandlerTask = std::function; using OvrMobileTask = std::function; using OvrJavaTask = std::function; - static void setHandler(VrHandler* handler); + static void setHandler(VrHandler* handler, bool noError = false); static bool withOvrMobile(const OvrMobileTask& task); protected: From de1513a6dd57d979d67192f8a39c5fc8ee50bbd5 Mon Sep 17 00:00:00 2001 From: Brad Davis Date: Wed, 6 Mar 2019 13:22:48 -0800 Subject: [PATCH 19/27] Turn on KHR_no_error for the quest frame player --- android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp b/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp index 78a4487284..d5b87af7fa 100644 --- a/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp +++ b/android/apps/questFramePlayer/src/main/cpp/RenderThread.cpp @@ -117,7 +117,8 @@ void RenderThread::setup() { { std::unique_lock lock(_frameLock); } ovr::VrHandler::initVr(); - ovr::VrHandler::setHandler(this); + // Enable KHR_no_error for this context + ovr::VrHandler::setHandler(this, true); makeCurrent(); From e8da6b5a0cf9c388f70ae9c04fabe26601fff944 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 8 Mar 2019 10:36:58 -0700 Subject: [PATCH 20/27] add getFlowData --- interface/src/avatar/MyAvatar.cpp | 57 ++++++++++++++++++++++++++++++- interface/src/avatar/MyAvatar.h | 2 ++ libraries/animation/src/Flow.cpp | 10 ++++++ libraries/animation/src/Flow.h | 4 +++ 4 files changed, 72 insertions(+), 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index faa9f88ae9..8d499cd8db 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5331,9 +5331,13 @@ void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr& otherAvatar) void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig, const QVariantMap& collisionsConfig) { if (_skeletonModel->isLoaded()) { - _skeletonModel->getRig().initFlow(isActive); auto &flow = _skeletonModel->getRig().getFlow(); auto &collisionSystem = flow.getCollisionSystem(); + if (!flow.isInitialized() && isActive) { + _skeletonModel->getRig().initFlow(true); + } else { + flow.setActive(isActive); + } collisionSystem.setActive(isCollidable); auto physicsGroups = physicsConfig.keys(); if (physicsGroups.size() > 0) { @@ -5384,6 +5388,57 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys } } +QVariantMap MyAvatar::getFlowData() { + QVariantMap flowData; + if (_skeletonModel->isLoaded()) { + auto jointNames = getJointNames(); + auto &flow = _skeletonModel->getRig().getFlow(); + auto &collisionSystem = flow.getCollisionSystem(); + bool initialized = flow.isInitialized(); + flowData.insert("initialized", initialized); + flowData.insert("active", flow.getActive()); + flowData.insert("colliding", collisionSystem.getActive()); + QVariantMap physicsData; + QVariantMap collisionsData; + QVariantMap threadData; + auto &groups = flow.getGroupSettings(); + for (auto &group : groups) { + QVariantMap settingsObject; + QString groupName = group.first; + FlowPhysicsSettings groupSettings = group.second; + settingsObject.insert("active", groupSettings._active); + settingsObject.insert("damping", groupSettings._damping); + settingsObject.insert("delta", groupSettings._delta); + settingsObject.insert("gravity", groupSettings._gravity); + settingsObject.insert("inertia", groupSettings._inertia); + settingsObject.insert("radius", groupSettings._radius); + settingsObject.insert("stiffness", groupSettings._stiffness); + physicsData.insert(groupName, settingsObject); + } + auto &collisions = collisionSystem.getCollisions(); + for (auto &collision : collisions) { + QVariantMap collisionObject; + collisionObject.insert("offset", vec3toVariant(collision._offset)); + collisionObject.insert("radius", collision._radius); + collisionObject.insert("jointIndex", collision._jointIndex); + QString jointName = jointNames.size() > collision._jointIndex ? jointNames[collision._jointIndex] : "unknown"; + collisionsData.insert(jointName, collisionObject); + } + int count = 0; + for (auto &thread : flow.getThreads()) { + QVariantList indices; + for (int index : thread._joints) { + indices.append(index); + } + threadData.insert(thread._jointsPointer->at(thread._joints[0]).getName(), indices); + } + flowData.insert("physics", physicsData); + flowData.insert("collisions", collisionsData); + flowData.insert("threads", threadData); + } + return flowData; +} + void MyAvatar::sendPacket(const QUuid& entityID, const EntityItemProperties& properties) const { auto treeRenderer = DependencyManager::get(); EntityTreePointer entityTree = treeRenderer ? treeRenderer->getTree() : nullptr; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 2c8dedd430..26aea8bf6e 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1198,6 +1198,8 @@ public: */ Q_INVOKABLE void useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig = QVariantMap(), const QVariantMap& collisionsConfig = QVariantMap()); + Q_INVOKABLE QVariantMap getFlowData(); + public slots: /**jsdoc diff --git a/libraries/animation/src/Flow.cpp b/libraries/animation/src/Flow.cpp index 3bb80b9375..ddc3354a2f 100644 --- a/libraries/animation/src/Flow.cpp +++ b/libraries/animation/src/Flow.cpp @@ -509,6 +509,7 @@ void Flow::calculateConstraints(const std::shared_ptr& skeleton, auto flowJoint = FlowJoint(i, parentIndex, -1, name, group, jointSettings); _flowJointData.insert(std::pair(i, flowJoint)); } + updateGroupSettings(group, jointSettings); } } else { if (PRESET_COLLISION_DATA.find(name) != PRESET_COLLISION_DATA.end()) { @@ -727,6 +728,7 @@ void Flow::setPhysicsSettingsForGroup(const QString& group, const FlowPhysicsSet joint.second.setSettings(settings); } } + updateGroupSettings(group, settings); } bool Flow::getJointPositionInWorldFrame(const AnimPoseVec& absolutePoses, int jointIndex, glm::vec3& position, glm::vec3 translation, glm::quat rotation) const { @@ -780,4 +782,12 @@ Flow& Flow::operator=(const Flow& otherFlow) { } } return *this; +} + +void Flow::updateGroupSettings(const QString& group, const FlowPhysicsSettings& settings) { + if (_groupSettings.find(group) != _groupSettings.end()) { + _groupSettings.insert(std::pair(group, settings)); + } else { + _groupSettings[group] = settings; + } } \ No newline at end of file diff --git a/libraries/animation/src/Flow.h b/libraries/animation/src/Flow.h index 35464e9420..d2eaaa22d9 100644 --- a/libraries/animation/src/Flow.h +++ b/libraries/animation/src/Flow.h @@ -149,6 +149,7 @@ public: void setCollisionSettingsByJoint(int jointIndex, const FlowCollisionSettings& settings); void setActive(bool active) { _active = active; } bool getActive() const { return _active; } + const std::vector& getCollisions() const { return _selfCollisions; } protected: std::vector _selfCollisions; std::vector _othersCollisions; @@ -293,6 +294,7 @@ public: void setOthersCollision(const QUuid& otherId, int jointIndex, const glm::vec3& position); FlowCollisionSystem& getCollisionSystem() { return _collisionSystem; } void setPhysicsSettingsForGroup(const QString& group, const FlowPhysicsSettings& settings); + const std::map& getGroupSettings() const { return _groupSettings; } void cleanUp(); signals: @@ -309,6 +311,7 @@ private: void setJoints(AnimPoseVec& relativePoses, const std::vector& overrideFlags); void updateJoints(AnimPoseVec& relativePoses, AnimPoseVec& absolutePoses); bool updateRootFramePositions(const AnimPoseVec& absolutePoses, size_t threadIndex); + void updateGroupSettings(const QString& group, const FlowPhysicsSettings& settings); void setScale(float scale); float _scale { 1.0f }; @@ -316,6 +319,7 @@ private: glm::vec3 _entityPosition; glm::quat _entityRotation; std::map _flowJointData; + std::map _groupSettings; std::vector _jointThreads; std::vector _flowJointKeywords; FlowCollisionSystem _collisionSystem; From a977bb6dc8f5708b12d7fec393f5fdf14a5287a5 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 8 Mar 2019 12:39:57 -0700 Subject: [PATCH 21/27] remove unuse variable --- interface/src/avatar/MyAvatar.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 8d499cd8db..b596a363f4 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5424,7 +5424,6 @@ QVariantMap MyAvatar::getFlowData() { QString jointName = jointNames.size() > collision._jointIndex ? jointNames[collision._jointIndex] : "unknown"; collisionsData.insert(jointName, collisionObject); } - int count = 0; for (auto &thread : flow.getThreads()) { QVariantList indices; for (int index : thread._joints) { From cbe920774f90fc917369c2c69d9fc692e3b31f4e Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 8 Mar 2019 12:45:29 -0700 Subject: [PATCH 22/27] add getFlowData jsdoc --- interface/src/avatar/MyAvatar.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 042a5c2a3c..c67cabe4f5 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1198,6 +1198,10 @@ public: */ Q_INVOKABLE void useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig = QVariantMap(), const QVariantMap& collisionsConfig = QVariantMap()); + /**jsdoc + * @function MyAvatar.getFlowData + * @returns {object} + */ Q_INVOKABLE QVariantMap getFlowData(); public slots: From f363d95ca2b9d66806e50cf74b1e8b8fa668f9bb Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 8 Mar 2019 13:02:47 -0700 Subject: [PATCH 23/27] clear group settings on init --- libraries/animation/src/Flow.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/animation/src/Flow.cpp b/libraries/animation/src/Flow.cpp index ddc3354a2f..6e210fe71c 100644 --- a/libraries/animation/src/Flow.cpp +++ b/libraries/animation/src/Flow.cpp @@ -463,6 +463,7 @@ void Flow::calculateConstraints(const std::shared_ptr& skeleton, auto flowPrefix = FLOW_JOINT_PREFIX.toUpper(); auto simPrefix = SIM_JOINT_PREFIX.toUpper(); std::vector handsIndices; + _groupSettings.clear(); for (int i = 0; i < skeleton->getNumJoints(); i++) { auto name = skeleton->getJointName(i); From 4858f648101d905d41a38c9a6195ba693d70de65 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 8 Mar 2019 14:36:47 -0700 Subject: [PATCH 24/27] get the colliding joints --- interface/src/avatar/MyAvatar.cpp | 26 ++++++++++++++++++++++++++ interface/src/avatar/MyAvatar.h | 7 +++++++ libraries/animation/src/Flow.h | 1 + 3 files changed, 34 insertions(+) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 7e0ee5c2fd..389263656d 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5404,7 +5404,18 @@ QVariantMap MyAvatar::getFlowData() { QVariantMap physicsData; QVariantMap collisionsData; QVariantMap threadData; + std::map groupJointsMap; + QVariantList jointCollisionData; auto &groups = flow.getGroupSettings(); + for (auto &joint : flow.getJoints()) { + auto &groupName = joint.second.getGroup(); + if (groups.find(groupName) != groups.end()) { + if (groupJointsMap.find(groupName) == groupJointsMap.end()) { + groupJointsMap.insert(std::pair(groupName, QVariantList())); + } + groupJointsMap[groupName].push_back(joint.second.getIndex()); + } + } for (auto &group : groups) { QVariantMap settingsObject; QString groupName = group.first; @@ -5416,8 +5427,10 @@ QVariantMap MyAvatar::getFlowData() { settingsObject.insert("inertia", groupSettings._inertia); settingsObject.insert("radius", groupSettings._radius); settingsObject.insert("stiffness", groupSettings._stiffness); + settingsObject.insert("jointIndices", groupJointsMap[groupName]); physicsData.insert(groupName, settingsObject); } + auto &collisions = collisionSystem.getCollisions(); for (auto &collision : collisions) { QVariantMap collisionObject; @@ -5441,6 +5454,19 @@ QVariantMap MyAvatar::getFlowData() { return flowData; } +QVariantList MyAvatar::getCollidingFlowJoints() { + QVariantList collidingFlowJoints; + if (_skeletonModel->isLoaded()) { + auto& flow = _skeletonModel->getRig().getFlow(); + for (auto &joint : flow.getJoints()) { + if (joint.second.isColliding()) { + collidingFlowJoints.append(joint.second.getIndex()); + } + } + } + return collidingFlowJoints; +} + void MyAvatar::initFlowFromFST() { if (_skeletonModel->isLoaded()) { auto &flowData = _skeletonModel->getHFMModel().flowData; diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index c67cabe4f5..e516364f61 100755 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -1204,6 +1204,13 @@ public: */ Q_INVOKABLE QVariantMap getFlowData(); + /**jsdoc + * returns the indices of every colliding flow joint + * @function MyAvatar.getCollidingFlowJoints + * @returns {int[]} + */ + Q_INVOKABLE QVariantList getCollidingFlowJoints(); + public slots: /**jsdoc diff --git a/libraries/animation/src/Flow.h b/libraries/animation/src/Flow.h index d2eaaa22d9..ad81c2be77 100644 --- a/libraries/animation/src/Flow.h +++ b/libraries/animation/src/Flow.h @@ -222,6 +222,7 @@ public: const glm::quat& getCurrentRotation() const { return _currentRotation; } const glm::vec3& getCurrentTranslation() const { return _initialTranslation; } const glm::vec3& getInitialPosition() const { return _initialPosition; } + bool isColliding() const { return _colliding; } protected: From bfcb1a8391e18b8c26efb602c6ad84ed354f4690 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 8 Mar 2019 15:57:25 -0700 Subject: [PATCH 25/27] Make methods thread safe --- interface/src/avatar/MyAvatar.cpp | 42 +++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 389263656d..73751f4dbe 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5333,6 +5333,15 @@ void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr& otherAvatar) } void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig, const QVariantMap& collisionsConfig) { + QVariantList result; + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "useFlow", + Q_ARG(bool, isActive), + Q_ARG(bool, isCollidable), + Q_ARG(const QVariantMap&, physicsConfig), + Q_ARG(const QVariantMap&, collisionsConfig)); + return; + } if (_skeletonModel->isLoaded()) { auto &flow = _skeletonModel->getRig().getFlow(); auto &collisionSystem = flow.getCollisionSystem(); @@ -5392,15 +5401,20 @@ void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& phys } QVariantMap MyAvatar::getFlowData() { - QVariantMap flowData; + QVariantMap result; + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "getFlowData", + Q_RETURN_ARG(QVariantMap, result)); + return result; + } if (_skeletonModel->isLoaded()) { auto jointNames = getJointNames(); auto &flow = _skeletonModel->getRig().getFlow(); auto &collisionSystem = flow.getCollisionSystem(); bool initialized = flow.isInitialized(); - flowData.insert("initialized", initialized); - flowData.insert("active", flow.getActive()); - flowData.insert("colliding", collisionSystem.getActive()); + result.insert("initialized", initialized); + result.insert("active", flow.getActive()); + result.insert("colliding", collisionSystem.getActive()); QVariantMap physicsData; QVariantMap collisionsData; QVariantMap threadData; @@ -5447,24 +5461,30 @@ QVariantMap MyAvatar::getFlowData() { } threadData.insert(thread._jointsPointer->at(thread._joints[0]).getName(), indices); } - flowData.insert("physics", physicsData); - flowData.insert("collisions", collisionsData); - flowData.insert("threads", threadData); + result.insert("physics", physicsData); + result.insert("collisions", collisionsData); + result.insert("threads", threadData); } - return flowData; + return result; } QVariantList MyAvatar::getCollidingFlowJoints() { - QVariantList collidingFlowJoints; + QVariantList result; + if (QThread::currentThread() != thread()) { + BLOCKING_INVOKE_METHOD(this, "getCollidingFlowJoints", + Q_RETURN_ARG(QVariantList, result)); + return result; + } + if (_skeletonModel->isLoaded()) { auto& flow = _skeletonModel->getRig().getFlow(); for (auto &joint : flow.getJoints()) { if (joint.second.isColliding()) { - collidingFlowJoints.append(joint.second.getIndex()); + result.append(joint.second.getIndex()); } } } - return collidingFlowJoints; + return result; } void MyAvatar::initFlowFromFST() { From df32b61eaf08cfc6d99a2813704db014b6caba7f Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 8 Mar 2019 16:01:18 -0700 Subject: [PATCH 26/27] remove unused variable --- interface/src/avatar/MyAvatar.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 73751f4dbe..9211be3b4f 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -5333,7 +5333,6 @@ void MyAvatar::addAvatarHandsToFlow(const std::shared_ptr& otherAvatar) } void MyAvatar::useFlow(bool isActive, bool isCollidable, const QVariantMap& physicsConfig, const QVariantMap& collisionsConfig) { - QVariantList result; if (QThread::currentThread() != thread()) { QMetaObject::invokeMethod(this, "useFlow", Q_ARG(bool, isActive), From 80150565f69472d4b54e2a938145f2f132a8f865 Mon Sep 17 00:00:00 2001 From: luiscuenca Date: Fri, 8 Mar 2019 16:30:28 -0700 Subject: [PATCH 27/27] Fix bug on group settings --- libraries/animation/src/Flow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/animation/src/Flow.cpp b/libraries/animation/src/Flow.cpp index 6e210fe71c..5bc2021e5e 100644 --- a/libraries/animation/src/Flow.cpp +++ b/libraries/animation/src/Flow.cpp @@ -786,7 +786,7 @@ Flow& Flow::operator=(const Flow& otherFlow) { } void Flow::updateGroupSettings(const QString& group, const FlowPhysicsSettings& settings) { - if (_groupSettings.find(group) != _groupSettings.end()) { + if (_groupSettings.find(group) == _groupSettings.end()) { _groupSettings.insert(std::pair(group, settings)); } else { _groupSettings[group] = settings;