From 62b21f52af68266d14f744742930c2cbc623b17b Mon Sep 17 00:00:00 2001 From: Philip Rosedale Date: Tue, 16 Sep 2014 01:18:42 -0700 Subject: [PATCH] fix for sharp change in attenuation when too near a source --- assignment-client/src/audio/AudioMixer.cpp | 243 ++++++++++----------- examples/playSoundLoop.js | 9 +- 2 files changed, 119 insertions(+), 133 deletions(-) diff --git a/assignment-client/src/audio/AudioMixer.cpp b/assignment-client/src/audio/AudioMixer.cpp index 714cd0e3f3..ca3a241f6c 100644 --- a/assignment-client/src/audio/AudioMixer.cpp +++ b/assignment-client/src/audio/AudioMixer.cpp @@ -102,7 +102,7 @@ AudioMixer::~AudioMixer() { const float ATTENUATION_BEGINS_AT_DISTANCE = 1.0f; const float ATTENUATION_AMOUNT_PER_DOUBLING_IN_DISTANCE = 0.18f; -const float ATTENUATION_EPSILON_DISTANCE = 0.1f; +const float RADIUS_OF_HEAD = 0.076f; int AudioMixer::addStreamToMixForListeningNodeWithStream(PositionalAudioStream* streamToAdd, AvatarAudioStream* listeningNodeStream) { @@ -112,6 +112,8 @@ int AudioMixer::addStreamToMixForListeningNodeWithStream(PositionalAudioStream* // Basically, we'll repeat that last frame until it has a frame to mix. Depending on how many times // we've repeated that frame in a row, we'll gradually fade that repeated frame into silence. // This improves the perceived quality of the audio slightly. + + bool showDebug = false; // (randFloat() < 0.05f); float repeatedFrameFadeFactor = 1.0f; @@ -140,112 +142,109 @@ int AudioMixer::addStreamToMixForListeningNodeWithStream(PositionalAudioStream* int numSamplesDelay = 0; float weakChannelAmplitudeRatio = 1.0f; - bool shouldAttenuate = (streamToAdd != listeningNodeStream); + bool shouldDistanceAttenuate = true; - if (shouldAttenuate) { + // if the two stream pointers do not match then these are different streams + glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream->getPosition(); + + float distanceBetween = glm::length(relativePosition); + + if (distanceBetween < EPSILON) { + distanceBetween = EPSILON; + } + + if (streamToAdd->getLastPopOutputTrailingLoudness() / distanceBetween <= _minAudibilityThreshold) { + // according to mixer performance we have decided this does not get to be mixed in + // bail out + return 0; + } + + ++_sumMixes; + + if (streamToAdd->getListenerUnattenuatedZone()) { + shouldDistanceAttenuate = !streamToAdd->getListenerUnattenuatedZone()->contains(listeningNodeStream->getPosition()); + } + + if (streamToAdd->getType() == PositionalAudioStream::Injector) { + attenuationCoefficient *= reinterpret_cast(streamToAdd)->getAttenuationRatio(); + if (showDebug) qDebug() << "AttenuationRatio: " << reinterpret_cast(streamToAdd)->getAttenuationRatio(); + } + + if (showDebug) { + qDebug() << "distance: " << distanceBetween; + } + + glm::quat inverseOrientation = glm::inverse(listeningNodeStream->getOrientation()); + + if (streamToAdd->getType() != PositionalAudioStream::Injector) { + // source is another avatar, apply fixed off-axis attenuation to make them quieter as they turn away from listener + glm::vec3 rotatedListenerPosition = glm::inverse(streamToAdd->getOrientation()) * relativePosition; - // if the two stream pointers do not match then these are different streams - glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream->getPosition(); + float angleOfDelivery = glm::angle(glm::vec3(0.0f, 0.0f, -1.0f), + glm::normalize(rotatedListenerPosition)); - float distanceBetween = glm::length(relativePosition); + const float MAX_OFF_AXIS_ATTENUATION = 0.2f; + const float OFF_AXIS_ATTENUATION_FORMULA_STEP = (1 - MAX_OFF_AXIS_ATTENUATION) / 2.0f; - if (distanceBetween < EPSILON) { - distanceBetween = EPSILON; - } + float offAxisCoefficient = MAX_OFF_AXIS_ATTENUATION + + (OFF_AXIS_ATTENUATION_FORMULA_STEP * (angleOfDelivery / PI_OVER_TWO)); - if (streamToAdd->getLastPopOutputTrailingLoudness() / distanceBetween <= _minAudibilityThreshold) { - // according to mixer performance we have decided this does not get to be mixed in - // bail out - return 0; - } - - ++_sumMixes; - - if (streamToAdd->getListenerUnattenuatedZone()) { - shouldAttenuate = !streamToAdd->getListenerUnattenuatedZone()->contains(listeningNodeStream->getPosition()); - } - - if (streamToAdd->getType() == PositionalAudioStream::Injector) { - attenuationCoefficient *= reinterpret_cast(streamToAdd)->getAttenuationRatio(); - } - - shouldAttenuate = shouldAttenuate && distanceBetween > ATTENUATION_EPSILON_DISTANCE; - - if (shouldAttenuate) { - glm::quat inverseOrientation = glm::inverse(listeningNodeStream->getOrientation()); + if (showDebug) { + qDebug() << "angleOfDelivery" << angleOfDelivery << "offAxisCoefficient: " << offAxisCoefficient; - float distanceSquareToSource = glm::dot(relativePosition, relativePosition); - float radius = 0.0f; - - if (streamToAdd->getType() == PositionalAudioStream::Injector) { - radius = reinterpret_cast(streamToAdd)->getRadius(); - } - - if (radius == 0 || (distanceSquareToSource > radius * radius)) { - // this is either not a spherical source, or the listener is outside the sphere - - if (radius > 0) { - // this is a spherical source - the distance used for the coefficient - // needs to be the closest point on the boundary to the source - - // ovveride the distance to the node with the distance to the point on the - // boundary of the sphere - distanceSquareToSource -= (radius * radius); - - } else { - // calculate the angle delivery for off-axis attenuation - glm::vec3 rotatedListenerPosition = glm::inverse(streamToAdd->getOrientation()) * relativePosition; - - float angleOfDelivery = glm::angle(glm::vec3(0.0f, 0.0f, -1.0f), - glm::normalize(rotatedListenerPosition)); - - const float MAX_OFF_AXIS_ATTENUATION = 0.2f; - const float OFF_AXIS_ATTENUATION_FORMULA_STEP = (1 - MAX_OFF_AXIS_ATTENUATION) / 2.0f; - - float offAxisCoefficient = MAX_OFF_AXIS_ATTENUATION + - (OFF_AXIS_ATTENUATION_FORMULA_STEP * (angleOfDelivery / PI_OVER_TWO)); - - // multiply the current attenuation coefficient by the calculated off axis coefficient - attenuationCoefficient *= offAxisCoefficient; - } - - glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; - - if (distanceBetween >= ATTENUATION_BEGINS_AT_DISTANCE) { - // calculate the distance coefficient using the distance to this node - float distanceCoefficient = 1 - (logf(distanceBetween / ATTENUATION_BEGINS_AT_DISTANCE) / logf(2.0f) - * ATTENUATION_AMOUNT_PER_DOUBLING_IN_DISTANCE); - - if (distanceCoefficient < 0) { - distanceCoefficient = 0; - } - - // multiply the current attenuation coefficient by the distance coefficient - attenuationCoefficient *= distanceCoefficient; - } - - // project the rotated source position vector onto the XZ plane - rotatedSourcePosition.y = 0.0f; - - // produce an oriented angle about the y-axis - bearingRelativeAngleToSource = glm::orientedAngle(glm::vec3(0.0f, 0.0f, -1.0f), - glm::normalize(rotatedSourcePosition), - glm::vec3(0.0f, 1.0f, 0.0f)); - - const float PHASE_AMPLITUDE_RATIO_AT_90 = 0.5; - - // figure out the number of samples of delay and the ratio of the amplitude - // in the weak channel for audio spatialization - float sinRatio = fabsf(sinf(bearingRelativeAngleToSource)); - numSamplesDelay = SAMPLE_PHASE_DELAY_AT_90 * sinRatio; - weakChannelAmplitudeRatio = 1 - (PHASE_AMPLITUDE_RATIO_AT_90 * sinRatio); - } } + // multiply the current attenuation coefficient by the calculated off axis coefficient + + attenuationCoefficient *= offAxisCoefficient; + } + + if (shouldDistanceAttenuate && (distanceBetween >= ATTENUATION_BEGINS_AT_DISTANCE)) { + // calculate the distance coefficient using the distance to this node + float distanceCoefficient = 1 - (logf(distanceBetween / ATTENUATION_BEGINS_AT_DISTANCE) / logf(2.0f) + * ATTENUATION_AMOUNT_PER_DOUBLING_IN_DISTANCE); + + if (distanceCoefficient < 0) { + distanceCoefficient = 0; + } + + // multiply the current attenuation coefficient by the distance coefficient + attenuationCoefficient *= distanceCoefficient; + if (showDebug) qDebug() << "distanceCoefficient: " << distanceCoefficient; + } + + // Compute sample delay for the two ears to create phase panning + glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; + + // project the rotated source position vector onto the XZ plane + rotatedSourcePosition.y = 0.0f; + + // produce an oriented angle about the y-axis + bearingRelativeAngleToSource = glm::orientedAngle(glm::vec3(0.0f, 0.0f, -1.0f), + glm::normalize(rotatedSourcePosition), + glm::vec3(0.0f, 1.0f, 0.0f)); + + const float PHASE_AMPLITUDE_RATIO_AT_90 = 0.5; + + // figure out the number of samples of delay and the ratio of the amplitude + // in the weak channel for audio spatialization + float sinRatio = fabsf(sinf(bearingRelativeAngleToSource)); + numSamplesDelay = SAMPLE_PHASE_DELAY_AT_90 * sinRatio; + weakChannelAmplitudeRatio = 1 - (PHASE_AMPLITUDE_RATIO_AT_90 * sinRatio); + + if (distanceBetween < RADIUS_OF_HEAD) { + // Diminish phase panning if source would be inside head + numSamplesDelay *= distanceBetween / RADIUS_OF_HEAD; + weakChannelAmplitudeRatio += (PHASE_AMPLITUDE_RATIO_AT_90 * sinRatio) * distanceBetween / RADIUS_OF_HEAD; + } + + if (showDebug) { + qDebug() << "attenuation: " << attenuationCoefficient; + qDebug() << "bearingRelativeAngleToSource: " << bearingRelativeAngleToSource << " numSamplesDelay: " << numSamplesDelay; } AudioRingBuffer::ConstIterator streamPopOutput = streamToAdd->getLastPopOutput(); - if (!streamToAdd->isStereo() && shouldAttenuate) { + if (!streamToAdd->isStereo()) { // this is a mono stream, which means it gets full attenuation and spatialization // if the bearing relative angle to source is > 0 then the delayed channel is the right one @@ -293,11 +292,7 @@ int AudioMixer::addStreamToMixForListeningNodeWithStream(PositionalAudioStream* } else { int stereoDivider = streamToAdd->isStereo() ? 1 : 2; - if (!shouldAttenuate) { - attenuationCoefficient = 1.0f; - } - - float attenuationAndFade = attenuationCoefficient * repeatedFrameFadeFactor; + float attenuationAndFade = attenuationCoefficient * repeatedFrameFadeFactor; for (int s = 0; s < NETWORK_BUFFER_LENGTH_SAMPLES_STEREO; s++) { _clientSamples[s] = glm::clamp(_clientSamples[s] + (int)(streamPopOutput[s / stereoDivider] * attenuationAndFade), @@ -305,19 +300,8 @@ int AudioMixer::addStreamToMixForListeningNodeWithStream(PositionalAudioStream* } } - if (_enableFilter && shouldAttenuate) { + if (_enableFilter) { - glm::vec3 relativePosition = streamToAdd->getPosition() - listeningNodeStream->getPosition(); - glm::quat inverseOrientation = glm::inverse(listeningNodeStream->getOrientation()); - glm::vec3 rotatedSourcePosition = inverseOrientation * relativePosition; - - // project the rotated source position vector onto the XZ plane - rotatedSourcePosition.y = 0.0f; - - // produce an oriented angle about the y-axis - float bearingAngleToSource = glm::orientedAngle(glm::vec3(0.0f, 0.0f, -1.0f), - glm::normalize(rotatedSourcePosition), - glm::vec3(0.0f, -1.0f, 0.0f)); const float TWO_OVER_PI = 2.0f / PI; const float ZERO_DB = 1.0f; @@ -337,36 +321,41 @@ int AudioMixer::addStreamToMixForListeningNodeWithStream(PositionalAudioStream* float penumbraFilterGainL; float penumbraFilterGainR; - // variable gain calculation broken down by quadrent - if (bearingAngleToSource < -PI_OVER_TWO && bearingAngleToSource > -PI) { + // variable gain calculation broken down by quadrant + if (-bearingRelativeAngleToSource < -PI_OVER_TWO && -bearingRelativeAngleToSource > -PI) { penumbraFilterGainL = TWO_OVER_PI * - (FILTER_GAIN_AT_0 - FILTER_GAIN_AT_180) * (bearingAngleToSource + PI_OVER_TWO) + FILTER_GAIN_AT_0; + (FILTER_GAIN_AT_0 - FILTER_GAIN_AT_180) * (-bearingRelativeAngleToSource + PI_OVER_TWO) + FILTER_GAIN_AT_0; penumbraFilterGainR = TWO_OVER_PI * - (FILTER_GAIN_AT_90 - FILTER_GAIN_AT_180) * (bearingAngleToSource + PI_OVER_TWO) + FILTER_GAIN_AT_90; - } else if (bearingAngleToSource <= PI && bearingAngleToSource > PI_OVER_TWO) { + (FILTER_GAIN_AT_90 - FILTER_GAIN_AT_180) * (-bearingRelativeAngleToSource + PI_OVER_TWO) + FILTER_GAIN_AT_90; + } else if (-bearingRelativeAngleToSource <= PI && -bearingRelativeAngleToSource > PI_OVER_TWO) { penumbraFilterGainL = TWO_OVER_PI * - (FILTER_GAIN_AT_180 - FILTER_GAIN_AT_90) * (bearingAngleToSource - PI) + FILTER_GAIN_AT_180; + (FILTER_GAIN_AT_180 - FILTER_GAIN_AT_90) * (-bearingRelativeAngleToSource - PI) + FILTER_GAIN_AT_180; penumbraFilterGainR = TWO_OVER_PI * - (FILTER_GAIN_AT_180 - FILTER_GAIN_AT_0) * (bearingAngleToSource - PI) + FILTER_GAIN_AT_180; - } else if (bearingAngleToSource <= PI_OVER_TWO && bearingAngleToSource > 0) { + (FILTER_GAIN_AT_180 - FILTER_GAIN_AT_0) * (-bearingRelativeAngleToSource - PI) + FILTER_GAIN_AT_180; + } else if (-bearingRelativeAngleToSource <= PI_OVER_TWO && -bearingRelativeAngleToSource > 0) { penumbraFilterGainL = TWO_OVER_PI * - (FILTER_GAIN_AT_90 - FILTER_GAIN_AT_0) * (bearingAngleToSource - PI_OVER_TWO) + FILTER_GAIN_AT_90; + (FILTER_GAIN_AT_90 - FILTER_GAIN_AT_0) * (-bearingRelativeAngleToSource - PI_OVER_TWO) + FILTER_GAIN_AT_90; penumbraFilterGainR = FILTER_GAIN_AT_0; } else { penumbraFilterGainL = FILTER_GAIN_AT_0; penumbraFilterGainR = TWO_OVER_PI * - (FILTER_GAIN_AT_0 - FILTER_GAIN_AT_90) * (bearingAngleToSource) + FILTER_GAIN_AT_0; + (FILTER_GAIN_AT_0 - FILTER_GAIN_AT_90) * (-bearingRelativeAngleToSource) + FILTER_GAIN_AT_0; + } + + if (distanceBetween < RADIUS_OF_HEAD) { + // Diminish effect if source would be inside head + penumbraFilterGainL += (1.f - penumbraFilterGainL) * (1.f - distanceBetween / RADIUS_OF_HEAD); + penumbraFilterGainR += (1.f - penumbraFilterGainR) * (1.f - distanceBetween / RADIUS_OF_HEAD); } + #if 0 - qDebug() << "avatar=" - << listeningNodeStream - << "gainL=" + qDebug() << "gainL=" << penumbraFilterGainL << "gainR=" << penumbraFilterGainR << "angle=" - << bearingAngleToSource; + << -bearingRelativeAngleToSource; #endif // set the gain on both filter channels diff --git a/examples/playSoundLoop.js b/examples/playSoundLoop.js index 9139494bbf..30f8762248 100644 --- a/examples/playSoundLoop.js +++ b/examples/playSoundLoop.js @@ -14,9 +14,9 @@ // A few sample files you may want to try: -//var sound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Guitars/Guitar+-+Nylon+A.raw"); +var sound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Guitars/Guitar+-+Nylon+A.raw"); //var sound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/220Sine.wav"); -var sound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Cocktail+Party+Snippets/Bandcamp.wav"); +//var sound = new Sound("https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Cocktail+Party+Snippets/Bandcamp.wav"); var soundPlaying = false; var options = new AudioInjectionOptions(); @@ -31,10 +31,7 @@ function maybePlaySound(deltaTime) { var properties = { type: "Sphere", position: options.position, - velocity: { x: 0, y: 0, z: 0}, - gravity: { x: 0, y: 0, z: 0}, - dimensions: { x: 0.2, y: 0.2, z: 0.2 }, - damping: 0.999, + dimensions: { x: 0.2, y: 0.2, z: 0.2 }, color: { red: 200, green: 0, blue: 0 } }; ball = Entities.addEntity(properties);