diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index 58b1df08c1..20d2711743 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -372,6 +372,13 @@ "help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.", "value-hidden": true }, + { + "name": "verify_http_password", + "label": "Verify HTTP Password", + "type": "password", + "help": "Must match the password entered above for change to be saved.", + "value-hidden": true + }, { "name": "maximum_user_capacity", "label": "Maximum User Capacity", diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index c31d6e2dfc..659372267c 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -904,10 +904,18 @@ function saveSettings() { var formJSON = form2js('settings-form', ".", false, cleanupFormValues, true); // check if we've set the basic http password - if so convert it to base64 + var canPost = true; if (formJSON["security"]) { var password = formJSON["security"]["http_password"]; + var verify_password = formJSON["security"]["verify_http_password"]; if (password && password.length > 0) { - formJSON["security"]["http_password"] = sha256_digest(password); + if (password != verify_password) { + bootbox.alert({"message": "Passwords must match!", "title":"Password Error"}); + canPost = false; + } else { + formJSON["security"]["http_password"] = sha256_digest(password); + delete formJSON["security"]["verify_http_password"]; + } } } @@ -923,7 +931,9 @@ function saveSettings() { $(this).blur(); // POST the form JSON to the domain-server settings.json endpoint so the settings are saved - postSettings(formJSON); + if (canPost) { + postSettings(formJSON); + } } $('body').on('click', '.save-button', function(e){ diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index c96a63ce1c..0217136a0c 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -4379,6 +4379,10 @@ void Application::update(float deltaTime) { PROFILE_RANGE_EX(simulation_physics, "HarvestChanges", 0xffffff00, (uint64_t)getActiveDisplayPlugin()->presentCount()); PerformanceTimer perfTimer("harvestChanges"); if (_physicsEngine->hasOutgoingChanges()) { + // grab the collision events BEFORE handleOutgoingChanges() because at this point + // we have a better idea of which objects we own or should own. + auto& collisionEvents = _physicsEngine->getCollisionEvents(); + getEntities()->getTree()->withWriteLock([&] { PerformanceTimer perfTimer("handleOutgoingChanges"); const VectorOfMotionStates& outgoingChanges = _physicsEngine->getOutgoingChanges(); @@ -4386,11 +4390,10 @@ void Application::update(float deltaTime) { avatarManager->handleOutgoingChanges(outgoingChanges); }); - auto collisionEvents = _physicsEngine->getCollisionEvents(); - avatarManager->handleCollisionEvents(collisionEvents); - if (!_aboutToQuit) { + // handleCollisionEvents() AFTER handleOutgoinChanges() PerformanceTimer perfTimer("entities"); + avatarManager->handleCollisionEvents(collisionEvents); // Collision events (and their scripts) must not be handled when we're locked, above. (That would risk // deadlock.) _entitySimulation->handleCollisionEvents(collisionEvents); diff --git a/libraries/audio-client/src/AudioClient.cpp b/libraries/audio-client/src/AudioClient.cpp index 1e3dc11338..2e532d67bf 100644 --- a/libraries/audio-client/src/AudioClient.cpp +++ b/libraries/audio-client/src/AudioClient.cpp @@ -112,6 +112,42 @@ private: bool _quit { false }; }; +void AudioInjectorsThread::prepare() { + _audio->prepareLocalAudioInjectors(); +} + +static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 2 + N samples + *dest++ = left; + *dest++ = right; + for (int n = 0; n < numExtraChannels; n++) { + *dest++ = 0; + } + } +} + +static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { + for (int i = 0; i < numSamples/2; i++) { + + // read 2 samples + int16_t left = *source++; + int16_t right = *source++; + + // write 1 sample + *dest++ = (int16_t)((left + right) / 2); + } +} + +static inline float convertToFloat(int16_t sample) { + return (float)sample * (1 / 32768.0f); +} + AudioClient::AudioClient() : AbstractAudioInterface(), _gate(this), @@ -127,6 +163,7 @@ AudioClient::AudioClient() : _loopbackAudioOutput(NULL), _loopbackOutputDevice(NULL), _inputRingBuffer(0), + _localInjectorsStream(0), _receivedAudioStream(RECEIVED_AUDIO_STREAM_CAPACITY_FRAMES), _isStereoInput(false), _outputStarveDetectionStartTimeMsec(0), @@ -144,13 +181,18 @@ AudioClient::AudioClient() : _reverbOptions(&_scriptReverbOptions), _inputToNetworkResampler(NULL), _networkToOutputResampler(NULL), + _localToOutputResampler(NULL), + _localAudioThread(this), _audioLimiter(AudioConstants::SAMPLE_RATE, OUTPUT_CHANNEL_COUNT), _outgoingAvatarAudioSequenceNumber(0), - _audioOutputIODevice(_receivedAudioStream, this), + _audioOutputIODevice(_localInjectorsStream, _receivedAudioStream, this), _stats(&_receivedAudioStream), _inputGate(), _positionGetter(DEFAULT_POSITION_GETTER), _orientationGetter(DEFAULT_ORIENTATION_GETTER) { + // avoid putting a lock in the device callback + assert(_localSamplesAvailable.is_lock_free()); + // deprecate legacy settings { Setting::Handle::Deprecated("maxFramesOverDesired", InboundAudioStream::MAX_FRAMES_OVER_DESIRED); @@ -176,6 +218,10 @@ AudioClient::AudioClient() : _checkDevicesThread->setPriority(QThread::LowPriority); _checkDevicesThread->start(); + // start a thread to process local injectors + _localAudioThread.setObjectName("LocalAudio Thread"); + _localAudioThread.start(); + configureReverb(); auto& packetReceiver = DependencyManager::get()->getPacketReceiver(); @@ -213,6 +259,7 @@ void AudioClient::reset() { _stats.reset(); _sourceReverb.reset(); _listenerReverb.reset(); + _localReverb.reset(); } void AudioClient::audioMixerKilled() { @@ -365,7 +412,7 @@ QAudioDeviceInfo defaultAudioDeviceForMode(QAudio::Mode mode) { CoUninitialize(); } - qCDebug(audioclient) << "DEBUG [" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << deviceName << "] [" << getNamedAudioDeviceForMode(mode, deviceName).deviceName() << "]"; return getNamedAudioDeviceForMode(mode, deviceName); #endif @@ -387,12 +434,12 @@ bool nativeFormatForAudioDevice(const QAudioDeviceInfo& audioDevice, audioFormat.setByteOrder(QAudioFormat::LittleEndian); if (!audioDevice.isFormatSupported(audioFormat)) { - qCDebug(audioclient) << "WARNING: The native format is" << audioFormat << "but isFormatSupported() failed."; + qCWarning(audioclient) << "The native format is" << audioFormat << "but isFormatSupported() failed."; return false; } // converting to/from this rate must produce an integral number of samples if (audioFormat.sampleRate() * AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL % AudioConstants::SAMPLE_RATE != 0) { - qCDebug(audioclient) << "WARNING: The native sample rate [" << audioFormat.sampleRate() << "] is not supported."; + qCWarning(audioclient) << "The native sample rate [" << audioFormat.sampleRate() << "] is not supported."; return false; } return true; @@ -726,12 +773,12 @@ QVector AudioClient::getDeviceNames(QAudio::Mode mode) { } bool AudioClient::switchInputToAudioDevice(const QString& inputDeviceName) { - qCDebug(audioclient) << "DEBUG [" << inputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << inputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName).deviceName() << "]"; return switchInputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioInput, inputDeviceName)); } bool AudioClient::switchOutputToAudioDevice(const QString& outputDeviceName) { - qCDebug(audioclient) << "DEBUG [" << outputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName).deviceName() << "]"; + qCDebug(audioclient) << "[" << outputDeviceName << "] [" << getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName).deviceName() << "]"; return switchOutputToAudioDevice(getNamedAudioDeviceForMode(QAudio::AudioOutput, outputDeviceName)); } @@ -762,6 +809,7 @@ void AudioClient::configureReverb() { p.wetDryMix = _reverbOptions->getWetDryMix(); _listenerReverb.setParameters(&p); + _localReverb.setParameters(&p); // used only for adding self-reverb to loopback audio p.sampleRate = _outputFormat.sampleRate(); @@ -808,6 +856,7 @@ void AudioClient::setReverb(bool reverb) { if (!_reverb) { _sourceReverb.reset(); _listenerReverb.reset(); + _localReverb.reset(); } } @@ -841,36 +890,6 @@ void AudioClient::setReverbOptions(const AudioEffectOptions* options) { } } -static void channelUpmix(int16_t* source, int16_t* dest, int numSamples, int numExtraChannels) { - - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *source++; - int16_t right = *source++; - - // write 2 + N samples - *dest++ = left; - *dest++ = right; - for (int n = 0; n < numExtraChannels; n++) { - *dest++ = 0; - } - } -} - -static void channelDownmix(int16_t* source, int16_t* dest, int numSamples) { - - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *source++; - int16_t right = *source++; - - // write 1 sample - *dest++ = (int16_t)((left + right) / 2); - } -} - void AudioClient::handleLocalEchoAndReverb(QByteArray& inputByteArray) { // If there is server echo, reverb will be applied to the recieved audio stream so no need to have it here. bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); @@ -1082,14 +1101,78 @@ void AudioClient::handleRecordedAudioInput(const QByteArray& audio) { PacketType::MicrophoneAudioWithEcho, _selectedCodecName); } -void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { +void AudioClient::prepareLocalAudioInjectors() { + if (_outputPeriod == 0) { + return; + } + + int bufferCapacity = _localInjectorsStream.getSampleCapacity(); + if (_localToOutputResampler) { + // avoid overwriting the buffer, + // instead of failing on writes because the buffer is used as a lock-free pipe + bufferCapacity -= + _localToOutputResampler->getMaxOutput(AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL) * + AudioConstants::STEREO; + bufferCapacity += 1; + } + + int samplesNeeded = std::numeric_limits::max(); + while (samplesNeeded > 0) { + // lock for every write to avoid locking out the device callback + // this lock is intentional - the buffer is only lock-free in its use in the device callback + Lock lock(_localAudioMutex); + + samplesNeeded = bufferCapacity - _localSamplesAvailable.load(std::memory_order_relaxed); + if (samplesNeeded <= 0) { + break; + } + + // get a network frame of local injectors' audio + if (!mixLocalAudioInjectors(_localMixBuffer)) { + break; + } + + // reverb + if (_reverb) { + _localReverb.render(_localMixBuffer, _localMixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } + + int samples; + if (_localToOutputResampler) { + // resample to output sample rate + int frames = _localToOutputResampler->render(_localMixBuffer, _localOutputMixBuffer, + AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + + // write to local injectors' ring buffer + samples = frames * AudioConstants::STEREO; + _localInjectorsStream.writeSamples(_localOutputMixBuffer, samples); + + } else { + // write to local injectors' ring buffer + samples = AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; + _localInjectorsStream.writeSamples(_localMixBuffer, + AudioConstants::NETWORK_FRAME_SAMPLES_STEREO); + } + + _localSamplesAvailable.fetch_add(samples, std::memory_order_release); + samplesNeeded -= samples; + } +} + +bool AudioClient::mixLocalAudioInjectors(float* mixBuffer) { QVector injectorsToRemove; // lock the injector vector Lock lock(_injectorsMutex); - for (AudioInjector* injector : getActiveLocalAudioInjectors()) { + if (_activeLocalAudioInjectors.size() == 0) { + return false; + } + + memset(mixBuffer, 0, AudioConstants::NETWORK_FRAME_SAMPLES_STEREO * sizeof(float)); + + for (AudioInjector* injector : _activeLocalAudioInjectors) { if (injector->getLocalBuffer()) { static const int HRTF_DATASET_INDEX = 1; @@ -1098,8 +1181,8 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { qint64 bytesToRead = numChannels * AudioConstants::NETWORK_FRAME_BYTES_PER_CHANNEL; // get one frame from the injector - memset(_scratchBuffer, 0, bytesToRead); - if (0 < injector->getLocalBuffer()->readData((char*)_scratchBuffer, bytesToRead)) { + memset(_localScratchBuffer, 0, bytesToRead); + if (0 < injector->getLocalBuffer()->readData((char*)_localScratchBuffer, bytesToRead)) { if (injector->isAmbisonic()) { @@ -1119,7 +1202,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { float qz = relativeOrientation.y; // Ambisonic gets spatialized into mixBuffer - injector->getLocalFOA().render(_scratchBuffer, mixBuffer, HRTF_DATASET_INDEX, + injector->getLocalFOA().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, qw, qx, qy, qz, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } else if (injector->isStereo()) { @@ -1127,7 +1210,7 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { // stereo gets directly mixed into mixBuffer float gain = injector->getVolume(); for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - mixBuffer[i] += (float)_scratchBuffer[i] * (1/32768.0f) * gain; + mixBuffer[i] += convertToFloat(_localScratchBuffer[i]) * gain; } } else { @@ -1136,10 +1219,10 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { glm::vec3 relativePosition = injector->getPosition() - _positionGetter(); float distance = glm::max(glm::length(relativePosition), EPSILON); float gain = gainForSource(distance, injector->getVolume()); - float azimuth = azimuthForSource(relativePosition); + float azimuth = azimuthForSource(relativePosition); // mono gets spatialized into mixBuffer - injector->getLocalHRTF().render(_scratchBuffer, mixBuffer, HRTF_DATASET_INDEX, + injector->getLocalHRTF().render(_localScratchBuffer, mixBuffer, HRTF_DATASET_INDEX, azimuth, distance, gain, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } @@ -1160,8 +1243,10 @@ void AudioClient::mixLocalAudioInjectors(float* mixBuffer) { for (AudioInjector* injector : injectorsToRemove) { qCDebug(audioclient) << "removing injector"; - getActiveLocalAudioInjectors().removeOne(injector); + _activeLocalAudioInjectors.removeOne(injector); } + + return true; } void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteArray& outputBuffer) { @@ -1172,33 +1257,24 @@ void AudioClient::processReceivedSamples(const QByteArray& decodedBuffer, QByteA outputBuffer.resize(_outputFrameSize * AudioConstants::SAMPLE_SIZE); int16_t* outputSamples = reinterpret_cast(outputBuffer.data()); - // convert network audio to float - for (int i = 0; i < AudioConstants::NETWORK_FRAME_SAMPLES_STEREO; i++) { - _mixBuffer[i] = (float)decodedSamples[i] * (1/32768.0f); - } - - // mix in active injectors - if (getActiveLocalAudioInjectors().size() > 0) { - mixLocalAudioInjectors(_mixBuffer); - } + bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); // apply stereo reverb - bool hasReverb = _reverb || _receivedAudioStream.hasReverb(); if (hasReverb) { updateReverbOptions(); - _listenerReverb.render(_mixBuffer, _mixBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + int16_t* reverbSamples = _networkToOutputResampler ? _networkScratchBuffer : outputSamples; + _listenerReverb.render(decodedSamples, reverbSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); } + // resample to output sample rate if (_networkToOutputResampler) { + const int16_t* inputSamples = hasReverb ? _networkScratchBuffer : decodedSamples; + _networkToOutputResampler->render(inputSamples, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + } - // resample to output sample rate - _audioLimiter.render(_mixBuffer, _scratchBuffer, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - _networkToOutputResampler->render(_scratchBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); - - } else { - - // no resampling needed - _audioLimiter.render(_mixBuffer, outputSamples, AudioConstants::NETWORK_FRAME_SAMPLES_PER_CHANNEL); + // if no transformations were applied, we still need to copy the buffer + if (!hasReverb && !_networkToOutputResampler) { + memcpy(outputSamples, decodedSamples, decodedBuffer.size()); } } @@ -1381,6 +1457,9 @@ void AudioClient::outputNotify() { bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDeviceInfo) { bool supportedFormat = false; + Lock lock(_localAudioMutex); + _localSamplesAvailable.exchange(0, std::memory_order_release); + // cleanup any previously initialized device if (_audioOutput) { _audioOutput->stop(); @@ -1391,12 +1470,24 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _loopbackOutputDevice = NULL; delete _loopbackAudioOutput; _loopbackAudioOutput = NULL; + + delete[] _outputMixBuffer; + _outputMixBuffer = NULL; + + delete[] _outputScratchBuffer; + _outputScratchBuffer = NULL; + + delete[] _localOutputMixBuffer; + _localOutputMixBuffer = NULL; } if (_networkToOutputResampler) { // if we were using an input to network resampler, delete it here delete _networkToOutputResampler; _networkToOutputResampler = NULL; + + delete _localToOutputResampler; + _localToOutputResampler = NULL; } if (!outputDeviceInfo.isNull()) { @@ -1416,6 +1507,7 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice assert(_outputFormat.sampleSize() == 16); _networkToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); + _localToOutputResampler = new AudioSRC(_desiredOutputFormat.sampleRate(), _outputFormat.sampleRate(), OUTPUT_CHANNEL_COUNT); } else { qCDebug(audioclient) << "No resampling required for network output to match actual output format."; @@ -1441,6 +1533,14 @@ bool AudioClient::switchOutputToAudioDevice(const QAudioDeviceInfo& outputDevice _audioOutput->start(&_audioOutputIODevice); lock.unlock(); + int periodSampleSize = _audioOutput->periodSize() / AudioConstants::SAMPLE_SIZE; + // device callback is not restricted to periodSampleSize, so double the mix/scratch buffer sizes + _outputPeriod = periodSampleSize * 2; + _outputMixBuffer = new float[_outputPeriod]; + _outputScratchBuffer = new int16_t[_outputPeriod]; + _localOutputMixBuffer = new float[_outputPeriod]; + _localInjectorsStream.resizeForFrameSize(_outputPeriod * 2); + qCDebug(audioclient) << "Output Buffer capacity in frames: " << _audioOutput->bufferSize() / AudioConstants::SAMPLE_SIZE / (float)deviceFrameSize << "requested bytes:" << requestedSize << "actual bytes:" << _audioOutput->bufferSize() << "os default:" << osDefaultBufferSize << "period size:" << _audioOutput->periodSize(); @@ -1550,26 +1650,61 @@ qint64 AudioClient::AudioOutputIODevice::readData(char * data, qint64 maxSize) { // samples requested from OUTPUT_CHANNEL_COUNT int deviceChannelCount = _audio->_outputFormat.channelCount(); int samplesRequested = (int)(maxSize / AudioConstants::SAMPLE_SIZE) * OUTPUT_CHANNEL_COUNT / deviceChannelCount; + // restrict samplesRequested to the size of our mix/scratch buffers + samplesRequested = std::min(samplesRequested, _audio->_outputPeriod); - int samplesPopped; - int bytesWritten; + int16_t* scratchBuffer = _audio->_outputScratchBuffer; + float* mixBuffer = _audio->_outputMixBuffer; - if ((samplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { - qCDebug(audiostream, "Read %d samples from buffer (%d available)", samplesPopped, _receivedAudioStream.getSamplesAvailable()); + int networkSamplesPopped; + if ((networkSamplesPopped = _receivedAudioStream.popSamples(samplesRequested, false)) > 0) { + qCDebug(audiostream, "Read %d samples from buffer (%d available, %d requested)", networkSamplesPopped, _receivedAudioStream.getSamplesAvailable(), samplesRequested); AudioRingBuffer::ConstIterator lastPopOutput = _receivedAudioStream.getLastPopOutput(); + lastPopOutput.readSamples(scratchBuffer, networkSamplesPopped); - // if required, upmix or downmix to deviceChannelCount - if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { - lastPopOutput.readSamples((int16_t*)data, samplesPopped); - } else if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { - lastPopOutput.readSamplesWithUpmix((int16_t*)data, samplesPopped, deviceChannelCount - OUTPUT_CHANNEL_COUNT); - } else { - lastPopOutput.readSamplesWithDownmix((int16_t*)data, samplesPopped); + for (int i = 0; i < networkSamplesPopped; i++) { + mixBuffer[i] = convertToFloat(scratchBuffer[i]); } - bytesWritten = (samplesPopped * AudioConstants::SAMPLE_SIZE) * deviceChannelCount / OUTPUT_CHANNEL_COUNT; + + samplesRequested = networkSamplesPopped; + } + + int injectorSamplesPopped = 0; + { + Lock lock(_audio->_localAudioMutex); + bool append = networkSamplesPopped > 0; + samplesRequested = std::min(samplesRequested, _audio->_localSamplesAvailable.load(std::memory_order_acquire)); + if ((injectorSamplesPopped = _localInjectorsStream.appendSamples(mixBuffer, samplesRequested, append)) > 0) { + _audio->_localSamplesAvailable.fetch_sub(injectorSamplesPopped, std::memory_order_release); + qCDebug(audiostream, "Read %d samples from injectors (%d available, %d requested)", injectorSamplesPopped, _localInjectorsStream.samplesAvailable(), samplesRequested); + } + } + + // prepare injectors for the next callback + QMetaObject::invokeMethod(&_audio->_localAudioThread, "prepare", Qt::QueuedConnection); + + int samplesPopped = std::max(networkSamplesPopped, injectorSamplesPopped); + int framesPopped = samplesPopped / AudioConstants::STEREO; + int bytesWritten; + if (samplesPopped > 0) { + if (deviceChannelCount == OUTPUT_CHANNEL_COUNT) { + // limit the audio + _audio->_audioLimiter.render(mixBuffer, (int16_t*)data, framesPopped); + } else { + _audio->_audioLimiter.render(mixBuffer, scratchBuffer, framesPopped); + + // upmix or downmix to deviceChannelCount + if (deviceChannelCount > OUTPUT_CHANNEL_COUNT) { + int extraChannels = deviceChannelCount - OUTPUT_CHANNEL_COUNT; + channelUpmix(scratchBuffer, (int16_t*)data, samplesPopped, extraChannels); + } else { + channelDownmix(scratchBuffer, (int16_t*)data, samplesPopped); + } + } + + bytesWritten = framesPopped * AudioConstants::SAMPLE_SIZE * deviceChannelCount; } else { // nothing on network, don't grab anything from injectors, and just return 0s - // this will flood the log: qCDebug(audioclient, "empty/partial network buffer"); memset(data, 0, maxSize); bytesWritten = maxSize; } diff --git a/libraries/audio-client/src/AudioClient.h b/libraries/audio-client/src/AudioClient.h index 123da35319..699ba71ef7 100644 --- a/libraries/audio-client/src/AudioClient.h +++ b/libraries/audio-client/src/AudioClient.h @@ -69,9 +69,24 @@ class QIODevice; class Transform; class NLPacket; +class AudioInjectorsThread : public QThread { + Q_OBJECT + +public: + AudioInjectorsThread(AudioClient* audio) : _audio(audio) {} + +public slots : + void prepare(); + +private: + AudioClient* _audio; +}; + class AudioClient : public AbstractAudioInterface, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY + + using LocalInjectorsStream = AudioMixRingBuffer; public: static const int MIN_BUFFER_FRAMES; static const int MAX_BUFFER_FRAMES; @@ -84,8 +99,10 @@ public: class AudioOutputIODevice : public QIODevice { public: - AudioOutputIODevice(MixedProcessedAudioStream& receivedAudioStream, AudioClient* audio) : - _receivedAudioStream(receivedAudioStream), _audio(audio), _unfulfilledReads(0) {}; + AudioOutputIODevice(LocalInjectorsStream& localInjectorsStream, MixedProcessedAudioStream& receivedAudioStream, + AudioClient* audio) : + _localInjectorsStream(localInjectorsStream), _receivedAudioStream(receivedAudioStream), + _audio(audio), _unfulfilledReads(0) {} void start() { open(QIODevice::ReadOnly | QIODevice::Unbuffered); } void stop() { close(); } @@ -93,6 +110,7 @@ public: qint64 writeData(const char * data, qint64 maxSize) override { return 0; } int getRecentUnfulfilledReads() { int unfulfilledReads = _unfulfilledReads; _unfulfilledReads = 0; return unfulfilledReads; } private: + LocalInjectorsStream& _localInjectorsStream; MixedProcessedAudioStream& _receivedAudioStream; AudioClient* _audio; int _unfulfilledReads; @@ -129,8 +147,6 @@ public: Q_INVOKABLE void setAvatarBoundingBoxParameters(glm::vec3 corner, glm::vec3 scale); - QVector& getActiveLocalAudioInjectors() { return _activeLocalAudioInjectors; } - void checkDevices(); static const float CALLBACK_ACCELERATOR_RATIO; @@ -171,6 +187,7 @@ public slots: int setOutputBufferSize(int numFrames, bool persist = true); + void prepareLocalAudioInjectors(); bool outputLocalInjector(AudioInjector* injector) override; bool shouldLoopbackInjectors() override { return _shouldEchoToServer; } @@ -218,7 +235,7 @@ protected: private: void outputFormatChanged(); - void mixLocalAudioInjectors(float* mixBuffer); + bool mixLocalAudioInjectors(float* mixBuffer); float azimuthForSource(const glm::vec3& relativePosition); float gainForSource(float distance, float volume); @@ -262,6 +279,10 @@ private: QAudioOutput* _loopbackAudioOutput; QIODevice* _loopbackOutputDevice; AudioRingBuffer _inputRingBuffer; + LocalInjectorsStream _localInjectorsStream; + // In order to use _localInjectorsStream as a lock-free pipe, + // use it with a single producer/consumer, and track available samples + std::atomic _localSamplesAvailable { 0 }; MixedProcessedAudioStream _receivedAudioStream; bool _isStereoInput; @@ -292,14 +313,28 @@ private: AudioEffectOptions* _reverbOptions; AudioReverb _sourceReverb { AudioConstants::SAMPLE_RATE }; AudioReverb _listenerReverb { AudioConstants::SAMPLE_RATE }; + AudioReverb _localReverb { AudioConstants::SAMPLE_RATE }; // possible streams needed for resample AudioSRC* _inputToNetworkResampler; AudioSRC* _networkToOutputResampler; + AudioSRC* _localToOutputResampler; + + // for network audio (used by network audio thread) + int16_t _networkScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + + // for local audio (used by audio injectors thread) + float _localMixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; + int16_t _localScratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; + float* _localOutputMixBuffer { NULL }; + AudioInjectorsThread _localAudioThread; + Mutex _localAudioMutex; + + // for output audio (used by this thread) + int _outputPeriod { 0 }; + float* _outputMixBuffer { NULL }; + int16_t* _outputScratchBuffer { NULL }; - // for local hrtf-ing - float _mixBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_STEREO]; - int16_t _scratchBuffer[AudioConstants::NETWORK_FRAME_SAMPLES_AMBISONIC]; AudioLimiter _audioLimiter; // Adds Reverb diff --git a/libraries/audio/src/AudioRingBuffer.cpp b/libraries/audio/src/AudioRingBuffer.cpp index 260c682cde..59b3e874d7 100644 --- a/libraries/audio/src/AudioRingBuffer.cpp +++ b/libraries/audio/src/AudioRingBuffer.cpp @@ -26,46 +26,51 @@ static const QString RING_BUFFER_OVERFLOW_DEBUG { "AudioRingBuffer::writeData has overflown the buffer. Overwriting old data." }; static const QString DROPPED_SILENT_DEBUG { "AudioRingBuffer::addSilentSamples dropping silent samples to prevent overflow." }; -AudioRingBuffer::AudioRingBuffer(int numFrameSamples, int numFramesCapacity) : +template +AudioRingBufferTemplate::AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity) : _numFrameSamples(numFrameSamples), _frameCapacity(numFramesCapacity), _sampleCapacity(numFrameSamples * numFramesCapacity), _bufferLength(numFrameSamples * (numFramesCapacity + 1)) { if (numFrameSamples) { - _buffer = new int16_t[_bufferLength]; - memset(_buffer, 0, _bufferLength * sizeof(int16_t)); + _buffer = new Sample[_bufferLength]; + memset(_buffer, 0, _bufferLength * SampleSize); _nextOutput = _buffer; _endOfLastWrite = _buffer; } static QString repeatedOverflowMessage = LogHandler::getInstance().addRepeatedMessageRegex(RING_BUFFER_OVERFLOW_DEBUG); static QString repeatedDroppedMessage = LogHandler::getInstance().addRepeatedMessageRegex(DROPPED_SILENT_DEBUG); -}; +} -AudioRingBuffer::~AudioRingBuffer() { +template +AudioRingBufferTemplate::~AudioRingBufferTemplate() { delete[] _buffer; } -void AudioRingBuffer::clear() { +template +void AudioRingBufferTemplate::clear() { _endOfLastWrite = _buffer; _nextOutput = _buffer; } -void AudioRingBuffer::reset() { +template +void AudioRingBufferTemplate::reset() { clear(); _overflowCount = 0; } -void AudioRingBuffer::resizeForFrameSize(int numFrameSamples) { +template +void AudioRingBufferTemplate::resizeForFrameSize(int numFrameSamples) { delete[] _buffer; _numFrameSamples = numFrameSamples; _sampleCapacity = numFrameSamples * _frameCapacity; _bufferLength = numFrameSamples * (_frameCapacity + 1); if (numFrameSamples) { - _buffer = new int16_t[_bufferLength]; - memset(_buffer, 0, _bufferLength * sizeof(int16_t)); + _buffer = new Sample[_bufferLength]; + memset(_buffer, 0, _bufferLength * SampleSize); } else { _buffer = nullptr; } @@ -73,17 +78,29 @@ void AudioRingBuffer::resizeForFrameSize(int numFrameSamples) { reset(); } -int AudioRingBuffer::readSamples(int16_t* destination, int maxSamples) { - return readData((char*)destination, maxSamples * sizeof(int16_t)) / sizeof(int16_t); +template +int AudioRingBufferTemplate::readSamples(Sample* destination, int maxSamples) { + return readData((char*)destination, maxSamples * SampleSize) / SampleSize; } -int AudioRingBuffer::writeSamples(const int16_t* source, int maxSamples) { - return writeData((char*)source, maxSamples * sizeof(int16_t)) / sizeof(int16_t); +template +int AudioRingBufferTemplate::appendSamples(Sample* destination, int maxSamples, bool append) { + if (append) { + return appendData((char*)destination, maxSamples * SampleSize) / SampleSize; + } else { + return readData((char*)destination, maxSamples * SampleSize) / SampleSize; + } } -int AudioRingBuffer::readData(char *data, int maxSize) { +template +int AudioRingBufferTemplate::writeSamples(const Sample* source, int maxSamples) { + return writeData((char*)source, maxSamples * SampleSize) / SampleSize; +} + +template +int AudioRingBufferTemplate::readData(char *data, int maxSize) { // only copy up to the number of samples we have available - int maxSamples = maxSize / sizeof(int16_t); + int maxSamples = maxSize / SampleSize; int numReadSamples = std::min(maxSamples, samplesAvailable()); if (_nextOutput + numReadSamples > _buffer + _bufferLength) { @@ -91,22 +108,56 @@ int AudioRingBuffer::readData(char *data, int maxSize) { int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput; // read to the end of the buffer - memcpy(data, _nextOutput, numSamplesToEnd * sizeof(int16_t)); + memcpy(data, _nextOutput, numSamplesToEnd * SampleSize); // read the rest from the beginning of the buffer - memcpy(data + (numSamplesToEnd * sizeof(int16_t)), _buffer, (numReadSamples - numSamplesToEnd) * sizeof(int16_t)); + memcpy(data + (numSamplesToEnd * SampleSize), _buffer, (numReadSamples - numSamplesToEnd) * SampleSize); } else { - memcpy(data, _nextOutput, numReadSamples * sizeof(int16_t)); + memcpy(data, _nextOutput, numReadSamples * SampleSize); } shiftReadPosition(numReadSamples); - return numReadSamples * sizeof(int16_t); + return numReadSamples * SampleSize; } -int AudioRingBuffer::writeData(const char* data, int maxSize) { +template +int AudioRingBufferTemplate::appendData(char *data, int maxSize) { + // only copy up to the number of samples we have available + int maxSamples = maxSize / SampleSize; + int numReadSamples = std::min(maxSamples, samplesAvailable()); + + Sample* dest = reinterpret_cast(data); + Sample* output = _nextOutput; + if (_nextOutput + numReadSamples > _buffer + _bufferLength) { + // we're going to need to do two reads to get this data, it wraps around the edge + int numSamplesToEnd = (_buffer + _bufferLength) - _nextOutput; + + // read to the end of the buffer + for (int i = 0; i < numSamplesToEnd; i++) { + *dest++ += *output++; + } + + // read the rest from the beginning of the buffer + output = _buffer; + for (int i = 0; i < (numReadSamples - numSamplesToEnd); i++) { + *dest++ += *output++; + } + } else { + for (int i = 0; i < numReadSamples; i++) { + *dest++ += *output++; + } + } + + shiftReadPosition(numReadSamples); + + return numReadSamples * SampleSize; +} + +template +int AudioRingBufferTemplate::writeData(const char* data, int maxSize) { // only copy up to the number of samples we have capacity for - int maxSamples = maxSize / sizeof(int16_t); + int maxSamples = maxSize / SampleSize; int numWriteSamples = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); @@ -124,20 +175,21 @@ int AudioRingBuffer::writeData(const char* data, int maxSize) { int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; // write to the end of the buffer - memcpy(_endOfLastWrite, data, numSamplesToEnd * sizeof(int16_t)); + memcpy(_endOfLastWrite, data, numSamplesToEnd * SampleSize); // write the rest to the beginning of the buffer - memcpy(_buffer, data + (numSamplesToEnd * sizeof(int16_t)), (numWriteSamples - numSamplesToEnd) * sizeof(int16_t)); + memcpy(_buffer, data + (numSamplesToEnd * SampleSize), (numWriteSamples - numSamplesToEnd) * SampleSize); } else { - memcpy(_endOfLastWrite, data, numWriteSamples * sizeof(int16_t)); + memcpy(_endOfLastWrite, data, numWriteSamples * SampleSize); } _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); - return numWriteSamples * sizeof(int16_t); + return numWriteSamples * SampleSize; } -int AudioRingBuffer::samplesAvailable() const { +template +int AudioRingBufferTemplate::samplesAvailable() const { if (!_endOfLastWrite) { return 0; } @@ -149,31 +201,8 @@ int AudioRingBuffer::samplesAvailable() const { return sampleDifference; } -int AudioRingBuffer::addSilentSamples(int silentSamples) { - // NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there - int numWriteSamples = std::min(silentSamples, _sampleCapacity); - int samplesRoomFor = _sampleCapacity - samplesAvailable(); - - if (numWriteSamples > samplesRoomFor) { - numWriteSamples = samplesRoomFor; - - qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG); - } - - if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { - int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; - memset(_endOfLastWrite, 0, numSamplesToEnd * sizeof(int16_t)); - memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * sizeof(int16_t)); - } else { - memset(_endOfLastWrite, 0, numWriteSamples * sizeof(int16_t)); - } - - _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); - - return numWriteSamples; -} - -int16_t* AudioRingBuffer::shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const { +template +typename AudioRingBufferTemplate::Sample* AudioRingBufferTemplate::shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const { // NOTE: It is possible to shift out-of-bounds if (|numSamplesShift| > 2 * _bufferLength), but this should not occur if (numSamplesShift > 0 && position + numSamplesShift >= _buffer + _bufferLength) { // this shift will wrap the position around to the beginning of the ring @@ -186,11 +215,37 @@ int16_t* AudioRingBuffer::shiftedPositionAccomodatingWrap(int16_t* position, int } } -float AudioRingBuffer::getFrameLoudness(const int16_t* frameStart) const { +template +int AudioRingBufferTemplate::addSilentSamples(int silentSamples) { + // NOTE: This implementation is nearly identical to writeData save for s/memcpy/memset, refer to comments there + int numWriteSamples = std::min(silentSamples, _sampleCapacity); + int samplesRoomFor = _sampleCapacity - samplesAvailable(); + + if (numWriteSamples > samplesRoomFor) { + numWriteSamples = samplesRoomFor; + + qCDebug(audio) << qPrintable(DROPPED_SILENT_DEBUG); + } + + if (_endOfLastWrite + numWriteSamples > _buffer + _bufferLength) { + int numSamplesToEnd = (_buffer + _bufferLength) - _endOfLastWrite; + memset(_endOfLastWrite, 0, numSamplesToEnd * SampleSize); + memset(_buffer, 0, (numWriteSamples - numSamplesToEnd) * SampleSize); + } else { + memset(_endOfLastWrite, 0, numWriteSamples * SampleSize); + } + + _endOfLastWrite = shiftedPositionAccomodatingWrap(_endOfLastWrite, numWriteSamples); + + return numWriteSamples; +} + +template +float AudioRingBufferTemplate::getFrameLoudness(const Sample* frameStart) const { // FIXME: This is a bad measure of loudness - normal estimation uses sqrt(sum(x*x)) float loudness = 0.0f; - const int16_t* sampleAt = frameStart; - const int16_t* bufferLastAt = _buffer + _bufferLength - 1; + const Sample* sampleAt = frameStart; + const Sample* bufferLastAt = _buffer + _bufferLength - 1; for (int i = 0; i < _numFrameSamples; ++i) { loudness += (float) std::abs(*sampleAt); @@ -203,14 +258,16 @@ float AudioRingBuffer::getFrameLoudness(const int16_t* frameStart) const { return loudness; } -float AudioRingBuffer::getFrameLoudness(ConstIterator frameStart) const { +template +float AudioRingBufferTemplate::getFrameLoudness(ConstIterator frameStart) const { if (frameStart.isNull()) { return 0.0f; } return getFrameLoudness(&(*frameStart)); } -int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { +template +int AudioRingBufferTemplate::writeSamples(ConstIterator source, int maxSamples) { int samplesToCopy = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); if (samplesToCopy > samplesRoomFor) { @@ -221,7 +278,7 @@ int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); } - int16_t* bufferLast = _buffer + _bufferLength - 1; + Sample* bufferLast = _buffer + _bufferLength - 1; for (int i = 0; i < samplesToCopy; i++) { *_endOfLastWrite = *source; _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; @@ -231,7 +288,8 @@ int AudioRingBuffer::writeSamples(ConstIterator source, int maxSamples) { return samplesToCopy; } -int AudioRingBuffer::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) { +template +int AudioRingBufferTemplate::writeSamplesWithFade(ConstIterator source, int maxSamples, float fade) { int samplesToCopy = std::min(maxSamples, _sampleCapacity); int samplesRoomFor = _sampleCapacity - samplesAvailable(); if (samplesToCopy > samplesRoomFor) { @@ -242,12 +300,16 @@ int AudioRingBuffer::writeSamplesWithFade(ConstIterator source, int maxSamples, qCDebug(audio) << qPrintable(RING_BUFFER_OVERFLOW_DEBUG); } - int16_t* bufferLast = _buffer + _bufferLength - 1; + Sample* bufferLast = _buffer + _bufferLength - 1; for (int i = 0; i < samplesToCopy; i++) { - *_endOfLastWrite = (int16_t)((float)(*source) * fade); + *_endOfLastWrite = (Sample)((float)(*source) * fade); _endOfLastWrite = (_endOfLastWrite == bufferLast) ? _buffer : _endOfLastWrite + 1; ++source; } return samplesToCopy; } + +// explicit instantiations for scratch/mix buffers +template class AudioRingBufferTemplate; +template class AudioRingBufferTemplate; diff --git a/libraries/audio/src/AudioRingBuffer.h b/libraries/audio/src/AudioRingBuffer.h index 29e7a9e998..bb32df19a2 100644 --- a/libraries/audio/src/AudioRingBuffer.h +++ b/libraries/audio/src/AudioRingBuffer.h @@ -21,15 +21,19 @@ const int DEFAULT_RING_BUFFER_FRAME_CAPACITY = 10; -class AudioRingBuffer { +template +class AudioRingBufferTemplate { + using Sample = T; + static const int SampleSize = sizeof(Sample); + public: - AudioRingBuffer(int numFrameSamples, int numFramesCapacity = DEFAULT_RING_BUFFER_FRAME_CAPACITY); - ~AudioRingBuffer(); + AudioRingBufferTemplate(int numFrameSamples, int numFramesCapacity = DEFAULT_RING_BUFFER_FRAME_CAPACITY); + ~AudioRingBufferTemplate(); // disallow copying - AudioRingBuffer(const AudioRingBuffer&) = delete; - AudioRingBuffer(AudioRingBuffer&&) = delete; - AudioRingBuffer& operator=(const AudioRingBuffer&) = delete; + AudioRingBufferTemplate(const AudioRingBufferTemplate&) = delete; + AudioRingBufferTemplate(AudioRingBufferTemplate&&) = delete; + AudioRingBufferTemplate& operator=(const AudioRingBufferTemplate&) = delete; /// Invalidate any data in the buffer void clear(); @@ -41,13 +45,27 @@ public: // FIXME: discards any data in the buffer void resizeForFrameSize(int numFrameSamples); + // Reading and writing to the buffer uses minimal shared data, such that + // in cases that avoid overwriting the buffer, a single producer/consumer + // may use this as a lock-free pipe (see audio-client/src/AudioClient.cpp). + // IMPORTANT: Avoid changes to the implementation that touch shared data unless you can + // maintain this behavior. + /// Read up to maxSamples into destination (will only read up to samplesAvailable()) /// Returns number of read samples - int readSamples(int16_t* destination, int maxSamples); + int readSamples(Sample* destination, int maxSamples); + + /// Append up to maxSamples into destination (will only read up to samplesAvailable()) + /// If append == false, behaves as readSamples + /// Returns number of appended samples + int appendSamples(Sample* destination, int maxSamples, bool append = true); + + /// Skip up to maxSamples (will only skip up to samplesAvailable()) + void skipSamples(int maxSamples) { shiftReadPosition(std::min(maxSamples, samplesAvailable())); } /// Write up to maxSamples from source (will only write up to sample capacity) /// Returns number of written samples - int writeSamples(const int16_t* source, int maxSamples); + int writeSamples(const Sample* source, int maxSamples); /// Write up to maxSamples silent samples (will only write until other data exists in the buffer) /// This method will not overwrite existing data in the buffer, instead dropping silent samples that would overflow @@ -58,13 +76,17 @@ public: /// Returns number of read bytes int readData(char* destination, int maxSize); + /// Append up to maxSize into destination + /// Returns number of read bytes + int appendData(char* destination, int maxSize); + /// Write up to maxSize from source /// Returns number of written bytes int writeData(const char* source, int maxSize); /// Returns a reference to the index-th sample offset from the current read sample - int16_t& operator[](const int index) { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } - const int16_t& operator[] (const int index) const { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } + Sample& operator[](const int index) { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } + const Sample& operator[] (const int index) const { return *shiftedPositionAccomodatingWrap(_nextOutput, index); } /// Essentially discards the next numSamples from the ring buffer /// NOTE: This is not checked - it is possible to shift past written data @@ -84,41 +106,104 @@ public: class ConstIterator { public: - ConstIterator(); - ConstIterator(int16_t* bufferFirst, int capacity, int16_t* at); + ConstIterator() : + _bufferLength(0), + _bufferFirst(NULL), + _bufferLast(NULL), + _at(NULL) {} + ConstIterator(Sample* bufferFirst, int capacity, Sample* at) : + _bufferLength(capacity), + _bufferFirst(bufferFirst), + _bufferLast(bufferFirst + capacity - 1), + _at(at) {} ConstIterator(const ConstIterator& rhs) = default; bool isNull() const { return _at == NULL; } bool operator==(const ConstIterator& rhs) { return _at == rhs._at; } bool operator!=(const ConstIterator& rhs) { return _at != rhs._at; } - const int16_t& operator*() { return *_at; } + const Sample& operator*() { return *_at; } - ConstIterator& operator=(const ConstIterator& rhs); - ConstIterator& operator++(); - ConstIterator operator++(int); - ConstIterator& operator--(); - ConstIterator operator--(int); - const int16_t& operator[] (int i); - ConstIterator operator+(int i); - ConstIterator operator-(int i); + ConstIterator& operator=(const ConstIterator& rhs) { + _bufferLength = rhs._bufferLength; + _bufferFirst = rhs._bufferFirst; + _bufferLast = rhs._bufferLast; + _at = rhs._at; + return *this; + } + ConstIterator& operator++() { + _at = (_at == _bufferLast) ? _bufferFirst : _at + 1; + return *this; + } + ConstIterator operator++(int) { + ConstIterator tmp(*this); + ++(*this); + return tmp; + } + ConstIterator& operator--() { + _at = (_at == _bufferFirst) ? _bufferLast : _at - 1; + return *this; + } + ConstIterator operator--(int) { + ConstIterator tmp(*this); + --(*this); + return tmp; + } + const Sample& operator[] (int i) { + return *atShiftedBy(i); + } + ConstIterator operator+(int i) { + return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i)); + } + ConstIterator operator-(int i) { + return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i)); + } + + void readSamples(Sample* dest, int numSamples) { + auto samplesToEnd = _bufferLast - _at + 1; + + if (samplesToEnd >= numSamples) { + memcpy(dest, _at, numSamples * SampleSize); + _at += numSamples; + } else { + auto samplesFromStart = numSamples - samplesToEnd; + memcpy(dest, _at, samplesToEnd * SampleSize); + memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * SampleSize); + + _at = _bufferFirst + samplesFromStart; + } + } + void readSamplesWithFade(Sample* dest, int numSamples, float fade) { + Sample* at = _at; + for (int i = 0; i < numSamples; i++) { + *dest = (float)*at * fade; + ++dest; + at = (at == _bufferLast) ? _bufferFirst : at + 1; + } + } - void readSamples(int16_t* dest, int numSamples); - void readSamplesWithFade(int16_t* dest, int numSamples, float fade); - void readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels); - void readSamplesWithDownmix(int16_t* dest, int numSamples); private: - int16_t* atShiftedBy(int i); + Sample* atShiftedBy(int i) { + i = (_at - _bufferFirst + i) % _bufferLength; + if (i < 0) { + i += _bufferLength; + } + return _bufferFirst + i; + } int _bufferLength; - int16_t* _bufferFirst; - int16_t* _bufferLast; - int16_t* _at; + Sample* _bufferFirst; + Sample* _bufferLast; + Sample* _at; }; - ConstIterator nextOutput() const; - ConstIterator lastFrameWritten() const; + ConstIterator nextOutput() const { + return ConstIterator(_buffer, _bufferLength, _nextOutput); + } + ConstIterator lastFrameWritten() const { + return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples; + } int writeSamples(ConstIterator source, int maxSamples); int writeSamplesWithFade(ConstIterator source, int maxSamples, float fade); @@ -126,8 +211,8 @@ public: float getFrameLoudness(ConstIterator frameStart) const; protected: - int16_t* shiftedPositionAccomodatingWrap(int16_t* position, int numSamplesShift) const; - float getFrameLoudness(const int16_t* frameStart) const; + Sample* shiftedPositionAccomodatingWrap(Sample* position, int numSamplesShift) const; + float getFrameLoudness(const Sample* frameStart) const; int _numFrameSamples; int _frameCapacity; @@ -135,138 +220,13 @@ protected: int _bufferLength; // actual _buffer length (_sampleCapacity + 1) int _overflowCount{ 0 }; // times the ring buffer has overwritten data - int16_t* _nextOutput{ nullptr }; - int16_t* _endOfLastWrite{ nullptr }; - int16_t* _buffer{ nullptr }; + Sample* _nextOutput{ nullptr }; + Sample* _endOfLastWrite{ nullptr }; + Sample* _buffer{ nullptr }; }; -// inline the iterator: -inline AudioRingBuffer::ConstIterator::ConstIterator() : - _bufferLength(0), - _bufferFirst(NULL), - _bufferLast(NULL), - _at(NULL) {} - -inline AudioRingBuffer::ConstIterator::ConstIterator(int16_t* bufferFirst, int capacity, int16_t* at) : - _bufferLength(capacity), - _bufferFirst(bufferFirst), - _bufferLast(bufferFirst + capacity - 1), - _at(at) {} - -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator=(const ConstIterator& rhs) { - _bufferLength = rhs._bufferLength; - _bufferFirst = rhs._bufferFirst; - _bufferLast = rhs._bufferLast; - _at = rhs._at; - return *this; -} - -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator++() { - _at = (_at == _bufferLast) ? _bufferFirst : _at + 1; - return *this; -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator++(int) { - ConstIterator tmp(*this); - ++(*this); - return tmp; -} - -inline AudioRingBuffer::ConstIterator& AudioRingBuffer::ConstIterator::operator--() { - _at = (_at == _bufferFirst) ? _bufferLast : _at - 1; - return *this; -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator--(int) { - ConstIterator tmp(*this); - --(*this); - return tmp; -} - -inline const int16_t& AudioRingBuffer::ConstIterator::operator[] (int i) { - return *atShiftedBy(i); -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator+(int i) { - return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(i)); -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::ConstIterator::operator-(int i) { - return ConstIterator(_bufferFirst, _bufferLength, atShiftedBy(-i)); -} - -inline int16_t* AudioRingBuffer::ConstIterator::atShiftedBy(int i) { - i = (_at - _bufferFirst + i) % _bufferLength; - if (i < 0) { - i += _bufferLength; - } - return _bufferFirst + i; -} - -inline void AudioRingBuffer::ConstIterator::readSamples(int16_t* dest, int numSamples) { - auto samplesToEnd = _bufferLast - _at + 1; - - if (samplesToEnd >= numSamples) { - memcpy(dest, _at, numSamples * sizeof(int16_t)); - _at += numSamples; - } else { - auto samplesFromStart = numSamples - samplesToEnd; - memcpy(dest, _at, samplesToEnd * sizeof(int16_t)); - memcpy(dest + samplesToEnd, _bufferFirst, samplesFromStart * sizeof(int16_t)); - - _at = _bufferFirst + samplesFromStart; - } -} - -inline void AudioRingBuffer::ConstIterator::readSamplesWithFade(int16_t* dest, int numSamples, float fade) { - int16_t* at = _at; - for (int i = 0; i < numSamples; i++) { - *dest = (float)*at * fade; - ++dest; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - } -} - -inline void AudioRingBuffer::ConstIterator::readSamplesWithUpmix(int16_t* dest, int numSamples, int numExtraChannels) { - int16_t* at = _at; - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - int16_t right = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - - // write 2 + N samples - *dest++ = left; - *dest++ = right; - for (int n = 0; n < numExtraChannels; n++) { - *dest++ = 0; - } - } -} - -inline void AudioRingBuffer::ConstIterator::readSamplesWithDownmix(int16_t* dest, int numSamples) { - int16_t* at = _at; - for (int i = 0; i < numSamples/2; i++) { - - // read 2 samples - int16_t left = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - int16_t right = *at; - at = (at == _bufferLast) ? _bufferFirst : at + 1; - - // write 1 sample - *dest++ = (int16_t)((left + right) / 2); - } -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::nextOutput() const { - return ConstIterator(_buffer, _bufferLength, _nextOutput); -} - -inline AudioRingBuffer::ConstIterator AudioRingBuffer::lastFrameWritten() const { - return ConstIterator(_buffer, _bufferLength, _endOfLastWrite) - _numFrameSamples; -} +// expose explicit instantiations for scratch/mix buffers +using AudioRingBuffer = AudioRingBufferTemplate; +using AudioMixRingBuffer = AudioRingBufferTemplate; #endif // hifi_AudioRingBuffer_h diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.cpp b/libraries/entities-renderer/src/EntityTreeRenderer.cpp index d277fd540f..60bb29f85f 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.cpp +++ b/libraries/entities-renderer/src/EntityTreeRenderer.cpp @@ -956,68 +956,29 @@ void EntityTreeRenderer::checkAndCallPreload(const EntityItemID& entityID, const } } -bool EntityTreeRenderer::isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision) { - EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); - if (!entity) { - return false; - } - QUuid simulatorID = entity->getSimulatorID(); - if (simulatorID.isNull()) { - // Can be null if it has never moved since being created or coming out of persistence. - // However, for there to be a collission, one of the two objects must be moving. - const EntityItemID& otherID = (id == collision.idA) ? collision.idB : collision.idA; - EntityItemPointer otherEntity = entityTree->findEntityByEntityItemID(otherID); - if (!otherEntity) { - return false; - } - simulatorID = otherEntity->getSimulatorID(); - } - - if (simulatorID.isNull() || (simulatorID != myNodeID)) { - return false; - } - - return true; -} - -void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision) { - - if (!isCollisionOwner(myNodeID, entityTree, id, collision)) { - return; - } - - SharedSoundPointer collisionSound; - float mass = 1.0; // value doesn't get used, but set it so compiler is quiet - AACube minAACube; - bool success = false; - _tree->withReadLock([&] { - EntityItemPointer entity = entityTree->findEntityByEntityItemID(id); - if (entity) { - collisionSound = entity->getCollisionSound(); - mass = entity->computeMass(); - minAACube = entity->getMinimumAACube(success); - } - }); - if (!success) { - return; - } +void EntityTreeRenderer::playEntityCollisionSound(EntityItemPointer entity, const Collision& collision) { + assert((bool)entity); + SharedSoundPointer collisionSound = entity->getCollisionSound(); if (!collisionSound) { return; } + bool success = false; + AACube minAACube = entity->getMinimumAACube(success); + if (!success) { + return; + } + float mass = entity->computeMass(); - const float COLLISION_PENETRATION_TO_VELOCITY = 50; // as a subsitute for RELATIVE entity->getVelocity() + const float COLLISION_PENETRATION_TO_VELOCITY = 50.0f; // as a subsitute for RELATIVE entity->getVelocity() // The collision.penetration is a pretty good indicator of changed velocity AFTER the initial contact, // but that first contact depends on exactly where we hit in the physics step. // We can get a more consistent initial-contact energy reading by using the changed velocity. // Note that velocityChange is not a good indicator for continuing collisions, because it does not distinguish // between bounce and sliding along a surface. - const float linearVelocity = (collision.type == CONTACT_EVENT_TYPE_START) ? - glm::length(collision.velocityChange) : - glm::length(collision.penetration) * COLLISION_PENETRATION_TO_VELOCITY; - const float energy = mass * linearVelocity * linearVelocity / 2.0f; - const glm::vec3 position = collision.contactPoint; + const float speedSquared = (collision.type == CONTACT_EVENT_TYPE_START) ? + glm::length2(collision.velocityChange) : + glm::length2(collision.penetration) * COLLISION_PENETRATION_TO_VELOCITY; + const float energy = mass * speedSquared / 2.0f; const float COLLISION_ENERGY_AT_FULL_VOLUME = (collision.type == CONTACT_EVENT_TYPE_START) ? 150.0f : 5.0f; const float COLLISION_MINIMUM_VOLUME = 0.005f; const float energyFactorOfFull = fmin(1.0f, energy / COLLISION_ENERGY_AT_FULL_VOLUME); @@ -1031,7 +992,7 @@ void EntityTreeRenderer::playEntityCollisionSound(const QUuid& myNodeID, EntityT // Shift the pitch down by ln(1 + (size / COLLISION_SIZE_FOR_STANDARD_PITCH)) / ln(2) const float COLLISION_SIZE_FOR_STANDARD_PITCH = 0.2f; const float stretchFactor = log(1.0f + (minAACube.getLargestDimension() / COLLISION_SIZE_FOR_STANDARD_PITCH)) / log(2); - AudioInjector::playSound(collisionSound, volume, stretchFactor, position); + AudioInjector::playSound(collisionSound, volume, stretchFactor, collision.contactPoint); } void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, const EntityItemID& idB, @@ -1041,30 +1002,28 @@ void EntityTreeRenderer::entityCollisionWithEntity(const EntityItemID& idA, cons if (!_tree || _shuttingDown) { return; } - // Don't respond to small continuous contacts. - const float COLLISION_MINUMUM_PENETRATION = 0.002f; - if ((collision.type == CONTACT_EVENT_TYPE_CONTINUE) && (glm::length(collision.penetration) < COLLISION_MINUMUM_PENETRATION)) { - return; - } - // See if we should play sounds EntityTreePointer entityTree = std::static_pointer_cast(_tree); const QUuid& myNodeID = DependencyManager::get()->getSessionUUID(); - playEntityCollisionSound(myNodeID, entityTree, idA, collision); - playEntityCollisionSound(myNodeID, entityTree, idB, collision); - // And now the entity scripts - if (isCollisionOwner(myNodeID, entityTree, idA, collision)) { + // trigger scripted collision sounds and events for locally owned objects + EntityItemPointer entityA = entityTree->findEntityByEntityItemID(idA); + if ((bool)entityA && myNodeID == entityA->getSimulatorID()) { + playEntityCollisionSound(entityA, collision); emit collisionWithEntity(idA, idB, collision); if (_entitiesScriptEngine) { _entitiesScriptEngine->callEntityScriptMethod(idA, "collisionWithEntity", idB, collision); } } - - if (isCollisionOwner(myNodeID, entityTree, idB, collision)) { - emit collisionWithEntity(idB, idA, collision); + EntityItemPointer entityB = entityTree->findEntityByEntityItemID(idB); + if ((bool)entityB && myNodeID == entityB->getSimulatorID()) { + playEntityCollisionSound(entityB, collision); + // since we're swapping A and B we need to send the inverted collision + Collision invertedCollision(collision); + invertedCollision.invert(); + emit collisionWithEntity(idB, idA, invertedCollision); if (_entitiesScriptEngine) { - _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, collision); + _entitiesScriptEngine->callEntityScriptMethod(idB, "collisionWithEntity", idA, invertedCollision); } } } diff --git a/libraries/entities-renderer/src/EntityTreeRenderer.h b/libraries/entities-renderer/src/EntityTreeRenderer.h index 8c021ad184..29d463b915 100644 --- a/libraries/entities-renderer/src/EntityTreeRenderer.h +++ b/libraries/entities-renderer/src/EntityTreeRenderer.h @@ -170,11 +170,7 @@ private: bool _wantScripts; QSharedPointer _entitiesScriptEngine; - bool isCollisionOwner(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision); - - void playEntityCollisionSound(const QUuid& myNodeID, EntityTreePointer entityTree, - const EntityItemID& id, const Collision& collision); + static void playEntityCollisionSound(EntityItemPointer entity, const Collision& collision); bool _lastPointerEventValid; PointerEvent _lastPointerEvent; diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp index 46bc46adab..6fb5d14329 100644 --- a/libraries/entities/src/EntityScriptingInterface.cpp +++ b/libraries/entities/src/EntityScriptingInterface.cpp @@ -1367,12 +1367,14 @@ bool EntityScriptingInterface::isChildOfParent(QUuid childID, QUuid parentID) { _entityTree->withReadLock([&] { EntityItemPointer parent = _entityTree->findEntityByEntityItemID(parentID); - parent->forEachDescendant([&](SpatiallyNestablePointer descendant) { - if(descendant->getID() == childID) { - isChild = true; - return; - } - }); + if (parent) { + parent->forEachDescendant([&](SpatiallyNestablePointer descendant) { + if (descendant->getID() == childID) { + isChild = true; + return; + } + }); + } }); return isChild; diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp index e75c5442b2..5fa86f6745 100644 --- a/libraries/entities/src/EntityTree.cpp +++ b/libraries/entities/src/EntityTree.cpp @@ -1560,6 +1560,8 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra return args->map->value(oldID); } EntityItemID newID = QUuid::createUuid(); + args->map->insert(oldID, newID); + EntityItemProperties properties = item->getProperties(); EntityItemID oldParentID = properties.getParentID(); if (oldParentID.isInvalidID()) { // no parent @@ -1575,6 +1577,43 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra } } + if (!properties.getXNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXNNeighborID()); + if (neighborEntity) { + properties.setXNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getXPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getXPNeighborID()); + if (neighborEntity) { + properties.setXPNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getYNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYNNeighborID()); + if (neighborEntity) { + properties.setYNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getYPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getYPNeighborID()); + if (neighborEntity) { + properties.setYPNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getZNNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZNNeighborID()); + if (neighborEntity) { + properties.setZNNeighborID(getMapped(neighborEntity)); + } + } + if (!properties.getZPNeighborID().isInvalidID()) { + auto neighborEntity = args->ourTree->findEntityByEntityItemID(properties.getZPNeighborID()); + if (neighborEntity) { + properties.setZPNeighborID(getMapped(neighborEntity)); + } + } + // set creation time to "now" for imported entities properties.setCreated(usecTimestampNow()); @@ -1592,7 +1631,6 @@ bool EntityTree::sendEntitiesOperation(OctreeElementPointer element, void* extra args->otherTree->addEntity(newID, properties); }); } - args->map->insert(oldID, newID); return newID; }; diff --git a/libraries/physics/src/ContactInfo.cpp b/libraries/physics/src/ContactInfo.cpp index c2ea6e8671..085f746a73 100644 --- a/libraries/physics/src/ContactInfo.cpp +++ b/libraries/physics/src/ContactInfo.cpp @@ -13,15 +13,25 @@ void ContactInfo::update(uint32_t currentStep, const btManifoldPoint& p) { _lastStep = currentStep; - ++_numSteps; positionWorldOnB = p.m_positionWorldOnB; normalWorldOnB = p.m_normalWorldOnB; distance = p.m_distance1; -} +} + +const uint32_t STEPS_BETWEEN_CONTINUE_EVENTS = 9; ContactEventType ContactInfo::computeType(uint32_t thisStep) { - if (_lastStep != thisStep) { - return CONTACT_EVENT_TYPE_END; + if (_continueExpiry == 0) { + _continueExpiry = thisStep + STEPS_BETWEEN_CONTINUE_EVENTS; + return CONTACT_EVENT_TYPE_START; } - return (_numSteps == 1) ? CONTACT_EVENT_TYPE_START : CONTACT_EVENT_TYPE_CONTINUE; + return (_lastStep == thisStep) ? CONTACT_EVENT_TYPE_CONTINUE : CONTACT_EVENT_TYPE_END; +} + +bool ContactInfo::readyForContinue(uint32_t thisStep) { + if (thisStep > _continueExpiry) { + _continueExpiry = thisStep + STEPS_BETWEEN_CONTINUE_EVENTS; + return true; + } + return false; } diff --git a/libraries/physics/src/ContactInfo.h b/libraries/physics/src/ContactInfo.h index 11c908a414..8d05f73b61 100644 --- a/libraries/physics/src/ContactInfo.h +++ b/libraries/physics/src/ContactInfo.h @@ -19,20 +19,22 @@ class ContactInfo { -public: +public: void update(uint32_t currentStep, const btManifoldPoint& p); ContactEventType computeType(uint32_t thisStep); const btVector3& getPositionWorldOnB() const { return positionWorldOnB; } btVector3 getPositionWorldOnA() const { return positionWorldOnB + normalWorldOnB * distance; } + bool readyForContinue(uint32_t thisStep); + btVector3 positionWorldOnB; btVector3 normalWorldOnB; btScalar distance; private: - uint32_t _lastStep = 0; - uint32_t _numSteps = 0; -}; + uint32_t _lastStep { 0 }; + uint32_t _continueExpiry { 0 }; +}; #endif // hifi_ContactEvent_h diff --git a/libraries/physics/src/EntityMotionState.cpp b/libraries/physics/src/EntityMotionState.cpp index 1833d0aba4..b0bdc34b52 100644 --- a/libraries/physics/src/EntityMotionState.cpp +++ b/libraries/physics/src/EntityMotionState.cpp @@ -762,6 +762,11 @@ void EntityMotionState::computeCollisionGroupAndMask(int16_t& group, int16_t& ma _entity->computeCollisionGroupAndFinalMask(group, mask); } +bool EntityMotionState::shouldBeLocallyOwned() const { + return (_outgoingPriority > VOLUNTEER_SIMULATION_PRIORITY && _outgoingPriority > _entity->getSimulationPriority()) || + _entity->getSimulatorID() == Physics::getSessionUUID(); +} + void EntityMotionState::upgradeOutgoingPriority(uint8_t priority) { _outgoingPriority = glm::max(_outgoingPriority, priority); } diff --git a/libraries/physics/src/EntityMotionState.h b/libraries/physics/src/EntityMotionState.h index 194d82805f..feac47d8ec 100644 --- a/libraries/physics/src/EntityMotionState.h +++ b/libraries/physics/src/EntityMotionState.h @@ -78,6 +78,8 @@ public: virtual void computeCollisionGroupAndMask(int16_t& group, int16_t& mask) const override; + bool shouldBeLocallyOwned() const override; + friend class PhysicalEntitySimulation; protected: diff --git a/libraries/physics/src/ObjectMotionState.h b/libraries/physics/src/ObjectMotionState.h index a7894998a8..1d258560c3 100644 --- a/libraries/physics/src/ObjectMotionState.h +++ b/libraries/physics/src/ObjectMotionState.h @@ -146,6 +146,8 @@ public: void dirtyInternalKinematicChanges() { _hasInternalKinematicChanges = true; } void clearInternalKinematicChanges() { _hasInternalKinematicChanges = false; } + virtual bool shouldBeLocallyOwned() const { return false; } + friend class PhysicsEngine; protected: diff --git a/libraries/physics/src/PhysicsEngine.cpp b/libraries/physics/src/PhysicsEngine.cpp index ba002d925c..f57be4eab3 100644 --- a/libraries/physics/src/PhysicsEngine.cpp +++ b/libraries/physics/src/PhysicsEngine.cpp @@ -270,7 +270,7 @@ void PhysicsEngine::stepSimulation() { } auto onSubStep = [this]() { - updateContactMap(); + this->updateContactMap(); }; int numSubsteps = _dynamicsWorld->stepSimulationWithSubstepCallback(timeStep, PHYSICS_ENGINE_MAX_NUM_SUBSTEPS, @@ -393,7 +393,6 @@ void PhysicsEngine::updateContactMap() { } const CollisionEvents& PhysicsEngine::getCollisionEvents() { - const uint32_t CONTINUE_EVENT_FILTER_FREQUENCY = 10; _collisionEvents.clear(); // scan known contacts and trigger events @@ -402,28 +401,42 @@ const CollisionEvents& PhysicsEngine::getCollisionEvents() { while (contactItr != _contactMap.end()) { ContactInfo& contact = contactItr->second; ContactEventType type = contact.computeType(_numContactFrames); - if(type != CONTACT_EVENT_TYPE_CONTINUE || _numSubsteps % CONTINUE_EVENT_FILTER_FREQUENCY == 0) { + const btScalar SIGNIFICANT_DEPTH = -0.002f; // penetrations have negative distance + if (type != CONTACT_EVENT_TYPE_CONTINUE || + (contact.distance < SIGNIFICANT_DEPTH && + contact.readyForContinue(_numContactFrames))) { ObjectMotionState* motionStateA = static_cast(contactItr->first._a); ObjectMotionState* motionStateB = static_cast(contactItr->first._b); - glm::vec3 velocityChange = (motionStateA ? motionStateA->getObjectLinearVelocityChange() : glm::vec3(0.0f)) + - (motionStateB ? motionStateB->getObjectLinearVelocityChange() : glm::vec3(0.0f)); - if (motionStateA) { + // NOTE: the MyAvatar RigidBody is the only object in the simulation that does NOT have a MotionState + // which means should we ever want to report ALL collision events against the avatar we can + // modify the logic below. + // + // We only create events when at least one of the objects is (or should be) owned in the local simulation. + if (motionStateA && (motionStateA->shouldBeLocallyOwned())) { QUuid idA = motionStateA->getObjectID(); QUuid idB; if (motionStateB) { idB = motionStateB->getObjectID(); } glm::vec3 position = bulletToGLM(contact.getPositionWorldOnB()) + _originOffset; + glm::vec3 velocityChange = motionStateA->getObjectLinearVelocityChange() + + (motionStateB ? motionStateB->getObjectLinearVelocityChange() : glm::vec3(0.0f)); glm::vec3 penetration = bulletToGLM(contact.distance * contact.normalWorldOnB); _collisionEvents.push_back(Collision(type, idA, idB, position, penetration, velocityChange)); - } else if (motionStateB) { + } else if (motionStateB && (motionStateB->shouldBeLocallyOwned())) { QUuid idB = motionStateB->getObjectID(); + QUuid idA; + if (motionStateA) { + idA = motionStateA->getObjectID(); + } glm::vec3 position = bulletToGLM(contact.getPositionWorldOnA()) + _originOffset; + glm::vec3 velocityChange = motionStateB->getObjectLinearVelocityChange() + + (motionStateA ? motionStateA->getObjectLinearVelocityChange() : glm::vec3(0.0f)); // NOTE: we're flipping the order of A and B (so that the first objectID is never NULL) - // hence we must negate the penetration. + // hence we negate the penetration (because penetration always points from B to A). glm::vec3 penetration = - bulletToGLM(contact.distance * contact.normalWorldOnB); - _collisionEvents.push_back(Collision(type, idB, QUuid(), position, penetration, velocityChange)); + _collisionEvents.push_back(Collision(type, idB, idA, position, penetration, velocityChange)); } } diff --git a/libraries/shared/src/RegisteredMetaTypes.cpp b/libraries/shared/src/RegisteredMetaTypes.cpp index 984529c4ba..7f12d6cc00 100644 --- a/libraries/shared/src/RegisteredMetaTypes.cpp +++ b/libraries/shared/src/RegisteredMetaTypes.cpp @@ -742,6 +742,12 @@ void collisionFromScriptValue(const QScriptValue &object, Collision& collision) // TODO: implement this when we know what it means to accept collision events from JS } +void Collision::invert() { + std::swap(idA, idB); + contactPoint += penetration; + penetration *= -1.0f; +} + QScriptValue quuidToScriptValue(QScriptEngine* engine, const QUuid& uuid) { if (uuid.isNull()) { return QScriptValue::NullValue; diff --git a/libraries/shared/src/RegisteredMetaTypes.h b/libraries/shared/src/RegisteredMetaTypes.h index 2aefd3aa47..498a8b3b3a 100644 --- a/libraries/shared/src/RegisteredMetaTypes.h +++ b/libraries/shared/src/RegisteredMetaTypes.h @@ -142,11 +142,13 @@ public: const glm::vec3& cPenetration, const glm::vec3& velocityChange) : type(cType), idA(cIdA), idB(cIdB), contactPoint(cPoint), penetration(cPenetration), velocityChange(velocityChange) { } + void invert(); // swap A and B + ContactEventType type; QUuid idA; QUuid idB; - glm::vec3 contactPoint; - glm::vec3 penetration; + glm::vec3 contactPoint; // on B in world-frame + glm::vec3 penetration; // from B towards A in world-frame glm::vec3 velocityChange; }; Q_DECLARE_METATYPE(Collision) diff --git a/script-archive/entity-server-filter-example.js b/script-archive/entity-server-filter-example.js index 4d4f7273f1..ad44bf1583 100644 --- a/script-archive/entity-server-filter-example.js +++ b/script-archive/entity-server-filter-example.js @@ -1,54 +1,106 @@ function filter(p) { - /* block comments are ok, but not double-slash end-of-line-comments */ - + /******************************************************/ + /* General Filter Comments + /* + - Custom filters must be named "filter" and must be global + - Block comments are ok, but not double-slash end-of-line-comments + - Certain JavaScript functions are not available, like Math.sign(), as they are undefined in QT's non-conforming JS + - HiFi's scripting interface is unavailable here. That means you can't call, for example, Users.*() + */ + /******************************************************/ + + /******************************************************/ + /* Simple Filter Examples + /******************************************************/ /* Simple example: if someone specifies name, add an 'x' to it. Note that print is ok to use. */ if (p.name) {p.name += 'x'; print('fixme name', p. name);} + /* This example clamps y. A better filter would probably zero y component of velocity and acceleration. */ if (p.position) {p.position.y = Math.min(1, p.position.y); print('fixme p.y', p.position.y);} - /* Can also reject altogether */ + + /* Can also reject new properties altogether by returning false */ if (p.userData) { return false; } + /* Reject if modifications made to Model properties */ if (p.modelURL || p.compoundShapeURL || p.shape || p.shapeType || p.url || p.fps || p.currentFrame || p.running || p.loop || p.firstFrame || p.lastFrame || p.hold || p.textures || p.xTextureURL || p.yTextureURL || p.zTextureURL) { return false; } + + /******************************************************/ + /* Physical Property Filter Examples + /* + NOTES about filtering physical properties: + - For now, ensure you always supply a new value for the filtered physical property + (instead of simply removing the property) + - Ensure you always specify a slightly different value for physical properties every + time your filter returns. Look to "var nearZero" below for an example). + This is necessary because Interface checks if a physical property has changed + when deciding whether to apply or reject the server's physical properties. + If a physical property's value doesn't change, Interface will reject the server's property value, + and Bullet will continue simulating the entity with stale physical properties. + Ensure that this value is not changed by such a small amount such that new values + fall within floating point precision boundaries. If you accidentally do this, prepare for many + hours of frustrating debugging :). + */ + /******************************************************/ /* Clamp velocity to maxVelocity units/second. Zeroing each component of acceleration keeps us from slamming.*/ - var maxVelocity = 5; if (p.velocity) { + var maxVelocity = 5; + /* Random near-zero value used as "zero" to prevent two sequential updates from being + exactly the same (which would cause them to be ignored) */ + var nearZero = 0.0001 * Math.random() + 0.001; + function sign(val) { + if (val > 0) { + return 1; + } else if (val < 0) { + return -1; + } else { + return 0; + } + } if (Math.abs(p.velocity.x) > maxVelocity) { - p.velocity.x = Math.sign(p.velocity.x) * maxVelocity; - p.acceleration.x = 0; + p.velocity.x = sign(p.velocity.x) * (maxVelocity + nearZero); + p.acceleration.x = nearZero; } if (Math.abs(p.velocity.y) > maxVelocity) { - p.velocity.y = Math.sign(p.velocity.y) * maxVelocity; - p.acceleration.y = 0; + p.velocity.y = sign(p.velocity.y) * (maxVelocity + nearZero); + p.acceleration.y = nearZero; } if (Math.abs(p.velocity.z) > maxVelocity) { - p.velocity.z = Math.sign(p.velocity.z) * maxVelocity; - p.acceleration.z = 0; + p.velocity.z = sign(p.velocity.z) * (maxVelocity + nearZero); + p.acceleration.z = nearZero; } } + /* Define an axis-aligned zone in which entities are not allowed to enter. */ /* This example zone corresponds to an area to the right of the spawnpoint in your Sandbox. It's an area near the big rock to the right. If an entity enters the zone, it'll move behind the rock.*/ - var boxMin = {x: 25.5, y: -0.48, z: -9.9}; - var boxMax = {x: 31.1, y: 4, z: -3.79}; - var zero = {x: 0.0, y: 0.0, z: 0.0}; - if (p.position) { + /* Random near-zero value used as "zero" to prevent two sequential updates from being + exactly the same (which would cause them to be ignored) */ + var nearZero = 0.0001 * Math.random() + 0.001; + /* Define the points that create the "NO ENTITIES ALLOWED" box */ + var boxMin = {x: 25.5, y: -0.48, z: -9.9}; + var boxMax = {x: 31.1, y: 4, z: -3.79}; + /* Define the point that you want entites that enter the box to appear */ + var resetPoint = {x: 29.5, y: 0.37 + nearZero, z: -2}; var x = p.position.x; var y = p.position.y; var z = p.position.z; if ((x > boxMin.x && x < boxMax.x) && (y > boxMin.y && y < boxMax.y) && (z > boxMin.z && z < boxMax.z)) { - /* Move it to the origin of the zone */ - p.position = boxMin; - p.velocity = zero; - p.acceleration = zero; + p.position = resetPoint; + if (p.velocity) { + p.velocity = {x: 0, y: nearZero, z: 0}; + } + if (p.acceleration) { + p.acceleration = {x: 0, y: nearZero, z: 0}; + } } }