diff --git a/interface/resources/qml/AudioScope.qml b/interface/resources/qml/AudioScope.qml new file mode 100644 index 0000000000..aea1473c3d --- /dev/null +++ b/interface/resources/qml/AudioScope.qml @@ -0,0 +1,634 @@ +// +// AudioScope.qml +// +// Created by Luis Cuenca on 11/22/2017 +// Copyright 2017 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or https://www.apache.org/licenses/LICENSE-2.0.html +// + +import QtQuick 2.5 +import QtQuick.Controls 1.4 +import "styles-uit" +import "controls-uit" as HifiControlsUit + +Item { + id: root + width: parent.width + height: parent.height + + property var _scopeInputData + property var _scopeOutputLeftData + property var _scopeOutputRightData + + property var _triggerInputData + property var _triggerOutputLeftData + property var _triggerOutputRightData + + property var _triggerValues: QtObject{ + property int x: parent.width/2 + property int y: parent.height/3 + } + + property var _triggered: false + property var _steps + property var _refreshMs: 32 + property var _framesPerSecond: AudioScope.getFramesPerSecond() + property var _isFrameUnits: true + + property var _holdStart: QtObject{ + property int x: 0 + property int y: 0 + } + + property var _holdEnd: QtObject{ + property int x: 0 + property int y: 0 + } + + property var _timeBeforeHold: 300 + property var _pressedTime: 0 + property var _isPressed: false + + property var _recOpacity : 0.0 + property var _recSign : 0.05 + + property var _outputLeftState: false + property var _outputRightState: false + + property var _wavFilePath: "" + + function isHolding() { + return (_pressedTime > _timeBeforeHold); + } + + function updateMeasureUnits() { + timeButton.text = _isFrameUnits ? "Display Frames" : "Milliseconds"; + fiveLabel.text = _isFrameUnits ? "5" : "" + (Math.round(1000 * 5.0/_framesPerSecond)); + twentyLabel.text = _isFrameUnits ? "20" : "" + (Math.round(1000 * 20.0/_framesPerSecond)); + fiftyLabel.text = _isFrameUnits ? "50" : "" + (Math.round(1000 * 50.0/_framesPerSecond)); + } + + function collectScopeData() { + if (inputCh.checked) { + _scopeInputData = AudioScope.scopeInput; + } + if (outputLeftCh.checked) { + _scopeOutputLeftData = AudioScope.scopeOutputLeft; + } + if (outputRightCh.checked) { + _scopeOutputRightData = AudioScope.scopeOutputRight; + } + } + + function collectTriggerData() { + if (inputCh.checked) { + _triggerInputData = AudioScope.triggerInput; + } + if (outputLeftCh.checked) { + _triggerOutputLeftData = AudioScope.triggerOutputLeft; + } + if (outputRightCh.checked) { + _triggerOutputRightData = AudioScope.triggerOutputRight; + } + } + + function setRecordingLabelOpacity(opacity) { + _recOpacity = opacity; + recCircle.opacity = _recOpacity; + recText.opacity = _recOpacity; + } + + function updateRecordingLabel() { + _recOpacity += _recSign; + if (_recOpacity > 1.0 || _recOpacity < 0.0) { + _recOpacity = _recOpacity > 1.0 ? 1.0 : 0.0; + _recSign *= -1; + } + setRecordingLabelOpacity(_recOpacity); + } + + function pullFreshValues() { + if (Audio.getRecording()) { + updateRecordingLabel(); + } + + if (!AudioScope.getPause()) { + if (!_triggered) { + collectScopeData(); + } + } + if (inputCh.checked || outputLeftCh.checked || outputRightCh.checked) { + mycanvas.requestPaint(); + } + } + + function startRecording() { + _wavFilePath = (new Date()).toISOString(); // yyyy-mm-ddThh:mm:ss.sssZ + _wavFilePath = _wavFilePath.replace(/[\-:]|\.\d*Z$/g, "").replace("T", "-") + ".wav"; + // Using controller recording default directory + _wavFilePath = Recording.getDefaultRecordingSaveDirectory() + _wavFilePath; + if (!Audio.startRecording(_wavFilePath)) { + Messages.sendMessage("Hifi-Notifications", JSON.stringify({message:"Error creating: "+_wavFilePath})); + updateRecordingUI(false); + } + } + + function stopRecording() { + Audio.stopRecording(); + setRecordingLabelOpacity(0.0); + Messages.sendMessage("Hifi-Notifications", JSON.stringify({message:"Saved: "+_wavFilePath})); + } + + function updateRecordingUI(isRecording) { + if (!isRecording) { + recordButton.text = "Record"; + recordButton.color = hifi.buttons.black; + outputLeftCh.checked = _outputLeftState; + outputRightCh.checked = _outputRightState; + } else { + recordButton.text = "Stop"; + recordButton.color = hifi.buttons.red; + _outputLeftState = outputLeftCh.checked; + _outputRightState = outputRightCh.checked; + outputLeftCh.checked = true; + outputRightCh.checked = true; + } + } + + function toggleRecording() { + if (Audio.getRecording()) { + updateRecordingUI(false); + stopRecording(); + } else { + updateRecordingUI(true); + startRecording(); + } + } + + Timer { + interval: _refreshMs; running: true; repeat: true + onTriggered: pullFreshValues() + } + + Canvas { + id: mycanvas + anchors.fill:parent + + onPaint: { + + function displayMeasureArea(ctx) { + + ctx.fillStyle = Qt.rgba(0.1, 0.1, 0.1, 1); + ctx.fillRect(_holdStart.x, 0, _holdEnd.x - _holdStart.x, height); + + ctx.lineWidth = "2"; + ctx.strokeStyle = "#555555"; + + ctx.beginPath(); + ctx.moveTo(_holdStart.x, 0); + ctx.lineTo(_holdStart.x, height); + ctx.moveTo(_holdEnd.x, 0); + ctx.lineTo(_holdEnd.x, height); + + ctx.moveTo(_holdStart.x, _holdStart.y); + ctx.lineTo(_holdEnd.x, _holdStart.y); + ctx.moveTo(_holdEnd.x, _holdEnd.y); + ctx.lineTo(_holdStart.x, _holdEnd.y); + + ctx.stroke(); + } + + function displayTrigger(ctx, lineWidth, color) { + var crossSize = 3; + var holeSize = 2; + + ctx.lineWidth = lineWidth; + ctx.strokeStyle = color; + + ctx.beginPath(); + ctx.moveTo(_triggerValues.x - (crossSize + holeSize), _triggerValues.y); + ctx.lineTo(_triggerValues.x - holeSize, _triggerValues.y); + ctx.moveTo(_triggerValues.x + holeSize, _triggerValues.y); + ctx.lineTo(_triggerValues.x + (crossSize + holeSize), _triggerValues.y); + + ctx.moveTo(_triggerValues.x, _triggerValues.y - (crossSize + holeSize)); + ctx.lineTo(_triggerValues.x, _triggerValues.y - holeSize); + ctx.moveTo(_triggerValues.x, _triggerValues.y + holeSize); + ctx.lineTo(_triggerValues.x, _triggerValues.y + (crossSize + holeSize)); + + ctx.stroke(); + } + + function displayBackground(ctx, datawidth, steps, lineWidth, color) { + var verticalPadding = 100; + + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + + ctx.moveTo(0, height/2); + ctx.lineTo(datawidth, height/2); + + var gap = datawidth/steps; + for (var i = 0; i < steps; i++) { + ctx.moveTo(i*gap + 1, verticalPadding); + ctx.lineTo(i*gap + 1, height-verticalPadding); + } + ctx.moveTo(datawidth-1, verticalPadding); + ctx.lineTo(datawidth-1, height-verticalPadding); + + ctx.stroke(); + } + + function drawScope(ctx, data, width, color) { + ctx.beginPath(); + ctx.strokeStyle = color; + ctx.lineWidth = width; + var x = 0; + for (var i = 0; i < data.length-1; i++) { + ctx.moveTo(x, data[i] + height/2); + ctx.lineTo(++x, data[i+1] + height/2); + } + ctx.stroke(); + } + + function getMeasurementText(dist) { + var datasize = _scopeInputData.length; + var value = 0; + if (fiveFrames.checked) { + value = (_isFrameUnits) ? 5.0*dist/datasize : (Math.round(1000 * 5.0/_framesPerSecond))*dist/datasize; + } else if (twentyFrames.checked) { + value = (_isFrameUnits) ? 20.0*dist/datasize : (Math.round(1000 * 20.0/_framesPerSecond))*dist/datasize; + } else if (fiftyFrames.checked) { + value = (_isFrameUnits) ? 50.0*dist/datasize : (Math.round(1000 * 50.0/_framesPerSecond))*dist/datasize; + } + value = Math.abs(Math.round(value*100)/100); + var measureText = "" + value + (_isFrameUnits ? " frames" : " milliseconds"); + return measureText; + } + + function drawMeasurements(ctx, color) { + ctx.fillStyle = color; + ctx.font = "normal 16px sans-serif"; + var fontwidth = 8; + var measureText = getMeasurementText(_holdEnd.x - _holdStart.x); + if (_holdStart.x < _holdEnd.x) { + ctx.fillText("" + height/2 - _holdStart.y, _holdStart.x-40, _holdStart.y); + ctx.fillText("" + height/2 - _holdEnd.y, _holdStart.x-40, _holdEnd.y); + ctx.fillText(measureText, _holdEnd.x+10, _holdEnd.y); + } else { + ctx.fillText("" + height/2 - _holdStart.y, _holdStart.x+10, _holdStart.y); + ctx.fillText("" + height/2 - _holdEnd.y, _holdStart.x+10, _holdEnd.y); + ctx.fillText(measureText, _holdEnd.x-fontwidth*measureText.length, _holdEnd.y); + } + } + + var ctx = getContext("2d"); + + ctx.fillStyle = Qt.rgba(0, 0, 0, 1); + ctx.fillRect(0, 0, width, height); + + if (isHolding()) { + displayMeasureArea(ctx); + } + + var guideLinesColor = "#555555" + var guideLinesWidth = "1" + + displayBackground(ctx, _scopeInputData.length, _steps, guideLinesWidth, guideLinesColor); + + var triggerWidth = "3" + var triggerColor = "#EFB400" + + if (AudioScope.getAutoTrigger()) { + displayTrigger(ctx, triggerWidth, triggerColor); + } + + var scopeWidth = "2" + var scopeInputColor = "#00B4EF" + var scopeOutputLeftColor = "#BB0000" + var scopeOutputRightColor = "#00BB00" + + if (!_triggered) { + if (inputCh.checked) { + drawScope(ctx, _scopeInputData, scopeWidth, scopeInputColor); + } + if (outputLeftCh.checked) { + drawScope(ctx, _scopeOutputLeftData, scopeWidth, scopeOutputLeftColor); + } + if (outputRightCh.checked) { + drawScope(ctx, _scopeOutputRightData, scopeWidth, scopeOutputRightColor); + } + } else { + if (inputCh.checked) { + drawScope(ctx, _triggerInputData, scopeWidth, scopeInputColor); + } + if (outputLeftCh.checked) { + drawScope(ctx, _triggerOutputLeftData, scopeWidth, scopeOutputLeftColor); + } + if (outputRightCh.checked) { + drawScope(ctx, _triggerOutputRightData, scopeWidth, scopeOutputRightColor); + } + } + + if (isHolding()) { + drawMeasurements(ctx, "#eeeeee"); + } + + if (_isPressed) { + _pressedTime += _refreshMs; + } + } + } + + MouseArea { + id: hitbox + anchors.fill: mycanvas + hoverEnabled: true + onPressed: { + _isPressed = true; + _pressedTime = 0; + _holdStart.x = mouseX; + _holdStart.y = mouseY; + } + onPositionChanged: { + _holdEnd.x = mouseX; + _holdEnd.y = mouseY; + } + onReleased: { + if (!isHolding() && AudioScope.getAutoTrigger()) { + _triggerValues.x = mouseX + _triggerValues.y = mouseY + AudioScope.setTriggerValues(mouseX, mouseY-height/2); + } + _isPressed = false; + _pressedTime = 0; + } + } + + HifiControlsUit.CheckBox { + id: activated + boxSize: 20 + anchors.top: parent.top; + anchors.left: parent.left; + anchors.topMargin: 8; + anchors.leftMargin: 20; + checked: AudioScope.getVisible(); + onCheckedChanged: { + AudioScope.setVisible(checked); + activelabel.text = AudioScope.getVisible() ? "On" : "Off" + } + } + + HifiControlsUit.Label { + id: activelabel + text: AudioScope.getVisible() ? "On" : "Off" + anchors.top: activated.top; + anchors.left: activated.right; + } + + HifiControlsUit.CheckBox { + id: outputLeftCh + boxSize: 20 + text: "Output L" + anchors.horizontalCenter: parent.horizontalCenter; + anchors.top: parent.top; + anchors.topMargin: 8; + onCheckedChanged: { + AudioScope.setServerEcho(outputLeftCh.checked || outputRightCh.checked); + } + } + + HifiControlsUit.Label { + text: "Channels"; + anchors.horizontalCenter: outputLeftCh.horizontalCenter; + anchors.bottom: outputLeftCh.top; + anchors.bottomMargin: 8; + } + + HifiControlsUit.CheckBox { + id: inputCh + boxSize: 20 + text: "Input Mono" + anchors.bottom: outputLeftCh.bottom; + anchors.right: outputLeftCh.left; + anchors.rightMargin: 40; + onCheckedChanged: { + AudioScope.setLocalEcho(checked); + } + } + + HifiControlsUit.CheckBox { + id: outputRightCh + boxSize: 20 + text: "Output R" + anchors.bottom: outputLeftCh.bottom; + anchors.left: outputLeftCh.right; + anchors.leftMargin: 40; + onCheckedChanged: { + AudioScope.setServerEcho(outputLeftCh.checked || outputRightCh.checked); + } + } + + HifiControlsUit.Button { + id: recordButton; + text: "Record"; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.right: parent.right; + anchors.bottom: parent.bottom; + anchors.rightMargin: 30; + anchors.bottomMargin: 8; + width: 95; + height: 55; + onClicked: { + toggleRecording(); + } + } + + HifiControlsUit.Button { + id: pauseButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + anchors.right: recordButton.left; + anchors.bottom: parent.bottom; + anchors.rightMargin: 30; + anchors.bottomMargin: 8; + height: 55; + width: 95; + text: " Pause "; + onClicked: { + AudioScope.togglePause(); + } + } + + HifiControlsUit.CheckBox { + id: twentyFrames + boxSize: 20 + anchors.left: parent.horizontalCenter; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + onCheckedChanged: { + if (checked){ + fiftyFrames.checked = false; + fiveFrames.checked = false; + AudioScope.selectAudioScopeTwentyFrames(); + _steps = 20; + AudioScope.setPause(false); + } + } + } + + HifiControlsUit.Label { + id:twentyLabel + anchors.left: twentyFrames.right; + anchors.verticalCenter: twentyFrames.verticalCenter; + } + + HifiControlsUit.Button { + id: timeButton; + color: hifi.buttons.black; + colorScheme: hifi.colorSchemes.dark; + text: "Display Frames"; + anchors.horizontalCenter: twentyFrames.horizontalCenter; + anchors.bottom: twentyFrames.top; + anchors.bottomMargin: 8; + height: 26; + onClicked: { + _isFrameUnits = !_isFrameUnits; + updateMeasureUnits(); + } + } + + HifiControlsUit.CheckBox { + id: fiveFrames + boxSize: 20 + anchors.horizontalCenter: parent.horizontalCenter; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + anchors.horizontalCenterOffset: -50; + checked: true; + onCheckedChanged: { + if (checked) { + fiftyFrames.checked = false; + twentyFrames.checked = false; + AudioScope.selectAudioScopeFiveFrames(); + _steps = 5; + AudioScope.setPause(false); + } + } + } + + HifiControlsUit.Label { + id:fiveLabel + anchors.left: fiveFrames.right; + anchors.verticalCenter: fiveFrames.verticalCenter; + } + + HifiControlsUit.CheckBox { + id: fiftyFrames + boxSize: 20 + anchors.horizontalCenter: parent.horizontalCenter; + anchors.bottom: parent.bottom; + anchors.bottomMargin: 8; + anchors.horizontalCenterOffset: 70; + onCheckedChanged: { + if (checked) { + twentyFrames.checked = false; + fiveFrames.checked = false; + AudioScope.selectAudioScopeFiftyFrames(); + _steps = 50; + AudioScope.setPause(false); + } + } + } + + HifiControlsUit.Label { + id:fiftyLabel + anchors.left: fiftyFrames.right; + anchors.verticalCenter: fiftyFrames.verticalCenter; + } + + HifiControlsUit.Switch { + id: triggerSwitch; + height: 26; + anchors.left: parent.left; + anchors.bottom: parent.bottom; + anchors.leftMargin: 75; + anchors.bottomMargin: 8; + labelTextOff: "Off"; + labelTextOn: "On"; + onCheckedChanged: { + if (!checked) AudioScope.setPause(false); + AudioScope.setPause(false); + AudioScope.setAutoTrigger(checked); + AudioScope.setTriggerValues(_triggerValues.x, _triggerValues.y-root.height/2); + } + } + + HifiControlsUit.Label { + text: "Trigger"; + anchors.left: triggerSwitch.left; + anchors.leftMargin: -15; + anchors.bottom: triggerSwitch.top; + } + + Rectangle { + id: recordIcon; + width:110; + height:40; + anchors.right: parent.right; + anchors.top: parent.top; + anchors.topMargin: 8; + color: "transparent" + + Text { + id: recText + text: "REC" + color: "red" + font.pixelSize: 30; + anchors.left: recCircle.right; + anchors.leftMargin: 10; + opacity: _recOpacity; + y: -8; + } + + Rectangle { + id: recCircle; + width: 25; + height: 25; + radius: width*0.5 + opacity: _recOpacity; + color: "red"; + } + } + + Component.onCompleted: { + _steps = AudioScope.getFramesPerScope(); + AudioScope.setTriggerValues(_triggerValues.x, _triggerValues.y-root.height/2); + activated.checked = true; + inputCh.checked = true; + updateMeasureUnits(); + } + + Connections { + target: AudioScope + onPauseChanged: { + if (!AudioScope.getPause()) { + pauseButton.text = "Pause"; + pauseButton.color = hifi.buttons.black; + AudioScope.setTriggered(false); + _triggered = false; + } else { + pauseButton.text = "Continue"; + pauseButton.color = hifi.buttons.blue; + } + } + onTriggered: { + _triggered = true; + collectTriggerData(); + AudioScope.setPause(true); + } + } +} diff --git a/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml b/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml index 5c9d6822c8..0e0786975e 100644 --- a/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml +++ b/interface/resources/qml/hifi/dialogs/content/AttachmentsContent.qml @@ -26,6 +26,7 @@ Item { } Connections { + id: onAttachmentsChangedConnection target: MyAvatar onAttachmentsChanged: reload() } @@ -34,6 +35,12 @@ Item { reload() } + function setAttachmentsVariant(attachments) { + onAttachmentsChangedConnection.enabled = false; + MyAvatar.setAttachmentsVariant(attachments); + onAttachmentsChangedConnection.enabled = true; + } + Column { width: pane.width @@ -92,11 +99,15 @@ Item { attachments.splice(index, 1); listView.model.remove(index, 1); } - onUpdateAttachment: MyAvatar.setAttachmentsVariant(attachments); + onUpdateAttachment: { + setAttachmentsVariant(attachments); + } } } - onCountChanged: MyAvatar.setAttachmentsVariant(attachments); + onCountChanged: { + setAttachmentsVariant(attachments); + } /* // DEBUG @@ -220,7 +231,7 @@ Item { }; attachments.push(template); listView.model.append({}); - MyAvatar.setAttachmentsVariant(attachments); + setAttachmentsVariant(attachments); } } @@ -250,7 +261,7 @@ Item { id: cancelAction text: "Cancel" onTriggered: { - MyAvatar.setAttachmentsVariant(originalAttachments); + setAttachmentsVariant(originalAttachments); closeDialog(); } } @@ -263,7 +274,7 @@ Item { console.log("Attachment " + i + ": " + attachments[i]); } - MyAvatar.setAttachmentsVariant(attachments); + setAttachmentsVariant(attachments); closeDialog(); } } diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index 9ec5cc6034..9bbb72357b 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -679,36 +679,16 @@ Menu::Menu() { }); auto audioIO = DependencyManager::get(); - addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoServerAudio, 0, false, - audioIO.data(), SLOT(toggleServerEcho())); - addCheckableActionToQMenuAndActionHash(audioDebugMenu, MenuOption::EchoLocalAudio, 0, false, - audioIO.data(), SLOT(toggleLocalEcho())); addActionToQMenuAndActionHash(audioDebugMenu, MenuOption::MuteEnvironment, 0, audioIO.data(), SLOT(sendMuteEnvironmentPacket())); - auto scope = DependencyManager::get(); - MenuWrapper* audioScopeMenu = audioDebugMenu->addMenu("Audio Scope"); - addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScope, Qt::CTRL | Qt::Key_F2, false, - scope.data(), SLOT(toggle())); - addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopePause, Qt::CTRL | Qt::SHIFT | Qt::Key_F2, false, - scope.data(), SLOT(togglePause())); - - addDisabledActionAndSeparator(audioScopeMenu, "Display Frames"); - { - QAction* fiveFrames = addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopeFiveFrames, - 0, true, scope.data(), SLOT(selectAudioScopeFiveFrames())); - - QAction* twentyFrames = addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopeTwentyFrames, - 0, false, scope.data(), SLOT(selectAudioScopeTwentyFrames())); - - QAction* fiftyFrames = addCheckableActionToQMenuAndActionHash(audioScopeMenu, MenuOption::AudioScopeFiftyFrames, - 0, false, scope.data(), SLOT(selectAudioScopeFiftyFrames())); - - QActionGroup* audioScopeFramesGroup = new QActionGroup(audioScopeMenu); - audioScopeFramesGroup->addAction(fiveFrames); - audioScopeFramesGroup->addAction(twentyFrames); - audioScopeFramesGroup->addAction(fiftyFrames); - } + action = addActionToQMenuAndActionHash(audioDebugMenu, MenuOption::AudioScope); + connect(action, &QAction::triggered, [] { + auto scriptEngines = DependencyManager::get(); + QUrl defaultScriptsLoc = PathUtils::defaultScriptsLocation(); + defaultScriptsLoc.setPath(defaultScriptsLoc.path() + "developer/utilities/audio/audioScope.js"); + scriptEngines->loadScript(defaultScriptsLoc.toString()); + }); // Developer > Physics >>> MenuWrapper* physicsOptionsMenu = developerMenu->addMenu("Physics"); diff --git a/interface/src/audio/AudioScope.cpp b/interface/src/audio/AudioScope.cpp index cf9984e32b..1a2e867d51 100644 --- a/interface/src/audio/AudioScope.cpp +++ b/interface/src/audio/AudioScope.cpp @@ -9,6 +9,7 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +#include #include #include @@ -21,13 +22,14 @@ #include "AudioScope.h" static const unsigned int DEFAULT_FRAMES_PER_SCOPE = 5; -static const unsigned int SCOPE_WIDTH = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL * DEFAULT_FRAMES_PER_SCOPE; static const unsigned int MULTIPLIER_SCOPE_HEIGHT = 20; static const unsigned int SCOPE_HEIGHT = 2 * 15 * MULTIPLIER_SCOPE_HEIGHT; AudioScope::AudioScope() : _isEnabled(false), _isPaused(false), + _isTriggered(false), + _autoTrigger(false), _scopeInputOffset(0), _scopeOutputOffset(0), _framesPerScope(DEFAULT_FRAMES_PER_SCOPE), @@ -43,6 +45,7 @@ AudioScope::AudioScope() : _outputRightD(DependencyManager::get()->allocateID()) { auto audioIO = DependencyManager::get(); + connect(&audioIO->getReceivedAudioStream(), &MixedProcessedAudioStream::addedSilence, this, &AudioScope::addStereoSilenceToScope); connect(&audioIO->getReceivedAudioStream(), &MixedProcessedAudioStream::addedLastFrameRepeatedWithFade, @@ -75,6 +78,18 @@ void AudioScope::selectAudioScopeFiftyFrames() { reallocateScope(50); } +void AudioScope::setLocalEcho(bool localEcho) { + DependencyManager::get()->setLocalEcho(localEcho); +} + +void AudioScope::setServerEcho(bool serverEcho) { + DependencyManager::get()->setServerEcho(serverEcho); +} + +float AudioScope::getFramesPerSecond(){ + return AudioConstants::NETWORK_FRAMES_PER_SEC; +} + void AudioScope::allocateScope() { _scopeInputOffset = 0; _scopeOutputOffset = 0; @@ -108,63 +123,14 @@ void AudioScope::freeScope() { } } -void AudioScope::render(RenderArgs* renderArgs, int width, int height) { - - if (!_isEnabled) { - return; - } - - static const glm::vec4 backgroundColor = { 0.4f, 0.4f, 0.4f, 0.6f }; - static const glm::vec4 gridColor = { 0.7f, 0.7f, 0.7f, 1.0f }; - static const glm::vec4 inputColor = { 0.3f, 1.0f, 0.3f, 1.0f }; - static const glm::vec4 outputLeftColor = { 1.0f, 0.3f, 0.3f, 1.0f }; - static const glm::vec4 outputRightColor = { 0.3f, 0.3f, 1.0f, 1.0f }; - static const int gridCols = 2; - int gridRows = _framesPerScope; - - int x = (width - (int)SCOPE_WIDTH) / 2; - int y = (height - (int)SCOPE_HEIGHT) / 2; - int w = (int)SCOPE_WIDTH; - int h = (int)SCOPE_HEIGHT; - - gpu::Batch& batch = *renderArgs->_batch; - auto geometryCache = DependencyManager::get(); - - // Grid uses its own pipeline, so draw it before setting another - const float GRID_EDGE = 0.005f; - geometryCache->renderGrid(batch, glm::vec2(x, y), glm::vec2(x + w, y + h), - gridRows, gridCols, GRID_EDGE, gridColor, true, _audioScopeGrid); - - geometryCache->useSimpleDrawPipeline(batch); - auto textureCache = DependencyManager::get(); - batch.setResourceTexture(0, textureCache->getWhiteTexture()); - - // FIXME - do we really need to reset this here? we know that we're called inside of ApplicationOverlay::renderOverlays - // which already set up our batch for us to have these settings - mat4 legacyProjection = glm::ortho(0, width, height, 0, -1000, 1000); - batch.setProjectionTransform(legacyProjection); - batch.setModelTransform(Transform()); - batch.resetViewTransform(); - - geometryCache->renderQuad(batch, x, y, w, h, backgroundColor, _audioScopeBackground); - renderLineStrip(batch, _inputID, inputColor, x, y, _samplesPerScope, _scopeInputOffset, _scopeInput); - renderLineStrip(batch, _outputLeftID, outputLeftColor, x, y, _samplesPerScope, _scopeOutputOffset, _scopeOutputLeft); - renderLineStrip(batch, _outputRightD, outputRightColor, x, y, _samplesPerScope, _scopeOutputOffset, _scopeOutputRight); -} - -void AudioScope::renderLineStrip(gpu::Batch& batch, int id, const glm::vec4& color, int x, int y, int n, int offset, const QByteArray* byteArray) { - +QVector AudioScope::getScopeVector(const QByteArray* byteArray, int offset) { int16_t sample; - int16_t* samples = ((int16_t*) byteArray->data()) + offset; + QVector points; + if (!_isEnabled || byteArray == NULL) return points; + int16_t* samples = ((int16_t*)byteArray->data()) + offset; int numSamplesToAverage = _framesPerScope / DEFAULT_FRAMES_PER_SCOPE; - int count = (n - offset) / numSamplesToAverage; - int remainder = (n - offset) % numSamplesToAverage; - y += SCOPE_HEIGHT / 2; - - auto geometryCache = DependencyManager::get(); - - QVector points; - + int count = (_samplesPerScope - offset) / numSamplesToAverage; + int remainder = (_samplesPerScope - offset) % numSamplesToAverage; // Compute and draw the sample averages from the offset position for (int i = count; --i >= 0; ) { @@ -173,7 +139,7 @@ void AudioScope::renderLineStrip(gpu::Batch& batch, int id, const glm::vec4& col sample += *samples++; } sample /= numSamplesToAverage; - points << glm::vec2(x++, y - sample); + points << -sample; } // Compute and draw the sample average across the wrap boundary @@ -182,16 +148,17 @@ void AudioScope::renderLineStrip(gpu::Batch& batch, int id, const glm::vec4& col for (int j = remainder; --j >= 0; ) { sample += *samples++; } - - samples = (int16_t*) byteArray->data(); + + samples = (int16_t*)byteArray->data(); for (int j = numSamplesToAverage - remainder; --j >= 0; ) { sample += *samples++; } sample /= numSamplesToAverage; - points << glm::vec2(x++, y - sample); - } else { - samples = (int16_t*) byteArray->data(); + points << -sample; + } + else { + samples = (int16_t*)byteArray->data(); } // Compute and draw the sample average from the beginning to the offset @@ -202,12 +169,51 @@ void AudioScope::renderLineStrip(gpu::Batch& batch, int id, const glm::vec4& col sample += *samples++; } sample /= numSamplesToAverage; - points << glm::vec2(x++, y - sample); + + points << -sample; + } + return points; +} + +bool AudioScope::shouldTrigger(const QVector& scope) { + int threshold = 4; + if (_autoTrigger && _triggerValues.x < scope.size()) { + for (int i = -4*threshold; i < +4*threshold; i++) { + int idx = _triggerValues.x + i; + idx = (idx < 0) ? 0 : (idx < scope.size() ? idx : scope.size() - 1); + int dif = abs(_triggerValues.y - scope[idx]); + if (dif < threshold) { + return true; + } + } + } + return false; +} + +void AudioScope::storeTriggerValues() { + _triggerInputData = _scopeInputData; + _triggerOutputLeftData = _scopeOutputLeftData; + _triggerOutputRightData = _scopeOutputRightData; + _isTriggered = true; + emit triggered(); +} + +void AudioScope::computeInputData() { + _scopeInputData = getScopeVector(_scopeInput, _scopeInputOffset); + if (shouldTrigger(_scopeInputData)) { + storeTriggerValues(); + } +} + +void AudioScope::computeOutputData() { + _scopeOutputLeftData = getScopeVector(_scopeOutputLeft, _scopeOutputOffset); + if (shouldTrigger(_scopeOutputLeftData)) { + storeTriggerValues(); + } + _scopeOutputRightData = getScopeVector(_scopeOutputRight, _scopeOutputOffset); + if (shouldTrigger(_scopeOutputRightData)) { + storeTriggerValues(); } - - - geometryCache->updateVertices(id, points, color); - geometryCache->renderVertices(batch, gpu::LINE_STRIP, id); } int AudioScope::addBufferToScope(QByteArray* byteArray, int frameOffset, const int16_t* source, int sourceSamplesPerChannel, @@ -231,7 +237,7 @@ int AudioScope::addBufferToScope(QByteArray* byteArray, int frameOffset, const i } int AudioScope::addSilenceToScope(QByteArray* byteArray, int frameOffset, int silentSamples) { - + // Short int pointer to mapped samples in byte array int16_t* destination = (int16_t*)byteArray->data(); @@ -271,6 +277,7 @@ void AudioScope::addStereoSamplesToScope(const QByteArray& samples) { _scopeOutputOffset = addBufferToScope(_scopeOutputRight, _scopeOutputOffset, samplesData, samplesPerChannel, 1, AudioConstants::STEREO); _scopeLastFrame = samples.right(AudioConstants::NETWORK_FRAME_BYTES_STEREO); + computeOutputData(); } void AudioScope::addLastFrameRepeatedWithFadeToScope(int samplesPerChannel) { @@ -302,4 +309,5 @@ void AudioScope::addInputToScope(const QByteArray& inputSamples) { _scopeInputOffset = addBufferToScope(_scopeInput, _scopeInputOffset, reinterpret_cast(inputSamples.data()), inputSamples.size() / sizeof(int16_t), INPUT_AUDIO_CHANNEL, NUM_INPUT_CHANNELS); + computeInputData(); } diff --git a/interface/src/audio/AudioScope.h b/interface/src/audio/AudioScope.h index e0c8840bb2..e99b8378e3 100644 --- a/interface/src/audio/AudioScope.h +++ b/interface/src/audio/AudioScope.h @@ -24,27 +24,60 @@ class AudioScope : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY + + Q_PROPERTY(QVector scopeInput READ getScopeInput) + Q_PROPERTY(QVector scopeOutputLeft READ getScopeOutputLeft) + Q_PROPERTY(QVector scopeOutputRight READ getScopeOutputRight) + + Q_PROPERTY(QVector triggerInput READ getTriggerInput) + Q_PROPERTY(QVector triggerOutputLeft READ getTriggerOutputLeft) + Q_PROPERTY(QVector triggerOutputRight READ getTriggerOutputRight) + public: // Audio scope methods for allocation/deallocation void allocateScope(); void freeScope(); void reallocateScope(int frames); - void render(RenderArgs* renderArgs, int width, int height); - public slots: void toggle() { setVisible(!_isEnabled); } void setVisible(bool visible); bool getVisible() const { return _isEnabled; } - void togglePause() { _isPaused = !_isPaused; } - void setPause(bool paused) { _isPaused = paused; } + void togglePause() { setPause(!_isPaused); } + void setPause(bool paused) { _isPaused = paused; emit pauseChanged(); } bool getPause() { return _isPaused; } + void toggleTrigger() { _autoTrigger = !_autoTrigger; } + bool getAutoTrigger() { return _autoTrigger; } + void setAutoTrigger(bool autoTrigger) { _isTriggered = false; _autoTrigger = autoTrigger; } + + void setTriggerValues(int x, int y) { _triggerValues.x = x; _triggerValues.y = y; } + void setTriggered(bool triggered) { _isTriggered = triggered; } + bool getTriggered() { return _isTriggered; } + + float getFramesPerSecond(); + int getFramesPerScope() { return _framesPerScope; } + void selectAudioScopeFiveFrames(); void selectAudioScopeTwentyFrames(); void selectAudioScopeFiftyFrames(); - + + QVector getScopeInput() { return _scopeInputData; }; + QVector getScopeOutputLeft() { return _scopeOutputLeftData; }; + QVector getScopeOutputRight() { return _scopeOutputRightData; }; + + QVector getTriggerInput() { return _triggerInputData; }; + QVector getTriggerOutputLeft() { return _triggerOutputLeftData; }; + QVector getTriggerOutputRight() { return _triggerOutputRightData; }; + + void setLocalEcho(bool serverEcho); + void setServerEcho(bool serverEcho); + +signals: + void pauseChanged(); + void triggered(); + protected: AudioScope(); @@ -55,24 +88,44 @@ private slots: void addInputToScope(const QByteArray& inputSamples); private: - // Audio scope methods for rendering - void renderLineStrip(gpu::Batch& batch, int id, const glm::vec4& color, int x, int y, int n, int offset, const QByteArray* byteArray); // Audio scope methods for data acquisition int addBufferToScope(QByteArray* byteArray, int frameOffset, const int16_t* source, int sourceSamples, unsigned int sourceChannel, unsigned int sourceNumberOfChannels, float fade = 1.0f); int addSilenceToScope(QByteArray* byteArray, int frameOffset, int silentSamples); + QVector getScopeVector(const QByteArray* scope, int offset); + + bool shouldTrigger(const QVector& scope); + void computeInputData(); + void computeOutputData(); + + void storeTriggerValues(); + bool _isEnabled; bool _isPaused; + bool _isTriggered; + bool _autoTrigger; int _scopeInputOffset; int _scopeOutputOffset; int _framesPerScope; int _samplesPerScope; + QByteArray* _scopeInput; QByteArray* _scopeOutputLeft; QByteArray* _scopeOutputRight; QByteArray _scopeLastFrame; + + QVector _scopeInputData; + QVector _scopeOutputLeftData; + QVector _scopeOutputRightData; + + QVector _triggerInputData; + QVector _triggerOutputLeftData; + QVector _triggerOutputRightData; + + + glm::ivec2 _triggerValues; int _audioScopeBackground; int _audioScopeGrid; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 1623fa3213..c874205611 100755 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -114,7 +114,8 @@ MyAvatar::MyAvatar(QThread* thread) : _skeletonModel = std::make_shared(this, nullptr); connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); - + connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady); + connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset); using namespace recording; _skeletonModel->flagAsCauterized(); diff --git a/interface/src/scripting/Audio.cpp b/interface/src/scripting/Audio.cpp index f9c1a95fb5..be9b4280f7 100644 --- a/interface/src/scripting/Audio.cpp +++ b/interface/src/scripting/Audio.cpp @@ -58,6 +58,21 @@ Audio::Audio() : _devices(_contextIsHMD) { enableNoiseReduction(enableNoiseReductionSetting.get()); } +bool Audio::startRecording(const QString& filepath) { + auto client = DependencyManager::get().data(); + return client->startRecording(filepath); +} + +bool Audio::getRecording() { + auto client = DependencyManager::get().data(); + return client->getRecording(); +} + +void Audio::stopRecording() { + auto client = DependencyManager::get().data(); + client->stopRecording(); +} + void Audio::setMuted(bool isMuted) { if (_isMuted != isMuted) { auto client = DependencyManager::get().data(); diff --git a/interface/src/scripting/Audio.h b/interface/src/scripting/Audio.h index abd2312cf0..0f0043510c 100644 --- a/interface/src/scripting/Audio.h +++ b/interface/src/scripting/Audio.h @@ -16,6 +16,7 @@ #include "AudioDevices.h" #include "AudioEffectOptions.h" #include "SettingHandle.h" +#include "AudioFileWav.h" namespace scripting { @@ -55,6 +56,10 @@ public: Q_INVOKABLE void setReverb(bool enable); Q_INVOKABLE void setReverbOptions(const AudioEffectOptions* options); + Q_INVOKABLE bool startRecording(const QString& filename); + Q_INVOKABLE void stopRecording(); + Q_INVOKABLE bool getRecording(); + signals: void nop(); void mutedChanged(bool isMuted); @@ -83,7 +88,6 @@ private: bool _isMuted { false }; bool _enableNoiseReduction { true }; // Match default value of AudioClient::_isNoiseGateEnabled. bool _contextIsHMD { false }; - AudioDevices* getDevices() { return &_devices; } AudioDevices _devices; }; diff --git a/interface/src/ui/ApplicationOverlay.cpp b/interface/src/ui/ApplicationOverlay.cpp index a99fe002ee..52b53a3298 100644 --- a/interface/src/ui/ApplicationOverlay.cpp +++ b/interface/src/ui/ApplicationOverlay.cpp @@ -82,7 +82,6 @@ void ApplicationOverlay::renderOverlay(RenderArgs* renderArgs) { // Now render the overlay components together into a single texture renderDomainConnectionStatusBorder(renderArgs); // renders the connected domain line - renderAudioScope(renderArgs); // audio scope in the very back - NOTE: this is the debug audio scope, not the VU meter renderOverlays(renderArgs); // renders Scripts Overlay and AudioScope renderQmlUi(renderArgs); // renders a unit quad with the QML UI texture, and the text overlays from scripts }); @@ -118,25 +117,6 @@ void ApplicationOverlay::renderQmlUi(RenderArgs* renderArgs) { geometryCache->renderUnitQuad(batch, glm::vec4(1), _qmlGeometryId); } -void ApplicationOverlay::renderAudioScope(RenderArgs* renderArgs) { - PROFILE_RANGE(app, __FUNCTION__); - - gpu::Batch& batch = *renderArgs->_batch; - auto geometryCache = DependencyManager::get(); - geometryCache->useSimpleDrawPipeline(batch); - auto textureCache = DependencyManager::get(); - batch.setResourceTexture(0, textureCache->getWhiteTexture()); - int width = renderArgs->_viewport.z; - int height = renderArgs->_viewport.w; - mat4 legacyProjection = glm::ortho(0, width, height, 0, ORTHO_NEAR_CLIP, ORTHO_FAR_CLIP); - batch.setProjectionTransform(legacyProjection); - batch.setModelTransform(Transform()); - batch.resetViewTransform(); - - // Render the audio scope - DependencyManager::get()->render(renderArgs, width, height); -} - void ApplicationOverlay::renderOverlays(RenderArgs* renderArgs) { PROFILE_RANGE(app, __FUNCTION__); diff --git a/interface/src/ui/ApplicationOverlay.h b/interface/src/ui/ApplicationOverlay.h index af4d8779d4..0d30123c61 100644 --- a/interface/src/ui/ApplicationOverlay.h +++ b/interface/src/ui/ApplicationOverlay.h @@ -32,7 +32,6 @@ private: void renderStatsAndLogs(RenderArgs* renderArgs); void renderDomainConnectionStatusBorder(RenderArgs* renderArgs); void renderQmlUi(RenderArgs* renderArgs); - void renderAudioScope(RenderArgs* renderArgs); void renderOverlays(RenderArgs* renderArgs); void buildFramebufferObject(); diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 78475f5b68..af86499101 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -79,6 +79,7 @@ Setting::Handle staticJitterBufferFrames("staticJitterBufferFrames", using Mutex = std::mutex; using Lock = std::unique_lock; Mutex _deviceMutex; +Mutex _recordMutex; // thread-safe QList getAvailableDevices(QAudio::Mode mode) { @@ -222,8 +223,7 @@ AudioClient::AudioClient() : // initialize wasapi; if getAvailableDevices is called from the CheckDevicesThread before this, it will crash getAvailableDevices(QAudio::AudioInput); getAvailableDevices(QAudio::AudioOutput); - - + // start a thread to detect any device changes _checkDevicesTimer = new QTimer(this); connect(_checkDevicesTimer, &QTimer::timeout, [this] { @@ -1845,11 +1845,9 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable(), samplesRequested); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); - for (int i = 0; i < networkSamplesPopped; i++) { mixBuffer[i] = convertToFloat(scratchBuffer[i]); } - samplesRequested = networkSamplesPopped; } @@ -1911,6 +1909,13 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { bytesWritten = maxSize; } + // send output buffer for recording + if (_audio->_isRecording) { + Lock lock(_recordMutex); + _audio->_audioFileWav.addRawAudioChunk(reinterpret_cast(scratchBuffer), bytesWritten); + } + + int bytesAudioOutputUnplayed = _audio->_audioOutput->bufferSize() - _audio->_audioOutput->bytesFree(); float msecsAudioOutputUnplayed = bytesAudioOutputUnplayed / (float)_audio->_outputFormat.bytesForDuration(USECS_PER_MSEC); _audio->_stats.updateOutputMsUnplayed(msecsAudioOutputUnplayed); @@ -1922,6 +1927,22 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { return bytesWritten; } +bool AudioClient::startRecording(const QString& filepath) { + if (!_audioFileWav.create(_outputFormat, filepath)) { + qDebug() << "Error creating audio file: " + filepath; + return false; + } + _isRecording = true; + return true; +} + +void AudioClient::stopRecording() { + if (_isRecording) { + _isRecording = false; + _audioFileWav.close(); + } +} + void AudioClient::loadSettings() { _receivedAudioStream.setDynamicJitterBufferEnabled(dynamicJitterBufferEnabled.get()); _receivedAudioStream.setStaticJitterBufferFrames(staticJitterBufferFrames.get()); diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 01a487455c..0ceb9c4dc3 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -47,11 +47,13 @@ #include #include + #include #include #include "AudioIOStats.h" +#include "AudioFileWav.h" #ifdef _WIN32 #pragma warning( push ) @@ -67,7 +69,6 @@ class QAudioInput; class QAudioOutput; class QIODevice; - class Transform; class NLPacket; @@ -118,6 +119,8 @@ public: const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; } MixedProcessedAudioStream& getReceivedAudioStream() { return _receivedAudioStream; } + const QAudioFormat& getOutputFormat() const { return _outputFormat; } + float getLastInputLoudness() const { return _lastInputLoudness; } // TODO: relative to noise floor? float getTimeSinceLastClip() const { return _timeSinceLastClip; } @@ -142,7 +145,7 @@ public: void setIsPlayingBackRecording(bool isPlayingBackRecording) { _isPlayingBackRecording = isPlayingBackRecording; } Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); - + bool outputLocalInjector(const AudioInjectorPointer& injector) override; QAudioDeviceInfo getActiveAudioDevice(QAudio::Mode mode) const; @@ -155,6 +158,13 @@ public: bool getNamedAudioDeviceForModeExists(QAudio::Mode mode, const QString& deviceName); + void setRecording(bool isRecording) { _isRecording = isRecording; }; + bool getRecording() { return _isRecording; }; + + bool startRecording(const QString& filename); + void stopRecording(); + + #ifdef Q_OS_WIN static QString getWinDeviceName(wchar_t* guid); #endif @@ -184,13 +194,17 @@ public slots: void toggleMute(); bool isMuted() { return _muted; } - virtual void setIsStereoInput(bool stereo) override; void setNoiseReduction(bool isNoiseGateEnabled); bool isNoiseReductionEnabled() const { return _isNoiseGateEnabled; } + bool getLocalEcho() { return _shouldEchoLocally; } + void setLocalEcho(bool localEcho) { _shouldEchoLocally = localEcho; } void toggleLocalEcho() { _shouldEchoLocally = !_shouldEchoLocally; } + + bool getServerEcho() { return _shouldEchoToServer; } + void setServerEcho(bool serverEcho) { _shouldEchoToServer = serverEcho; } void toggleServerEcho() { _shouldEchoToServer = !_shouldEchoToServer; } void processReceivedSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer); @@ -239,6 +253,8 @@ signals: void muteEnvironmentRequested(glm::vec3 position, float radius); + void outputBufferReceived(const QByteArray _outputBuffer); + protected: AudioClient(); ~AudioClient(); @@ -354,9 +370,8 @@ private: int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; float* _localOutputMixBuffer { NULL }; Mutex _localAudioMutex; - AudioLimiter _audioLimiter; - + // Adds Reverb void configureReverb(); void updateReverbOptions(); @@ -391,6 +406,8 @@ private: QList _inputDevices; QList _outputDevices; + AudioFileWav _audioFileWav; + bool _hasReceivedFirstPacket { false }; QVector _activeLocalAudioInjectors; @@ -412,6 +429,8 @@ private: QTimer* _checkDevicesTimer { nullptr }; QTimer* _checkPeakValuesTimer { nullptr }; + + bool _isRecording { false }; }; diff --git a/libraries/audio-client/src/AudioFileWav.cpp b/libraries/audio-client/src/AudioFileWav.cpp new file mode 100644 index 0000000000..613628883c --- /dev/null +++ b/libraries/audio-client/src/AudioFileWav.cpp @@ -0,0 +1,69 @@ +// +// AudioWavFile.h +// libraries/audio-client/src +// +// Created by Luis Cuenca on 12/1/2017. +// 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 +// + +#include "AudioFileWav.h" + +bool AudioFileWav::create(const QAudioFormat& audioFormat, const QString& filepath) { + if (_file.isOpen()) { + _file.close(); + } + _file.setFileName(filepath); + if (!_file.open(QIODevice::WriteOnly)) { + return false; + } + addHeader(audioFormat); + return true; +} + +bool AudioFileWav::addRawAudioChunk(char* chunk, int size) { + if (_file.isOpen()) { + QDataStream stream(&_file); + stream.writeRawData(chunk, size); + return true; + } + return false; +} + +void AudioFileWav::close() { + QDataStream stream(&_file); + stream.setByteOrder(QDataStream::LittleEndian); + + // fill RIFF and size data on header + _file.seek(4); + stream << quint32(_file.size() - 8); + _file.seek(40); + stream << quint32(_file.size() - 44); + _file.close(); +} + +void AudioFileWav::addHeader(const QAudioFormat& audioFormat) { + QDataStream stream(&_file); + + stream.setByteOrder(QDataStream::LittleEndian); + + // RIFF + stream.writeRawData("RIFF", 4); + stream << quint32(0); + stream.writeRawData("WAVE", 4); + + // Format description PCM = 16 + stream.writeRawData("fmt ", 4); + stream << quint32(16); + stream << quint16(1); + stream << quint16(audioFormat.channelCount()); + stream << quint32(audioFormat.sampleRate()); + stream << quint32(audioFormat.sampleRate() * audioFormat.channelCount() * audioFormat.sampleSize() / 8); // bytes per second + stream << quint16(audioFormat.channelCount() * audioFormat.sampleSize() / 8); // block align + stream << quint16(audioFormat.sampleSize()); // bits Per Sample + // Init data chunck + stream.writeRawData("data", 4); + stream << quint32(0); +} diff --git a/libraries/audio-client/src/AudioFileWav.h b/libraries/audio-client/src/AudioFileWav.h new file mode 100644 index 0000000000..7e9c83a23b --- /dev/null +++ b/libraries/audio-client/src/AudioFileWav.h @@ -0,0 +1,34 @@ +// +// AudioWavFile.h +// libraries/audio-client/src +// +// Created by Luis Cuenca on 12/1/2017. +// 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 +// + +#ifndef hifi_AudioFileWav_h +#define hifi_AudioFileWav_h + +#include +#include +#include +#include +#include + +class AudioFileWav : public QObject { + Q_OBJECT +public: + AudioFileWav() {} + bool create(const QAudioFormat& audioFormat, const QString& filepath); + bool addRawAudioChunk(char* chunk, int size); + void close(); + +private: + void addHeader(const QAudioFormat& audioFormat); + QFile _file; +}; + +#endif // hifi_AudioFileWav_h \ No newline at end of file diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp index 437ac623e3..bb7f141cd9 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.cpp @@ -1217,6 +1217,15 @@ void Avatar::setModelURLFinished(bool success) { } } +// rig is ready +void Avatar::rigReady() { + buildUnscaledEyeHeightCache(); +} + +// rig has been reset. +void Avatar::rigReset() { + clearUnscaledEyeHeightCache(); +} // create new model, can return an instance of a SoftAttachmentModel rather then Model static std::shared_ptr allocateAttachmentModel(bool isSoft, const Rig& rigOverride, bool isCauterized) { @@ -1584,18 +1593,17 @@ void Avatar::ensureInScene(AvatarSharedPointer self, const render::ScenePointer& } } +// thread-safe float Avatar::getEyeHeight() const { - - if (QThread::currentThread() != thread()) { - float result = DEFAULT_AVATAR_EYE_HEIGHT; - BLOCKING_INVOKE_METHOD(const_cast(this), "getEyeHeight", Q_RETURN_ARG(float, result)); - return result; - } - return getModelScale() * getUnscaledEyeHeight(); } +// thread-safe float Avatar::getUnscaledEyeHeight() const { + return _unscaledEyeHeightCache.get(); +} + +void Avatar::buildUnscaledEyeHeightCache() { float skeletonHeight = getUnscaledEyeHeightFromSkeleton(); // Sanity check by looking at the model extents. @@ -1606,12 +1614,16 @@ float Avatar::getUnscaledEyeHeight() const { // This helps prevent absurdly large avatars from exceeding the domain height limit. const float MESH_SLOP_RATIO = 1.5f; if (meshHeight > skeletonHeight * MESH_SLOP_RATIO) { - return meshHeight; + _unscaledEyeHeightCache.set(meshHeight); } else { - return skeletonHeight; + _unscaledEyeHeightCache.set(skeletonHeight); } } +void Avatar::clearUnscaledEyeHeightCache() { + _unscaledEyeHeightCache.set(DEFAULT_AVATAR_EYE_HEIGHT); +} + float Avatar::getUnscaledEyeHeightFromSkeleton() const { // TODO: if performance becomes a concern we can cache this value rather then computing it everytime. diff --git a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h index 44d4ef4c51..c75b54fdc4 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/Avatar.h +++ b/libraries/avatars-renderer/src/avatars-renderer/Avatar.h @@ -257,8 +257,8 @@ public: Q_INVOKABLE virtual float getEyeHeight() const override; - // returns eye height of avatar in meters, ignoreing avatar scale. - // if _targetScale is 1 then this will be identical to getEyeHeight; + // returns eye height of avatar in meters, ignoring avatar scale. + // if _targetScale is 1 then this will be identical to getEyeHeight. virtual float getUnscaledEyeHeight() const override; // returns true, if an acurate eye height estimage can be obtained by inspecting the avatar model skeleton and geometry, @@ -280,10 +280,17 @@ public slots: glm::vec3 getRightPalmPosition() const; glm::quat getRightPalmRotation() const; + // hooked up to Model::setURLFinished signal void setModelURLFinished(bool success); + // hooked up to Model::rigReady & rigReset signals + void rigReady(); + void rigReset(); + protected: float getUnscaledEyeHeightFromSkeleton() const; + void buildUnscaledEyeHeightCache(); + void clearUnscaledEyeHeightCache(); virtual const QString& getSessionDisplayNameForTransport() const override { return _empty; } // Save a tiny bit of bandwidth. Mixer won't look at what we send. QString _empty{}; virtual void maybeUpdateSessionDisplayNameFromTransport(const QString& sessionDisplayName) override { _sessionDisplayName = sessionDisplayName; } // don't use no-op setter! @@ -384,6 +391,8 @@ protected: float _displayNameTargetAlpha { 1.0f }; float _displayNameAlpha { 1.0f }; + + ThreadSafeValueCache _unscaledEyeHeightCache { DEFAULT_AVATAR_EYE_HEIGHT }; }; #endif // hifi_Avatar_h diff --git a/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.cpp b/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.cpp index e870e2de12..4382216575 100644 --- a/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.cpp +++ b/libraries/avatars-renderer/src/avatars-renderer/OtherAvatar.cpp @@ -13,4 +13,6 @@ OtherAvatar::OtherAvatar(QThread* thread) : Avatar(thread) { _headData = new Head(this); _skeletonModel = std::make_shared(this, nullptr); connect(_skeletonModel.get(), &Model::setURLFinished, this, &Avatar::setModelURLFinished); + connect(_skeletonModel.get(), &Model::rigReady, this, &Avatar::rigReady); + connect(_skeletonModel.get(), &Model::rigReset, this, &Avatar::rigReset); } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index de98675733..d7dd2837cb 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -483,8 +483,22 @@ public: virtual void setTargetScale(float targetScale); float getDomainLimitedScale() const; - float getDomainMinScale() const; - float getDomainMaxScale() const; + + /**jsdoc + * returns the minimum scale allowed for this avatar in the current domain. + * This value can change as the user changes avatars or when changing domains. + * @function AvatarData.getDomainMinScale + * @returns {number} minimum scale allowed for this avatar in the current domain. + */ + Q_INVOKABLE float getDomainMinScale() const; + + /**jsdoc + * returns the maximum scale allowed for this avatar in the current domain. + * This value can change as the user changes avatars or when changing domains. + * @function AvatarData.getDomainMaxScale + * @returns {number} maximum scale allowed for this avatar in the current domain. + */ + Q_INVOKABLE float getDomainMaxScale() const; // returns eye height of avatar in meters, ignoreing avatar scale. // if _targetScale is 1 then this will be identical to getEyeHeight; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 199cb29f53..c4bc435691 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -286,6 +286,7 @@ void Model::reset() { if (isLoaded()) { const FBXGeometry& geometry = getFBXGeometry(); _rig.reset(geometry); + emit rigReset(); } } @@ -322,6 +323,7 @@ bool Model::updateGeometry() { _blendedVertexBuffers.push_back(buffer); } needFullUpdate = true; + emit rigReady(); } return needFullUpdate; } diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index fadd44b745..7568a17342 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -273,6 +273,8 @@ signals: void setURLFinished(bool success); void setCollisionModelURLFinished(bool success); void requestRenderUpdate(); + void rigReady(); + void rigReset(); protected: diff --git a/scripts/developer/utilities/audio/audioScope.js b/scripts/developer/utilities/audio/audioScope.js new file mode 100644 index 0000000000..00c9e4b725 --- /dev/null +++ b/scripts/developer/utilities/audio/audioScope.js @@ -0,0 +1,17 @@ +var qml = Script.resourcesPath() + '/qml/AudioScope.qml'; +var window = new OverlayWindow({ + title: 'Audio Scope', + source: qml, + width: 1200, + height: 500 +}); +window.closed.connect(function () { + if (Audio.getRecording()) { + Audio.stopRecording(); + } + AudioScope.setVisible(false); + AudioScope.setLocalEcho(false); + AudioScope.setServerEcho(false); + AudioScope.selectAudioScopeFiveFrames(); + Script.stop(); +}); \ No newline at end of file diff --git a/scripts/system/controllers/controllerModules/equipEntity.js b/scripts/system/controllers/controllerModules/equipEntity.js index b8c20d5bd6..a250f77b2e 100644 --- a/scripts/system/controllers/controllerModules/equipEntity.js +++ b/scripts/system/controllers/controllerModules/equipEntity.js @@ -10,7 +10,7 @@ getControllerJointIndex, enableDispatcherModule, disableDispatcherModule, Messages, makeDispatcherModuleParameters, makeRunningValues, Settings, entityHasActions, Vec3, Overlays, flatten, Xform, getControllerWorldLocation, ensureDynamic, entityIsCloneable, - cloneEntity, DISPATCHER_PROPERTIES + cloneEntity, DISPATCHER_PROPERTIES, TEAR_AWAY_DISTANCE */ Script.include("/~/system/libraries/Xform.js"); @@ -138,9 +138,9 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var dimensions; if (overlayInfoSet.type === "sphere") { - dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize * EQUIP_SPHERE_SCALE_FACTOR; + dimensions = (overlayInfoSet.hotspot.radius / 2) * overlayInfoSet.currentSize * EQUIP_SPHERE_SCALE_FACTOR; } else { - dimensions = overlayInfoSet.hotspot.radius * 2 * overlayInfoSet.currentSize; + dimensions = (overlayInfoSet.hotspot.radius / 2) * overlayInfoSet.currentSize; } overlayInfoSet.overlays.forEach(function(overlay) { @@ -162,7 +162,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa var ATTACH_POINT_SETTINGS = "io.highfidelity.attachPoints"; - var EQUIP_RADIUS = 0.2; // radius used for palm vs equip-hotspot for equipping. + var EQUIP_RADIUS = 1.0; // radius used for palm vs equip-hotspot for equipping. var HAPTIC_PULSE_STRENGTH = 1.0; var HAPTIC_PULSE_DURATION = 13.0; @@ -322,7 +322,9 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa } } else { var wearableProps = getWearableData(props); + var sensorToScaleFactor = MyAvatar.sensorToWorldScale; if (wearableProps && wearableProps.joints) { + result.push({ key: entityID.toString() + "0", entityID: entityID, @@ -332,7 +334,7 @@ EquipHotspotBuddy.prototype.update = function(deltaTime, timestamp, controllerDa z: 0 }, worldPosition: entityXform.pos, - radius: EQUIP_RADIUS, + radius: EQUIP_RADIUS * sensorToScaleFactor, joints: wearableProps.joints, modelURL: null, modelScale: null diff --git a/scripts/system/controllers/controllerModules/scaleAvatar.js b/scripts/system/controllers/controllerModules/scaleAvatar.js index e4ae3654e1..1868b0228a 100644 --- a/scripts/system/controllers/controllerModules/scaleAvatar.js +++ b/scripts/system/controllers/controllerModules/scaleAvatar.js @@ -12,6 +12,10 @@ (function () { var dispatcherUtils = Script.require("/~/system/libraries/controllerDispatcherUtils.js"); + function clamp(val, min, max) { + return Math.max(min, Math.min(max, val)); + } + function ScaleAvatar(hand) { this.hand = hand; this.scalingStartAvatarScale = 0; @@ -61,7 +65,7 @@ controllerData.controllerLocations[this.otherHand()].position)); var newAvatarScale = (scalingCurrentDistance / this.scalingStartDistance) * this.scalingStartAvatarScale; - MyAvatar.scale = newAvatarScale; + MyAvatar.scale = clamp(newAvatarScale, MyAvatar.getDomainMinScale(), MyAvatar.getDomainMaxScale()); MyAvatar.scaleChanged(); } return dispatcherUtils.makeRunningValues(true, [], []); diff --git a/scripts/system/html/js/marketplacesInject.js b/scripts/system/html/js/marketplacesInject.js index 1346bcd750..878c3b51f1 100644 --- a/scripts/system/html/js/marketplacesInject.js +++ b/scripts/system/html/js/marketplacesInject.js @@ -327,6 +327,19 @@ }); } + // fix for 10108 - marketplace category cannot scroll + function injectAddScrollbarToCategories() { + $('#categories-dropdown').on('show.bs.dropdown', function () { + $('body > div.container').css('display', 'none') + $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': 'auto', 'height': 'calc(100vh - 110px)' }) + }); + + $('#categories-dropdown').on('hide.bs.dropdown', function () { + $('body > div.container').css('display', '') + $('#categories-dropdown > ul.dropdown-menu').css({ 'overflow': '', 'height': '' }) + }); + } + function injectHiFiCode() { if (commerceMode) { maybeAddLogInButton(); @@ -358,6 +371,7 @@ } injectUnfocusOnSearch(); + injectAddScrollbarToCategories(); } function injectHiFiItemPageCode() { diff --git a/scripts/system/libraries/entitySelectionTool.js b/scripts/system/libraries/entitySelectionTool.js index d947a1d397..b8ba146757 100644 --- a/scripts/system/libraries/entitySelectionTool.js +++ b/scripts/system/libraries/entitySelectionTool.js @@ -1031,6 +1031,71 @@ SelectionDisplay = (function() { that.updateHandles(); }; + // Function: Calculate New Bound Extremes + // uses dot product to discover new top and bottom on the new referential (max and min) + that.calculateNewBoundExtremes = function(boundPointList, referenceVector) { + + if (boundPointList.length < 2) { + return [null, null]; + } + + var refMax = boundPointList[0]; + var refMin = boundPointList[1]; + + var dotMax = Vec3.dot(boundPointList[0], referenceVector); + var dotMin = Vec3.dot(boundPointList[1], referenceVector); + + if (dotMin > dotMax) { + dotMax = dotMin; + dotMin = Vec3.dot(boundPointList[0], referenceVector); + refMax = boundPointList[1]; + refMin = boundPointList[0]; + } + + for (var i = 2; i < boundPointList.length ; i++) { + var dotAux = Vec3.dot(boundPointList[i], referenceVector); + if (dotAux > dotMax) { + dotMax = dotAux; + refMax = boundPointList[i]; + } else if (dotAux < dotMin) { + dotMin = dotAux; + refMin = boundPointList[i]; + } + } + return [refMin, refMax]; + } + + // Function: Project Bounding Box Points + // Projects all 6 bounding box points: Top, Bottom, Left, Right, Near, Far (assumes center 0,0,0) onto + // one of the basis of the new avatar referencial + // dimensions - dimensions of the AABB (axis aligned bounding box) on the standard basis + // [1, 0, 0], [0, 1, 0], [0, 0, 1] + // v - projection vector + // rotateHandleOffset - offset for the rotation handle gizmo position + that.projectBoundingBoxPoints = function(dimensions, v, rotateHandleOffset) { + var projT_v = Vec3.dot(Vec3.multiply((dimensions.y / 2) + rotateHandleOffset, Vec3.UNIT_Y), v); + projT_v = Vec3.multiply(projT_v, v); + + var projB_v = Vec3.dot(Vec3.multiply(-(dimensions.y / 2) - rotateHandleOffset, Vec3.UNIT_Y), v); + projB_v = Vec3.multiply(projB_v, v); + + var projL_v = Vec3.dot(Vec3.multiply((dimensions.x / 2) + rotateHandleOffset, Vec3.UNIT_X), v); + projL_v = Vec3.multiply(projL_v, v); + + var projR_v = Vec3.dot(Vec3.multiply(-1.0 * (dimensions.x / 2) - 1.0 * rotateHandleOffset, Vec3.UNIT_X), v); + projR_v = Vec3.multiply(projR_v, v); + + var projN_v = Vec3.dot(Vec3.multiply((dimensions.z / 2) + rotateHandleOffset, Vec3.FRONT), v); + projN_v = Vec3.multiply(projN_v, v); + + var projF_v = Vec3.dot(Vec3.multiply(-1.0 * (dimensions.z / 2) - 1.0 * rotateHandleOffset, Vec3.FRONT), v); + projF_v = Vec3.multiply(projF_v, v); + + var projList = [projT_v, projB_v, projL_v, projR_v, projN_v, projF_v]; + + return that.calculateNewBoundExtremes(projList, v); + }; + // FUNCTION: UPDATE ROTATION HANDLES that.updateRotationHandles = function() { var diagonal = (Vec3.length(SelectionManager.worldDimensions) / 2) * 1.1; @@ -1043,10 +1108,10 @@ SelectionDisplay = (function() { } else { outerAlpha = 0.5; } - + // prev 0.05 var rotateHandleOffset = 0.05; - var top, far, left, bottom, near, right, boundsCenter, objectCenter, BLN, BRN, BLF, TLN, TRN, TLF, TRF; + var boundsCenter, objectCenter; var dimensions, rotation; if (spaceMode === SPACE_LOCAL) { @@ -1058,280 +1123,66 @@ SelectionDisplay = (function() { dimensions = SelectionManager.worldDimensions; var position = objectCenter; - top = objectCenter.y + (dimensions.y / 2); - far = objectCenter.z + (dimensions.z / 2); - left = objectCenter.x + (dimensions.x / 2); - - bottom = objectCenter.y - (dimensions.y / 2); - near = objectCenter.z - (dimensions.z / 2); - right = objectCenter.x - (dimensions.x / 2); - boundsCenter = objectCenter; var yawCorner; var pitchCorner; var rollCorner; - // determine which bottom corner we are closest to - /*------------------------------ - example: - - BRF +--------+ BLF - | | - | | - BRN +--------+ BLN - - * - - ------------------------------*/ - var cameraPosition = Camera.getPosition(); - if (cameraPosition.x > objectCenter.x) { - // must be BRF or BRN - if (cameraPosition.z < objectCenter.z) { + var look = Vec3.normalize(Vec3.subtract(cameraPosition, objectCenter)); - yawHandleRotation = Quat.fromVec3Degrees({ - x: 270, - y: 90, - z: 0 - }); - pitchHandleRotation = Quat.fromVec3Degrees({ - x: 0, - y: 90, - z: 0 - }); - rollHandleRotation = Quat.fromVec3Degrees({ - x: 0, - y: 0, - z: 0 - }); + // place yaw, pitch and roll rotations on the avatar referential + + var avatarReferential = Quat.multiply(MyAvatar.orientation, Quat.fromVec3Degrees({ + x: 0, + y: 180, + z: 0 + })); + var upVector = Quat.getUp(avatarReferential); + var rightVector = Vec3.multiply(-1, Quat.getRight(avatarReferential)); + var frontVector = Quat.getFront(avatarReferential); + + // project all 6 bounding box points: Top, Bottom, Left, Right, Near, Far (assumes center 0,0,0) + // onto the new avatar referential - yawCorner = { - x: left + rotateHandleOffset, - y: bottom - rotateHandleOffset, - z: near - rotateHandleOffset - }; - - pitchCorner = { - x: right - rotateHandleOffset, - y: top + rotateHandleOffset, - z: near - rotateHandleOffset - }; - - rollCorner = { - x: left + rotateHandleOffset, - y: top + rotateHandleOffset, - z: far + rotateHandleOffset - }; - - yawCenter = { - x: boundsCenter.x, - y: bottom, - z: boundsCenter.z - }; - pitchCenter = { - x: right, - y: boundsCenter.y, - z: boundsCenter.z - }; - rollCenter = { - x: boundsCenter.x, - y: boundsCenter.y, - z: far - }; - - - Overlays.editOverlay(pitchHandle, { - url: ROTATE_ARROW_WEST_SOUTH_URL - }); - Overlays.editOverlay(rollHandle, { - url: ROTATE_ARROW_WEST_SOUTH_URL - }); - - - } else { - - yawHandleRotation = Quat.fromVec3Degrees({ - x: 270, - y: 0, - z: 0 - }); - pitchHandleRotation = Quat.fromVec3Degrees({ - x: 180, - y: 270, - z: 0 - }); - rollHandleRotation = Quat.fromVec3Degrees({ - x: 0, - y: 0, - z: 90 - }); - - yawCorner = { - x: left + rotateHandleOffset, - y: bottom - rotateHandleOffset, - z: far + rotateHandleOffset - }; - - pitchCorner = { - x: right - rotateHandleOffset, - y: top + rotateHandleOffset, - z: far + rotateHandleOffset - }; - - rollCorner = { - x: left + rotateHandleOffset, - y: top + rotateHandleOffset, - z: near - rotateHandleOffset - }; - - - yawCenter = { - x: boundsCenter.x, - y: bottom, - z: boundsCenter.z - }; - pitchCenter = { - x: right, - y: boundsCenter.y, - z: boundsCenter.z - }; - rollCenter = { - x: boundsCenter.x, - y: boundsCenter.y, - z: near - }; - - Overlays.editOverlay(pitchHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - Overlays.editOverlay(rollHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - } - } else { - - // must be BLF or BLN - if (cameraPosition.z < objectCenter.z) { - - yawHandleRotation = Quat.fromVec3Degrees({ - x: 270, - y: 180, - z: 0 - }); - pitchHandleRotation = Quat.fromVec3Degrees({ - x: 90, - y: 0, - z: 90 - }); - rollHandleRotation = Quat.fromVec3Degrees({ - x: 0, - y: 0, - z: 180 - }); - - yawCorner = { - x: right - rotateHandleOffset, - y: bottom - rotateHandleOffset, - z: near - rotateHandleOffset - }; - - pitchCorner = { - x: left + rotateHandleOffset, - y: top + rotateHandleOffset, - z: near - rotateHandleOffset - }; - - rollCorner = { - x: right - rotateHandleOffset, - y: top + rotateHandleOffset, - z: far + rotateHandleOffset - }; - - yawCenter = { - x: boundsCenter.x, - y: bottom, - z: boundsCenter.z - }; - pitchCenter = { - x: left, - y: boundsCenter.y, - z: boundsCenter.z - }; - rollCenter = { - x: boundsCenter.x, - y: boundsCenter.y, - z: far - }; - - Overlays.editOverlay(pitchHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - Overlays.editOverlay(rollHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - - } else { - - yawHandleRotation = Quat.fromVec3Degrees({ - x: 270, - y: 270, - z: 0 - }); - pitchHandleRotation = Quat.fromVec3Degrees({ - x: 180, - y: 270, - z: 0 - }); - rollHandleRotation = Quat.fromVec3Degrees({ - x: 0, - y: 0, - z: 180 - }); - - yawCorner = { - x: right - rotateHandleOffset, - y: bottom - rotateHandleOffset, - z: far + rotateHandleOffset - }; - - rollCorner = { - x: right - rotateHandleOffset, - y: top + rotateHandleOffset, - z: near - rotateHandleOffset - }; - - pitchCorner = { - x: left + rotateHandleOffset, - y: top + rotateHandleOffset, - z: far + rotateHandleOffset - }; - - yawCenter = { - x: boundsCenter.x, - y: bottom, - z: boundsCenter.z - }; - rollCenter = { - x: boundsCenter.x, - y: boundsCenter.y, - z: near - }; - pitchCenter = { - x: left, - y: boundsCenter.y, - z: boundsCenter.z - }; - - Overlays.editOverlay(pitchHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - Overlays.editOverlay(rollHandle, { - url: ROTATE_ARROW_WEST_NORTH_URL - }); - - } - } + // UP + var projUP = that.projectBoundingBoxPoints(dimensions, upVector, rotateHandleOffset); + // RIGHT + var projRIGHT = that.projectBoundingBoxPoints(dimensions, rightVector, rotateHandleOffset); + // FRONT + var projFRONT = that.projectBoundingBoxPoints(dimensions, frontVector, rotateHandleOffset); + + // YAW + yawCenter = Vec3.sum(boundsCenter, projUP[0]); + yawCorner = Vec3.sum(boundsCenter, Vec3.sum(Vec3.sum(projUP[0], projRIGHT[1]), projFRONT[1])); + + yawHandleRotation = Quat.lookAt( + yawCorner, + Vec3.sum(yawCorner, upVector), + Vec3.subtract(yawCenter,yawCorner)); + yawHandleRotation = Quat.multiply(Quat.angleAxis(45, upVector), yawHandleRotation); + + // PTCH + pitchCorner = Vec3.sum(boundsCenter, Vec3.sum(Vec3.sum(projUP[1], projRIGHT[0]), projFRONT[1])); + pitchCenter = Vec3.sum(boundsCenter, projRIGHT[0]); + + pitchHandleRotation = Quat.lookAt( + pitchCorner, + Vec3.sum(pitchCorner, rightVector), + Vec3.subtract(pitchCenter,pitchCorner)); + pitchHandleRotation = Quat.multiply(Quat.angleAxis(45, rightVector), pitchHandleRotation); + + // ROLL + rollCorner = Vec3.sum(boundsCenter, Vec3.sum(Vec3.sum(projUP[1], projRIGHT[1]), projFRONT[0])); + rollCenter = Vec3.sum(boundsCenter, projFRONT[0]); + + rollHandleRotation = Quat.lookAt( + rollCorner, + Vec3.sum(rollCorner, frontVector), + Vec3.subtract(rollCenter,rollCorner)); + rollHandleRotation = Quat.multiply(Quat.angleAxis(45, frontVector), rollHandleRotation); + var rotateHandlesVisible = true; var rotationOverlaysVisible = false; @@ -1382,6 +1233,8 @@ SelectionDisplay = (function() { position: rollCorner, rotation: rollHandleRotation }); + + }; // FUNCTION: UPDATE HANDLE SIZES @@ -3422,7 +3275,7 @@ SelectionDisplay = (function() { y: innerRadius * ROTATION_DISPLAY_SIZE_Y_MULTIPLIER }, lineHeight: innerRadius * ROTATION_DISPLAY_LINE_HEIGHT_MULTIPLIER, - text: normalizeDegrees(angleFromZero) + "°" + text: normalizeDegrees(-angleFromZero) + "°" }; if (wantDebug) { print(" TranslatedPos: " + position.x + ", " + position.y + ", " + position.z); @@ -3483,6 +3336,13 @@ SelectionDisplay = (function() { initialPosition = SelectionManager.worldPosition; rotationNormal = { x: 0, y: 0, z: 0 }; rotationNormal[rotAroundAxis] = 1; + //get the correct axis according to the avatar referencial + var avatarReferential = Quat.multiply(MyAvatar.orientation, Quat.fromVec3Degrees({ + x: 0, + y: 0, + z: 0 + })); + rotationNormal = Vec3.multiplyQbyV(avatarReferential, rotationNormal); // Size the overlays to the current selection size var diagonal = (Vec3.length(SelectionManager.worldDimensions) / 2) * 1.1; @@ -3584,11 +3444,11 @@ SelectionDisplay = (function() { var snapAngle = snapToInner ? innerSnapAngle : 1.0; angleFromZero = Math.floor(angleFromZero / snapAngle) * snapAngle; - var vec3Degrees = { x: 0, y: 0, z: 0 }; - vec3Degrees[rotAroundAxis] = angleFromZero; - var rotChange = Quat.fromVec3Degrees(vec3Degrees); + + var rotChange = Quat.angleAxis(angleFromZero, rotationNormal); updateSelectionsRotation(rotChange); - + //present angle in avatar referencial + angleFromZero = -angleFromZero; updateRotationDegreesOverlay(angleFromZero, handleRotation, rotCenter); // update the rotation display accordingly...