From 666830c04d16cac35f021b02ff77e22c16821f16 Mon Sep 17 00:00:00 2001 From: Stephen Birarda Date: Mon, 8 Jun 2015 11:30:12 -0700 Subject: [PATCH] add a sound once first AM packet received --- examples/dialTone.js | 23 + interface/resources/sounds/short1.wav | Bin 0 -> 57000 bytes interface/src/Application.cpp | 401 +++++++++--------- libraries/audio-client/src/AudioClient.cpp | 11 + libraries/audio-client/src/AudioClient.h | 63 +-- libraries/audio/src/AudioInjector.cpp | 100 ++--- libraries/audio/src/Sound.cpp | 34 +- .../src/AudioScriptingInterface.cpp | 6 +- .../src/AudioScriptingInterface.h | 9 +- libraries/shared/src/PathUtils.cpp | 2 +- libraries/shared/src/PathUtils.h | 13 +- 11 files changed, 358 insertions(+), 304 deletions(-) create mode 100644 examples/dialTone.js create mode 100644 interface/resources/sounds/short1.wav diff --git a/examples/dialTone.js b/examples/dialTone.js new file mode 100644 index 0000000000..0748d0ba94 --- /dev/null +++ b/examples/dialTone.js @@ -0,0 +1,23 @@ +// +// dialTone.js +// examples +// +// Created by Stephen Birarda on 06/08/15. +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +// setup the local sound we're going to use +var connectSound = SoundCache.getSound("file://" + Paths.resources + "sounds/short1.wav"); + +// setup the options needed for that sound +var connectSoundOptions = { + localOnly: true +} + +// play the sound locally once we get the first audio packet from a mixer +Audio.receivedFirstPacket.connect(function(){ + Audio.playSound(connectSound, connectSoundOptions); +}); diff --git a/interface/resources/sounds/short1.wav b/interface/resources/sounds/short1.wav new file mode 100644 index 0000000000000000000000000000000000000000..fb03f5dd49dbcac1baa3062fece55f699c631605 GIT binary patch literal 57000 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 DependencyManager::registerInheritance(); Setting::init(); - + // Set dependencies auto addressManager = DependencyManager::set(); auto nodeList = DependencyManager::set(NodeType::Agent, listenPort); @@ -289,6 +289,7 @@ bool setupEssentials(int& argc, char** argv) { auto discoverabilityManager = DependencyManager::set(); auto sceneScriptingInterface = DependencyManager::set(); auto offscreenUi = DependencyManager::set(); + auto pathUtils = DependencyManager::set(); return true; } @@ -355,9 +356,9 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : QFontDatabase::addApplicationFont(PathUtils::resourcesPath() + "styles/Inconsolata.otf"); _window->setWindowTitle("Interface"); - + Model::setAbstractViewStateInterface(this); // The model class will sometimes need to know view state details from us - + auto nodeList = DependencyManager::get(); _myAvatar = DependencyManager::get()->getMyAvatar(); @@ -369,7 +370,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _bookmarks = new Bookmarks(); // Before setting up the menu _runningScriptsWidget = new RunningScriptsWidget(_window); - + // start the nodeThread so its event loop is running QThread* nodeThread = new QThread(this); nodeThread->setObjectName("Datagram Processor Thread"); @@ -377,14 +378,14 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : // make sure the node thread is given highest priority nodeThread->setPriority(QThread::TimeCriticalPriority); - + _datagramProcessor = new DatagramProcessor(nodeList.data()); - + // have the NodeList use deleteLater from DM customDeleter nodeList->setCustomDeleter([](Dependency* dependency) { static_cast(dependency)->deleteLater(); }); - + // put the NodeList and datagram processing on the node thread nodeList->moveToThread(nodeThread); @@ -401,20 +402,22 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : // put the audio processing on a separate thread QThread* audioThread = new QThread(); audioThread->setObjectName("Audio Thread"); - + auto audioIO = DependencyManager::get(); - + audioIO->setPositionGetter(getPositionForAudio); audioIO->setOrientationGetter(getOrientationForAudio); - + audioIO->moveToThread(audioThread); connect(audioThread, &QThread::started, audioIO.data(), &AudioClient::start); connect(audioIO.data(), &AudioClient::destroyed, audioThread, &QThread::quit); connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater); connect(audioIO.data(), &AudioClient::muteToggled, this, &Application::audioMuteToggled); + connect(audioIO.data(), &AudioClient::receivedFirstPacket, + &AudioScriptingInterface::getInstance(), &AudioScriptingInterface::receivedFirstPacket); audioThread->start(); - + const DomainHandler& domainHandler = nodeList->getDomainHandler(); connect(&domainHandler, SIGNAL(hostnameChanged(const QString&)), SLOT(domainChanged(const QString&))); @@ -433,7 +436,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : auto discoverabilityManager = DependencyManager::get(); connect(locationUpdateTimer, &QTimer::timeout, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); locationUpdateTimer->start(DATA_SERVER_LOCATION_CHANGE_UPDATE_MSECS); - + // if we get a domain change, immediately attempt update location in metaverse server connect(&nodeList->getDomainHandler(), &DomainHandler::connectedToDomain, discoverabilityManager.data(), &DiscoverabilityManager::updateLocation); @@ -467,13 +470,13 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : // once the event loop has started, check and signal for an access token QMetaObject::invokeMethod(&accountManager, "checkAndSignalForAccessToken", Qt::QueuedConnection); - + auto addressManager = DependencyManager::get(); - + // use our MyAvatar position and quat for address manager path addressManager->setPositionGetter(getPositionForPath); addressManager->setOrientationGetter(getOrientationForPath); - + connect(addressManager.data(), &AddressManager::hostChanged, this, &Application::updateWindowTitle); connect(this, &QCoreApplication::aboutToQuit, addressManager.data(), &AddressManager::storeCurrentAddress); @@ -505,7 +508,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : cache->setMaximumCacheSize(MAXIMUM_CACHE_SIZE); cache->setCacheDirectory(!cachePath.isEmpty() ? cachePath : "interfaceCache"); networkAccessManager.setCache(cache); - + ResourceCache::setRequestLimit(3); _window->setCentralWidget(_glWidget); @@ -556,11 +559,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : connect(nodeList.data(), SIGNAL(dataReceived(const quint8, const int)), bandwidthRecorder.data(), SLOT(updateInboundData(const quint8, const int))); - connect(&_myAvatar->getSkeletonModel(), &SkeletonModel::skeletonLoaded, + connect(&_myAvatar->getSkeletonModel(), &SkeletonModel::skeletonLoaded, this, &Application::checkSkeleton, Qt::QueuedConnection); // Setup the userInputMapper with the actions - // Setup the keyboardMouseDevice and the user input mapper with the default bindings + // Setup the keyboardMouseDevice and the user input mapper with the default bindings _keyboardMouseDevice.registerToUserInputMapper(_userInputMapper); _keyboardMouseDevice.assignDefaultInputMapping(_userInputMapper); @@ -576,7 +579,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : // do this as late as possible so that all required subsystems are initialized loadScripts(); } - + loadSettings(); int SAVE_SETTINGS_INTERVAL = 10 * MSECS_PER_SECOND; // Let's save every seconds for now connect(&_settingsTimer, &QTimer::timeout, this, &Application::saveSettings); @@ -586,12 +589,12 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : _settingsTimer.setSingleShot(false); _settingsTimer.setInterval(SAVE_SETTINGS_INTERVAL); _settingsThread.start(); - + _trayIcon->show(); - + // set the local loopback interface for local sounds from audio scripts AudioScriptingInterface::getInstance().setLocalAudioInterface(audioIO.data()); - + #ifdef HAVE_RTMIDI // setup the MIDIManager MIDIManager& midiManagerInstance = MIDIManager::getInstance(); @@ -599,7 +602,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : #endif this->installEventFilter(this); - // The offscreen UI needs to intercept the mouse and keyboard + // The offscreen UI needs to intercept the mouse and keyboard // events coming from the onscreen window _glWidget->installEventFilter(DependencyManager::get().data()); @@ -617,7 +620,7 @@ Application::Application(int& argc, char** argv, QElapsedTimer &startup_time) : void Application::aboutToQuit() { emit beforeAboutToQuit(); - + _aboutToQuit = true; cleanupBeforeQuit(); } @@ -629,7 +632,7 @@ void Application::cleanupBeforeQuit() { _datagramProcessor->shutdown(); // tell the datagram processor we're shutting down, so it can short circuit _entities.shutdown(); // tell the entities system we're shutting down, so it will stop running scripts ScriptEngine::stopAllScripts(this); // stop all currently running global scripts - + // first stop all timers directly or by invokeMethod // depending on what thread they run in locationUpdateTimer->stop(); @@ -656,11 +659,11 @@ void Application::cleanupBeforeQuit() { // let the avatar mixer know we're out MyAvatar::sendKillAvatar(); - + // stop the AudioClient QMetaObject::invokeMethod(DependencyManager::get().data(), "stop", Qt::BlockingQueuedConnection); - + // destroy the AudioClient so it and its thread have a chance to go down safely DependencyManager::destroy(); @@ -669,12 +672,12 @@ void Application::cleanupBeforeQuit() { #endif } -Application::~Application() { +Application::~Application() { EntityTree* tree = _entities.getTree(); tree->lockForWrite(); _entities.getTree()->setSimulation(NULL); tree->unlock(); - + _octreeProcessor.terminate(); _entityEditSender.terminate(); @@ -684,7 +687,7 @@ Application::~Application() { _myAvatar = NULL; ModelEntityItem::cleanupLoadedAnimations(); - + // stop the glWidget frame timer so it doesn't call paintGL _glWidget->stopFrameTimer(); @@ -699,10 +702,10 @@ Application::~Application() { DependencyManager::destroy(); DependencyManager::destroy(); DependencyManager::destroy(); - + QThread* nodeThread = DependencyManager::get()->thread(); DependencyManager::destroy(); - + // ask the node thread to quit and wait until it is done nodeThread->quit(); nodeThread->wait(); @@ -756,8 +759,8 @@ void Application::initializeGL() { initDisplay(); qCDebug(interfaceapp, "Initialized Display."); - // The UI can't be created until the primary OpenGL - // context is created, because it needs to share + // The UI can't be created until the primary OpenGL + // context is created, because it needs to share // texture resources initializeUi(); qCDebug(interfaceapp, "Initialized Offscreen UI."); @@ -909,7 +912,7 @@ void Application::paintGL() { OculusManager::display(_glWidget, _myAvatar->getWorldAlignedOrientation(), _myAvatar->getDefaultEyePosition(), _myCamera); } } else if (TV3DManager::isConnected()) { - + TV3DManager::display(_myCamera); } else { @@ -928,7 +931,7 @@ void Application::paintGL() { if (Menu::getInstance()->isOptionChecked(MenuOption::FullscreenMirror)) { _rearMirrorTools->render(true, _glWidget->mapFromGlobal(QCursor::pos())); } else if (Menu::getInstance()->isOptionChecked(MenuOption::Mirror)) { - renderRearViewMirror(_mirrorViewRect); + renderRearViewMirror(_mirrorViewRect); } auto finalFbo = DependencyManager::get()->render(); @@ -945,7 +948,7 @@ void Application::paintGL() { if (!OculusManager::isConnected() || OculusManager::allowSwap()) { _glWidget->swapBuffers(); - } + } if (OculusManager::isConnected()) { OculusManager::endFrameTiming(); @@ -1035,7 +1038,7 @@ void Application::updateProjectionMatrix(Camera& camera, bool updateViewFrustum) // Tell our viewFrustum about this change, using the application camera if (updateViewFrustum) { loadViewFrustum(camera, _viewFrustum); - } + } glMatrixMode(GL_MODELVIEW); } @@ -1103,11 +1106,11 @@ bool Application::event(QEvent* event) { // handle custom URL if (event->type() == QEvent::FileOpen) { - + QFileOpenEvent* fileEvent = static_cast(event); QUrl url = fileEvent->url(); - + if (!url.isEmpty()) { QString urlString = url.toString(); if (canAcceptURL(urlString)) { @@ -1116,7 +1119,7 @@ bool Application::event(QEvent* event) { } return false; } - + if (HFActionEvent::types().contains(event->type())) { _controllerScriptingInterface.handleMetaEvent(static_cast(event)); } @@ -1182,7 +1185,7 @@ void Application::keyPressEvent(QKeyEvent* event) { Menu::getInstance()->triggerOption(MenuOption::AddressBar); } else if (isShifted) { Menu::getInstance()->triggerOption(MenuOption::LodTools); - } + } break; case Qt::Key_F: { @@ -1223,7 +1226,7 @@ void Application::keyPressEvent(QKeyEvent* event) { case Qt::Key_Backslash: Menu::getInstance()->triggerOption(MenuOption::Chat); break; - + case Qt::Key_Up: if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { if (!isShifted) { @@ -1352,18 +1355,18 @@ void Application::keyPressEvent(QKeyEvent* event) { computePickRay(getTrueMouseX(), getTrueMouseY())); sendEvent(this, &startActionEvent); } - + break; } case Qt::Key_Escape: { OculusManager::abandonCalibration(); - + if (!event->isAutoRepeat()) { // this starts the HFCancelEvent HFBackEvent startBackEvent(HFBackEvent::startType()); sendEvent(this, &startBackEvent); } - + break; } @@ -1425,7 +1428,7 @@ void Application::keyReleaseEvent(QKeyEvent* event) { void Application::focusOutEvent(QFocusEvent* event) { _keyboardMouseDevice.focusOutEvent(event); - + // synthesize events for keys currently pressed, since we may not get their release events foreach (int key, _keysPressed) { QKeyEvent event(QEvent::KeyRelease, key, Qt::NoModifier); @@ -1440,12 +1443,12 @@ void Application::mouseMoveEvent(QMouseEvent* event, unsigned int deviceID) { if (!_lastMouseMoveWasSimulated) { _lastMouseMove = usecTimestampNow(); } - + if (_aboutToQuit) { return; } - - if (Menu::getInstance()->isOptionChecked(MenuOption::Fullscreen) + + if (Menu::getInstance()->isOptionChecked(MenuOption::Fullscreen) && !Menu::getInstance()->isOptionChecked(MenuOption::EnableVRMode)) { // Show/hide menu bar in fullscreen if (event->globalY() > _menuBarHeight) { @@ -1458,7 +1461,7 @@ void Application::mouseMoveEvent(QMouseEvent* event, unsigned int deviceID) { } _entities.mouseMoveEvent(event, deviceID); - + _controllerScriptingInterface.emitMouseMoveEvent(event, deviceID); // send events to any registered scripts // if one of our scripts have asked to capture this event, then stop processing it if (_controllerScriptingInterface.isMouseCaptured()) { @@ -1466,7 +1469,7 @@ void Application::mouseMoveEvent(QMouseEvent* event, unsigned int deviceID) { } _keyboardMouseDevice.mouseMoveEvent(event, deviceID); - + } void Application::mousePressEvent(QMouseEvent* event, unsigned int deviceID) { @@ -1492,7 +1495,7 @@ void Application::mousePressEvent(QMouseEvent* event, unsigned int deviceID) { _mouseDragStartedX = getTrueMouseX(); _mouseDragStartedY = getTrueMouseY(); _mousePressed = true; - + if (mouseOnScreen()) { if (DependencyManager::get()->mousePressEvent(getMouseX(), getMouseY())) { // stop propagation @@ -1509,7 +1512,7 @@ void Application::mousePressEvent(QMouseEvent* event, unsigned int deviceID) { return; } } - + // nobody handled this - make it an action event on the _window object HFActionEvent actionEvent(HFActionEvent::startType(), computePickRay(event->x(), event->y())); @@ -1557,14 +1560,14 @@ void Application::mouseReleaseEvent(QMouseEvent* event, unsigned int deviceID) { if (event->button() == Qt::LeftButton) { _mousePressed = false; - + if (Menu::getInstance()->isOptionChecked(MenuOption::Stats) && mouseOnScreen()) { // let's set horizontal offset to give stats some margin to mirror int horizontalOffset = MIRROR_VIEW_WIDTH; Stats::getInstance()->checkClick(getMouseX(), getMouseY(), getMouseDragStartedX(), getMouseDragStartedY(), horizontalOffset); } - + // fire an action end event HFActionEvent actionEvent(HFActionEvent::endType(), computePickRay(event->x(), event->y())); @@ -1673,7 +1676,7 @@ void Application::dropEvent(QDropEvent *event) { } } } - + if (atLeastOneFileAccepted) { event->acceptProposedAction(); } @@ -1693,7 +1696,7 @@ void Application::dragEnterEvent(QDragEnterEvent* event) { bool Application::acceptSnapshot(const QString& urlString) { QUrl url(urlString); QString snapshotPath = url.toLocalFile(); - + SnapshotMetaData* snapshotData = Snapshot::parseSnapshotData(snapshotPath); if (snapshotData) { if (!snapshotData->getURL().toString().isEmpty()) { @@ -1703,7 +1706,7 @@ bool Application::acceptSnapshot(const QString& urlString) { QMessageBox msgBox; msgBox.setText("No location details were found in the file " + snapshotPath + ", try dragging in an authentic Hifi snapshot."); - + msgBox.setStandardButtons(QMessageBox::Ok); msgBox.exec(); } @@ -1736,7 +1739,7 @@ void Application::checkFPS() { void Application::idle() { PerformanceTimer perfTimer("idle"); - + if (_aboutToQuit) { return; // bail early, nothing to do here. } @@ -1865,15 +1868,15 @@ void Application::setEnableVRMode(bool enableVRMode) { OculusManager::recalibrate(); } else { OculusManager::abandonCalibration(); - + _mirrorCamera.setHmdPosition(glm::vec3()); _mirrorCamera.setHmdRotation(glm::quat()); _myCamera.setHmdPosition(glm::vec3()); _myCamera.setHmdRotation(glm::quat()); } - + resizeGL(); - + updateCursorVisibility(); } @@ -1926,7 +1929,7 @@ int Application::getMouseDragStartedY() const { FaceTracker* Application::getActiveFaceTracker() { auto faceshift = DependencyManager::get(); auto dde = DependencyManager::get(); - + return (dde->isActive() ? static_cast(dde.data()) : (faceshift->isActive() ? static_cast(faceshift.data()) : NULL)); } @@ -2059,14 +2062,14 @@ void Application::saveSettings() { bool Application::importEntities(const QString& urlOrFilename) { _entityClipboard.eraseAllOctreeElements(); - + QUrl url(urlOrFilename); - + // if the URL appears to be invalid or relative, then it is probably a local file if (!url.isValid() || url.isRelative()) { url = QUrl::fromLocalFile(urlOrFilename); } - + bool success = _entityClipboard.readFromURL(url.toString()); if (success) { _entityClipboard.reaverageOctreeElements(); @@ -2090,7 +2093,7 @@ void Application::initDisplay() { void Application::init() { // Make sure Login state is up to date DependencyManager::get()->toggleLoginDialog(); - + _environment.init(); DependencyManager::get()->init(this); @@ -2118,7 +2121,7 @@ void Application::init() { _timerStart.start(); _lastTimeUpdated.start(); - + // when --url in command line, teleport to location const QString HIFI_URL_COMMAND_LINE_KEY = "--url"; int urlIndex = arguments().indexOf(HIFI_URL_COMMAND_LINE_KEY); @@ -2126,11 +2129,11 @@ void Application::init() { if (urlIndex != -1) { addressLookupString = arguments().value(urlIndex + 1); } - + DependencyManager::get()->loadSettings(addressLookupString); - + qCDebug(interfaceapp) << "Loaded settings"; - + #ifdef __APPLE__ if (Menu::getInstance()->isOptionChecked(MenuOption::SixenseEnabled)) { // on OS X we only setup sixense if the user wants it on - this allows running without the hid_init crash @@ -2238,7 +2241,7 @@ void Application::updateMouseRay() { PickRay pickRay = computePickRay(getTrueMouseX(), getTrueMouseY()); _mouseRayOrigin = pickRay.origin; _mouseRayDirection = pickRay.direction; - + // adjust for mirroring if (_myCamera.getMode() == CAMERA_MODE_MIRROR) { glm::vec3 mouseRayOffset = _mouseRayOrigin - _viewFrustum.getPosition(); @@ -2270,11 +2273,11 @@ void Application::updateMyAvatarLookAtPosition() { lookAtSpot = OculusManager::getRightEyePosition(); } } - + } else { AvatarSharedPointer lookingAt = _myAvatar->getLookAtTargetAvatar().toStrongRef(); if (lookingAt && _myAvatar != lookingAt.data()) { - + isLookingAtSomeone = true; // If I am looking at someone else, look directly at one of their eyes if (tracker && !tracker->isMuted()) { @@ -2361,7 +2364,7 @@ void Application::updateDialogs(float deltaTime) { bool showWarnings = Menu::getInstance()->isOptionChecked(MenuOption::PipelineWarnings); PerformanceWarning warn(showWarnings, "Application::updateDialogs()"); auto dialogsManager = DependencyManager::get(); - + // Update bandwidth dialog, if any BandwidthDialog* bandwidthDialog = dialogsManager->getBandwidthDialog(); if (bandwidthDialog) { @@ -2567,7 +2570,7 @@ void Application::update(float deltaTime) { } } - // send packet containing downstream audio stats to the AudioMixer + // send packet containing downstream audio stats to the AudioMixer { quint64 sinceLastNack = now - _lastSendDownstreamAudioStats; if (sinceLastNack > TOO_LONG_SINCE_LAST_SEND_DOWNSTREAM_AUDIO_STATS) { @@ -2589,64 +2592,64 @@ int Application::sendNackPackets() { // iterates thru all nodes in NodeList auto nodeList = DependencyManager::get(); - + nodeList->eachNode([&](const SharedNodePointer& node){ - + if (node->getActiveSocket() && node->getType() == NodeType::EntityServer) { - + QUuid nodeUUID = node->getUUID(); - + // if there are octree packets from this node that are waiting to be processed, // don't send a NACK since the missing packets may be among those waiting packets. if (_octreeProcessor.hasPacketsToProcessFrom(nodeUUID)) { return; } - + _octreeSceneStatsLock.lockForRead(); - + // retreive octree scene stats of this node if (_octreeServerSceneStats.find(nodeUUID) == _octreeServerSceneStats.end()) { _octreeSceneStatsLock.unlock(); return; } - + // get sequence number stats of node, prune its missing set, and make a copy of the missing set SequenceNumberStats& sequenceNumberStats = _octreeServerSceneStats[nodeUUID].getIncomingOctreeSequenceNumberStats(); sequenceNumberStats.pruneMissingSet(); const QSet missingSequenceNumbers = sequenceNumberStats.getMissingSet(); - + _octreeSceneStatsLock.unlock(); - + // construct nack packet(s) for this node int numSequenceNumbersAvailable = missingSequenceNumbers.size(); QSet::const_iterator missingSequenceNumbersIterator = missingSequenceNumbers.constBegin(); while (numSequenceNumbersAvailable > 0) { - + char* dataAt = packet; int bytesRemaining = MAX_PACKET_SIZE; - + // pack header int numBytesPacketHeader = nodeList->populatePacketHeader(packet, PacketTypeOctreeDataNack); dataAt += numBytesPacketHeader; bytesRemaining -= numBytesPacketHeader; - + // calculate and pack the number of sequence numbers int numSequenceNumbersRoomFor = (bytesRemaining - sizeof(uint16_t)) / sizeof(OCTREE_PACKET_SEQUENCE); uint16_t numSequenceNumbers = min(numSequenceNumbersAvailable, numSequenceNumbersRoomFor); uint16_t* numSequenceNumbersAt = (uint16_t*)dataAt; *numSequenceNumbersAt = numSequenceNumbers; dataAt += sizeof(uint16_t); - + // pack sequence numbers for (int i = 0; i < numSequenceNumbers; i++) { OCTREE_PACKET_SEQUENCE* sequenceNumberAt = (OCTREE_PACKET_SEQUENCE*)dataAt; *sequenceNumberAt = *missingSequenceNumbersIterator; dataAt += sizeof(OCTREE_PACKET_SEQUENCE); - + missingSequenceNumbersIterator++; } numSequenceNumbersAvailable -= numSequenceNumbers; - + // send it nodeList->writeUnverifiedDatagram(packet, dataAt - packet, node); packetsSent++; @@ -2688,7 +2691,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node int unknownJurisdictionServers = 0; auto nodeList = DependencyManager::get(); - + nodeList->eachNode([&](const SharedNodePointer& node) { // only send to the NodeTypes that are serverType if (node->getActiveSocket() && node->getType() == serverType) { @@ -2748,17 +2751,17 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node if (wantExtraDebugging) { qCDebug(interfaceapp, "perServerPPS: %d perUnknownServer: %d", perServerPPS, perUnknownServer); } - + nodeList->eachNode([&](const SharedNodePointer& node){ // only send to the NodeTypes that are serverType if (node->getActiveSocket() && node->getType() == serverType) { - + // get the server bounds for this server QUuid nodeUUID = node->getUUID(); - + bool inView = false; bool unknownView = false; - + // if we haven't heard from this voxel server, go ahead and send it a query, so we // can get the jurisdiction... if (jurisdictions.find(nodeUUID) == jurisdictions.end()) { @@ -2768,9 +2771,9 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node } } else { const JurisdictionMap& map = (jurisdictions)[nodeUUID]; - + unsigned char* rootCode = map.getRootOctalCode(); - + if (rootCode) { VoxelPositionSize rootDetails; voxelDetailsForCode(rootCode, rootDetails); @@ -2780,7 +2783,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node rootDetails.s * TREE_SCALE); - + ViewFrustum::location serverFrustumLocation = _viewFrustum.cubeInFrustum(serverBounds); if (serverFrustumLocation != ViewFrustum::OUTSIDE) { inView = true; @@ -2793,7 +2796,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node } } } - + if (inView) { _octreeQuery.setMaxQueryPacketsPerSecond(perServerPPS); } else if (unknownView) { @@ -2801,7 +2804,7 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node qCDebug(interfaceapp) << "no known jurisdiction for node " << *node << ", give it budget of " << perUnknownServer << " to send us jurisdiction."; } - + // set the query's position/orientation to be degenerate in a manner that will get the scene quickly // If there's only one server, then don't do this, and just let the normal voxel query pass through // as expected... this way, we will actually get a valid scene if there is one to be seen @@ -2828,12 +2831,12 @@ void Application::queryOctree(NodeType_t serverType, PacketType packetType, Node // insert packet type/version and node UUID endOfQueryPacket += nodeList->populatePacketHeader(reinterpret_cast(endOfQueryPacket), packetType); - + // encode the query data... endOfQueryPacket += _octreeQuery.getBroadcastData(endOfQueryPacket); - + int packetLength = endOfQueryPacket - queryPacket; - + // make sure we still have an active socket nodeList->writeUnverifiedDatagram(reinterpret_cast(queryPacket), packetLength, node); } @@ -2850,7 +2853,7 @@ bool Application::isHMDMode() const { QRect Application::getDesirableApplicationGeometry() { QRect applicationGeometry = getWindow()->geometry(); - + // If our parent window is on the HMD, then don't use its geometry, instead use // the "main screen" geometry. HMDToolsDialog* hmdTools = DependencyManager::get()->getHMDToolsDialog(); @@ -2911,14 +2914,14 @@ void Application::updateShadowMap() { glm::vec3 lightDirection = getSunDirection(); glm::quat rotation = rotationBetween(IDENTITY_FRONT, lightDirection); glm::quat inverseRotation = glm::inverse(rotation); - + const float SHADOW_MATRIX_DISTANCES[] = { 0.0f, 2.0f, 6.0f, 14.0f, 30.0f }; const glm::vec2 MAP_COORDS[] = { glm::vec2(0.0f, 0.0f), glm::vec2(0.5f, 0.0f), glm::vec2(0.0f, 0.5f), glm::vec2(0.5f, 0.5f) }; - + float frustumScale = 1.0f / (_viewFrustum.getFarClip() - _viewFrustum.getNearClip()); loadViewFrustum(_myCamera, _viewFrustum); - + int matrixCount = 1; //int targetSize = fbo->width(); int sourceSize = shadowFramebuffer->getWidth(); @@ -2960,12 +2963,12 @@ void Application::updateShadowMap() { _shadowDistances[i] = -glm::distance(_viewFrustum.getPosition(), center) - radius * RADIUS_SCALE; } center = inverseRotation * center; - + // to reduce texture "shimmer," move in texel increments float texelSize = (2.0f * radius) / targetSize; center = glm::vec3(roundf(center.x / texelSize) * texelSize, roundf(center.y / texelSize) * texelSize, roundf(center.z / texelSize) * texelSize); - + glm::vec3 minima(center.x - radius, center.y - radius, center.z - radius); glm::vec3 maxima(center.x + radius, center.y + radius, center.z + radius); @@ -3004,7 +3007,7 @@ void Application::updateShadowMap() { // store view matrix without translation, which we'll use for precision-sensitive objects updateUntranslatedViewMatrix(); - + // Equivalent to what is happening with _untranslatedViewMatrix and the _viewMatrixTranslation // the viewTransofmr object is updatded with the correct values and saved, // this is what is used for rendering the Entities and avatars @@ -3047,9 +3050,9 @@ void Application::updateShadowMap() { glMatrixMode(GL_MODELVIEW); } - + // fbo->release(); - + glViewport(0, 0, _glWidget->getDeviceWidth(), _glWidget->getDeviceHeight()); activeRenderingThread = nullptr; } @@ -3081,11 +3084,11 @@ bool Application::shouldRenderMesh(float largestDimension, float distanceToCamer return DependencyManager::get()->shouldRenderMesh(largestDimension, distanceToCamera); } -float Application::getSizeScale() const { +float Application::getSizeScale() const { return DependencyManager::get()->getOctreeSizeScale(); } -int Application::getBoundaryLevelAdjust() const { +int Application::getBoundaryLevelAdjust() const { return DependencyManager::get()->getBoundaryLevelAdjust(); } @@ -3109,27 +3112,27 @@ PickRay Application::computePickRay(float x, float y) const { QImage Application::renderAvatarBillboard() { auto primaryFramebuffer = DependencyManager::get()->getPrimaryFramebuffer(); glBindFramebuffer(GL_FRAMEBUFFER, gpu::GLBackend::getFramebufferID(primaryFramebuffer)); - + // clear the alpha channel so the background is transparent glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_TRUE); glClearColor(0.0, 0.0, 0.0, 0.0); glClear(GL_COLOR_BUFFER_BIT); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE); - + // the "glow" here causes an alpha of one Glower glower; - + const int BILLBOARD_SIZE = 64; renderRearViewMirror(QRect(0, _glWidget->getDeviceHeight() - BILLBOARD_SIZE, BILLBOARD_SIZE, BILLBOARD_SIZE), true); - + QImage image(BILLBOARD_SIZE, BILLBOARD_SIZE, QImage::Format_ARGB32); glReadPixels(0, 0, BILLBOARD_SIZE, BILLBOARD_SIZE, GL_BGRA, GL_UNSIGNED_BYTE, image.bits()); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); - + glBindFramebuffer(GL_FRAMEBUFFER, 0); - + return image; } @@ -3214,11 +3217,11 @@ void Application::displaySide(Camera& theCamera, bool selfAvatarOnly, bool billb } if (renderSide != RenderArgs::MONO) { glm::mat4 invView = glm::inverse(_untranslatedViewMatrix); - + viewTransform.evalFromRawMatrix(invView); viewTransform.preTranslate(_viewMatrixTranslation); } - + setViewTransform(viewTransform); glTranslatef(_viewMatrixTranslation.x, _viewMatrixTranslation.y, _viewMatrixTranslation.z); @@ -3269,7 +3272,7 @@ void Application::displaySide(Camera& theCamera, bool selfAvatarOnly, bool billb const float APPROXIMATE_DISTANCE_FROM_HORIZON = 0.1f; const float DOUBLE_APPROXIMATE_DISTANCE_FROM_HORIZON = 0.2f; - glm::vec3 sunDirection = (getAvatarPosition() - closestData.getSunLocation()) + glm::vec3 sunDirection = (getAvatarPosition() - closestData.getSunLocation()) / closestData.getAtmosphereOuterRadius(); float height = glm::distance(theCamera.getPosition(), closestData.getAtmosphereCenter()); if (height < closestData.getAtmosphereInnerRadius()) { @@ -3277,20 +3280,20 @@ void Application::displaySide(Camera& theCamera, bool selfAvatarOnly, bool billb alpha = 0.0f; if (sunDirection.y > -APPROXIMATE_DISTANCE_FROM_HORIZON) { - float directionY = glm::clamp(sunDirection.y, - -APPROXIMATE_DISTANCE_FROM_HORIZON, APPROXIMATE_DISTANCE_FROM_HORIZON) + float directionY = glm::clamp(sunDirection.y, + -APPROXIMATE_DISTANCE_FROM_HORIZON, APPROXIMATE_DISTANCE_FROM_HORIZON) + APPROXIMATE_DISTANCE_FROM_HORIZON; alpha = (directionY / DOUBLE_APPROXIMATE_DISTANCE_FROM_HORIZON); } - + } else if (height < closestData.getAtmosphereOuterRadius()) { alpha = (height - closestData.getAtmosphereInnerRadius()) / (closestData.getAtmosphereOuterRadius() - closestData.getAtmosphereInnerRadius()); if (sunDirection.y > -APPROXIMATE_DISTANCE_FROM_HORIZON) { - float directionY = glm::clamp(sunDirection.y, - -APPROXIMATE_DISTANCE_FROM_HORIZON, APPROXIMATE_DISTANCE_FROM_HORIZON) + float directionY = glm::clamp(sunDirection.y, + -APPROXIMATE_DISTANCE_FROM_HORIZON, APPROXIMATE_DISTANCE_FROM_HORIZON) + APPROXIMATE_DISTANCE_FROM_HORIZON; alpha = (directionY / DOUBLE_APPROXIMATE_DISTANCE_FROM_HORIZON); } @@ -3331,14 +3334,14 @@ void Application::displaySide(Camera& theCamera, bool selfAvatarOnly, bool billb glEnable(GL_LIGHTING); glEnable(GL_DEPTH_TEST); - + DependencyManager::get()->prepare(); if (!selfAvatarOnly) { // draw a red sphere float originSphereRadius = 0.05f; DependencyManager::get()->renderSphere(originSphereRadius, 15, 15, glm::vec4(1.0f, 0.0f, 0.0f, 1.0f)); - + // render models... if (DependencyManager::get()->shouldRenderEntities()) { PerformanceTimer perfTimer("entities"); @@ -3359,7 +3362,7 @@ void Application::displaySide(Camera& theCamera, bool selfAvatarOnly, bool billb renderMode = RenderArgs::MIRROR_RENDER_MODE; } _entities.render(renderMode, renderSide, renderDebugFlags); - + if (!Menu::getInstance()->isOptionChecked(MenuOption::Wireframe)) { // Restaure polygon mode glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); @@ -3380,13 +3383,13 @@ void Application::displaySide(Camera& theCamera, bool selfAvatarOnly, bool billb DependencyManager::get()->render(); } } - + bool mirrorMode = (theCamera.getMode() == CAMERA_MODE_MIRROR); - + { PerformanceTimer perfTimer("avatars"); DependencyManager::get()->renderAvatars(mirrorMode ? RenderArgs::MIRROR_RENDER_MODE : RenderArgs::NORMAL_RENDER_MODE, - false, selfAvatarOnly); + false, selfAvatarOnly); } if (!billboard) { @@ -3396,7 +3399,7 @@ void Application::displaySide(Camera& theCamera, bool selfAvatarOnly, bool billb DependencyManager::get()->setGlobalAtmosphere(skyStage->getAtmosphere()); DependencyManager::get()->setGlobalSkybox(skybox); - PROFILE_RANGE("DeferredLighting"); + PROFILE_RANGE("DeferredLighting"); PerformanceTimer perfTimer("lighting"); DependencyManager::get()->render(); } @@ -3404,9 +3407,9 @@ void Application::displaySide(Camera& theCamera, bool selfAvatarOnly, bool billb { PerformanceTimer perfTimer("avatarsPostLighting"); DependencyManager::get()->renderAvatars(mirrorMode ? RenderArgs::MIRROR_RENDER_MODE : RenderArgs::NORMAL_RENDER_MODE, - true, selfAvatarOnly); + true, selfAvatarOnly); } - + //Render the sixense lasers if (Menu::getInstance()->isOptionChecked(MenuOption::SixenseLasers)) { _myAvatar->renderLaserPointers(); @@ -3414,7 +3417,7 @@ void Application::displaySide(Camera& theCamera, bool selfAvatarOnly, bool billb if (!selfAvatarOnly) { _nodeBoundsDisplay.draw(); - + // Render the world box if (theCamera.getMode() != CAMERA_MODE_MIRROR && Menu::getInstance()->isOptionChecked(MenuOption::Stats)) { PerformanceTimer perfTimer("worldBox"); @@ -3491,9 +3494,9 @@ void Application::computeOffAxisFrustum(float& left, float& right, float& bottom _displayViewFrustum.computeOffAxisFrustum(left, right, bottom, top, nearVal, farVal, nearClipPlane, farClipPlane); if (OculusManager::isConnected()) { OculusManager::overrideOffAxisFrustum(left, right, bottom, top, nearVal, farVal, nearClipPlane, farClipPlane); - + } else if (TV3DManager::isConnected()) { - TV3DManager::overrideOffAxisFrustum(left, right, bottom, top, nearVal, farVal, nearClipPlane, farClipPlane); + TV3DManager::overrideOffAxisFrustum(left, right, bottom, top, nearVal, farVal, nearClipPlane, farClipPlane); } } @@ -3503,8 +3506,8 @@ bool Application::getShadowsEnabled() { menubar->isOptionChecked(MenuOption::CascadedShadows); } -bool Application::getCascadeShadowsEnabled() { - return Menu::getInstance()->isOptionChecked(MenuOption::CascadedShadows); +bool Application::getCascadeShadowsEnabled() { + return Menu::getInstance()->isOptionChecked(MenuOption::CascadedShadows); } glm::vec2 Application::getScaledScreenPoint(glm::vec2 projectedPoint) { @@ -3551,22 +3554,22 @@ void Application::renderRearViewMirror(const QRect& region, bool billboard) { _myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_BODY_DISTANCE * _myAvatar->getScale()); } else { // HEAD zoom level - // FIXME note that the positioing of the camera relative to the avatar can suffer limited - // precision as the user's position moves further away from the origin. Thus at - // /1e7,1e7,1e7 (well outside the buildable volume) the mirror camera veers and sways - // wildly as you rotate your avatar because the floating point values are becoming - // larger, squeezing out the available digits of precision you have available at the - // human scale for camera positioning. + // FIXME note that the positioing of the camera relative to the avatar can suffer limited + // precision as the user's position moves further away from the origin. Thus at + // /1e7,1e7,1e7 (well outside the buildable volume) the mirror camera veers and sways + // wildly as you rotate your avatar because the floating point values are becoming + // larger, squeezing out the available digits of precision you have available at the + // human scale for camera positioning. - // Previously there was a hack to correct this using the mechanism of repositioning - // the avatar at the origin of the world for the purposes of rendering the mirror, - // but it resulted in failing to render the avatar's head model in the mirror view - // when in first person mode. Presumably this was because of some missed culling logic - // that was not accounted for in the hack. + // Previously there was a hack to correct this using the mechanism of repositioning + // the avatar at the origin of the world for the purposes of rendering the mirror, + // but it resulted in failing to render the avatar's head model in the mirror view + // when in first person mode. Presumably this was because of some missed culling logic + // that was not accounted for in the hack. - // This was removed in commit 71e59cfa88c6563749594e25494102fe01db38e9 but could be further + // This was removed in commit 71e59cfa88c6563749594e25494102fe01db38e9 but could be further // investigated in order to adapt the technique while fixing the head rendering issue, - // but the complexity of the hack suggests that a better approach + // but the complexity of the hack suggests that a better approach _mirrorCamera.setPosition(_myAvatar->getHead()->getEyePosition() + _myAvatar->getOrientation() * glm::vec3(0.0f, 0.0f, -1.0f) * MIRROR_REARVIEW_DISTANCE * _myAvatar->getScale()); } @@ -3577,7 +3580,7 @@ void Application::renderRearViewMirror(const QRect& region, bool billboard) { if (billboard) { QSize size = DependencyManager::get()->getFrameBufferSize(); glViewport(region.x(), size.height() - region.y() - region.height(), region.width(), region.height()); - glScissor(region.x(), size.height() - region.y() - region.height(), region.width(), region.height()); + glScissor(region.x(), size.height() - region.y() - region.height(), region.width(), region.height()); } else { // if not rendering the billboard, the region is in device independent coordinates; must convert to device QSize size = DependencyManager::get()->getFrameBufferSize(); @@ -3618,7 +3621,7 @@ void Application::resetSensors() { QWindow* mainWindow = _window->windowHandle(); QPoint windowCenter = mainWindow->geometry().center(); _glWidget->cursor().setPos(currentScreen, windowCenter); - + _myAvatar->reset(); QMetaObject::invokeMethod(DependencyManager::get().data(), "reset", Qt::QueuedConnection); @@ -3651,11 +3654,11 @@ void Application::updateWindowTitle(){ QString connectionStatus = nodeList->getDomainHandler().isConnected() ? "" : " (NOT CONNECTED) "; QString username = AccountManager::getInstance().getAccountInfo().getUsername(); QString currentPlaceName = DependencyManager::get()->getHost(); - + if (currentPlaceName.isEmpty()) { currentPlaceName = nodeList->getDomainHandler().getHostname(); } - + QString title = QString() + (!username.isEmpty() ? username + " @ " : QString()) + currentPlaceName + connectionStatus + buildVersion; @@ -3701,7 +3704,7 @@ void Application::domainConnectionDenied(const QString& reason) { void Application::connectedToDomain(const QString& hostname) { AccountManager& accountManager = AccountManager::getInstance(); const QUuid& domainID = DependencyManager::get()->getDomainHandler().getUUID(); - + if (accountManager.isLoggedIn() && !domainID.isNull()) { _notifiedPacketVersionMismatchThisDomain = false; } @@ -3885,7 +3888,7 @@ void Application::saveScripts() { Settings settings; settings.beginWriteArray(SETTINGS_KEY); settings.remove(""); - + QStringList runningScripts = getRunningScripts(); int i = 0; for (auto it = runningScripts.begin(); it != runningScripts.end(); ++it) { @@ -3936,7 +3939,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("Overlays", &_overlays); qScriptRegisterMetaType(scriptEngine, OverlayPropertyResultToScriptValue, OverlayPropertyResultFromScriptValue); - qScriptRegisterMetaType(scriptEngine, RayToOverlayIntersectionResultToScriptValue, + qScriptRegisterMetaType(scriptEngine, RayToOverlayIntersectionResultToScriptValue, RayToOverlayIntersectionResultFromScriptValue); QScriptValue windowValue = scriptEngine->registerGlobalObject("Window", DependencyManager::get().data()); @@ -3947,7 +3950,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri LocationScriptingInterface::locationSetter); scriptEngine->registerFunction("WebWindow", WebWindowClass::constructor, 1); - + scriptEngine->registerGlobalObject("Menu", MenuScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("Settings", SettingsScriptingInterface::getInstance()); scriptEngine->registerGlobalObject("AudioDevice", AudioDeviceScriptingInterface::getInstance()); @@ -3959,7 +3962,7 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri qScriptRegisterMetaType(scriptEngine, DownloadInfoResultToScriptValue, DownloadInfoResultFromScriptValue); scriptEngine->registerGlobalObject("AvatarManager", DependencyManager::get().data()); - + scriptEngine->registerGlobalObject("Joysticks", &JoystickScriptingInterface::getInstance()); qScriptRegisterMetaType(scriptEngine, joystickToScriptValue, joystickFromScriptValue); @@ -3967,6 +3970,8 @@ void Application::registerScriptEngineWithApplicationServices(ScriptEngine* scri scriptEngine->registerGlobalObject("LODManager", DependencyManager::get().data()); + scriptEngine->registerGlobalObject("Paths", DependencyManager::get().data()); + QScriptValue hmdInterface = scriptEngine->registerGlobalObject("HMD", &HMDScriptingInterface::getInstance()); scriptEngine->registerFunction(hmdInterface, "getHUDLookAtPosition2D", HMDScriptingInterface::getHUDLookAtPosition2D, 0); scriptEngine->registerFunction(hmdInterface, "getHUDLookAtPosition3D", HMDScriptingInterface::getHUDLookAtPosition3D, 0); @@ -4014,7 +4019,7 @@ void Application::initializeAcceptedFiles() { bool Application::canAcceptURL(const QString& urlString) { initializeAcceptedFiles(); - + QUrl url(urlString); if (urlString.startsWith(HIFI_URL_SCHEME)) { return true; @@ -4032,7 +4037,7 @@ bool Application::canAcceptURL(const QString& urlString) { bool Application::acceptURL(const QString& urlString) { initializeAcceptedFiles(); - + if (urlString.startsWith(HIFI_URL_SCHEME)) { // this is a hifi URL - have the AddressManager handle it QMetaObject::invokeMethod(DependencyManager::get().data(), "handleLookupString", @@ -4070,19 +4075,19 @@ bool Application::askToSetAvatarUrl(const QString& url) { msgBox.exec(); return false; } - + // Download the FST file, to attempt to determine its model type QVariantHash fstMapping = FSTReader::downloadMapping(url); - + FSTReader::ModelType modelType = FSTReader::predictModelType(fstMapping); - + QMessageBox msgBox; msgBox.setIcon(QMessageBox::Question); msgBox.setWindowTitle("Set Avatar"); QPushButton* headButton = NULL; QPushButton* bodyButton = NULL; QPushButton* bodyAndHeadButton = NULL; - + QString modelName = fstMapping["name"].toString(); QString message; QString typeInfo; @@ -4101,7 +4106,7 @@ bool Application::askToSetAvatarUrl(const QString& url) { message = QString("Would you like to use '") + modelName + QString("' for your avatar?"); bodyAndHeadButton = msgBox.addButton(tr("Yes"), QMessageBox::ActionRole); break; - + default: message = QString("Would you like to use '") + modelName + QString("' for some part of your avatar head?"); headButton = msgBox.addButton(tr("Use for Head"), QMessageBox::ActionRole); @@ -4127,7 +4132,7 @@ bool Application::askToSetAvatarUrl(const QString& url) { } else { qCDebug(interfaceapp) << "Declined to use the avatar: " << url; } - + return true; } @@ -4152,7 +4157,7 @@ ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUser if (isAboutToQuit()) { return NULL; } - + QUrl scriptUrl(scriptFilename); const QString& scriptURLString = scriptUrl.toString(); if (_scriptEnginesHash.contains(scriptURLString) && loadScriptFromEditor @@ -4163,18 +4168,18 @@ ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUser ScriptEngine* scriptEngine = new ScriptEngine(NO_SCRIPT, "", &_controllerScriptingInterface); scriptEngine->setUserLoaded(isUserLoaded); - + if (scriptFilename.isNull()) { // this had better be the script editor (we should de-couple so somebody who thinks they are loading a script // doesn't just get an empty script engine) - + // we can complete setup now since there isn't a script we have to load registerScriptEngineWithApplicationServices(scriptEngine); } else { // connect to the appropriate signals of this script engine connect(scriptEngine, &ScriptEngine::scriptLoaded, this, &Application::handleScriptEngineLoaded); connect(scriptEngine, &ScriptEngine::errorLoadingScript, this, &Application::handleScriptLoadError); - + // get the script engine object to load the script at the designated script URL scriptEngine->loadURL(scriptUrl); } @@ -4189,11 +4194,11 @@ ScriptEngine* Application::loadScript(const QString& scriptFilename, bool isUser void Application::handleScriptEngineLoaded(const QString& scriptFilename) { ScriptEngine* scriptEngine = qobject_cast(sender()); - + _scriptEnginesHash.insertMulti(scriptFilename, scriptEngine); _runningScriptsWidget->setRunningScripts(getRunningScripts()); UserActivityLogger::getInstance().loadedScript(scriptFilename); - + // register our application services and set it off on its own thread registerScriptEngineWithApplicationServices(scriptEngine); } @@ -4303,7 +4308,7 @@ void Application::updateMyAvatarTransform() { glm::vec3 newOriginOffset = avatarPosition; int halfExtent = (int)HALF_SIMULATION_EXTENT; for (int i = 0; i < 3; ++i) { - newOriginOffset[i] = (float)(glm::max(halfExtent, + newOriginOffset[i] = (float)(glm::max(halfExtent, ((int)(avatarPosition[i] / SIMULATION_OFFSET_QUANTIZATION)) * (int)SIMULATION_OFFSET_QUANTIZATION)); } // TODO: Andrew to replace this with method that actually moves existing object positions in PhysicsEngine @@ -4317,23 +4322,23 @@ void Application::domainSettingsReceived(const QJsonObject& domainSettingsObject const QString PER_VOXEL_COST_KEY = "per-voxel-credits"; const QString PER_METER_CUBED_COST_KEY = "per-meter-cubed-credits"; const QString VOXEL_WALLET_UUID = "voxel-wallet"; - + const QJsonObject& voxelObject = domainSettingsObject[VOXEL_SETTINGS_KEY].toObject(); - + qint64 satoshisPerVoxel = 0; qint64 satoshisPerMeterCubed = 0; QUuid voxelWalletUUID; - + if (!domainSettingsObject.isEmpty()) { float perVoxelCredits = (float) voxelObject[PER_VOXEL_COST_KEY].toDouble(); float perMeterCubedCredits = (float) voxelObject[PER_METER_CUBED_COST_KEY].toDouble(); - + satoshisPerVoxel = (qint64) floorf(perVoxelCredits * SATOSHIS_PER_CREDIT); satoshisPerMeterCubed = (qint64) floorf(perMeterCubedCredits * SATOSHIS_PER_CREDIT); - + voxelWalletUUID = QUuid(voxelObject[VOXEL_WALLET_UUID].toString()); } - + qCDebug(interfaceapp) << "Octree edits costs are" << satoshisPerVoxel << "per octree cell and" << satoshisPerMeterCubed << "per meter cubed"; qCDebug(interfaceapp) << "Destination wallet UUID for edit payments is" << voxelWalletUUID; } @@ -4536,7 +4541,7 @@ bool Application::isVSyncOn() const { } else { return true; } - */ + */ #endif return true; } @@ -4634,14 +4639,14 @@ void Application::notifyPacketVersionMismatch() { void Application::checkSkeleton() { if (_myAvatar->getSkeletonModel().isActive() && !_myAvatar->getSkeletonModel().hasSkeleton()) { qCDebug(interfaceapp) << "MyAvatar model has no skeleton"; - + QString message = "Your selected avatar body has no skeleton.\n\nThe default body will be loaded..."; QMessageBox msgBox; msgBox.setText(message); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.setIcon(QMessageBox::Warning); msgBox.exec(); - + _myAvatar->useBodyURL(DEFAULT_BODY_MODEL_URL, "Default"); } else { _physicsEngine.setCharacterController(_myAvatar->getCharacterController()); @@ -4654,7 +4659,7 @@ void Application::showFriendsWindow() { const int FRIENDS_WINDOW_WIDTH = 290; const int FRIENDS_WINDOW_HEIGHT = 500; if (!_friendsWindow) { - _friendsWindow = new WebWindowClass(FRIENDS_WINDOW_TITLE, FRIENDS_WINDOW_URL, FRIENDS_WINDOW_WIDTH, + _friendsWindow = new WebWindowClass(FRIENDS_WINDOW_TITLE, FRIENDS_WINDOW_URL, FRIENDS_WINDOW_WIDTH, FRIENDS_WINDOW_HEIGHT, false); connect(_friendsWindow, &WebWindowClass::closed, this, &Application::friendsWindowClosed); } @@ -4695,16 +4700,16 @@ QSize Application::getDeviceSize() const { return _glWidget->getDeviceSize(); } -int Application::getTrueMouseX() const { - return _glWidget->mapFromGlobal(QCursor::pos()).x(); +int Application::getTrueMouseX() const { + return _glWidget->mapFromGlobal(QCursor::pos()).x(); } -int Application::getTrueMouseY() const { - return _glWidget->mapFromGlobal(QCursor::pos()).y(); +int Application::getTrueMouseY() const { + return _glWidget->mapFromGlobal(QCursor::pos()).y(); } -bool Application::isThrottleRendering() const { - return _glWidget->isThrottleRendering(); +bool Application::isThrottleRendering() const { + return _glWidget->isThrottleRendering(); } PickRay Application::computePickRay() const { diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 41c8cb9537..340ca9374d 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -153,6 +153,7 @@ void AudioClient::reset() { } void AudioClient::audioMixerKilled() { + _hasReceivedFirstPacket = false; _outgoingAvatarAudioSequenceNumber = 0; _stats.reset(); } @@ -481,6 +482,7 @@ void AudioClient::start() { qCDebug(audioclient) << "Unable to set up audio input because of a problem with input format."; qCDebug(audioclient) << "The closest format available is" << inputDeviceInfo.nearestFormat(_desiredInputFormat); } + if (!outputFormatSupported) { qCDebug(audioclient) << "Unable to set up audio output because of a problem with output format."; qCDebug(audioclient) << "The closest format available is" << outputDeviceInfo.nearestFormat(_desiredOutputFormat); @@ -489,6 +491,7 @@ void AudioClient::start() { if (_audioInput) { _inputFrameBuffer.initialize( _inputFormat.channelCount(), _audioInput->bufferSize() * 8 ); } + _inputGain.initialize(); _sourceGain.initialize(); _noiseSource.initialize(); @@ -926,6 +929,14 @@ void AudioClient::addReceivedAudioToStream(const QByteArray& audioByteArray) { DependencyManager::get()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveFirstAudioPacket); if (_audioOutput) { + + if (!_hasReceivedFirstPacket) { + _hasReceivedFirstPacket = true; + + // have the audio scripting interface emit a signal to say we just connected to mixer + emit receivedFirstPacket(); + } + // Audio output must exist and be correctly set up if we're going to process received audio _receivedAudioStream.parseData(audioByteArray); } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index f9392c6a10..3b2c1c1ae6 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -37,6 +37,7 @@ #include #include #include +#include #include #include "AudioIOStats.h" @@ -57,7 +58,7 @@ static const int NUM_AUDIO_CHANNELS = 2; static const int DEFAULT_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 3; static const int MIN_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 1; static const int MAX_AUDIO_OUTPUT_BUFFER_SIZE_FRAMES = 20; -#if defined(Q_OS_ANDROID) || defined(Q_OS_WIN) +#if defined(Q_OS_ANDROID) || defined(Q_OS_WIN) static const int DEFAULT_AUDIO_OUTPUT_STARVE_DETECTION_ENABLED = false; #else static const int DEFAULT_AUDIO_OUTPUT_STARVE_DETECTION_ENABLED = true; @@ -88,14 +89,14 @@ public: void stop() { close(); } qint64 readData(char * data, qint64 maxSize); qint64 writeData(const char * data, qint64 maxSize) { return 0; } - + int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; } private: MixedProcessedAudioStream& _receivedAudioStream; AudioClient* _audio; int _unfulfilledReads; }; - + const MixedProcessedAudioStream& getReceivedAudioStream() const { return _receivedAudioStream; } MixedProcessedAudioStream& getReceivedAudioStream() { return _receivedAudioStream; } @@ -105,30 +106,30 @@ public: float getAudioAverageInputLoudness() const { return _lastInputLoudness; } int getDesiredJitterBufferFrames() const { return _receivedAudioStream.getDesiredJitterBufferFrames(); } - + bool isMuted() { return _muted; } - + const AudioIOStats& getStats() const { return _stats; } float getInputRingBufferMsecsAvailable() const; float getAudioOutputMsecsUnplayed() const; int getOutputBufferSize() { return _outputBufferSizeFrames.get(); } - + bool getOutputStarveDetectionEnabled() { return _outputStarveDetectionEnabled.get(); } void setOutputStarveDetectionEnabled(bool enabled) { _outputStarveDetectionEnabled.set(enabled); } int getOutputStarveDetectionPeriod() { return _outputStarveDetectionPeriodMsec.get(); } void setOutputStarveDetectionPeriod(int msecs) { _outputStarveDetectionPeriodMsec.set(msecs); } - + int getOutputStarveDetectionThreshold() { return _outputStarveDetectionThreshold.get(); } void setOutputStarveDetectionThreshold(int threshold) { _outputStarveDetectionThreshold.set(threshold); } - + void setPositionGetter(AudioPositionGetter positionGetter) { _positionGetter = positionGetter; } void setOrientationGetter(AudioOrientationGetter orientationGetter) { _orientationGetter = orientationGetter; } static const float CALLBACK_ACCELERATOR_RATIO; - + public slots: void start(); void stop(); @@ -140,7 +141,7 @@ public slots: void reset(); void audioMixerKilled(); void toggleMute(); - + virtual void enableAudioSourceInject(bool enable); virtual void selectAudioSourcePinkNoise(); virtual void selectAudioSourceSine440(); @@ -148,10 +149,10 @@ public slots: virtual void setIsStereoInput(bool stereo); void toggleAudioNoiseReduction() { _isNoiseGateEnabled = !_isNoiseGateEnabled; } - + void toggleLocalEcho() { _shouldEchoLocally = !_shouldEchoLocally; } void toggleServerEcho() { _shouldEchoToServer = !_shouldEchoToServer; } - + void processReceivedSamples(const QByteArray& inputBuffer, QByteArray& outputBuffer); void sendMuteEnvironmentPacket(); @@ -172,10 +173,10 @@ public slots: void setReverbOptions(const AudioEffectOptions* options); void outputNotify(); - + void loadSettings(); void saveSettings(); - + signals: bool muteToggled(); void inputReceived(const QByteArray& inputSamples); @@ -184,14 +185,16 @@ signals: void deviceChanged(); + void receivedFirstPacket(); + protected: AudioClient(); ~AudioClient(); - + virtual void customDeleter() { deleteLater(); } - + private: void outputFormatChanged(); @@ -216,35 +219,35 @@ private: QString _inputAudioDeviceName; QString _outputAudioDeviceName; - + quint64 _outputStarveDetectionStartTimeMsec; int _outputStarveDetectionCount; - + Setting::Handle _outputBufferSizeFrames; Setting::Handle _outputStarveDetectionEnabled; Setting::Handle _outputStarveDetectionPeriodMsec; // Maximum number of starves per _outputStarveDetectionPeriod before increasing buffer size Setting::Handle _outputStarveDetectionThreshold; - + StDev _stdev; QElapsedTimer _timeSinceLastReceived; float _averagedLatency; float _lastInputLoudness; float _timeSinceLastClip; int _totalInputAudioSamples; - + bool _muted; bool _shouldEchoLocally; bool _shouldEchoToServer; bool _isNoiseGateEnabled; bool _audioSourceInjectEnabled; - + bool _reverb; AudioEffectOptions _scriptReverbOptions; AudioEffectOptions _zoneReverbOptions; AudioEffectOptions* _reverbOptions; ty_gverb* _gverb; - + // possible soxr streams needed for resample soxr* _inputToNetworkResampler; soxr* _networkToOutputResampler; @@ -268,17 +271,17 @@ private: // Input framebuffer AudioBufferFloat32 _inputFrameBuffer; - + // Input gain AudioGain _inputGain; - + // Post tone/pink noise generator gain AudioGain _sourceGain; // Pink noise source bool _noiseSourceEnabled; AudioSourcePinkNoise _noiseSource; - + // Tone source bool _toneSourceEnabled; AudioSourceTone _toneSource; @@ -286,17 +289,19 @@ private: quint16 _outgoingAvatarAudioSequenceNumber; AudioOutputIODevice _audioOutputIODevice; - + AudioIOStats _stats; - + AudioNoiseGate _inputGate; - + AudioPositionGetter _positionGetter; AudioOrientationGetter _orientationGetter; - + QVector _inputDevices; QVector _outputDevices; void checkDevices(); + + bool _hasReceivedFirstPacket = false; }; diff --git a/libraries/audio/src/AudioInjector.cpp b/libraries/audio/src/AudioInjector.cpp index ae397ba97e..5456624b68 100644 --- a/libraries/audio/src/AudioInjector.cpp +++ b/libraries/audio/src/AudioInjector.cpp @@ -26,7 +26,7 @@ AudioInjector::AudioInjector(QObject* parent) : QObject(parent) { - + } AudioInjector::AudioInjector(Sound* sound, const AudioInjectorOptions& injectorOptions) : @@ -39,24 +39,24 @@ AudioInjector::AudioInjector(const QByteArray& audioData, const AudioInjectorOpt _audioData(audioData), _options(injectorOptions) { - + } void AudioInjector::setIsFinished(bool isFinished) { _isFinished = isFinished; - + if (_isFinished) { emit finished(); - + if (_localBuffer) { _localBuffer->stop(); _localBuffer->deleteLater(); _localBuffer = NULL; } - + _isStarted = false; _shouldStop = false; - + if (_shouldDeleteAfterFinish) { // we've been asked to delete after finishing, trigger a queued deleteLater here qCDebug(audio) << "AudioInjector triggering delete from setIsFinished"; @@ -69,16 +69,16 @@ void AudioInjector::injectAudio() { if (!_isStarted) { // check if we need to offset the sound by some number of seconds if (_options.secondOffset > 0.0f) { - + // convert the offset into a number of bytes int byteOffset = (int) floorf(AudioConstants::SAMPLE_RATE * _options.secondOffset * (_options.stereo ? 2.0f : 1.0f)); byteOffset *= sizeof(int16_t); - + _currentSendPosition = byteOffset; } else { _currentSendPosition = 0; } - + if (_options.localOnly) { injectLocally(); } else { @@ -86,7 +86,7 @@ void AudioInjector::injectAudio() { } } else { qCDebug(audio) << "AudioInjector::injectAudio called but already started."; - } + } } void AudioInjector::restart() { @@ -100,37 +100,37 @@ void AudioInjector::injectLocally() { bool success = false; if (_localAudioInterface) { if (_audioData.size() > 0) { - + _localBuffer = new AudioInjectorLocalBuffer(_audioData, this); - + _localBuffer->open(QIODevice::ReadOnly); _localBuffer->setShouldLoop(_options.loop); _localBuffer->setVolume(_options.volume); - + // give our current send position to the local buffer _localBuffer->setCurrentOffset(_currentSendPosition); - + success = _localAudioInterface->outputLocalInjector(_options.stereo, this); - + // if we're not looping and the buffer tells us it is empty then emit finished connect(_localBuffer, &AudioInjectorLocalBuffer::bufferEmpty, this, &AudioInjector::stop); - + if (!success) { qCDebug(audio) << "AudioInjector::injectLocally could not output locally via _localAudioInterface"; } } else { qCDebug(audio) << "AudioInjector::injectLocally called without any data in Sound QByteArray"; } - + } else { qCDebug(audio) << "AudioInjector::injectLocally cannot inject locally with no local audio interface present."; } - + if (!success) { // we never started so we are finished, call our stop method stop(); } - + } const uchar MAX_INJECTOR_VOLUME = 0xFF; @@ -140,65 +140,65 @@ void AudioInjector::injectToMixer() { _currentSendPosition >= _audioData.size()) { _currentSendPosition = 0; } - + auto nodeList = DependencyManager::get(); - + // make sure we actually have samples downloaded to inject if (_audioData.size()) { - + // setup the packet for injected audio QByteArray injectAudioPacket = nodeList->byteArrayWithPopulatedHeader(PacketTypeInjectAudio); QDataStream packetStream(&injectAudioPacket, QIODevice::Append); - + // pack some placeholder sequence number for now int numPreSequenceNumberBytes = injectAudioPacket.size(); packetStream << (quint16)0; - + // pack stream identifier (a generated UUID) packetStream << QUuid::createUuid(); - + // pack the stereo/mono type of the stream packetStream << _options.stereo; - + // pack the flag for loopback uchar loopbackFlag = (uchar) true; packetStream << loopbackFlag; - + // pack the position for injected audio int positionOptionOffset = injectAudioPacket.size(); packetStream.writeRawData(reinterpret_cast(&_options.position), sizeof(_options.position)); - + // pack our orientation for injected audio int orientationOptionOffset = injectAudioPacket.size(); packetStream.writeRawData(reinterpret_cast(&_options.orientation), sizeof(_options.orientation)); - + // pack zero for radius float radius = 0; packetStream << radius; - + // pack 255 for attenuation byte int volumeOptionOffset = injectAudioPacket.size(); quint8 volume = MAX_INJECTOR_VOLUME * _options.volume; packetStream << volume; - + packetStream << _options.ignorePenumbra; - + QElapsedTimer timer; timer.start(); int nextFrame = 0; - + int numPreAudioDataBytes = injectAudioPacket.size(); bool shouldLoop = _options.loop; - + // loop to send off our audio in NETWORK_BUFFER_LENGTH_SAMPLES_PER_CHANNEL byte chunks quint16 outgoingInjectedAudioSequenceNumber = 0; while (_currentSendPosition < _audioData.size() && !_shouldStop) { - + int bytesToCopy = std::min(((_options.stereo) ? 2 : 1) * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL, _audioData.size() - _currentSendPosition); - + // Measure the loudness of this frame _loudness = 0.0f; for (int i = 0; i < bytesToCopy; i += sizeof(int16_t)) { @@ -215,45 +215,45 @@ void AudioInjector::injectToMixer() { sizeof(_options.orientation)); volume = MAX_INJECTOR_VOLUME * _options.volume; memcpy(injectAudioPacket.data() + volumeOptionOffset, &volume, sizeof(volume)); - + // resize the QByteArray to the right size injectAudioPacket.resize(numPreAudioDataBytes + bytesToCopy); // pack the sequence number memcpy(injectAudioPacket.data() + numPreSequenceNumberBytes, &outgoingInjectedAudioSequenceNumber, sizeof(quint16)); - + // copy the next NETWORK_BUFFER_LENGTH_BYTES_PER_CHANNEL bytes to the packet memcpy(injectAudioPacket.data() + numPreAudioDataBytes, _audioData.data() + _currentSendPosition, bytesToCopy); - + // grab our audio mixer from the NodeList, if it exists SharedNodePointer audioMixer = nodeList->soloNodeOfType(NodeType::AudioMixer); - + // send off this audio packet nodeList->writeDatagram(injectAudioPacket, audioMixer); outgoingInjectedAudioSequenceNumber++; - + _currentSendPosition += bytesToCopy; - + // send two packets before the first sleep so the mixer can start playback right away - + if (_currentSendPosition != bytesToCopy && _currentSendPosition < _audioData.size()) { - + // process events in case we have been told to stop and be deleted QCoreApplication::processEvents(); - + if (_shouldStop) { break; } - + // not the first packet and not done // sleep for the appropriate time int usecToSleep = (++nextFrame * AudioConstants::NETWORK_FRAME_USECS) - timer.nsecsElapsed() / 1000; - + if (usecToSleep > 0) { usleep(usecToSleep); - } + } } if (shouldLoop && _currentSendPosition >= _audioData.size()) { @@ -261,13 +261,13 @@ void AudioInjector::injectToMixer() { } } } - + setIsFinished(true); } void AudioInjector::stop() { _shouldStop = true; - + if (_options.localOnly) { // we're only a local injector, so we can say we are finished right away too setIsFinished(true); diff --git a/libraries/audio/src/Sound.cpp b/libraries/audio/src/Sound.cpp index 577328ee18..7dc6010f8f 100644 --- a/libraries/audio/src/Sound.cpp +++ b/libraries/audio/src/Sound.cpp @@ -53,21 +53,25 @@ Sound::Sound(const QUrl& url, bool isStereo) : _isStereo(isStereo), _isReady(false) { - + } void Sound::downloadFinished(QNetworkReply* reply) { // replace our byte array with the downloaded data QByteArray rawAudioByteArray = reply->readAll(); + QString fileName = reply->url().fileName(); - if (reply->hasRawHeader("Content-Type")) { + const QString WAV_EXTENSION = ".wav"; + + if (reply->hasRawHeader("Content-Type") || fileName.endsWith(WAV_EXTENSION)) { QByteArray headerContentType = reply->rawHeader("Content-Type"); // WAV audio file encountered if (headerContentType == "audio/x-wav" || headerContentType == "audio/wav" - || headerContentType == "audio/wave") { + || headerContentType == "audio/wave" + || fileName.endsWith(WAV_EXTENSION)) { QByteArray outputAudioByteArray; @@ -80,7 +84,7 @@ void Sound::downloadFinished(QNetworkReply* reply) { _isStereo = true; qCDebug(audio) << "Processing sound of" << rawAudioByteArray.size() << "bytes from" << reply->url() << "as stereo audio file."; } - + // Process as RAW file downSample(rawAudioByteArray); } @@ -88,7 +92,7 @@ void Sound::downloadFinished(QNetworkReply* reply) { } else { qCDebug(audio) << "Network reply without 'Content-Type'."; } - + _isReady = true; reply->deleteLater(); } @@ -99,16 +103,16 @@ void Sound::downSample(const QByteArray& rawAudioByteArray) { // we want to convert it to the format that the audio-mixer wants // which is signed, 16-bit, 24Khz - + int numSourceSamples = rawAudioByteArray.size() / sizeof(AudioConstants::AudioSample); - + int numDestinationBytes = rawAudioByteArray.size() / sizeof(AudioConstants::AudioSample); if (_isStereo && numSourceSamples % 2 != 0) { numDestinationBytes += sizeof(AudioConstants::AudioSample); } - + _byteArray.resize(numDestinationBytes); - + int16_t* sourceSamples = (int16_t*) rawAudioByteArray.data(); int16_t* destinationSamples = (int16_t*) _byteArray.data(); @@ -134,22 +138,22 @@ void Sound::downSample(const QByteArray& rawAudioByteArray) { } void Sound::trimFrames() { - + const uint32_t inputFrameCount = _byteArray.size() / sizeof(int16_t); const uint32_t trimCount = 1024; // number of leading and trailing frames to trim - + if (inputFrameCount <= (2 * trimCount)) { return; } - + int16_t* inputFrameData = (int16_t*)_byteArray.data(); AudioEditBufferFloat32 editBuffer(1, inputFrameCount); editBuffer.copyFrames(1, inputFrameCount, inputFrameData, false /*copy in*/); - + editBuffer.linearFade(0, trimCount, true); editBuffer.linearFade(inputFrameCount - trimCount, inputFrameCount, false); - + editBuffer.copyFrames(1, inputFrameCount, inputFrameData, true /*copy out*/); } @@ -238,7 +242,7 @@ void Sound::interpretAsWav(const QByteArray& inputAudioByteArray, QByteArray& ou } else if (qFromLittleEndian(fileHeader.wave.numChannels) > 2) { qCDebug(audio) << "Currently not support audio files with more than 2 channels."; } - + if (qFromLittleEndian(fileHeader.wave.bitsPerSample) != 16) { qCDebug(audio) << "Currently not supporting non 16bit audio files."; return; diff --git a/libraries/script-engine/src/AudioScriptingInterface.cpp b/libraries/script-engine/src/AudioScriptingInterface.cpp index e210ee6f6e..9e3e924933 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.cpp +++ b/libraries/script-engine/src/AudioScriptingInterface.cpp @@ -28,19 +28,19 @@ AudioScriptingInterface& AudioScriptingInterface::getInstance() { AudioScriptingInterface::AudioScriptingInterface() : _localAudioInterface(NULL) { - + } ScriptAudioInjector* AudioScriptingInterface::playSound(Sound* sound, const AudioInjectorOptions& injectorOptions) { if (QThread::currentThread() != thread()) { ScriptAudioInjector* injector = NULL; - + QMetaObject::invokeMethod(this, "playSound", Qt::BlockingQueuedConnection, Q_RETURN_ARG(ScriptAudioInjector*, injector), Q_ARG(Sound*, sound), Q_ARG(const AudioInjectorOptions&, injectorOptions)); return injector; } - + if (sound) { // stereo option isn't set from script, this comes from sound metadata or filename AudioInjectorOptions optionsCopy = injectorOptions; diff --git a/libraries/script-engine/src/AudioScriptingInterface.h b/libraries/script-engine/src/AudioScriptingInterface.h index bbc9a57db8..d74e1ed1e0 100644 --- a/libraries/script-engine/src/AudioScriptingInterface.h +++ b/libraries/script-engine/src/AudioScriptingInterface.h @@ -22,22 +22,23 @@ class AudioScriptingInterface : public QObject { Q_OBJECT public: static AudioScriptingInterface& getInstance(); - + void setLocalAudioInterface(AbstractAudioInterface* audioInterface) { _localAudioInterface = audioInterface; } - + protected: // this method is protected to stop C++ callers from calling, but invokable from script Q_INVOKABLE ScriptAudioInjector* playSound(Sound* sound, const AudioInjectorOptions& injectorOptions = AudioInjectorOptions()); - + Q_INVOKABLE void injectGeneratedNoise(bool inject); Q_INVOKABLE void selectPinkNoise(); Q_INVOKABLE void selectSine440(); Q_INVOKABLE void setStereoInput(bool stereo); - + signals: void mutedByMixer(); void environmentMuted(); + void receivedFirstPacket(); private: AudioScriptingInterface(); diff --git a/libraries/shared/src/PathUtils.cpp b/libraries/shared/src/PathUtils.cpp index 84c8ae4939..79e83e9b40 100644 --- a/libraries/shared/src/PathUtils.cpp +++ b/libraries/shared/src/PathUtils.cpp @@ -18,7 +18,7 @@ #include "PathUtils.h" -QString& PathUtils::resourcesPath() { +const QString& PathUtils::resourcesPath() { #ifdef Q_OS_MAC static QString staticResourcePath = QCoreApplication::applicationDirPath() + "/../Resources/"; #else diff --git a/libraries/shared/src/PathUtils.h b/libraries/shared/src/PathUtils.h index 6b6893574b..12b1d57641 100644 --- a/libraries/shared/src/PathUtils.h +++ b/libraries/shared/src/PathUtils.h @@ -12,12 +12,17 @@ #ifndef hifi_PathUtils_h #define hifi_PathUtils_h +#include -#include +#include "DependencyManager.h" -namespace PathUtils { - QString& resourcesPath(); -} +class PathUtils : public QObject, public Dependency { + Q_OBJECT + SINGLETON_DEPENDENCY + Q_PROPERTY(QString resources READ resourcesPath) +public: + static const QString& resourcesPath(); +}; QString fileNameWithoutExtension(const QString& fileName, const QVector possibleExtensions); QString findMostRecentFileExtension(const QString& originalFileName, QVector possibleExtensions);