diff --git a/interface/resources/qml/hifi/audio/Audio.qml b/interface/resources/qml/hifi/audio/Audio.qml index efbf663838..1a0457fd0a 100644 --- a/interface/resources/qml/hifi/audio/Audio.qml +++ b/interface/resources/qml/hifi/audio/Audio.qml @@ -125,22 +125,6 @@ Rectangle { } } - HifiControlsUit.Switch { - id: stereoInput; - height: root.switchHeight; - switchWidth: root.switchWidth; - labelTextOn: qsTr("Stereo input"); - backgroundOnColor: "#E3E3E3"; - checked: AudioScriptingInterface.isStereoInput; - onCheckedChanged: { - AudioScriptingInterface.isStereoInput = checked; - checked = Qt.binding(function() { return AudioScriptingInterface.isStereoInput; }); // restore binding - } - } - } - - ColumnLayout { - spacing: 24; HifiControlsUit.Switch { height: root.switchHeight; switchWidth: root.switchWidth; @@ -152,6 +136,23 @@ Rectangle { checked = Qt.binding(function() { return AudioScriptingInterface.noiseReduction; }); // restore binding } } + } + + ColumnLayout { + spacing: 24; + HifiControlsUit.Switch { + id: warnMutedSwitch + height: root.switchHeight; + switchWidth: root.switchWidth; + labelTextOn: qsTr("Warn when muted"); + backgroundOnColor: "#E3E3E3"; + checked: AudioScriptingInterface.warnWhenMuted; + onClicked: { + AudioScriptingInterface.warnWhenMuted = checked; + checked = Qt.binding(function() { return AudioScriptingInterface.warnWhenMuted; }); // restore binding + } + } + HifiControlsUit.Switch { id: audioLevelSwitch @@ -165,6 +166,20 @@ Rectangle { checked = Qt.binding(function() { return AvatarInputs.showAudioTools; }); // restore binding } } + + HifiControlsUit.Switch { + id: stereoInput; + height: root.switchHeight; + switchWidth: root.switchWidth; + labelTextOn: qsTr("Stereo input"); + backgroundOnColor: "#E3E3E3"; + checked: AudioScriptingInterface.isStereoInput; + onCheckedChanged: { + AudioScriptingInterface.isStereoInput = checked; + checked = Qt.binding(function() { return AudioScriptingInterface.isStereoInput; }); // restore binding + } + } + } } diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index 2c4c29ff65..4a4b3c146b 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -25,6 +25,9 @@ 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] @@ -37,11 +40,14 @@ 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) { @@ -73,6 +79,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; @@ -105,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 fcf3c181da..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; @@ -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 b2e6167ffa..1c10d24f23 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -1531,6 +1531,15 @@ 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 87e0f68e72..b9648219a5 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; } + virtual bool getLocalEcho() override { return _shouldEchoLocally; } virtual void setLocalEcho(bool localEcho) override { _shouldEchoLocally = localEcho; } virtual void toggleLocalEcho() override { _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/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 c597f75bca..e715e97575 100644 --- a/scripts/system/audioMuteOverlay.js +++ b/scripts/system/audioMuteOverlay.js @@ -1,104 +1,144 @@ -"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 // -(function () { // BEGIN LOCAL_SCOPE - var utilsPath = Script.resolvePath('../developer/libraries/utils.js'); - Script.include(utilsPath); +"use strict"; - var TWEEN_SPEED = 0.025; - var MIX_AMOUNT = 0.25; +/* global Audio, Script, Overlays, Quat, MyAvatar, HMD */ - 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() { // BEGIN LOCAL_SCOPE - Script.update.connect(update); - Script.scriptEnding.connect(cleanup); + var lastShortTermInputLoudness = 0.0; + var lastLongTermInputLoudness = 0.0; + var sampleRate = 8.0; // Hz - function update(dt) { - if (!Audio.muted) { - if (hasOverlay()) { - deleteOverlay(); - } - } else if (!hasOverlay()) { - createOverlay(); + 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.0, y: -0.5, 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 { - updateOverlay(); + var textDimensions = { x: 100, y: 50 }; + warningOverlayID = Overlays.addOverlay("text", { + name: "Muted-Warning", + font: { size: 36 }, + text: warningText, + x: (Window.innerWidth - textDimensions.x) / 2, + y: (Window.innerHeight - textDimensions.y), + width: textDimensions.x, + height: textDimensions.y, + textColor: { red: 226, green: 51, blue: 77 }, + backgroundAlpha: 0, + visible: true + }); } } - 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; - } + function hideWarning() { + if (!warningOverlayID) { + return; } - // 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 - }); + Overlays.deleteOverlay(warningOverlayID); + warningOverlayID = null; } - function deleteOverlay() { - Overlays.deleteOverlay(overlayID); + 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); + } + + 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() { - deleteOverlay(); - Audio.muted.disconnect(onMuteToggled); - Script.update.disconnect(update); + stopPoll(); } + + Script.scriptEnding.connect(cleanup); + + startOrStopPoll(); + Audio.mutedChanged.connect(startOrStopPoll); + Audio.warnWhenMutedChanged.connect(startOrStopPoll); + }()); // END LOCAL_SCOPE \ No newline at end of file