expose Audio.devices.input/output

This commit is contained in:
Zach Pomerantz 2017-05-31 19:39:53 -04:00
parent 4a3f2e1d09
commit 954e4979f8
8 changed files with 322 additions and 79 deletions

View file

@ -13,6 +13,7 @@
#define hifi_scripting_Audio_h
#include "AudioScriptingInterface.h"
#include "AudioDevices.h"
namespace scripting {
@ -22,14 +23,21 @@ class Audio : public AudioScriptingInterface {
// TODO: Q_PROPERTY(bool mute)
// TODO: Q_PROPERTY(bool noiseReduction)
// TODO: Q_PROPERTY(bool reverb)
// TODO: Q_PROPERTY(float inputVolume)
// TODO: Q_PROPERTY(bool showMicLevel)
// TODO: Q_PROPERTY(? devices)
// TODO: Q_PROPERTY(QString context)
Q_PROPERTY(AudioDevices* devices READ getDevices)
public:
virtual ~Audio() {}
protected:
Audio() {}
private:
AudioDevices* getDevices() { return &_devices; }
AudioDevices _devices;
};
};

View file

@ -10,6 +10,7 @@
//
#include "AudioClient.h"
/*
#include "AudioDeviceScriptingInterface.h"
#include "SettingsScriptingInterface.h"
@ -269,3 +270,4 @@ void AudioDeviceScriptingInterface::currentDeviceUpdate(const QString& name, QAu
}
}
}
*/

View file

@ -12,6 +12,7 @@
#ifndef hifi_AudioDeviceScriptingInterface_h
#define hifi_AudioDeviceScriptingInterface_h
/*
#include <QObject>
#include <QString>
#include <QVector>
@ -103,5 +104,6 @@ private:
QString _currentInputDevice;
QString _currentOutputDevice;
};
*/
#endif // hifi_AudioDeviceScriptingInterface_h

View file

@ -0,0 +1,146 @@
//
// AudioDevices.cpp
// interface/src/scripting
//
// Created by Zach Pomerantz on 28/5/2017.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include "AudioDevices.h"
#include "AudioClient.h"
using namespace scripting;
QHash<int, QByteArray> AudioDeviceList::_roles {
{ Qt::DisplayRole, "display" },
{ Qt::CheckStateRole, "selected" }
};
Qt::ItemFlags AudioDeviceList::_flags { Qt::ItemIsSelectable | Qt::ItemIsEnabled };
AudioDeviceList::AudioDeviceList(QAudio::Mode mode) : _mode(mode) {
}
int AudioDeviceList::rowCount(const QModelIndex& parent) const {
Q_UNUSED(parent);
return _devices.size();
}
QVariant AudioDeviceList::data(const QModelIndex& index, int role) const {
if (!index.isValid() || index.row() >= _devices.size()) {
return QVariant();
}
if (role == Qt::DisplayRole) {
return _devices.at(index.row()).name;
} else if (role == Qt::CheckStateRole) {
return _devices.at(index.row()).selected;
} else {
return QVariant();
}
}
QHash<int, QByteArray> AudioDeviceList::roleNames() const {
return _roles;
}
Qt::ItemFlags AudioDeviceList::flags(const QModelIndex& index) const {
return _flags;
}
bool AudioDeviceList::setData(const QModelIndex& index, const QVariant &value, int role) {
if (!index.isValid() || index.row() >= _devices.size()) {
return false;
}
bool success = false;
if (role == Qt::CheckStateRole) {
auto selected = value.toBool();
auto& device = _devices[index.row()];
// only allow switching to a new device, not deactivating an in-use device
if (selected
// skip if already selected
&& selected != device.selected) {
auto client = DependencyManager::get<AudioClient>();
bool success;
QMetaObject::invokeMethod(client.data(), "switchAudioDevice", Qt::BlockingQueuedConnection,
Q_RETURN_ARG(bool, success),
Q_ARG(QAudio::Mode, _mode),
Q_ARG(const QAudioDeviceInfo&, device.info));
if (success) {
device.selected = true;
emit dataChanged(index, index, { Qt::CheckStateRole });
}
}
}
return success;
}
void AudioDeviceList::setDevice(const QAudioDeviceInfo& device) {
_selectedDevice = device;
for (auto i = 0; i < _devices.size(); ++i) {
AudioDevice& device = _devices[i];
if (device.selected && device.info != _selectedDevice) {
device.selected = false;
auto index = createIndex(i , 0);
emit dataChanged(index, index, { Qt::CheckStateRole });
} else if (!device.selected && device.info == _selectedDevice) {
device.selected = true;
auto index = createIndex(i , 0);
emit dataChanged(index, index, { Qt::CheckStateRole });
}
}
}
void AudioDeviceList::populate(const QList<QAudioDeviceInfo>& devices) {
beginResetModel();
_devices.clear();
foreach(const QAudioDeviceInfo& deviceInfo, devices) {
AudioDevice device;
device.info = deviceInfo;
device.name = device.info.deviceName();
device.selected = (device.info == _selectedDevice);
_devices.push_back(device);
}
endResetModel();
}
AudioDevices::AudioDevices() {
auto client = DependencyManager::get<AudioClient>();
// connections are made after client is initialized, so we must also fetch the devices
connect(client.data(), &AudioClient::deviceChanged, this, &AudioDevices::onDeviceChanged, Qt::QueuedConnection);
_inputs.setDevice(client->getActiveAudioDevice(QAudio::AudioInput));
_outputs.setDevice(client->getActiveAudioDevice(QAudio::AudioOutput));
connect(client.data(), &AudioClient::devicesChanged, this, &AudioDevices::onDevicesChanged, Qt::QueuedConnection);
_inputs.populate(client->getAudioDevices(QAudio::AudioInput));
_outputs.populate(client->getAudioDevices(QAudio::AudioOutput));
}
void AudioDevices::onDeviceChanged(QAudio::Mode mode, const QAudioDeviceInfo& device) {
if (mode == QAudio::AudioInput) {
_inputs.setDevice(device);
} else { // if (mode == QAudio::AudioOutput)
_outputs.setDevice(device);
}
}
void AudioDevices::onDevicesChanged(QAudio::Mode mode, const QList<QAudioDeviceInfo>& devices) {
if (mode == QAudio::AudioInput) {
_inputs.populate(devices);
} else { // if (mode == QAudio::AudioOutput)
_outputs.populate(devices);
}
}

