Merge pull request #6409 from birarda/injector-hybrid

more efficient threading of networked AudioInjector objects
This commit is contained in:
Clément Brisset 2015-11-30 15:36:41 -08:00
commit e168966b87
9 changed files with 571 additions and 208 deletions

View file

@ -17,6 +17,7 @@
#include <QtNetwork/QNetworkReply>
#include <AvatarHashMap.h>
#include <AudioInjectorManager.h>
#include <AssetClient.h>
#include <MessagesClient.h>
#include <NetworkAccessManager.h>
@ -62,6 +63,7 @@ Agent::Agent(NLPacket& packet) :
DependencyManager::set<ResourceCacheSharedItems>();
DependencyManager::set<SoundCache>();
DependencyManager::set<AudioInjectorManager>();
DependencyManager::set<recording::Deck>();
DependencyManager::set<recording::Recorder>();
DependencyManager::set<RecordingScriptingInterface>();
@ -418,7 +420,7 @@ void Agent::processAgentAvatarAndAudio(float deltaTime) {
glm::quat headOrientation = scriptedAvatar->getHeadOrientation();
audioPacket->writePrimitive(headOrientation);
}else if (nextSoundOutput) {
} else if (nextSoundOutput) {
// assume scripted avatar audio is mono and set channel flag to zero
audioPacket->writePrimitive((quint8)0);
@ -451,6 +453,7 @@ void Agent::processAgentAvatarAndAudio(float deltaTime) {
void Agent::aboutToFinish() {
setIsAvatar(false);// will stop timers for sending billboards and identity packets
if (_scriptEngine) {
_scriptEngine->stop();
}
@ -463,4 +466,7 @@ void Agent::aboutToFinish() {
DependencyManager::destroy<AssetClient>();
assetThread->quit();
assetThread->wait();
// cleanup the AudioInjectorManager (and any still running injectors)
DependencyManager::destroy<AudioInjectorManager>();
}

View file

@ -0,0 +1,42 @@
//
// injectorTests.js
// examples
//
// Created by Stephen Birarda on 11/16/15.
// Copyright 2014 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
//
var soundURL = "http://hifi-public.s3.amazonaws.com/birarda/medium-crowd.wav";
var audioOptions = {
position: { x: 0.0, y: 0.0, z: 0.0 },
volume: 0.5
};
var sound = SoundCache.getSound(soundURL);
var injector = null;
var restarting = false;
Script.update.connect(function(){
if (sound.downloaded) {
if (!injector) {
injector = Audio.playSound(sound, audioOptions);
} else if (!injector.isPlaying && !restarting) {
restarting = true;
Script.setTimeout(function(){
print("Calling restart for a stopped injector from script.");
injector.restart();
}, 1000);
} else if (injector.isPlaying) {
restarting = false;
if (Math.random() < 0.0001) {
print("Calling restart for a running injector from script.");
injector.restart();
}
}
}
})

View file

@ -50,6 +50,7 @@
#include <AssetClient.h>
#include <AssetUpload.h>
#include <AutoUpdater.h>
#include <AudioInjectorManager.h>
#include <CursorManager.h>
#include <DeferredLightingEffect.h>
#include <display-plugins/DisplayPlugin.h>
@ -340,6 +341,7 @@ bool setupEssentials(int& argc, char** argv) {
DependencyManager::set<PathUtils>();
DependencyManager::set<InterfaceActionFactory>();
DependencyManager::set<AssetClient>();
DependencyManager::set<AudioInjectorManager>();
DependencyManager::set<MessagesClient>();
DependencyManager::set<UserInputMapper>();
DependencyManager::set<controller::ScriptingInterface, ControllerScriptingInterface>();
@ -894,6 +896,10 @@ void Application::cleanupBeforeQuit() {
// destroy the AudioClient so it and its thread have a chance to go down safely
DependencyManager::destroy<AudioClient>();
// destroy the AudioInjectorManager so it and its thread have a chance to go down safely
// this will also stop any ongoing network injectors
DependencyManager::destroy<AudioInjectorManager>();
// Destroy third party processes after scripts have finished using them.
#ifdef HAVE_DDE

View file

@ -18,6 +18,7 @@
#include <UUID.h>
#include "AbstractAudioInterface.h"
#include "AudioInjectorManager.h"
#include "AudioRingBuffer.h"
#include "AudioLogging.h"
#include "SoundCache.h"
@ -35,6 +36,7 @@ AudioInjector::AudioInjector(Sound* sound, const AudioInjectorOptions& injectorO
_audioData(sound->getByteArray()),
_options(injectorOptions)
{
}
AudioInjector::AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions) :
@ -44,32 +46,28 @@ AudioInjector::AudioInjector(const QByteArray& audioData, const AudioInjectorOpt
}
void AudioInjector::setIsFinished(bool isFinished) {
_isFinished = isFinished;
// In all paths, regardless of isFinished argument. restart() passes false to prepare for new play, and injectToMixer() needs _shouldStop reset.
_shouldStop = false;
if (_isFinished) {
emit finished();
if (_localBuffer) {
_localBuffer->stop();
_localBuffer->deleteLater();
_localBuffer = NULL;
}
_isStarted = false;
if (_shouldDeleteAfterFinish) {
// we've been asked to delete after finishing, trigger a queued deleteLater here
QMetaObject::invokeMethod(this, "deleteLater", Qt::QueuedConnection);
}
void AudioInjector::finish() {
bool shouldDelete = (_state == State::NotFinishedWithPendingDelete);
_state = State::Finished;
emit finished();
if (_localBuffer) {
_localBuffer->stop();
_localBuffer->deleteLater();
_localBuffer = NULL;
}
if (shouldDelete) {
// we've been asked to delete after finishing, trigger a deleteLater here
deleteLater();
}
}
void AudioInjector::injectAudio() {
if (!_isStarted) {
_isStarted = true;
void AudioInjector::setupInjection() {
if (!_hasSetup) {
_hasSetup = true;
// check if we need to offset the sound by some number of seconds
if (_options.secondOffset > 0.0f) {
@ -81,33 +79,50 @@ void AudioInjector::injectAudio() {
} else {
_currentSendOffset = 0;
}
if (_options.localOnly) {
injectLocally();
} else {
injectToMixer();
}
} else {
qCDebug(audio) << "AudioInjector::injectAudio called but already started.";
qCDebug(audio) << "AudioInjector::setupInjection called but already setup.";
}
}
void AudioInjector::restart() {
_isPlaying = true;
connect(this, &AudioInjector::finished, this, &AudioInjector::restartPortionAfterFinished);
if (!_isStarted || _isFinished) {
emit finished();
} else {
stop();
// grab the AudioInjectorManager
auto injectorManager = DependencyManager::get<AudioInjectorManager>();
if (thread() != QThread::currentThread()) {
QMetaObject::invokeMethod(this, "restart");
if (!_options.localOnly) {
// notify the AudioInjectorManager to wake up in case it's waiting for new injectors
injectorManager->notifyInjectorReadyCondition();
}
return;
}
// reset the current send offset to zero
_currentSendOffset = 0;
// check our state to decide if we need extra handling for the restart request
if (_state == State::Finished) {
// we finished playing, need to reset state so we can get going again
_hasSetup = false;
_shouldStop = false;
_state = State::NotFinished;
// call inject audio to start injection over again
setupInjection();
// if we're a local injector call inject locally to start injecting again
if (_options.localOnly) {
injectLocally();
} else {
// wake the AudioInjectorManager back up if it's stuck waiting
injectorManager->restartFinishedInjector(this);
}
}
}
void AudioInjector::restartPortionAfterFinished() {
disconnect(this, &AudioInjector::finished, this, &AudioInjector::restartPortionAfterFinished);
setIsFinished(false);
QMetaObject::invokeMethod(this, "injectAudio", Qt::QueuedConnection);
}
void AudioInjector::injectLocally() {
bool AudioInjector::injectLocally() {
bool success = false;
if (_localAudioInterface) {
if (_audioData.size() > 0) {
@ -138,153 +153,174 @@ void AudioInjector::injectLocally() {
// we never started so we are finished, call our stop method
stop();
}
return success;
}
const uchar MAX_INJECTOR_VOLUME = 0xFF;
static const uint64_t NEXT_FRAME_DELTA_ERROR_OR_FINISHED = 0;
static const uint64_t NEXT_FRAME_DELTA_IMMEDIATELY = 1;
void AudioInjector::injectToMixer() {
if (_currentSendOffset < 0 ||
_currentSendOffset >= _audioData.size()) {
_currentSendOffset = 0;
uint64_t AudioInjector::injectNextFrame() {
if (_state == AudioInjector::State::Finished) {
qDebug() << "AudioInjector::injectNextFrame called but AudioInjector has finished and was not restarted. Returning.";
return NEXT_FRAME_DELTA_ERROR_OR_FINISHED;
}
auto nodeList = DependencyManager::get<NodeList>();
// make sure we actually have samples downloaded to inject
if (_audioData.size()) {
auto audioPacket = NLPacket::create(PacketType::InjectAudio);
// setup the packet for injected audio
QDataStream audioPacketStream(audioPacket.get());
// pack some placeholder sequence number for now
audioPacketStream << (quint16) 0;
// pack stream identifier (a generated UUID)
audioPacketStream << QUuid::createUuid();
// pack the stereo/mono type of the stream
audioPacketStream << _options.stereo;
// pack the flag for loopback
uchar loopbackFlag = (uchar) true;
audioPacketStream << loopbackFlag;
// pack the position for injected audio
int positionOptionOffset = audioPacket->pos();
audioPacketStream.writeRawData(reinterpret_cast<const char*>(&_options.position),
sizeof(_options.position));
// pack our orientation for injected audio
audioPacketStream.writeRawData(reinterpret_cast<const char*>(&_options.orientation),
sizeof(_options.orientation));
// pack zero for radius
float radius = 0;
audioPacketStream << radius;
// pack 255 for attenuation byte
int volumeOptionOffset = audioPacket->pos();
quint8 volume = MAX_INJECTOR_VOLUME * _options.volume;
audioPacketStream << volume;
audioPacketStream << _options.ignorePenumbra;
int audioDataOffset = audioPacket->pos();
QElapsedTimer timer;
timer.start();
int nextFrame = 0;
bool shouldLoop = _options.loop;
// loop to send off our audio in NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL byte chunks
quint16 outgoingInjectedAudioSequenceNumber = 0;
while (_currentSendOffset < _audioData.size() && !_shouldStop) {
int bytesToCopy = std::min((_options.stereo ? 2 : 1) * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL,
_audioData.size() - _currentSendOffset);
// Measure the loudness of this frame
_loudness = 0.0f;
for (int i = 0; i < bytesToCopy; i += sizeof(int16_t)) {
_loudness += abs(*reinterpret_cast<int16_t*>(_audioData.data() + _currentSendOffset + i)) /
(AudioConstants::MAX_SAMPLE_VALUE / 2.0f);
}
_loudness /= (float)(bytesToCopy / sizeof(int16_t));
// if we haven't setup the packet to send then do so now
static int positionOptionOffset = -1;
static int volumeOptionOffset = -1;
static int audioDataOffset = -1;
if (!_currentPacket) {
if (_currentSendOffset < 0 ||
_currentSendOffset >= _audioData.size()) {
_currentSendOffset = 0;
}
// make sure we actually have samples downloaded to inject
if (_audioData.size()) {
audioPacket->seek(0);
_outgoingSequenceNumber = 0;
_nextFrame = 0;
// pack the sequence number
audioPacket->writePrimitive(outgoingInjectedAudioSequenceNumber);
if (!_frameTimer) {
_frameTimer = std::unique_ptr<QElapsedTimer>(new QElapsedTimer);
}
audioPacket->seek(positionOptionOffset);
audioPacket->writePrimitive(_options.position);
audioPacket->writePrimitive(_options.orientation);
volume = MAX_INJECTOR_VOLUME * _options.volume;
audioPacket->seek(volumeOptionOffset);
audioPacket->writePrimitive(volume);
audioPacket->seek(audioDataOffset);
// copy the next NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL bytes to the packet
audioPacket->write(_audioData.data() + _currentSendOffset, bytesToCopy);
// set the correct size used for this packet
audioPacket->setPayloadSize(audioPacket->pos());
// grab our audio mixer from the NodeList, if it exists
SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer);
_frameTimer->restart();
if (audioMixer) {
// send off this audio packet
nodeList->sendUnreliablePacket(*audioPacket, *audioMixer);
outgoingInjectedAudioSequenceNumber++;
}
_currentSendOffset += bytesToCopy;
// send two packets before the first sleep so the mixer can start playback right away
if (_currentSendOffset != bytesToCopy && _currentSendOffset < _audioData.size()) {
// process events in case we have been told to stop and be deleted
QCoreApplication::processEvents();
if (_shouldStop) {
break;
}
// not the first packet and not done
// sleep for the appropriate time
int usecToSleep = (++nextFrame * (_options.stereo ? 2 : 1) * AudioConstants::NETWORK_FRAME_USECS) - timer.nsecsElapsed() / 1000;
if (usecToSleep > 0) {
usleep(usecToSleep);
}
}
if (shouldLoop && _currentSendOffset >= _audioData.size()) {
_currentSendOffset = 0;
}
_currentPacket = NLPacket::create(PacketType::InjectAudio);
// setup the packet for injected audio
QDataStream audioPacketStream(_currentPacket.get());
// pack some placeholder sequence number for now
audioPacketStream << (quint16) 0;
// pack stream identifier (a generated UUID)
audioPacketStream << QUuid::createUuid();
// pack the stereo/mono type of the stream
audioPacketStream << _options.stereo;
// pack the flag for loopback
uchar loopbackFlag = (uchar) true;
audioPacketStream << loopbackFlag;
// pack the position for injected audio
positionOptionOffset = _currentPacket->pos();
audioPacketStream.writeRawData(reinterpret_cast<const char*>(&_options.position),
sizeof(_options.position));
// pack our orientation for injected audio
audioPacketStream.writeRawData(reinterpret_cast<const char*>(&_options.orientation),
sizeof(_options.orientation));
// pack zero for radius
float radius = 0;
audioPacketStream << radius;
// pack 255 for attenuation byte
volumeOptionOffset = _currentPacket->pos();
quint8 volume = MAX_INJECTOR_VOLUME;
audioPacketStream << volume;
audioPacketStream << _options.ignorePenumbra;
audioDataOffset = _currentPacket->pos();
} else {
// no samples to inject, return immediately
qDebug() << "AudioInjector::injectNextFrame() called with no samples to inject. Returning.";
return NEXT_FRAME_DELTA_ERROR_OR_FINISHED;
}
}
setIsFinished(true);
_isPlaying = !_isFinished; // Which can be false if a restart was requested
int bytesToCopy = std::min((_options.stereo ? 2 : 1) * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL,
_audioData.size() - _currentSendOffset);
// Measure the loudness of this frame
_loudness = 0.0f;
for (int i = 0; i < bytesToCopy; i += sizeof(int16_t)) {
_loudness += abs(*reinterpret_cast<int16_t*>(_audioData.data() + _currentSendOffset + i)) /
(AudioConstants::MAX_SAMPLE_VALUE / 2.0f);
}
_loudness /= (float)(bytesToCopy / sizeof(int16_t));
_currentPacket->seek(0);
// pack the sequence number
_currentPacket->writePrimitive(_outgoingSequenceNumber);
_currentPacket->seek(positionOptionOffset);
_currentPacket->writePrimitive(_options.position);
_currentPacket->writePrimitive(_options.orientation);
quint8 volume = MAX_INJECTOR_VOLUME * _options.volume;
_currentPacket->seek(volumeOptionOffset);
_currentPacket->writePrimitive(volume);
_currentPacket->seek(audioDataOffset);
// copy the next NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL bytes to the packet
_currentPacket->write(_audioData.data() + _currentSendOffset, bytesToCopy);
// set the correct size used for this packet
_currentPacket->setPayloadSize(_currentPacket->pos());
// grab our audio mixer from the NodeList, if it exists
auto nodeList = DependencyManager::get<NodeList>();
SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer);
if (audioMixer) {
// send off this audio packet
nodeList->sendUnreliablePacket(*_currentPacket, *audioMixer);
_outgoingSequenceNumber++;
}
_currentSendOffset += bytesToCopy;
if (_currentSendOffset >= _audioData.size()) {
// we're at the end of the audio data to send
if (_options.loop) {
// we were asked to loop, set our send offset to 0
_currentSendOffset = 0;
} else {
// we weren't to loop, say that we're done now
qDebug() << "AudioInjector::injectNextFrame has sent all data and was not asked to loop - calling finish().";
finish();
return NEXT_FRAME_DELTA_ERROR_OR_FINISHED;
}
}
if (_currentSendOffset == bytesToCopy) {
// ask AudioInjectorManager to call us right away again to
// immediately send the first two frames so the mixer can start using the audio right away
return NEXT_FRAME_DELTA_IMMEDIATELY;
} else {
return (++_nextFrame * AudioConstants::NETWORK_FRAME_USECS) - _frameTimer->nsecsElapsed() / 1000;
}
}
void AudioInjector::stop() {
_shouldStop = true;
// trigger a call on the injector's thread to change state to finished
QMetaObject::invokeMethod(this, "finish");
}
if (_options.localOnly) {
// we're only a local injector, so we can say we are finished right away too
_isPlaying = false;
setIsFinished(true);
void AudioInjector::triggerDeleteAfterFinish() {
// make sure this fires on the AudioInjector thread
if (thread() != QThread::currentThread()) {
QMetaObject::invokeMethod(this, "triggerDeleteAfterFinish", Qt::QueuedConnection);
return;
}
if (_state == State::Finished) {
stopAndDeleteLater();
} else {
_state = State::NotFinishedWithPendingDelete;
}
}
@ -336,28 +372,40 @@ AudioInjector* AudioInjector::playSound(const QString& soundUrl, const float vol
AudioInjector* AudioInjector::playSoundAndDelete(const QByteArray& buffer, const AudioInjectorOptions options, AbstractAudioInterface* localInterface) {
AudioInjector* sound = playSound(buffer, options, localInterface);
sound->triggerDeleteAfterFinish();
if (sound) {
sound->_state = AudioInjector::State::NotFinishedWithPendingDelete;
}
return sound;
}
AudioInjector* AudioInjector::playSound(const QByteArray& buffer, const AudioInjectorOptions options, AbstractAudioInterface* localInterface) {
QThread* injectorThread = new QThread();
injectorThread->setObjectName("Audio Injector Thread");
AudioInjector* injector = new AudioInjector(buffer, options);
injector->_isPlaying = true;
injector->setLocalAudioInterface(localInterface);
injector->moveToThread(injectorThread);
// start injecting when the injector thread starts
connect(injectorThread, &QThread::started, injector, &AudioInjector::injectAudio);
// connect the right slots and signals for AudioInjector and thread cleanup
connect(injector, &AudioInjector::destroyed, injectorThread, &QThread::quit);
connect(injectorThread, &QThread::finished, injectorThread, &QThread::deleteLater);
injectorThread->start();
return injector;
// grab the AudioInjectorManager
auto injectorManager = DependencyManager::get<AudioInjectorManager>();
// setup parameters required for injection
injector->setupInjection();
if (options.localOnly) {
if (injector->injectLocally()) {
// local injection succeeded, return the pointer to injector
return injector;
} else {
// unable to inject locally, return a nullptr
return nullptr;
}
} else {
// attempt to thread the new injector
if (injectorManager->threadInjector(injector)) {
return injector;
} else {
// we failed to thread the new injector (we are at the max number of injector threads)
return nullptr;
}
}
}

View file

@ -12,6 +12,9 @@
#ifndef hifi_AudioInjector_h
#define hifi_AudioInjector_h
#include <memory>
#include <QtCore/QElapsedTimer>
#include <QtCore/QObject>
#include <QtCore/QSharedPointer>
#include <QtCore/QThread>
@ -19,11 +22,14 @@
#include <glm/glm.hpp>
#include <glm/gtx/quaternion.hpp>
#include <NLPacket.h>
#include "AudioInjectorLocalBuffer.h"
#include "AudioInjectorOptions.h"
#include "Sound.h"
class AbstractAudioInterface;
class AudioInjectorManager;
// In order to make scripting cleaner for the AudioInjector, the script now holds on to the AudioInjector object
// until it dies.
@ -32,11 +38,17 @@ class AudioInjector : public QObject {
Q_OBJECT
public:
enum class State : uint8_t {
NotFinished,
NotFinishedWithPendingDelete,
Finished
};
AudioInjector(QObject* parent);
AudioInjector(Sound* sound, const AudioInjectorOptions& injectorOptions);
AudioInjector(const QByteArray& audioData, const AudioInjectorOptions& injectorOptions);
bool isFinished() const { return _isFinished; }
bool isFinished() const { return _state == State::Finished; }
int getCurrentSendOffset() const { return _currentSendOffset; }
void setCurrentSendOffset(int currentSendOffset) { _currentSendOffset = currentSendOffset; }
@ -51,40 +63,46 @@ public:
static AudioInjector* playSound(const QString& soundUrl, const float volume, const float stretchFactor, const glm::vec3 position);
public slots:
void injectAudio();
void restart();
void stop();
void triggerDeleteAfterFinish() { _shouldDeleteAfterFinish = true; }
void triggerDeleteAfterFinish();
void stopAndDeleteLater();
const AudioInjectorOptions& getOptions() const { return _options; }
void setOptions(const AudioInjectorOptions& options) { _options = options; }
float getLoudness() const { return _loudness; }
bool isPlaying() const { return _isPlaying; }
void restartPortionAfterFinished();
bool isPlaying() const { return _state == State::NotFinished || _state == State::NotFinishedWithPendingDelete; }
signals:
void finished();
private:
void injectToMixer();
void injectLocally();
void restarting();
void setIsFinished(bool isFinished);
private slots:
void finish();
private:
void setupInjection();
uint64_t injectNextFrame();
bool injectLocally();
QByteArray _audioData;
AudioInjectorOptions _options;
State _state { State::NotFinished };
bool _hasSetup = false;
bool _shouldStop = false;
float _loudness = 0.0f;
bool _isPlaying = false;
bool _isStarted = false;
bool _isFinished = false;
bool _shouldDeleteAfterFinish = false;
int _currentSendOffset = 0;
AbstractAudioInterface* _localAudioInterface = NULL;
AudioInjectorLocalBuffer* _localBuffer = NULL;
std::unique_ptr<NLPacket> _currentPacket { nullptr };
AbstractAudioInterface* _localAudioInterface { nullptr };
AudioInjectorLocalBuffer* _localBuffer { nullptr };
int _nextFrame { 0 };
std::unique_ptr<QElapsedTimer> _frameTimer { nullptr };
quint16 _outgoingSequenceNumber { 0 };
friend class AudioInjectorManager;
};

View file

@ -0,0 +1,166 @@
//
// AudioInjectorManager.cpp
// libraries/audio/src
//
// Created by Stephen Birarda on 2015-11-16.
// Copyright 2015 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 "AudioInjectorManager.h"
#include <QtCore/QCoreApplication>
#include <SharedUtil.h>
#include "AudioConstants.h"
#include "AudioInjector.h"
AudioInjectorManager::~AudioInjectorManager() {
_shouldStop = true;
Lock lock(_injectorsMutex);
// make sure any still living injectors are stopped and deleted
while (!_injectors.empty()) {
// grab the injector at the front
auto& timePointerPair = _injectors.top();
// ask it to stop and be deleted
timePointerPair.second->stopAndDeleteLater();
_injectors.pop();
}
// get rid of the lock now that we've stopped all living injectors
lock.unlock();
// in case the thread is waiting for injectors wake it up now
_injectorReady.notify_one();
// quit and wait on the manager thread, if we ever created it
if (_thread) {
_thread->quit();
_thread->wait();
}
}
void AudioInjectorManager::createThread() {
_thread = new QThread;
_thread->setObjectName("Audio Injector Thread");
// when the thread is started, have it call our run to handle injection of audio
connect(_thread, &QThread::started, this, &AudioInjectorManager::run, Qt::DirectConnection);
// start the thread
_thread->start();
}
void AudioInjectorManager::run() {
while (!_shouldStop) {
// wait until the next injector is ready, or until we get a new injector given to us
Lock lock(_injectorsMutex);
if (_injectors.size() > 0) {
// when does the next injector need to send a frame?
// do we get to wait or should we just go for it now?
auto timeInjectorPair = _injectors.top();
auto nextTimestamp = timeInjectorPair.first;
int64_t difference = int64_t(nextTimestamp - usecTimestampNow());
if (difference > 0) {
_injectorReady.wait_for(lock, std::chrono::microseconds(difference));
}
if (_injectors.size() > 0) {
// loop through the injectors in the map and send whatever frames need to go out
auto front = _injectors.top();
while (_injectors.size() > 0 && front.first <= usecTimestampNow()) {
// either way we're popping this injector off - get a copy first
auto injector = front.second;
_injectors.pop();
if (!injector.isNull()) {
// this is an injector that's ready to go, have it send a frame now
auto nextCallDelta = injector->injectNextFrame();
if (nextCallDelta > 0 && !injector->isFinished()) {
// re-enqueue the injector with the correct timing
_injectors.emplace(usecTimestampNow() + nextCallDelta, injector);
}
}
if (_injectors.size() > 0) {
front = _injectors.top();
} else {
// no more injectors to look at, break
break;
}
}
}
} else {
// we have no current injectors, wait until we get at least one before we do anything
_injectorReady.wait(lock);
}
// unlock the lock in case something in process events needs to modify the queue
lock.unlock();
QCoreApplication::processEvents();
}
}
static const int MAX_INJECTORS_PER_THREAD = 40; // calculated based on AudioInjector time to send frame, with sufficient padding
bool AudioInjectorManager::threadInjector(AudioInjector* injector) {
if (_shouldStop) {
qDebug() << "AudioInjectorManager::threadInjector asked to thread injector but is shutting down.";
return false;
}
// guard the injectors vector with a mutex
Lock lock(_injectorsMutex);
// check if we'll be able to thread this injector (do we have < max concurrent injectors)
if (_injectors.size() < MAX_INJECTORS_PER_THREAD) {
if (!_thread) {
createThread();
}
// move the injector to the QThread
injector->moveToThread(_thread);
// handle a restart once the injector has finished
// add the injector to the queue with a send timestamp of now
_injectors.emplace(usecTimestampNow(), InjectorQPointer { injector });
// notify our wait condition so we can inject two frames for this injector immediately
_injectorReady.notify_one();
return true;
} else {
// unable to thread this injector, at the max
qDebug() << "AudioInjectorManager::threadInjector could not thread AudioInjector - at max of"
<< MAX_INJECTORS_PER_THREAD << "current audio injectors.";
return false;
}
}
void AudioInjectorManager::restartFinishedInjector(AudioInjector* injector) {
if (!_shouldStop) {
// guard the injectors vector with a mutex
Lock lock(_injectorsMutex);
// add the injector to the queue with a send timestamp of now
_injectors.emplace(usecTimestampNow(), InjectorQPointer { injector });
// notify our wait condition so we can inject two frames for this injector immediately
_injectorReady.notify_one();
}
}

View file

@ -0,0 +1,73 @@
//
// AudioInjectorManager.h
// libraries/audio/src
//
// Created by Stephen Birarda on 2015-11-16.
// Copyright 2015 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
//
#pragma once
#ifndef hifi_AudioInjectorManager_h
#define hifi_AudioInjectorManager_h
#include <condition_variable>
#include <queue>
#include <mutex>
#include <QtCore/QPointer>
#include <QtCore/QThread>
#include <DependencyManager.h>
class AudioInjector;
class AudioInjectorManager : public QObject, public Dependency {
Q_OBJECT
SINGLETON_DEPENDENCY
public:
~AudioInjectorManager();
private slots:
void run();
private:
using InjectorQPointer = QPointer<AudioInjector>;
using TimeInjectorPointerPair = std::pair<uint64_t, InjectorQPointer>;
struct greaterTime {
bool operator() (const TimeInjectorPointerPair& x, const TimeInjectorPointerPair& y) const {
return x.first > y.first;
}
};
using InjectorQueue = std::priority_queue<TimeInjectorPointerPair,
std::deque<TimeInjectorPointerPair>,
greaterTime>;
using Mutex = std::mutex;
using Lock = std::unique_lock<Mutex>;
bool threadInjector(AudioInjector* injector);
void restartFinishedInjector(AudioInjector* injector);
void notifyInjectorReadyCondition() { _injectorReady.notify_one(); }
AudioInjectorManager() {};
AudioInjectorManager(const AudioInjectorManager&) = delete;
AudioInjectorManager& operator=(const AudioInjectorManager&) = delete;
void createThread();
QThread* _thread { nullptr };
bool _shouldStop { false };
InjectorQueue _injectors;
Mutex _injectorsMutex;
std::condition_variable _injectorReady;
friend class AudioInjector;
};
#endif // hifi_AudioInjectorManager_h

View file

@ -86,6 +86,7 @@ void Sound::downloadFinished(const QByteArray& data) {
}
_isReady = true;
emit ready();
}
void Sound::downSample(const QByteArray& rawAudioByteArray) {

View file

@ -30,6 +30,9 @@ public:
const QByteArray& getByteArray() { return _byteArray; }
signals:
void ready();
private:
QByteArray _byteArray;
bool _isStereo;