Merge branch 'master' into 21334

This commit is contained in:
David Rowe 2017-05-12 10:34:16 +12:00
commit 021c3dff32
67 changed files with 1960 additions and 573 deletions

View file

@ -402,7 +402,7 @@ void AvatarMixer::handleAvatarIdentityPacket(QSharedPointer<ReceivedMessage> mes
AvatarData::parseAvatarIdentityPacket(message->getMessage(), identity);
bool identityChanged = false;
bool displayNameChanged = false;
avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged);
avatar.processAvatarIdentity(identity, identityChanged, displayNameChanged, senderNode->getClockSkewUsec());
if (identityChanged) {
QMutexLocker nodeDataLocker(&nodeData->getMutex());
nodeData->flagIdentityChange();

View file

@ -35,6 +35,11 @@
{ "from": "Vive.RightApplicationMenu", "to": "Standard.RightSecondaryThumb" },
{ "from": "Vive.LeftHand", "to": "Standard.LeftHand", "when": [ "Application.InHMD" ] },
{ "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] }
{ "from": "Vive.RightHand", "to": "Standard.RightHand", "when": [ "Application.InHMD" ] },
{ "from": "Vive.LeftFoot", "to" : "Standard.LeftFoot", "when": [ "Application.InHMD"] },
{ "from": "Vive.RightFoot", "to" : "Standard.RightFoot", "when": [ "Application.InHMD"] },
{ "from": "Vive.Hips", "to" : "Standard.Hips", "when": [ "Application.InHMD"] },
{ "from": "Vive.Spine2", "to" : "Standard.Spine2", "when": [ "Application.InHMD"] },
{ "from": "Vive.Head", "to" : "Standard.Head", "when" : [ "Application.InHMD"] }
]
}

View file