View file

@ -0,0 +1,76 @@
//
// AudioDeviceScriptingInterface.h
// interface/src/scripting
//
// Created by Zach Pomerantz on 28/5/2017.
// Copyright 2017 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_scripting_AudioDevices_h
#define hifi_scripting_AudioDevices_h
#include <QObject>
#include <QAbstractListModel>
#include <QAudioDeviceInfo>
namespace scripting {
class AudioDevice {
public:
QAudioDeviceInfo info;
QString name;
bool selected { false };
};
class AudioDeviceList : public QAbstractListModel {
Q_OBJECT
public:
AudioDeviceList(QAudio::Mode mode);
int rowCount(const QModelIndex& parent) const override;
QVariant data(const QModelIndex& index, int role) const override;
QHash<int, QByteArray> roleNames() const override;
Qt::ItemFlags flags(const QModelIndex& index) const override;
bool setData(const QModelIndex& index, const QVariant &value, int role) override;
void setDevice(const QAudioDeviceInfo& device);
void populate(const QList<QAudioDeviceInfo>& devices);
private:
static QHash<int, QByteArray> _roles;
static Qt::ItemFlags _flags;
QAudio::Mode _mode;
QAudioDeviceInfo _selectedDevice;
QList<AudioDevice> _devices;
};
class AudioDevices : public QObject {
Q_OBJECT
Q_PROPERTY(AudioDeviceList* input READ getInputList)
Q_PROPERTY(AudioDeviceList* output READ getOutputList)
public:
AudioDevices();
private slots:
void onDeviceChanged(QAudio::Mode mode, const QAudioDeviceInfo& device);
void onDevicesChanged(QAudio::Mode mode, const QList<QAudioDeviceInfo>& devices);
private:
AudioDeviceList* getInputList() { return &_inputs; }
AudioDeviceList* getOutputList() { return &_outputs; }
AudioDeviceList _inputs { QAudio::AudioInput };
AudioDeviceList _outputs { QAudio::AudioOutput };
bool tester;
};
};
#endif // hifi_scripting_AudioDevices_h

View file

