diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index cc63f4450e..95bd8f25d5 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -62,6 +63,7 @@ Agent::Agent(NLPacket& packet) : DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -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(); assetThread->quit(); assetThread->wait(); + + // cleanup the AudioInjectorManager (and any still running injectors) + DependencyManager::destroy(); } diff --git a/examples/tests/injectorTest.js b/examples/tests/injectorTest.js new file mode 100644 index 0000000000..171186c91e --- /dev/null +++ b/examples/tests/injectorTest.js @@ -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(); + } + } + } +}) diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 2afa8cead5..355572ae9e 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -50,6 +50,7 @@ #include #include #include +#include #include #include #include @@ -340,6 +341,7 @@ bool setupEssentials(int& argc, char** argv) { DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); + DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); DependencyManager::set(); @@ -894,6 +896,10 @@ void Application::cleanupBeforeQuit() { // destroy the AudioClient so it and its thread have a chance to go down safely DependencyManager::destroy(); + + // 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(); // Destroy third party processes after scripts have finished using them. #ifdef HAVE_DDE diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index bd807f8dbd..a0d78da7f8 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -18,6 +18,7 @@ #include #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(); + + 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(); - - // 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(&_options.position), - sizeof(_options.position)); - - // pack our orientation for injected audio - audioPacketStream.writeRawData(reinterpret_cast(&_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(_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(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(&_options.position), + sizeof(_options.position)); + + // pack our orientation for injected audio + audioPacketStream.writeRawData(reinterpret_cast(&_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(_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(); + 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(); + + // 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; + } + } } diff --git a/libraries/audio/src/AudioInjector.h b/libraries/audio/src/AudioInjector.h index 0e98fe1682..f815b6fe3a 100644 --- a/libraries/audio/src/AudioInjector.h +++ b/libraries/audio/src/AudioInjector.h @@ -12,6 +12,9 @@ #ifndef hifi_AudioInjector_h #define hifi_AudioInjector_h +#include + +#include #include #include #include @@ -19,11 +22,14 @@ #include #include +#include + #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 _currentPacket { nullptr }; + AbstractAudioInterface* _localAudioInterface { nullptr }; + AudioInjectorLocalBuffer* _localBuffer { nullptr }; + + int _nextFrame { 0 }; + std::unique_ptr _frameTimer { nullptr }; + quint16 _outgoingSequenceNumber { 0 }; + + friend class AudioInjectorManager; }; diff --git a/libraries/audio/src/AudioInjectorManager.cpp b/libraries/audio/src/AudioInjectorManager.cpp new file mode 100644 index 0000000000..f504b31907 --- /dev/null +++ b/libraries/audio/src/AudioInjectorManager.cpp @@ -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 + +#include + +#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(); + } +} diff --git a/libraries/audio/src/AudioInjectorManager.h b/libraries/audio/src/AudioInjectorManager.h new file mode 100644 index 0000000000..91648fff39 --- /dev/null +++ b/libraries/audio/src/AudioInjectorManager.h @@ -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 +#include +#include + +#include +#include + +#include + +class AudioInjector; + +class AudioInjectorManager : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY +public: + ~AudioInjectorManager(); +private slots: + void run(); +private: + + using InjectorQPointer = QPointer; + using TimeInjectorPointerPair = std::pair; + + struct greaterTime { + bool operator() (const TimeInjectorPointerPair& x, const TimeInjectorPointerPair& y) const { + return x.first > y.first; + } + }; + + using InjectorQueue = std::priority_queue, + greaterTime>; + using Mutex = std::mutex; + using Lock = std::unique_lock; + + 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 diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index 2ce2c47fef..12f63e0a12 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -86,6 +86,7 @@ void Sound::downloadFinished(const QByteArray& data) { } _isReady = true; + emit ready(); } void Sound::downSample(const QByteArray& rawAudioByteArray) { diff --git a/libraries/audio/src/Sound.h b/libraries/audio/src/Sound.h index 842c395a7d..91dbef8c6a 100644 --- a/libraries/audio/src/Sound.h +++ b/libraries/audio/src/Sound.h @@ -30,6 +30,9 @@ public: const QByteArray& getByteArray() { return _byteArray; } +signals: + void ready(); + private: QByteArray _byteArray; bool _isStereo;