@ -35,11 +35,6 @@ Rectangle {
property string title: "Audio Options"
signal sendToScript(var message);
//set models after Components is shown
Component.onCompleted: {
refreshTimer.start()
refreshTimerOutput.start()
}
Component {
id: separator
@ -84,7 +79,7 @@ Rectangle {
}
Connections {
target: AvatarInputs
target: AvatarInputs !== undefined ? AvatarInputs : null
onShowAudioToolsChanged: {
audioTools.checkbox.checked = showAudioTools
}
@ -105,10 +100,12 @@ Rectangle {
id: audioTools
width: parent.width
anchors { left: parent.left; right: parent.right; leftMargin: 30 }
checkbox.checked: AvatarInputs.showAudioTools
checkbox.checked: AvatarInputs !== undefined ? AvatarInputs.showAudioTools : false
text.text: qsTr("Show audio level meter")
onCheckBoxClicked: {
AvatarInputs.showAudioTools = checked
if (AvatarInputs !== undefined) {
AvatarInputs.showAudioTools = checked
}
}
}
@ -138,30 +135,34 @@ Rectangle {
}
ListView {
Timer {
id: refreshTimer
interval: 1
repeat: false
onTriggered: {
//refresh model
inputAudioListView.model = undefined
inputAudioListView.model = AudioDevice.inputAudioDevices
}
}
id: inputAudioListView
anchors { left: parent.left; right: parent.right; leftMargin: 70 }
height: 125
spacing: 16
spacing: 0
clip: true
snapMode: ListView.SnapToItem
delegate: AudioCheckbox {
model: AudioDevice
delegate: Item {
width: parent.width
checkbox.checked: (modelData === AudioDevice.getInputDevice())
text.text: modelData
onCheckBoxClicked: {
if (checked) {
AudioDevice.setInputDevice(modelData)
refreshTimer.start()
visible: devicemode === 0
height: visible ? 36 : 0
AudioCheckbox {
id: cbin
anchors.verticalCenter: parent.verticalCenter
Binding {
target: cbin.checkbox
property: 'checked'
value: devicechecked
}
width: parent.width
cbchecked: devicechecked
text.text: devicename
onCheckBoxClicked: {
if (checked) {
AudioDevice.setInputDeviceAsync(devicename)
}
}
}
}
@ -191,31 +192,33 @@ Rectangle {
text: qsTr("CHOOSE OUTPUT DEVICE")
}
}
ListView {
id: outputAudioListView
Timer {
id: refreshTimerOutput
interval: 1
repeat: false
onTriggered: {
//refresh model
outputAudioListView.model = undefined
outputAudioListView.model = AudioDevice.outputAudioDevices
}
}
anchors { left: parent.left; right: parent.right; leftMargin: 70 }
height: 250
spacing: 16
spacing: 0
clip: true
snapMode: ListView.SnapToItem
delegate: AudioCheckbox {
model: AudioDevice
delegate: Item {
width: parent.width
checkbox.checked: (modelData === AudioDevice.getOutputDevice())
text.text: modelData
onCheckBoxClicked: {
if (checked) {
AudioDevice.setOutputDevice(modelData)
refreshTimerOutput.start()
visible: devicemode === 1
height: visible ? 36 : 0
AudioCheckbox {
id: cbout
width: parent.width
anchors.verticalCenter: parent.verticalCenter
Binding {
target: cbout.checkbox
property: 'checked'
value: devicechecked
}
text.text: devicename
onCheckBoxClicked: {
if (checked) {
AudioDevice.setOutputDeviceAsync(devicename)
}
}
}
}

View file

@ -844,7 +844,7 @@ Rectangle {
boxSize: 24;
onClicked: {
var newValue = model.connection !== "friend";
connectionsUserModel.setProperty(model.userIndex, styleData.role, newValue);
connectionsUserModel.setProperty(model.userIndex, styleData.role, (newValue ? "friend" : "connection"));
connectionsUserModelData[model.userIndex][styleData.role] = newValue; // Defensive programming
pal.sendToScript({method: newValue ? 'addFriend' : 'removeFriend', params: model.userName});

View file

@ -8,6 +8,7 @@ Row {
id: row
spacing: 16
property alias checkbox: cb
property alias cbchecked: cb.checked
property alias text: txt
signal checkBoxClicked(bool checked)

View file

@ -33,6 +33,6 @@ StackView {
TabletPreferencesDialog {
id: root
objectName: "TabletGeneralPreferences"
showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect"]
showCategories: ["UI", "Snapshots", "Scripts", "Privacy", "Octree", "HMD", "Sixense Controllers", "Perception Neuron", "Kinect", "Vive Pucks Configuration"]
}
}

View file

@ -136,8 +136,8 @@ Item {
for (var i = 0; i < sections.length; i++) {
totalHeight += sections[i].height + sections[i].getPreferencesHeight();
}
console.log(totalHeight);
return totalHeight;
var bottomPadding = 100;
return (totalHeight + bottomPadding);
}
}
}

View file

@ -941,10 +941,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer, bo
// sessionRunTime will be reset soon by loadSettings. Grab it now to get previous session value.
// The value will be 0 if the user blew away settings this session, which is both a feature and a bug.
static const QString TESTER = "HIFI_TESTER";
auto gpuIdent = GPUIdent::getInstance();
auto glContextData = getGLContextData();
QJsonObject properties = {
{ "version", applicationVersion() },
{ "tester", QProcessEnvironment::systemEnvironment().contains(TESTER) },
{ "previousSessionCrashed", _previousSessionCrashed },
{ "previousSessionRuntime", sessionRunTime.get() },
{ "cpu_architecture", QSysInfo::currentCpuArchitecture() },
@ -1688,7 +1690,6 @@ void Application::updateHeartbeat() const {
void Application::aboutToQuit() {
emit beforeAboutToQuit();
DependencyManager::get<AudioClient>()->beforeAboutToQuit();
foreach(auto inputPlugin, PluginManager::getInstance()->getInputPlugins()) {
if (inputPlugin->isActive()) {
@ -1789,14 +1790,13 @@ void Application::cleanupBeforeQuit() {
_snapshotSoundInjector->stop();
}
// stop audio after QML, as there are unexplained audio crashes originating in qtwebengine
// stop the AudioClient, synchronously
// FIXME: something else is holding a reference to AudioClient,
// so it must be explicitly synchronously stopped here
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(),
"stop", Qt::BlockingQueuedConnection);
"cleanupBeforeQuit", Qt::BlockingQueuedConnection);
// destroy Audio so it and its threads have a chance to go down safely
// this must happen after QML, as there are unexplained audio crashes originating in qtwebengine
DependencyManager::destroy<AudioClient>();
DependencyManager::destroy<AudioInjectorManager>();
@ -2053,6 +2053,8 @@ void Application::initializeUi() {
rootContext->setContextProperty("ApplicationCompositor", &getApplicationCompositor());
rootContext->setContextProperty("AvatarInputs", AvatarInputs::getInstance());
if (auto steamClient = PluginManager::getInstance()->getSteamClientPlugin()) {
rootContext->setContextProperty("Steam", new SteamScriptingInterface(engine, steamClient.get()));
}

View file

@ -48,7 +48,7 @@ void MyHead::simulate(float deltaTime) {
FaceTracker* faceTracker = qApp->getActiveFaceTracker();
_isFaceTrackerConnected = faceTracker != NULL && !faceTracker->isMuted();
if (_isFaceTrackerConnected) {
_blendshapeCoefficients = faceTracker->getBlendshapeCoefficients();
_transientBlendshapeCoefficients = faceTracker->getBlendshapeCoefficients();
if (typeid(*faceTracker) == typeid(DdeFaceTracker)) {
@ -60,11 +60,11 @@ void MyHead::simulate(float deltaTime) {
const int FUNNEL_BLENDSHAPE = 40;
const int SMILE_LEFT_BLENDSHAPE = 28;
const int SMILE_RIGHT_BLENDSHAPE = 29;
_blendshapeCoefficients[JAW_OPEN_BLENDSHAPE] += _audioJawOpen;
_blendshapeCoefficients[SMILE_LEFT_BLENDSHAPE] += _mouth4;
_blendshapeCoefficients[SMILE_RIGHT_BLENDSHAPE] += _mouth4;
_blendshapeCoefficients[MMMM_BLENDSHAPE] += _mouth2;
_blendshapeCoefficients[FUNNEL_BLENDSHAPE] += _mouth3;
_transientBlendshapeCoefficients[JAW_OPEN_BLENDSHAPE] += _audioJawOpen;
_transientBlendshapeCoefficients[SMILE_LEFT_BLENDSHAPE] += _mouth4;
_transientBlendshapeCoefficients[SMILE_RIGHT_BLENDSHAPE] += _mouth4;
_transientBlendshapeCoefficients[MMMM_BLENDSHAPE] += _mouth2;
_transientBlendshapeCoefficients[FUNNEL_BLENDSHAPE] += _mouth3;
}
applyEyelidOffset(getFinalOrientationInWorldFrame());
}

View file

@ -11,21 +11,19 @@
#include "AudioClient.h"
#include "AudioDeviceScriptingInterface.h"
#include "SettingsScriptingInterface.h"
AudioDeviceScriptingInterface* AudioDeviceScriptingInterface::getInstance() {
static AudioDeviceScriptingInterface sharedInstance;
return &sharedInstance;
}
QStringList AudioDeviceScriptingInterface::inputAudioDevices() const
{
return DependencyManager::get<AudioClient>()->getDeviceNames(QAudio::AudioInput).toList();;
QStringList AudioDeviceScriptingInterface::inputAudioDevices() const {
return _inputAudioDevices;
}
QStringList AudioDeviceScriptingInterface::outputAudioDevices() const
{
return DependencyManager::get<AudioClient>()->getDeviceNames(QAudio::AudioOutput).toList();;
QStringList AudioDeviceScriptingInterface::outputAudioDevices() const {
return _outputAudioDevices;
}
bool AudioDeviceScriptingInterface::muted()
@ -33,11 +31,27 @@ bool AudioDeviceScriptingInterface::muted()
return getMuted();
}
AudioDeviceScriptingInterface::AudioDeviceScriptingInterface() {
AudioDeviceScriptingInterface::AudioDeviceScriptingInterface(): QAbstractListModel(nullptr) {
connect(DependencyManager::get<AudioClient>().data(), &AudioClient::muteToggled,
this, &AudioDeviceScriptingInterface::muteToggled);
connect(DependencyManager::get<AudioClient>().data(), &AudioClient::deviceChanged,
this, &AudioDeviceScriptingInterface::deviceChanged);
this, &AudioDeviceScriptingInterface::onDeviceChanged, Qt::QueuedConnection);
connect(DependencyManager::get<AudioClient>().data(), &AudioClient::currentInputDeviceChanged,
this, &AudioDeviceScriptingInterface::onCurrentInputDeviceChanged, Qt::QueuedConnection);
connect(DependencyManager::get<AudioClient>().data(), &AudioClient::currentOutputDeviceChanged,
this, &AudioDeviceScriptingInterface::onCurrentOutputDeviceChanged, Qt::QueuedConnection);
//fill up model
onDeviceChanged();
//set up previously saved device
SettingsScriptingInterface* settings = SettingsScriptingInterface::getInstance();
const QString inDevice = settings->getValue("audio_input_device").toString();
if (inDevice != _currentInputDevice) {
setInputDeviceAsync(inDevice);
}
const QString outDevice = settings->getValue("audio_output_device").toString();
if (outDevice != _currentOutputDevice) {
setOutputDeviceAsync(outDevice);
}
}
bool AudioDeviceScriptingInterface::setInputDevice(const QString& deviceName) {
@ -58,6 +72,43 @@ bool AudioDeviceScriptingInterface::setOutputDevice(const QString& deviceName) {
return result;
}
bool AudioDeviceScriptingInterface::setDeviceFromMenu(const QString& deviceMenuName) {
QAudio::Mode mode;
if (deviceMenuName.indexOf("for Output") != -1) {
mode = QAudio::AudioOutput;
} else if (deviceMenuName.indexOf("for Input") != -1) {
mode = QAudio::AudioInput;
} else {
return false;
}
for (ScriptingAudioDeviceInfo di: _devices) {
if (mode == di.mode && deviceMenuName.contains(di.name)) {
if (mode == QAudio::AudioOutput) {
setOutputDeviceAsync(di.name);
} else {
setInputDeviceAsync(di.name);
}
return true;
}
}
return false;
}
void AudioDeviceScriptingInterface::setInputDeviceAsync(const QString& deviceName) {
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(), "switchInputToAudioDevice",
Qt::QueuedConnection,
Q_ARG(const QString&, deviceName));
}
void AudioDeviceScriptingInterface::setOutputDeviceAsync(const QString& deviceName) {
QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(), "switchOutputToAudioDevice",
Qt::QueuedConnection,
Q_ARG(const QString&, deviceName));
}
QString AudioDeviceScriptingInterface::getInputDevice() {
return DependencyManager::get<AudioClient>()->getDeviceName(QAudio::AudioInput);
}
@ -116,3 +167,105 @@ void AudioDeviceScriptingInterface::setMuted(bool muted)
bool AudioDeviceScriptingInterface::getMuted() {
return DependencyManager::get<AudioClient>()->isMuted();
}
QVariant AudioDeviceScriptingInterface::data(const QModelIndex& index, int role) const {
//sanity
if (!index.isValid() || index.row() >= _devices.size())
return QVariant();
if (role == Qt::DisplayRole || role == DisplayNameRole) {
return _devices.at(index.row()).name;
} else if (role == SelectedRole) {
return _devices.at(index.row()).selected;
} else if (role == AudioModeRole) {
return (int)_devices.at(index.row()).mode;
}
return QVariant();
}
int AudioDeviceScriptingInterface::rowCount(const QModelIndex& parent) const {
Q_UNUSED(parent)
return _devices.size();
}
QHash<int, QByteArray> AudioDeviceScriptingInterface::roleNames() const {
QHash<int, QByteArray> roles;
roles.insert(DisplayNameRole, "devicename");
roles.insert(SelectedRole, "devicechecked");
roles.insert(AudioModeRole, "devicemode");
return roles;
}
void AudioDeviceScriptingInterface::onDeviceChanged()
{
beginResetModel();
_outputAudioDevices.clear();
_devices.clear();
_currentOutputDevice = getOutputDevice();
for (QString name: getOutputDevices()) {
ScriptingAudioDeviceInfo di;
di.name = name;
di.selected = (name == _currentOutputDevice);
di.mode = QAudio::AudioOutput;
_devices.append(di);
_outputAudioDevices.append(name);
}
emit outputAudioDevicesChanged(_outputAudioDevices);
_inputAudioDevices.clear();
_currentInputDevice = getInputDevice();
for (QString name: getInputDevices()) {
ScriptingAudioDeviceInfo di;
di.name = name;
di.selected = (name == _currentInputDevice);
di.mode = QAudio::AudioInput;
_devices.append(di);
_inputAudioDevices.append(name);
}
emit inputAudioDevicesChanged(_inputAudioDevices);
endResetModel();
emit deviceChanged();
}
void AudioDeviceScriptingInterface::onCurrentInputDeviceChanged(const QString& name)
{
currentDeviceUpdate(name, QAudio::AudioInput);
//we got a signal that device changed. Save it now
SettingsScriptingInterface* settings = SettingsScriptingInterface::getInstance();
settings->setValue("audio_input_device", name);
emit currentInputDeviceChanged(name);
}
void AudioDeviceScriptingInterface::onCurrentOutputDeviceChanged(const QString& name)
{
currentDeviceUpdate(name, QAudio::AudioOutput);
//we got a signal that device changed. Save it now
SettingsScriptingInterface* settings = SettingsScriptingInterface::getInstance();
settings->setValue("audio_output_device", name);
emit currentOutputDeviceChanged(name);
}
void AudioDeviceScriptingInterface::currentDeviceUpdate(const QString& name, QAudio::Mode mode)
{
QVector<int> role;
role.append(SelectedRole);
for (int i = 0; i < _devices.size(); i++) {
ScriptingAudioDeviceInfo di = _devices.at(i);
if (di.mode != mode) {
continue;
}
if (di.selected && di.name != name ) {
di.selected = false;
_devices[i] = di;
emit dataChanged(index(i, 0), index(i, 0), role);
}
if (di.name == name) {
di.selected = true;
_devices[i] = di;
emit dataChanged(index(i, 0), index(i, 0), role);
}
}
}

View file

@ -15,10 +15,18 @@
#include <QObject>
#include <QString>
#include <QVector>
#include <QAbstractListModel>
#include <QAudio>
class AudioEffectOptions;
class AudioDeviceScriptingInterface : public QObject {
struct ScriptingAudioDeviceInfo {
QString name;
bool selected;
QAudio::Mode mode;
};
class AudioDeviceScriptingInterface : public QAbstractListModel {
Q_OBJECT
Q_PROPERTY(QStringList inputAudioDevices READ inputAudioDevices NOTIFY inputAudioDevicesChanged)
@ -32,9 +40,26 @@ public:
QStringList outputAudioDevices() const;
bool muted();
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames() const override;
enum Roles {
DisplayNameRole = Qt::UserRole,
SelectedRole,
AudioModeRole
};
private slots:
void onDeviceChanged();
void onCurrentInputDeviceChanged(const QString& name);
void onCurrentOutputDeviceChanged(const QString& name);
void currentDeviceUpdate(const QString& name, QAudio::Mode mode);
public slots:
bool setInputDevice(const QString& deviceName);
bool setOutputDevice(const QString& deviceName);
bool setDeviceFromMenu(const QString& deviceMenuName);
QString getInputDevice();
QString getOutputDevice();
@ -55,15 +80,28 @@ public slots:
void setMuted(bool muted);
void setInputDeviceAsync(const QString& deviceName);
void setOutputDeviceAsync(const QString& deviceName);
private:
AudioDeviceScriptingInterface();
signals:
void muteToggled();
void deviceChanged();
void currentInputDeviceChanged(const QString& name);
void currentOutputDeviceChanged(const QString& name);
void mutedChanged(bool muted);
void inputAudioDevicesChanged(QStringList inputAudioDevices);
void outputAudioDevicesChanged(QStringList outputAudioDevices);
private:
QVector<ScriptingAudioDeviceInfo> _devices;
QStringList _inputAudioDevices;
QStringList _outputAudioDevices;
QString _currentInputDevice;
QString _currentOutputDevice;
};
#endif // hifi_AudioDeviceScriptingInterface_h

View file

@ -225,10 +225,6 @@ void Web3DOverlay::setMaxFPS(uint8_t maxFPS) {
}
void Web3DOverlay::render(RenderArgs* args) {
if (!_visible || !getParentVisible()) {
return;
}
QOpenGLContext * currentContext = QOpenGLContext::currentContext();
QSurface * currentSurface = currentContext->surface();
if (!_webSurface) {
@ -282,6 +278,10 @@ void Web3DOverlay::render(RenderArgs* args) {
_webSurface->resize(QSize(_resolution.x, _resolution.y));
}
if (!_visible || !getParentVisible()) {
return;
}
vec2 halfSize = getSize() / 2.0f;
vec4 color(toGlm(getColor()), getAlpha());

View file

@ -76,42 +76,58 @@ using Mutex = std::mutex;
using Lock = std::unique_lock<Mutex>;
static Mutex _deviceMutex;
// background thread that continuously polls for device changes
class CheckDevicesThread : public QThread {
class BackgroundThread : public QThread {
public:
const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000;
BackgroundThread(AudioClient* client) : QThread((QObject*)client), _client(client) {}
virtual void join() = 0;
protected:
AudioClient* _client;
};
CheckDevicesThread(AudioClient* audioClient)
: _audioClient(audioClient) {
}
void beforeAboutToQuit() {
Lock lock(_checkDevicesMutex);
_quit = true;
// background thread continuously polling device changes
class CheckDevicesThread : public BackgroundThread {
public:
CheckDevicesThread(AudioClient* client) : BackgroundThread(client) {}
void join() override {
_shouldQuit = true;
std::unique_lock<std::mutex> lock(_joinMutex);
_joinCondition.wait(lock, [&]{ return !_isRunning; });
}
protected:
void run() override {
while (true) {
{
Lock lock(_checkDevicesMutex);
if (_quit) {
break;
}
_audioClient->checkDevices();
}
while (!_shouldQuit) {
_client->checkDevices();
const unsigned long DEVICE_CHECK_INTERVAL_MSECS = 2 * 1000;
QThread::msleep(DEVICE_CHECK_INTERVAL_MSECS);
}
std::lock_guard<std::mutex> lock(_joinMutex);
_isRunning = false;
_joinCondition.notify_one();
}
private:
AudioClient* _audioClient { nullptr };
Mutex _checkDevicesMutex;
bool _quit { false };
std::atomic<bool> _shouldQuit { false };
bool _isRunning { true };
std::mutex _joinMutex;
std::condition_variable _joinCondition;
};
void AudioInjectorsThread::prepare() {
_audio->prepareLocalAudioInjectors();
}
// background thread buffering local injectors
class LocalInjectorsThread : public BackgroundThread {
Q_OBJECT
public:
LocalInjectorsThread(AudioClient* client) : BackgroundThread(client) {}
void join() override { return; }
private slots:
void prepare() { _client->prepareLocalAudioInjectors(); }
};
#include "AudioClient.moc"
static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) {
for (int i = 0; i < numSamples/2; i++) {
@ -179,7 +195,6 @@ AudioClient::AudioClient() :
_inputToNetworkResampler(NULL),
_networkToOutputResampler(NULL),
_localToOutputResampler(NULL),
_localAudioThread(this),
_audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT),
_outgoingAvatarAudioSequenceNumber(0),
_audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this),
@ -210,13 +225,14 @@ AudioClient::AudioClient() :
// start a thread to detect any device changes
_checkDevicesThread = new CheckDevicesThread(this);
_checkDevicesThread->setObjectName("CheckDevices Thread");
_checkDevicesThread->setObjectName("AudioClient CheckDevices Thread");
_checkDevicesThread->setPriority(QThread::LowPriority);
_checkDevicesThread->start();
// start a thread to process local injectors
_localAudioThread.setObjectName("LocalAudio Thread");
_localAudioThread.start();
_localInjectorsThread = new LocalInjectorsThread(this);
_localInjectorsThread->setObjectName("AudioClient LocalInjectors Thread");
_localInjectorsThread->start();
configureReverb();
@ -231,18 +247,32 @@ AudioClient::AudioClient() :
}
AudioClient::~AudioClient() {
delete _checkDevicesThread;
stop();
if (_codec && _encoder) {
_codec->releaseEncoder(_encoder);
_encoder = nullptr;
}
}
void AudioClient::beforeAboutToQuit() {
static_cast<CheckDevicesThread*>(_checkDevicesThread)->beforeAboutToQuit();
void AudioClient::customDeleter() {
deleteLater();
}
void AudioClient::cleanupBeforeQuit() {
// FIXME: this should be put in customDeleter, but there is still a reference to this when it is called,
// so this must be explicitly, synchronously stopped
stop();
if (_checkDevicesThread) {
static_cast<BackgroundThread*>(_checkDevicesThread)->join();
delete _checkDevicesThread;
}
if (_localInjectorsThread) {
static_cast<BackgroundThread*>(_localInjectorsThread)->join();
delete _localInjectorsThread;
}
}
void AudioClient::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) {
qCDebug(audioclient) << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec;
@ -769,7 +799,8 @@ QString AudioClient::getDefaultDeviceName(QAudio::Mode mode) {
QVector<QString> AudioClient::getDeviceNames(QAudio::Mode mode) {
QVector<QString> deviceNames;
foreach(QAudioDeviceInfo audioDevice, getAvailableDevices(mode)) {
const QList<QAudioDeviceInfo> &availableDevice = getAvailableDevices(mode);
foreach(const QAudioDeviceInfo &audioDevice, availableDevice) {
deviceNames << audioDevice.deviceName().trimmed();
}
return deviceNames;
@ -1096,11 +1127,19 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) {
handleAudioInput(audioBuffer);
}
void AudioClient::prepareLocalAudioInjectors() {
void AudioClient::prepareLocalAudioInjectors(std::unique_ptr<Lock> localAudioLock) {
bool doSynchronously = localAudioLock.operator bool();
if (!localAudioLock) {
localAudioLock.reset(new Lock(_localAudioMutex));
}
int samplesNeeded = std::numeric_limits<int>::max();
while (samplesNeeded > 0) {
// unlock between every write to allow device switching
Lock lock(_localAudioMutex);
if (!doSynchronously) {
// unlock between every write to allow device switching
localAudioLock->unlock();
localAudioLock->lock();
}
// in case of a device switch, consider bufferCapacity volatile across iterations
if (_outputPeriod == 0) {
@ -1154,16 +1193,16 @@ void AudioClient::prepareLocalAudioInjectors() {
}
bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
QVector<AudioInjector*> injectorsToRemove;
// lock the injector vector
Lock lock(_injectorsMutex);
if (_activeLocalAudioInjectors.size() == 0) {
// check the flag for injectors before attempting to lock
if (!_localInjectorsAvailable.load(std::memory_order_acquire)) {
return false;
}
// lock the injectors
Lock lock(_injectorsMutex);
QVector<AudioInjector*> injectorsToRemove;
memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float));
for (AudioInjector* injector : _activeLocalAudioInjectors) {
@ -1242,6 +1281,9 @@ bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) {
_activeLocalAudioInjectors.removeOne(injector);
}
// update the flag
_localInjectorsAvailable.exchange(!_activeLocalAudioInjectors.empty(), std::memory_order_release);
return true;
}
@ -1328,11 +1370,14 @@ bool AudioClient::outputLocalInjector(AudioInjector* injector) {
// move local buffer to the LocalAudioThread to avoid dataraces with AudioInjector (like stop())
injectorBuffer->setParent(nullptr);
injectorBuffer->moveToThread(&_localAudioThread);
injectorBuffer->moveToThread(_localInjectorsThread);
// update the flag
_localInjectorsAvailable.exchange(true, std::memory_order_release);
} else {
qCDebug(audioclient) << "injector exists in active list already";
}
return true;
} else {
@ -1358,7 +1403,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn
_audioInput->stop();
_inputDevice = NULL;
delete _audioInput;
_audioInput->deleteLater();
_audioInput = NULL;
_numInputCallbackBytes = 0;
@ -1374,6 +1419,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn
if (!inputDeviceInfo.isNull()) {
qCDebug(audioclient) << "The audio input device " << inputDeviceInfo.deviceName() << "is available.";
_inputAudioDeviceName = inputDeviceInfo.deviceName().trimmed();
emit currentInputDeviceChanged(_inputAudioDeviceName);
if (adjustedFormatForAudioDevice(inputDeviceInfo, _desiredInputFormat, _inputFormat)) {
qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat;
@ -1455,18 +1501,20 @@ void AudioClient::outputNotify() {
bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) {
bool supportedFormat = false;
Lock lock(_localAudioMutex);
Lock localAudioLock(_localAudioMutex);
_localSamplesAvailable.exchange(0, std::memory_order_release);
// cleanup any previously initialized device
if (_audioOutput) {
_audioOutput->stop();
delete _audioOutput;
//must be deleted in next eventloop cycle when its called from notify()
_audioOutput->deleteLater();
_audioOutput = NULL;
_loopbackOutputDevice = NULL;
delete _loopbackAudioOutput;
//must be deleted in next eventloop cycle when its called from notify()
_loopbackAudioOutput->deleteLater();
_loopbackAudioOutput = NULL;
delete[] _outputMixBuffer;
@ -1491,6 +1539,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
if (!outputDeviceInfo.isNull()) {
qCDebug(audioclient) << "The audio output device " << outputDeviceInfo.deviceName() << "is available.";
_outputAudioDeviceName = outputDeviceInfo.deviceName().trimmed();
emit currentOutputDeviceChanged(_outputAudioDeviceName);
if (adjustedFormatForAudioDevice(outputDeviceInfo, _desiredOutputFormat, _outputFormat)) {
qCDebug(audioclient) << "The format to be used for audio output is" << _outputFormat;
@ -1525,14 +1574,23 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
connect(_audioOutput, &QAudioOutput::stateChanged, [&, frameSize, requestedSize](QAudio::State state) {
if (state == QAudio::ActiveState) {
// restrict device callback to _outputPeriod samples
_outputPeriod = (_audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE) * 2;
_outputPeriod = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE;
// device callback may exceed reported period, so double it to avoid stutter
_outputPeriod *= 2;
_outputMixBuffer = new float[_outputPeriod];
_outputScratchBuffer = new int16_t[_outputPeriod];
// size local output mix buffer based on resampled network frame size
_networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
_localOutputMixBuffer = new float[_networkPeriod];
int networkPeriod = _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_STEREO);
_localOutputMixBuffer = new float[networkPeriod];
// local period should be at least twice the output period,
// in case two device reads happen before more data can be read (worst case)
int localPeriod = _outputPeriod * 2;
// round up to an exact multiple of networkPeriod
localPeriod = ((localPeriod + networkPeriod - 1) / networkPeriod) * networkPeriod;
// this ensures lowest latency without stutter from underrun
_localInjectorsStream.resizeForFrameSize(localPeriod);
int bufferSize = _audioOutput->bufferSize();
@ -1547,6 +1605,9 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
qCDebug(audioclient) << "local buffer (samples):" << localPeriod;
disconnect(_audioOutput, &QAudioOutput::stateChanged, 0, 0);
// unlock to avoid a deadlock with the device callback (which always succeeds this initialization)
localAudioLock.unlock();
}
});
connect(_audioOutput, &QAudioOutput::notify, this, &AudioClient::outputNotify);
@ -1685,12 +1746,24 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) {
int injectorSamplesPopped = 0;
{
bool append = networkSamplesPopped > 0;
// this does not require a lock as of the only two functions adding to _localSamplesAvailable (samples count):
// check the samples we have available locklessly; this is possible because only two functions add to the count:
// - prepareLocalAudioInjectors will only increase samples count
// - switchOutputToAudioDevice will zero samples count
// stop the device, so that readData will exhaust the existing buffer or see a zeroed samples count
// and start the device, which can only see a zeroed samples count
samplesRequested = std::min(samplesRequested, _audio->_localSamplesAvailable.load(std::memory_order_acquire));
// - switchOutputToAudioDevice will zero samples count,
// stop the device - so that readData will exhaust the existing buffer or see a zeroed samples count,
// and start the device - which can then only see a zeroed samples count
int samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire);
// if we do not have enough samples buffered despite having injectors, buffer them synchronously
if (samplesAvailable < samplesRequested && _audio->_localInjectorsAvailable.load(std::memory_order_acquire)) {
// try_to_lock, in case the device is being shut down already
std::unique_ptr<Lock> localAudioLock(new Lock(_audio->_localAudioMutex, std::try_to_lock));
if (localAudioLock->owns_lock()) {
_audio->prepareLocalAudioInjectors(std::move(localAudioLock));
samplesAvailable = _audio->_localSamplesAvailable.load(std::memory_order_acquire);
}
}
samplesRequested = std::min(samplesRequested, samplesAvailable);
if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) {
_audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release);
qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested);
@ -1698,7 +1771,7 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) {
}
// prepare injectors for the next callback
QMetaObject::invokeMethod(&_audio->_localAudioThread, "prepare", Qt::QueuedConnection);
QMetaObject::invokeMethod(_audio->_localInjectorsThread, "prepare", Qt::QueuedConnection);
int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped);
int framesPopped = samplesPopped / AudioConstants::STEREO;

View file

@ -71,19 +71,6 @@ class QIODevice;
class Transform;
class NLPacket;
class AudioInjectorsThread : public QThread {
Q_OBJECT
public:
AudioInjectorsThread(AudioClient* audio) : _audio(audio) {}
public slots :
void prepare();
private:
AudioClient* _audio;
};
class AudioClient : public AbstractAudioInterface, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
@ -158,7 +145,7 @@ public:
Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale);
void checkDevices();
bool outputLocalInjector(AudioInjector* injector) override;
static const float CALLBACK_ACCELERATOR_RATIO;
@ -169,6 +156,7 @@ public:
public slots:
void start();
void stop();
void cleanupBeforeQuit();
void handleAudioEnvironmentDataPacket(QSharedPointer<ReceivedMessage> message);
void handleAudioDataPacket(QSharedPointer<ReceivedMessage> message);
@ -184,8 +172,6 @@ public slots:
void audioMixerKilled();
void toggleMute();
void beforeAboutToQuit();
virtual void setIsStereoInput(bool stereo) override;
void toggleAudioNoiseReduction() { _isNoiseGateEnabled = !_isNoiseGateEnabled; }
@ -198,8 +184,6 @@ public slots:
int setOutputBufferSize(int numFrames, bool persist = true);
void prepareLocalAudioInjectors();
bool outputLocalInjector(AudioInjector* injector) override;
bool shouldLoopbackInjectors() override { return _shouldEchoToServer; }
bool switchInputToAudioDevice(const QString& inputDeviceName);
@ -238,17 +222,23 @@ signals:
void muteEnvironmentRequested(glm::vec3 position, float radius);
void currentOutputDeviceChanged(const QString& name);
void currentInputDeviceChanged(const QString& name);
protected:
AudioClient();
~AudioClient();
virtual void customDeleter() override {
deleteLater();
}
virtual void customDeleter() override;
private:
friend class CheckDevicesThread;
friend class LocalInjectorsThread;
void outputFormatChanged();
void handleAudioInput(QByteArray& audioBuffer);
void checkDevices();
void prepareLocalAudioInjectors(std::unique_ptr<Lock> localAudioLock = nullptr);
bool mixLocalAudioInjectors(float* mixBuffer);
float azimuthForSource(const glm::vec3& relativePosition);
float gainForSource(float distance, float volume);
@ -295,8 +285,9 @@ private:
AudioRingBuffer _inputRingBuffer;
LocalInjectorsStream _localInjectorsStream;
// In order to use _localInjectorsStream as a lock-free pipe,
// use it with a single producer/consumer, and track available samples
// use it with a single producer/consumer, and track available samples and injectors
std::atomic<int> _localSamplesAvailable { 0 };
std::atomic<bool> _localInjectorsAvailable { false };
MixedProcessedAudioStream _receivedAudioStream;
bool _isStereoInput;
@ -337,19 +328,17 @@ private:
// for network audio (used by network audio thread)
int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
// for local audio (used by audio injectors thread)
int _networkPeriod { 0 };
float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
float* _localOutputMixBuffer { NULL };
AudioInjectorsThread _localAudioThread;
Mutex _localAudioMutex;
// for output audio (used by this thread)
int _outputPeriod { 0 };
float* _outputMixBuffer { NULL };
int16_t* _outputScratchBuffer { NULL };
// for local audio (used by audio injectors thread)
float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO];
int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC];
float* _localOutputMixBuffer { NULL };
Mutex _localAudioMutex;
AudioLimiter _audioLimiter;
// Adds Reverb
@ -392,12 +381,13 @@ private:
QString _selectedCodecName;
Encoder* _encoder { nullptr }; // for outbound mic stream
QThread* _checkDevicesThread { nullptr };
RateCounter<> _silentOutbound;
RateCounter<> _audioOutbound;
RateCounter<> _silentInbound;
RateCounter<> _audioInbound;
QThread* _checkDevicesThread { nullptr };
QThread* _localInjectorsThread { nullptr };
};

View file

@ -32,12 +32,12 @@ public:
const Transform& transform, glm::vec3 avatarBoundingBoxCorner, glm::vec3 avatarBoundingBoxScale,
PacketType packetType, QString codecName = QString(""));
public slots:
// threadsafe
// moves injector->getLocalBuffer() to another thread (so removes its parent)
// take care to delete it when ~AudioInjector, as parenting Qt semantics will not work
virtual bool outputLocalInjector(AudioInjector* injector) = 0;
public slots:
virtual bool shouldLoopbackInjectors() { return false; }
virtual void setIsStereoInput(bool stereo) = 0;

View file

@ -369,23 +369,25 @@ void Avatar::simulate(float deltaTime, bool inView) {
PerformanceTimer perfTimer("simulate");
{
PROFILE_RANGE(simulation, "updateJoints");
if (inView && _hasNewJointData) {
_skeletonModel->getRig()->copyJointsFromJointData(_jointData);
glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset());
_skeletonModel->getRig()->computeExternalPoses(rootTransform);
_jointDataSimulationRate.increment();
_skeletonModel->simulate(deltaTime, true);
locationChanged(); // joints changed, so if there are any children, update them.
_hasNewJointData = false;
glm::vec3 headPosition = getPosition();
if (!_skeletonModel->getHeadPosition(headPosition)) {
headPosition = getPosition();
}
if (inView) {
Head* head = getHead();
head->setPosition(headPosition);
if (_hasNewJointData) {
_skeletonModel->getRig()->copyJointsFromJointData(_jointData);
glm::mat4 rootTransform = glm::scale(_skeletonModel->getScale()) * glm::translate(_skeletonModel->getOffset());
_skeletonModel->getRig()->computeExternalPoses(rootTransform);
_jointDataSimulationRate.increment();
_skeletonModel->simulate(deltaTime, true);
locationChanged(); // joints changed, so if there are any children, update them.
_hasNewJointData = false;
glm::vec3 headPosition = getPosition();
if (!_skeletonModel->getHeadPosition(headPosition)) {
headPosition = getPosition();
}
head->setPosition(headPosition);
}
head->setScale(getUniformScale());
head->simulate(deltaTime);
} else {

View file

@ -89,8 +89,7 @@ void Head::simulate(float deltaTime) {
_timeWithoutTalking += deltaTime;
if ((_averageLoudness - _longTermAverageLoudness) > TALKING_LOUDNESS) {
_timeWithoutTalking = 0.0f;
} else if (_timeWithoutTalking < BLINK_AFTER_TALKING && _timeWithoutTalking >= BLINK_AFTER_TALKING) {
} else if (_timeWithoutTalking - deltaTime < BLINK_AFTER_TALKING && _timeWithoutTalking >= BLINK_AFTER_TALKING) {
forceBlink = true;
}
@ -152,7 +151,7 @@ void Head::simulate(float deltaTime) {
_mouth2,
_mouth3,
_mouth4,
_blendshapeCoefficients);
_transientBlendshapeCoefficients);
applyEyelidOffset(getOrientation());
@ -203,6 +202,13 @@ void Head::calculateMouthShapes(float deltaTime) {
float trailingAudioJawOpenRatio = (100.0f - deltaTime * NORMAL_HZ) / 100.0f; // --> 0.99 at 60 Hz
_trailingAudioJawOpen = glm::mix(_trailingAudioJawOpen, _audioJawOpen, trailingAudioJawOpenRatio);
// truncate _mouthTime when mouth goes quiet to prevent floating point error on increment
const float SILENT_TRAILING_JAW_OPEN = 0.0002f;
const float MAX_SILENT_MOUTH_TIME = 10.0f;
if (_trailingAudioJawOpen < SILENT_TRAILING_JAW_OPEN && _mouthTime > MAX_SILENT_MOUTH_TIME) {
_mouthTime = 0.0f;
}
// Advance time at a rate proportional to loudness, and move the mouth shapes through
// a cycle at differing speeds to create a continuous random blend of shapes.
_mouthTime += sqrtf(_averageLoudness) * TIMESTEP_CONSTANT * deltaTimeRatio;
@ -228,15 +234,15 @@ void Head::applyEyelidOffset(glm::quat headOrientation) {
for (int i = 0; i < 2; i++) {
const int LEFT_EYE = 8;
float eyeCoefficient = _blendshapeCoefficients[i] - _blendshapeCoefficients[LEFT_EYE + i]; // Raw value
float eyeCoefficient = _transientBlendshapeCoefficients[i] - _transientBlendshapeCoefficients[LEFT_EYE + i];
eyeCoefficient = glm::clamp(eyelidOffset + eyeCoefficient * (1.0f - eyelidOffset), -1.0f, 1.0f);
if (eyeCoefficient > 0.0f) {
_blendshapeCoefficients[i] = eyeCoefficient;
_blendshapeCoefficients[LEFT_EYE + i] = 0.0f;
_transientBlendshapeCoefficients[i] = eyeCoefficient;
_transientBlendshapeCoefficients[LEFT_EYE + i] = 0.0f;
} else {
_blendshapeCoefficients[i] = 0.0f;
_blendshapeCoefficients[LEFT_EYE + i] = -eyeCoefficient;
_transientBlendshapeCoefficients[i] = 0.0f;
_transientBlendshapeCoefficients[LEFT_EYE + i] = -eyeCoefficient;
}
}
}

View file

@ -445,17 +445,17 @@ QByteArray AvatarData::toByteArray(AvatarDataDetail dataDetail, quint64 lastSent
if (hasFaceTrackerInfo) {
auto startSection = destinationBuffer;
auto faceTrackerInfo = reinterpret_cast<AvatarDataPacket::FaceTrackerInfo*>(destinationBuffer);
auto blendshapeCoefficients = _headData->getSummedBlendshapeCoefficients();
faceTrackerInfo->leftEyeBlink = _headData->_leftEyeBlink;
faceTrackerInfo->rightEyeBlink = _headData->_rightEyeBlink;
faceTrackerInfo->averageLoudness = _headData->_averageLoudness;
faceTrackerInfo->browAudioLift = _headData->_browAudioLift;
faceTrackerInfo->numBlendshapeCoefficients = _headData->_blendshapeCoefficients.size();
faceTrackerInfo->numBlendshapeCoefficients = blendshapeCoefficients.size();
destinationBuffer += sizeof(AvatarDataPacket::FaceTrackerInfo);
// followed by a variable number of float coefficients
memcpy(destinationBuffer, _headData->_blendshapeCoefficients.data(), _headData->_blendshapeCoefficients.size() * sizeof(float));
destinationBuffer += _headData->_blendshapeCoefficients.size() * sizeof(float);
memcpy(destinationBuffer, blendshapeCoefficients.data(), blendshapeCoefficients.size() * sizeof(float));
destinationBuffer += blendshapeCoefficients.size() * sizeof(float);
int numBytes = destinationBuffer - startSection;
if (outboundDataRateOut) {
@ -965,7 +965,7 @@ int AvatarData::parseDataFromBuffer(const QByteArray& buffer) {
const int coefficientsSize = sizeof(float) * numCoefficients;
PACKET_READ_CHECK(FaceTrackerCoefficients, coefficientsSize);
_headData->_blendshapeCoefficients.resize(numCoefficients); // make sure there's room for the copy!
_headData->_baseBlendshapeCoefficients.resize(numCoefficients);
_headData->_transientBlendshapeCoefficients.resize(numCoefficients);
memcpy(_headData->_blendshapeCoefficients.data(), sourceBuffer, coefficientsSize);
sourceBuffer += coefficientsSize;
int numBytesRead = sourceBuffer - startSection;
@ -1495,11 +1495,14 @@ QUrl AvatarData::cannonicalSkeletonModelURL(const QUrl& emptyURL) const {
return _skeletonModelURL.scheme() == "file" ? emptyURL : _skeletonModelURL;
}
void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged) {
void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged, const qint64 clockSkew) {
if (identity.updatedAt < _identityUpdatedAt) {
// Consider the case where this packet is being processed on Client A, and Client A is connected to Sandbox B.
// If Client A's system clock is *ahead of* Sandbox B's system clock, "clockSkew" will be *negative*.
// If Client A's system clock is *behind* Sandbox B's system clock, "clockSkew" will be *positive*.
if ((_identityUpdatedAt > identity.updatedAt - clockSkew) && (_identityUpdatedAt != 0)) {
qCDebug(avatars) << "Ignoring late identity packet for avatar " << getSessionUUID()
<< "identity.updatedAt:" << identity.updatedAt << "_identityUpdatedAt:" << _identityUpdatedAt;
<< "_identityUpdatedAt (" << _identityUpdatedAt << ") is greater than identity.updatedAt - clockSkew (" << identity.updatedAt << "-" << clockSkew << ")";
return;
}
@ -1535,7 +1538,7 @@ void AvatarData::processAvatarIdentity(const Identity& identity, bool& identityC
// use the timestamp from this identity, since we want to honor the updated times in "server clock"
// this will overwrite any changes we made locally to this AvatarData's _identityUpdatedAt
_identityUpdatedAt = identity.updatedAt;
_identityUpdatedAt = identity.updatedAt - clockSkew;
}
QByteArray AvatarData::identityByteArray() const {

View file

@ -538,7 +538,7 @@ public:
// identityChanged returns true if identity has changed, false otherwise.
// displayNameChanged returns true if displayName has changed, false otherwise.
void processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged);
void processAvatarIdentity(const Identity& identity, bool& identityChanged, bool& displayNameChanged, const qint64 clockSkew);
QByteArray identityByteArray() const;

View file

@ -148,7 +148,8 @@ void AvatarHashMap::processAvatarIdentityPacket(QSharedPointer<ReceivedMessage>
auto avatar = newOrExistingAvatar(identity.uuid, sendingNode);
bool identityChanged = false;
bool displayNameChanged = false;
avatar->processAvatarIdentity(identity, identityChanged, displayNameChanged);
// In this case, the "sendingNode" is the Avatar Mixer.
avatar->processAvatarIdentity(identity, identityChanged, displayNameChanged, sendingNode->getClockSkewUsec());
}
}

View file

@ -34,8 +34,9 @@ HeadData::HeadData(AvatarData* owningAvatar) :
_rightEyeBlink(0.0f),
_averageLoudness(0.0f),
_browAudioLift(0.0f),
_baseBlendshapeCoefficients(QVector<float>(0, 0.0f)),
_currBlendShapeCoefficients(QVector<float>(0, 0.0f)),
_blendshapeCoefficients(QVector<float>(0, 0.0f)),
_transientBlendshapeCoefficients(QVector<float>(0, 0.0f)),
_summedBlendshapeCoefficients(QVector<float>(0, 0.0f)),
_owningAvatar(owningAvatar)
{
@ -85,22 +86,22 @@ static const QMap<QString, int>& getBlendshapesLookupMap() {
}
const QVector<float>& HeadData::getSummedBlendshapeCoefficients() {
int maxSize = std::max(_baseBlendshapeCoefficients.size(), _blendshapeCoefficients.size());
if (_currBlendShapeCoefficients.size() != maxSize) {
_currBlendShapeCoefficients.resize(maxSize);
int maxSize = std::max(_blendshapeCoefficients.size(), _transientBlendshapeCoefficients.size());
if (_summedBlendshapeCoefficients.size() != maxSize) {
_summedBlendshapeCoefficients.resize(maxSize);
}
for (int i = 0; i < maxSize; i++) {
if (i >= _baseBlendshapeCoefficients.size()) {
_currBlendShapeCoefficients[i] = _blendshapeCoefficients[i];
} else if (i >= _blendshapeCoefficients.size()) {
_currBlendShapeCoefficients[i] = _baseBlendshapeCoefficients[i];
if (i >= _blendshapeCoefficients.size()) {
_summedBlendshapeCoefficients[i] = _transientBlendshapeCoefficients[i];
} else if (i >= _transientBlendshapeCoefficients.size()) {
_summedBlendshapeCoefficients[i] = _blendshapeCoefficients[i];
} else {
_currBlendShapeCoefficients[i] = _baseBlendshapeCoefficients[i] + _blendshapeCoefficients[i];
_summedBlendshapeCoefficients[i] = _blendshapeCoefficients[i] + _transientBlendshapeCoefficients[i];
}
}
return _currBlendShapeCoefficients;
return _summedBlendshapeCoefficients;
}
void HeadData::setBlendshape(QString name, float val) {
@ -112,10 +113,10 @@ void HeadData::setBlendshape(QString name, float val) {
if (_blendshapeCoefficients.size() <= it.value()) {
_blendshapeCoefficients.resize(it.value() + 1);
}
if (_baseBlendshapeCoefficients.size() <= it.value()) {
_baseBlendshapeCoefficients.resize(it.value() + 1);
if (_transientBlendshapeCoefficients.size() <= it.value()) {
_transientBlendshapeCoefficients.resize(it.value() + 1);
}
_baseBlendshapeCoefficients[it.value()] = val;
_blendshapeCoefficients[it.value()] = val;
}
}
@ -131,14 +132,16 @@ QJsonObject HeadData::toJson() const {
QJsonObject blendshapesJson;
for (auto name : blendshapeLookupMap.keys()) {
auto index = blendshapeLookupMap[name];
if (index >= _blendshapeCoefficients.size()) {
continue;
float value = 0.0f;
if (index < _blendshapeCoefficients.size()) {
value += _blendshapeCoefficients[index];
}
auto value = _blendshapeCoefficients[index];
if (value == 0.0f) {
continue;
if (index < _transientBlendshapeCoefficients.size()) {
value += _transientBlendshapeCoefficients[index];
}
if (value != 0.0f) {
blendshapesJson[name] = value;
}
blendshapesJson[name] = value;
}
if (!blendshapesJson.isEmpty()) {
headJson[JSON_AVATAR_HEAD_BLENDSHAPE_COEFFICIENTS] = blendshapesJson;
@ -163,8 +166,8 @@ void HeadData::fromJson(const QJsonObject& json) {
QJsonArray blendshapeCoefficientsJson = jsonValue.toArray();
for (const auto& blendshapeCoefficient : blendshapeCoefficientsJson) {
blendshapeCoefficients.push_back((float)blendshapeCoefficient.toDouble());
setBlendshapeCoefficients(blendshapeCoefficients);
}
setBlendshapeCoefficients(blendshapeCoefficients);
} else if (jsonValue.isObject()) {
QJsonObject blendshapeCoefficientsJson = jsonValue.toObject();
for (const QString& name : blendshapeCoefficientsJson.keys()) {

View file

@ -93,8 +93,8 @@ protected:
float _browAudioLift;
QVector<float> _blendshapeCoefficients;
QVector<float> _baseBlendshapeCoefficients;
QVector<float> _currBlendShapeCoefficients;
QVector<float> _transientBlendshapeCoefficients;
QVector<float> _summedBlendshapeCoefficients;
AvatarData* _owningAvatar;
private:

View file

@ -22,20 +22,20 @@
#include <BuildInfo.h>
#include <GLMHelpers.h>
QString SAVE_DIRECTORY = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + "/" + BuildInfo::MODIFIED_ORGANIZATION + "/" + BuildInfo::INTERFACE_NAME + "/hifi-input-recordings/";
QString FILE_PREFIX_NAME = "input-recording-";
QString COMPRESS_EXTENSION = ".tar.gz";
namespace controller {
QJsonObject poseToJsonObject(const Pose pose) {
QJsonObject newPose;
QJsonArray translation;
translation.append(pose.translation.x);
translation.append(pose.translation.y);
translation.append(pose.translation.z);
QJsonArray rotation;
rotation.append(pose.rotation.x);
rotation.append(pose.rotation.y);
@ -69,7 +69,7 @@ namespace controller {
QJsonArray angularVelocity = object["angularVelocity"].toArray();
pose.valid = object["valid"].toBool();
pose.translation.x = translation[0].toDouble();
pose.translation.y = translation[1].toDouble();
pose.translation.z = translation[2].toDouble();
@ -89,13 +89,13 @@ namespace controller {
return pose;
}
void exportToFile(QJsonObject& object) {
if (!QDir(SAVE_DIRECTORY).exists()) {
QDir().mkdir(SAVE_DIRECTORY);
}
QString timeStamp = QDateTime::currentDateTime().toString(Qt::ISODate);
timeStamp.replace(":", "-");
QString fileName = SAVE_DIRECTORY + FILE_PREFIX_NAME + timeStamp + COMPRESS_EXTENSION;
@ -124,7 +124,7 @@ namespace controller {
status = true;
return object;
}
InputRecorder::InputRecorder() {}
InputRecorder::~InputRecorder() {}
@ -195,16 +195,16 @@ namespace controller {
_framesRecorded = data["frameCount"].toInt();
QJsonArray actionArrayList = data["actionList"].toArray();
QJsonArray poseArrayList = data["poseList"].toArray();
for (int actionIndex = 0; actionIndex < actionArrayList.size(); actionIndex++) {
QJsonArray actionState = actionArrayList[actionIndex].toArray();
for (int index = 0; index < actionState.size(); index++) {
_currentFrameActions[index] = actionState[index].toInt();
_currentFrameActions[index] = actionState[index].toDouble();
}
_actionStateList.push_back(_currentFrameActions);
_currentFrameActions = ActionStates(toInt(Action::NUM_ACTIONS));
}
for (int poseIndex = 0; poseIndex < poseArrayList.size(); poseIndex++) {
QJsonArray poseState = poseArrayList[poseIndex].toArray();
for (int index = 0; index < poseState.size(); index++) {
@ -250,13 +250,13 @@ namespace controller {
for(auto& channel : _currentFramePoses) {
channel = Pose();
}
for(auto& channel : _currentFrameActions) {
channel = 0.0f;
}
}
}
float InputRecorder::getActionState(controller::Action action) {
if (_actionStateList.size() > 0 ) {
return _actionStateList[_playCount][toInt(action)];

View file

@ -1,5 +1,5 @@
set(TARGET_NAME gpu-gl)
setup_hifi_library()
setup_hifi_library(Concurrent)
link_hifi_libraries(shared gl gpu)
if (UNIX)
target_link_libraries(${TARGET_NAME} pthread)

View file

@ -160,8 +160,6 @@ const uvec3 GLVariableAllocationSupport::INITIAL_MIP_TRANSFER_DIMENSIONS { 64, 6
WorkQueue GLVariableAllocationSupport::_transferQueue;
WorkQueue GLVariableAllocationSupport::_promoteQueue;
WorkQueue GLVariableAllocationSupport::_demoteQueue;
TexturePointer GLVariableAllocationSupport::_currentTransferTexture;
TransferJobPointer GLVariableAllocationSupport::_currentTransferJob;
size_t GLVariableAllocationSupport::_frameTexturesCreated { 0 };
#define OVERSUBSCRIBED_PRESSURE_VALUE 0.95f
@ -176,30 +174,19 @@ const uvec3 GLVariableAllocationSupport::MAX_TRANSFER_DIMENSIONS { 1024, 1024, 1
const size_t GLVariableAllocationSupport::MAX_TRANSFER_SIZE = GLVariableAllocationSupport::MAX_TRANSFER_DIMENSIONS.x * GLVariableAllocationSupport::MAX_TRANSFER_DIMENSIONS.y * 4;
#if THREADED_TEXTURE_BUFFERING
std::shared_ptr<std::thread> TransferJob::_bufferThread { nullptr };
std::atomic<bool> TransferJob::_shutdownBufferingThread { false };
Mutex TransferJob::_mutex;
TransferJob::VoidLambdaQueue TransferJob::_bufferLambdaQueue;
void TransferJob::startTransferLoop() {
if (_bufferThread) {
return;
}
_shutdownBufferingThread = false;
_bufferThread = std::make_shared<std::thread>([] {
TransferJob::bufferLoop();
TexturePointer GLVariableAllocationSupport::_currentTransferTexture;
TransferJobPointer GLVariableAllocationSupport::_currentTransferJob;
QThreadPool* TransferJob::_bufferThreadPool { nullptr };
void TransferJob::startBufferingThread() {
static std::once_flag once;
std::call_once(once, [&] {
_bufferThreadPool = new QThreadPool(qApp);
_bufferThreadPool->setMaxThreadCount(1);
});
}
void TransferJob::stopTransferLoop() {
if (!_bufferThread) {
return;
}
_shutdownBufferingThread = true;
_bufferThread->join();
_bufferThread.reset();
_shutdownBufferingThread = false;
}
#endif
TransferJob::TransferJob(const GLTexture& parent, uint16_t sourceMip, uint16_t targetMip, uint8_t face, uint32_t lines, uint32_t lineOffset)
@ -233,7 +220,6 @@ TransferJob::TransferJob(const GLTexture& parent, uint16_t sourceMip, uint16_t t
// Buffering can invoke disk IO, so it should be off of the main and render threads
_bufferingLambda = [=] {
_mipData = _parent._gpuObject.accessStoredMipFace(sourceMip, face)->createView(_transferSize, _transferOffset);
_bufferingCompleted = true;
};
_transferLambda = [=] {
@ -243,65 +229,66 @@ TransferJob::TransferJob(const GLTexture& parent, uint16_t sourceMip, uint16_t t
}
TransferJob::TransferJob(const GLTexture& parent, std::function<void()> transferLambda)
: _parent(parent), _bufferingCompleted(true), _transferLambda(transferLambda) {
: _parent(parent), _bufferingRequired(false), _transferLambda(transferLambda) {
}
TransferJob::~TransferJob() {
Backend::updateTextureTransferPendingSize(_transferSize, 0);
}
bool TransferJob::tryTransfer() {
// Disable threaded texture transfer for now
#if THREADED_TEXTURE_BUFFERING
// Are we ready to transfer
if (_bufferingCompleted) {
_transferLambda();
if (!bufferingCompleted()) {
startBuffering();
return false;
}
#else
if (_bufferingRequired) {
_bufferingLambda();
}
#endif
_transferLambda();
return true;
}
#if THREADED_TEXTURE_BUFFERING
bool TransferJob::bufferingRequired() const {
if (!_bufferingRequired) {
return false;
}
// The default state of a QFuture is with status Canceled | Started | Finished,
// so we have to check isCancelled before we check the actual state
if (_bufferingStatus.isCanceled()) {
return true;
}
startBuffering();
return false;
#else
if (!_bufferingCompleted) {
_bufferingLambda();
_bufferingCompleted = true;
}
_transferLambda();
return true;
#endif
return !_bufferingStatus.isStarted();
}
#if THREADED_TEXTURE_BUFFERING
bool TransferJob::bufferingCompleted() const {
if (!_bufferingRequired) {
return true;
}
// The default state of a QFuture is with status Canceled | Started | Finished,
// so we have to check isCancelled before we check the actual state
if (_bufferingStatus.isCanceled()) {
return false;
}
return _bufferingStatus.isFinished();
}
void TransferJob::startBuffering() {
if (_bufferingStarted) {
return;
}
_bufferingStarted = true;
{
Lock lock(_mutex);
_bufferLambdaQueue.push(_bufferingLambda);
}
}
void TransferJob::bufferLoop() {
while (!_shutdownBufferingThread) {
VoidLambdaQueue workingQueue;
{
Lock lock(_mutex);
_bufferLambdaQueue.swap(workingQueue);
}
if (workingQueue.empty()) {
QThread::msleep(5);
continue;
}
while (!workingQueue.empty()) {
workingQueue.front()();
workingQueue.pop();
}
if (bufferingRequired()) {
assert(_bufferingStatus.isCanceled());
_bufferingStatus = QtConcurrent::run(_bufferThreadPool, [=] {
_bufferingLambda();
});
assert(!_bufferingStatus.isCanceled());
assert(_bufferingStatus.isStarted());
}
}
#endif
@ -316,7 +303,9 @@ GLVariableAllocationSupport::~GLVariableAllocationSupport() {
void GLVariableAllocationSupport::addMemoryManagedTexture(const TexturePointer& texturePointer) {
_memoryManagedTextures.push_back(texturePointer);
addToWorkQueue(texturePointer);
if (MemoryPressureState::Idle != _memoryPressureState) {
addToWorkQueue(texturePointer);
}
}
void GLVariableAllocationSupport::addToWorkQueue(const TexturePointer& texturePointer) {
@ -345,10 +334,8 @@ void GLVariableAllocationSupport::addToWorkQueue(const TexturePointer& texturePo
break;
case MemoryPressureState::Idle:
break;
default:
Q_UNREACHABLE();
break;
}
}
@ -364,10 +351,10 @@ WorkQueue& GLVariableAllocationSupport::getActiveWorkQueue() {
case MemoryPressureState::Transfer:
return _transferQueue;
default:
case MemoryPressureState::Idle:
Q_UNREACHABLE();
break;
}
Q_UNREACHABLE();
return empty;
}
@ -460,16 +447,11 @@ void GLVariableAllocationSupport::updateMemoryPressure() {
}
if (newState != _memoryPressureState) {
_memoryPressureState = newState;
#if THREADED_TEXTURE_BUFFERING
if (MemoryPressureState::Transfer == _memoryPressureState) {
TransferJob::stopTransferLoop();
TransferJob::startBufferingThread();
}
_memoryPressureState = newState;
if (MemoryPressureState::Transfer == _memoryPressureState) {
TransferJob::startTransferLoop();
}
#else
_memoryPressureState = newState;
#endif
// Clear the existing queue
_transferQueue = WorkQueue();
@ -487,49 +469,111 @@ void GLVariableAllocationSupport::updateMemoryPressure() {
}
}
TexturePointer GLVariableAllocationSupport::getNextWorkQueueItem(WorkQueue& workQueue) {
while (!workQueue.empty()) {
auto workTarget = workQueue.top();
auto texture = workTarget.first.lock();
if (!texture) {
workQueue.pop();
continue;
}
// Check whether the resulting texture can actually have work performed
GLTexture* gltexture = Backend::getGPUObject<GLTexture>(*texture);
GLVariableAllocationSupport* vartexture = dynamic_cast<GLVariableAllocationSupport*>(gltexture);
switch (_memoryPressureState) {
case MemoryPressureState::Oversubscribed:
if (vartexture->canDemote()) {
return texture;
}
break;
case MemoryPressureState::Undersubscribed:
if (vartexture->canPromote()) {
return texture;
}
break;
case MemoryPressureState::Transfer:
if (vartexture->hasPendingTransfers()) {
return texture;
}
break;
case MemoryPressureState::Idle:
Q_UNREACHABLE();
break;
}
// If we got here, then the texture has no work to do in the current state,
// so pop it off the queue and continue
workQueue.pop();
}
return TexturePointer();
}
void GLVariableAllocationSupport::processWorkQueue(WorkQueue& workQueue) {
if (workQueue.empty()) {
return;
}
// Get the front of the work queue to perform work
auto texture = getNextWorkQueueItem(workQueue);
if (!texture) {
return;
}
// Grab the first item off the demote queue
PROFILE_RANGE(render_gpu_gl, __FUNCTION__);
GLTexture* gltexture = Backend::getGPUObject<GLTexture>(*texture);
GLVariableAllocationSupport* vartexture = dynamic_cast<GLVariableAllocationSupport*>(gltexture);
switch (_memoryPressureState) {
case MemoryPressureState::Oversubscribed:
vartexture->demote();
workQueue.pop();
addToWorkQueue(texture);
break;
case MemoryPressureState::Undersubscribed:
vartexture->promote();
workQueue.pop();
addToWorkQueue(texture);
break;
case MemoryPressureState::Transfer:
if (vartexture->executeNextTransfer(texture)) {
workQueue.pop();
addToWorkQueue(texture);
#if THREADED_TEXTURE_BUFFERING
// Eagerly start the next buffering job if possible
texture = getNextWorkQueueItem(workQueue);
if (texture) {
gltexture = Backend::getGPUObject<GLTexture>(*texture);
vartexture = dynamic_cast<GLVariableAllocationSupport*>(gltexture);
vartexture->executeNextBuffer(texture);
}
#endif
}
break;
case MemoryPressureState::Idle:
Q_UNREACHABLE();
break;
}
}
void GLVariableAllocationSupport::processWorkQueues() {
if (MemoryPressureState::Idle == _memoryPressureState) {
return;
}
auto& workQueue = getActiveWorkQueue();
PROFILE_RANGE(render_gpu_gl, __FUNCTION__);
while (!workQueue.empty()) {
auto workTarget = workQueue.top();
workQueue.pop();
auto texture = workTarget.first.lock();
if (!texture) {
continue;
}
// Grab the first item off the demote queue
GLTexture* gltexture = Backend::getGPUObject<GLTexture>(*texture);
GLVariableAllocationSupport* vartexture = dynamic_cast<GLVariableAllocationSupport*>(gltexture);
if (MemoryPressureState::Oversubscribed == _memoryPressureState) {
if (!vartexture->canDemote()) {
continue;
}
vartexture->demote();
_memoryPressureStateStale = true;
} else if (MemoryPressureState::Undersubscribed == _memoryPressureState) {
if (!vartexture->canPromote()) {
continue;
}
vartexture->promote();
_memoryPressureStateStale = true;
} else if (MemoryPressureState::Transfer == _memoryPressureState) {
if (!vartexture->hasPendingTransfers()) {
continue;
}
vartexture->executeNextTransfer(texture);
} else {
Q_UNREACHABLE();
}
// Reinject into the queue if more work to be done
addToWorkQueue(texture);
break;
}
// Do work on the front of the queue
processWorkQueue(workQueue);
if (workQueue.empty()) {
_memoryPressureState = MemoryPressureState::Idle;
@ -543,28 +587,83 @@ void GLVariableAllocationSupport::manageMemory() {
processWorkQueues();
}
bool GLVariableAllocationSupport::executeNextTransfer(const TexturePointer& currentTexture) {
#if THREADED_TEXTURE_BUFFERING
// If a transfer job is active on the buffering thread, but has not completed it's buffering lambda,
// then we need to exit early, since we don't want to have the transfer job leave scope while it's
// being used in another thread -- See https://highfidelity.fogbugz.com/f/cases/4626
if (_currentTransferJob && !_currentTransferJob->bufferingCompleted()) {
return false;
}
#endif
void GLVariableAllocationSupport::executeNextTransfer(const TexturePointer& currentTexture) {
if (_populatedMip <= _allocatedMip) {
#if THREADED_TEXTURE_BUFFERING
_currentTransferJob.reset();
_currentTransferTexture.reset();
#endif
return true;
}
// If the transfer queue is empty, rebuild it
if (_pendingTransfers.empty()) {
populateTransferQueue();
}
bool result = false;
if (!_pendingTransfers.empty()) {
#if THREADED_TEXTURE_BUFFERING
// If there is a current transfer, but it's not the top of the pending transfer queue, then it's an orphan, so we want to abandon it.
if (_currentTransferJob && _currentTransferJob != _pendingTransfers.front()) {
_currentTransferJob.reset();
}
if (!_currentTransferJob) {
// Keeping hold of a strong pointer to the transfer job ensures that if the pending transfer queue is rebuilt, the transfer job
// doesn't leave scope, causing a crash in the buffering thread
_currentTransferJob = _pendingTransfers.front();
// Keeping hold of a strong pointer during the transfer ensures that the transfer thread cannot try to access a destroyed texture
_currentTransferTexture = currentTexture;
}
// transfer jobs use asynchronous buffering of the texture data because it may involve disk IO, so we execute a try here to determine if the buffering
// is complete
if (_currentTransferJob->tryTransfer()) {
_pendingTransfers.pop();
// Once a given job is finished, release the shared pointers keeping them alive
_currentTransferTexture.reset();
_currentTransferJob.reset();
result = true;
}
#else
if (_pendingTransfers.front()->tryTransfer()) {
_pendingTransfers.pop();
result = true;
}
#endif
}
return result;
}
#if THREADED_TEXTURE_BUFFERING
void GLVariableAllocationSupport::executeNextBuffer(const TexturePointer& currentTexture) {
if (_currentTransferJob && !_currentTransferJob->bufferingCompleted()) {
return;
}
// If the transfer queue is empty, rebuild it
if (_pendingTransfers.empty()) {
populateTransferQueue();
}
if (!_pendingTransfers.empty()) {
// Keeping hold of a strong pointer during the transfer ensures that the transfer thread cannot try to access a destroyed texture
_currentTransferTexture = currentTexture;
// Keeping hold of a strong pointer to the transfer job ensures that if the pending transfer queue is rebuilt, the transfer job
// doesn't leave scope, causing a crash in the buffering thread
_currentTransferJob = _pendingTransfers.front();
// transfer jobs use asynchronous buffering of the texture data because it may involve disk IO, so we execute a try here to determine if the buffering
// is complete
if (_currentTransferJob->tryTransfer()) {
_pendingTransfers.pop();
_currentTransferTexture.reset();
_currentTransferJob.reset();
if (!_currentTransferJob) {
_currentTransferJob = _pendingTransfers.front();
_currentTransferTexture = currentTexture;
}
_currentTransferJob->startBuffering();
}
}
#endif

View file

@ -8,6 +8,9 @@
#ifndef hifi_gpu_gl_GLTexture_h
#define hifi_gpu_gl_GLTexture_h
#include <QtCore/QThreadPool>
#include <QtConcurrent>
#include "GLShared.h"
#include "GLBackend.h"
#include "GLTexelFormat.h"
@ -47,24 +50,19 @@ public:
class TransferJob {
using VoidLambda = std::function<void()>;
using VoidLambdaQueue = std::queue<VoidLambda>;
using ThreadPointer = std::shared_ptr<std::thread>;
const GLTexture& _parent;
Texture::PixelsPointer _mipData;
size_t _transferOffset { 0 };
size_t _transferSize { 0 };
// Indicates if a transfer from backing storage to interal storage has started
bool _bufferingStarted { false };
bool _bufferingCompleted { false };
bool _bufferingRequired { true };
VoidLambda _transferLambda;
VoidLambda _bufferingLambda;
#if THREADED_TEXTURE_BUFFERING
static Mutex _mutex;
static VoidLambdaQueue _bufferLambdaQueue;
static ThreadPointer _bufferThread;
static std::atomic<bool> _shutdownBufferingThread;
static void bufferLoop();
// Indicates if a transfer from backing storage to interal storage has started
QFuture<void> _bufferingStatus;
static QThreadPool* _bufferThreadPool;
#endif
public:
@ -75,14 +73,13 @@ public:
bool tryTransfer();
#if THREADED_TEXTURE_BUFFERING
static void startTransferLoop();
static void stopTransferLoop();
void startBuffering();
bool bufferingRequired() const;
bool bufferingCompleted() const;
static void startBufferingThread();
#endif
private:
#if THREADED_TEXTURE_BUFFERING
void startBuffering();
#endif
void transfer();
};
@ -100,8 +97,10 @@ protected:
static WorkQueue _transferQueue;
static WorkQueue _promoteQueue;
static WorkQueue _demoteQueue;
#if THREADED_TEXTURE_BUFFERING
static TexturePointer _currentTransferTexture;
static TransferJobPointer _currentTransferJob;
#endif
static const uvec3 INITIAL_MIP_TRANSFER_DIMENSIONS;
static const uvec3 MAX_TRANSFER_DIMENSIONS;
static const size_t MAX_TRANSFER_SIZE;
@ -109,6 +108,8 @@ protected:
static void updateMemoryPressure();
static void processWorkQueues();
static void processWorkQueue(WorkQueue& workQueue);
static TexturePointer getNextWorkQueueItem(WorkQueue& workQueue);
static void addToWorkQueue(const TexturePointer& texture);
static WorkQueue& getActiveWorkQueue();
@ -118,7 +119,10 @@ protected:
bool canPromote() const { return _allocatedMip > _minAllocatedMip; }
bool canDemote() const { return _allocatedMip < _maxAllocatedMip; }
bool hasPendingTransfers() const { return _populatedMip > _allocatedMip; }
void executeNextTransfer(const TexturePointer& currentTexture);
#if THREADED_TEXTURE_BUFFERING
void executeNextBuffer(const TexturePointer& currentTexture);
#endif
bool executeNextTransfer(const TexturePointer& currentTexture);
virtual void populateTransferQueue() = 0;
virtual void promote() = 0;
virtual void demote() = 0;

View file

@ -17,7 +17,6 @@
#include <thread>
#define INCREMENTAL_TRANSFER 0
#define THREADED_TEXTURE_BUFFERING 1
#define GPU_SSBO_TRANSFORM_OBJECT 1
namespace gpu { namespace gl45 {

View file

@ -17,6 +17,7 @@
#include <unordered_set>
#include <QDir>
#include <QSaveFile>
#include <PathUtils.h>
@ -110,13 +111,14 @@ FilePointer FileCache::writeFile(const char* data, File::Metadata&& metadata) {
return file;
}
// write the new file
FILE* saveFile = fopen(filepath.c_str(), "wb");
if (saveFile != nullptr && fwrite(data, metadata.length, 1, saveFile) && fclose(saveFile) == 0) {
QSaveFile saveFile(QString::fromStdString(filepath));
if (saveFile.open(QIODevice::WriteOnly)
&& saveFile.write(data, metadata.length) == static_cast<qint64>(metadata.length)
&& saveFile.commit()) {
file = addFile(std::move(metadata), filepath);
} else {
qCWarning(file_cache, "[%s] Failed to write %s (%s)", _dirname.c_str(), metadata.key.c_str(), strerror(errno));
errno = 0;
qCWarning(file_cache, "[%s] Failed to write %s", _dirname.c_str(), metadata.key.c_str());
}
return file;

View file

@ -24,6 +24,7 @@ class AudioScriptingInterface : public QObject, public Dependency {
SINGLETON_DEPENDENCY
public:
virtual ~AudioScriptingInterface() {}
void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; }
protected:

View file

@ -250,6 +250,10 @@ static void addButtonProxyToQmlTablet(QQuickItem* qmlTablet, TabletButtonProxy*
if (QThread::currentThread() != qmlTablet->thread()) {
connectionType = Qt::BlockingQueuedConnection;
}
if (buttonProxy == NULL){
qCCritical(scriptengine) << "TabletScriptingInterface addButtonProxyToQmlTablet buttonProxy is NULL";
return;
}
bool hasResult = QMetaObject::invokeMethod(qmlTablet, "addButtonProxy", connectionType,
Q_RETURN_ARG(QVariant, resultVar), Q_ARG(QVariant, buttonProxy->getProperties()));
if (!hasResult) {

View file

@ -10,6 +10,7 @@
//
#include "ViveControllerManager.h"
#include <algorithm>
#include <PerfStat.h>
#include <PathUtils.h>
@ -20,7 +21,13 @@
#include <NumericalConstants.h>
#include <ui-plugins/PluginContainer.h>
#include <UserActivityLogger.h>
#include <NumericalConstants.h>
#include <Preferences.h>
#include <SettingHandle.h>
#include <OffscreenUi.h>
#include <GLMHelpers.h>
#include <glm/ext.hpp>
#include <glm/gtc/quaternion.hpp>
#include <controllers/UserInputMapper.h>
@ -36,14 +43,32 @@ void releaseOpenVrSystem();
static const char* CONTROLLER_MODEL_STRING = "vr_controller_05_wireless_b";
const quint64 CALIBRATION_TIMELAPSE = 2 * USECS_PER_SECOND;
static const char* MENU_PARENT = "Avatar";
static const char* MENU_NAME = "Vive Controllers";
static const char* MENU_PATH = "Avatar" ">" "Vive Controllers";
static const char* RENDER_CONTROLLERS = "Render Hand Controllers";
static const int MIN_PUCK_COUNT = 2;
static const int MIN_FEET_AND_HIPS = 3;
static const int MIN_FEET_HIPS_CHEST = 4;
static const int FIRST_FOOT = 0;
static const int SECOND_FOOT = 1;
static const int HIP = 2;
static const int CHEST = 3;
const char* ViveControllerManager::NAME { "OpenVR" };
static glm::mat4 computeOffset(glm::mat4 defaultToReferenceMat, glm::mat4 defaultJointMat, controller::Pose puckPose) {
glm::mat4 poseMat = createMatFromQuatAndPos(puckPose.rotation, puckPose.translation);
glm::mat4 referenceJointMat = defaultToReferenceMat * defaultJointMat;
return glm::inverse(poseMat) * referenceJointMat;
}
static bool sortPucksYPosition(std::pair<uint32_t, controller::Pose> firstPuck, std::pair<uint32_t, controller::Pose> secondPuck) {
return (firstPuck.second.translation.y < firstPuck.second.translation.y);
}
bool ViveControllerManager::isSupported() const {
return openVrSupported();
}
@ -125,6 +150,7 @@ void ViveControllerManager::pluginUpdate(float deltaTime, const controller::Inpu
void ViveControllerManager::InputDevice::update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) {
_poseStateMap.clear();
_buttonPressedMap.clear();
_validTrackedObjects.clear();
// While the keyboard is open, we defer strictly to the keyboard values
if (isOpenVrKeyboardShown()) {
@ -143,6 +169,7 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle
// collect poses for all generic trackers
for (int i = 0; i < vr::k_unMaxTrackedDeviceCount; i++) {
handleTrackedObject(i, inputCalibrationData);
handleHmd(i, inputCalibrationData);
}
// handle haptics
@ -164,10 +191,27 @@ void ViveControllerManager::InputDevice::update(float deltaTime, const controlle
numTrackedControllers++;
}
_trackedControllers = numTrackedControllers;
if (checkForCalibrationEvent()) {
quint64 currentTime = usecTimestampNow();
if (!_timeTilCalibrationSet) {
_timeTilCalibrationSet = true;
_timeTilCalibration = currentTime + CALIBRATION_TIMELAPSE;
}
if (currentTime > _timeTilCalibration && !_triggersPressedHandled) {
_triggersPressedHandled = true;
calibrateOrUncalibrate(inputCalibrationData);
}
} else {
_triggersPressedHandled = false;
_timeTilCalibrationSet = false;
}
updateCalibratedLimbs();
}
void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) {
uint32_t poseIndex = controller::TRACKED_OBJECT_00 + deviceIndex;
if (_system->IsTrackedDeviceConnected(deviceIndex) &&
@ -185,12 +229,140 @@ void ViveControllerManager::InputDevice::handleTrackedObject(uint32_t deviceInde
// transform into avatar frame
glm::mat4 controllerToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat;
_poseStateMap[poseIndex] = pose.transform(controllerToAvatar);
_validTrackedObjects.push_back(std::make_pair(poseIndex, _poseStateMap[poseIndex]));
} else {
controller::Pose invalidPose;
_poseStateMap[poseIndex] = invalidPose;
}
}
void ViveControllerManager::InputDevice::calibrateOrUncalibrate(const controller::InputCalibrationData& inputCalibration) {
if (!_calibrated) {
calibrate(inputCalibration);
} else {
uncalibrate();
}
}
void ViveControllerManager::InputDevice::calibrate(const controller::InputCalibrationData& inputCalibration) {
// convert the hmd head from sensor space to avatar space
glm::mat4 hmdSensorFlippedMat = inputCalibration.hmdSensorMat * Matrices::Y_180;
glm::mat4 sensorToAvatarMat = glm::inverse(inputCalibration.avatarMat) * inputCalibration.sensorToWorldMat;
glm::mat4 hmdAvatarMat = sensorToAvatarMat * hmdSensorFlippedMat;
// cancel the roll and pitch for the hmd head
glm::quat hmdRotation = cancelOutRollAndPitch(glmExtractRotation(hmdAvatarMat));
glm::vec3 hmdTranslation = extractTranslation(hmdAvatarMat);
glm::mat4 currentHmd = createMatFromQuatAndPos(hmdRotation, hmdTranslation);
// calculate the offset from the centerOfEye to defaultHeadMat
glm::mat4 defaultHeadOffset = glm::inverse(inputCalibration.defaultCenterEyeMat) * inputCalibration.defaultHeadMat;
glm::mat4 currentHead = currentHmd * defaultHeadOffset;
// calculate the defaultToRefrenceXform
glm::mat4 defaultToReferenceMat = currentHead * glm::inverse(inputCalibration.defaultHeadMat);
int puckCount = (int)_validTrackedObjects.size();
_config = _preferedConfig;
if (_config != Config::Auto && puckCount < MIN_PUCK_COUNT) {
uncalibrate();
return;
} else if (_config == Config::Auto){
if (puckCount == MIN_PUCK_COUNT) {
_config = Config::Feet;
} else if (puckCount == MIN_FEET_AND_HIPS) {
_config = Config::FeetAndHips;
} else if (puckCount >= MIN_FEET_HIPS_CHEST) {
_config = Config::FeetHipsAndChest;
} else {
uncalibrate();
return;
}
}
std::sort(_validTrackedObjects.begin(), _validTrackedObjects.end(), sortPucksYPosition);
auto& firstFoot = _validTrackedObjects[FIRST_FOOT];
auto& secondFoot = _validTrackedObjects[SECOND_FOOT];
controller::Pose& firstFootPose = firstFoot.second;
controller::Pose& secondFootPose = secondFoot.second;
if (firstFootPose.translation.x < secondFootPose.translation.x) {
_jointToPuckMap[controller::LEFT_FOOT] = firstFoot.first;
_pucksOffset[firstFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftFoot, firstFootPose);
_jointToPuckMap[controller::RIGHT_FOOT] = secondFoot.first;
_pucksOffset[secondFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightFoot, secondFootPose);
} else {
_jointToPuckMap[controller::LEFT_FOOT] = secondFoot.first;
_pucksOffset[secondFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultLeftFoot, secondFootPose);
_jointToPuckMap[controller::RIGHT_FOOT] = firstFoot.first;
_pucksOffset[firstFoot.first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultRightFoot, firstFootPose);
}
if (_config == Config::Feet) {
// done
} else if (_config == Config::FeetAndHips && puckCount >= MIN_FEET_AND_HIPS) {
_jointToPuckMap[controller::HIPS] = _validTrackedObjects[HIP].first;
_pucksOffset[_validTrackedObjects[HIP].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHips, _validTrackedObjects[HIP].second);
} else if (_config == Config::FeetHipsAndChest && puckCount >= MIN_FEET_HIPS_CHEST) {
_jointToPuckMap[controller::HIPS] = _validTrackedObjects[HIP].first;
_pucksOffset[_validTrackedObjects[HIP].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultHips, _validTrackedObjects[HIP].second);
_jointToPuckMap[controller::SPINE2] = _validTrackedObjects[CHEST].first;
_pucksOffset[_validTrackedObjects[CHEST].first] = computeOffset(defaultToReferenceMat, inputCalibration.defaultSpine2, _validTrackedObjects[CHEST].second);
} else {
uncalibrate();
return;
}
_calibrated = true;
}
void ViveControllerManager::InputDevice::uncalibrate() {
_config = Config::Auto;
_pucksOffset.clear();
_jointToPuckMap.clear();
_calibrated = false;
}
void ViveControllerManager::InputDevice::updateCalibratedLimbs() {
_poseStateMap[controller::LEFT_FOOT] = addOffsetToPuckPose(controller::LEFT_FOOT);
_poseStateMap[controller::RIGHT_FOOT] = addOffsetToPuckPose(controller::RIGHT_FOOT);
_poseStateMap[controller::HIPS] = addOffsetToPuckPose(controller::HIPS);
_poseStateMap[controller::SPINE2] = addOffsetToPuckPose(controller::SPINE2);
}
controller::Pose ViveControllerManager::InputDevice::addOffsetToPuckPose(int joint) const {
auto puck = _jointToPuckMap.find(joint);
if (puck != _jointToPuckMap.end()) {
uint32_t puckIndex = puck->second;
auto puckPose = _poseStateMap.find(puckIndex);
auto puckOffset = _pucksOffset.find(puckIndex);
if ((puckPose != _poseStateMap.end()) && (puckOffset != _pucksOffset.end())) {
return puckPose->second.postTransform(puckOffset->second);
}
}
return controller::Pose();
}
void ViveControllerManager::InputDevice::handleHmd(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData) {
uint32_t poseIndex = controller::TRACKED_OBJECT_00 + deviceIndex;
if (_system->IsTrackedDeviceConnected(deviceIndex) &&
_system->GetTrackedDeviceClass(deviceIndex) == vr::TrackedDeviceClass_HMD &&
_nextSimPoseData.vrPoses[deviceIndex].bPoseIsValid) {
const mat4& mat = _nextSimPoseData.poses[deviceIndex];
const vec3 linearVelocity = _nextSimPoseData.linearVelocities[deviceIndex];
const vec3 angularVelocity = _nextSimPoseData.angularVelocities[deviceIndex];
handleHeadPoseEvent(inputCalibrationData, mat, linearVelocity, angularVelocity);
}
}
void ViveControllerManager::InputDevice::handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand) {
if (_system->IsTrackedDeviceConnected(deviceIndex) &&
@ -262,7 +434,7 @@ void ViveControllerManager::InputDevice::handleAxisEvent(float deltaTime, uint32
_axisStateMap[isLeftHand ? LY : RY] = stick.y;
} else if (axis == vr::k_EButton_SteamVR_Trigger) {
_axisStateMap[isLeftHand ? LT : RT] = x;
// The click feeling on the Vive controller trigger represents a value of *precisely* 1.0,
// The click feeling on the Vive controller trigger represents a value of *precisely* 1.0,
// so we can expose that as an additional button
if (x >= 1.0f) {
_buttonPressedMap.insert(isLeftHand ? LT_CLICK : RT_CLICK);
@ -276,6 +448,14 @@ enum ViveButtonChannel {
RIGHT_APP_MENU
};
bool ViveControllerManager::InputDevice::checkForCalibrationEvent() {
auto& endOfMap = _buttonPressedMap.end();
auto& leftTrigger = _buttonPressedMap.find(controller::LT);
auto& rightTrigger = _buttonPressedMap.find(controller::RT);
auto& leftAppButton = _buttonPressedMap.find(LEFT_APP_MENU);
auto& rightAppButton = _buttonPressedMap.find(RIGHT_APP_MENU);
return ((leftTrigger != endOfMap && leftAppButton != endOfMap) && (rightTrigger != endOfMap && rightAppButton != endOfMap));
}
// These functions do translation from the Steam IDs to the standard controller IDs
void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint32_t button, bool pressed, bool touched, bool isLeftHand) {
@ -305,6 +485,19 @@ void ViveControllerManager::InputDevice::handleButtonEvent(float deltaTime, uint
}
}
void ViveControllerManager::InputDevice::handleHeadPoseEvent(const controller::InputCalibrationData& inputCalibrationData, const mat4& mat,
const vec3& linearVelocity, const vec3& angularVelocity) {
//perform a 180 flip to make the HMD face the +z instead of -z, beacuse the head faces +z
glm::mat4 matYFlip = mat * Matrices::Y_180;
controller::Pose pose(extractTranslation(matYFlip), glmExtractRotation(matYFlip), linearVelocity, angularVelocity);
glm::mat4 sensorToAvatar = glm::inverse(inputCalibrationData.avatarMat) * inputCalibrationData.sensorToWorldMat;
glm::mat4 defaultHeadOffset = glm::inverse(inputCalibrationData.defaultCenterEyeMat) * inputCalibrationData.defaultHeadMat;
controller::Pose hmdHeadPose = pose.transform(sensorToAvatar);
_poseStateMap[controller::HEAD] = hmdHeadPose.postTransform(defaultHeadOffset);
}
void ViveControllerManager::InputDevice::handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData,
const mat4& mat, const vec3& linearVelocity,
const vec3& angularVelocity, bool isLeftHand) {
@ -353,7 +546,7 @@ void ViveControllerManager::InputDevice::hapticsHelper(float deltaTime, bool lef
float hapticTime = strength * MAX_HAPTIC_TIME;
if (hapticTime < duration * 1000.0f) {
_system->TriggerHapticPulse(deviceIndex, 0, hapticTime);
}
}
float remainingHapticTime = duration - (hapticTime / 1000.0f + deltaTime * 1000.0f); // in milliseconds
if (leftHand) {
@ -364,6 +557,74 @@ void ViveControllerManager::InputDevice::hapticsHelper(float deltaTime, bool lef
}
}
void ViveControllerManager::InputDevice::loadSettings() {
Settings settings;
settings.beginGroup("PUCK_CONFIG");
{
_preferedConfig = (Config)settings.value("configuration", QVariant((int)Config::Auto)).toInt();
}
settings.endGroup();
}
void ViveControllerManager::InputDevice::saveSettings() const {
Settings settings;
settings.beginGroup("PUCK_CONFIG");
{
settings.setValue(QString("configuration"), (int)_preferedConfig);
}
settings.endGroup();
}
QString ViveControllerManager::InputDevice::configToString() {
QString currentConfig;
switch (_preferedConfig) {
case Config::Auto:
currentConfig = "Auto";
break;
case Config::Feet:
currentConfig = "Feet";
break;
case Config::FeetAndHips:
currentConfig = "FeetAndHips";
break;
case Config::FeetHipsAndChest:
currentConfig = "FeetHipsAndChest";
break;
}
return currentConfig;
}
void ViveControllerManager::InputDevice::setConfigFromString(const QString& value) {
if (value == "Auto") {
_preferedConfig = Config::Auto;
} else if (value == "Feet") {
_preferedConfig = Config::Feet;
} else if (value == "FeetAndHips") {
_preferedConfig = Config::FeetAndHips;
} else if (value == "FeetHipsAndChest") {
_preferedConfig = Config::FeetHipsAndChest;
}
}
void ViveControllerManager::InputDevice::createPreferences() {
loadSettings();
auto preferences = DependencyManager::get<Preferences>();
static const QString VIVE_PUCKS_CONFIG = "Vive Pucks Configuration";
{
auto getter = [this]()->QString { return configToString(); };
auto setter = [this](const QString& value) { setConfigFromString(value); saveSettings(); };
auto preference = new ComboBoxPreference(VIVE_PUCKS_CONFIG, "Configuration", getter, setter);
QStringList list = (QStringList() << "Auto" << "Feet" << "FeetAndHips" << "FeetHipsAndChest");
preference->setItems(list);
preferences->addPreference(preference);
}
}
controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableInputs() const {
using namespace controller;
QVector<Input::NamedPair> availableInputs{
@ -404,6 +665,11 @@ controller::Input::NamedVector ViveControllerManager::InputDevice::getAvailableI
// 3d location of controller
makePair(LEFT_HAND, "LeftHand"),
makePair(RIGHT_HAND, "RightHand"),
makePair(LEFT_FOOT, "LeftFoot"),
makePair(RIGHT_FOOT, "RightFoot"),
makePair(HIPS, "Hips"),
makePair(SPINE2, "Spine2"),
makePair(HEAD, "Head"),
// 16 tracked poses
makePair(TRACKED_OBJECT_00, "TrackedObject00"),

View file

@ -14,9 +14,11 @@
#include <QObject>
#include <unordered_set>
#include <vector>
#include <map>
#include <utility>
#include <GLMHelpers.h>
#include <model/Geometry.h>
#include <gpu/Texture.h>
#include <controllers/InputDevice.h>
@ -48,23 +50,31 @@ public:
private:
class InputDevice : public controller::InputDevice {
public:
InputDevice(vr::IVRSystem*& system) : controller::InputDevice("Vive"), _system(system) {}
InputDevice(vr::IVRSystem*& system) : controller::InputDevice("Vive"), _system(system) { createPreferences(); }
private:
// Device functions
controller::Input::NamedVector getAvailableInputs() const override;
QString getDefaultMappingConfig() const override;
void update(float deltaTime, const controller::InputCalibrationData& inputCalibrationData) override;
void focusOutEvent() override;
void createPreferences();
bool triggerHapticPulse(float strength, float duration, controller::Hand hand) override;
void hapticsHelper(float deltaTime, bool leftHand);
void calibrateOrUncalibrate(const controller::InputCalibrationData& inputCalibration);
void calibrate(const controller::InputCalibrationData& inputCalibration);
void uncalibrate();
controller::Pose addOffsetToPuckPose(int joint) const;
void updateCalibratedLimbs();
bool checkForCalibrationEvent();
void handleHandController(float deltaTime, uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData, bool isLeftHand);
void handleHmd(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData);
void handleTrackedObject(uint32_t deviceIndex, const controller::InputCalibrationData& inputCalibrationData);
void handleButtonEvent(float deltaTime, uint32_t button, bool pressed, bool touched, bool isLeftHand);
void handleAxisEvent(float deltaTime, uint32_t axis, float x, float y, bool isLeftHand);
void handlePoseEvent(float deltaTime, const controller::InputCalibrationData& inputCalibrationData, const mat4& mat,
const vec3& linearVelocity, const vec3& angularVelocity, bool isLeftHand);
void handleHeadPoseEvent(const controller::InputCalibrationData& inputCalibrationData, const mat4& mat, const vec3& linearVelocity,
const vec3& angularVelocity);
void partitionTouchpad(int sButton, int xAxis, int yAxis, int centerPsuedoButton, int xPseudoButton, int yPseudoButton);
class FilteredStick {
@ -90,10 +100,15 @@ private:
float _timer { 0.0f };
glm::vec2 _stick { 0.0f, 0.0f };
};
enum class Config { Feet, FeetAndHips, FeetHipsAndChest, Auto };
Config _config { Config::Auto };
Config _preferedConfig { Config::Auto };
FilteredStick _filteredLeftStick;
FilteredStick _filteredRightStick;
std::vector<std::pair<uint32_t, controller::Pose>> _validTrackedObjects;
std::map<uint32_t, glm::mat4> _pucksOffset;
std::map<int, uint32_t> _jointToPuckMap;
// perform an action when the InputDevice mutex is acquired.
using Locker = std::unique_lock<std::recursive_mutex>;
template <typename F>
@ -101,12 +116,20 @@ private:
int _trackedControllers { 0 };
vr::IVRSystem*& _system;
quint64 _timeTilCalibration { 0.0f };
float _leftHapticStrength { 0.0f };
float _leftHapticDuration { 0.0f };
float _rightHapticStrength { 0.0f };
float _rightHapticDuration { 0.0f };
bool _triggersPressedHandled { false };
bool _calibrated { false };
bool _timeTilCalibrationSet { false };
mutable std::recursive_mutex _lock;
QString configToString();
void setConfigFromString(const QString& value);
void loadSettings();
void saveSettings() const;
friend class ViveControllerManager;
};

View file

@ -1376,7 +1376,9 @@ function MyController(hand) {
visible: true,
alpha: 1,
parentID: AVATAR_SELF_ID,
parentJointIndex: this.controllerJointIndex,
parentJointIndex: MyAvatar.getJointIndex(this.hand === RIGHT_HAND ?
"_CAMERA_RELATIVE_CONTROLLER_RIGHTHAND" :
"_CAMERA_RELATIVE_CONTROLLER_LEFTHAND"),
endParentID: farParentID
};
this.overlayLine = Overlays.addOverlay("line3d", lineProperties);

View file

@ -96,7 +96,7 @@ function calcSpawnInfo(hand, height) {
* @param hand [number] -1 indicates no hand, Controller.Standard.RightHand or Controller.Standard.LeftHand
* @param clientOnly [bool] true indicates tablet model is only visible to client.
*/
WebTablet = function (url, width, dpi, hand, clientOnly, location) {
WebTablet = function (url, width, dpi, hand, clientOnly, location, visible) {
var _this = this;
@ -107,6 +107,8 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location) {
this.depth = TABLET_NATURAL_DIMENSIONS.z * tabletScaleFactor;
this.landscape = false;
visible = visible === true;
if (dpi) {
this.dpi = dpi;
} else {
@ -125,7 +127,8 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location) {
"grabbableKey": {"grabbable": true}
}),
dimensions: this.getDimensions(),
parentID: AVATAR_SELF_ID
parentID: AVATAR_SELF_ID,
visible: visible
};
// compute position, rotation & parentJointIndex of the tablet
@ -158,7 +161,8 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location) {
parentID: this.tabletEntityID,
parentJointIndex: -1,
showKeyboardFocusHighlight: false,
isAA: HMD.active
isAA: HMD.active,
visible: visible
});
var HOME_BUTTON_Y_OFFSET = (this.height / 2) - (this.height / 20);
@ -168,7 +172,7 @@ WebTablet = function (url, width, dpi, hand, clientOnly, location) {
localRotation: {x: 0, y: 1, z: 0, w: 0},
dimensions: { x: 4 * tabletScaleFactor, y: 4 * tabletScaleFactor, z: 4 * tabletScaleFactor},
alpha: 0.0,
visible: true,
visible: visible,
drawInFront: false,
parentID: this.tabletEntityID,
parentJointIndex: -1

View file

@ -3,6 +3,7 @@
// examples
//
// Created by Brad hefta-Gaub on 10/1/14.
// Modified by Daniela Fontes @DanielaFifo and Tiago Andrade @TagoWill on 4/7/2017
// Copyright 2014 High Fidelity, Inc.
//
// This script implements a class useful for building tools for editing entities.
@ -2592,6 +2593,16 @@ SelectionDisplay = (function() {
// pivot - point to use as a pivot
// offset - the position of the overlay tool relative to the selections center position
var makeStretchTool = function(stretchMode, direction, pivot, offset, customOnMove) {
// directionFor3DStretch - direction and pivot for 3D stretch
// distanceFor3DStretch - distance from the intersection point and the handController
// used to increase the scale taking into account the distance to the object
// DISTANCE_INFLUENCE_THRESHOLD - constant that holds the minimum distance where the
// distance to the object will influence the stretch/resize/scale
var directionFor3DStretch = getDirectionsFor3DStretch(stretchMode);
var distanceFor3DStretch = 0;
var DISTANCE_INFLUENCE_THRESHOLD = 1.2;
var signs = {
x: direction.x < 0 ? -1 : (direction.x > 0 ? 1 : 0),
y: direction.y < 0 ? -1 : (direction.y > 0 ? 1 : 0),
@ -2603,18 +2614,23 @@ SelectionDisplay = (function() {
y: Math.abs(direction.y) > 0 ? 1 : 0,
z: Math.abs(direction.z) > 0 ? 1 : 0,
};
var numDimensions = mask.x + mask.y + mask.z;
var planeNormal = null;
var lastPick = null;
var lastPick3D = null;
var initialPosition = null;
var initialDimensions = null;
var initialIntersection = null;
var initialProperties = null;
var registrationPoint = null;
var deltaPivot = null;
var deltaPivot3D = null;
var pickRayPosition = null;
var pickRayPosition3D = null;
var rotation = null;
var onBegin = function(event) {
@ -2652,8 +2668,20 @@ SelectionDisplay = (function() {
// Scaled offset in world coordinates
var scaledOffsetWorld = vec3Mult(initialDimensions, offsetRP);
pickRayPosition = Vec3.sum(initialPosition, Vec3.multiplyQbyV(rotation, scaledOffsetWorld));
if (directionFor3DStretch) {
// pivot, offset and pickPlanePosition for 3D manipulation
var scaledPivot3D = Vec3.multiply(0.5, Vec3.multiply(1.0, directionFor3DStretch));
deltaPivot3D = Vec3.subtract(centeredRP, scaledPivot3D);
var scaledOffsetWorld3D = vec3Mult(initialDimensions,
Vec3.subtract(Vec3.multiply(0.5, Vec3.multiply(-1.0, directionFor3DStretch)),
centeredRP));
pickRayPosition3D = Vec3.sum(initialPosition, Vec3.multiplyQbyV(rotation, scaledOffsetWorld));
}
var start = null;
var end = null;
if (numDimensions == 1 && mask.x) {
@ -2754,12 +2782,25 @@ SelectionDisplay = (function() {
};
}
}
planeNormal = Vec3.multiplyQbyV(rotation, planeNormal);
var pickRay = generalComputePickRay(event.x, event.y);
lastPick = rayPlaneIntersection(pickRay,
pickRayPosition,
planeNormal);
var planeNormal3D = {
x: 0,
y: 0,
z: 0
};
if (directionFor3DStretch) {
lastPick3D = rayPlaneIntersection(pickRay,
pickRayPosition3D,
planeNormal3D);
distanceFor3DStretch = Vec3.length(Vec3.subtract(pickRayPosition3D, pickRay.origin));
}
SelectionManager.saveProperties();
};
@ -2790,24 +2831,50 @@ SelectionDisplay = (function() {
dimensions = SelectionManager.worldDimensions;
rotation = SelectionManager.worldRotation;
}
var localDeltaPivot = deltaPivot;
var localSigns = signs;
var pickRay = generalComputePickRay(event.x, event.y);
newPick = rayPlaneIntersection(pickRay,
// Are we using handControllers or Mouse - only relevant for 3D tools
var controllerPose = getControllerWorldLocation(activeHand, true);
if (HMD.isHMDAvailable()
&& HMD.isHandControllerAvailable() && controllerPose.valid && that.triggered && directionFor3DStretch) {
localDeltaPivot = deltaPivot3D;
newPick = pickRay.origin;
var vector = Vec3.subtract(newPick, lastPick3D);
vector = Vec3.multiplyQbyV(Quat.inverse(rotation), vector);
if (distanceFor3DStretch > DISTANCE_INFLUENCE_THRESHOLD) {
// Range of Motion
vector = Vec3.multiply(distanceFor3DStretch , vector);
}
localSigns = directionFor3DStretch;
} else {
newPick = rayPlaneIntersection(pickRay,
pickRayPosition,
planeNormal);
var vector = Vec3.subtract(newPick, lastPick);
var vector = Vec3.subtract(newPick, lastPick);
vector = Vec3.multiplyQbyV(Quat.inverse(rotation), vector);
vector = vec3Mult(mask, vector);
vector = Vec3.multiplyQbyV(Quat.inverse(rotation), vector);
vector = vec3Mult(mask, vector);
}
if (customOnMove) {
var change = Vec3.multiply(-1, vec3Mult(signs, vector));
var change = Vec3.multiply(-1, vec3Mult(localSigns, vector));
customOnMove(vector, change);
} else {
vector = grid.snapToSpacing(vector);
var changeInDimensions = Vec3.multiply(-1, vec3Mult(signs, vector));
var changeInDimensions = Vec3.multiply(-1, vec3Mult(localSigns, vector));
var newDimensions;
if (proportional) {
var absX = Math.abs(changeInDimensions.x);
@ -2829,37 +2896,39 @@ SelectionDisplay = (function() {
} else {
newDimensions = Vec3.sum(initialDimensions, changeInDimensions);
}
newDimensions.x = Math.max(newDimensions.x, MINIMUM_DIMENSION);
newDimensions.y = Math.max(newDimensions.y, MINIMUM_DIMENSION);
newDimensions.z = Math.max(newDimensions.z, MINIMUM_DIMENSION);
var changeInPosition = Vec3.multiplyQbyV(rotation, vec3Mult(deltaPivot, changeInDimensions));
var newPosition = Vec3.sum(initialPosition, changeInPosition);
for (var i = 0; i < SelectionManager.selections.length; i++) {
Entities.editEntity(SelectionManager.selections[i], {
position: newPosition,
dimensions: newDimensions,
});
}
var wantDebug = false;
if (wantDebug) {
print(stretchMode);
//Vec3.print(" newIntersection:", newIntersection);
Vec3.print(" vector:", vector);
//Vec3.print(" oldPOS:", oldPOS);
//Vec3.print(" newPOS:", newPOS);
Vec3.print(" changeInDimensions:", changeInDimensions);
Vec3.print(" newDimensions:", newDimensions);
Vec3.print(" changeInPosition:", changeInPosition);
Vec3.print(" newPosition:", newPosition);
}
SelectionManager._update();
}
newDimensions.x = Math.max(newDimensions.x, MINIMUM_DIMENSION);
newDimensions.y = Math.max(newDimensions.y, MINIMUM_DIMENSION);
newDimensions.z = Math.max(newDimensions.z, MINIMUM_DIMENSION);
var changeInPosition = Vec3.multiplyQbyV(rotation, vec3Mult(localDeltaPivot, changeInDimensions));
var newPosition = Vec3.sum(initialPosition, changeInPosition);
for (var i = 0; i < SelectionManager.selections.length; i++) {
Entities.editEntity(SelectionManager.selections[i], {
position: newPosition,
dimensions: newDimensions,
});
}
var wantDebug = false;
if (wantDebug) {
print(stretchMode);
//Vec3.print(" newIntersection:", newIntersection);
Vec3.print(" vector:", vector);
//Vec3.print(" oldPOS:", oldPOS);
//Vec3.print(" newPOS:", newPOS);
Vec3.print(" changeInDimensions:", changeInDimensions);
Vec3.print(" newDimensions:", newDimensions);
Vec3.print(" changeInPosition:", changeInPosition);
Vec3.print(" newPosition:", newPosition);
}
SelectionManager._update();
};
@ -2870,6 +2939,75 @@ SelectionDisplay = (function() {
onEnd: onEnd
};
};
// Direction for the stretch tool when using hand controller
var directionsFor3DGrab = {
LBN: {
x: 1,
y: 1,
z: 1
},
RBN: {
x: -1,
y: 1,
z: 1
},
LBF: {
x: 1,
y: 1,
z: -1
},
RBF: {
x: -1,
y: 1,
z: -1
},
LTN: {
x: 1,
y: -1,
z: 1
},
RTN: {
x: -1,
y: -1,
z: 1
},
LTF: {
x: 1,
y: -1,
z: -1
},
RTF: {
x: -1,
y: -1,
z: -1
}
};
// Returns a vector with directions for the stretch tool in 3D using hand controllers
function getDirectionsFor3DStretch(mode) {
if (mode === "STRETCH_LBN") {
return directionsFor3DGrab.LBN;
} else if (mode === "STRETCH_RBN") {
return directionsFor3DGrab.RBN;
} else if (mode === "STRETCH_LBF") {
return directionsFor3DGrab.LBF;
} else if (mode === "STRETCH_RBF") {
return directionsFor3DGrab.RBF;
} else if (mode === "STRETCH_LTN") {
return directionsFor3DGrab.LTN;
} else if (mode === "STRETCH_RTN") {
return directionsFor3DGrab.RTN;
} else if (mode === "STRETCH_LTF") {
return directionsFor3DGrab.LTF;
} else if (mode === "STRETCH_RTF") {
return directionsFor3DGrab.RTF;
} else {
return null;
}
}
function addStretchTool(overlay, mode, pivot, direction, offset, handleMove) {
if (!pivot) {

View file

@ -366,6 +366,8 @@
return nearestAvatar;
}
function messageSend(message) {
// we always append whether or not we are logged in...
message.isLoggedIn = Account.isLoggedIn();
Messages.sendMessage(MESSAGE_CHANNEL, JSON.stringify(message));
}
function handStringMessageSend(message) {
@ -463,7 +465,9 @@
endHandshakeAnimation();
// No-op if we were successful, but this way we ensure that failures and abandoned handshakes don't leave us
// in a weird state.
request({ uri: requestUrl, method: 'DELETE' }, debug);
if (Account.isLoggedIn()) {
request({ uri: requestUrl, method: 'DELETE' }, debug);
}
}
function updateTriggers(value, fromKeyboard, hand) {
@ -590,7 +594,7 @@
}
}
function makeConnection(id) {
function makeConnection(id, isLoggedIn) {
// send done to let the connection know you have made connection.
messageSend({
key: "done",
@ -606,7 +610,10 @@
// It would be "simpler" to skip this and just look at the response, but:
// 1. We don't want to bother the metaverse with request that we know will fail.
// 2. We don't want our code here to be dependent on precisely how the metaverse responds (400, 401, etc.)
if (!Account.isLoggedIn()) {
// 3. We also don't want to connect to someone who is anonymous _now_, but was not earlier and still has
// the same node id. Since the messaging doesn't say _who_ isn't logged in, fail the same as if we are
// not logged in.
if (!Account.isLoggedIn() || isLoggedIn === false) {
handleConnectionResponseAndMaybeRepeat("401:Unauthorized", {statusCode: 401});
return;
}
@ -628,8 +635,12 @@
// we change states, start the connectionInterval where we check
// to be sure the hand is still close enough. If not, we terminate
// the interval, go back to the waiting state. If we make it
// the entire CONNECTING_TIME, we make the connection.
function startConnecting(id, jointIndex) {
// the entire CONNECTING_TIME, we make the connection. We pass in
// whether or not the connecting id is actually logged in, as now we
// will allow to start the connection process but have it stop with a
// fail message before trying to call the backend if the other guy isn't
// logged in.
function startConnecting(id, jointIndex, isLoggedIn) {
var count = 0;
debug("connecting", id, "hand", jointIndex);
// do we need to do this?
@ -671,7 +682,7 @@
startHandshake();
} else if (count > CONNECTING_TIME / CONNECTING_INTERVAL) {
debug("made connection with " + id);
makeConnection(id);
makeConnection(id, isLoggedIn);
stopConnecting();
}
}, CONNECTING_INTERVAL);
@ -736,7 +747,7 @@
if (state === STATES.WAITING && (!connectingId || connectingId === senderID)) {
if (message.id === MyAvatar.sessionUUID) {
stopWaiting();
startConnecting(senderID, exisitingOrSearchedJointIndex());
startConnecting(senderID, exisitingOrSearchedJointIndex(), message.isLoggedIn);
} else if (connectingId) {
// this is for someone else (we lost race in connectionRequest),
// so lets start over
@ -755,12 +766,12 @@
startHandshake();
break;
}
startConnecting(senderID, connectingHandJointIndex);
startConnecting(senderID, connectingHandJointIndex, message.isLoggedIn);
}
break;
case "done":
delete waitingList[senderID];
if (connectionId !== senderID) {
if (connectingId !== senderID) {
break;
}
if (state === STATES.CONNECTING) {
@ -775,7 +786,7 @@
} else {
// they just created a connection request to us, and we are connecting to
// them, so lets just stop connecting and make connection..
makeConnection(connectingId);
makeConnection(connectingId, message.isLoggedIn);
stopConnecting();
}
} else {

View file

@ -268,7 +268,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
break;
case 'refreshConnections':
print('Refreshing Connections...');
getConnectionData();
getConnectionData(false);
UserActivityLogger.palAction("refresh_connections", "");
break;
case 'removeConnection':
@ -281,25 +281,27 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
print("Error: unable to remove connection", connectionUserName, error || response.status);
return;
}
getConnectionData();
getConnectionData(false);
});
break
case 'removeFriend':
friendUserName = message.params;
print("Removing " + friendUserName + " from friends.");
request({
uri: METAVERSE_BASE + '/api/v1/user/friends/' + friendUserName,
method: 'DELETE'
}, function (error, response) {
if (error || (response.status !== 'success')) {
print("Error: unable to unfriend", friendUserName, error || response.status);
print("Error: unable to unfriend " + friendUserName, error || response.status);
return;
}
getConnectionData();
getConnectionData(friendUserName);
});
break
case 'addFriend':
friendUserName = message.params;
print("Adding " + friendUserName + " to friends.");
request({
uri: METAVERSE_BASE + '/api/v1/user/friends',
method: 'POST',
@ -312,7 +314,7 @@ function fromQml(message) { // messages are {method, params}, like json-rpc. See
print("Error: unable to friend " + friendUserName, error || response.status);
return;
}
getConnectionData(); // For now, just refresh all connection data. Later, just refresh the one friended row.
getConnectionData(friendUserName);
}
);
break;
@ -360,8 +362,6 @@ function getProfilePicture(username, callback) { // callback(url) if successfull
});
}
function getAvailableConnections(domain, callback) { // callback([{usename, location}...]) if successfull. (Logs otherwise)
// The back end doesn't do user connections yet. Fake it by getting all users that have made themselves accessible to us,
// and pretending that they are all connections.
url = METAVERSE_BASE + '/api/v1/users?'
if (domain) {
url += 'status=' + domain.slice(1, -1); // without curly braces
@ -372,8 +372,19 @@ function getAvailableConnections(domain, callback) { // callback([{usename, loca
callback(connectionsData.users);
});
}
function getConnectionData(domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick.
function getInfoAboutUser(specificUsername, callback) {
url = METAVERSE_BASE + '/api/v1/users?filter=connections'
requestJSON(url, function (connectionsData) {
for (user in connectionsData.users) {
if (connectionsData.users[user].username === specificUsername) {
callback(connectionsData.users[user]);
return;
}
}
callback(false);
});
}
function getConnectionData(specificUsername, domain) { // Update all the usernames that I am entitled to see, using my login but not dependent on canKick.
function frob(user) { // get into the right format
var formattedSessionId = user.location.node_id || '';
if (formattedSessionId !== '' && formattedSessionId.indexOf("{") != 0) {
@ -387,15 +398,25 @@ function getConnectionData(domain) { // Update all the usernames that I am entit
placeName: (user.location.root || user.location.domain || {}).name || ''
};
}
getAvailableConnections(domain, function (users) {
if (domain) {
users.forEach(function (user) {
if (specificUsername) {
getInfoAboutUser(specificUsername, function (user) {
if (user) {
updateUser(frob(user));
});
} else {
sendToQml({ method: 'connections', params: users.map(frob) });
}
});
} else {
print('Error: Unable to find information about ' + specificUsername + ' in connectionsData!');
}
});
} else {
getAvailableConnections(domain, function (users) {
if (domain) {
users.forEach(function (user) {
updateUser(frob(user));
});
} else {
sendToQml({ method: 'connections', params: users.map(frob) });
}
});
}
}
//
@ -472,7 +493,7 @@ function populateNearbyUserList(selectData, oldAudioData) {
data.push(avatarPalDatum);
print('PAL data:', JSON.stringify(avatarPalDatum));
});
getConnectionData(location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain).
getConnectionData(false, location.domainId); // Even admins don't get relationship data in requestUsernameFromID (which is still needed for admin status, which comes from domain).
conserveResources = Object.keys(avatarsOfInterest).length > 20;
sendToQml({ method: 'nearbyUsers', params: data });
if (selectData) {

View file

@ -17,32 +17,22 @@
const INPUT = "Input";
const OUTPUT = "Output";
function parseMenuItem(item) {
const USE = "Use ";
const FOR_INPUT = " for " + INPUT;
const FOR_OUTPUT = " for " + OUTPUT;
if (item.slice(0, USE.length) == USE) {
if (item.slice(-FOR_INPUT.length) == FOR_INPUT) {
return { device: item.slice(USE.length, -FOR_INPUT.length), mode: INPUT };
} else if (item.slice(-FOR_OUTPUT.length) == FOR_OUTPUT) {
return { device: item.slice(USE.length, -FOR_OUTPUT.length), mode: OUTPUT };
}
}
}
const SELECT_AUDIO_SCRIPT_STARTUP_TIMEOUT = 300;
//
// VAR DEFINITIONS
//
var debugPrintStatements = true;
const INPUT_DEVICE_SETTING = "audio_input_device";
const OUTPUT_DEVICE_SETTING = "audio_output_device";
var audioDevicesList = [];
var audioDevicesList = []; // placeholder for menu items
var wasHmdActive = false; // assume it's not active to start
var switchedAudioInputToHMD = false;
var switchedAudioOutputToHMD = false;
var previousSelectedInputAudioDevice = "";
var previousSelectedOutputAudioDevice = "";
var skipMenuEvents = true;
var interfaceInputDevice = "";
var interfaceOutputDevice = "";
//
// BEGIN FUNCTION DEFINITIONS
@ -56,56 +46,37 @@ function debug() {
function setupAudioMenus() {
// menu events can be triggered asynchronously; skip them for 200ms to avoid recursion and false switches
skipMenuEvents = true;
Script.setTimeout(function() { skipMenuEvents = false; }, 200);
removeAudioMenus();
// Setup audio input devices
Menu.addSeparator("Audio", "Input Audio Device");
var inputDevices = AudioDevice.getInputDevices();
for (var i = 0; i < inputDevices.length; i++) {
var audioDeviceMenuString = "Use " + inputDevices[i] + " for Input";
var currentInputDevice = AudioDevice.getInputDevice()
for (var i = 0; i < AudioDevice.inputAudioDevices.length; i++) {
var audioDeviceMenuString = "Use " + AudioDevice.inputAudioDevices[i] + " for Input";
Menu.addMenuItem({
menuName: "Audio",
menuItemName: audioDeviceMenuString,
isCheckable: true,
isChecked: inputDevices[i] == AudioDevice.getInputDevice()
isChecked: AudioDevice.inputAudioDevices[i] == currentInputDevice
});
audioDevicesList.push(audioDeviceMenuString);
}
// Setup audio output devices
Menu.addSeparator("Audio", "Output Audio Device");
var outputDevices = AudioDevice.getOutputDevices();
for (var i = 0; i < outputDevices.length; i++) {
var audioDeviceMenuString = "Use " + outputDevices[i] + " for Output";
var currentOutputDevice = AudioDevice.getOutputDevice()
for (var i = 0; i < AudioDevice.outputAudioDevices.length; i++) {
var audioDeviceMenuString = "Use " + AudioDevice.outputAudioDevices[i] + " for Output";
Menu.addMenuItem({
menuName: "Audio",
menuItemName: audioDeviceMenuString,
isCheckable: true,
isChecked: outputDevices[i] == AudioDevice.getOutputDevice()
isChecked: AudioDevice.outputAudioDevices[i] == currentOutputDevice
});
audioDevicesList.push(audioDeviceMenuString);
}
}
function checkDeviceMismatch() {
var inputDeviceSetting = Settings.getValue(INPUT_DEVICE_SETTING);
var interfaceInputDevice = AudioDevice.getInputDevice();
if (interfaceInputDevice != inputDeviceSetting) {
debug("Input Setting & Device mismatch! Input SETTING: " + inputDeviceSetting + "Input DEVICE IN USE: " + interfaceInputDevice);
switchAudioDevice("Use " + inputDeviceSetting + " for Input");
}
var outputDeviceSetting = Settings.getValue(OUTPUT_DEVICE_SETTING);
var interfaceOutputDevice = AudioDevice.getOutputDevice();
if (interfaceOutputDevice != outputDeviceSetting) {
debug("Output Setting & Device mismatch! Output SETTING: " + outputDeviceSetting + "Output DEVICE IN USE: " + interfaceOutputDevice);
switchAudioDevice("Use " + outputDeviceSetting + " for Output");
}
}
function removeAudioMenus() {
Menu.removeSeparator("Audio", "Input Audio Device");
Menu.removeSeparator("Audio", "Output Audio Device");
@ -124,67 +95,28 @@ function removeAudioMenus() {
function onDevicechanged() {
debug("System audio devices changed. Removing and replacing Audio Menus...");
setupAudioMenus();
checkDeviceMismatch();
}
function onMenuEvent(audioDeviceMenuString) {
if (!skipMenuEvents) {
switchAudioDevice(audioDeviceMenuString);
if (Menu.isOptionChecked(audioDeviceMenuString) &&
(audioDeviceMenuString !== interfaceInputDevice &&
audioDeviceMenuString !== interfaceOutputDevice)) {
AudioDevice.setDeviceFromMenu(audioDeviceMenuString)
}
}
function switchAudioDevice(audioDeviceMenuString) {
// if the device is not plugged in, short-circuit
if (!~audioDevicesList.indexOf(audioDeviceMenuString)) {
return;
}
var selection = parseMenuItem(audioDeviceMenuString);
if (!selection) {
debug("Invalid Audio audioDeviceMenuString! Doesn't end with 'for Input' or 'for Output'");
return;
}
// menu events can be triggered asynchronously; skip them for 200ms to avoid recursion and false switches
skipMenuEvents = true;
Script.setTimeout(function() { skipMenuEvents = false; }, 200);
var selectedDevice = selection.device;
if (selection.mode == INPUT) {
var currentInputDevice = AudioDevice.getInputDevice();
if (selectedDevice != currentInputDevice) {
debug("Switching audio INPUT device from " + currentInputDevice + " to " + selectedDevice);
Menu.setIsOptionChecked("Use " + currentInputDevice + " for Input", false);
if (AudioDevice.setInputDevice(selectedDevice)) {
Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice);
Menu.setIsOptionChecked(audioDeviceMenuString, true);
} else {
debug("Error setting audio input device!")
Menu.setIsOptionChecked(audioDeviceMenuString, false);
}
function onCurrentDeviceChanged() {
debug("System audio device switched. ");
interfaceInputDevice = "Use " + AudioDevice.getInputDevice() + " for Input";
interfaceOutputDevice = "Use " + AudioDevice.getOutputDevice() + " for Output";
for (var index = 0; index < audioDevicesList.length; index++) {
if (audioDevicesList[index] === interfaceInputDevice ||
audioDevicesList[index] === interfaceOutputDevice) {
if (Menu.isOptionChecked(audioDevicesList[index]) === false)
Menu.setIsOptionChecked(audioDevicesList[index], true);
} else {
debug("Selected input device is the same as the current input device!")
Settings.setValue(INPUT_DEVICE_SETTING, selectedDevice);
Menu.setIsOptionChecked(audioDeviceMenuString, true);
AudioDevice.setInputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue)
}
} else if (selection.mode == OUTPUT) {
var currentOutputDevice = AudioDevice.getOutputDevice();
if (selectedDevice != currentOutputDevice) {
debug("Switching audio OUTPUT device from " + currentOutputDevice + " to " + selectedDevice);
Menu.setIsOptionChecked("Use " + currentOutputDevice + " for Output", false);
if (AudioDevice.setOutputDevice(selectedDevice)) {
Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice);
Menu.setIsOptionChecked(audioDeviceMenuString, true);
} else {
debug("Error setting audio output device!")
Menu.setIsOptionChecked(audioDeviceMenuString, false);
}
} else {
debug("Selected output device is the same as the current output device!")
Settings.setValue(OUTPUT_DEVICE_SETTING, selectedDevice);
Menu.setIsOptionChecked(audioDeviceMenuString, true);
AudioDevice.setOutputDevice(selectedDevice); // Still try to force-set the device (in case the user's trying to forcefully debug an issue)
if (Menu.isOptionChecked(audioDevicesList[index]) === true)
Menu.setIsOptionChecked(audioDevicesList[index], false);
}
}
}
@ -192,12 +124,12 @@ function switchAudioDevice(audioDeviceMenuString) {
function restoreAudio() {
if (switchedAudioInputToHMD) {
debug("Switching back from HMD preferred audio input to: " + previousSelectedInputAudioDevice);
switchAudioDevice("Use " + previousSelectedInputAudioDevice + " for Input");
AudioDevice.setInputDeviceAsync(previousSelectedInputAudioDevice)
switchedAudioInputToHMD = false;
}
if (switchedAudioOutputToHMD) {
debug("Switching back from HMD preferred audio output to: " + previousSelectedOutputAudioDevice);
switchAudioDevice("Use " + previousSelectedOutputAudioDevice + " for Output");
AudioDevice.setOutputDeviceAsync(previousSelectedOutputAudioDevice)
switchedAudioOutputToHMD = false;
}
}
@ -224,7 +156,7 @@ function checkHMDAudio() {
debug("previousSelectedInputAudioDevice: " + previousSelectedInputAudioDevice);
if (hmdPreferredAudioInput != previousSelectedInputAudioDevice) {
switchedAudioInputToHMD = true;
switchAudioDevice("Use " + hmdPreferredAudioInput + " for Input");
AudioDevice.setInputDeviceAsync(hmdPreferredAudioInput)
}
}
if (hmdPreferredAudioOutput !== "") {
@ -233,7 +165,7 @@ function checkHMDAudio() {
debug("previousSelectedOutputAudioDevice: " + previousSelectedOutputAudioDevice);
if (hmdPreferredAudioOutput != previousSelectedOutputAudioDevice) {
switchedAudioOutputToHMD = true;
switchAudioDevice("Use " + hmdPreferredAudioOutput + " for Output");
AudioDevice.setOutputDeviceAsync(hmdPreferredAudioOutput)
}
}
} else {
@ -255,14 +187,15 @@ function checkHMDAudio() {
Script.setTimeout(function () {
debug("Connecting deviceChanged(), displayModeChanged(), and switchAudioDevice()...");
AudioDevice.deviceChanged.connect(onDevicechanged);
AudioDevice.currentInputDeviceChanged.connect(onCurrentDeviceChanged);
AudioDevice.currentOutputDeviceChanged.connect(onCurrentDeviceChanged);
HMD.displayModeChanged.connect(checkHMDAudio);
Menu.menuItemEvent.connect(onMenuEvent);
debug("Setting up Audio I/O menu for the first time...");
setupAudioMenus();
checkDeviceMismatch();
debug("Checking HMD audio status...")
checkHMDAudio();
}, 3000);
}, SELECT_AUDIO_SCRIPT_STARTUP_TIMEOUT);
debug("Connecting scriptEnding()");
Script.scriptEnding.connect(function () {
@ -270,6 +203,8 @@ Script.scriptEnding.connect(function () {
removeAudioMenus();
Menu.menuItemEvent.disconnect(onMenuEvent);
HMD.displayModeChanged.disconnect(checkHMDAudio);
AudioDevice.currentInputDeviceChanged.disconnect(onCurrentDeviceChanged);
AudioDevice.currentOutputDeviceChanged.disconnect(onCurrentDeviceChanged);
AudioDevice.deviceChanged.disconnect(onDevicechanged);
});

View file

@ -111,7 +111,7 @@ function onMessage(message) {
case 'openSettings':
if ((HMD.active && Settings.getValue("hmdTabletBecomesToolbar", false))
|| (!HMD.active && Settings.getValue("desktopTabletBecomesToolbar", true))) {
Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "General Preferences");
Desktop.show("hifi/dialogs/GeneralPreferencesDialog.qml", "GeneralPreferencesDialog");
} else {
tablet.loadQMLOnTop("TabletGeneralPreferences.qml");
}

View file

@ -92,7 +92,7 @@
tabletScalePercentage = getTabletScalePercentageFromSettings();
UIWebTablet = new WebTablet("qml/hifi/tablet/TabletRoot.qml",
DEFAULT_WIDTH * (tabletScalePercentage / 100),
null, activeHand, true);
null, activeHand, true, null, false);
UIWebTablet.register();
HMD.tabletID = UIWebTablet.tabletEntityID;
HMD.homeButtonID = UIWebTablet.homeButtonID;

View file

@ -1,4 +1,5 @@
"use strict";
/*jslint nomen: true, plusplus: true, vars: true*/
/*global AvatarList, Entities, EntityViewer, Script, SoundCache, Audio, print, randFloat*/
//
@ -38,19 +39,27 @@ var DEFAULT_SOUND_DATA = {
playbackGapRange: 0 // in ms
};
//var AGENT_AVATAR_POSITION = { x: -1.5327, y: 0.672515, z: 5.91573 };
var AGENT_AVATAR_POSITION = { x: -2.83785, y: 1.45243, z: -13.6042 };
//var isACScript = this.EntityViewer !== undefined;
var isACScript = true;
Script.include("http://hifi-content.s3.amazonaws.com/ryan/development/utils_ryan.js");
if (isACScript) {
Agent.isAvatar = true; // This puts a robot at 0,0,0, but is currently necessary in order to use AvatarList.
Avatar.skeletonModelURL = "http://hifi-content.s3.amazonaws.com/ozan/dev/avatars/invisible_avatar/invisible_avatar.fst";
Avatar.position = AGENT_AVATAR_POSITION;
Agent.isListeningToAudioStream = true;
}
function ignore() {}
function debug() { // Display the arguments not just [Object object].
//print.apply(null, [].map.call(arguments, JSON.stringify));
}
function randFloat(low, high) {
return low + Math.random() * (high - low);
}
if (isACScript) {
EntityViewer.setCenterRadius(QUERY_RADIUS);
}
@ -93,6 +102,7 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n
return;
}
var properties, soundData; // Latest data, pulled from local octree.
// getEntityProperties locks the tree, which competes with the asynchronous processing of queryOctree results.
// Most entity updates are fast and only a very few do getEntityProperties.
function ensureSoundData() { // We only getEntityProperities when we need to.
@ -115,43 +125,54 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n
}
}
}
// Stumbling on big new pile of entities will do a lot of getEntityProperties. Once.
if (that.lastUserDataUpdate < userDataCutoff) { // NO DATA => SOUND DATA
ensureSoundData();
}
if (!that.url) { // NO DATA => NO DATA
return that.stop();
}
if (!that.sound) { // SOUND DATA => DOWNLOADING
that.sound = SoundCache.getSound(soundData.url); // SoundCache can manage duplicates better than we can.
}
if (!that.sound.downloaded) { // DOWNLOADING => DOWNLOADING
return;
}
if (that.playAfter > now) { // DOWNLOADING | WAITING => WAITING
return;
}
ensureSoundData(); // We'll try to play/setOptions and will need position, so we might as well get soundData, too.
if (soundData.url !== that.url) { // WAITING => NO DATA (update next time around)
return that.stop();
}
var options = {
position: properties.position,
loop: soundData.loop || DEFAULT_SOUND_DATA.loop,
volume: soundData.volume || DEFAULT_SOUND_DATA.volume
};
function repeat() {
return !options.loop && (soundData.playbackGap >= 0);
}
function randomizedNextPlay() { // time of next play or recheck, randomized to distribute the work
var range = soundData.playbackGapRange || DEFAULT_SOUND_DATA.playbackGapRange,
base = repeat() ? ((that.sound.duration * MSEC_PER_SEC) + (soundData.playbackGap || DEFAULT_SOUND_DATA.playbackGap)) : RECHECK_TIME;
return now + base + randFloat(-Math.min(base, range), range);
}
if (that.injector && soundData.playing === false) {
that.injector.stop();
that.injector = null;
}
if (!that.injector) {
if (soundData.playing === false) { // WAITING => PLAYING | WAITING
return;
@ -165,6 +186,7 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n
}
return;
}
that.injector.setOptions(options); // PLAYING => UPDATE POSITION ETC
if (!that.injector.playing) { // Subtle: a looping sound will not check playbackGap.
if (repeat()) { // WAITING => PLAYING
@ -178,6 +200,7 @@ function EntityDatum(entityIdentifier) { // Just the data of an entity that we n
}
};
}
function internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar) {
ignore(avatarPosition, avatar); // We could use avatars and/or avatarPositions to prioritize which ones to play.
var entitySound = entityCache[entityIdentifier];
@ -186,7 +209,9 @@ function internEntityDatum(entityIdentifier, timestamp, avatarPosition, avatar)
}
entitySound.timestamp = timestamp; // Might be updated for multiple avatars. That's fine.
}
var nUpdates = UPDATES_PER_STATS_LOG, lastStats = Date.now();
function updateAllEntityData() { // A fast update of all entities we know about. A few make sounds.
var now = Date.now(),
expirationCutoff = now - EXPIRATION_TIME,

View file

@ -0,0 +1,70 @@
//
// Created by Alan-Michael Moody on 5/2/2017
//
(function () {
var thisEntityID;
this.preload = function (entityID) {
thisEntityID = entityID;
};
var SCAN_RATE = 100; //ms
var REFERENCE_FRAME_COUNT = 30;
var MAX_AUDIO_THRESHOLD = 16000;
var framePool = [];
function scanEngine() {
var avatarLoudnessPool = [];
function average(a) {
var sum = 0;
var total = a.length;
for (var i = 0; i < total; i++) {
sum += a[i];
}
return Math.round(sum / total);
}
function audioClamp(input) {
if (input > MAX_AUDIO_THRESHOLD) return MAX_AUDIO_THRESHOLD;
return input;
}
var avatars = AvatarList.getAvatarIdentifiers();
avatars.forEach(function (id) {
var avatar = AvatarList.getAvatar(id);
avatarLoudnessPool.push(audioClamp(Math.round(avatar.audioLoudness)));
});
framePool.push(average(avatarLoudnessPool));
if (framePool.length >= REFERENCE_FRAME_COUNT) {
framePool.shift();
}
function normalizedAverage(a) {
a = a.map(function (v) {
return Math.round(( 100 / MAX_AUDIO_THRESHOLD ) * v);
});
return average(a);
}
var norm = normalizedAverage(framePool);
// we have a range of 55 to -53 degrees for the needle
var scaledDegrees = (norm / -.94) + 54.5; // shifting scale from 100 to 55 to -53 ish its more like -51 ;
Entities.setAbsoluteJointRotationInObjectFrame(thisEntityID, 0, Quat.fromPitchYawRollDegrees(0, 0, scaledDegrees));
}
Script.setInterval(function () {
scanEngine();
}, SCAN_RATE);
});

View file

@ -0,0 +1,79 @@
//
// Created by Alan-Michael Moody on 4/17/2017
//
(function () {
var barID;
this.preload = function (entityID) {
var children = Entities.getChildrenIDs(entityID);
var childZero = Entities.getEntityProperties(children[0]);
barID = childZero.id;
};
var SCAN_RATE = 100; //ms
var REFERENCE_FRAME_COUNT = 30;
var MAX_AUDIO_THRESHOLD = 16000;
var framePool = [];
function scanEngine() {
var avatarLoudnessPool = [];
function average(a) {
var sum = 0;
var total = a.length;
for (var i = 0; i < total; i++) {
sum += a[i];
}
return Math.round(sum / total);
}
function audioClamp(input) {
if (input > MAX_AUDIO_THRESHOLD) return MAX_AUDIO_THRESHOLD;
return input;
}
var avatars = AvatarList.getAvatarIdentifiers();
avatars.forEach(function (id) {
var avatar = AvatarList.getAvatar(id);
avatarLoudnessPool.push(audioClamp(Math.round(avatar.audioLoudness)));
});
framePool.push(average(avatarLoudnessPool));
if (framePool.length >= REFERENCE_FRAME_COUNT) {
framePool.shift();
}
function normalizedAverage(a) {
a = a.map(function (v) {
return Math.round(( 100 / MAX_AUDIO_THRESHOLD ) * v);
});
return average(a);
}
var norm = normalizedAverage(framePool);
var barProperties = Entities.getEntityProperties(barID);
var colorShift = 2.55 * norm; //shifting the scale to 0 - 255
var xShift = norm / 52; // changing scale from 0-100 to 0-1.9 ish
var normShift = xShift - 0.88; //shifting local displacement (-0.88)
var halfShift = xShift / 2;
Entities.editEntity(barID, {
dimensions: {x: xShift, y: barProperties.dimensions.y, z: barProperties.dimensions.z},
localPosition: {x: normShift - (halfShift), y: -0.0625, z: -0.015},
color: {red: colorShift, green: barProperties.color.green, blue: barProperties.color.blue}
});
}
Script.setInterval(function () {
scanEngine();
}, SCAN_RATE);
});

View file

@ -0,0 +1,92 @@
//
// Created by Alan-Michael Moody on 4/17/2017
//
(function () {
var barID, textID;
this.preload = function (entityID) {
var children = Entities.getChildrenIDs(entityID);
var childZero = Entities.getEntityProperties(children[0]);
var childOne = Entities.getEntityProperties(children[1]);
var childZeroUserData = JSON.parse(Entities.getEntityProperties(children[0]).userData);
if (childZeroUserData.name === "bar") {
barID = childZero.id;
textID = childOne.id;
} else {
barID = childOne.id;
textID = childZero.id;
}
};
var SCAN_RATE = 100; //ms
var REFERENCE_FRAME_COUNT = 30;
var MAX_AUDIO_THRESHOLD = 16000;
var framePool = [];
function scanEngine() {
var avatarLoudnessPool = [];
function average(a) {
var sum = 0;
var total = a.length;
for (var i = 0; i < total; i++) {
sum += a[i];
}
return Math.round(sum / total);
}
function audioClamp(input) {
if (input > MAX_AUDIO_THRESHOLD) return MAX_AUDIO_THRESHOLD;
return input;
}
var avatars = AvatarList.getAvatarIdentifiers();
avatars.forEach(function (id) {
var avatar = AvatarList.getAvatar(id);
avatarLoudnessPool.push(audioClamp(Math.round(avatar.audioLoudness)));
});
framePool.push(average(avatarLoudnessPool));
if (framePool.length >= REFERENCE_FRAME_COUNT) {
framePool.shift();
}
function normalizedAverage(a) {
a = a.map(function (v) {
return Math.round(( 100 / MAX_AUDIO_THRESHOLD ) * v);
});
return average(a);
}
var norm = normalizedAverage(framePool);
Entities.editEntity(textID, {text: "Loudness: % " + norm});
var barProperties = Entities.getEntityProperties(barID);
var colorShift = 2.55 * norm; //shifting the scale to 0 - 255
var xShift = norm / 100; // changing scale from 0-100 to 0-1
var normShift = xShift - .5; //shifting scale form 0-1 to -.5 to .5
var halfShift = xShift / 2 ;
Entities.editEntity(barID, {
dimensions: {x: xShift, y: barProperties.dimensions.y, z: barProperties.dimensions.z},
localPosition: {x: normShift - (halfShift), y: 0, z: 0.1},
color: {red: colorShift, green: barProperties.color.green, blue: barProperties.color.blue}
});
}
Script.setInterval(function () {
scanEngine();
}, SCAN_RATE);
});

View file

@ -0,0 +1,24 @@
//
// Created by Alan-Michael Moody on 5/2/2017
//
'use strict';
(function () {
var pos = Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation));
var meter = {
stand: {
type: 'Model',
modelURL: 'https://binaryrelay.com/files/public-docs/hifi/meter/applauseOmeter.fbx',
lifetime: '3600',
script: 'https://binaryrelay.com/files/public-docs/hifi/meter/applauseOmeter.js',
position: Vec3.sum(pos, {x: 0, y: 2.0, z: 0})
}
};
Entities.addEntity(meter.stand);
})();

View file

@ -0,0 +1,67 @@
//
// Created by Alan-Michael Moody on 4/17/2017
//
"use strict";
(function () { // BEGIN LOCAL_SCOPE
var pos = Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation));
var graph = {
background: {
type: "Box",
dimensions: {x: 1, y: 1, z: .1},
color: {
red: 128,
green: 128,
blue: 128
},
lifetime: "3600",
script: "https://binaryrelay.com/files/public-docs/hifi/meter/basic/meter.js",
position: pos
},
bar: {
type: "Box",
parentID: "",
userData: '{"name":"bar"}',
dimensions: {x: .05, y: .25, z: .1},
color: {
red: 0,
green: 0,
blue: 0
},
lifetime: "3600",
position: Vec3.sum(pos, {x: -0.495, y: 0, z: 0.1})
},
displayText: {
type: "Text",
parentID: "",
userData: '{"name":"displayText"}',
text: "Loudness: % ",
textColor: {
red: 0,
green: 0,
blue: 0
},
backgroundColor: {
red: 128,
green: 128,
blue: 128
},
visible: 0.5,
dimensions: {x: 0.70, y: 0.15, z: 0.1},
lifetime: "3600",
position: Vec3.sum(pos, {x: 0, y: 0.4, z: 0.06})
}
};
var background = Entities.addEntity(graph.background);
graph.bar.parentID = background;
graph.displayText.parentID = background;
var bar = Entities.addEntity(graph.bar);
var displayText = Entities.addEntity(graph.displayText);
})(); // END LOCAL_SCOPE

View file

@ -0,0 +1,43 @@
//
// Created by Alan-Michael Moody on 4/17/2017
//
"use strict";
(function () {
var pos = Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation));
var graph = {
background: {
type: "Model",
modelURL: "https://binaryrelay.com/files/public-docs/hifi/meter/plastic/meter-plastic.fbx",
color: {
red: 128,
green: 128,
blue: 128
},
lifetime: "3600",
script: "https://binaryrelay.com/files/public-docs/hifi/meter/plastic/meter.js",
position: pos
},
bar: {
type: "Box",
parentID: "",
userData: '{"name":"bar"}',
dimensions: {x: .05, y: .245, z: .07},
color: {
red: 0,
green: 0,
blue: 0
},
lifetime: "3600",
position: Vec3.sum(pos, {x: -0.90, y: 0, z: -0.15})
}
};
graph.bar.parentID = Entities.addEntity(graph.background);
Entities.addEntity(graph.bar);
})();

View file

@ -0,0 +1,67 @@
//
// Created by Alan-Michael Moody on 4/17/2017
//
"use strict";
(function () {
var pos = Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation));
var graph = {
background: {
type: "Model",
modelURL: "https://binaryrelay.com/files/public-docs/hifi/meter/text-entity/meter-text-entity.fbx",
color: {
red: 128,
green: 128,
blue: 128
},
lifetime: "3600",
script: "https://binaryrelay.com/files/public-docs/hifi/meter/text-entity/meter.js",
position: pos
},
bar: {
type: "Box",
parentID: "",
userData: '{"name":"bar"}',
dimensions: {x: .05, y: .245, z: .07},
color: {
red: 0,
green: 0,
blue: 0
},
lifetime: "3600",
position: Vec3.sum(pos, {x: -0.88, y: 0, z: -0.15})
},
displayText: {
type: "Text",
parentID: "",
userData: '{"name":"displayText"}',
text: "Make Some Noise:",
textColor: {
red: 0,
green: 0,
blue: 0
},
backgroundColor: {
red: 255,
green: 255,
blue: 255
},
dimensions: {x: .82, y: 0.115, z: 0.15},
lifetime: "3600",
lineHeight: .08,
position: Vec3.sum(pos, {x: -0.2, y: 0.175, z: -0.035})
}
};
var background = Entities.addEntity(graph.background);
graph.bar.parentID = background;
graph.displayText.parentID = background;
var bar = Entities.addEntity(graph.bar);
var displayText = Entities.addEntity(graph.displayText);
})();

View file

@ -0,0 +1,42 @@
//
// Created by Alan-Michael Moody on 4/17/2017
//
"use strict";
(function () {
var pos = Vec3.sum(MyAvatar.position, Quat.getFront(MyAvatar.orientation));
var graph = {
background: {
type: "Model",
modelURL: "https://binaryrelay.com/files/public-docs/hifi/meter/wood/meter-wood.fbx",
color: {
red: 128,
green: 128,
blue: 128
},
lifetime: "3600",
script: "https://binaryrelay.com/files/public-docs/hifi/meter/wood/meter.js",
position: pos
},
bar: {
type: "Box",
parentID: "",
userData: '{"name":"bar"}',
dimensions: {x: .05, y: .245, z: .07},
color: {
red: 0,
green: 0,
blue: 0
},
lifetime: "3600",
position: Vec3.sum(pos, {x: -0.88, y: 0, z: -0.15})
}
};
graph.bar.parentID = Entities.addEntity(graph.background);
Entities.addEntity(graph.bar);
})();

View file

@ -0,0 +1,89 @@
//
// Created by Alan-Michael Moody on 4/17/2017
//
(function () {
var barID, textID, originalText;
this.preload = function (entityID) {
var children = Entities.getChildrenIDs(entityID);
var childZero = Entities.getEntityProperties(children[0]);
var childOne = Entities.getEntityProperties(children[1]);
var childZeroUserData = JSON.parse(Entities.getEntityProperties(children[0]).userData);
if (childZeroUserData.name === "bar") {
barID = childZero.id;
textID = childOne.id;
originalText = childOne.text
} else {
barID = childOne.id;
textID = childZero.id;
originalText = childZero.text;
}
};
var SCAN_RATE = 100; //ms
var REFERENCE_FRAME_COUNT = 30;
var MAX_AUDIO_THRESHOLD = 16000;
var framePool = [];
function scanEngine() {
var avatarLoudnessPool = [];
function average(a) {
var sum = 0;
var total = a.length;
for (var i = 0; i < total; i++) {
sum += a[i];
}
return Math.round(sum / total);
}
function audioClamp(input) {
if (input > MAX_AUDIO_THRESHOLD) return MAX_AUDIO_THRESHOLD;
return input;
}
var avatars = AvatarList.getAvatarIdentifiers();
avatars.forEach(function (id) {
var avatar = AvatarList.getAvatar(id);
avatarLoudnessPool.push(audioClamp(Math.round(avatar.audioLoudness)));
});
framePool.push(average(avatarLoudnessPool));
if (framePool.length >= REFERENCE_FRAME_COUNT) {
framePool.shift();
}
function normalizedAverage(a) {
a = a.map(function (v) {
return Math.round(( 100 / MAX_AUDIO_THRESHOLD ) * v);
});
return average(a);
}
var norm = normalizedAverage(framePool);
Entities.editEntity(textID, {text: originalText + " % " + norm});
var barProperties = Entities.getEntityProperties(barID);
var colorShift = 2.55 * norm; //shifting the scale to 0 - 255
var xShift = norm / 52; // changing scale from 0-100 to 0-1.9 ish
var normShift = xShift - 0.88; //shifting local displacement (-0.88)
var halfShift = xShift / 2;
Entities.editEntity(barID, {
dimensions: {x: xShift, y: barProperties.dimensions.y, z: barProperties.dimensions.z},
localPosition: {x: normShift - ( halfShift ), y: -0.0625, z: -0.015},
color: {red: colorShift, green: barProperties.color.green, blue: barProperties.color.blue}
});
}
Script.setInterval(function () {
scanEngine();
}, SCAN_RATE);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB