From 73aafaf518e0a9bb23677f159069359d4a63b128 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 5 Oct 2016 18:12:58 -0700 Subject: [PATCH 1/9] Add simple sound to avatars For now I just used the piano1.wav that howard suggested long ago for another project. We use Agent.playAvatarSound, and tell the mixer to mix audio for each avatar by setting Agent.setIsListeningToAudioStream to true. --- .../tests/performance/crowd-agent.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/scripts/developer/tests/performance/crowd-agent.js b/scripts/developer/tests/performance/crowd-agent.js index 5df576cf99..48b838697b 100644 --- a/scripts/developer/tests/performance/crowd-agent.js +++ b/scripts/developer/tests/performance/crowd-agent.js @@ -15,6 +15,9 @@ // a script like summon.js calls up to n avatars to be around you. var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; +var soundIntervalId; +var SOUND_POLL_INTERVAL = 500; // ms +var SOUND_URL = "http://howard-stearns.github.io/models/sounds/piano1.wav"; print('crowd-agent version 1'); @@ -41,9 +44,26 @@ function startAgent(parameters) { // Can also be used to update. Avatar.startAnimation(data.url, data.fps || 30, 1.0, (data.loopFlag === undefined) ? true : data.loopFlag, false, data.startFrame || 0, data.endFrame); } print('crowd-agent avatars started'); + Agent.isListeningToAudioStream = true; + soundIntervalId = playSound(); + print('crowd-agent sound loop started'); +} +function playSound() { + // Load a sound + var sound = SoundCache.getSound(SOUND_URL); + function loopSound(sound) { + // since we cannot loop, for now lets just see if we are making sounds. If + // not, then play a sound. + if (!Agent.isPlayingAvatarSound) { + Agent.playAvatarSound(sound); + } + }; + return Script.setInterval(function() {loopSound(sound);}, SOUND_POLL_INTERVAL); } function stopAgent(parameters) { Agent.isAvatar = false; + Agent.isListeningToAudioStream = false; + Script.clearInterval(soundIntervalId); print('crowd-agent stopped', JSON.stringify(parameters), JSON.stringify(Agent)); } From a0c731a0c61629ccb37c492d638b5f7bd104f0a4 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 6 Oct 2016 14:57:40 -0700 Subject: [PATCH 2/9] Initial attempt Kinda works, but sounds 'scratchy'. Surely I've done something lame. --- assignment-client/src/Agent.cpp | 80 +++++++++++++++++++++++++++++++-- assignment-client/src/Agent.h | 13 +++++- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 65443b0574..9c8825ca65 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -33,6 +33,9 @@ #include #include +#include +#include + #include #include // TODO: consider moving to scriptengine.h @@ -71,6 +74,8 @@ Agent::Agent(ReceivedMessage& message) : { PacketType::OctreeStats, PacketType::EntityData, PacketType::EntityErase }, this, "handleOctreePacket"); packetReceiver.registerListener(PacketType::Jurisdiction, this, "handleJurisdictionPacket"); + packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat"); + connect(&_receivedAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &Agent::handleMismatchAudioFormat); } void Agent::handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode) { @@ -213,6 +218,66 @@ void Agent::nodeActivated(SharedNodePointer activatedNode) { _pendingScriptRequest = nullptr; } + if (activatedNode->getType() == NodeType::AudioMixer) { + negotiateAudioFormat(); + } + +} + +void Agent::negotiateAudioFormat() { + auto nodeList = DependencyManager::get(); + auto negotiateFormatPacket = NLPacket::create(PacketType::NegotiateAudioFormat); + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + quint8 numberOfCodecs = (quint8)codecPlugins.size(); + negotiateFormatPacket->writePrimitive(numberOfCodecs); + for (auto& plugin : codecPlugins) { + auto codecName = plugin->getName(); + negotiateFormatPacket->writeString(codecName); + } + + // grab our audio mixer from the NodeList, if it exists + SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer); + + if (audioMixer) { + // send off this mute packet + nodeList->sendPacket(std::move(negotiateFormatPacket), *audioMixer); + } + qInfo() << "negotiateAudioFormat called"; +} + +void Agent::handleSelectedAudioFormat(QSharedPointer message) { + QString selectedCodecName = message->readString(); + selectAudioFormat(selectedCodecName); +} + +void Agent::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { + qDebug() << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec; + selectAudioFormat(recievedCodec); +} + +void Agent::selectAudioFormat(const QString& selectedCodecName) { + _selectedCodecName = selectedCodecName; + + qDebug() << "Selected Codec:" << _selectedCodecName; + + // release any old codec encoder/decoder first... + if (_codec && _encoder) { + _codec->releaseEncoder(_encoder); + _encoder = nullptr; + _codec = nullptr; + } + _receivedAudioStream.cleanupCodec(); + + auto codecPlugins = PluginManager::getInstance()->getCodecPlugins(); + for (auto& plugin : codecPlugins) { + if (_selectedCodecName == plugin->getName()) { + _codec = plugin; + _receivedAudioStream.setupCodec(plugin, _selectedCodecName, AudioConstants::STEREO); + _encoder = plugin->createEncoder(AudioConstants::SAMPLE_RATE, AudioConstants::MONO); + qDebug() << "Selected Codec Plugin:" << _codec.get(); + break; + } + } } void Agent::scriptRequestFinished() { @@ -438,8 +503,7 @@ void Agent::processAgentAvatarAndAudio(float deltaTime) { } else if (nextSoundOutput) { // write the codec - QString codecName; - audioPacket->writeString(codecName); + audioPacket->writeString(_selectedCodecName); // assume scripted avatar audio is mono and set channel flag to zero audioPacket->writePrimitive((quint8)0); @@ -448,9 +512,19 @@ void Agent::processAgentAvatarAndAudio(float deltaTime) { audioPacket->writePrimitive(scriptedAvatar->getPosition()); glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); audioPacket->writePrimitive(headOrientation); + + // encode it + QByteArray decodedBuffer(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); + QByteArray encodedBuffer; + if (_encoder) { + _encoder->encode(decodedBuffer, encodedBuffer); + } else { + encodedBuffer = decodedBuffer; + } + // write the raw audio data - audioPacket->write(reinterpret_cast(nextSoundOutput), numAvailableSamples * sizeof(int16_t)); + audioPacket->write(encodedBuffer.data(), encodedBuffer.size()); } // write audio packet to AudioMixer nodes diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 3f4e36374c..0ebb28797f 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -26,6 +26,8 @@ #include #include +#include + #include "MixedAudioStream.h" @@ -66,12 +68,16 @@ private slots: void handleAudioPacket(QSharedPointer message); void handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode); void handleJurisdictionPacket(QSharedPointer message, SharedNodePointer senderNode); + void handleSelectedAudioFormat(QSharedPointer message); + void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec); void processAgentAvatarAndAudio(float deltaTime); - void nodeActivated(SharedNodePointer activatedNode); private: + void negotiateAudioFormat(); + void selectAudioFormat(const QString& selectedCodecName); + std::unique_ptr _scriptEngine; EntityEditPacketSender _entityEditSender; EntityTreeHeadlessViewer _entityViewer; @@ -92,6 +98,11 @@ private: bool _isAvatar = false; QTimer* _avatarIdentityTimer = nullptr; QHash _outgoingScriptAudioSequenceNumbers; + + CodecPluginPointer _codec; + QString _selectedCodecName; + Encoder* _encoder { nullptr }; + }; From 95aa18f66d3b62407d7fdd4d188319152252cf54 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Thu, 6 Oct 2016 16:18:22 -0700 Subject: [PATCH 3/9] Some cleanup unnecessary stuff removed, added some cleanup code. --- assignment-client/src/Agent.cpp | 14 +++++++------- assignment-client/src/Agent.h | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 9c8825ca65..067cab41da 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -75,7 +75,6 @@ Agent::Agent(ReceivedMessage& message) : this, "handleOctreePacket"); packetReceiver.registerListener(PacketType::Jurisdiction, this, "handleJurisdictionPacket"); packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat"); - connect(&_receivedAudioStream, &InboundAudioStream::mismatchedAudioCodec, this, &Agent::handleMismatchAudioFormat); } void Agent::handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode) { @@ -250,11 +249,6 @@ void Agent::handleSelectedAudioFormat(QSharedPointer message) { selectAudioFormat(selectedCodecName); } -void Agent::handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec) { - qDebug() << __FUNCTION__ << "sendingNode:" << *node << "currentCodec:" << currentCodec << "recievedCodec:" << recievedCodec; - selectAudioFormat(recievedCodec); -} - void Agent::selectAudioFormat(const QString& selectedCodecName) { _selectedCodecName = selectedCodecName; @@ -504,7 +498,7 @@ void Agent::processAgentAvatarAndAudio(float deltaTime) { } else if (nextSoundOutput) { // write the codec audioPacket->writeString(_selectedCodecName); - + // assume scripted avatar audio is mono and set channel flag to zero audioPacket->writePrimitive((quint8)0); @@ -559,4 +553,10 @@ void Agent::aboutToFinish() { // cleanup the AudioInjectorManager (and any still running injectors) DependencyManager::destroy(); + + // cleanup codec & encoder + if (_codec && _encoder) { + _codec->releaseEncoder(_encoder); + _encoder = nullptr; + } } diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 0ebb28797f..e65d03a5b1 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -69,7 +69,6 @@ private slots: void handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode); void handleJurisdictionPacket(QSharedPointer message, SharedNodePointer senderNode); void handleSelectedAudioFormat(QSharedPointer message); - void handleMismatchAudioFormat(SharedNodePointer node, const QString& currentCodec, const QString& recievedCodec); void processAgentAvatarAndAudio(float deltaTime); void nodeActivated(SharedNodePointer activatedNode); From b4c064a5386ee37c7ff049b041c7b49a3b5c4b16 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Fri, 7 Oct 2016 13:20:08 -0700 Subject: [PATCH 4/9] Hack in a 100hz timer sounds crappy, more to do, just push to not lose anything. --- assignment-client/src/Agent.cpp | 14 +++++++++----- assignment-client/src/Agent.h | 6 +++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 067cab41da..9b7e8b45c5 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -373,8 +373,13 @@ void Agent::executeScript() { DependencyManager::set(_entityViewer.getTree()); + _avatarAudioTimer = new QTimer(this); + _avatarAudioTimer->setTimerType(Qt::PreciseTimer); + connect(_avatarAudioTimer, SIGNAL(timeout()), this, SLOT(processAgentAvatarAndAudio())); + _avatarAudioTimer->start(10); + // wire up our additional agent related processing to the update signal - QObject::connect(_scriptEngine.get(), &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio); + //QObject::connect(_scriptEngine.get(), &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio); _scriptEngine->run(); @@ -420,10 +425,10 @@ void Agent::sendAvatarIdentityPacket() { } } -void Agent::processAgentAvatarAndAudio(float deltaTime) { +void Agent::processAgentAvatarAndAudio() { if (!_scriptEngine->isFinished() && _isAvatar) { auto scriptedAvatar = DependencyManager::get(); - const int SCRIPT_AUDIO_BUFFER_SAMPLES = AudioConstants::SAMPLE_RATE / SCRIPT_FPS + 0.5; + const int SCRIPT_AUDIO_BUFFER_SAMPLES = AudioConstants::SAMPLE_RATE / 100;//SCRIPT_FPS + 0.5; const int SCRIPT_AUDIO_BUFFER_BYTES = SCRIPT_AUDIO_BUFFER_SAMPLES * sizeof(int16_t); QByteArray avatarByteArray = scriptedAvatar->toByteArray(true, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); @@ -513,10 +518,9 @@ void Agent::processAgentAvatarAndAudio(float deltaTime) { if (_encoder) { _encoder->encode(decodedBuffer, encodedBuffer); } else { - encodedBuffer = decodedBuffer; + audioPacket->write(decodedBuffer.data(), decodedBuffer.size()); } - // write the raw audio data audioPacket->write(encodedBuffer.data(), encodedBuffer.size()); } diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index e65d03a5b1..13ed8f7958 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -70,7 +71,7 @@ private slots: void handleJurisdictionPacket(QSharedPointer message, SharedNodePointer senderNode); void handleSelectedAudioFormat(QSharedPointer message); - void processAgentAvatarAndAudio(float deltaTime); + void processAgentAvatarAndAudio(); void nodeActivated(SharedNodePointer activatedNode); private: @@ -101,8 +102,7 @@ private: CodecPluginPointer _codec; QString _selectedCodecName; Encoder* _encoder { nullptr }; - - + QTimer* _avatarAudioTimer; }; #endif // hifi_Agent_h From b58c36cb12939af1161816559d73b926561995fe Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 10 Oct 2016 17:57:04 -0700 Subject: [PATCH 5/9] Rolling my own 100hz timer Starting with the simplest possible thing - just sleep for 10000 microseconds. Can make it adaptive if need be. --- assignment-client/src/Agent.cpp | 48 ++++++++++++++++++++++++--------- assignment-client/src/Agent.h | 21 +++++++++++++-- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 9b7e8b45c5..c0e4d939af 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -48,6 +49,18 @@ static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10; +// this should send a signal every 10ms, with pretty good precision +void AvatarAudioTimer::start() { + qDebug() << "AvatarAudioTimer::start called"; + const int TARGET_INTERVAL_USEC = 10000; // 10ms + while (!_quit) { + // simplest possible timer + usleep(TARGET_INTERVAL_USEC); + emit avatarTick(); + } + qDebug() << "AvatarAudioTimer is finished"; +} + Agent::Agent(ReceivedMessage& message) : ThreadedAssignment(message), _entityEditSender(), @@ -121,7 +134,6 @@ void Agent::handleAudioPacket(QSharedPointer message) { _receivedAudioStream.parseData(*message); _lastReceivedAudioLoudness = _receivedAudioStream.getNextOutputFrameLoudness(); - _receivedAudioStream.clearBuffer(); } @@ -372,11 +384,15 @@ void Agent::executeScript() { entityScriptingInterface->setEntityTree(_entityViewer.getTree()); DependencyManager::set(_entityViewer.getTree()); - - _avatarAudioTimer = new QTimer(this); - _avatarAudioTimer->setTimerType(Qt::PreciseTimer); - connect(_avatarAudioTimer, SIGNAL(timeout()), this, SLOT(processAgentAvatarAndAudio())); - _avatarAudioTimer->start(10); + + qDebug() << "Connecting avatarAudioTimer and starting..."; + AvatarAudioTimer* audioTimerWorker = new AvatarAudioTimer(); + audioTimerWorker->moveToThread(&_avatarAudioTimerThread); + connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAndAudio); + connect(this, &Agent::startAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::start); + connect(this, &Agent::stopAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::stop); + connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater); + _avatarAudioTimerThread.start(); // wire up our additional agent related processing to the update signal //QObject::connect(_scriptEngine.get(), &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio); @@ -406,6 +422,10 @@ void Agent::setIsAvatar(bool isAvatar) { // start the timers _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); + + // tell the audiotimer worker to start working + emit startAvatarAudioTimer(); + } if (!_isAvatar) { @@ -428,7 +448,7 @@ void Agent::sendAvatarIdentityPacket() { void Agent::processAgentAvatarAndAudio() { if (!_scriptEngine->isFinished() && _isAvatar) { auto scriptedAvatar = DependencyManager::get(); - const int SCRIPT_AUDIO_BUFFER_SAMPLES = AudioConstants::SAMPLE_RATE / 100;//SCRIPT_FPS + 0.5; + const int SCRIPT_AUDIO_BUFFER_SAMPLES = AudioConstants::SAMPLE_RATE / 100 + 0.5; const int SCRIPT_AUDIO_BUFFER_BYTES = SCRIPT_AUDIO_BUFFER_SAMPLES * sizeof(int16_t); QByteArray avatarByteArray = scriptedAvatar->toByteArray(true, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); @@ -513,16 +533,15 @@ void Agent::processAgentAvatarAndAudio() { audioPacket->writePrimitive(headOrientation); // encode it - QByteArray decodedBuffer(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); - QByteArray encodedBuffer; - if (_encoder) { + if(_encoder) { + QByteArray decodedBuffer(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); + QByteArray encodedBuffer; _encoder->encode(decodedBuffer, encodedBuffer); + audioPacket->write(encodedBuffer.data(), encodedBuffer.size()); } else { - audioPacket->write(decodedBuffer.data(), decodedBuffer.size()); + audioPacket->write(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); } - // write the raw audio data - audioPacket->write(encodedBuffer.data(), encodedBuffer.size()); } // write audio packet to AudioMixer nodes @@ -558,6 +577,9 @@ void Agent::aboutToFinish() { // cleanup the AudioInjectorManager (and any still running injectors) DependencyManager::destroy(); + emit stopAvatarAudioTimer(); + _avatarAudioTimerThread.quit(); + // cleanup codec & encoder if (_codec && _encoder) { _codec->releaseEncoder(_encoder); diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 13ed8f7958..ba90a3247e 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -32,6 +32,20 @@ #include "MixedAudioStream.h" +class AvatarAudioTimer : public QObject { + Q_OBJECT + +signals: + void avatarTick(); + +public slots: + void start(); + void stop() { _quit = true; } + +private: + bool _quit { false }; +}; + class Agent : public ThreadedAssignment { Q_OBJECT @@ -60,6 +74,7 @@ public: public slots: void run() override; void playAvatarSound(SharedSoundPointer avatarSound) { setAvatarSound(avatarSound); } + void processAgentAvatarAndAudio(); private slots: void requestScript(); @@ -71,9 +86,11 @@ private slots: void handleJurisdictionPacket(QSharedPointer message, SharedNodePointer senderNode); void handleSelectedAudioFormat(QSharedPointer message); - void processAgentAvatarAndAudio(); void nodeActivated(SharedNodePointer activatedNode); +signals: + void startAvatarAudioTimer(); + void stopAvatarAudioTimer(); private: void negotiateAudioFormat(); void selectAudioFormat(const QString& selectedCodecName); @@ -102,7 +119,7 @@ private: CodecPluginPointer _codec; QString _selectedCodecName; Encoder* _encoder { nullptr }; - QTimer* _avatarAudioTimer; + QThread _avatarAudioTimerThread; }; #endif // hifi_Agent_h From 0794d95bdcaa4625571759e6dd1bc58dca6619e9 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Mon, 10 Oct 2016 19:58:16 -0700 Subject: [PATCH 6/9] Fix merge Take the new crowd-agent.js, my changes were temporary. --- .../tests/performance/crowd-agent.js | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/scripts/developer/tests/performance/crowd-agent.js b/scripts/developer/tests/performance/crowd-agent.js index ffcff331d7..6b73d66c4f 100644 --- a/scripts/developer/tests/performance/crowd-agent.js +++ b/scripts/developer/tests/performance/crowd-agent.js @@ -15,9 +15,6 @@ // a script like summon.js calls up to n avatars to be around you. var MESSAGE_CHANNEL = "io.highfidelity.summon-crowd"; -var soundIntervalId; -var SOUND_POLL_INTERVAL = 500; // ms -var SOUND_URL = "http://howard-stearns.github.io/models/sounds/piano1.wav"; print('crowd-agent version 2'); @@ -67,26 +64,9 @@ function startAgent(parameters) { // Can also be used to update. Avatar.startAnimation(data.url, data.fps || 30, 1.0, (data.loopFlag === undefined) ? true : data.loopFlag, false, data.startFrame || 0, data.endFrame); } print('crowd-agent avatars started'); - Agent.isListeningToAudioStream = true; - soundIntervalId = playSound(); - print('crowd-agent sound loop started'); -} -function playSound() { - // Load a sound - var sound = SoundCache.getSound(SOUND_URL); - function loopSound(sound) { - // since we cannot loop, for now lets just see if we are making sounds. If - // not, then play a sound. - if (!Agent.isPlayingAvatarSound) { - Agent.playAvatarSound(sound); - } - }; - return Script.setInterval(function() {loopSound(sound);}, SOUND_POLL_INTERVAL); } function stopAgent(parameters) { Agent.isAvatar = false; - Agent.isListeningToAudioStream = false; - Script.clearInterval(soundIntervalId); print('crowd-agent stopped', JSON.stringify(parameters), JSON.stringify(Agent)); } From ef844cbd00bc8b74d563c3102b02f879cab16fb4 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 11 Oct 2016 09:33:55 -0700 Subject: [PATCH 7/9] Cleanup artifacts Seems playAvatarSound could be called from a thread other than the Agent's thread. So, artifacts happen when that pointer changes while a 'tick' is happening. Also cleaned up code a bit, got rid of some hard-coded stuff I had in just for dev purposes. --- assignment-client/src/Agent.cpp | 35 ++++++++++++++++++++------------- assignment-client/src/Agent.h | 2 +- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index dc9b572a2e..9be55c15a7 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -52,10 +52,15 @@ static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10; // this should send a signal every 10ms, with pretty good precision void AvatarAudioTimer::start() { qDebug() << "AvatarAudioTimer::start called"; + auto startTime = usecTimestampNow(); + quint64 frameCounter = 0; const int TARGET_INTERVAL_USEC = 10000; // 10ms while (!_quit) { + frameCounter++; // simplest possible timer - usleep(TARGET_INTERVAL_USEC); + quint64 targetTime = startTime + frameCounter * TARGET_INTERVAL_USEC; + quint64 interval = std::max((quint64)0, targetTime - usecTimestampNow()); + usleep(interval); emit avatarTick(); } qDebug() << "AvatarAudioTimer is finished"; @@ -91,6 +96,16 @@ Agent::Agent(ReceivedMessage& message) : packetReceiver.registerListener(PacketType::SelectedAudioFormat, this, "handleSelectedAudioFormat"); } +void Agent::playAvatarSound(SharedSoundPointer sound) { + // this must happen on Agent's main thread + if (QThread::currentThread() != thread()) { + QMetaObject::invokeMethod(this, "playAvatarSound", Q_ARG(SharedSoundPointer, sound)); + return; + } else { + setAvatarSound(sound); + } +} + void Agent::handleOctreePacket(QSharedPointer message, SharedNodePointer senderNode) { auto packetType = message->getType(); @@ -233,7 +248,6 @@ void Agent::nodeActivated(SharedNodePointer activatedNode) { if (activatedNode->getType() == NodeType::AudioMixer) { negotiateAudioFormat(); } - } void Agent::negotiateAudioFormat() { @@ -254,7 +268,6 @@ void Agent::negotiateAudioFormat() { // send off this mute packet nodeList->sendPacket(std::move(negotiateFormatPacket), *audioMixer); } - qInfo() << "negotiateAudioFormat called"; } void Agent::handleSelectedAudioFormat(QSharedPointer message) { @@ -386,7 +399,6 @@ void Agent::executeScript() { DependencyManager::set(_entityViewer.getTree()); - qDebug() << "Connecting avatarAudioTimer and starting..."; AvatarAudioTimer* audioTimerWorker = new AvatarAudioTimer(); audioTimerWorker->moveToThread(&_avatarAudioTimerThread); connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAndAudio); @@ -395,9 +407,6 @@ void Agent::executeScript() { connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater); _avatarAudioTimerThread.start(); - // wire up our additional agent related processing to the update signal - //QObject::connect(_scriptEngine.get(), &ScriptEngine::update, this, &Agent::processAgentAvatarAndAudio); - _scriptEngine->run(); Frame::clearFrameHandler(AUDIO_FRAME_TYPE); @@ -424,7 +433,7 @@ void Agent::setIsAvatar(bool isAvatar) { // start the timers _avatarIdentityTimer->start(AVATAR_IDENTITY_PACKET_SEND_INTERVAL_MSECS); - // tell the audiotimer worker to start working + // tell the avatarAudioTimer to start ticking emit startAvatarAudioTimer(); } @@ -464,8 +473,6 @@ void Agent::sendAvatarIdentityPacket() { void Agent::processAgentAvatarAndAudio() { if (!_scriptEngine->isFinished() && _isAvatar) { auto scriptedAvatar = DependencyManager::get(); - const int SCRIPT_AUDIO_BUFFER_SAMPLES = AudioConstants::SAMPLE_RATE / 100 + 0.5; - const int SCRIPT_AUDIO_BUFFER_BYTES = SCRIPT_AUDIO_BUFFER_SAMPLES * sizeof(int16_t); QByteArray avatarByteArray = scriptedAvatar->toByteArray(true, randFloat() < AVATAR_SEND_FULL_UPDATE_RATIO); scriptedAvatar->doneEncoding(true); @@ -484,7 +491,7 @@ void Agent::processAgentAvatarAndAudio() { // if we have an avatar audio stream then send it out to our audio-mixer bool silentFrame = true; - int16_t numAvailableSamples = SCRIPT_AUDIO_BUFFER_SAMPLES; + int16_t numAvailableSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; const int16_t* nextSoundOutput = NULL; if (_avatarSound) { @@ -492,8 +499,8 @@ void Agent::processAgentAvatarAndAudio() { nextSoundOutput = reinterpret_cast(soundByteArray.data() + _numAvatarSoundSentBytes); - int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > SCRIPT_AUDIO_BUFFER_BYTES - ? SCRIPT_AUDIO_BUFFER_BYTES + int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL + ? AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL : soundByteArray.size() - _numAvatarSoundSentBytes; numAvailableSamples = (int16_t)numAvailableBytes / sizeof(int16_t); @@ -529,7 +536,7 @@ void Agent::processAgentAvatarAndAudio() { } // write the number of silent samples so the audio-mixer can uphold timing - audioPacket->writePrimitive(SCRIPT_AUDIO_BUFFER_SAMPLES); + audioPacket->writePrimitive(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); // use the orientation and position of this avatar for the source of this audio audioPacket->writePrimitive(scriptedAvatar->getPosition()); diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index ba90a3247e..e89a8a9b13 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -73,7 +73,7 @@ public: public slots: void run() override; - void playAvatarSound(SharedSoundPointer avatarSound) { setAvatarSound(avatarSound); } + void playAvatarSound(SharedSoundPointer avatarSound);// { setAvatarSound(avatarSound); } void processAgentAvatarAndAudio(); private slots: From 1fb7b42f0b63d6e6d8fa1454a97ab233be47c4e7 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Tue, 11 Oct 2016 09:37:42 -0700 Subject: [PATCH 8/9] One more comment removed --- assignment-client/src/Agent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index e89a8a9b13..cbb1af2e33 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -73,7 +73,7 @@ public: public slots: void run() override; - void playAvatarSound(SharedSoundPointer avatarSound);// { setAvatarSound(avatarSound); } + void playAvatarSound(SharedSoundPointer avatarSound); void processAgentAvatarAndAudio(); private slots: From c8850a8b8e22c12d5dafb7ed6d3d42423b766f32 Mon Sep 17 00:00:00 2001 From: David Kelly Date: Wed, 12 Oct 2016 12:44:24 -0700 Subject: [PATCH 9/9] PR feedback AgentAvatarTimer now in its own files. The thread still is created the same way, and started only when you are an avatar, but now I remember to stop it when you no longer are one. Audio is still at 100Hz, but avatar messages go at 60Hz as before. Unsure it matters, but easy to do. Looking at that buzz now. --- assignment-client/src/Agent.cpp | 188 ++++++++++----------- assignment-client/src/Agent.h | 19 +-- assignment-client/src/AvatarAudioTimer.cpp | 33 ++++ assignment-client/src/AvatarAudioTimer.h | 31 ++++ 4 files changed, 157 insertions(+), 114 deletions(-) create mode 100644 assignment-client/src/AvatarAudioTimer.cpp create mode 100644 assignment-client/src/AvatarAudioTimer.h diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index 9be55c15a7..14b3c0e90f 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -46,26 +46,10 @@ #include "AbstractAudioInterface.h" #include "Agent.h" +#include "AvatarAudioTimer.h" static const int RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES = 10; -// this should send a signal every 10ms, with pretty good precision -void AvatarAudioTimer::start() { - qDebug() << "AvatarAudioTimer::start called"; - auto startTime = usecTimestampNow(); - quint64 frameCounter = 0; - const int TARGET_INTERVAL_USEC = 10000; // 10ms - while (!_quit) { - frameCounter++; - // simplest possible timer - quint64 targetTime = startTime + frameCounter * TARGET_INTERVAL_USEC; - quint64 interval = std::max((quint64)0, targetTime - usecTimestampNow()); - usleep(interval); - emit avatarTick(); - } - qDebug() << "AvatarAudioTimer is finished"; -} - Agent::Agent(ReceivedMessage& message) : ThreadedAssignment(message), _entityEditSender(), @@ -399,14 +383,17 @@ void Agent::executeScript() { DependencyManager::set(_entityViewer.getTree()); + // 100Hz timer for audio AvatarAudioTimer* audioTimerWorker = new AvatarAudioTimer(); audioTimerWorker->moveToThread(&_avatarAudioTimerThread); - connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAndAudio); + connect(audioTimerWorker, &AvatarAudioTimer::avatarTick, this, &Agent::processAgentAvatarAudio); connect(this, &Agent::startAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::start); connect(this, &Agent::stopAvatarAudioTimer, audioTimerWorker, &AvatarAudioTimer::stop); connect(&_avatarAudioTimerThread, &QThread::finished, audioTimerWorker, &QObject::deleteLater); _avatarAudioTimerThread.start(); - + + // 60Hz timer for avatar + QObject::connect(_scriptEngine.get(), &ScriptEngine::update, this, &Agent::processAgentAvatar); _scriptEngine->run(); Frame::clearFrameHandler(AUDIO_FRAME_TYPE); @@ -460,6 +447,7 @@ void Agent::setIsAvatar(bool isAvatar) { nodeList->sendPacketList(std::move(packetList), *node); }); } + emit stopAvatarAudioTimer(); } } @@ -470,7 +458,7 @@ void Agent::sendAvatarIdentityPacket() { } } -void Agent::processAgentAvatarAndAudio() { +void Agent::processAgentAvatar() { if (!_scriptEngine->isFinished() && _isAvatar) { auto scriptedAvatar = DependencyManager::get(); @@ -486,102 +474,106 @@ void Agent::processAgentAvatarAndAudio() { auto nodeList = DependencyManager::get(); nodeList->broadcastToNodes(std::move(avatarPacket), NodeSet() << NodeType::AvatarMixer); + } +} - if (_isListeningToAudioStream || _avatarSound) { - // if we have an avatar audio stream then send it out to our audio-mixer - bool silentFrame = true; +void Agent::processAgentAvatarAudio() { + if (_isAvatar && (_isListeningToAudioStream || _avatarSound)) { + // if we have an avatar audio stream then send it out to our audio-mixer + auto scriptedAvatar = DependencyManager::get(); + bool silentFrame = true; - int16_t numAvailableSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; - const int16_t* nextSoundOutput = NULL; + int16_t numAvailableSamples = AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL; + const int16_t* nextSoundOutput = NULL; - if (_avatarSound) { - const QByteArray& soundByteArray = _avatarSound->getByteArray(); - nextSoundOutput = reinterpret_cast(soundByteArray.data() + if (_avatarSound) { + const QByteArray& soundByteArray = _avatarSound->getByteArray(); + nextSoundOutput = reinterpret_cast(soundByteArray.data() + _numAvatarSoundSentBytes); - int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL - ? AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL - : soundByteArray.size() - _numAvatarSoundSentBytes; - numAvailableSamples = (int16_t)numAvailableBytes / sizeof(int16_t); + int numAvailableBytes = (soundByteArray.size() - _numAvatarSoundSentBytes) > AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL + ? AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL + : soundByteArray.size() - _numAvatarSoundSentBytes; + numAvailableSamples = (int16_t)numAvailableBytes / sizeof(int16_t); - // check if the all of the _numAvatarAudioBufferSamples to be sent are silence - for (int i = 0; i < numAvailableSamples; ++i) { - if (nextSoundOutput[i] != 0) { - silentFrame = false; - break; - } - } - - _numAvatarSoundSentBytes += numAvailableBytes; - if (_numAvatarSoundSentBytes == soundByteArray.size()) { - // we're done with this sound object - so set our pointer back to NULL - // and our sent bytes back to zero - _avatarSound.clear(); - _numAvatarSoundSentBytes = 0; + // check if the all of the _numAvatarAudioBufferSamples to be sent are silence + for (int i = 0; i < numAvailableSamples; ++i) { + if (nextSoundOutput[i] != 0) { + silentFrame = false; + break; } } - auto audioPacket = NLPacket::create(silentFrame + _numAvatarSoundSentBytes += numAvailableBytes; + if (_numAvatarSoundSentBytes == soundByteArray.size()) { + // we're done with this sound object - so set our pointer back to NULL + // and our sent bytes back to zero + _avatarSound.clear(); + _numAvatarSoundSentBytes = 0; + } + } + + auto audioPacket = NLPacket::create(silentFrame ? PacketType::SilentAudioFrame : PacketType::MicrophoneAudioNoEcho); - // seek past the sequence number, will be packed when destination node is known - audioPacket->seek(sizeof(quint16)); - - if (silentFrame) { - if (!_isListeningToAudioStream) { - // if we have a silent frame and we're not listening then just send nothing and break out of here - return; - } - - // write the number of silent samples so the audio-mixer can uphold timing - audioPacket->writePrimitive(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - - // use the orientation and position of this avatar for the source of this audio - audioPacket->writePrimitive(scriptedAvatar->getPosition()); - glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); - audioPacket->writePrimitive(headOrientation); - - } else if (nextSoundOutput) { - // write the codec - audioPacket->writeString(_selectedCodecName); - - // assume scripted avatar audio is mono and set channel flag to zero - audioPacket->writePrimitive((quint8)0); - - // use the orientation and position of this avatar for the source of this audio - audioPacket->writePrimitive(scriptedAvatar->getPosition()); - glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); - audioPacket->writePrimitive(headOrientation); - - // encode it - if(_encoder) { - QByteArray decodedBuffer(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); - QByteArray encodedBuffer; - _encoder->encode(decodedBuffer, encodedBuffer); - audioPacket->write(encodedBuffer.data(), encodedBuffer.size()); - } else { - audioPacket->write(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); - } + // seek past the sequence number, will be packed when destination node is known + audioPacket->seek(sizeof(quint16)); + if (silentFrame) { + if (!_isListeningToAudioStream) { + // if we have a silent frame and we're not listening then just send nothing and break out of here + return; } - // write audio packet to AudioMixer nodes - auto nodeList = DependencyManager::get(); - nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node){ - // only send to nodes of type AudioMixer - if (node->getType() == NodeType::AudioMixer) { - // pack sequence number - quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++; - audioPacket->seek(0); - audioPacket->writePrimitive(sequence); + // write the number of silent samples so the audio-mixer can uphold timing + audioPacket->writePrimitive(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + + // use the orientation and position of this avatar for the source of this audio + audioPacket->writePrimitive(scriptedAvatar->getPosition()); + glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); + audioPacket->writePrimitive(headOrientation); + + } else if (nextSoundOutput) { + + // write the codec + audioPacket->writeString(_selectedCodecName); + + // assume scripted avatar audio is mono and set channel flag to zero + audioPacket->writePrimitive((quint8)0); + + // use the orientation and position of this avatar for the source of this audio + audioPacket->writePrimitive(scriptedAvatar->getPosition()); + glm::quat headOrientation = scriptedAvatar->getHeadOrientation(); + audioPacket->writePrimitive(headOrientation); + + // encode it + if(_encoder) { + QByteArray decodedBuffer(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); + QByteArray encodedBuffer; + _encoder->encode(decodedBuffer, encodedBuffer); + audioPacket->write(encodedBuffer.data(), encodedBuffer.size()); + } else { + audioPacket->write(reinterpret_cast(nextSoundOutput), numAvailableSamples*sizeof(int16_t)); + } - // send audio packet - nodeList->sendUnreliablePacket(*audioPacket, *node); - } - }); } + + // write audio packet to AudioMixer nodes + auto nodeList = DependencyManager::get(); + nodeList->eachNode([this, &nodeList, &audioPacket](const SharedNodePointer& node) { + // only send to nodes of type AudioMixer + if (node->getType() == NodeType::AudioMixer) { + // pack sequence number + quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++; + audioPacket->seek(0); + audioPacket->writePrimitive(sequence); + + // send audio packet + nodeList->sendUnreliablePacket(*audioPacket, *node); + } + }); } } diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index cbb1af2e33..67dba10de2 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -31,21 +31,6 @@ #include "MixedAudioStream.h" - -class AvatarAudioTimer : public QObject { - Q_OBJECT - -signals: - void avatarTick(); - -public slots: - void start(); - void stop() { _quit = true; } - -private: - bool _quit { false }; -}; - class Agent : public ThreadedAssignment { Q_OBJECT @@ -74,7 +59,6 @@ public: public slots: void run() override; void playAvatarSound(SharedSoundPointer avatarSound); - void processAgentAvatarAndAudio(); private slots: void requestScript(); @@ -87,6 +71,9 @@ private slots: void handleSelectedAudioFormat(QSharedPointer message); void nodeActivated(SharedNodePointer activatedNode); + + void processAgentAvatar(); + void processAgentAvatarAudio(); signals: void startAvatarAudioTimer(); diff --git a/assignment-client/src/AvatarAudioTimer.cpp b/assignment-client/src/AvatarAudioTimer.cpp new file mode 100644 index 0000000000..857209df7c --- /dev/null +++ b/assignment-client/src/AvatarAudioTimer.cpp @@ -0,0 +1,33 @@ +// +// AvatarAudioTimer.cpp +// assignment-client/src +// +// Created by David Kelly on 10/12/13. +// Copyright 2016 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 +#include +#include "AvatarAudioTimer.h" + +// this should send a signal every 10ms, with pretty good precision. Hardcoding +// to 10ms since that's what you'd want for audio. +void AvatarAudioTimer::start() { + qDebug() << "AvatarAudioTimer::start called"; + auto startTime = usecTimestampNow(); + quint64 frameCounter = 0; + const int TARGET_INTERVAL_USEC = 10000; // 10ms + while (!_quit) { + frameCounter++; + // simplest possible timer + quint64 targetTime = startTime + frameCounter * TARGET_INTERVAL_USEC; + quint64 interval = std::max((quint64)0, targetTime - usecTimestampNow()); + usleep(interval); + emit avatarTick(); + } + qDebug() << "AvatarAudioTimer is finished"; +} + + diff --git a/assignment-client/src/AvatarAudioTimer.h b/assignment-client/src/AvatarAudioTimer.h new file mode 100644 index 0000000000..1f6381b030 --- /dev/null +++ b/assignment-client/src/AvatarAudioTimer.h @@ -0,0 +1,31 @@ +// +// AvatarAudioTimer.h +// assignment-client/src +// +// Created by David Kelly on 10/12/13. +// Copyright 2016 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_AvatarAudioTimer_h +#define hifi_AvatarAudioTimer_h + +#include + +class AvatarAudioTimer : public QObject { + Q_OBJECT + +signals: + void avatarTick(); + +public slots: + void start(); + void stop() { _quit = true; } + +private: + bool _quit { false }; +}; + +#endif //hifi_AvatarAudioTimer_h