From 72ddc78ad659b7ea320499746fe0e9726bff6cb3 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 25 Feb 2013 11:52:38 -0800 Subject: [PATCH 01/29] domain server only returns newest of types in SOLO_AGENT_TYPES_STRING --- domain/src/main.cpp | 36 +++++++++++++++++++++++++++++++----- shared/src/Agent.cpp | 3 +++ shared/src/Agent.h | 1 + shared/src/AgentList.cpp | 2 ++ shared/src/AgentList.h | 1 + 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/domain/src/main.cpp b/domain/src/main.cpp index 5f9b075c48..7dd89df557 100644 --- a/domain/src/main.cpp +++ b/domain/src/main.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include "AgentList.h" #include "SharedUtil.h" @@ -42,6 +43,15 @@ const int LOGOFF_CHECK_INTERVAL = 5000; int lastActiveCount = 0; AgentList agentList(DOMAIN_LISTEN_PORT); +unsigned char * addAgentToBroadcastPacket(unsigned char *currentPosition, Agent *agentToAdd) { + *currentPosition++ = agentToAdd->type; + + currentPosition += packSocket(currentPosition, agentToAdd->publicSocket); + currentPosition += packSocket(currentPosition, agentToAdd->localSocket); + + // return the new unsigned char * for broadcast packet + return currentPosition; +} int main(int argc, const char * argv[]) { @@ -60,6 +70,8 @@ int main(int argc, const char * argv[]) agentList.startSilentAgentRemovalThread(); + std::map newestSoloAgents; + while (true) { if (agentList.getAgentSocket()->receive((sockaddr *)&agentPublicAddress, packetData, &receivedBytes)) { agentType = packetData[0]; @@ -73,17 +85,31 @@ int main(int argc, const char * argv[]) for(std::vector::iterator agent = agentList.agents.begin(); agent != agentList.agents.end(); agent++) { if (DEBUG_TO_SELF || !agent->matches((sockaddr *)&agentPublicAddress, (sockaddr *)&agentLocalAddress, agentType)) { - *currentBufferPos++ = agent->type; - - currentBufferPos += packSocket(currentBufferPos, agent->publicSocket); - currentBufferPos += packSocket(currentBufferPos, agent->localSocket); + if (strchr(SOLO_AGENT_TYPES_STRING, (int) agent->type) == NULL) { + // this is an agent of which there can be multiple, just add them to the packet + currentBufferPos = addAgentToBroadcastPacket(currentBufferPos, &(*agent)); + } else { + std::cout << "We have a solo agent: " << &(*agent) << "\n"; + // solo agent, we need to only send newest + if (newestSoloAgents[agent->type] == NULL || + newestSoloAgents[agent->type]->firstRecvTimeUsecs < agent->firstRecvTimeUsecs) { + // we have to set the newer solo agent to add it to the broadcast later + newestSoloAgents[agent->type] = &(*agent); + } + } } else { // this is the agent, just update last receive to now agent->lastRecvTimeUsecs = usecTimestampNow(); } } - ; + for (std::map::iterator agentIterator = newestSoloAgents.begin(); + agentIterator != newestSoloAgents.end(); + agentIterator++) { + std::cout << "Newest agent: " << agentIterator->second << "\n"; + // this is the newest alive solo agent, add them to the packet + currentBufferPos = addAgentToBroadcastPacket(currentBufferPos, agentIterator->second); + } if ((packetBytesWithoutLeadingChar = (currentBufferPos - startPointer))) { agentList.getAgentSocket()->send((sockaddr *)&agentPublicAddress, broadcastPacket, packetBytesWithoutLeadingChar + 1); diff --git a/shared/src/Agent.cpp b/shared/src/Agent.cpp index 5b48033225..5790329c80 100644 --- a/shared/src/Agent.cpp +++ b/shared/src/Agent.cpp @@ -22,6 +22,8 @@ Agent::Agent(sockaddr *agentPublicSocket, sockaddr *agentLocalSocket, char agent memcpy(localSocket, agentLocalSocket, sizeof(sockaddr)); type = agentType; + + firstRecvTimeUsecs = usecTimestampNow(); lastRecvTimeUsecs = usecTimestampNow(); activeSocket = NULL; @@ -43,6 +45,7 @@ Agent::Agent(const Agent &otherAgent) { activeSocket = NULL; } + firstRecvTimeUsecs = otherAgent.firstRecvTimeUsecs; lastRecvTimeUsecs = otherAgent.lastRecvTimeUsecs; type = otherAgent.type; diff --git a/shared/src/Agent.h b/shared/src/Agent.h index efcebc3b05..90073a92b0 100644 --- a/shared/src/Agent.h +++ b/shared/src/Agent.h @@ -28,6 +28,7 @@ class Agent { char type; timeval pingStarted; int pingMsecs; + double firstRecvTimeUsecs; double lastRecvTimeUsecs; bool isSelf; AgentData *linkedData; diff --git a/shared/src/AgentList.cpp b/shared/src/AgentList.cpp index d068ab15fc..fdf4e430ff 100644 --- a/shared/src/AgentList.cpp +++ b/shared/src/AgentList.cpp @@ -11,6 +11,8 @@ #include #include "SharedUtil.h" +const char * SOLO_AGENT_TYPES_STRING = "M"; + bool stopAgentRemovalThread = false; pthread_mutex_t vectorChangeMutex = PTHREAD_MUTEX_INITIALIZER; diff --git a/shared/src/AgentList.h b/shared/src/AgentList.h index 399340b841..734d64da9e 100644 --- a/shared/src/AgentList.h +++ b/shared/src/AgentList.h @@ -16,6 +16,7 @@ const unsigned short AGENT_SOCKET_LISTEN_PORT = 40103; const int AGENT_SILENCE_THRESHOLD_USECS = 2 * 1000000; +extern const char *SOLO_AGENT_TYPES_STRINGg; class AgentList { public: From 5910fdd7b71ce6217d6b43e4bbc307cf3de3f989 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 25 Feb 2013 12:24:42 -0800 Subject: [PATCH 02/29] AgentList returns agents vector and agentSocket by reference --- interface/src/main.cpp | 8 ++++---- shared/src/AgentList.cpp | 8 ++++++-- shared/src/AgentList.h | 15 +++++++++------ 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 44ed1b8ac9..9b3ad7a39f 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -223,7 +223,7 @@ void Timer(int extra) output[0] = 'I'; packSocket(output + 1, localAddress, htons(AGENT_SOCKET_LISTEN_PORT)); - agentList.getAgentSocket()->send(DOMAIN_IP, DOMAINSERVER_PORT, output, 7); + agentList.getAgentSocket().send(DOMAIN_IP, DOMAINSERVER_PORT, output, 7); // Ping the agents we can see agentList.pingAgents(); @@ -570,7 +570,7 @@ void display(void) if (display_field) field.render(); // Render heads of other agents - for(std::vector::iterator agent = agentList.agents.begin(); agent != agentList.agents.end(); agent++) { + for(std::vector::iterator agent = agentList.getAgents().begin(); agent != agentList.getAgents().end(); agent++) { if (agent->linkedData != NULL) { Head *agentHead = (Head *)agent->linkedData; glPushMatrix(); @@ -659,7 +659,7 @@ void display(void) // Draw number of nearby people always char agents[100]; - sprintf(agents, "Agents nearby: %ld\n", agentList.agents.size()); + sprintf(agents, "Agents nearby: %ld\n", agentList.getAgents().size()); drawtext(WIDTH-200,20, 0.10, 0, 1.0, 0, agents, 1, 1, 0); glPopMatrix(); @@ -744,7 +744,7 @@ void *networkReceive(void *args) char *incomingPacket = new char[MAX_PACKET_SIZE]; while (!stopNetworkReceiveThread) { - if (agentList.getAgentSocket()->receive(&senderAddress, incomingPacket, &bytesReceived)) { + if (agentList.getAgentSocket().receive(&senderAddress, incomingPacket, &bytesReceived)) { packetcount++; bytescount += bytesReceived; diff --git a/shared/src/AgentList.cpp b/shared/src/AgentList.cpp index fdf4e430ff..0a72c3065e 100644 --- a/shared/src/AgentList.cpp +++ b/shared/src/AgentList.cpp @@ -30,8 +30,12 @@ AgentList::~AgentList() { stopSilentAgentRemovalThread(); } -UDPSocket * AgentList::getAgentSocket() { - return &agentSocket; +std::vector& AgentList::getAgents() { + return agents; +} + +UDPSocket& AgentList::getAgentSocket() { + return agentSocket; } void AgentList::processAgentData(sockaddr *senderAddress, void *packetData, size_t dataBytes) { diff --git a/shared/src/AgentList.h b/shared/src/AgentList.h index 734d64da9e..2755058503 100644 --- a/shared/src/AgentList.h +++ b/shared/src/AgentList.h @@ -16,18 +16,19 @@ const unsigned short AGENT_SOCKET_LISTEN_PORT = 40103; const int AGENT_SILENCE_THRESHOLD_USECS = 2 * 1000000; -extern const char *SOLO_AGENT_TYPES_STRINGg; +extern const char *SOLO_AGENT_TYPES_STRING; class AgentList { public: AgentList(); AgentList(int socketListenPort); ~AgentList(); - std::vector agents; + void(*linkedDataCreateCallback)(Agent *); void(*audioMixerSocketUpdate)(in_addr_t, in_port_t); - - UDPSocket* getAgentSocket(); + + std::vector& getAgents(); + UDPSocket& getAgentSocket(); int updateList(unsigned char *packetData, size_t dataBytes); bool addOrUpdateAgent(sockaddr *publicSocket, sockaddr *localSocket, char agentType); @@ -38,11 +39,13 @@ class AgentList { void startSilentAgentRemovalThread(); void stopSilentAgentRemovalThread(); private: + UDPSocket agentSocket; + std::vector agents; + pthread_t removeSilentAgentsThread; + int indexOfMatchingAgent(sockaddr *senderAddress); void updateAgentWithData(sockaddr *senderAddress, void *packetData, size_t dataBytes); void handlePingReply(sockaddr *agentAddress); - UDPSocket agentSocket; - pthread_t removeSilentAgentsThread; }; #endif /* defined(__hifi__AgentList__) */ From 34b8a7bbeb76f14e83960892eb9664fc8e5ab3df Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 25 Feb 2013 12:51:52 -0800 Subject: [PATCH 03/29] use getters and setters for all member variables in AgentList and Agent --- domain/src/main.cpp | 22 ++++++------ interface/src/main.cpp | 10 +++--- shared/src/Agent.cpp | 75 ++++++++++++++++++++++++++++++++++++---- shared/src/Agent.h | 33 ++++++++++++------ shared/src/AgentList.cpp | 51 ++++++++++++--------------- 5 files changed, 129 insertions(+), 62 deletions(-) diff --git a/domain/src/main.cpp b/domain/src/main.cpp index 7dd89df557..424416ca6c 100644 --- a/domain/src/main.cpp +++ b/domain/src/main.cpp @@ -44,10 +44,10 @@ int lastActiveCount = 0; AgentList agentList(DOMAIN_LISTEN_PORT); unsigned char * addAgentToBroadcastPacket(unsigned char *currentPosition, Agent *agentToAdd) { - *currentPosition++ = agentToAdd->type; + *currentPosition++ = agentToAdd->getType(); - currentPosition += packSocket(currentPosition, agentToAdd->publicSocket); - currentPosition += packSocket(currentPosition, agentToAdd->localSocket); + currentPosition += packSocket(currentPosition, agentToAdd->getPublicSocket()); + currentPosition += packSocket(currentPosition, agentToAdd->getLocalSocket()); // return the new unsigned char * for broadcast packet return currentPosition; @@ -73,7 +73,7 @@ int main(int argc, const char * argv[]) std::map newestSoloAgents; while (true) { - if (agentList.getAgentSocket()->receive((sockaddr *)&agentPublicAddress, packetData, &receivedBytes)) { + if (agentList.getAgentSocket().receive((sockaddr *)&agentPublicAddress, packetData, &receivedBytes)) { agentType = packetData[0]; unpackSocket(&packetData[1], (sockaddr *)&agentLocalAddress); @@ -82,24 +82,24 @@ int main(int argc, const char * argv[]) currentBufferPos = broadcastPacket + 1; startPointer = currentBufferPos; - for(std::vector::iterator agent = agentList.agents.begin(); agent != agentList.agents.end(); agent++) { + for(std::vector::iterator agent = agentList.getAgents().begin(); agent != agentList.getAgents().end(); agent++) { if (DEBUG_TO_SELF || !agent->matches((sockaddr *)&agentPublicAddress, (sockaddr *)&agentLocalAddress, agentType)) { - if (strchr(SOLO_AGENT_TYPES_STRING, (int) agent->type) == NULL) { + if (strchr(SOLO_AGENT_TYPES_STRING, (int) agent->getType()) == NULL) { // this is an agent of which there can be multiple, just add them to the packet currentBufferPos = addAgentToBroadcastPacket(currentBufferPos, &(*agent)); } else { std::cout << "We have a solo agent: " << &(*agent) << "\n"; // solo agent, we need to only send newest - if (newestSoloAgents[agent->type] == NULL || - newestSoloAgents[agent->type]->firstRecvTimeUsecs < agent->firstRecvTimeUsecs) { + if (newestSoloAgents[agent->getType()] == NULL || + newestSoloAgents[agent->getType()]->getFirstRecvTimeUsecs() < agent->getFirstRecvTimeUsecs()) { // we have to set the newer solo agent to add it to the broadcast later - newestSoloAgents[agent->type] = &(*agent); + newestSoloAgents[agent->getType()] = &(*agent); } } } else { // this is the agent, just update last receive to now - agent->lastRecvTimeUsecs = usecTimestampNow(); + agent->setLastRecvTimeUsecs(usecTimestampNow()); } } @@ -112,7 +112,7 @@ int main(int argc, const char * argv[]) } if ((packetBytesWithoutLeadingChar = (currentBufferPos - startPointer))) { - agentList.getAgentSocket()->send((sockaddr *)&agentPublicAddress, broadcastPacket, packetBytesWithoutLeadingChar + 1); + agentList.getAgentSocket().send((sockaddr *)&agentPublicAddress, broadcastPacket, packetBytesWithoutLeadingChar + 1); } } } diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 9b3ad7a39f..6d19004bfe 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -571,8 +571,8 @@ void display(void) // Render heads of other agents for(std::vector::iterator agent = agentList.getAgents().begin(); agent != agentList.getAgents().end(); agent++) { - if (agent->linkedData != NULL) { - Head *agentHead = (Head *)agent->linkedData; + if (agent->getLinkedData() != NULL) { + Head *agentHead = (Head *)agent->getLinkedData(); glPushMatrix(); glm::vec3 pos = agentHead->getPos(); glTranslatef(-pos.x, -pos.y, -pos.z); @@ -583,7 +583,7 @@ void display(void) if (!display_head) balls.render(); - // Render the world box + // Render the world box if (!display_head && stats_on) render_world_box(); // Render my own head @@ -851,8 +851,8 @@ void mouseoverFunc( int x, int y) } void attachNewHeadToAgent(Agent *newAgent) { - if (newAgent->linkedData == NULL) { - newAgent->linkedData = new Head(); + if (newAgent->getLinkedData() == NULL) { + newAgent->setLinkedData(new Head()); } } diff --git a/shared/src/Agent.cpp b/shared/src/Agent.cpp index 5790329c80..a635314b64 100644 --- a/shared/src/Agent.cpp +++ b/shared/src/Agent.cpp @@ -58,6 +58,75 @@ Agent& Agent::operator=(Agent otherAgent) { return *this; } +Agent::~Agent() { + delete publicSocket; + delete localSocket; + delete linkedData; +} + +char Agent::getType() { + return type; +} + +void Agent::setType(char newType) { + type = newType; +} + +double Agent::getFirstRecvTimeUsecs() { + return firstRecvTimeUsecs; +} + +void Agent::setFirstRecvTimeUsecs(double newTimeUsecs) { + firstRecvTimeUsecs = newTimeUsecs; +} + +double Agent::getLastRecvTimeUsecs() { + return lastRecvTimeUsecs; +} + +void Agent::setLastRecvTimeUsecs(double newTimeUsecs) { + lastRecvTimeUsecs = newTimeUsecs; +} + +sockaddr* Agent::getPublicSocket() { + return publicSocket; +} + +void Agent::setPublicSocket(sockaddr *newSocket) { + publicSocket = newSocket; +} + +sockaddr* Agent::getLocalSocket() { + return localSocket; +} + +void Agent::setLocalSocket(sockaddr *newSocket) { + publicSocket = newSocket; +} + +sockaddr* Agent::getActiveSocket() { + return activeSocket; +} + +void Agent::activateLocalSocket() { + activeSocket = localSocket; +} + +void Agent::activatePublicSocket() { + activeSocket = publicSocket; +} + + + +AgentData* Agent::getLinkedData() { + return linkedData; +} + +void Agent::setLinkedData(AgentData *newData) { + linkedData = newData; +} + + bool Agent::operator==(const Agent& otherAgent) { return matches(otherAgent.publicSocket, otherAgent.localSocket, otherAgent.type); } @@ -71,12 +140,6 @@ void Agent::swap(Agent &first, Agent &second) { swap(first.linkedData, second.linkedData); } -Agent::~Agent() { - delete publicSocket; - delete localSocket; - delete linkedData; -} - bool Agent::matches(sockaddr *otherPublicSocket, sockaddr *otherLocalSocket, char otherAgentType) { // checks if two agent objects are the same agent (same type + local + public address) return type == otherAgentType diff --git a/shared/src/Agent.h b/shared/src/Agent.h index 90073a92b0..dafbeb768f 100644 --- a/shared/src/Agent.h +++ b/shared/src/Agent.h @@ -18,24 +18,35 @@ class Agent { Agent(); Agent(sockaddr *agentPublicSocket, sockaddr *agentLocalSocket, char agentType); Agent(const Agent &otherAgent); + ~Agent(); Agent& operator=(Agent otherAgent); bool operator==(const Agent& otherAgent); - ~Agent(); - + bool matches(sockaddr *otherPublicSocket, sockaddr *otherLocalSocket, char otherAgentType); - - sockaddr *publicSocket, *localSocket, *activeSocket; - char type; - timeval pingStarted; - int pingMsecs; - double firstRecvTimeUsecs; - double lastRecvTimeUsecs; - bool isSelf; - AgentData *linkedData; + char getType(); + void setType(char newType); + double getFirstRecvTimeUsecs(); + void setFirstRecvTimeUsecs(double newTimeUsecs); + double getLastRecvTimeUsecs(); + void setLastRecvTimeUsecs(double newTimeUsecs); + sockaddr* getPublicSocket(); + void setPublicSocket(sockaddr *newSocket); + sockaddr* getLocalSocket(); + void setLocalSocket(sockaddr *newSocket); + sockaddr* getActiveSocket(); + void activatePublicSocket(); + void activateLocalSocket(); + AgentData* getLinkedData(); + void setLinkedData(AgentData *newData); friend std::ostream& operator<<(std::ostream& os, const Agent* agent); private: void swap(Agent &first, Agent &second); + sockaddr *publicSocket, *localSocket, *activeSocket; + char type; + double firstRecvTimeUsecs; + double lastRecvTimeUsecs; + AgentData *linkedData; }; std::ostream& operator<<(std::ostream& os, const Agent* agent); diff --git a/shared/src/AgentList.cpp b/shared/src/AgentList.cpp index 0a72c3065e..d1c4db24ce 100644 --- a/shared/src/AgentList.cpp +++ b/shared/src/AgentList.cpp @@ -75,22 +75,22 @@ void AgentList::updateAgentWithData(sockaddr *senderAddress, void *packetData, s if (agentIndex != -1) { Agent *matchingAgent = &agents[agentIndex]; - matchingAgent->lastRecvTimeUsecs = usecTimestampNow(); + matchingAgent->setLastRecvTimeUsecs(usecTimestampNow()); - if (matchingAgent->linkedData == NULL) { + if (matchingAgent->getLinkedData() == NULL) { if (linkedDataCreateCallback != NULL) { linkedDataCreateCallback(matchingAgent); } } - matchingAgent->linkedData->parseData(packetData, dataBytes); + matchingAgent->getLinkedData()->parseData(packetData, dataBytes); } } int AgentList::indexOfMatchingAgent(sockaddr *senderAddress) { for(std::vector::iterator agent = agents.begin(); agent != agents.end(); agent++) { - if (agent->activeSocket != NULL && socketMatch(agent->activeSocket, senderAddress)) { + if (agent->getActiveSocket() != NULL && socketMatch(agent->getActiveSocket(), senderAddress)) { return agent - agents.begin(); } } @@ -140,10 +140,10 @@ bool AgentList::addOrUpdateAgent(sockaddr *publicSocket, sockaddr *localSocket, if (socketMatch(publicSocket, localSocket)) { // likely debugging scenario with DS + agent on local network // set the agent active right away - newAgent.activeSocket = newAgent.localSocket; + newAgent.activatePublicSocket(); } - if (newAgent.type == 'M' && audioMixerSocketUpdate != NULL) { + if (newAgent.getType() == 'M' && audioMixerSocketUpdate != NULL) { // this is an audio mixer // for now that means we need to tell the audio class // to use the local socket information the domain server gave us @@ -160,10 +160,10 @@ bool AgentList::addOrUpdateAgent(sockaddr *publicSocket, sockaddr *localSocket, return true; } else { - if (agent->type == 'M') { + if (agent->getType() == 'M') { // until the Audio class also uses our agentList, we need to update // the lastRecvTimeUsecs for the audio mixer so it doesn't get killed and re-added continously - agent->lastRecvTimeUsecs = usecTimestampNow(); + agent->setLastRecvTimeUsecs(usecTimestampNow()); } // we had this agent already, do nothing for now @@ -175,9 +175,9 @@ void AgentList::broadcastToAgents(char *broadcastData, size_t dataBytes) { for(std::vector::iterator agent = agents.begin(); agent != agents.end(); agent++) { // for now assume we only want to send to other interface clients // until the Audio class uses the AgentList - if (agent->activeSocket != NULL && agent->type == 'I') { + if (agent->getActiveSocket() != NULL && agent->getType() == 'I') { // we know which socket is good for this agent, send there - agentSocket.send(agent->activeSocket, broadcastData, dataBytes); + agentSocket.send(agent->getActiveSocket(), broadcastData, dataBytes); } } } @@ -186,15 +186,15 @@ void AgentList::pingAgents() { char payload[] = "P"; for(std::vector::iterator agent = agents.begin(); agent != agents.end(); agent++) { - if (agent->type == 'I') { - if (agent->activeSocket != NULL) { + if (agent->getType() == 'I') { + if (agent->getActiveSocket() != NULL) { // we know which socket is good for this agent, send there - agentSocket.send(agent->activeSocket, payload, 1); + agentSocket.send(agent->getActiveSocket(), payload, 1); } else { // ping both of the sockets for the agent so we can figure out // which socket we can use - agentSocket.send(agent->publicSocket, payload, 1); - agentSocket.send(agent->localSocket, payload, 1); + agentSocket.send(agent->getPublicSocket(), payload, 1); + agentSocket.send(agent->getLocalSocket(), payload, 1); } } } @@ -203,19 +203,12 @@ void AgentList::pingAgents() { void AgentList::handlePingReply(sockaddr *agentAddress) { for(std::vector::iterator agent = agents.begin(); agent != agents.end(); agent++) { // check both the public and local addresses for each agent to see if we find a match - // prioritize the private address so that we prune erroneous local matches - sockaddr *matchedSocket = NULL; - - if (socketMatch(agent->publicSocket, agentAddress)) { - matchedSocket = agent->publicSocket; - } else if (socketMatch(agent->localSocket, agentAddress)) { - matchedSocket = agent->localSocket; - } - - if (matchedSocket != NULL) { - // matched agent, stop checking - // update the agent's ping - agent->activeSocket = matchedSocket; + // prioritize the private address so that we prune erroneous local matches + if (socketMatch(agent->getPublicSocket(), agentAddress)) { + agent->activatePublicSocket(); + break; + } else if (socketMatch(agent->getLocalSocket(), agentAddress)) { + agent->activateLocalSocket(); break; } } @@ -229,7 +222,7 @@ void *removeSilentAgents(void *args) { checkTimeUSecs = usecTimestampNow(); for(std::vector::iterator agent = agents->begin(); agent != agents->end();) { - if ((checkTimeUSecs - agent->lastRecvTimeUsecs) > AGENT_SILENCE_THRESHOLD_USECS) { + if ((checkTimeUSecs - agent->getLastRecvTimeUsecs()) > AGENT_SILENCE_THRESHOLD_USECS) { std::cout << "Killing agent " << &(*agent) << "\n"; pthread_mutex_lock(&vectorChangeMutex); agent = agents->erase(agent); From b7fed56c1600d40498058ea9a8fb7cbec62f5b82 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 25 Feb 2013 13:06:36 -0800 Subject: [PATCH 04/29] remove extraneous logging for Domain server solo agents --- domain/src/main.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/domain/src/main.cpp b/domain/src/main.cpp index 424416ca6c..7b6f75ad3f 100644 --- a/domain/src/main.cpp +++ b/domain/src/main.cpp @@ -89,7 +89,6 @@ int main(int argc, const char * argv[]) // this is an agent of which there can be multiple, just add them to the packet currentBufferPos = addAgentToBroadcastPacket(currentBufferPos, &(*agent)); } else { - std::cout << "We have a solo agent: " << &(*agent) << "\n"; // solo agent, we need to only send newest if (newestSoloAgents[agent->getType()] == NULL || newestSoloAgents[agent->getType()]->getFirstRecvTimeUsecs() < agent->getFirstRecvTimeUsecs()) { @@ -106,7 +105,6 @@ int main(int argc, const char * argv[]) for (std::map::iterator agentIterator = newestSoloAgents.begin(); agentIterator != newestSoloAgents.end(); agentIterator++) { - std::cout << "Newest agent: " << agentIterator->second << "\n"; // this is the newest alive solo agent, add them to the packet currentBufferPos = addAgentToBroadcastPacket(currentBufferPos, agentIterator->second); } From 888ef2249d025a9a47ae8078d8c3af749cf29ee4 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Mon, 25 Feb 2013 14:31:56 -0800 Subject: [PATCH 05/29] Added angular velocity and angular movement to hand, changed hand to block. --- interface/src/Hand.cpp | 57 ++++++++++++++++++++++++++++++++++++------ interface/src/Hand.h | 4 ++- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/interface/src/Hand.cpp b/interface/src/Hand.cpp index 89209d309a..16698a74d1 100644 --- a/interface/src/Hand.cpp +++ b/interface/src/Hand.cpp @@ -24,14 +24,24 @@ Hand::Hand(glm::vec3 initcolor) scale.z = scale.y * 1.0; } +void Hand::addAngularVelocity (float pRate, float yRate, float rRate) { + pitchRate += pRate; + yawRate += yRate; + rollRate += rRate; +} + void Hand::render() { -// glPushMatrix(); -// glTranslatef(position.x, position.y, position.z); -// glColor3f(color.x, color.y, color.z); -// glScalef(scale.x, scale.y, scale.z); -// glutSolidSphere(1.5, 20, 20); -// glPopMatrix(); + glPushMatrix(); + glTranslatef(position.x, position.y, position.z); + glRotatef(yaw, 0, 1, 0); + glRotatef(pitch, 1, 0, 0); + glRotatef(roll, 0, 0, 1); + glColor3f(color.x, color.y, color.z); + glScalef(scale.x, scale.y, scale.z); + //glutSolidSphere(1.5, 20, 20); + glutSolidCube(1.0); + glPopMatrix(); } void Hand::reset() @@ -39,17 +49,48 @@ void Hand::reset() position.x = DEFAULT_X; position.y = DEFAULT_Y; position.z = DEFAULT_Z; + pitch = yaw = roll = 0; + pitchRate = yawRate = rollRate = 0; setTarget(position); velocity.x = velocity.y = velocity.z = 0; } void Hand::simulate(float deltaTime) { - // If noise, add wandering movement + const float VNOISE = 0.1; + const float RSPRING = 0.01; + const float RNOISE = 0.1; + + // If noise, add a bit of random velocity if (noise) { - position += noise * 0.1f * glm::vec3(randFloat() - 0.5, randFloat() - 0.5, randFloat() - 0.5); + glm::vec3 nVel(randFloat() - 0.5f, randFloat() - 0.5f, randFloat() - 0.5f); + nVel *= VNOISE; + addVelocity(nVel); + + addAngularVelocity(RNOISE*(randFloat() - 0.5f), + RNOISE*(randFloat() - 0.5f), + RNOISE*(randFloat() - 0.5f)); } + position += velocity*deltaTime; + + pitch += pitchRate; + yaw += yawRate; + roll += rollRate; + // Decay position of hand toward target position -= deltaTime*(position - target); + // Decay velocity + velocity *= 1.0 - deltaTime; + + // Decay Angular Velocity + pitchRate *= 1.0 - deltaTime; + yawRate *= 1.0 - deltaTime; + rollRate *= 1.0 - deltaTime; + + // Add spring effect to return hand rotation to zero + pitchRate -= pitch * RSPRING; + yawRate -= yaw * RSPRING; + rollRate -= roll * RSPRING; + } \ No newline at end of file diff --git a/interface/src/Hand.h b/interface/src/Hand.h index 9f64343673..01478839ee 100644 --- a/interface/src/Hand.h +++ b/interface/src/Hand.h @@ -23,12 +23,14 @@ public: void render (); void reset (); void setNoise (float mag) { noise = mag; }; - void addVel (glm::vec3 v) { velocity += v; }; + void addVelocity (glm::vec3 v) { velocity += v; }; + void addAngularVelocity (float pRate, float yRate, float rRate); glm::vec3 getPos() { return position; }; void setPos(glm::vec3 p) { position = p; }; void setTarget(glm::vec3 t) { target = t; }; private: glm::vec3 position, target, velocity, color, scale; + float pitch, yaw, roll, pitchRate, yawRate, rollRate; float noise; }; From eb37d1fbf8b588ff970e531124b1ba0ca6a5afb3 Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Mon, 25 Feb 2013 15:06:28 -0800 Subject: [PATCH 06/29] Added ability to drag hand by clicking and dragging on the screen. --- interface/src/Hand.cpp | 12 +++++++++--- interface/src/Head.h | 4 +++- interface/src/main.cpp | 13 +++++++++++-- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/interface/src/Hand.cpp b/interface/src/Hand.cpp index 16698a74d1..dc6f73e456 100644 --- a/interface/src/Hand.cpp +++ b/interface/src/Hand.cpp @@ -57,9 +57,11 @@ void Hand::reset() void Hand::simulate(float deltaTime) { - const float VNOISE = 0.1; + const float VNOISE = 0.01; const float RSPRING = 0.01; + const float PSPRING = 0.4; const float RNOISE = 0.1; + const float VDECAY = 5.0; // If noise, add a bit of random velocity if (noise) { @@ -77,11 +79,15 @@ void Hand::simulate(float deltaTime) yaw += yawRate; roll += rollRate; + // Spring effect to return hand to target; + glm::vec3 sVel = target - position; + sVel *= PSPRING; + addVelocity(sVel); // Decay position of hand toward target - position -= deltaTime*(position - target); + //position -= deltaTime*(position - target); // Decay velocity - velocity *= 1.0 - deltaTime; + velocity *= 1.0 - deltaTime*VDECAY; // Decay Angular Velocity pitchRate *= 1.0 - deltaTime; diff --git a/interface/src/Head.h b/interface/src/Head.h index 038f37bfc5..8fc73c7e8c 100644 --- a/interface/src/Head.h +++ b/interface/src/Head.h @@ -60,6 +60,9 @@ class Head : public AgentData { void SetNewHeadTarget(float, float); glm::vec3 getPos() { return position; }; void setPos(glm::vec3 newpos) { position = newpos; }; + + Hand * hand; + private: float noise; float Pitch; @@ -99,7 +102,6 @@ class Head : public AgentData { void readSensors(); float renderYaw, renderPitch; // Pitch from view frustum when this is own head. - Hand * hand; }; #endif \ No newline at end of file diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 6d19004bfe..3ae6e4e411 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -144,14 +144,15 @@ int display_head_mouse = 1; // Display sample mouse pointer controlled int head_mouse_x, head_mouse_y; int head_lean_x, head_lean_y; -int mouse_x, mouse_y; // Where is the mouse +int mouse_x, mouse_y; // Where is the mouse +int mouse_start_x, mouse_start_y; // Mouse location at start of last down click int mouse_pressed = 0; // true if mouse has been pressed (clear when finished) int nearbyAgents = 0; // How many other people near you is the domain server reporting? int speed; -// +// // Serial USB Variables // @@ -813,6 +814,8 @@ void mouseFunc( int button, int state, int x, int y ) mouse_y = y; mouse_pressed = 1; lattice.mouseClick((float)x/(float)WIDTH,(float)y/(float)HEIGHT); + mouse_start_x = x; + mouse_start_y = y; } if( button == GLUT_LEFT_BUTTON && state == GLUT_UP ) { @@ -833,6 +836,12 @@ void motionFunc( int x, int y) char mouse_string[20]; sprintf(mouse_string, "M %d %d\n", mouse_x, mouse_y); //network_send(UDP_socket, mouse_string, strlen(mouse_string)); + + // Send dragged mouse vector to the hand; + float dx = mouse_x - mouse_start_x; + float dy = mouse_y - mouse_start_y; + glm::vec3 vel(dx*0.003, -dy*0.003, 0); + myHead.hand->addVelocity(vel); } lattice.mouseClick((float)x/(float)WIDTH,(float)y/(float)HEIGHT); From 82435582a804ff9d34e69691ea34beb543ec5d5f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 25 Feb 2013 15:19:53 -0800 Subject: [PATCH 07/29] fix birarda's stupid bug in timestamp creation --- shared/src/SharedUtil.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/src/SharedUtil.cpp b/shared/src/SharedUtil.cpp index 5880b8919a..d9de883024 100644 --- a/shared/src/SharedUtil.cpp +++ b/shared/src/SharedUtil.cpp @@ -9,11 +9,11 @@ #include "SharedUtil.h" double usecTimestamp(timeval *time) { - return (time->tv_sec * 1000000.0); + return (time->tv_sec * 1000000.0 + time->tv_usec); } double usecTimestampNow() { timeval now; gettimeofday(&now, NULL); - return (now.tv_sec * 1000000.0); + return (now.tv_sec * 1000000.0 + now.tv_usec); } \ No newline at end of file From f55782b9681d625ec768c7b5653bc01007583e7f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 25 Feb 2013 17:07:32 -0800 Subject: [PATCH 08/29] refactor mixer to use AgentList, AudioRingBuffer as AgentData --- interface/src/Audio.cpp | 53 +++---- interface/src/Head.cpp | 4 + interface/src/Head.h | 2 + mixer/src/main.cpp | 266 +++++++++++---------------------- shared/src/Agent.cpp | 9 +- shared/src/AgentData.h | 1 + shared/src/AgentList.h | 2 +- shared/src/AudioRingBuffer.cpp | 89 +++++++++-- shared/src/AudioRingBuffer.h | 36 ++++- shared/src/UDPSocket.cpp | 1 - 10 files changed, 222 insertions(+), 241 deletions(-) diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index b433b2a199..27c2a33a0b 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -18,18 +18,12 @@ Oscilloscope * scope; -const short BUFFER_LENGTH_BYTES = 1024; -const short BUFFER_LENGTH_SAMPLES = BUFFER_LENGTH_BYTES / sizeof(int16_t); - const short PACKET_LENGTH_BYTES = 1024; const short PACKET_LENGTH_SAMPLES = PACKET_LENGTH_BYTES / sizeof(int16_t); const int PHASE_DELAY_AT_90 = 20; const float AMPLITUDE_RATIO_AT_90 = 0.5; -const short RING_BUFFER_FRAMES = 10; -const short RING_BUFFER_SIZE_SAMPLES = RING_BUFFER_FRAMES * BUFFER_LENGTH_SAMPLES; - const int SAMPLE_RATE = 22050; const float JITTER_BUFFER_LENGTH_MSECS = 30.0; const short JITTER_BUFFER_SAMPLES = JITTER_BUFFER_LENGTH_MSECS * (SAMPLE_RATE / 1000.0); @@ -90,7 +84,7 @@ int audioCallback (const void *inputBuffer, audioMixerSocket.sin_family = AF_INET; audioMixerSocket.sin_addr.s_addr = data->mixerAddress; audioMixerSocket.sin_port = data->mixerPort; - data->audioSocket->send((sockaddr *)&audioMixerSocket, (void *)inputLeft, BUFFER_LENGTH_BYTES); + data->audioSocket->send((sockaddr *)&audioMixerSocket, (void *)inputLeft, BUFFER_LENGTH_BYTES); } // @@ -134,27 +128,27 @@ int audioCallback (const void *inputBuffer, // if we've been reset, and there isn't any new packets yet // just play some silence - if (ringBuffer->endOfLastWrite != NULL) { + if (ringBuffer->getEndOfLastWrite() != NULL) { - if (!ringBuffer->started && ringBuffer->diffLastWriteNextOutput() <= PACKET_LENGTH_SAMPLES + JITTER_BUFFER_SAMPLES) { + if (!ringBuffer->isStarted() && ringBuffer->diffLastWriteNextOutput() <= PACKET_LENGTH_SAMPLES + JITTER_BUFFER_SAMPLES) { printf("Held back\n"); } else if (ringBuffer->diffLastWriteNextOutput() < PACKET_LENGTH_SAMPLES) { - ringBuffer->started = false; + ringBuffer->setStarted(false); starve_counter++; printf("Starved #%d\n", starve_counter); data->wasStarved = 10; // Frames to render the indication that the system was starved. } else { - ringBuffer->started = true; + ringBuffer->setStarted(true); // play whatever we have in the audio buffer // no sample overlap, either a direct copy of the audio data, or a copy with some appended silence - memcpy(queueBuffer, ringBuffer->nextOutput, BUFFER_LENGTH_BYTES); + memcpy(queueBuffer, ringBuffer->getNextOutput(), BUFFER_LENGTH_BYTES); - ringBuffer->nextOutput += BUFFER_LENGTH_SAMPLES; + ringBuffer->setNextOutput(ringBuffer->getNextOutput() + BUFFER_LENGTH_SAMPLES); - if (ringBuffer->nextOutput == ringBuffer->buffer + RING_BUFFER_SIZE_SAMPLES) { - ringBuffer->nextOutput = ringBuffer->buffer; + if (ringBuffer->getNextOutput() == ringBuffer->getBuffer() + RING_BUFFER_SAMPLES) { + ringBuffer->setNextOutput(ringBuffer->getBuffer()); } } } @@ -204,7 +198,6 @@ void *receiveAudioViaUDP(void *args) { while (!stopAudioReceiveThread) { if (sharedAudioData->audioSocket->receive((void *)receivedData, &receivedBytes)) { - bool firstSample = (currentReceiveTime.tv_sec == 0); gettimeofday(¤tReceiveTime, NULL); @@ -233,28 +226,24 @@ void *receiveAudioViaUDP(void *args) { AudioRingBuffer *ringBuffer = sharedAudioData->ringBuffer; - if (ringBuffer->endOfLastWrite == NULL) { - ringBuffer->endOfLastWrite = ringBuffer->buffer; - } else if (ringBuffer->diffLastWriteNextOutput() > RING_BUFFER_SIZE_SAMPLES - PACKET_LENGTH_SAMPLES) { - std::cout << "NAB: " << ringBuffer->nextOutput - ringBuffer->buffer << "\n"; - std::cout << "LAW: " << ringBuffer->endOfLastWrite - ringBuffer->buffer << "\n"; - std::cout << "D: " << ringBuffer->diffLastWriteNextOutput() << "\n"; - std::cout << "Full\n"; + if (ringBuffer->getEndOfLastWrite() == NULL) { + ringBuffer->setEndOfLastWrite(ringBuffer->getBuffer()); + } else if (ringBuffer->diffLastWriteNextOutput() > RING_BUFFER_SAMPLES - PACKET_LENGTH_SAMPLES) { // reset us to started state - ringBuffer->endOfLastWrite = ringBuffer->buffer; - ringBuffer->nextOutput = ringBuffer->buffer; - ringBuffer->started = false; + ringBuffer->setEndOfLastWrite(ringBuffer->getBuffer()); + ringBuffer->setNextOutput(ringBuffer->getBuffer()); + ringBuffer->setStarted(false); } - int16_t *copyToPointer = ringBuffer->endOfLastWrite; + int16_t *copyToPointer = ringBuffer->getEndOfLastWrite(); // just copy the recieved data to the right spot and then add packet length to previous pointer memcpy(copyToPointer, receivedData, PACKET_LENGTH_BYTES); - ringBuffer->endOfLastWrite += PACKET_LENGTH_SAMPLES; + ringBuffer->setEndOfLastWrite(ringBuffer->getEndOfLastWrite() + PACKET_LENGTH_SAMPLES); - if (ringBuffer->endOfLastWrite == ringBuffer->buffer + RING_BUFFER_SIZE_SAMPLES) { - ringBuffer->endOfLastWrite = ringBuffer->buffer; + if (ringBuffer->getEndOfLastWrite() == ringBuffer->getBuffer() + RING_BUFFER_SAMPLES) { + ringBuffer->setEndOfLastWrite(ringBuffer->getBuffer()); } if (LOG_SAMPLE_DELAY) { @@ -284,7 +273,7 @@ Audio::Audio(Oscilloscope * s) // setup a UDPSocket audioData->audioSocket = new UDPSocket(AUDIO_UDP_LISTEN_PORT); - audioData->ringBuffer = new AudioRingBuffer(RING_BUFFER_SIZE_SAMPLES); + audioData->ringBuffer = new AudioRingBuffer(); AudioRecThreadStruct threadArgs; threadArgs.sharedAudioData = audioData; @@ -359,7 +348,7 @@ void Audio::render(int screenWidth, int screenHeight) float timeLeftInCurrentBuffer = 0; if (audioData->lastCallback.tv_usec > 0) timeLeftInCurrentBuffer = diffclock(&audioData->lastCallback, ¤tTime)/(1000.0*(float)BUFFER_LENGTH_SAMPLES/(float)SAMPLE_RATE) * frameWidth; - if (audioData->ringBuffer->endOfLastWrite != NULL) + if (audioData->ringBuffer->getEndOfLastWrite() != NULL) remainingBuffer = audioData->ringBuffer->diffLastWriteNextOutput() / BUFFER_LENGTH_SAMPLES * frameWidth; if (audioData->wasStarved == 0) glColor3f(0, 1, 0); diff --git a/interface/src/Head.cpp b/interface/src/Head.cpp index d69a3fa35f..6930d7a77a 100644 --- a/interface/src/Head.cpp +++ b/interface/src/Head.cpp @@ -64,6 +64,10 @@ Head::~Head() { // all data is primitive, do nothing } +Head* Head::clone() const { + return new Head(*this); +} + void Head::reset() { Pitch = Yaw = Roll = 0; diff --git a/interface/src/Head.h b/interface/src/Head.h index 8fc73c7e8c..b554d4541e 100644 --- a/interface/src/Head.h +++ b/interface/src/Head.h @@ -24,6 +24,8 @@ class Head : public AgentData { public: Head(); ~Head(); + Head* clone() const; + void reset(); void UpdatePos(float frametime, SerialInterface * serialInterface, int head_mirror, glm::vec3 * gravity); void setNoise (float mag) { noise = mag; } diff --git a/mixer/src/main.cpp b/mixer/src/main.cpp index eb125e1831..379298b828 100644 --- a/mixer/src/main.cpp +++ b/mixer/src/main.cpp @@ -13,24 +13,21 @@ #include #include #include "AudioRingBuffer.h" -#include "UDPSocket.h" +#include +#include const int MAX_AGENTS = 1000; const int LOGOFF_CHECK_INTERVAL = 1000; const unsigned short MIXER_LISTEN_PORT = 55443; -const int BUFFER_LENGTH_BYTES = 1024; -const int BUFFER_LENGTH_SAMPLES = BUFFER_LENGTH_BYTES / sizeof(int16_t); + const float SAMPLE_RATE = 22050.0; const float BUFFER_SEND_INTERVAL_USECS = (BUFFER_LENGTH_SAMPLES/SAMPLE_RATE) * 1000000; const short JITTER_BUFFER_MSECS = 20; const short JITTER_BUFFER_SAMPLES = JITTER_BUFFER_MSECS * (SAMPLE_RATE / 1000.0); -const short RING_BUFFER_FRAMES = 10; -const short RING_BUFFER_SAMPLES = RING_BUFFER_FRAMES * BUFFER_LENGTH_SAMPLES; - const long MAX_SAMPLE_VALUE = std::numeric_limits::max(); const long MIN_SAMPLE_VALUE = std::numeric_limits::min(); @@ -40,190 +37,100 @@ const int DOMAINSERVER_PORT = 40102; const int MAX_SOURCE_BUFFERS = 20; -sockaddr_in agentAddress; - -UDPSocket audioSocket = UDPSocket(MIXER_LISTEN_PORT); - -struct AgentList { - char *address; - unsigned short port; - bool active; - timeval time; - bool bufferTransmitted; -} agents[MAX_AGENTS]; - -int numAgents = 0; - -AudioRingBuffer *sourceBuffers[MAX_SOURCE_BUFFERS]; - -double diffclock(timeval *clock1, timeval *clock2) -{ - double diffms = (clock2->tv_sec - clock1->tv_sec) * 1000.0; - diffms += (clock2->tv_usec - clock1->tv_usec) / 1000.0; // us to ms - return diffms; -} - -double usecTimestamp(timeval *time, double addedUsecs = 0) { - return (time->tv_sec * 1000000.0) + time->tv_usec + addedUsecs; -} +AgentList agentList(MIXER_LISTEN_PORT); void *sendBuffer(void *args) { int sentBytes; - int currentFrame = 1; - timeval startTime, sendTime, now; + int nextFrame = 0; + timeval startTime; int16_t *clientMix = new int16_t[BUFFER_LENGTH_SAMPLES]; long *masterMix = new long[BUFFER_LENGTH_SAMPLES]; - + gettimeofday(&startTime, NULL); while (true) { sentBytes = 0; - - for (int wb = 0; wb < BUFFER_LENGTH_SAMPLES; wb++) { - masterMix[wb] = 0; + + for (int ms = 0; ms < BUFFER_LENGTH_SAMPLES; ms++) { + masterMix[ms] = 0; } - gettimeofday(&sendTime, NULL); - - for (int b = 0; b < MAX_SOURCE_BUFFERS; b++) { - if (sourceBuffers[b]->endOfLastWrite != NULL) { - if (!sourceBuffers[b]->started - && sourceBuffers[b]->diffLastWriteNextOutput() <= BUFFER_LENGTH_SAMPLES + JITTER_BUFFER_SAMPLES) { - std::cout << "Held back buffer " << b << ".\n"; - } else if (sourceBuffers[b]->diffLastWriteNextOutput() < BUFFER_LENGTH_SAMPLES) { - std::cout << "Buffer " << b << " starved.\n"; - sourceBuffers[b]->started = false; - } else { - sourceBuffers[b]->started = true; - agents[b].bufferTransmitted = true; - - for (int s = 0; s < BUFFER_LENGTH_SAMPLES; s++) { - masterMix[s] += sourceBuffers[b]->nextOutput[s]; - } - - sourceBuffers[b]->nextOutput += BUFFER_LENGTH_SAMPLES; - - if (sourceBuffers[b]->nextOutput >= sourceBuffers[b]->buffer + RING_BUFFER_SAMPLES) { - sourceBuffers[b]->nextOutput = sourceBuffers[b]->buffer; - } - } - } - } - - for (int a = 0; a < numAgents; a++) { - if (diffclock(&agents[a].time, &sendTime) <= LOGOFF_CHECK_INTERVAL) { - - int16_t *previousOutput = NULL; - if (agents[a].bufferTransmitted) { - previousOutput = (sourceBuffers[a]->nextOutput == sourceBuffers[a]->buffer) - ? sourceBuffers[a]->buffer + RING_BUFFER_SAMPLES - BUFFER_LENGTH_SAMPLES - : sourceBuffers[a]->nextOutput - BUFFER_LENGTH_SAMPLES; - agents[a].bufferTransmitted = false; - } - - for(int as = 0; as < BUFFER_LENGTH_SAMPLES; as++) { - long longSample = previousOutput != NULL - ? masterMix[as] - previousOutput[as] - : masterMix[as]; - - int16_t shortSample; - - if (longSample < 0) { - shortSample = std::max(longSample, MIN_SAMPLE_VALUE); - } else { - shortSample = std::min(longSample, MAX_SAMPLE_VALUE); - } - - clientMix[as] = shortSample; - - // std::cout << as << " - CM: " << clientMix[as] << " MM: " << masterMix[as] << "\n"; - // std::cout << previousOutput - sourceBuffers[a]->buffer << "\n"; - - if (previousOutput != NULL) { - // std::cout << "PO: " << previousOutput[as] << "\n"; - } - - } - - sentBytes = audioSocket.send(agents[a].address, agents[a].port, clientMix, BUFFER_LENGTH_BYTES); + for (int ab = 0; ab < agentList.getAgents().size(); ab++) { + AudioRingBuffer *agentBuffer = (AudioRingBuffer *)agentList.getAgents()[ab].getLinkedData(); - if (sentBytes < BUFFER_LENGTH_BYTES) { - std::cout << "Error sending mix packet! " << sentBytes << strerror(errno) << "\n"; + if (agentBuffer != NULL && agentBuffer->getEndOfLastWrite() != NULL) { + if (!agentBuffer->isStarted() && agentBuffer->diffLastWriteNextOutput() <= BUFFER_LENGTH_SAMPLES + JITTER_BUFFER_SAMPLES) { + printf("Held back buffer %d.\n", ab); + } else if (agentBuffer->diffLastWriteNextOutput() < BUFFER_LENGTH_SAMPLES) { + printf("Buffer %d starved.\n", ab); + agentBuffer->setStarted(false); + } else { + // good buffer, add this to the mix + agentBuffer->setStarted(true); + agentBuffer->setAddedToMix(true); + + for (int s = 0; s < BUFFER_LENGTH_SAMPLES; s++) { + masterMix[s] += agentBuffer->getNextOutput()[s]; + } + + agentBuffer->setNextOutput(agentBuffer->getNextOutput() + BUFFER_LENGTH_SAMPLES); + + if (agentBuffer->getNextOutput() >= agentBuffer->getBuffer() + RING_BUFFER_SAMPLES) { + agentBuffer->setNextOutput(agentBuffer->getBuffer()); + } } } - } - - gettimeofday(&now, NULL); + } - double usecToSleep = usecTimestamp(&startTime, (currentFrame * BUFFER_SEND_INTERVAL_USECS)) - usecTimestamp(&now); + for (int ab = 0; ab < agentList.getAgents().size(); ab++) { + Agent *agent = &agentList.getAgents()[ab]; + AudioRingBuffer *agentBuffer = (AudioRingBuffer *)agent->getLinkedData(); + int16_t *previousOutput = NULL; + + if (agentBuffer != NULL && agentBuffer->wasAddedToMix()) { + previousOutput = (agentBuffer->getNextOutput() == agentBuffer->getBuffer()) + ? agentBuffer->getBuffer() + RING_BUFFER_SAMPLES - BUFFER_LENGTH_SAMPLES + : agentBuffer->getNextOutput() - BUFFER_LENGTH_SAMPLES; + agentBuffer->setAddedToMix(false); + } + + for (int s = 0; s < BUFFER_LENGTH_SAMPLES; s++) { + long longSample = (previousOutput != NULL) + ? masterMix[s] - previousOutput[s] + : masterMix[s]; + + int16_t shortSample; + + if (longSample < 0) { + shortSample = std::max(longSample, MIN_SAMPLE_VALUE); + } else { + shortSample = std::min(longSample, MAX_SAMPLE_VALUE); + } + + clientMix[s] = shortSample; + } + + agentList.getAgentSocket().send(agent->getPublicSocket(), clientMix, BUFFER_LENGTH_BYTES); + } + + double usecToSleep = usecTimestamp(&startTime) + (++nextFrame * BUFFER_SEND_INTERVAL_USECS) - usecTimestampNow(); + if (usecToSleep > 0) { usleep(usecToSleep); } else { - std::cout << "NOT SLEEPING!"; + std::cout << "Took too much time, not sleeping!\n"; } - - currentFrame++; } pthread_exit(0); } -int addAgent(sockaddr_in *newAddress, void *audioData) { - // Search for agent in list and add if needed - int is_new = 0; - int i = 0; - - for (i = 0; i < numAgents; i++) { - if (strcmp(inet_ntoa(newAddress->sin_addr), agents[i].address) == 0 - && ntohs(newAddress->sin_port) == agents[i].port) { - break; - } - } - - if ((i == numAgents) || (agents[i].active == false)) { - is_new = 1; - - agents[i].address = new char(); - strcpy(agents[i].address, inet_ntoa(newAddress->sin_addr)); - - agents[i].bufferTransmitted = false; - } - - - agents[i].port = ntohs(newAddress->sin_port); - agents[i].active = true; - gettimeofday(&agents[i].time, NULL); - - if (sourceBuffers[i]->endOfLastWrite == NULL) { - sourceBuffers[i]->endOfLastWrite = sourceBuffers[i]->buffer; - } else if (sourceBuffers[i]->diffLastWriteNextOutput() > RING_BUFFER_SAMPLES - BUFFER_LENGTH_SAMPLES) { - // reset us to started state - sourceBuffers[i]->endOfLastWrite = sourceBuffers[i]->buffer; - sourceBuffers[i]->nextOutput = sourceBuffers[i]->buffer; - sourceBuffers[i]->started = false; - } - - memcpy(sourceBuffers[i]->endOfLastWrite, audioData, BUFFER_LENGTH_BYTES); - - sourceBuffers[i]->endOfLastWrite += BUFFER_LENGTH_SAMPLES; - - if (sourceBuffers[i]->endOfLastWrite >= sourceBuffers[i]->buffer + RING_BUFFER_SAMPLES) { - sourceBuffers[i]->endOfLastWrite = sourceBuffers[i]->buffer; - } - - if (i == numAgents) { - numAgents++; - } - - return is_new; -} - void *reportAliveToDS(void *args) { - timeval lastSend, now; + timeval lastSend; unsigned char output[7]; while (true) { @@ -231,11 +138,9 @@ void *reportAliveToDS(void *args) { *output = 'M'; packSocket(output + 1, 895283510, htons(MIXER_LISTEN_PORT)); - audioSocket.send(DOMAIN_IP, DOMAINSERVER_PORT, output, 7); + agentList.getAgentSocket().send(DOMAIN_IP, DOMAINSERVER_PORT, output, 7); - gettimeofday(&now, NULL); - - double usecToSleep = 1000000 - (usecTimestamp(&now) - usecTimestamp(&lastSend)); + double usecToSleep = 1000000 - (usecTimestampNow() - usecTimestamp(&lastSend)); if (usecToSleep > 0) { usleep(usecToSleep); @@ -245,11 +150,18 @@ void *reportAliveToDS(void *args) { } } +void attachNewBufferToAgent(Agent *newAgent) { + if (newAgent->getLinkedData() == NULL) { + newAgent->setLinkedData(new AudioRingBuffer()); + } +} + int main(int argc, const char * argv[]) -{ - timeval lastAgentUpdate; +{ ssize_t receivedBytes = 0; + agentList.linkedDataCreateCallback = attachNewBufferToAgent; + // setup the agentSocket to report to domain server pthread_t reportAliveThread; pthread_create(&reportAliveThread, NULL, reportAliveToDS, NULL); @@ -270,25 +182,19 @@ int main(int argc, const char * argv[]) printf("Using static domainserver IP: %s\n", DOMAIN_IP); } - gettimeofday(&lastAgentUpdate, NULL); - - int16_t packetData[BUFFER_LENGTH_SAMPLES]; - - for (int b = 0; b < MAX_SOURCE_BUFFERS; b++) { - sourceBuffers[b] = new AudioRingBuffer(10 * BUFFER_LENGTH_SAMPLES); - } + int16_t *packetData = new int16_t[BUFFER_LENGTH_SAMPLES]; pthread_t sendBufferThread; pthread_create(&sendBufferThread, NULL, sendBuffer, NULL); + + sockaddr *agentAddress = new sockaddr; while (true) { - if(audioSocket.receive((sockaddr *)&agentAddress, packetData, &receivedBytes)) { + if(agentList.getAgentSocket().receive(agentAddress, packetData, &receivedBytes)) { if (receivedBytes == BUFFER_LENGTH_BYTES) { - if (addAgent(&agentAddress, packetData)) { - std::cout << "Added agent: " << - inet_ntoa(agentAddress.sin_addr) << " on " << - ntohs(agentAddress.sin_port) << "\n"; - } + // add or update the existing interface agent + agentList.addOrUpdateAgent(agentAddress, agentAddress, 'I'); + agentList.updateAgentWithData(agentAddress, (void *)packetData, receivedBytes); } } } diff --git a/shared/src/Agent.cpp b/shared/src/Agent.cpp index a635314b64..cacb3a5f94 100644 --- a/shared/src/Agent.cpp +++ b/shared/src/Agent.cpp @@ -49,8 +49,11 @@ Agent::Agent(const Agent &otherAgent) { lastRecvTimeUsecs = otherAgent.lastRecvTimeUsecs; type = otherAgent.type; - // linked data is transient, gets re-assigned on next packet receive - linkedData = NULL; + if (otherAgent.linkedData != NULL) { + linkedData = otherAgent.linkedData->clone(); + } else { + linkedData = NULL; + } } Agent& Agent::operator=(Agent otherAgent) { @@ -116,8 +119,6 @@ void Agent::activatePublicSocket() { activeSocket = publicSocket; } - - AgentData* Agent::getLinkedData() { return linkedData; } diff --git a/shared/src/AgentData.h b/shared/src/AgentData.h index e39f5ade6b..9db7535521 100644 --- a/shared/src/AgentData.h +++ b/shared/src/AgentData.h @@ -13,6 +13,7 @@ class AgentData { public: virtual ~AgentData() = 0; virtual void parseData(void * data, int size) = 0; + virtual AgentData* clone() const = 0; }; #endif diff --git a/shared/src/AgentList.h b/shared/src/AgentList.h index 2755058503..0741e63d7b 100644 --- a/shared/src/AgentList.h +++ b/shared/src/AgentList.h @@ -33,6 +33,7 @@ class AgentList { int updateList(unsigned char *packetData, size_t dataBytes); bool addOrUpdateAgent(sockaddr *publicSocket, sockaddr *localSocket, char agentType); void processAgentData(sockaddr *senderAddress, void *packetData, size_t dataBytes); + void updateAgentWithData(sockaddr *senderAddress, void *packetData, size_t dataBytes); void broadcastToAgents(char *broadcastData, size_t dataBytes); void sendToAgent(Agent *destAgent, void *packetData, size_t dataBytes); void pingAgents(); @@ -44,7 +45,6 @@ class AgentList { pthread_t removeSilentAgentsThread; int indexOfMatchingAgent(sockaddr *senderAddress); - void updateAgentWithData(sockaddr *senderAddress, void *packetData, size_t dataBytes); void handlePingReply(sockaddr *agentAddress); }; diff --git a/shared/src/AudioRingBuffer.cpp b/shared/src/AudioRingBuffer.cpp index f79df7945d..48b916588b 100644 --- a/shared/src/AudioRingBuffer.cpp +++ b/shared/src/AudioRingBuffer.cpp @@ -8,20 +8,90 @@ #include "AudioRingBuffer.h" -AudioRingBuffer::AudioRingBuffer(short ringBufferSamples) { - ringBufferLengthSamples = ringBufferSamples; +AudioRingBuffer::AudioRingBuffer() { started = false; + addedToMix = false; endOfLastWrite = NULL; - buffer = new int16_t[ringBufferLengthSamples]; + buffer = new int16_t[RING_BUFFER_SAMPLES]; nextOutput = buffer; }; +AudioRingBuffer::AudioRingBuffer(const AudioRingBuffer &otherRingBuffer) { + started = otherRingBuffer.started; + addedToMix = otherRingBuffer.addedToMix; + + buffer = new int16_t[RING_BUFFER_SAMPLES]; + memcpy(buffer, otherRingBuffer.buffer, sizeof(int16_t) * RING_BUFFER_SAMPLES); + + nextOutput = buffer + (otherRingBuffer.nextOutput - otherRingBuffer.buffer); + endOfLastWrite = buffer + (otherRingBuffer.endOfLastWrite - otherRingBuffer.buffer); +} + AudioRingBuffer::~AudioRingBuffer() { delete[] buffer; }; +AudioRingBuffer* AudioRingBuffer::clone() const { + return new AudioRingBuffer(*this); +} + +int16_t* AudioRingBuffer::getNextOutput() { + return nextOutput; +} + +void AudioRingBuffer::setNextOutput(int16_t *newPointer) { + nextOutput = newPointer; +} + +int16_t* AudioRingBuffer::getEndOfLastWrite() { + return endOfLastWrite; +} + +void AudioRingBuffer::setEndOfLastWrite(int16_t *newPointer) { + endOfLastWrite = newPointer; +} + +int16_t* AudioRingBuffer::getBuffer() { + return buffer; +} + +bool AudioRingBuffer::isStarted() { + return started; +} + +void AudioRingBuffer::setStarted(bool status) { + started = status; +} + +bool AudioRingBuffer::wasAddedToMix() { + return addedToMix; +} + +void AudioRingBuffer::setAddedToMix(bool added) { + addedToMix = added; +} + +void AudioRingBuffer::parseData(void *data, int size) { + int16_t *audioData = (int16_t *)data; + + if (endOfLastWrite == NULL) { + endOfLastWrite = buffer; + } else if (diffLastWriteNextOutput() > RING_BUFFER_SAMPLES - BUFFER_LENGTH_SAMPLES) { + endOfLastWrite = buffer; + nextOutput = buffer; + started = false; + } + + memcpy(endOfLastWrite, audioData, BUFFER_LENGTH_BYTES); + endOfLastWrite += BUFFER_LENGTH_SAMPLES; + + if (endOfLastWrite >= buffer + RING_BUFFER_SAMPLES) { + endOfLastWrite = buffer; + } +} + short AudioRingBuffer::diffLastWriteNextOutput() { if (endOfLastWrite == NULL) { @@ -30,20 +100,9 @@ short AudioRingBuffer::diffLastWriteNextOutput() short sampleDifference = endOfLastWrite - nextOutput; if (sampleDifference < 0) { - sampleDifference += ringBufferLengthSamples; + sampleDifference += RING_BUFFER_SAMPLES; } return sampleDifference; } } - -short AudioRingBuffer::bufferOverlap(int16_t *pointer, short addedDistance) -{ - short samplesLeft = (buffer + ringBufferLengthSamples) - pointer; - - if (samplesLeft < addedDistance) { - return addedDistance - samplesLeft; - } else { - return 0; - } -} diff --git a/shared/src/AudioRingBuffer.h b/shared/src/AudioRingBuffer.h index eecdbd0c55..c8a656728d 100644 --- a/shared/src/AudioRingBuffer.h +++ b/shared/src/AudioRingBuffer.h @@ -11,20 +11,40 @@ #include #include +#include "AgentData.h" -class AudioRingBuffer { +const int BUFFER_LENGTH_BYTES = 1024; +const int BUFFER_LENGTH_SAMPLES = BUFFER_LENGTH_BYTES / sizeof(int16_t); + +const short RING_BUFFER_FRAMES = 10; +const short RING_BUFFER_SAMPLES = RING_BUFFER_FRAMES * BUFFER_LENGTH_SAMPLES; + +class AudioRingBuffer : public AgentData { public: + AudioRingBuffer(); + ~AudioRingBuffer(); + AudioRingBuffer(const AudioRingBuffer &otherRingBuffer); + + void parseData(void *data, int size); + AudioRingBuffer* clone() const; + + int16_t* getNextOutput(); + void setNextOutput(int16_t *newPointer); + int16_t* getEndOfLastWrite(); + void setEndOfLastWrite(int16_t *newPointer); + int16_t* getBuffer(); + bool isStarted(); + void setStarted(bool status); + bool wasAddedToMix(); + void setAddedToMix(bool added); + + short diffLastWriteNextOutput(); + private: int16_t *nextOutput; int16_t *endOfLastWrite; int16_t *buffer; - short ringBufferLengthSamples; bool started; - - short diffLastWriteNextOutput(); - short bufferOverlap(int16_t *pointer, short addedDistance); - - AudioRingBuffer(short ringBufferSamples); - ~AudioRingBuffer(); + bool addedToMix; }; #endif /* defined(__interface__AudioRingBuffer__) */ diff --git a/shared/src/UDPSocket.cpp b/shared/src/UDPSocket.cpp index ec4c63803e..85ebcf2056 100644 --- a/shared/src/UDPSocket.cpp +++ b/shared/src/UDPSocket.cpp @@ -113,7 +113,6 @@ bool UDPSocket::receive(sockaddr *recvAddress, void *receivedData, ssize_t *rece int UDPSocket::send(sockaddr *destAddress, const void *data, size_t byteLength) { // send data via UDP - int sent_bytes = sendto(handle, (const char*)data, byteLength, 0, (sockaddr *) destAddress, sizeof(sockaddr_in)); From 9e4386819150597f625bc8332918e0d0a8a2b1bb Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 25 Feb 2013 17:13:44 -0800 Subject: [PATCH 09/29] refactor Audio.cpp in interface to use parseData in ARB --- interface/src/Audio.cpp | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index 27c2a33a0b..6e9136b4b5 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -225,26 +225,7 @@ void *receiveAudioViaUDP(void *args) { } AudioRingBuffer *ringBuffer = sharedAudioData->ringBuffer; - - if (ringBuffer->getEndOfLastWrite() == NULL) { - ringBuffer->setEndOfLastWrite(ringBuffer->getBuffer()); - } else if (ringBuffer->diffLastWriteNextOutput() > RING_BUFFER_SAMPLES - PACKET_LENGTH_SAMPLES) { - - // reset us to started state - ringBuffer->setEndOfLastWrite(ringBuffer->getBuffer()); - ringBuffer->setNextOutput(ringBuffer->getBuffer()); - ringBuffer->setStarted(false); - } - - int16_t *copyToPointer = ringBuffer->getEndOfLastWrite(); - - // just copy the recieved data to the right spot and then add packet length to previous pointer - memcpy(copyToPointer, receivedData, PACKET_LENGTH_BYTES); - ringBuffer->setEndOfLastWrite(ringBuffer->getEndOfLastWrite() + PACKET_LENGTH_SAMPLES); - - if (ringBuffer->getEndOfLastWrite() == ringBuffer->getBuffer() + RING_BUFFER_SAMPLES) { - ringBuffer->setEndOfLastWrite(ringBuffer->getBuffer()); - } + ringBuffer->parseData(receivedData, PACKET_LENGTH_BYTES); if (LOG_SAMPLE_DELAY) { gettimeofday(&previousReceiveTime, NULL); From a3411b20442e102ff040a026c9e8ebeae8eb7465 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 26 Feb 2013 10:28:47 -0800 Subject: [PATCH 10/29] add missing cstring include for memcpy --- shared/src/AudioRingBuffer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/shared/src/AudioRingBuffer.cpp b/shared/src/AudioRingBuffer.cpp index 48b916588b..d234a405e3 100644 --- a/shared/src/AudioRingBuffer.cpp +++ b/shared/src/AudioRingBuffer.cpp @@ -6,6 +6,7 @@ // Copyright (c) 2013 HighFidelity, Inc. All rights reserved. // +#include #include "AudioRingBuffer.h" AudioRingBuffer::AudioRingBuffer() { From 04f3398466a92e9ce2349c55820fea9789cf7c71 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 26 Feb 2013 13:13:40 -0800 Subject: [PATCH 11/29] send interface client position to audio mixer --- domain/src/main.cpp | 2 -- interface/src/Audio.cpp | 24 +++++++++++++++++++++++- interface/src/Audio.h | 3 ++- interface/src/AudioData.h | 10 ++++++---- interface/src/main.cpp | 2 +- mixer/src/main.cpp | 11 +++-------- shared/src/AgentList.cpp | 2 +- shared/src/AgentList.h | 1 + shared/src/AudioRingBuffer.cpp | 16 ++++++++++++++-- shared/src/UDPSocket.h | 2 +- 10 files changed, 52 insertions(+), 21 deletions(-) diff --git a/domain/src/main.cpp b/domain/src/main.cpp index 7b6f75ad3f..9a8b6bc64f 100644 --- a/domain/src/main.cpp +++ b/domain/src/main.cpp @@ -32,8 +32,6 @@ #include "SharedUtil.h" const int DOMAIN_LISTEN_PORT = 40102; - -const int MAX_PACKET_SIZE = 1500; unsigned char packetData[MAX_PACKET_SIZE]; const int LOGOFF_CHECK_INTERVAL = 5000; diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index 6e9136b4b5..4990e4f5de 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -84,7 +84,25 @@ int audioCallback (const void *inputBuffer, audioMixerSocket.sin_family = AF_INET; audioMixerSocket.sin_addr.s_addr = data->mixerAddress; audioMixerSocket.sin_port = data->mixerPort; - data->audioSocket->send((sockaddr *)&audioMixerSocket, (void *)inputLeft, BUFFER_LENGTH_BYTES); + + int leadingBytes = 1 + (sizeof(float) * 3); + + // we need the amount of bytes in the buffer + 1 for type + 12 for 3 floats for position + unsigned char *dataPacket = new unsigned char[BUFFER_LENGTH_BYTES + leadingBytes]; + + dataPacket[0] = 'I'; + + // memcpy the three float positions + for (int p = 0; p < 3; p++) { + memcpy(dataPacket + 1 + (p * sizeof(float)), &data->sourcePosition[p], sizeof(float)); + } + + // copy the audio data to the last 1024 bytes of the data packet + memcpy(dataPacket + leadingBytes, inputLeft, BUFFER_LENGTH_BYTES); + + data->audioSocket->send((sockaddr *)&audioMixerSocket, dataPacket, BUFFER_LENGTH_BYTES + leadingBytes); + + delete dataPacket; } // @@ -236,6 +254,10 @@ void *receiveAudioViaUDP(void *args) { pthread_exit(0); } +void Audio::setSourcePosition(glm::vec3 newPosition) { + audioData->sourcePosition = newPosition; +} + /** * Initialize portaudio and start an audio stream. * Should be called at the beginning of program exection. diff --git a/interface/src/Audio.h b/interface/src/Audio.h index 7f1fa9143f..0798486106 100644 --- a/interface/src/Audio.h +++ b/interface/src/Audio.h @@ -25,11 +25,12 @@ public: void getInputLoudness(float * lastLoudness, float * averageLoudness); void updateMixerParams(in_addr_t mixerAddress, in_port_t mixerPort); + void setSourcePosition(glm::vec3 position); + // terminates audio I/O bool terminate(); private: bool initialized; - AudioData *audioData; // protects constructor so that public init method is used diff --git a/interface/src/AudioData.h b/interface/src/AudioData.h index e5094f8263..4efe39884a 100644 --- a/interface/src/AudioData.h +++ b/interface/src/AudioData.h @@ -11,17 +11,22 @@ #include #include +#include #include "AudioRingBuffer.h" #include "UDPSocket.h" class AudioData { - public: + public: + AudioData(int bufferLength); + ~AudioData(); AudioRingBuffer *ringBuffer; UDPSocket *audioSocket; int16_t *samplesToQueue; + glm::vec3 sourcePosition; + // store current mixer address and port in_addr_t mixerAddress; in_port_t mixerPort; @@ -34,9 +39,6 @@ class AudioData { float lastInputLoudness; float averagedInputLoudness; - - AudioData(int bufferLength); - ~AudioData(); }; #endif /* defined(__interface__AudioData__) */ diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 3ae6e4e411..9b02250633 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -53,7 +53,6 @@ int simulate_on = 1; // Network Socket and network constants // -const int MAX_PACKET_SIZE = 1500; char DOMAIN_HOSTNAME[] = "highfidelity.below92.com"; char DOMAIN_IP[100] = ""; // IP Address will be used first if not empty string const int DOMAINSERVER_PORT = 40102; @@ -500,6 +499,7 @@ void update_pos(float frametime) myHead.setRenderYaw(myHead.getRenderYaw() + render_yaw_rate); myHead.setRenderPitch(render_pitch); myHead.setPos(glm::vec3(location[0], location[1], location[2])); + audio.setSourcePosition(glm::vec3(location[0], location[1], location[2])); // Get audio loudness data from audio input device float loudness, averageLoudness; diff --git a/mixer/src/main.cpp b/mixer/src/main.cpp index 379298b828..37093396ce 100644 --- a/mixer/src/main.cpp +++ b/mixer/src/main.cpp @@ -16,12 +16,8 @@ #include #include -const int MAX_AGENTS = 1000; -const int LOGOFF_CHECK_INTERVAL = 1000; - const unsigned short MIXER_LISTEN_PORT = 55443; - const float SAMPLE_RATE = 22050.0; const float BUFFER_SEND_INTERVAL_USECS = (BUFFER_LENGTH_SAMPLES/SAMPLE_RATE) * 1000000; @@ -35,7 +31,6 @@ char DOMAIN_HOSTNAME[] = "highfidelity.below92.com"; char DOMAIN_IP[100] = ""; // IP Address will be re-set by lookup on startup const int DOMAINSERVER_PORT = 40102; -const int MAX_SOURCE_BUFFERS = 20; AgentList agentList(MIXER_LISTEN_PORT); @@ -182,7 +177,7 @@ int main(int argc, const char * argv[]) printf("Using static domainserver IP: %s\n", DOMAIN_IP); } - int16_t *packetData = new int16_t[BUFFER_LENGTH_SAMPLES]; + unsigned char *packetData = new unsigned char[MAX_PACKET_SIZE]; pthread_t sendBufferThread; pthread_create(&sendBufferThread, NULL, sendBuffer, NULL); @@ -191,9 +186,9 @@ int main(int argc, const char * argv[]) while (true) { if(agentList.getAgentSocket().receive(agentAddress, packetData, &receivedBytes)) { - if (receivedBytes == BUFFER_LENGTH_BYTES) { + if (receivedBytes > BUFFER_LENGTH_BYTES) { // add or update the existing interface agent - agentList.addOrUpdateAgent(agentAddress, agentAddress, 'I'); + agentList.addOrUpdateAgent(agentAddress, agentAddress, packetData[0]); agentList.updateAgentWithData(agentAddress, (void *)packetData, receivedBytes); } } diff --git a/shared/src/AgentList.cpp b/shared/src/AgentList.cpp index d1c4db24ce..fe280894ac 100644 --- a/shared/src/AgentList.cpp +++ b/shared/src/AgentList.cpp @@ -147,7 +147,7 @@ bool AgentList::addOrUpdateAgent(sockaddr *publicSocket, sockaddr *localSocket, // this is an audio mixer // for now that means we need to tell the audio class // to use the local socket information the domain server gave us - sockaddr_in *localSocketIn = (sockaddr_in *)localSocket; + sockaddr_in *localSocketIn = (sockaddr_in *)publicSocket; audioMixerSocketUpdate(localSocketIn->sin_addr.s_addr, localSocketIn->sin_port); } diff --git a/shared/src/AgentList.h b/shared/src/AgentList.h index 0741e63d7b..4fb6a2d989 100644 --- a/shared/src/AgentList.h +++ b/shared/src/AgentList.h @@ -14,6 +14,7 @@ #include "Agent.h" #include "UDPSocket.h" +const int MAX_PACKET_SIZE = 1500; const unsigned short AGENT_SOCKET_LISTEN_PORT = 40103; const int AGENT_SILENCE_THRESHOLD_USECS = 2 * 1000000; extern const char *SOLO_AGENT_TYPES_STRING; diff --git a/shared/src/AudioRingBuffer.cpp b/shared/src/AudioRingBuffer.cpp index d234a405e3..e20860a7bf 100644 --- a/shared/src/AudioRingBuffer.cpp +++ b/shared/src/AudioRingBuffer.cpp @@ -75,8 +75,20 @@ void AudioRingBuffer::setAddedToMix(bool added) { } void AudioRingBuffer::parseData(void *data, int size) { - int16_t *audioData = (int16_t *)data; + int16_t *audioDataStart = (int16_t *) data; + if (size > BUFFER_LENGTH_BYTES) { + float position[3]; + unsigned char *charData = (unsigned char *) data; + + for (int p = 0; p < 3; p ++) { + memcpy(&position[p], charData + 1 + (sizeof(float) * p), sizeof(float)); + } + + audioDataStart = (int16_t *) charData + 1 + (sizeof(float) * 3); + } + + if (endOfLastWrite == NULL) { endOfLastWrite = buffer; } else if (diffLastWriteNextOutput() > RING_BUFFER_SAMPLES - BUFFER_LENGTH_SAMPLES) { @@ -85,7 +97,7 @@ void AudioRingBuffer::parseData(void *data, int size) { started = false; } - memcpy(endOfLastWrite, audioData, BUFFER_LENGTH_BYTES); + memcpy(endOfLastWrite, audioDataStart, BUFFER_LENGTH_BYTES); endOfLastWrite += BUFFER_LENGTH_SAMPLES; if (endOfLastWrite >= buffer + RING_BUFFER_SAMPLES) { diff --git a/shared/src/UDPSocket.h b/shared/src/UDPSocket.h index 27a3c48ad3..b2389ad73f 100644 --- a/shared/src/UDPSocket.h +++ b/shared/src/UDPSocket.h @@ -13,7 +13,7 @@ #include #include -#define MAX_BUFFER_LENGTH_BYTES 1024 +#define MAX_BUFFER_LENGTH_BYTES 1500 class UDPSocket { public: From 31b93b036e5d9750c5756e0a2f8a52dd912f9484 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 26 Feb 2013 13:43:50 -0800 Subject: [PATCH 12/29] temporary test against birarda's domain server --- interface/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 9b02250633..80e1f91e9d 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -54,7 +54,7 @@ int simulate_on = 1; // char DOMAIN_HOSTNAME[] = "highfidelity.below92.com"; -char DOMAIN_IP[100] = ""; // IP Address will be used first if not empty string +char DOMAIN_IP[100] = "192.168.1.47"; // IP Address will be used first if not empty string const int DOMAINSERVER_PORT = 40102; AgentList agentList; From 38cdd615a13ea2cb814dd364bbf0e31503c1960e Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 26 Feb 2013 13:59:33 -0800 Subject: [PATCH 13/29] switch back to EC2 DS for sanity check --- interface/src/main.cpp | 2 +- shared/src/AgentList.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 80e1f91e9d..9b02250633 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -54,7 +54,7 @@ int simulate_on = 1; // char DOMAIN_HOSTNAME[] = "highfidelity.below92.com"; -char DOMAIN_IP[100] = "192.168.1.47"; // IP Address will be used first if not empty string +char DOMAIN_IP[100] = ""; // IP Address will be used first if not empty string const int DOMAINSERVER_PORT = 40102; AgentList agentList; diff --git a/shared/src/AgentList.cpp b/shared/src/AgentList.cpp index fe280894ac..d1c4db24ce 100644 --- a/shared/src/AgentList.cpp +++ b/shared/src/AgentList.cpp @@ -147,7 +147,7 @@ bool AgentList::addOrUpdateAgent(sockaddr *publicSocket, sockaddr *localSocket, // this is an audio mixer // for now that means we need to tell the audio class // to use the local socket information the domain server gave us - sockaddr_in *localSocketIn = (sockaddr_in *)publicSocket; + sockaddr_in *localSocketIn = (sockaddr_in *)localSocket; audioMixerSocketUpdate(localSocketIn->sin_addr.s_addr, localSocketIn->sin_port); } From 7bda2c058184297b6d8b27f3f07dfc71c3b18633 Mon Sep 17 00:00:00 2001 From: stojce Date: Wed, 27 Feb 2013 01:52:40 +0100 Subject: [PATCH 14/29] #19069 Add iris textures to avatar head, change lighting --- interface/src/Head.cpp | 64 ++++++++++++++++++++++++++++++--------- interface/src/Texture.cpp | 2 +- interface/src/main.cpp | 6 ++-- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/interface/src/Head.cpp b/interface/src/Head.cpp index 6930d7a77a..eb5c294105 100644 --- a/interface/src/Head.cpp +++ b/interface/src/Head.cpp @@ -8,10 +8,13 @@ #include #include -#include #include "Head.h" -#include "Util.h" -#include "SerialInterface.h" +#include +#include +#include +#include + +using namespace std; float skinColor[] = {1.0, 0.84, 0.66}; float browColor[] = {210.0/255.0, 105.0/255.0, 30.0/255.0}; @@ -28,6 +31,12 @@ float browThickness = 0.16; const float DECAY = 0.1; +char iris_texture_file[] = "interface.app/Contents/Resources/images/green_eye.png"; + +static vector iris_texture; +unsigned int iris_texture_width = 512; +unsigned int iris_texture_height = 256; + Head::Head() { position.x = position.y = position.z = 0; @@ -58,6 +67,14 @@ Head::Head() renderPitch = 0.0; setNoise(0); hand = new Hand(glm::vec3(skinColor[0], skinColor[1], skinColor[2])); + + + if (iris_texture.size() == 0) { + unsigned error = lodepng::decode(iris_texture, iris_texture_width, iris_texture_height, iris_texture_file); + if (error != 0) { + std::cout << "error " << error << ": " << lodepng_error_text(error) << std::endl; + } + } } Head::~Head() { @@ -212,11 +229,11 @@ void Head::simulate(float deltaTime) { SetNewHeadTarget((randFloat()-0.5)*20.0, (randFloat()-0.5)*45.0); } - + if (0) { - - // Pick new target + + // Pick new target PitchTarget = (randFloat() - 0.5)*45; YawTarget = (randFloat() - 0.5)*22; } @@ -326,14 +343,27 @@ void Head::render(int faceToFace, float * myLocation) glutSolidSphere(0.25, 30, 30); } glPopMatrix(); + // Right Pupil + GLUquadric *sphere = gluNewQuadric(); + gluQuadricTexture(sphere, GL_TRUE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + gluQuadricOrientation(sphere, GLU_OUTSIDE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, iris_texture_width, iris_texture_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, &iris_texture[0]); + glPushMatrix(); + { glRotatef(EyeballPitch[1], 1, 0, 0); glRotatef(EyeballYaw[1] + PupilConverge, 0, 1, 0); glTranslatef(0,0,.35); - if (!eyeContact) glColor3f(0,0,0); else glColor3f(0.3,0.3,0.3); - //glRotatef(90,0,1,0); - glutSolidSphere(PupilSize, 15, 15); + glRotatef(-75,1,0,0); + glScalef(1.0, 0.4, 1.0); + + glEnable(GL_TEXTURE_2D); + gluSphere(sphere, PupilSize, 15, 15); + glDisable(GL_TEXTURE_2D); + } glPopMatrix(); // Left Eye @@ -349,14 +379,20 @@ void Head::render(int faceToFace, float * myLocation) glPopMatrix(); // Left Pupil glPushMatrix(); + { glRotatef(EyeballPitch[0], 1, 0, 0); glRotatef(EyeballYaw[0] - PupilConverge, 0, 1, 0); - glTranslatef(0,0,.35); - if (!eyeContact) glColor3f(0,0,0); else glColor3f(0.3,0.3,0.3); - //glRotatef(90,0,1,0); - glutSolidSphere(PupilSize, 15, 15); - glPopMatrix(); + glTranslatef(0, 0, .35); + glRotatef(-75, 1, 0, 0); + glScalef(1.0, 0.4, 1.0); + + glEnable(GL_TEXTURE_2D); + gluSphere(sphere, PupilSize, 15, 15); + glDisable(GL_TEXTURE_2D); + } + gluDeleteQuadric(sphere); + glPopMatrix(); } glPopMatrix(); diff --git a/interface/src/Texture.cpp b/interface/src/Texture.cpp index d7fc2c9a26..e0671cc881 100644 --- a/interface/src/Texture.cpp +++ b/interface/src/Texture.cpp @@ -9,7 +9,7 @@ #include "Texture.h" #include "InterfaceConfig.h" -#include +#include #include #include diff --git a/interface/src/main.cpp b/interface/src/main.cpp index 9b02250633..4692189cb6 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -531,7 +531,7 @@ void display(void) glEnable(GL_COLOR_MATERIAL); glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); - GLfloat light_position0[] = { 1.0, 1.0, 0.0, 0.0 }; + GLfloat light_position0[] = { 1.0, 1.0, 0.75, 0.0 }; glLightfv(GL_LIGHT0, GL_POSITION, light_position0); GLfloat ambient_color[] = { 0.125, 0.305, 0.5 }; glLightfv(GL_LIGHT0, GL_AMBIENT, ambient_color); @@ -539,10 +539,10 @@ void display(void) glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse_color); GLfloat specular_color[] = { 1.0, 1.0, 1.0, 1.0}; glLightfv(GL_LIGHT0, GL_SPECULAR, specular_color); - + glMaterialfv(GL_FRONT, GL_SPECULAR, specular_color); glMateriali(GL_FRONT, GL_SHININESS, 96); - + // Rotate, translate to camera location glRotatef(myHead.getRenderPitch(), 1, 0, 0); glRotatef(myHead.getRenderYaw(), 0, 1, 0); From 4d6bbba93d8e84d5d189a02e4cacab79a4845e46 Mon Sep 17 00:00:00 2001 From: stojce Date: Wed, 6 Mar 2013 07:20:11 +0100 Subject: [PATCH 15/29] added eye texture --- interface/resources/images/green_eye.png | Bin 0 -> 67566 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 interface/resources/images/green_eye.png diff --git a/interface/resources/images/green_eye.png b/interface/resources/images/green_eye.png new file mode 100644 index 0000000000000000000000000000000000000000..05bf108a00c798437ef2570de45a40435666f7b4 GIT binary patch literal 67566 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 literal 0 HcmV?d00001 From 0b81ef5565ba0d636bf355924733315d98e87676 Mon Sep 17 00:00:00 2001 From: stojce Date: Wed, 6 Mar 2013 07:22:55 +0100 Subject: [PATCH 16/29] ignored files --- .gitignore | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b6ca7c09e8..fa3df0078e 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,13 @@ profile *.moved-aside DerivedData .idea/ -*.hmap \ No newline at end of file +*.hmap + +interface/Debug/ +interface/external/ +interface/hifi.build/ + +shared/Debug/ +shared/hifi.build/ + +interface/includes/InterfaceConfig.h From 3b3ab9f68f2624b538509e1af9b3cba76308e4ee Mon Sep 17 00:00:00 2001 From: stojce Date: Wed, 6 Mar 2013 08:08:18 +0100 Subject: [PATCH 17/29] set texture binding of first render call only --- interface/src/Head.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/interface/src/Head.cpp b/interface/src/Head.cpp index eb5c294105..1a7dc9a2d2 100644 --- a/interface/src/Head.cpp +++ b/interface/src/Head.cpp @@ -33,10 +33,12 @@ const float DECAY = 0.1; char iris_texture_file[] = "interface.app/Contents/Resources/images/green_eye.png"; -static vector iris_texture; +vector iris_texture; unsigned int iris_texture_width = 512; unsigned int iris_texture_height = 256; +GLUquadric *sphere = gluNewQuadric(); + Head::Head() { position.x = position.y = position.z = 0; @@ -79,6 +81,7 @@ Head::Head() Head::~Head() { // all data is primitive, do nothing + gluDeleteQuadric(sphere); } Head* Head::clone() const { @@ -345,12 +348,14 @@ void Head::render(int faceToFace, float * myLocation) glPopMatrix(); // Right Pupil - GLUquadric *sphere = gluNewQuadric(); - gluQuadricTexture(sphere, GL_TRUE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - gluQuadricOrientation(sphere, GLU_OUTSIDE); - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, iris_texture_width, iris_texture_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, &iris_texture[0]); + if (!sphere) { + sphere = gluNewQuadric(); + gluQuadricTexture(sphere, GL_TRUE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + gluQuadricOrientation(sphere, GLU_OUTSIDE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, iris_texture_width, iris_texture_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, &iris_texture[0]); + } glPushMatrix(); { @@ -391,7 +396,6 @@ void Head::render(int faceToFace, float * myLocation) glDisable(GL_TEXTURE_2D); } - gluDeleteQuadric(sphere); glPopMatrix(); } From 2a4a7b4ab8ea888f76dfb1aeebaf250d7d19d204 Mon Sep 17 00:00:00 2001 From: stojce Date: Wed, 6 Mar 2013 08:16:40 +0100 Subject: [PATCH 18/29] call gluDeleteQuadric only if sphare is initialised --- interface/src/Head.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/interface/src/Head.cpp b/interface/src/Head.cpp index 1a7dc9a2d2..90e40ed180 100644 --- a/interface/src/Head.cpp +++ b/interface/src/Head.cpp @@ -80,8 +80,9 @@ Head::Head() } Head::~Head() { - // all data is primitive, do nothing - gluDeleteQuadric(sphere); + if (sphere) { + gluDeleteQuadric(sphere); + } } Head* Head::clone() const { From 14e3160d502a2afa60728da730f5a8ef1d38d43c Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Mar 2013 17:46:57 -0700 Subject: [PATCH 19/29] only keep the threshold number of frames on mixer as well --- mixer/src/main.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/mixer/src/main.cpp b/mixer/src/main.cpp index 155f63f9e0..5735b6e03d 100644 --- a/mixer/src/main.cpp +++ b/mixer/src/main.cpp @@ -81,6 +81,22 @@ void *sendBuffer(void *args) printf("Buffer %d starved.\n", i); agentBuffer->setStarted(false); } else { + + // check if we have more than we need to play out + int thresholdFrames = ceilf((BUFFER_LENGTH_SAMPLES_PER_CHANNEL + JITTER_BUFFER_SAMPLES) / (float)BUFFER_LENGTH_SAMPLES_PER_CHANNEL); + int thresholdSamples = thresholdFrames * BUFFER_LENGTH_SAMPLES_PER_CHANNEL; + + if (agentBuffer->diffLastWriteNextOutput() > thresholdSamples) { + // we need to push the next output forwards + int samplesToPush = agentBuffer->diffLastWriteNextOutput() - thresholdSamples; + + if (agentBuffer->getNextOutput() + samplesToPush > agentBuffer->getBuffer()) { + agentBuffer->setNextOutput(agentBuffer->getBuffer() + (samplesToPush - (agentBuffer->getBuffer() + RING_BUFFER_SAMPLES - agentBuffer->getNextOutput()))); + } else { + agentBuffer->setNextOutput(agentBuffer->getNextOutput() + samplesToPush); + } + } + // good buffer, add this to the mix agentBuffer->setStarted(true); agentBuffer->setAddedToMix(true); From 73f6d9311dd785d5cfaed754cded6f2fb392d086 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Mar 2013 18:56:14 -0700 Subject: [PATCH 20/29] local loopback test from mixer for sanity check --- mixer/src/main.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/mixer/src/main.cpp b/mixer/src/main.cpp index 5735b6e03d..50b7b554fc 100644 --- a/mixer/src/main.cpp +++ b/mixer/src/main.cpp @@ -39,6 +39,8 @@ const int PHASE_DELAY_AT_90 = 20; const int AGENT_LOOPBACK_MODIFIER = 307; +const int LOOPBACK_SANITY_CHECK = 1; + char DOMAIN_HOSTNAME[] = "highfidelity.below92.com"; char DOMAIN_IP[100] = ""; // IP Address will be re-set by lookup on startup const int DOMAINSERVER_PORT = 40102; @@ -299,14 +301,24 @@ int main(int argc, const char * argv[]) pthread_t sendBufferThread; pthread_create(&sendBufferThread, NULL, sendBuffer, NULL); + int16_t *loopbackAudioPacket; + if (LOOPBACK_SANITY_CHECK) { + loopbackAudioPacket = new int16_t[1024]; + } + sockaddr *agentAddress = new sockaddr; while (true) { if(agentList.getAgentSocket().receive(agentAddress, packetData, &receivedBytes)) { if (packetData[0] == 'I') { // add or update the existing interface agent - agentList.addOrUpdateAgent(agentAddress, agentAddress, packetData[0]); - agentList.updateAgentWithData(agentAddress, (void *)packetData, receivedBytes); + if (!LOOPBACK_SANITY_CHECK) { + agentList.addOrUpdateAgent(agentAddress, agentAddress, packetData[0]); + agentList.updateAgentWithData(agentAddress, (void *)packetData, receivedBytes); + } else { + memcpy(loopbackAudioPacket, packetData + 1 + (sizeof(float) * 4), 1024); + agentList.getAgentSocket().send(agentAddress, loopbackAudioPacket, 1024); + } } } } From 83952e7e32968252f793a59b28ce3782bb1343f6 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Mar 2013 19:10:49 -0700 Subject: [PATCH 21/29] stop the loopback sanity check, timing off --- mixer/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixer/src/main.cpp b/mixer/src/main.cpp index 50b7b554fc..793119f6f6 100644 --- a/mixer/src/main.cpp +++ b/mixer/src/main.cpp @@ -39,7 +39,7 @@ const int PHASE_DELAY_AT_90 = 20; const int AGENT_LOOPBACK_MODIFIER = 307; -const int LOOPBACK_SANITY_CHECK = 1; +const int LOOPBACK_SANITY_CHECK = 0; char DOMAIN_HOSTNAME[] = "highfidelity.below92.com"; char DOMAIN_IP[100] = ""; // IP Address will be re-set by lookup on startup From 7503db3e19e00892c1cedeb1e6fa56fa556f7e7c Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Mar 2013 19:26:21 -0700 Subject: [PATCH 22/29] output time difference in receive of packets from client --- mixer/src/main.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mixer/src/main.cpp b/mixer/src/main.cpp index 793119f6f6..7d9462fd49 100644 --- a/mixer/src/main.cpp +++ b/mixer/src/main.cpp @@ -63,14 +63,10 @@ void *sendBuffer(void *args) timeval startTime, lastSend; gettimeofday(&startTime, NULL); - gettimeofday(&lastSend, NULL); while (true) { sentBytes = 0; - printf("Last send was %f ms ago\n", (usecTimestampNow() - usecTimestamp(&lastSend)) / 1000); - gettimeofday(&lastSend, NULL); - for (int i = 0; i < agentList.getAgents().size(); i++) { AudioRingBuffer *agentBuffer = (AudioRingBuffer *) agentList.getAgents()[i].getLinkedData(); @@ -307,10 +303,16 @@ int main(int argc, const char * argv[]) } sockaddr *agentAddress = new sockaddr; + timeval lastReceive; + gettimeofday(&lastReceive, NULL); while (true) { if(agentList.getAgentSocket().receive(agentAddress, packetData, &receivedBytes)) { if (packetData[0] == 'I') { + + printf("Last receive was %f ms ago\n", (usecTimestampNow() - usecTimestamp(&lastReceive)) / 1000); + gettimeofday(&lastreceive, NULL); + // add or update the existing interface agent if (!LOOPBACK_SANITY_CHECK) { agentList.addOrUpdateAgent(agentAddress, agentAddress, packetData[0]); From 53b89193f3991d537e8f303cfc492928369d78b6 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 11 Mar 2013 19:27:23 -0700 Subject: [PATCH 23/29] fix typo for last receive --- mixer/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mixer/src/main.cpp b/mixer/src/main.cpp index 7d9462fd49..566b48dfed 100644 --- a/mixer/src/main.cpp +++ b/mixer/src/main.cpp @@ -311,7 +311,7 @@ int main(int argc, const char * argv[]) if (packetData[0] == 'I') { printf("Last receive was %f ms ago\n", (usecTimestampNow() - usecTimestamp(&lastReceive)) / 1000); - gettimeofday(&lastreceive, NULL); + gettimeofday(&lastReceive, NULL); // add or update the existing interface agent if (!LOOPBACK_SANITY_CHECK) { From e97aba03a3054eab971444ceeccb5ddcd21e0953 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Mar 2013 10:09:07 -0700 Subject: [PATCH 24/29] move StdDev to shared library --- interface/src/Audio.cpp | 5 +++-- interface/src/Util.cpp | 42 ------------------------------------ interface/src/Util.h | 12 ----------- interface/src/main.cpp | 24 --------------------- shared/src/StdDev.cpp | 48 +++++++++++++++++++++++++++++++++++++++++ shared/src/StdDev.h | 27 +++++++++++++++++++++++ 6 files changed, 78 insertions(+), 80 deletions(-) create mode 100644 shared/src/StdDev.cpp create mode 100644 shared/src/StdDev.h diff --git a/interface/src/Audio.cpp b/interface/src/Audio.cpp index 60f28ca5df..cd839fb6fe 100644 --- a/interface/src/Audio.cpp +++ b/interface/src/Audio.cpp @@ -12,10 +12,11 @@ #include #include #include +#include +#include +#include #include "Audio.h" #include "Util.h" -#include -#include "UDPSocket.h" Oscilloscope * scope; diff --git a/interface/src/Util.cpp b/interface/src/Util.cpp index 4ef06dfed4..fdd6feacd0 100644 --- a/interface/src/Util.cpp +++ b/interface/src/Util.cpp @@ -13,48 +13,6 @@ #include "world.h" #include "Util.h" -// -// Standard Deviation Object -// - -const int MAX_STDEV_SAMPLES = 1000; // Don't add more than this number of samples. - -StDev::StDev() { - data = new float[MAX_STDEV_SAMPLES]; - sampleCount = 0; -} - -void StDev::reset() { - sampleCount = 0; -} - -void StDev::addValue(float v) { - data[sampleCount++] = v; - if (sampleCount == MAX_STDEV_SAMPLES) sampleCount = 0; -} - -float StDev::getAverage() { - float average = 0; - for (int i = 0; i < sampleCount; i++) { - average += data[i]; - } - if (sampleCount > 0) - return average/(float)sampleCount; - else return 0; -} - -float StDev::getStDev() { - float average = getAverage(); - float stdev = 0; - for (int i = 0; i < sampleCount; i++) { - stdev += powf(data[i] - average, 2); - } - if (sampleCount > 1) - return sqrt(stdev/(float)(sampleCount - 1.0)); - else - return 0; -} - // Return the azimuth angle in degrees between two points. float azimuth_to(glm::vec3 head_pos, glm::vec3 source_pos) { return atan2(head_pos.x - source_pos.x, head_pos.z - source_pos.z) * 180 / PI; diff --git a/interface/src/Util.h b/interface/src/Util.h index 3b91223b35..c83504a779 100644 --- a/interface/src/Util.h +++ b/interface/src/Util.h @@ -24,17 +24,5 @@ void drawvec3(int x, int y, float scale, float rotate, float thick, int mono, gl float r=1.0, float g=1.0, float b=1.0); double diffclock(timeval *clock1,timeval *clock2); -class StDev { -public: - StDev(); - void reset(); - void addValue(float v); - float getAverage(); - float getStDev(); - int getSamples() {return sampleCount;}; -private: - float * data; - int sampleCount; -}; #endif diff --git a/interface/src/main.cpp b/interface/src/main.cpp index abe1206afa..f1e69839a0 100644 --- a/interface/src/main.cpp +++ b/interface/src/main.cpp @@ -915,30 +915,6 @@ int main(int argc, char** argv) printf("Failed lookup domainserver\n"); } } else printf("Using static domainserver IP: %s\n", DOMAIN_IP); - - printf("Testing stats math... "); - StDev stdevtest; - stdevtest.reset(); - stdevtest.addValue(1345); - stdevtest.addValue(1301); - stdevtest.addValue(1368); - stdevtest.addValue(1322); - stdevtest.addValue(1310); - stdevtest.addValue(1370); - stdevtest.addValue(1318); - stdevtest.addValue(1350); - stdevtest.addValue(1303); - stdevtest.addValue(1299); - - if (stdevtest.getSamples() != 10) - printf("Samples=FAIL "); - - if (floor(stdevtest.getAverage()*100.0) != 132859.0) - printf("Average=FAIL "); - - if (floor(stdevtest.getStDev()*100.0) != 2746.0) - printf("Stdev=FAIL "); - printf("\n"); // the callback for our instance of AgentList is attachNewHeadToAgent agentList.linkedDataCreateCallback = &attachNewHeadToAgent; diff --git a/shared/src/StdDev.cpp b/shared/src/StdDev.cpp new file mode 100644 index 0000000000..040f51dd50 --- /dev/null +++ b/shared/src/StdDev.cpp @@ -0,0 +1,48 @@ +// +// StdDev.cpp +// hifi +// +// Created by Philip Rosedale on 3/12/13. +// +// + +#include "StdDev.h" +#include + +const int MAX_STDEV_SAMPLES = 1000; // Don't add more than this number of samples. + +StDev::StDev() { + data = new float[MAX_STDEV_SAMPLES]; + sampleCount = 0; +} + +void StDev::reset() { + sampleCount = 0; +} + +void StDev::addValue(float v) { + data[sampleCount++] = v; + if (sampleCount == MAX_STDEV_SAMPLES) sampleCount = 0; +} + +float StDev::getAverage() { + float average = 0; + for (int i = 0; i < sampleCount; i++) { + average += data[i]; + } + if (sampleCount > 0) + return average/(float)sampleCount; + else return 0; +} + +float StDev::getStDev() { + float average = getAverage(); + float stdev = 0; + for (int i = 0; i < sampleCount; i++) { + stdev += powf(data[i] - average, 2); + } + if (sampleCount > 1) + return sqrt(stdev/(float)(sampleCount - 1.0)); + else + return 0; +} \ No newline at end of file diff --git a/shared/src/StdDev.h b/shared/src/StdDev.h new file mode 100644 index 0000000000..2c0d0485c4 --- /dev/null +++ b/shared/src/StdDev.h @@ -0,0 +1,27 @@ +// +// StdDev.h +// hifi +// +// Created by Philip Rosedale on 3/12/13. +// +// + +#ifndef __hifi__StdDev__ +#define __hifi__StdDev__ + +#include + +class StDev { + public: + StDev(); + void reset(); + void addValue(float v); + float getAverage(); + float getStDev(); + int getSamples() {return sampleCount;}; + private: + float * data; + int sampleCount; +}; + +#endif /* defined(__hifi__StdDev__) */ From 367a553f1e9883d7e51e39e6b71a95cf75cc0202 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Mar 2013 10:15:37 -0700 Subject: [PATCH 25/29] compute standard deviation for one client receive times --- mixer/src/main.cpp | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/mixer/src/main.cpp b/mixer/src/main.cpp index 566b48dfed..f6f89765cc 100644 --- a/mixer/src/main.cpp +++ b/mixer/src/main.cpp @@ -12,9 +12,10 @@ #include #include #include -#include "AudioRingBuffer.h" #include #include +#include +#include "AudioRingBuffer.h" const unsigned short MIXER_LISTEN_PORT = 55443; @@ -46,6 +47,7 @@ char DOMAIN_IP[100] = ""; // IP Address will be re-set by lookup on startup const int DOMAINSERVER_PORT = 40102; AgentList agentList(MIXER_LISTEN_PORT); +StDev stdev; void plateauAdditionOfSamples(int16_t &mixSample, int16_t sampleToAdd) { long sumSample = sampleToAdd + mixSample; @@ -60,7 +62,7 @@ void *sendBuffer(void *args) { int sentBytes; int nextFrame = 0; - timeval startTime, lastSend; + timeval startTime; gettimeofday(&startTime, NULL); @@ -305,12 +307,27 @@ int main(int argc, const char * argv[]) sockaddr *agentAddress = new sockaddr; timeval lastReceive; gettimeofday(&lastReceive, NULL); + + bool firstSample = true; while (true) { if(agentList.getAgentSocket().receive(agentAddress, packetData, &receivedBytes)) { if (packetData[0] == 'I') { + + // Compute and report standard deviation for jitter calculation + if (firstSample) { + stdev.reset(); + firstSample = false; + } else { + double tDiff = (usecTimestampNow() - usecTimestamp(&lastReceive)) / 1000; + stdev.addValue(tDiff); + + if (stdev.getSamples() > 500) { + printf("Avg: %4.2f, Stdev: %4.2f\n", stdev.getAverage(), stdev.getStDev()); + stdev.reset(); + } + } - printf("Last receive was %f ms ago\n", (usecTimestampNow() - usecTimestamp(&lastReceive)) / 1000); gettimeofday(&lastReceive, NULL); // add or update the existing interface agent From 242d6898ba96d190c3322de3dbc50cb988e66b7f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Mar 2013 10:22:28 -0700 Subject: [PATCH 26/29] clean up the gitignore, add some basic CMake information to README --- .gitignore | 11 +---------- README.md | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index fa3df0078e..b6ca7c09e8 100644 --- a/.gitignore +++ b/.gitignore @@ -23,13 +23,4 @@ profile *.moved-aside DerivedData .idea/ -*.hmap - -interface/Debug/ -interface/external/ -interface/hifi.build/ - -shared/Debug/ -shared/hifi.build/ - -interface/includes/InterfaceConfig.h +*.hmap \ No newline at end of file diff --git a/README.md b/README.md index 3f0c53db6b..49112f2e38 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,17 @@ interface ========= -Test platform for various render and interface tests for next-gen VR system \ No newline at end of file +Test platform for various render and interface tests for next-gen VR system. + +CMake +===== + +This project uses CMake to generate build files and project files for your platform. + +Create a build directory in the root of your checkout and then run the CMake build from there. This will keep the rest of the directory clean, and makes the gitignore a little easier to handle, since we can just ignore build. + + mkdir build + cd build + cmake .. -GXCode + +Those are the commands used on OS X to run CMake from the build folder and generate XCode project files. \ No newline at end of file From b9ca7c213414382172c2cae54a3c643d9973a943 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Mar 2013 10:23:12 -0700 Subject: [PATCH 27/29] change some sentence structure in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 49112f2e38..51de544590 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ CMake This project uses CMake to generate build files and project files for your platform. -Create a build directory in the root of your checkout and then run the CMake build from there. This will keep the rest of the directory clean, and makes the gitignore a little easier to handle, since we can just ignore build. +Create a build directory in the root of your checkout and then run the CMake build from there. This will keep the rest of the directory clean, and makes the gitignore a little easier to handle (since we can just ignore build). mkdir build cd build From f29510ceb09784bd10a5a72ce1368b73a8069e6c Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Mar 2013 10:24:02 -0700 Subject: [PATCH 28/29] remove setSourcePosition that re-appeared on merge --- interface/src/Audio.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/interface/src/Audio.h b/interface/src/Audio.h index e4760c4f6f..cee2fb07e3 100644 --- a/interface/src/Audio.h +++ b/interface/src/Audio.h @@ -29,8 +29,6 @@ public: void getInputLoudness(float * lastLoudness, float * averageLoudness); void updateMixerParams(in_addr_t mixerAddress, in_port_t mixerPort); - void setSourcePosition(glm::vec3 position); - // terminates audio I/O bool terminate(); private: From 0739d395686f429d13760584453525d4eeb87f0f Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Tue, 12 Mar 2013 10:42:41 -0700 Subject: [PATCH 29/29] add missing copy constructors for Head and Hand --- interface/src/Hand.cpp | 19 ++++++++++++++++ interface/src/Hand.h | 1 + interface/src/Head.cpp | 51 +++++++++++++++++++++++++++++++++++++----- interface/src/Head.h | 1 + 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/interface/src/Hand.cpp b/interface/src/Hand.cpp index ea57c91a9b..b23d89f7bd 100644 --- a/interface/src/Hand.cpp +++ b/interface/src/Hand.cpp @@ -27,6 +27,25 @@ Hand::Hand(glm::vec3 initcolor) renderPointer = true; } +Hand::Hand(const Hand &otherHand) { + color = otherHand.color; + noise = otherHand.noise; + scale = otherHand.scale; + position = otherHand.position; + target = otherHand.target; + velocity = otherHand.color; + pitch = otherHand.pitch; + yaw = otherHand.yaw; + roll = otherHand.roll; + pitchRate = otherHand.pitchRate; + yawRate = otherHand.yawRate; + rollRate = otherHand.rollRate; + transmitterTimer = otherHand.transmitterTimer; + transmitterHz = otherHand.transmitterHz; + transmitterPackets = otherHand.transmitterPackets; + renderPointer = otherHand.renderPointer; +} + void Hand::reset() { position.x = DEFAULT_X; diff --git a/interface/src/Hand.h b/interface/src/Hand.h index 3c32c3553d..bda0fb2145 100644 --- a/interface/src/Hand.h +++ b/interface/src/Hand.h @@ -19,6 +19,7 @@ class Hand { public: Hand(glm::vec3 color); + Hand(const Hand &otherHand); void simulate (float deltaTime); void render (int isMine); void reset (); diff --git a/interface/src/Head.cpp b/interface/src/Head.cpp index cc330c396c..eea423eb8f 100644 --- a/interface/src/Head.cpp +++ b/interface/src/Head.cpp @@ -72,11 +72,10 @@ Head::Head() averageLoudness = 0.0; lastLoudness = 0.0; browAudioLift = 0.0; + noise = 0; - setNoise(0); hand = new Hand(glm::vec3(skinColor[0], skinColor[1], skinColor[2])); - if (iris_texture.size() == 0) { unsigned error = lodepng::decode(iris_texture, iris_texture_width, iris_texture_height, iris_texture_file); if (error != 0) { @@ -85,6 +84,50 @@ Head::Head() } } +Head::Head(const Head &otherHead) { + position = otherHead.position; + PupilSize = otherHead.PupilSize; + interPupilDistance = otherHead.interPupilDistance; + interBrowDistance = otherHead.interBrowDistance; + NominalPupilSize = otherHead.NominalPupilSize; + Yaw = otherHead.Yaw; + EyebrowPitch[0] = otherHead.EyebrowPitch[0]; + EyebrowPitch[1] = otherHead.EyebrowPitch[1]; + EyebrowRoll[0] = otherHead.EyebrowRoll[0]; + EyebrowRoll[1] = otherHead.EyebrowRoll[1]; + MouthPitch = otherHead.MouthPitch; + MouthYaw = otherHead.MouthYaw; + MouthWidth = otherHead.MouthWidth; + MouthHeight = otherHead.MouthHeight; + EyeballPitch[0] = otherHead.EyeballPitch[0]; + EyeballPitch[1] = otherHead.EyeballPitch[1]; + EyeballScaleX = otherHead.EyeballScaleX; + EyeballScaleY = otherHead.EyeballScaleY; + EyeballScaleZ = otherHead.EyeballScaleZ; + EyeballYaw[0] = otherHead.EyeballYaw[0]; + EyeballYaw[1] = otherHead.EyeballYaw[1]; + PitchTarget = otherHead.PitchTarget; + YawTarget = otherHead.YawTarget; + NoiseEnvelope = otherHead.NoiseEnvelope; + PupilConverge = otherHead.PupilConverge; + leanForward = otherHead.leanForward; + leanSideways = otherHead.leanSideways; + eyeContact = otherHead.eyeContact; + eyeContactTarget = otherHead.eyeContactTarget; + scale = otherHead.scale; + renderYaw = otherHead.renderYaw; + renderPitch = otherHead.renderPitch; + audioAttack = otherHead.audioAttack; + loudness = otherHead.loudness; + averageLoudness = otherHead.averageLoudness; + lastLoudness = otherHead.lastLoudness; + browAudioLift = otherHead.browAudioLift; + noise = otherHead.noise; + + Hand newHand = Hand(*otherHead.hand); + hand = &newHand; +} + Head::~Head() { if (sphere) { gluDeleteQuadric(sphere); @@ -95,10 +138,6 @@ Head* Head::clone() const { return new Head(*this); } -Head* Head::clone() const { - return new Head(*this); -} - void Head::reset() { Pitch = Yaw = Roll = 0; diff --git a/interface/src/Head.h b/interface/src/Head.h index c7ffc9502d..fe22c9efdb 100644 --- a/interface/src/Head.h +++ b/interface/src/Head.h @@ -24,6 +24,7 @@ class Head : public AgentData { public: Head(); ~Head(); + Head(const Head &otherHead); Head* clone() const; void reset();