@ -76,6 +76,13 @@ using Mutex = std::mutex;
using Lock = std::unique_lock<Mutex>;
static Mutex _deviceMutex;
// thread-safe
QList<QAudioDeviceInfo> getAvailableDevices(QAudio::Mode mode) {
// NOTE: availableDevices() clobbers the Qt internal device list
Lock lock(_deviceMutex);
return QAudioDeviceInfo::availableDevices(mode);
}
class BackgroundThread : public QThread {
public:
BackgroundThread(AudioClient* client) : QThread((QObject*)client), _client(client) {}
@ -115,6 +122,45 @@ private:
std::condition_variable _joinCondition;
};
// now called from a background thread, to keep blocking operations off the audio thread
void AudioClient::checkDevices() {
auto inputDevices = getAvailableDevices(QAudio::AudioInput);
auto outputDevices = getAvailableDevices(QAudio::AudioOutput);
Lock lock(_deviceMutex);
if (inputDevices != _inputDevices) {
_inputDevices.swap(inputDevices);
emit devicesChanged(QAudio::AudioInput, _inputDevices);
}
if (outputDevices != _outputDevices) {
_outputDevices.swap(outputDevices);
emit devicesChanged(QAudio::AudioOutput, _outputDevices);
}
}
QAudioDeviceInfo AudioClient::getActiveAudioDevice(QAudio::Mode mode) const {
Lock lock(_deviceMutex);
if (mode == QAudio::AudioInput) {
return _inputDeviceInfo;
} else { // if (mode == QAudio::AudioOutput)
return _outputDeviceInfo;
}
}
QList<QAudioDeviceInfo> AudioClient::getAudioDevices(QAudio::Mode mode) const {
Lock lock(_deviceMutex);
if (mode == QAudio::AudioInput) {
return _inputDevices;
} else { // if (mode == QAudio::AudioOutput)
return _outputDevices;
}
}
// background thread buffering local injectors
class LocalInjectorsThread : public BackgroundThread {
Q_OBJECT
@ -220,8 +266,9 @@ AudioClient::AudioClient() :
connect(&_receivedAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &AudioClient::handleMismatchAudioFormat);
_inputDevices = getDeviceNames(QAudio::AudioInput);
_outputDevices = getDeviceNames(QAudio::AudioOutput);
// initialize wasapi; if getAvailableDevices is called from the CheckDevicesThread before this, it will crash
getAvailableDevices(QAudio::AudioInput);
getAvailableDevices(QAudio::AudioOutput);
// start a thread to detect any device changes
_checkDevicesThread = new CheckDevicesThread(this);
@ -295,13 +342,6 @@ void AudioClient::audioMixerKilled() {
emit disconnected();
}
// thread-safe
QList<QAudioDeviceInfo> getAvailableDevices(QAudio::Mode mode) {
// NOTE: availableDevices() clobbers the Qt internal device list
Lock lock(_deviceMutex);
return QAudioDeviceInfo::availableDevices(mode);
}
QAudioDeviceInfo getNamedAudioDeviceForMode(QAudio::Mode mode, const QString& deviceName) {
QAudioDeviceInfo result;
foreach(QAudioDeviceInfo audioDevice, getAvailableDevices(mode)) {
@ -315,7 +355,7 @@ QAudioDeviceInfo getNamedAudioDeviceForMode(QAudio::Mode mode, const QString& de
}
#ifdef Q_OS_WIN
QString friendlyNameForAudioDevice(IMMDevice* pEndpoint) {
QString getWinDeviceName(IMMDevice* pEndpoint) {
QString deviceName;
IPropertyStore* pPropertyStore;
pEndpoint->OpenPropertyStore(STGM_READ, &pPropertyStore);
@ -336,7 +376,7 @@ QString friendlyNameForAudioDevice(IMMDevice* pEndpoint) {
return deviceName;
}
QString AudioClient::friendlyNameForAudioDevice(wchar_t* guid) {
QString AudioClient::getWinDeviceName(wchar_t* guid) {
QString deviceName;
HRESULT hr = S_OK;
CoInitialize(nullptr);
@ -348,7 +388,7 @@ QString AudioClient::friendlyNameForAudioDevice(wchar_t* guid) {
printf("Audio Error: device not found\n");
deviceName = QString("NONE");
} else {
deviceName = ::friendlyNameForAudioDevice(pEndpoint);
deviceName = ::getWinDeviceName(pEndpoint);
}
pMMDeviceEnumerator->Release();
pMMDeviceEnumerator = nullptr;
@ -431,7 +471,7 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) {
printf("Audio Error: device not found\n");
deviceName = QString("NONE");
} else {
deviceName = friendlyNameForAudioDevice(pEndpoint);
deviceName = getWinDeviceName(pEndpoint);
}
pMMDeviceEnumerator->Release();
pMMDeviceEnumerator = NULL;
@ -791,29 +831,12 @@ void AudioClient::selectAudioFormat(const QString& selectedCodecName) {
}
QString AudioClient::getDefaultDeviceName(QAudio::Mode mode) {
QAudioDeviceInfo deviceInfo = defaultAudioDeviceForMode(mode);
return deviceInfo.deviceName();
}
QVector<QString> AudioClient::getDeviceNames(QAudio::Mode mode) {
QVector<QString> deviceNames;
const QList<QAudioDeviceInfo> &availableDevice = getAvailableDevices(mode);
foreach(const QAudioDeviceInfo &audioDevice, availableDevice) {
deviceNames << audioDevice.deviceName().trimmed();
bool AudioClient::switchAudioDevice(QAudio::Mode mode, const QAudioDeviceInfo& deviceInfo) {
if (mode == QAudio::AudioInput) {
return switchInputToAudioDevice(deviceInfo);
} else { // if (mode == QAudio::AudioOutput)
return switchOutputToAudioDevice(deviceInfo);
}
return deviceNames;
}
bool AudioClient::switchInputToAudioDevice(const QString& inputDeviceName) {
qCDebug(audioclient) << "[" << inputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName).deviceName() << "]";
return switchInputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName));
}
bool AudioClient::switchOutputToAudioDevice(const QString& outputDeviceName) {
qCDebug(audioclient) << "[" << outputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName).deviceName() << "]";
return switchOutputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName));
}
void AudioClient::configureReverb() {
@ -1357,7 +1380,7 @@ void AudioClient::setIsStereoInput(bool isStereoInput) {
}
// change in channel count for desired input format, restart the input device
switchInputToAudioDevice(_inputAudioDeviceName);
switchInputToAudioDevice(_inputDeviceInfo);
}
}
@ -1397,6 +1420,9 @@ void AudioClient::outputFormatChanged() {
bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceInfo) {
bool supportedFormat = false;
// NOTE: device start() uses the Qt internal device list
Lock lock(_deviceMutex);
// cleanup any previously initialized device
if (_audioInput) {
// The call to stop() causes _inputDevice to be destructed.
@ -1409,7 +1435,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn
_audioInput = NULL;
_numInputCallbackBytes = 0;
_inputAudioDeviceName = "";
_inputDeviceInfo = QAudioDeviceInfo();
}
if (_inputToNetworkResampler) {
@ -1424,8 +1450,8 @@ 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);
_inputDeviceInfo = inputDeviceInfo;
emit deviceChanged(QAudio::AudioInput, inputDeviceInfo);
if (adjustedFormatForAudioDevice(inputDeviceInfo, _desiredInputFormat, _inputFormat)) {
qCDebug(audioclient) << "The format to be used for audio input is" << _inputFormat;
@ -1460,10 +1486,7 @@ bool AudioClient::switchInputToAudioDevice(const QAudioDeviceInfo& inputDeviceIn
int numFrameSamples = calculateNumberOfFrameSamples(_numInputCallbackBytes);
_inputRingBuffer.resizeForFrameSize(numFrameSamples);
// NOTE: device start() uses the Qt internal device list
Lock lock(_deviceMutex);
_inputDevice = _audioInput->start();
lock.unlock();
if (_inputDevice) {
connect(_inputDevice, SIGNAL(readyRead()), this, SLOT(handleMicAudioInput()));
@ -1511,6 +1534,9 @@ void AudioClient::outputNotify() {
bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) {
bool supportedFormat = false;
// NOTE: device start() uses the Qt internal device list
Lock lock(_deviceMutex);
Lock localAudioLock(_localAudioMutex);
_localSamplesAvailable.exchange(0, std::memory_order_release);
@ -1536,6 +1562,8 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
delete[] _localOutputMixBuffer;
_localOutputMixBuffer = NULL;
_outputDeviceInfo = QAudioDeviceInfo();
}
if (_networkToOutputResampler) {
@ -1549,8 +1577,8 @@ 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);
_outputDeviceInfo = outputDeviceInfo;
emit deviceChanged(QAudio::AudioOutput, outputDeviceInfo);
if (adjustedFormatForAudioDevice(outputDeviceInfo, _desiredOutputFormat, _outputFormat)) {
qCDebug(audioclient) << "The format to be used for audio output is" << _outputFormat;
@ -1625,10 +1653,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice
_audioOutputIODevice.start();
// NOTE: device start() uses the Qt internal device list
Lock lock(_deviceMutex);
_audioOutput->start(&_audioOutputIODevice);
lock.unlock();
// setup a loopback audio output device
_loopbackAudioOutput = new QAudioOutput(outputDeviceInfo, _outputFormat, this);
@ -1821,19 +1846,6 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) {
return bytesWritten;
}
// now called from a background thread, to keep blocking operations off the audio thread
void AudioClient::checkDevices() {
QVector<QString> inputDevices = getDeviceNames(QAudio::AudioInput);
QVector<QString> outputDevices = getDeviceNames(QAudio::AudioOutput);
if (inputDevices != _inputDevices || outputDevices != _outputDevices) {
_inputDevices = inputDevices;
_outputDevices = outputDevices;
emit deviceChanged();
}
}
void AudioClient::loadSettings() {
_receivedAudioStream.setDynamicJitterBufferEnabled(dynamicJitterBufferEnabled.get());
_receivedAudioStream.setStaticJitterBufferFrames(staticJitterBufferFrames.get());

View file

@ -146,10 +146,13 @@ public:
bool outputLocalInjector(AudioInjector* injector) override;
QAudioDeviceInfo getActiveAudioDevice(QAudio::Mode mode) const;
QList<QAudioDeviceInfo> getAudioDevices(QAudio::Mode mode) const;
static const float CALLBACK_ACCELERATOR_RATIO;
#ifdef Q_OS_WIN
static QString friendlyNameForAudioDevice(wchar_t* guid);
static QString getWinDeviceName(wchar_t* guid);
#endif
public slots:
@ -185,12 +188,7 @@ public slots:
bool shouldLoopbackInjectors() override { return _shouldEchoToServer; }
bool switchInputToAudioDevice(const QString& inputDeviceName);
bool switchOutputToAudioDevice(const QString& outputDeviceName);
QString getDeviceName(QAudio::Mode mode) const { return (mode == QAudio::AudioInput) ?
_inputAudioDeviceName : _outputAudioDeviceName; }
QString getDefaultDeviceName(QAudio::Mode mode);
QVector<QString> getDeviceNames(QAudio::Mode mode);
bool switchAudioDevice(QAudio::Mode mode, const QAudioDeviceInfo& deviceInfo);
float getInputVolume() const { return (_audioInput) ? (float)_audioInput->volume() : 0.0f; }
void setInputVolume(float volume) { if (_audioInput) _audioInput->setVolume(volume); }
@ -212,7 +210,9 @@ signals:
void noiseGateClosed();
void changeDevice(const QAudioDeviceInfo& outputDeviceInfo);
void deviceChanged();
void deviceChanged(QAudio::Mode mode, const QAudioDeviceInfo& device);
void devicesChanged(QAudio::Mode mode, const QList<QAudioDeviceInfo>& devices);
void receivedFirstPacket();
void disconnected();
@ -221,9 +221,6 @@ signals:
void muteEnvironmentRequested(glm::vec3 position, float radius);
void currentOutputDeviceChanged(const QString& name);
void currentInputDeviceChanged(const QString& name);
protected:
AudioClient();
~AudioClient();
@ -290,9 +287,6 @@ private:
MixedProcessedAudioStream _receivedAudioStream;
bool _isStereoInput;
QString _inputAudioDeviceName;
QString _outputAudioDeviceName;
quint64 _outputStarveDetectionStartTimeMsec;
int _outputStarveDetectionCount;
@ -368,8 +362,11 @@ private:
glm::vec3 avatarBoundingBoxCorner;
glm::vec3 avatarBoundingBoxScale;
QVector<QString> _inputDevices;
QVector<QString> _outputDevices;
QAudioDeviceInfo _inputDeviceInfo;
QAudioDeviceInfo _outputDeviceInfo;
QList<QAudioDeviceInfo> _inputDevices;
QList<QAudioDeviceInfo> _outputDevices;
bool _hasReceivedFirstPacket { false };

View file

@ -244,7 +244,7 @@ QString OculusDisplayPlugin::getPreferredAudioInDevice() const {
if (!OVR_SUCCESS(ovr_GetAudioDeviceInGuidStr(buffer))) {
return QString();
}
return AudioClient::friendlyNameForAudioDevice(buffer);
return AudioClient::getWinDeviceName(buffer);
}
QString OculusDisplayPlugin::getPreferredAudioOutDevice() const {
@ -252,7 +252,7 @@ QString OculusDisplayPlugin::getPreferredAudioOutDevice() const {
if (!OVR_SUCCESS(ovr_GetAudioDeviceOutGuidStr(buffer))) {
return QString();
}
return AudioClient::friendlyNameForAudioDevice(buffer);
return AudioClient::getWinDeviceName(buffer);
}
OculusDisplayPlugin::~OculusDisplayPlugin() {