diff --git a/assignment-client/src/Agent.cpp b/assignment-client/src/Agent.cpp index e6c14d06da..5720ecaaf5 100644 --- a/assignment-client/src/Agent.cpp +++ b/assignment-client/src/Agent.cpp @@ -147,6 +147,15 @@ void Agent::readPendingDatagrams() { } } else if (datagramPacketType == PacketTypeMixedAudio) { + + QUuid senderUUID = uuidFromPacketHeader(receivedPacket); + + // parse sequence number for this packet + int numBytesPacketHeader = numBytesForPacketHeader(receivedPacket); + const char* sequenceAt = receivedPacket.constData() + numBytesPacketHeader; + quint16 sequence = *(reinterpret_cast(sequenceAt)); + _incomingMixedAudioSequenceNumberStats.sequenceNumberReceived(sequence, senderUUID); + // parse the data and grab the average loudness _receivedAudioBuffer.parseData(receivedPacket); diff --git a/assignment-client/src/Agent.h b/assignment-client/src/Agent.h index 9af95e757c..ec8f7c88cb 100644 --- a/assignment-client/src/Agent.h +++ b/assignment-client/src/Agent.h @@ -71,6 +71,8 @@ private: ModelTreeHeadlessViewer _modelViewer; MixedAudioRingBuffer _receivedAudioBuffer; + SequenceNumberStats _incomingMixedAudioSequenceNumberStats; + AvatarHashMap _avatarHashMap; }; diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 407a4413d5..663aef81a7 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -78,7 +78,8 @@ AudioMixer::AudioMixer(const QByteArray& packet) : _sumListeners(0), _sumMixes(0), _sourceUnattenuatedZone(NULL), - _listenerUnattenuatedZone(NULL) + _listenerUnattenuatedZone(NULL), + _lastSendAudioStreamStatsTime(usecTimestampNow()) { } @@ -451,7 +452,7 @@ void AudioMixer::sendStatsPacket() { AudioMixerClientData* clientData = static_cast(node->getLinkedData()); if (clientData) { QString property = "jitterStats." + node->getUUID().toString(); - QString value = clientData->getJitterBufferStats(); + QString value = clientData->getAudioStreamStatsString(); statsObject2[qPrintable(property)] = value; somethingToSend = true; sizeOfStats += property.size() + value.size(); @@ -562,7 +563,7 @@ void AudioMixer::run() { QElapsedTimer timer; timer.start(); - char* clientMixBuffer = new char[NETWORK_BUFFER_LENGTH_BYTES_STEREO + char* clientMixBuffer = new char[NETWORK_BUFFER_LENGTH_BYTES_STEREO + sizeof(quint16) + numBytesForPacketHeaderGivenPacketType(PacketTypeMixedAudio)]; int usecToSleep = BUFFER_SEND_INTERVAL_USECS; @@ -631,20 +632,50 @@ void AudioMixer::run() { ++framesSinceCutoffEvent; } + + const quint64 TOO_LONG_SINCE_LAST_SEND_AUDIO_STREAM_STATS = 1 * USECS_PER_SECOND; + + bool sendAudioStreamStats = false; + quint64 now = usecTimestampNow(); + if (now - _lastSendAudioStreamStatsTime > TOO_LONG_SINCE_LAST_SEND_AUDIO_STREAM_STATS) { + _lastSendAudioStreamStatsTime = now; + sendAudioStreamStats = true; + } + foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { if (node->getType() == NodeType::Agent && node->getActiveSocket() && node->getLinkedData() && ((AudioMixerClientData*) node->getLinkedData())->getAvatarAudioRingBuffer()) { + + AudioMixerClientData* nodeData = (AudioMixerClientData*)node->getLinkedData(); + prepareMixForListeningNode(node.data()); + // pack header int numBytesPacketHeader = populatePacketHeader(clientMixBuffer, PacketTypeMixedAudio); + char* dataAt = clientMixBuffer + numBytesPacketHeader; - memcpy(clientMixBuffer + numBytesPacketHeader, _clientSamples, NETWORK_BUFFER_LENGTH_BYTES_STEREO); - nodeList->writeDatagram(clientMixBuffer, NETWORK_BUFFER_LENGTH_BYTES_STEREO + numBytesPacketHeader, node); + // pack sequence number + quint16 sequence = nodeData->getOutgoingSequenceNumber(); + memcpy(dataAt, &sequence, sizeof(quint16)); + dataAt += sizeof(quint16); + + // pack mixed audio samples + memcpy(dataAt, _clientSamples, NETWORK_BUFFER_LENGTH_BYTES_STEREO); + dataAt += NETWORK_BUFFER_LENGTH_BYTES_STEREO; + + // send mixed audio packet + nodeList->writeDatagram(clientMixBuffer, dataAt - clientMixBuffer, node); + nodeData->incrementOutgoingMixedAudioSequenceNumber(); + // send an audio stream stats packet if it's time + if (sendAudioStreamStats) { + nodeData->sendAudioStreamStatsPackets(node); + } + ++_sumListeners; } } - + // push forward the next output pointers for any audio buffers we used foreach (const SharedNodePointer& node, nodeList->getNodeHash()) { if (node->getLinkedData()) { diff --git a/assignment-client/src/audio/AudioMixer.h b/assignment-client/src/audio/AudioMixer.h index 8560978689..2c94f32edc 100644 --- a/assignment-client/src/audio/AudioMixer.h +++ b/assignment-client/src/audio/AudioMixer.h @@ -58,6 +58,8 @@ private: AABox* _sourceUnattenuatedZone; AABox* _listenerUnattenuatedZone; static bool _useDynamicJitterBuffers; + + quint64 _lastSendAudioStreamStatsTime; }; #endif // hifi_AudioMixer_h diff --git a/assignment-client/src/audio/AudioMixerClientData.cpp b/assignment-client/src/audio/AudioMixerClientData.cpp index 395cebf9c2..9b14ecfd19 100644 --- a/assignment-client/src/audio/AudioMixerClientData.cpp +++ b/assignment-client/src/audio/AudioMixerClientData.cpp @@ -20,7 +20,9 @@ #include "AudioMixerClientData.h" AudioMixerClientData::AudioMixerClientData() : - _ringBuffers() + _ringBuffers(), + _outgoingMixedAudioSequenceNumber(0), + _incomingAvatarAudioSequenceNumberStats() { } @@ -44,16 +46,24 @@ AvatarAudioRingBuffer* AudioMixerClientData::getAvatarAudioRingBuffer() const { } int AudioMixerClientData::parseData(const QByteArray& packet) { + + // parse sequence number for this packet + int numBytesPacketHeader = numBytesForPacketHeader(packet); + const char* sequenceAt = packet.constData() + numBytesPacketHeader; + quint16 sequence = *(reinterpret_cast(sequenceAt)); + PacketType packetType = packetTypeForPacket(packet); if (packetType == PacketTypeMicrophoneAudioWithEcho || packetType == PacketTypeMicrophoneAudioNoEcho || packetType == PacketTypeSilentAudioFrame) { + _incomingAvatarAudioSequenceNumberStats.sequenceNumberReceived(sequence); + // grab the AvatarAudioRingBuffer from the vector (or create it if it doesn't exist) AvatarAudioRingBuffer* avatarRingBuffer = getAvatarAudioRingBuffer(); // read the first byte after the header to see if this is a stereo or mono buffer - quint8 channelFlag = packet.at(numBytesForPacketHeader(packet)); + quint8 channelFlag = packet.at(numBytesForPacketHeader(packet) + sizeof(quint16)); bool isStereo = channelFlag == 1; if (avatarRingBuffer && avatarRingBuffer->isStereo() != isStereo) { @@ -76,7 +86,9 @@ int AudioMixerClientData::parseData(const QByteArray& packet) { // this is injected audio // grab the stream identifier for this injected audio - QUuid streamIdentifier = QUuid::fromRfc4122(packet.mid(numBytesForPacketHeader(packet), NUM_BYTES_RFC4122_UUID)); + QUuid streamIdentifier = QUuid::fromRfc4122(packet.mid(numBytesForPacketHeader(packet) + sizeof(quint16), NUM_BYTES_RFC4122_UUID)); + + _incomingInjectedAudioSequenceNumberStatsMap[streamIdentifier].sequenceNumberReceived(sequence); InjectedAudioRingBuffer* matchingInjectedRingBuffer = NULL; @@ -133,6 +145,9 @@ void AudioMixerClientData::pushBuffersAfterFrameSend() { } else if (audioBuffer->getType() == PositionalAudioRingBuffer::Injector && audioBuffer->hasStarted() && audioBuffer->isStarved()) { // this is an empty audio buffer that has starved, safe to delete + // also delete its sequence number stats + QUuid streamIdentifier = ((InjectedAudioRingBuffer*)audioBuffer)->getStreamIdentifier(); + _incomingInjectedAudioSequenceNumberStatsMap.remove(streamIdentifier); delete audioBuffer; i = _ringBuffers.erase(i); continue; @@ -141,42 +156,123 @@ void AudioMixerClientData::pushBuffersAfterFrameSend() { } } -QString AudioMixerClientData::getJitterBufferStats() const { +AudioStreamStats AudioMixerClientData::getAudioStreamStatsOfStream(const PositionalAudioRingBuffer* ringBuffer) const { + AudioStreamStats streamStats; + SequenceNumberStats streamSequenceNumberStats; + + streamStats._streamType = ringBuffer->getType(); + if (streamStats._streamType == PositionalAudioRingBuffer::Injector) { + streamStats._streamIdentifier = ((InjectedAudioRingBuffer*)ringBuffer)->getStreamIdentifier(); + streamSequenceNumberStats = _incomingInjectedAudioSequenceNumberStatsMap.value(streamStats._streamIdentifier); + } else { + streamSequenceNumberStats = _incomingAvatarAudioSequenceNumberStats; + } + streamStats._jitterBufferFrames = ringBuffer->getCurrentJitterBufferFrames(); + + streamStats._packetsReceived = streamSequenceNumberStats.getNumReceived(); + streamStats._packetsUnreasonable = streamSequenceNumberStats.getNumUnreasonable(); + streamStats._packetsEarly = streamSequenceNumberStats.getNumEarly(); + streamStats._packetsLate = streamSequenceNumberStats.getNumLate(); + streamStats._packetsLost = streamSequenceNumberStats.getNumLost(); + streamStats._packetsRecovered = streamSequenceNumberStats.getNumRecovered(); + streamStats._packetsDuplicate = streamSequenceNumberStats.getNumDuplicate(); + + return streamStats; +} + +void AudioMixerClientData::sendAudioStreamStatsPackets(const SharedNodePointer& destinationNode) const { + + char packet[MAX_PACKET_SIZE]; + NodeList* nodeList = NodeList::getInstance(); + + // The append flag is a boolean value that will be packed right after the header. The first packet sent + // inside this method will have 0 for this flag, while every subsequent packet will have 1 for this flag. + // The sole purpose of this flag is so the client can clear its map of injected audio stream stats when + // it receives a packet with an appendFlag of 0. This prevents the buildup of dead audio stream stats in the client. + quint8 appendFlag = 0; + + // pack header + int numBytesPacketHeader = populatePacketHeader(packet, PacketTypeAudioStreamStats); + char* headerEndAt = packet + numBytesPacketHeader; + + // calculate how many stream stat structs we can fit in each packet + const int numStreamStatsRoomFor = (MAX_PACKET_SIZE - numBytesPacketHeader - sizeof(quint8) - sizeof(quint16)) / sizeof(AudioStreamStats); + + // pack and send stream stats packets until all ring buffers' stats are sent + int numStreamStatsRemaining = _ringBuffers.size(); + QList::ConstIterator ringBuffersIterator = _ringBuffers.constBegin(); + while (numStreamStatsRemaining > 0) { + + char* dataAt = headerEndAt; + + // pack the append flag + memcpy(dataAt, &appendFlag, sizeof(quint8)); + appendFlag = 1; + dataAt += sizeof(quint8); + + // calculate and pack the number of stream stats to follow + quint16 numStreamStatsToPack = std::min(numStreamStatsRemaining, numStreamStatsRoomFor); + memcpy(dataAt, &numStreamStatsToPack, sizeof(quint16)); + dataAt += sizeof(quint16); + + // pack the calculated number of stream stats + for (int i = 0; i < numStreamStatsToPack; i++) { + AudioStreamStats streamStats = getAudioStreamStatsOfStream(*ringBuffersIterator); + memcpy(dataAt, &streamStats, sizeof(AudioStreamStats)); + dataAt += sizeof(AudioStreamStats); + + ringBuffersIterator++; + } + numStreamStatsRemaining -= numStreamStatsToPack; + + // send the current packet + nodeList->writeDatagram(packet, dataAt - packet, destinationNode); + } +} + +QString AudioMixerClientData::getAudioStreamStatsString() const { QString result; AvatarAudioRingBuffer* avatarRingBuffer = getAvatarAudioRingBuffer(); if (avatarRingBuffer) { int desiredJitterBuffer = avatarRingBuffer->getDesiredJitterBufferFrames(); int calculatedJitterBuffer = avatarRingBuffer->getCalculatedDesiredJitterBufferFrames(); int currentJitterBuffer = avatarRingBuffer->getCurrentJitterBufferFrames(); - int resetCount = avatarRingBuffer->getResetCount(); + int overflowCount = avatarRingBuffer->getOverflowCount(); int samplesAvailable = avatarRingBuffer->samplesAvailable(); int framesAvailable = (samplesAvailable / avatarRingBuffer->getSamplesPerFrame()); - result += "mic.desired:" + QString::number(desiredJitterBuffer) - + " calculated:" + QString::number(calculatedJitterBuffer) - + " current:" + QString::number(currentJitterBuffer) - + " available:" + QString::number(framesAvailable) - + " samples:" + QString::number(samplesAvailable) - + " resets:" + QString::number(resetCount); + AudioStreamStats streamStats = getAudioStreamStatsOfStream(avatarRingBuffer); + result += "mic.desired:" + QString::number(desiredJitterBuffer) + + " calculated:" + QString::number(calculatedJitterBuffer) + + " current:" + QString::number(currentJitterBuffer) + + " available:" + QString::number(framesAvailable) + + " samples:" + QString::number(samplesAvailable) + + " overflows:" + QString::number(overflowCount) + + " early:" + QString::number(streamStats._packetsEarly) + + " late:" + QString::number(streamStats._packetsLate) + + " lost:" + QString::number(streamStats._packetsLost); } else { result = "mic unknown"; } - + for (int i = 0; i < _ringBuffers.size(); i++) { if (_ringBuffers[i]->getType() == PositionalAudioRingBuffer::Injector) { int desiredJitterBuffer = _ringBuffers[i]->getDesiredJitterBufferFrames(); int calculatedJitterBuffer = _ringBuffers[i]->getCalculatedDesiredJitterBufferFrames(); int currentJitterBuffer = _ringBuffers[i]->getCurrentJitterBufferFrames(); - int resetCount = _ringBuffers[i]->getResetCount(); + int overflowCount = _ringBuffers[i]->getOverflowCount(); int samplesAvailable = _ringBuffers[i]->samplesAvailable(); int framesAvailable = (samplesAvailable / _ringBuffers[i]->getSamplesPerFrame()); - result += "| injected["+QString::number(i)+"].desired:" + QString::number(desiredJitterBuffer) - + " calculated:" + QString::number(calculatedJitterBuffer) - + " current:" + QString::number(currentJitterBuffer) - + " available:" + QString::number(framesAvailable) - + " samples:" + QString::number(samplesAvailable) - + " resets:" + QString::number(resetCount); + AudioStreamStats streamStats = getAudioStreamStatsOfStream(_ringBuffers[i]); + result += "| injected[" + QString::number(i) + "].desired:" + QString::number(desiredJitterBuffer) + + " calculated:" + QString::number(calculatedJitterBuffer) + + " current:" + QString::number(currentJitterBuffer) + + " available:" + QString::number(framesAvailable) + + " samples:" + QString::number(samplesAvailable) + + " overflows:" + QString::number(overflowCount) + + " early:" + QString::number(streamStats._packetsEarly) + + " late:" + QString::number(streamStats._packetsLate) + + " lost:" + QString::number(streamStats._packetsLost); } } - return result; } diff --git a/assignment-client/src/audio/AudioMixerClientData.h b/assignment-client/src/audio/AudioMixerClientData.h index 1760b96f5d..65fd4b3da3 100644 --- a/assignment-client/src/audio/AudioMixerClientData.h +++ b/assignment-client/src/audio/AudioMixerClientData.h @@ -17,6 +17,8 @@ #include #include "AvatarAudioRingBuffer.h" +#include "AudioStreamStats.h" +#include "SequenceNumberStats.h" class AudioMixerClientData : public NodeData { public: @@ -30,10 +32,20 @@ public: void checkBuffersBeforeFrameSend(AABox* checkSourceZone = NULL, AABox* listenerZone = NULL); void pushBuffersAfterFrameSend(); - QString getJitterBufferStats() const; + AudioStreamStats getAudioStreamStatsOfStream(const PositionalAudioRingBuffer* ringBuffer) const; + QString getAudioStreamStatsString() const; + + void sendAudioStreamStatsPackets(const SharedNodePointer& destinationNode) const; + + void incrementOutgoingMixedAudioSequenceNumber() { _outgoingMixedAudioSequenceNumber++; } + quint16 getOutgoingSequenceNumber() const { return _outgoingMixedAudioSequenceNumber; } private: QList _ringBuffers; + + quint16 _outgoingMixedAudioSequenceNumber; + SequenceNumberStats _incomingAvatarAudioSequenceNumberStats; + QHash _incomingInjectedAudioSequenceNumberStatsMap; }; #endif // hifi_AudioMixerClientData_h diff --git a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp index a228b87491..3d2ca6ddc3 100644 --- a/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp +++ b/assignment-client/src/octree/OctreeInboundPacketProcessor.cpp @@ -216,7 +216,7 @@ int OctreeInboundPacketProcessor::sendNackPackets() { } const SharedNodePointer& destinationNode = NodeList::getInstance()->getNodeHash().value(nodeUUID); - const QSet& missingSequenceNumbers = nodeStats.getMissingSequenceNumbers(); + const QSet& missingSequenceNumbers = nodeStats.getIncomingEditSequenceNumberStats().getMissingSet(); // construct nack packet(s) for this node int numSequenceNumbersAvailable = missingSequenceNumbers.size(); @@ -266,8 +266,7 @@ SingleSenderStats::SingleSenderStats() _totalLockWaitTime(0), _totalElementsInPacket(0), _totalPackets(0), - _incomingLastSequence(0), - _missingSequenceNumbers() + _incomingEditSequenceNumberStats() { } @@ -275,74 +274,8 @@ SingleSenderStats::SingleSenderStats() void SingleSenderStats::trackInboundPacket(unsigned short int incomingSequence, quint64 transitTime, int editsInPacket, quint64 processTime, quint64 lockWaitTime) { - const int UINT16_RANGE = std::numeric_limits::max() + 1; - const int MAX_REASONABLE_SEQUENCE_GAP = 1000; // this must be less than UINT16_RANGE / 2 for rollover handling to work - const int MAX_MISSING_SEQUENCE_SIZE = 100; - - unsigned short int expectedSequence = _totalPackets == 0 ? incomingSequence : _incomingLastSequence + (unsigned short int)1; - - if (incomingSequence == expectedSequence) { // on time - _incomingLastSequence = incomingSequence; - } else { // out of order - int incoming = (int)incomingSequence; - int expected = (int)expectedSequence; - - // check if the gap between incoming and expected is reasonable, taking possible rollover into consideration - int absGap = std::abs(incoming - expected); - if (absGap >= UINT16_RANGE - MAX_REASONABLE_SEQUENCE_GAP) { - // rollover likely occurred between incoming and expected. - // correct the larger of the two so that it's within [-UINT16_RANGE, -1] while the other remains within [0, UINT16_RANGE-1] - if (incoming > expected) { - incoming -= UINT16_RANGE; - } else { - expected -= UINT16_RANGE; - } - } else if (absGap > MAX_REASONABLE_SEQUENCE_GAP) { - // ignore packet if gap is unreasonable - qDebug() << "ignoring unreasonable packet... sequence:" << incomingSequence - << "_incomingLastSequence:" << _incomingLastSequence; - return; - } - - // now that rollover has been corrected for (if it occurred), incoming and expected can be - // compared to each other directly, though one of them might be negative - if (incoming > expected) { // early - // add all sequence numbers that were skipped to the missing sequence numbers list - for (int missingSequence = expected; missingSequence < incoming; missingSequence++) { - _missingSequenceNumbers.insert(missingSequence < 0 ? missingSequence + UINT16_RANGE : missingSequence); - } - _incomingLastSequence = incomingSequence; - } else { // late - // remove this from missing sequence number if it's in there - _missingSequenceNumbers.remove(incomingSequence); - - // do not update _incomingLastSequence; it shouldn't become smaller - } - } - - // prune missing sequence list if it gets too big; sequence numbers that are older than MAX_REASONABLE_SEQUENCE_GAP - // will be removed. - if (_missingSequenceNumbers.size() > MAX_MISSING_SEQUENCE_SIZE) { - // some older sequence numbers may be from before a rollover point; this must be handled. - // some sequence numbers in this list may be larger than _incomingLastSequence, indicating that they were received - // before the most recent rollover. - int cutoff = (int)_incomingLastSequence - MAX_REASONABLE_SEQUENCE_GAP; - if (cutoff >= 0) { - foreach(unsigned short int missingSequence, _missingSequenceNumbers) { - unsigned short int nonRolloverCutoff = (unsigned short int)cutoff; - if (missingSequence > _incomingLastSequence || missingSequence <= nonRolloverCutoff) { - _missingSequenceNumbers.remove(missingSequence); - } - } - } else { - unsigned short int rolloverCutoff = (unsigned short int)(cutoff + UINT16_RANGE); - foreach(unsigned short int missingSequence, _missingSequenceNumbers) { - if (missingSequence > _incomingLastSequence && missingSequence <= rolloverCutoff) { - _missingSequenceNumbers.remove(missingSequence); - } - } - } - } + // track sequence number + _incomingEditSequenceNumberStats.sequenceNumberReceived(incomingSequence); // update other stats _totalTransitTime += transitTime; diff --git a/assignment-client/src/octree/OctreeInboundPacketProcessor.h b/assignment-client/src/octree/OctreeInboundPacketProcessor.h index e09696a5fd..960282384b 100644 --- a/assignment-client/src/octree/OctreeInboundPacketProcessor.h +++ b/assignment-client/src/octree/OctreeInboundPacketProcessor.h @@ -14,9 +14,10 @@ #ifndef hifi_OctreeInboundPacketProcessor_h #define hifi_OctreeInboundPacketProcessor_h -#include - #include + +#include "SequenceNumberStats.h" + class OctreeServer; class SingleSenderStats { @@ -32,7 +33,8 @@ public: { return _totalElementsInPacket == 0 ? 0 : _totalProcessTime / _totalElementsInPacket; } quint64 getAverageLockWaitTimePerElement() const { return _totalElementsInPacket == 0 ? 0 : _totalLockWaitTime / _totalElementsInPacket; } - const QSet& getMissingSequenceNumbers() const { return _missingSequenceNumbers; } + + const SequenceNumberStats& getIncomingEditSequenceNumberStats() const { return _incomingEditSequenceNumberStats; } void trackInboundPacket(unsigned short int incomingSequence, quint64 transitTime, int editsInPacket, quint64 processTime, quint64 lockWaitTime); @@ -42,9 +44,7 @@ public: quint64 _totalLockWaitTime; quint64 _totalElementsInPacket; quint64 _totalPackets; - - unsigned short int _incomingLastSequence; - QSet _missingSequenceNumbers; + SequenceNumberStats _incomingEditSequenceNumberStats; }; typedef QHash NodeToSenderStatsMap; diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index f9d2d635c5..72ac7ba62a 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -2182,7 +2182,8 @@ int Application::sendNackPackets() { OctreeSceneStats& stats = _octreeServerSceneStats[nodeUUID]; // make copy of missing sequence numbers from stats - const QSet missingSequenceNumbers = stats.getMissingSequenceNumbers(); + const QSet missingSequenceNumbers = + stats.getIncomingOctreeSequenceNumberStats().getMissingSet(); _octreeSceneStatsLock.unlock(); @@ -3304,6 +3305,10 @@ void Application::nodeKilled(SharedNodePointer node) { _particleEditSender.nodeKilled(node); _modelEditSender.nodeKilled(node); + if (node->getType() == NodeType::AudioMixer) { + QMetaObject::invokeMethod(&_audio, "resetIncomingMixedAudioSequenceNumberStats"); + } + if (node->getType() == NodeType::VoxelServer) { QUuid nodeUUID = node->getUUID(); // see if this is the first we've heard of this node... @@ -3627,6 +3632,9 @@ ScriptEngine* Application::loadScript(const QString& scriptName, bool loadScript // when the application is about to quit, stop our script engine so it unwinds properly connect(this, SIGNAL(aboutToQuit()), scriptEngine, SLOT(stop())); + NodeList* nodeList = NodeList::getInstance(); + connect(nodeList, &NodeList::nodeKilled, scriptEngine, &ScriptEngine::nodeKilled); + scriptEngine->moveToThread(workerThread); // Starts an event loop, and emits workerThread->started() diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index 65912f83e8..f5f148969c 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -102,7 +102,9 @@ Audio::Audio(int16_t initialJitterBufferSamples, QObject* parent) : _samplesPerScope(NETWORK_SAMPLES_PER_FRAME * _framesPerScope), _scopeInput(0), _scopeOutputLeft(0), - _scopeOutputRight(0) + _scopeOutputRight(0), + _audioMixerAvatarStreamStats(), + _outgoingAvatarAudioSequenceNumber(0) { // clear the array of locally injected samples memset(_localProceduralSamples, 0, NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL); @@ -118,6 +120,9 @@ void Audio::init(QGLWidget *parent) { void Audio::reset() { _ringBuffer.reset(); + _outgoingAvatarAudioSequenceNumber = 0; + _audioMixerInjectedStreamStatsMap.clear(); + _incomingMixedAudioSequenceNumberStats.reset(); } QAudioDeviceInfo getNamedAudioDeviceForMode(QAudio::Mode mode, const QString& deviceName) { @@ -421,7 +426,7 @@ void Audio::handleAudioInput() { static char audioDataPacket[MAX_PACKET_SIZE]; static int numBytesPacketHeader = numBytesForPacketHeaderGivenPacketType(PacketTypeMicrophoneAudioNoEcho); - static int leadingBytes = numBytesPacketHeader + sizeof(glm::vec3) + sizeof(glm::quat) + sizeof(quint8); + static int leadingBytes = numBytesPacketHeader + sizeof(quint16) + sizeof(glm::vec3) + sizeof(glm::quat) + sizeof(quint8); static int16_t* networkAudioSamples = (int16_t*) (audioDataPacket + leadingBytes); @@ -653,6 +658,10 @@ void Audio::handleAudioInput() { char* currentPacketPtr = audioDataPacket + populatePacketHeader(audioDataPacket, packetType); + // pack sequence number + memcpy(currentPacketPtr, &_outgoingAvatarAudioSequenceNumber, sizeof(quint16)); + currentPacketPtr += sizeof(quint16); + // set the mono/stereo byte *currentPacketPtr++ = isStereo; @@ -665,6 +674,7 @@ void Audio::handleAudioInput() { currentPacketPtr += sizeof(headOrientation); nodeList->writeDatagram(audioDataPacket, numAudioBytes + leadingBytes, audioMixer); + _outgoingAvatarAudioSequenceNumber++; Application::getInstance()->getBandwidthMeter()->outputStream(BandwidthMeter::AUDIO) .updateValue(numAudioBytes + leadingBytes); @@ -707,6 +717,36 @@ void Audio::addReceivedAudioToBuffer(const QByteArray& audioByteArray) { Application::getInstance()->getBandwidthMeter()->inputStream(BandwidthMeter::AUDIO).updateValue(audioByteArray.size()); } +void Audio::parseAudioStreamStatsPacket(const QByteArray& packet) { + + int numBytesPacketHeader = numBytesForPacketHeader(packet); + const char* dataAt = packet.constData() + numBytesPacketHeader; + + // parse the appendFlag, clear injected audio stream stats if 0 + quint8 appendFlag = *(reinterpret_cast(dataAt)); + dataAt += sizeof(quint8); + if (!appendFlag) { + _audioMixerInjectedStreamStatsMap.clear(); + } + + // parse the number of stream stats structs to follow + quint16 numStreamStats = *(reinterpret_cast(dataAt)); + dataAt += sizeof(quint16); + + // parse the stream stats + AudioStreamStats streamStats; + for (quint16 i = 0; i < numStreamStats; i++) { + memcpy(&streamStats, dataAt, sizeof(AudioStreamStats)); + dataAt += sizeof(AudioStreamStats); + + if (streamStats._streamType == PositionalAudioRingBuffer::Microphone) { + _audioMixerAvatarStreamStats = streamStats; + } else { + _audioMixerInjectedStreamStatsMap[streamStats._streamIdentifier] = streamStats; + } + } +} + // NOTE: numSamples is the total number of single channel samples, since callers will always call this with stereo // data we know that we will have 2x samples for each stereo time sample at the format's sample rate void Audio::addSpatialAudioToBuffer(unsigned int sampleTime, const QByteArray& spatialAudio, unsigned int numSamples) { @@ -806,6 +846,16 @@ void Audio::toggleStereoInput() { } void Audio::processReceivedAudio(const QByteArray& audioByteArray) { + + QUuid senderUUID = uuidFromPacketHeader(audioByteArray); + + // parse sequence number for this packet + int numBytesPacketHeader = numBytesForPacketHeader(audioByteArray); + const char* sequenceAt = audioByteArray.constData() + numBytesPacketHeader; + quint16 sequence = *((quint16*)sequenceAt); + _incomingMixedAudioSequenceNumberStats.sequenceNumberReceived(sequence, senderUUID); + + // parse audio data _ringBuffer.parseData(audioByteArray); float networkOutputToOutputRatio = (_desiredOutputFormat.sampleRate() / (float) _outputFormat.sampleRate()) @@ -828,7 +878,8 @@ void Audio::processReceivedAudio(const QByteArray& audioByteArray) { QByteArray outputBuffer; outputBuffer.resize(numDeviceOutputSamples * sizeof(int16_t)); - int numSamplesNeededToStartPlayback = NETWORK_BUFFER_LENGTH_SAMPLES_STEREO + (_jitterBufferSamples * 2); + int numSamplesNeededToStartPlayback = std::min(NETWORK_BUFFER_LENGTH_SAMPLES_STEREO + (_jitterBufferSamples * 2), + _ringBuffer.getSampleCapacity()); if (!_ringBuffer.isNotStarvedOrHasMinimumSamples(numSamplesNeededToStartPlayback)) { // We are still waiting for enough samples to begin playback @@ -845,6 +896,7 @@ void Audio::processReceivedAudio(const QByteArray& audioByteArray) { buffer.resize(numNetworkOutputSamples * sizeof(int16_t)); _ringBuffer.readSamples((int16_t*)buffer.data(), numNetworkOutputSamples); + // Accumulate direct transmission of audio from sender to receiver if (Menu::getInstance()->isOptionChecked(MenuOption::AudioSpatialProcessingIncludeOriginal)) { emit preProcessOriginalInboundAudio(sampleTime, buffer, _desiredOutputFormat); diff --git a/interface/src/Audio.h b/interface/src/Audio.h index 74fc373cb0..9f04e5cb03 100644 --- a/interface/src/Audio.h +++ b/interface/src/Audio.h @@ -16,6 +16,7 @@ #include #include "InterfaceConfig.h" +#include "AudioStreamStats.h" #include #include @@ -72,13 +73,17 @@ public: bool getProcessSpatialAudio() const { return _processSpatialAudio; } + const SequenceNumberStats& getIncomingMixedAudioSequenceNumberStats() const { return _incomingMixedAudioSequenceNumberStats; } + public slots: void start(); void stop(); void addReceivedAudioToBuffer(const QByteArray& audioByteArray); + void parseAudioStreamStatsPacket(const QByteArray& packet); void addSpatialAudioToBuffer(unsigned int sampleTime, const QByteArray& spatialAudio, unsigned int numSamples); void handleAudioInput(); void reset(); + void resetIncomingMixedAudioSequenceNumberStats() { _incomingMixedAudioSequenceNumberStats.reset(); } void toggleMute(); void toggleAudioNoiseReduction(); void toggleToneInjection(); @@ -102,6 +107,9 @@ public slots: float getInputVolume() const { return (_audioInput) ? _audioInput->volume() : 0.0f; } void setInputVolume(float volume) { if (_audioInput) _audioInput->setVolume(volume); } + const AudioStreamStats& getAudioMixerAvatarStreamStats() const { return _audioMixerAvatarStreamStats; } + const QHash& getAudioMixerInjectedStreamStatsMap() const { return _audioMixerInjectedStreamStatsMap; } + signals: bool muteToggled(); void preProcessOriginalInboundAudio(unsigned int sampleTime, QByteArray& samples, const QAudioFormat& format); @@ -233,6 +241,11 @@ private: QByteArray* _scopeOutputLeft; QByteArray* _scopeOutputRight; + AudioStreamStats _audioMixerAvatarStreamStats; + QHash _audioMixerInjectedStreamStatsMap; + + quint16 _outgoingAvatarAudioSequenceNumber; + SequenceNumberStats _incomingMixedAudioSequenceNumberStats; }; diff --git a/interface/src/DatagramProcessor.cpp b/interface/src/DatagramProcessor.cpp index 29528da126..a159af7be5 100644 --- a/interface/src/DatagramProcessor.cpp +++ b/interface/src/DatagramProcessor.cpp @@ -51,7 +51,10 @@ void DatagramProcessor::processDatagrams() { QMetaObject::invokeMethod(&application->_audio, "addReceivedAudioToBuffer", Qt::QueuedConnection, Q_ARG(QByteArray, incomingPacket)); break; - + case PacketTypeAudioStreamStats: + QMetaObject::invokeMethod(&application->_audio, "parseAudioStreamStatsPacket", Qt::QueuedConnection, + Q_ARG(QByteArray, incomingPacket)); + break; case PacketTypeParticleAddResponse: // this will keep creatorTokenIDs to IDs mapped correctly Particle::handleAddParticleResponse(incomingPacket); diff --git a/interface/src/ui/OctreeStatsDialog.cpp b/interface/src/ui/OctreeStatsDialog.cpp index 3296d8ccb2..afa799815f 100644 --- a/interface/src/ui/OctreeStatsDialog.cpp +++ b/interface/src/ui/OctreeStatsDialog.cpp @@ -365,13 +365,14 @@ void OctreeStatsDialog::showOctreeServersOfType(int& serverCount, NodeType_t ser QString incomingPacketsString = locale.toString((uint)stats.getIncomingPackets()); QString incomingBytesString = locale.toString((uint)stats.getIncomingBytes()); QString incomingWastedBytesString = locale.toString((uint)stats.getIncomingWastedBytes()); - QString incomingOutOfOrderString = locale.toString((uint)stats.getIncomingOutOfOrder()); - QString incomingLateString = locale.toString((uint)stats.getIncomingLate()); - QString incomingReallyLateString = locale.toString((uint)stats.getIncomingReallyLate()); - QString incomingEarlyString = locale.toString((uint)stats.getIncomingEarly()); - QString incomingLikelyLostString = locale.toString((uint)stats.getIncomingLikelyLost()); - QString incomingRecovered = locale.toString((uint)stats.getIncomingRecovered()); - QString incomingDuplicateString = locale.toString((uint)stats.getIncomingPossibleDuplicate()); + const SequenceNumberStats& seqStats = stats.getIncomingOctreeSequenceNumberStats(); + QString incomingOutOfOrderString = locale.toString((uint)seqStats.getNumOutOfOrder()); + QString incomingLateString = locale.toString((uint)seqStats.getNumLate()); + QString incomingUnreasonableString = locale.toString((uint)seqStats.getNumUnreasonable()); + QString incomingEarlyString = locale.toString((uint)seqStats.getNumEarly()); + QString incomingLikelyLostString = locale.toString((uint)seqStats.getNumLost()); + QString incomingRecovered = locale.toString((uint)seqStats.getNumRecovered()); + QString incomingDuplicateString = locale.toString((uint)seqStats.getNumDuplicate()); int clockSkewInMS = node->getClockSkewUsec() / (int)USECS_PER_MSEC; QString incomingFlightTimeString = locale.toString((int)stats.getIncomingFlightTimeAverage()); @@ -385,7 +386,7 @@ void OctreeStatsDialog::showOctreeServersOfType(int& serverCount, NodeType_t ser serverDetails << "
" << " Out of Order: " << qPrintable(incomingOutOfOrderString) << "/ Early: " << qPrintable(incomingEarlyString) << "/ Late: " << qPrintable(incomingLateString) << - "/ Really Late: " << qPrintable(incomingReallyLateString) << + "/ Unreasonable: " << qPrintable(incomingUnreasonableString) << "/ Duplicate: " << qPrintable(incomingDuplicateString); serverDetails << "
" << diff --git a/interface/src/ui/Stats.cpp b/interface/src/ui/Stats.cpp index fa62ecdb9b..379dd35df7 100644 --- a/interface/src/ui/Stats.cpp +++ b/interface/src/ui/Stats.cpp @@ -286,11 +286,16 @@ void Stats::display( pingVoxel = totalPingVoxel/voxelServerCount; } - lines = _expanded ? 4 : 3; + + Audio* audio = Application::getInstance()->getAudio(); + const AudioStreamStats& audioMixerAvatarStreamStats = audio->getAudioMixerAvatarStreamStats(); + const QHash& audioMixerInjectedStreamStatsMap = audio->getAudioMixerInjectedStreamStatsMap(); + + lines = _expanded ? 10 + audioMixerInjectedStreamStatsMap.size(): 3; drawBackground(backgroundColor, horizontalOffset, 0, _pingStatsWidth, lines * STATS_PELS_PER_LINE + 10); horizontalOffset += 5; - Audio* audio = Application::getInstance()->getAudio(); + char audioJitter[30]; sprintf(audioJitter, @@ -299,10 +304,9 @@ void Stats::display( (float) audio->getNetworkSampleRate() * 1000.f); drawText(30, glWidget->height() - 22, scale, rotation, font, audioJitter, color); - + char audioPing[30]; sprintf(audioPing, "Audio ping: %d", pingAudio); - char avatarPing[30]; sprintf(avatarPing, "Avatar ping: %d", pingAvatar); @@ -322,12 +326,54 @@ void Stats::display( verticalOffset += STATS_PELS_PER_LINE; drawText(horizontalOffset, verticalOffset, scale, rotation, font, voxelMaxPing, color); + + char audioMixerStatsLabelString[] = "AudioMixer stats:"; + char streamStatsFormatLabelString[] = "early/late/lost, jframes"; + + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, audioMixerStatsLabelString, color); + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, streamStatsFormatLabelString, color); + + + char downstreamLabelString[] = " Downstream:"; + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, downstreamLabelString, color); + + const SequenceNumberStats& downstreamAudioSequenceNumberStats = audio->getIncomingMixedAudioSequenceNumberStats(); + char downstreamAudioStatsString[30]; + sprintf(downstreamAudioStatsString, " mix: %d/%d/%d, %d", downstreamAudioSequenceNumberStats.getNumEarly(), + downstreamAudioSequenceNumberStats.getNumLate(), downstreamAudioSequenceNumberStats.getNumLost(), + audio->getJitterBufferSamples() / NETWORK_BUFFER_LENGTH_SAMPLES_STEREO); + + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, downstreamAudioStatsString, color); + + char upstreamLabelString[] = " Upstream:"; + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, upstreamLabelString, color); + + char upstreamAudioStatsString[30]; + sprintf(upstreamAudioStatsString, " mic: %d/%d/%d, %d", audioMixerAvatarStreamStats._packetsEarly, + audioMixerAvatarStreamStats._packetsLate, audioMixerAvatarStreamStats._packetsLost, + audioMixerAvatarStreamStats._jitterBufferFrames); + + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, upstreamAudioStatsString, color); + + foreach(AudioStreamStats injectedStreamStats, audioMixerInjectedStreamStatsMap) { + sprintf(upstreamAudioStatsString, " inj: %d/%d/%d, %d", injectedStreamStats._packetsEarly, + injectedStreamStats._packetsLate, injectedStreamStats._packetsLost, injectedStreamStats._jitterBufferFrames); + + verticalOffset += STATS_PELS_PER_LINE; + drawText(horizontalOffset, verticalOffset, scale, rotation, font, upstreamAudioStatsString, color); + } } verticalOffset = 0; horizontalOffset = _lastHorizontalOffset + _generalStatsWidth + _pingStatsWidth + 2; } - + MyAvatar* myAvatar = Application::getInstance()->getAvatar(); glm::vec3 avatarPos = myAvatar->getPosition(); diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index 129dc47bd0..e5c1230832 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -61,6 +61,11 @@ void AudioInjector::injectAudio() { QByteArray injectAudioPacket = byteArrayWithPopulatedHeader(PacketTypeInjectAudio); QDataStream packetStream(&injectAudioPacket, QIODevice::Append); + // pack some placeholder sequence number for now + int numPreSequenceNumberBytes = injectAudioPacket.size(); + packetStream << (quint16)0; + + // pack stream identifier (a generated UUID) packetStream << QUuid::createUuid(); // pack the flag for loopback @@ -91,6 +96,7 @@ void AudioInjector::injectAudio() { bool shouldLoop = _options.getLoop(); // loop to send off our audio in NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL byte chunks + quint16 outgoingInjectedAudioSequenceNumber = 0; while (currentSendPosition < soundByteArray.size() && !_shouldStop) { int bytesToCopy = std::min(NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL, @@ -98,6 +104,9 @@ void AudioInjector::injectAudio() { // resize the QByteArray to the right size injectAudioPacket.resize(numPreAudioDataBytes + bytesToCopy); + + // pack the sequence number + memcpy(injectAudioPacket.data() + numPreSequenceNumberBytes, &outgoingInjectedAudioSequenceNumber, sizeof(quint16)); // copy the next NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL bytes to the packet memcpy(injectAudioPacket.data() + numPreAudioDataBytes, soundByteArray.data() + currentSendPosition, bytesToCopy); @@ -107,6 +116,7 @@ void AudioInjector::injectAudio() { // send off this audio packet nodeList->writeDatagram(injectAudioPacket, audioMixer); + outgoingInjectedAudioSequenceNumber++; currentSendPosition += bytesToCopy; diff --git a/libraries/audio/src/AudioRingBuffer.cpp b/libraries/audio/src/AudioRingBuffer.cpp index a2d3715462..6ae3b19541 100644 --- a/libraries/audio/src/AudioRingBuffer.cpp +++ b/libraries/audio/src/AudioRingBuffer.cpp @@ -16,13 +16,14 @@ #include #include "PacketHeaders.h" - #include "AudioRingBuffer.h" + AudioRingBuffer::AudioRingBuffer(int numFrameSamples, bool randomAccessMode) : NodeData(), - _resetCount(0), + _overflowCount(0), _sampleCapacity(numFrameSamples * RING_BUFFER_LENGTH_FRAMES), + _isFull(false), _numFrameSamples(numFrameSamples), _isStarved(true), _hasStarted(false), @@ -64,8 +65,9 @@ void AudioRingBuffer::resizeForFrameSize(qint64 numFrameSamples) { } int AudioRingBuffer::parseData(const QByteArray& packet) { - int numBytesPacketHeader = numBytesForPacketHeader(packet); - return writeData(packet.data() + numBytesPacketHeader, packet.size() - numBytesPacketHeader); + // skip packet header and sequence number + int numBytesBeforeAudioData = numBytesForPacketHeader(packet) + sizeof(quint16); + return writeData(packet.data() + numBytesBeforeAudioData, packet.size() - numBytesBeforeAudioData); } qint64 AudioRingBuffer::readSamples(int16_t* destination, qint64 maxSamples) { @@ -109,6 +111,9 @@ qint64 AudioRingBuffer::readData(char *data, qint64 maxSize) { // push the position of _nextOutput by the number of samples read _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, numReadSamples); + if (numReadSamples > 0) { + _isFull = false; + } return numReadSamples * sizeof(int16_t); } @@ -120,16 +125,15 @@ qint64 AudioRingBuffer::writeSamples(const int16_t* source, qint64 maxSamples) { qint64 AudioRingBuffer::writeData(const char* data, qint64 maxSize) { // make sure we have enough bytes left for this to be the right amount of audio // otherwise we should not copy that data, and leave the buffer pointers where they are - - quint64 samplesToCopy = std::min((quint64)(maxSize / sizeof(int16_t)), (quint64)_sampleCapacity); - - if (_hasStarted && samplesToCopy > _sampleCapacity - samplesAvailable()) { - // this read will cross the next output, so call us starved and reset the buffer - qDebug() << "Filled the ring buffer. Resetting."; - _endOfLastWrite = _buffer; - _nextOutput = _buffer; - _isStarved = true; - _resetCount++; + int samplesToCopy = std::min((quint64)(maxSize / sizeof(int16_t)), (quint64)_sampleCapacity); + + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + if (samplesToCopy > samplesRoomFor) { + // there's not enough room for this write. erase old data to make room for this new data + int samplesToDelete = samplesToCopy - samplesRoomFor; + _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, samplesToDelete); + _overflowCount++; + qDebug() << "Overflowed ring buffer! Overwriting old data"; } if (_endOfLastWrite + samplesToCopy <= _buffer + _sampleCapacity) { @@ -141,7 +145,10 @@ qint64 AudioRingBuffer::writeData(const char* data, qint64 maxSize) { } _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, samplesToCopy); - + if (samplesToCopy > 0 && _endOfLastWrite == _nextOutput) { + _isFull = true; + } + return samplesToCopy * sizeof(int16_t); } @@ -154,36 +161,51 @@ const int16_t& AudioRingBuffer::operator[] (const int index) const { } void AudioRingBuffer::shiftReadPosition(unsigned int numSamples) { - _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, numSamples); + if (numSamples > 0) { + _nextOutput = shiftedPositionAccomodatingWrap(_nextOutput, numSamples); + _isFull = false; + } } unsigned int AudioRingBuffer::samplesAvailable() const { if (!_endOfLastWrite) { return 0; - } else { - int sampleDifference = _endOfLastWrite - _nextOutput; - - if (sampleDifference < 0) { - sampleDifference += _sampleCapacity; - } - - return sampleDifference; } + if (_isFull) { + return _sampleCapacity; + } + + int sampleDifference = _endOfLastWrite - _nextOutput; + if (sampleDifference < 0) { + sampleDifference += _sampleCapacity; + } + return sampleDifference; } -void AudioRingBuffer::addSilentFrame(int numSilentSamples) { +int AudioRingBuffer::addSilentFrame(int numSilentSamples) { + + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + if (numSilentSamples > samplesRoomFor) { + // there's not enough room for this write. write as many silent samples as we have room for + numSilentSamples = samplesRoomFor; + qDebug() << "Dropping some silent samples to prevent ring buffer overflow"; + } + // memset zeroes into the buffer, accomodate a wrap around the end // push the _endOfLastWrite to the correct spot if (_endOfLastWrite + numSilentSamples <= _buffer + _sampleCapacity) { memset(_endOfLastWrite, 0, numSilentSamples * sizeof(int16_t)); - _endOfLastWrite += numSilentSamples; } else { int numSamplesToEnd = (_buffer + _sampleCapacity) - _endOfLastWrite; memset(_endOfLastWrite, 0, numSamplesToEnd * sizeof(int16_t)); memset(_buffer, 0, (numSilentSamples - numSamplesToEnd) * sizeof(int16_t)); - - _endOfLastWrite = _buffer + (numSilentSamples - numSamplesToEnd); } + _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numSilentSamples); + if (numSilentSamples > 0 && _nextOutput == _endOfLastWrite) { + _isFull = true; + } + + return numSilentSamples * sizeof(int16_t); } bool AudioRingBuffer::isNotStarvedOrHasMinimumSamples(unsigned int numRequiredSamples) const { diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index 1ddcadeceb..8d19f9c0bb 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -71,10 +71,10 @@ public: bool isStarved() const { return _isStarved; } void setIsStarved(bool isStarved) { _isStarved = isStarved; } - int getResetCount() const { return _resetCount; } /// how many times has the ring buffer written past the end and reset + int getOverflowCount() const { return _overflowCount; } /// how many times has the ring buffer has overwritten old data bool hasStarted() const { return _hasStarted; } - void addSilentFrame(int numSilentSamples); + int addSilentFrame(int numSilentSamples); protected: // disallow copying of AudioRingBuffer objects AudioRingBuffer(const AudioRingBuffer&); @@ -82,9 +82,10 @@ protected: int16_t* shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const; - int _resetCount; /// how many times has the ring buffer written past the end and done a reset + int _overflowCount; /// how many times has the ring buffer has overwritten old data int _sampleCapacity; + bool _isFull; int _numFrameSamples; int16_t* _nextOutput; int16_t* _endOfLastWrite; diff --git a/libraries/audio/src/AudioStreamStats.h b/libraries/audio/src/AudioStreamStats.h new file mode 100644 index 0000000000..004d697fcf --- /dev/null +++ b/libraries/audio/src/AudioStreamStats.h @@ -0,0 +1,46 @@ +// +// AudioStreamStats.h +// libraries/audio/src +// +// Created by Yixin Wang on 6/25/2014 +// 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 +// + +#ifndef hifi_AudioStreamStats_h +#define hifi_AudioStreamStats_h + +#include "PositionalAudioRingBuffer.h" + +class AudioStreamStats { +public: + AudioStreamStats() + : _streamType(PositionalAudioRingBuffer::Microphone), + _streamIdentifier(), + _jitterBufferFrames(0), + _packetsReceived(0), + _packetsUnreasonable(0), + _packetsEarly(0), + _packetsLate(0), + _packetsLost(0), + _packetsRecovered(0), + _packetsDuplicate(0) + {} + + PositionalAudioRingBuffer::Type _streamType; + QUuid _streamIdentifier; + + quint16 _jitterBufferFrames; + + quint32 _packetsReceived; + quint32 _packetsUnreasonable; + quint32 _packetsEarly; + quint32 _packetsLate; + quint32 _packetsLost; + quint32 _packetsRecovered; + quint32 _packetsDuplicate; +}; + +#endif // hifi_AudioStreamStats_h diff --git a/libraries/audio/src/InjectedAudioRingBuffer.cpp b/libraries/audio/src/InjectedAudioRingBuffer.cpp index 9b6529b49f..0d7cea356b 100644 --- a/libraries/audio/src/InjectedAudioRingBuffer.cpp +++ b/libraries/audio/src/InjectedAudioRingBuffer.cpp @@ -38,6 +38,9 @@ int InjectedAudioRingBuffer::parseData(const QByteArray& packet) { QDataStream packetStream(packet); packetStream.skipRawData(numBytesForPacketHeader(packet)); + // push past the sequence number + packetStream.skipRawData(sizeof(quint16)); + // push past the stream identifier packetStream.skipRawData(NUM_BYTES_RFC4122_UUID); diff --git a/libraries/audio/src/PositionalAudioRingBuffer.cpp b/libraries/audio/src/PositionalAudioRingBuffer.cpp index acd688bdc0..546ed97fe2 100644 --- a/libraries/audio/src/PositionalAudioRingBuffer.cpp +++ b/libraries/audio/src/PositionalAudioRingBuffer.cpp @@ -107,6 +107,9 @@ int PositionalAudioRingBuffer::parseData(const QByteArray& packet) { // skip the packet header (includes the source UUID) int readBytes = numBytesForPacketHeader(packet); + + // skip the sequence number + readBytes += sizeof(quint16); // hop over the channel flag that has already been read in AudioMixerClientData readBytes += sizeof(quint8); @@ -203,7 +206,7 @@ bool PositionalAudioRingBuffer::shouldBeAddedToMix() { if (!isNotStarvedOrHasMinimumSamples(samplesPerFrame + desiredJitterBufferSamples)) { // if the buffer was starved, allow it to accrue at least the desired number of // jitter buffer frames before we start taking frames from it for mixing - + if (_shouldOutputStarveDebug) { _shouldOutputStarveDebug = false; } @@ -253,7 +256,7 @@ void PositionalAudioRingBuffer::updateDesiredJitterBufferFrames() { _desiredJitterBufferFrames = 1; // HACK to see if this fixes the audio silence } else { const float USECS_PER_FRAME = NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL * USECS_PER_SECOND / (float)SAMPLE_RATE; - + _desiredJitterBufferFrames = ceilf((float)_interframeTimeGapStats.getWindowMaxGap() / USECS_PER_FRAME); if (_desiredJitterBufferFrames < 1) { _desiredJitterBufferFrames = 1; diff --git a/libraries/networking/src/PacketHeaders.cpp b/libraries/networking/src/PacketHeaders.cpp index e2bc46b3be..a5c05a6ae9 100644 --- a/libraries/networking/src/PacketHeaders.cpp +++ b/libraries/networking/src/PacketHeaders.cpp @@ -50,6 +50,8 @@ PacketVersion versionForPacketType(PacketType type) { case PacketTypeMicrophoneAudioNoEcho: case PacketTypeMicrophoneAudioWithEcho: case PacketTypeSilentAudioFrame: + return 2; + case PacketTypeMixedAudio: return 1; case PacketTypeAvatarData: return 3; diff --git a/libraries/networking/src/PacketHeaders.h b/libraries/networking/src/PacketHeaders.h index 0f87b0e607..83350a32d1 100644 --- a/libraries/networking/src/PacketHeaders.h +++ b/libraries/networking/src/PacketHeaders.h @@ -40,7 +40,7 @@ enum PacketType { PacketTypeCreateAssignment, PacketTypeDomainOAuthRequest, PacketTypeMuteEnvironment, - PacketTypeDataServerSend, // reusable + PacketTypeAudioStreamStats, PacketTypeDataServerConfirm, PacketTypeVoxelQuery, PacketTypeVoxelData, diff --git a/libraries/networking/src/SequenceNumberStats.cpp b/libraries/networking/src/SequenceNumberStats.cpp new file mode 100644 index 0000000000..15d3c0542e --- /dev/null +++ b/libraries/networking/src/SequenceNumberStats.cpp @@ -0,0 +1,184 @@ +// +// SequenceNumberStats.cpp +// libraries/networking/src +// +// Created by Yixin Wang on 6/25/2014 +// 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 +// + +#include "SequenceNumberStats.h" + +#include + +SequenceNumberStats::SequenceNumberStats() + : _lastReceived(std::numeric_limits::max()), + _missingSet(), + _numReceived(0), + _numUnreasonable(0), + _numEarly(0), + _numLate(0), + _numLost(0), + _numRecovered(0), + _numDuplicate(0), + _lastSenderUUID() +{ +} + +void SequenceNumberStats::reset() { + _missingSet.clear(); + _numReceived = 0; + _numUnreasonable = 0; + _numEarly = 0; + _numLate = 0; + _numLost = 0; + _numRecovered = 0; + _numDuplicate = 0; +} + +static const int UINT16_RANGE = std::numeric_limits::max() + 1; +static const int MAX_REASONABLE_SEQUENCE_GAP = 1000; // this must be less than UINT16_RANGE / 2 for rollover handling to work + +void SequenceNumberStats::sequenceNumberReceived(quint16 incoming, QUuid senderUUID, const bool wantExtraDebugging) { + + // if the sender node has changed, reset all stats + if (senderUUID != _lastSenderUUID) { + qDebug() << "sequence number stats was reset due to new sender node"; + qDebug() << "previous:" << _lastSenderUUID << "current:" << senderUUID; + reset(); + _lastSenderUUID = senderUUID; + } + + // determine our expected sequence number... handle rollover appropriately + quint16 expected = _numReceived > 0 ? _lastReceived + (quint16)1 : incoming; + + _numReceived++; + + if (incoming == expected) { // on time + _lastReceived = incoming; + } else { // out of order + + if (wantExtraDebugging) { + qDebug() << "out of order... got:" << incoming << "expected:" << expected; + } + + int incomingInt = (int)incoming; + int expectedInt = (int)expected; + + // check if the gap between incoming and expected is reasonable, taking possible rollover into consideration + int absGap = std::abs(incomingInt - expectedInt); + if (absGap >= UINT16_RANGE - MAX_REASONABLE_SEQUENCE_GAP) { + // rollover likely occurred between incoming and expected. + // correct the larger of the two so that it's within [-UINT16_RANGE, -1] while the other remains within [0, UINT16_RANGE-1] + if (incomingInt > expectedInt) { + incomingInt -= UINT16_RANGE; + } else { + expectedInt -= UINT16_RANGE; + } + } else if (absGap > MAX_REASONABLE_SEQUENCE_GAP) { + // ignore packet if gap is unreasonable + qDebug() << "ignoring unreasonable sequence number:" << incoming + << "previous:" << _lastReceived; + _numUnreasonable++; + return; + } + + // now that rollover has been corrected for (if it occurred), incoming and expected can be + // compared to each other directly, though one of them might be negative + if (incomingInt > expectedInt) { // early + if (wantExtraDebugging) { + qDebug() << "this packet is earlier than expected..."; + qDebug() << ">>>>>>>> missing gap=" << (incomingInt - expectedInt); + } + + _numEarly++; + _numLost += (incomingInt - expectedInt); + + // add all sequence numbers that were skipped to the missing sequence numbers list + for (int missingInt = expectedInt; missingInt < incomingInt; missingInt++) { + _missingSet.insert((quint16)(missingInt < 0 ? missingInt + UINT16_RANGE : missingInt)); + } + + // prune missing sequence list if it gets too big; sequence numbers that are older than MAX_REASONABLE_SEQUENCE_GAP + // will be removed. + if (_missingSet.size() > MAX_REASONABLE_SEQUENCE_GAP) { + pruneMissingSet(wantExtraDebugging); + } + + _lastReceived = incoming; + } else { // late + if (wantExtraDebugging) { + qDebug() << "this packet is later than expected..."; + } + _numLate++; + + // remove this from missing sequence number if it's in there + if (_missingSet.remove(incoming)) { + if (wantExtraDebugging) { + qDebug() << "found it in _missingSet"; + } + _numLost--; + _numRecovered++; + } else { + if (wantExtraDebugging) { + qDebug() << "sequence:" << incoming << "was NOT found in _missingSet and is probably a duplicate"; + } + _numDuplicate++; + } + + // do not update _incomingLastSequence; it shouldn't become smaller + } + } +} + +void SequenceNumberStats::pruneMissingSet(const bool wantExtraDebugging) { + if (wantExtraDebugging) { + qDebug() << "pruning _missingSet! size:" << _missingSet.size(); + } + + // some older sequence numbers may be from before a rollover point; this must be handled. + // some sequence numbers in this list may be larger than _incomingLastSequence, indicating that they were received + // before the most recent rollover. + int cutoff = (int)_lastReceived - MAX_REASONABLE_SEQUENCE_GAP; + if (cutoff >= 0) { + quint16 nonRolloverCutoff = (quint16)cutoff; + QSet::iterator i = _missingSet.begin(); + while (i != _missingSet.end()) { + quint16 missing = *i; + if (wantExtraDebugging) { + qDebug() << "checking item:" << missing << "is it in need of pruning?"; + qDebug() << "old age cutoff:" << nonRolloverCutoff; + } + + if (missing > _lastReceived || missing < nonRolloverCutoff) { + i = _missingSet.erase(i); + if (wantExtraDebugging) { + qDebug() << "pruning really old missing sequence:" << missing; + } + } else { + i++; + } + } + } else { + quint16 rolloverCutoff = (quint16)(cutoff + UINT16_RANGE); + QSet::iterator i = _missingSet.begin(); + while (i != _missingSet.end()) { + quint16 missing = *i; + if (wantExtraDebugging) { + qDebug() << "checking item:" << missing << "is it in need of pruning?"; + qDebug() << "old age cutoff:" << rolloverCutoff; + } + + if (missing > _lastReceived && missing < rolloverCutoff) { + i = _missingSet.erase(i); + if (wantExtraDebugging) { + qDebug() << "pruning really old missing sequence:" << missing; + } + } else { + i++; + } + } + } +} diff --git a/libraries/networking/src/SequenceNumberStats.h b/libraries/networking/src/SequenceNumberStats.h new file mode 100644 index 0000000000..b2561552ef --- /dev/null +++ b/libraries/networking/src/SequenceNumberStats.h @@ -0,0 +1,53 @@ +// +// SequenceNumberStats.h +// libraries/networking/src +// +// Created by Yixin Wang on 6/25/2014 +// 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 +// + +#ifndef hifi_SequenceNumberStats_h +#define hifi_SequenceNumberStats_h + +#include "SharedUtil.h" +#include + +class SequenceNumberStats { +public: + SequenceNumberStats(); + + void reset(); + + void sequenceNumberReceived(quint16 incoming, QUuid senderUUID = QUuid(), const bool wantExtraDebugging = false); + + quint32 getNumReceived() const { return _numReceived; } + quint32 getNumUnreasonable() const { return _numUnreasonable; } + quint32 getNumOutOfOrder() const { return _numEarly + _numLate; } + quint32 getNumEarly() const { return _numEarly; } + quint32 getNumLate() const { return _numLate; } + quint32 getNumLost() const { return _numLost; } + quint32 getNumRecovered() const { return _numRecovered; } + quint32 getNumDuplicate() const { return _numDuplicate; } + const QSet& getMissingSet() const { return _missingSet; } + +private: + void pruneMissingSet(const bool wantExtraDebugging); + + quint16 _lastReceived; + QSet _missingSet; + + quint32 _numReceived; + quint32 _numUnreasonable; + quint32 _numEarly; + quint32 _numLate; + quint32 _numLost; + quint32 _numRecovered; + quint32 _numDuplicate; + + QUuid _lastSenderUUID; +}; + +#endif // hifi_SequenceNumberStats_h diff --git a/libraries/octree/src/OctreeSceneStats.cpp b/libraries/octree/src/OctreeSceneStats.cpp index 28445ec327..e585c8dfe6 100644 --- a/libraries/octree/src/OctreeSceneStats.cpp +++ b/libraries/octree/src/OctreeSceneStats.cpp @@ -21,10 +21,6 @@ #include "OctreeSceneStats.h" -const uint16_t MAX_MISSING_SEQUENCE = 100; /// how many items in our _missingSequenceNumbers before we start to prune them -const uint16_t MAX_MISSING_SEQUENCE_OLD_AGE = 1000; /// age we allow items in _missingSequenceNumbers to be before pruning - - const int samples = 100; OctreeSceneStats::OctreeSceneStats() : _isReadyToSend(false), @@ -39,14 +35,7 @@ OctreeSceneStats::OctreeSceneStats() : _incomingPacket(0), _incomingBytes(0), _incomingWastedBytes(0), - _incomingLastSequence(0), - _incomingLikelyLost(0), - _incomingRecovered(0), - _incomingEarly(0), - _incomingLate(0), - _incomingReallyLate(0), - _incomingPossibleDuplicate(0), - _missingSequenceNumbers(), + _incomingOctreeSequenceNumberStats(), _incomingFlightTimeAverage(samples), _jurisdictionRoot(NULL) { @@ -150,15 +139,8 @@ void OctreeSceneStats::copyFromOther(const OctreeSceneStats& other) { _incomingPacket = other._incomingPacket; _incomingBytes = other._incomingBytes; _incomingWastedBytes = other._incomingWastedBytes; - _incomingLastSequence = other._incomingLastSequence; - _incomingLikelyLost = other._incomingLikelyLost; - _incomingRecovered = other._incomingRecovered; - _incomingEarly = other._incomingEarly; - _incomingLate = other._incomingLate; - _incomingReallyLate = other._incomingReallyLate; - _incomingPossibleDuplicate = other._incomingPossibleDuplicate; - - _missingSequenceNumbers = other._missingSequenceNumbers; + + _incomingOctreeSequenceNumberStats = other._incomingOctreeSequenceNumberStats; } @@ -875,155 +857,8 @@ void OctreeSceneStats::trackIncomingOctreePacket(const QByteArray& packet, qDebug() << "ignoring unreasonable packet... flightTime:" << flightTime; return; // ignore any packets that are unreasonable } - - const int UINT16_RANGE = std::numeric_limits::max() + 1; - - // determine our expected sequence number... handle rollover appropriately - OCTREE_PACKET_SEQUENCE expected = _incomingPacket > 0 ? _incomingLastSequence + (quint16)1 : sequence; - - const int USECS_PER_MSEC = 1000; - float flightTimeMsecs = flightTime / USECS_PER_MSEC; - _incomingFlightTimeAverage.updateAverage(flightTimeMsecs); - - // track out of order and possibly lost packets... - if (sequence == _incomingLastSequence) { - if (wantExtraDebugging) { - qDebug() << "last packet duplicate got:" << sequence << "_incomingLastSequence:" << _incomingLastSequence; - } - } else { - if (sequence != expected) { - if (wantExtraDebugging) { - qDebug() << "out of order... got:" << sequence << "expected:" << expected; - } - - int sequenceInt = (int)sequence; - int expectedInt = (int)expected; - - // if distance between sequence and expected are more than half of the total range of possible seq numbers, - // assume that a rollover occurred between the two. - // correct the larger one so it's in the range [-UINT16_RANGE, -1] while the other remains in [0, UINT16_RANGE-1] - // after doing so, sequenceInt and expectedInt can be correctly compared to each other, though one may be negative - if (std::abs(sequenceInt - expectedInt) > UINT16_RANGE / 2) { - if (sequenceInt > expectedInt) { - sequenceInt -= UINT16_RANGE; - } - else { - expectedInt -= UINT16_RANGE; - } - } - - // Guard against possible corrupted packets... with bad sequence numbers - const int MAX_RESONABLE_SEQUENCE_OFFSET = 2000; - const int MIN_RESONABLE_SEQUENCE_OFFSET = -2000; - - int sequenceOffset = (sequenceInt - expectedInt); - if (sequenceOffset > MAX_RESONABLE_SEQUENCE_OFFSET || sequenceOffset < MIN_RESONABLE_SEQUENCE_OFFSET) { - qDebug() << "ignoring unreasonable packet... sequence:" << sequence << "_incomingLastSequence:" << _incomingLastSequence; - return; // ignore any packets that are unreasonable - } - - // if the sequence is less than our expected, then this might be a packet - // that was delayed and so we should find it in our lostSequence list - if (sequenceInt < expectedInt) { - - // if no rollover between them: sequenceInt, expectedInt are both in range [0, UINT16_RANGE-1] - // if rollover between them: sequenceInt in [-UINT16_RANGE, -1], expectedInt in [0, UINT16_RANGE-1] - - if (wantExtraDebugging) { - qDebug() << "this packet is later than expected..."; - } - if (sequenceInt < expectedInt - MAX_MISSING_SEQUENCE_OLD_AGE) { - _incomingReallyLate++; - } - else { - _incomingLate++; - } - - if (_missingSequenceNumbers.contains(sequence)) { - if (wantExtraDebugging) { - qDebug() << "found it in _missingSequenceNumbers"; - } - _missingSequenceNumbers.remove(sequence); - _incomingLikelyLost--; - _incomingRecovered++; - } - else { - // if we're still in our pruning window, and we didn't find it in our missing list, - // than this is really unexpected and can probably only happen if the packet was a - // duplicate - if (sequenceInt >= expectedInt - MAX_MISSING_SEQUENCE_OLD_AGE) { - if (wantExtraDebugging) { - qDebug() << "sequence:" << sequence << "WAS NOT found in _missingSequenceNumbers, and not that old... (expected - MAX_MISSING_SEQUENCE_OLD_AGE):" - << (uint16_t)(expectedInt - MAX_MISSING_SEQUENCE_OLD_AGE); - } - _incomingPossibleDuplicate++; - } - } - - // don't update _incomingLastSequence in this case. - // only bump the last sequence if it was greater than our expected sequence, this will keep us from - // accidentally going backwards when an out of order (recovered) packet comes in - - } else { // sequenceInt > expectedInt - - // if no rollover between them: sequenceInt, expectedInt are both in range [0, UINT16_RANGE-1] - // if rollover between them: sequenceInt in [0, UINT16_RANGE-1], expectedInt in [-UINT16_RANGE, -1] - - if (wantExtraDebugging) { - qDebug() << "this packet is earlier than expected..."; - } - _incomingEarly++; - - // hmm... so, we either didn't get some packets, or this guy came early... - int missing = sequenceInt - expectedInt; - if (wantExtraDebugging) { - qDebug() << ">>>>>>>> missing gap=" << missing; - } - _incomingLikelyLost += missing; - for (int missingSequenceInt = expectedInt; missingSequenceInt < sequenceInt; missingSequenceInt++) { - OCTREE_PACKET_SEQUENCE missingSequence = missingSequenceInt >= 0 ? missingSequenceInt : missingSequenceInt + UINT16_RANGE; - _missingSequenceNumbers << missingSequence; - } - - _incomingLastSequence = sequence; - } - } else { // sequence = expected - - _incomingLastSequence = sequence; - } - } - - // do some garbage collecting on our _missingSequenceNumbers - if (_missingSequenceNumbers.size() > MAX_MISSING_SEQUENCE) { - if (wantExtraDebugging) { - qDebug() << "too many _missingSequenceNumbers:" << _missingSequenceNumbers.size(); - } - - int oldAgeCutoff = (int)_incomingLastSequence - MAX_MISSING_SEQUENCE_OLD_AGE; - - foreach(uint16_t missingItem, _missingSequenceNumbers) { - if (wantExtraDebugging) { - qDebug() << "checking item:" << missingItem << "is it in need of pruning?"; - qDebug() << "(_incomingLastSequence - MAX_MISSING_SEQUENCE_OLD_AGE):" - << (uint16_t)((int)_incomingLastSequence - MAX_MISSING_SEQUENCE_OLD_AGE); - } - - bool prune; - if (oldAgeCutoff >= 0) { - prune = (missingItem <= oldAgeCutoff || missingItem > _incomingLastSequence); - } - else { - prune = (missingItem <= oldAgeCutoff + UINT16_RANGE && missingItem > _incomingLastSequence); - } - - if (prune) { - if (wantExtraDebugging) { - qDebug() << "pruning really old missing sequence:" << missingItem; - } - _missingSequenceNumbers.remove(missingItem); - } - } - } + + _incomingOctreeSequenceNumberStats.sequenceNumberReceived(sequence); // track packets here... _incomingPacket++; diff --git a/libraries/octree/src/OctreeSceneStats.h b/libraries/octree/src/OctreeSceneStats.h index 1c468a8dc6..d7b65c63be 100644 --- a/libraries/octree/src/OctreeSceneStats.h +++ b/libraries/octree/src/OctreeSceneStats.h @@ -17,6 +17,7 @@ #include #include "JurisdictionMap.h" #include "OctreePacketData.h" +#include "SequenceNumberStats.h" #define GREENISH 0x40ff40d0 #define YELLOWISH 0xffef40c0 @@ -164,16 +165,9 @@ public: quint32 getIncomingPackets() const { return _incomingPacket; } quint64 getIncomingBytes() const { return _incomingBytes; } quint64 getIncomingWastedBytes() const { return _incomingWastedBytes; } - quint32 getIncomingOutOfOrder() const { return _incomingLate + _incomingEarly; } - quint32 getIncomingLikelyLost() const { return _incomingLikelyLost; } - quint32 getIncomingRecovered() const { return _incomingRecovered; } - quint32 getIncomingEarly() const { return _incomingEarly; } - quint32 getIncomingLate() const { return _incomingLate; } - quint32 getIncomingReallyLate() const { return _incomingReallyLate; } - quint32 getIncomingPossibleDuplicate() const { return _incomingPossibleDuplicate; } float getIncomingFlightTimeAverage() { return _incomingFlightTimeAverage.getAverage(); } - const QSet& getMissingSequenceNumbers() const { return _missingSequenceNumbers; } + const SequenceNumberStats& getIncomingOctreeSequenceNumberStats() const { return _incomingOctreeSequenceNumberStats; } private: @@ -268,14 +262,8 @@ private: quint64 _incomingBytes; quint64 _incomingWastedBytes; - quint16 _incomingLastSequence; /// last incoming sequence number - quint32 _incomingLikelyLost; /// count of packets likely lost, may be off by _incomingReallyLate count - quint32 _incomingRecovered; /// packets that were late, and we had in our missing list, we consider recovered - quint32 _incomingEarly; /// out of order earlier than expected - quint32 _incomingLate; /// out of order later than expected - quint32 _incomingReallyLate; /// out of order and later than MAX_MISSING_SEQUENCE_OLD_AGE late - quint32 _incomingPossibleDuplicate; /// out of order possibly a duplicate - QSet _missingSequenceNumbers; + SequenceNumberStats _incomingOctreeSequenceNumberStats; + SimpleMovingAverage _incomingFlightTimeAverage; // features related items diff --git a/libraries/script-engine/src/ScriptEngine.cpp b/libraries/script-engine/src/ScriptEngine.cpp index c5fb0a263d..55b99d6039 100644 --- a/libraries/script-engine/src/ScriptEngine.cpp +++ b/libraries/script-engine/src/ScriptEngine.cpp @@ -460,13 +460,17 @@ void ScriptEngine::run() { _numAvatarSoundSentBytes = 0; } } - + QByteArray audioPacket = byteArrayWithPopulatedHeader(silentFrame ? PacketTypeSilentAudioFrame : PacketTypeMicrophoneAudioNoEcho); QDataStream packetStream(&audioPacket, QIODevice::Append); + // pack a placeholder value for sequence number for now, will be packed when destination node is known + int numPreSequenceNumberBytes = audioPacket.size(); + packetStream << (quint16)0; + // use the orientation and position of this avatar for the source of this audio packetStream.writeRawData(reinterpret_cast(&_avatarData->getPosition()), sizeof(glm::vec3)); glm::quat headOrientation = _avatarData->getHeadOrientation(); @@ -486,7 +490,19 @@ void ScriptEngine::run() { numAvailableSamples * sizeof(int16_t)); } - nodeList->broadcastToNodes(audioPacket, NodeSet() << NodeType::AudioMixer); + // write audio packet to AudioMixer nodes + NodeList* nodeList = NodeList::getInstance(); + foreach(const SharedNodePointer& node, nodeList->getNodeHash()) { + // only send to nodes of type AudioMixer + if (node->getType() == NodeType::AudioMixer) { + // pack sequence number + quint16 sequence = _outgoingScriptAudioSequenceNumbers[node->getUUID()]++; + memcpy(audioPacket.data() + numPreSequenceNumberBytes, &sequence, sizeof(quint16)); + + // send audio packet + nodeList->writeDatagram(audioPacket, node); + } + } } } @@ -662,3 +678,7 @@ void ScriptEngine::include(const QString& includeFile) { _engine.clearExceptions(); } } + +void ScriptEngine::nodeKilled(SharedNodePointer node) { + _outgoingScriptAudioSequenceNumbers.remove(node->getUUID()); +} diff --git a/libraries/script-engine/src/ScriptEngine.h b/libraries/script-engine/src/ScriptEngine.h index bf2ac40568..5b01b8124a 100644 --- a/libraries/script-engine/src/ScriptEngine.h +++ b/libraries/script-engine/src/ScriptEngine.h @@ -100,6 +100,8 @@ public slots: void include(const QString& includeFile); void print(const QString& message); + void nodeKilled(SharedNodePointer node); + signals: void update(float deltaTime); void scriptEnding(); @@ -146,6 +148,7 @@ private: ScriptUUID _uuidLibrary; AnimationCache _animationCache; + QHash _outgoingScriptAudioSequenceNumbers; }; #endif // hifi_ScriptEngine_h diff --git a/tests/audio/CMakeLists.txt b/tests/audio/CMakeLists.txt new file mode 100644 index 0000000000..5c5178cb71 --- /dev/null +++ b/tests/audio/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 2.8) + +if (WIN32) + cmake_policy (SET CMP0020 NEW) +endif (WIN32) + +set(TARGET_NAME audio-tests) + +set(ROOT_DIR ../..) +set(MACRO_DIR ${ROOT_DIR}/cmake/macros) + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +#find_package(Qt5Network REQUIRED) +#find_package(Qt5Script REQUIRED) +#find_package(Qt5Widgets REQUIRED) + +include(${MACRO_DIR}/SetupHifiProject.cmake) +setup_hifi_project(${TARGET_NAME} TRUE) + +include(${MACRO_DIR}/AutoMTC.cmake) +auto_mtc(${TARGET_NAME} ${ROOT_DIR}) + +#qt5_use_modules(${TARGET_NAME} Network Script Widgets) + +#include glm +include(${MACRO_DIR}/IncludeGLM.cmake) +include_glm(${TARGET_NAME} ${ROOT_DIR}) + +# link in the shared libraries +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(audio ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR}) + +IF (WIN32) + target_link_libraries(${TARGET_NAME} Winmm Ws2_32) +ENDIF(WIN32) + diff --git a/tests/audio/src/AudioRingBufferTests.cpp b/tests/audio/src/AudioRingBufferTests.cpp new file mode 100644 index 0000000000..506e81e13e --- /dev/null +++ b/tests/audio/src/AudioRingBufferTests.cpp @@ -0,0 +1,146 @@ +// +// AudioRingBufferTests.cpp +// tests/audio/src +// +// Created by Yixin Wang on 6/24/2014 +// 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 +// + +#include "AudioRingBufferTests.h" + +#include "SharedUtil.h" + +void AudioRingBufferTests::assertBufferSize(const AudioRingBuffer& buffer, int samples) { + if (buffer.samplesAvailable() != samples) { + qDebug("Unexpected num samples available! Exptected: %d Actual: %d\n", samples, buffer.samplesAvailable()); + } +} + +void AudioRingBufferTests::runAllTests() { + + int16_t writeData[10000]; + for (int i = 0; i < 10000; i++) { writeData[i] = i; } + int writeIndexAt; + + int16_t readData[10000]; + int readIndexAt; + + + AudioRingBuffer ringBuffer(10); // makes buffer of 100 int16_t samples + for (int T = 0; T < 300; T++) { + + writeIndexAt = 0; + readIndexAt = 0; + + // write 73 samples, 73 samples in buffer + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 73) / sizeof(int16_t); + assertBufferSize(ringBuffer, 73); + + // read 43 samples, 30 samples in buffer + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 43) / sizeof(int16_t); + assertBufferSize(ringBuffer, 30); + + // write 70 samples, 100 samples in buffer (full) + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 70) / sizeof(int16_t); + assertBufferSize(ringBuffer, 100); + + // read 100 samples, 0 samples in buffer (empty) + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 100) / sizeof(int16_t); + assertBufferSize(ringBuffer, 0); + + + // verify 143 samples of read data + for (int i = 0; i < 143; i++) { + if (readData[i] != i) { + qDebug("first readData[%d] incorrect! Expcted: %d Actual: %d", i, i, readData[i]); + return; + } + } + + + writeIndexAt = 0; + readIndexAt = 0; + + // write 59 samples, 59 samples in buffer + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 59) / sizeof(int16_t); + assertBufferSize(ringBuffer, 59); + + // write 99 samples, 100 samples in buffer + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 99) / sizeof(int16_t); + assertBufferSize(ringBuffer, 100); + + // read 100 samples, 0 samples in buffer + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 100) / sizeof(int16_t); + assertBufferSize(ringBuffer, 0); + + // verify 100 samples of read data + for (int i = 0; i < 100; i++) { + readData[i] = writeIndexAt - 100 + i; + } + + + + + writeIndexAt = 0; + readIndexAt = 0; + + // write 77 samples, 77 samples in buffer + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 77) / sizeof(int16_t); + assertBufferSize(ringBuffer, 77); + + // write 24 samples, 100 samples in buffer (overwrote one sample: "0") + writeIndexAt += ringBuffer.writeSamples(&writeData[writeIndexAt], 24) / sizeof(int16_t); + assertBufferSize(ringBuffer, 100); + + // write 29 silent samples, 100 samples in buffer, make sure non were added + int samplesWritten; + if ((samplesWritten = ringBuffer.addSilentFrame(29)) != 0) { + qDebug("addSilentFrame(29) incorrect! Expected: 0 Actual: %d", samplesWritten); + return; + } + assertBufferSize(ringBuffer, 100); + + // read 3 samples, 97 samples in buffer (expect to read "1", "2", "3") + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 3) / sizeof(int16_t); + for (int i = 0; i < 3; i++) { + if (readData[i] != i + 1) { + qDebug("Second readData[%d] incorrect! Expcted: %d Actual: %d", i, i + 1, readData[i]); + return; + } + } + assertBufferSize(ringBuffer, 97); + + // write 4 silent samples, 100 samples in buffer + if ((samplesWritten = ringBuffer.addSilentFrame(4) / sizeof(int16_t)) != 3) { + qDebug("addSilentFrame(4) incorrect! Exptected: 3 Actual: %d", samplesWritten); + return; + } + assertBufferSize(ringBuffer, 100); + + // read back 97 samples (the non-silent samples), 3 samples in buffer (expect to read "4" thru "100") + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 97) / sizeof(int16_t); + for (int i = 3; i < 100; i++) { + if (readData[i] != i + 1) { + qDebug("third readData[%d] incorrect! Expcted: %d Actual: %d", i, i + 1, readData[i]); + return; + } + } + assertBufferSize(ringBuffer, 3); + + // read back 3 silent samples, 0 samples in buffer + readIndexAt += ringBuffer.readSamples(&readData[readIndexAt], 3) / sizeof(int16_t); + for (int i = 100; i < 103; i++) { + if (readData[i] != 0) { + qDebug("Fourth readData[%d] incorrect! Expcted: %d Actual: %d", i, 0, readData[i]); + return; + } + } + assertBufferSize(ringBuffer, 0); + } + + qDebug() << "PASSED"; +} + diff --git a/tests/audio/src/AudioRingBufferTests.h b/tests/audio/src/AudioRingBufferTests.h new file mode 100644 index 0000000000..20cbe74699 --- /dev/null +++ b/tests/audio/src/AudioRingBufferTests.h @@ -0,0 +1,25 @@ +// +// AudioRingBufferTests.h +// tests/audio/src +// +// Created by Yixin Wang on 6/24/2014 +// 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 +// + +#ifndef hifi_AudioRingBufferTests_h +#define hifi_AudioRingBufferTests_h + +#include "AudioRingBuffer.h" + + +namespace AudioRingBufferTests { + + void runAllTests(); + + void assertBufferSize(const AudioRingBuffer& buffer, int samples); +}; + +#endif // hifi_AudioRingBufferTests_h diff --git a/tests/audio/src/main.cpp b/tests/audio/src/main.cpp new file mode 100644 index 0000000000..10f1a2e522 --- /dev/null +++ b/tests/audio/src/main.cpp @@ -0,0 +1,19 @@ +// +// main.cpp +// tests/audio/src +// +// 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 +// + +#include "AudioRingBufferTests.h" +#include + +int main(int argc, char** argv) { + AudioRingBufferTests::runAllTests(); + printf("all tests passed. press enter to exit\n"); + getchar(); + return 0; +} diff --git a/tests/networking/CMakeLists.txt b/tests/networking/CMakeLists.txt new file mode 100644 index 0000000000..2e094d2ce7 --- /dev/null +++ b/tests/networking/CMakeLists.txt @@ -0,0 +1,39 @@ +cmake_minimum_required(VERSION 2.8) + +if (WIN32) + cmake_policy (SET CMP0020 NEW) +endif (WIN32) + +set(TARGET_NAME networking-tests) + +set(ROOT_DIR ../..) +set(MACRO_DIR ${ROOT_DIR}/cmake/macros) + +# setup for find modules +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/modules/") + +#find_package(Qt5Network REQUIRED) +#find_package(Qt5Script REQUIRED) +#find_package(Qt5Widgets REQUIRED) + +include(${MACRO_DIR}/SetupHifiProject.cmake) +setup_hifi_project(${TARGET_NAME} TRUE) + +include(${MACRO_DIR}/AutoMTC.cmake) +auto_mtc(${TARGET_NAME} ${ROOT_DIR}) + +#qt5_use_modules(${TARGET_NAME} Network Script Widgets) + +#include glm +include(${MACRO_DIR}/IncludeGLM.cmake) +include_glm(${TARGET_NAME} ${ROOT_DIR}) + +# link in the shared libraries +include(${MACRO_DIR}/LinkHifiLibrary.cmake) +link_hifi_library(shared ${TARGET_NAME} ${ROOT_DIR}) +link_hifi_library(networking ${TARGET_NAME} ${ROOT_DIR}) + +IF (WIN32) + target_link_libraries(${TARGET_NAME} Winmm Ws2_32) +ENDIF(WIN32) + diff --git a/tests/networking/src/SequenceNumberStatsTests.cpp b/tests/networking/src/SequenceNumberStatsTests.cpp new file mode 100644 index 0000000000..89a14deb20 --- /dev/null +++ b/tests/networking/src/SequenceNumberStatsTests.cpp @@ -0,0 +1,267 @@ +// +// AudioRingBufferTests.cpp +// tests/networking/src +// +// Created by Yixin Wang on 6/24/2014 +// 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 +// + +#include "SequenceNumberStatsTests.h" + +#include "SharedUtil.h" +#include + + +void SequenceNumberStatsTests::runAllTests() { + + rolloverTest(); + earlyLateTest(); + duplicateTest(); + pruneTest(); +} + +const int UINT16_RANGE = std::numeric_limits::max() + 1; + + +void SequenceNumberStatsTests::rolloverTest() { + + SequenceNumberStats stats; + + // insert enough samples to cause 3 rollovers + quint16 seq = 79; // start on some random number + + for (int R = 0; R < 2; R++) { + for (int i = 0; i < 3 * UINT16_RANGE; i++) { + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + + assert(stats.getNumDuplicate() == 0); + assert(stats.getNumEarly() == 0); + assert(stats.getNumLate() == 0); + assert(stats.getNumLost() == 0); + assert(stats.getNumReceived() == i + 1); + assert(stats.getNumRecovered() == 0); + } + stats.reset(); + } +} + +void SequenceNumberStatsTests::earlyLateTest() { + + SequenceNumberStats stats; + quint16 seq = 65530; + int numSent = 0; + + int numEarly = 0; + int numLate = 0; + int numLost = 0; + int numRecovered = 0; + + for (int R = 0; R < 2; R++) { + for (int T = 0; T < 10000; T++) { + + // insert 7 consecutive + for (int i = 0; i < 7; i++) { + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + + assert(stats.getNumDuplicate() == 0); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == numRecovered); + } + + // skip 10 + quint16 skipped = seq; + seq = seq + (quint16)10; + + // insert 36 consecutive + numEarly++; + numLost += 10; + for (int i = 0; i < 36; i++) { + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + + assert(stats.getNumDuplicate() == 0); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == numRecovered); + } + + // send ones we skipped + for (int i = 0; i < 10; i++) { + stats.sequenceNumberReceived(skipped); + skipped = skipped + (quint16)1; + numSent++; + numLate++; + numLost--; + numRecovered++; + + assert(stats.getNumDuplicate() == 0); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == numRecovered); + } + } + stats.reset(); + } +} + +void SequenceNumberStatsTests::duplicateTest() { + + SequenceNumberStats stats; + quint16 seq = 12345; + int numSent = 0; + + int numDuplicate = 0; + int numEarly = 0; + int numLate = 0; + int numLost = 0; + + for (int R = 0; R < 2; R++) { + for (int T = 0; T < 10000; T++) { + + quint16 duplicate = seq; + + // insert 7 consecutive + for (int i = 0; i < 7; i++) { + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + + assert(stats.getNumDuplicate() == numDuplicate); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == 0); + } + + // skip 10 + seq = seq + (quint16)10; + + + quint16 duplicate2 = seq; + + numEarly++; + numLost += 10; + // insert 36 consecutive + for (int i = 0; i < 36; i++) { + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + + assert(stats.getNumDuplicate() == numDuplicate); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == 0); + } + + // send 5 duplicates from before skip + for (int i = 0; i < 5; i++) { + stats.sequenceNumberReceived(duplicate); + duplicate = duplicate + (quint16)1; + numSent++; + numDuplicate++; + numLate++; + + assert(stats.getNumDuplicate() == numDuplicate); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == 0); + } + + // send 5 duplicates from after skip + for (int i = 0; i < 5; i++) { + stats.sequenceNumberReceived(duplicate2); + duplicate2 = duplicate2 + (quint16)1; + numSent++; + numDuplicate++; + numLate++; + + assert(stats.getNumDuplicate() == numDuplicate); + assert(stats.getNumEarly() == numEarly); + assert(stats.getNumLate() == numLate); + assert(stats.getNumLost() == numLost); + assert(stats.getNumReceived() == numSent); + assert(stats.getNumRecovered() == 0); + } + } + stats.reset(); + } +} + +void SequenceNumberStatsTests::pruneTest() { + + SequenceNumberStats stats; + quint16 seq = 54321; + int numSent = 0; + + int numEarly = 0; + int numLost = 0; + + for (int R = 0; R < 2; R++) { + for (int T = 0; T < 1000; T++) { + // insert 1 seq + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + + // skip 1000 seq + seq = seq + (quint16)1000; + quint16 highestSkipped = seq - (quint16)1; + + // insert 1 seq + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + numEarly++; + numLost += 1000; + + // skip 10 seq + seq = seq + (quint16)10; + quint16 highestSkipped2 = seq - (quint16)1; + + // insert 1 seq + // insert 1 seq + stats.sequenceNumberReceived(seq); + seq = seq + (quint16)1; + numSent++; + numEarly++; + numLost += 10; + + const QSet& missingSet = stats.getMissingSet(); + assert(missingSet.size() <= 1000); + + for (int i = 0; i < 10; i++) { + assert(missingSet.contains(highestSkipped2)); + highestSkipped2 = highestSkipped2 - (quint16)1; + } + + for (int i = 0; i < 989; i++) { + assert(missingSet.contains(highestSkipped)); + highestSkipped = highestSkipped - (quint16)1; + } + for (int i = 0; i < 11; i++) { + assert(!missingSet.contains(highestSkipped)); + highestSkipped = highestSkipped - (quint16)1; + } + } + stats.reset(); + } +} diff --git a/tests/networking/src/SequenceNumberStatsTests.h b/tests/networking/src/SequenceNumberStatsTests.h new file mode 100644 index 0000000000..53a0b66480 --- /dev/null +++ b/tests/networking/src/SequenceNumberStatsTests.h @@ -0,0 +1,28 @@ +// +// AudioRingBufferTests.h +// tests/networking/src +// +// Created by Yixin Wang on 6/24/2014 +// 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 +// + +#ifndef hifi_SequenceNumberStatsTests_h +#define hifi_SequenceNumberStatsTests_h + +#include "SequenceNumberStatsTests.h" +#include "SequenceNumberStats.h" + +namespace SequenceNumberStatsTests { + + void runAllTests(); + + void rolloverTest(); + void earlyLateTest(); + void duplicateTest(); + void pruneTest(); +}; + +#endif // hifi_SequenceNumberStatsTests_h diff --git a/tests/networking/src/main.cpp b/tests/networking/src/main.cpp new file mode 100644 index 0000000000..91a59a0e41 --- /dev/null +++ b/tests/networking/src/main.cpp @@ -0,0 +1,19 @@ +// +// main.cpp +// tests/networking/src +// +// 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 +// + +#include "SequenceNumberStatsTests.h" +#include + +int main(int argc, char** argv) { + SequenceNumberStatsTests::runAllTests(); + printf("tests passed! press enter to exit"); + getchar(); + return 0; +}