1/* 2 * Copyright (C) 2010, Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions 6 * are met: 7 * 1. Redistributions of source code must retain the above copyright 8 * notice, this list of conditions and the following disclaimer. 9 * 2. Redistributions in binary form must reproduce the above copyright 10 * notice, this list of conditions and the following disclaimer in the 11 * documentation and/or other materials provided with the distribution. 12 * 13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY 14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY 17 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 */ 24 25#include "config.h" 26 27#if ENABLE(WEB_AUDIO) 28 29#include "modules/webaudio/PannerNode.h" 30 31#include "core/dom/ExecutionContext.h" 32#include "platform/audio/HRTFPanner.h" 33#include "modules/webaudio/AudioBufferSourceNode.h" 34#include "modules/webaudio/AudioContext.h" 35#include "modules/webaudio/AudioNodeInput.h" 36#include "modules/webaudio/AudioNodeOutput.h" 37#include "wtf/MathExtras.h" 38 39namespace blink { 40 41static void fixNANs(double &x) 42{ 43 if (std::isnan(x) || std::isinf(x)) 44 x = 0.0; 45} 46 47PannerNode::PannerNode(AudioContext* context, float sampleRate) 48 : AudioNode(context, sampleRate) 49 , m_panningModel(Panner::PanningModelHRTF) 50 , m_distanceModel(DistanceEffect::ModelInverse) 51 , m_position(0, 0, 0) 52 , m_orientation(1, 0, 0) 53 , m_velocity(0, 0, 0) 54 , m_isAzimuthElevationDirty(true) 55 , m_isDistanceConeGainDirty(true) 56 , m_isDopplerRateDirty(true) 57 , m_lastGain(-1.0) 58 , m_cachedAzimuth(0) 59 , m_cachedElevation(0) 60 , m_cachedDistanceConeGain(1.0f) 61 , m_cachedDopplerRate(1) 62 , m_connectionCount(0) 63{ 64 // Load the HRTF database asynchronously so we don't block the Javascript thread while creating the HRTF database. 65 // The HRTF panner will return zeroes until the database is loaded. 66 listener()->createAndLoadHRTFDatabaseLoader(context->sampleRate()); 67 68 addInput(); 69 addOutput(AudioNodeOutput::create(this, 2)); 70 71 // Node-specific default mixing rules. 72 m_channelCount = 2; 73 m_channelCountMode = ClampedMax; 74 m_channelInterpretation = AudioBus::Speakers; 75 76 setNodeType(NodeTypePanner); 77 78 initialize(); 79} 80 81PannerNode::~PannerNode() 82{ 83 ASSERT(!isInitialized()); 84} 85 86void PannerNode::dispose() 87{ 88 uninitialize(); 89 AudioNode::dispose(); 90} 91 92void PannerNode::pullInputs(size_t framesToProcess) 93{ 94 // We override pullInputs(), so we can detect new AudioSourceNodes which have connected to us when new connections are made. 95 // These AudioSourceNodes need to be made aware of our existence in order to handle doppler shift pitch changes. 96 if (m_connectionCount != context()->connectionCount()) { 97 m_connectionCount = context()->connectionCount(); 98 99 // A map for keeping track if we have visited a node or not. This prevents feedback loops 100 // from recursing infinitely. See crbug.com/331446. 101 HashMap<AudioNode*, bool> visitedNodes; 102 103 // Recursively go through all nodes connected to us 104 notifyAudioSourcesConnectedToNode(this, visitedNodes); 105 } 106 107 AudioNode::pullInputs(framesToProcess); 108} 109 110void PannerNode::process(size_t framesToProcess) 111{ 112 AudioBus* destination = output(0)->bus(); 113 114 if (!isInitialized() || !input(0)->isConnected() || !m_panner.get()) { 115 destination->zero(); 116 return; 117 } 118 119 AudioBus* source = input(0)->bus(); 120 if (!source) { 121 destination->zero(); 122 return; 123 } 124 125 // The audio thread can't block on this lock, so we call tryLock() instead. 126 MutexTryLocker tryLocker(m_processLock); 127 MutexTryLocker tryListenerLocker(listener()->listenerLock()); 128 129 if (tryLocker.locked() && tryListenerLocker.locked()) { 130 // HRTFDatabase should be loaded before proceeding for offline audio context when the panning model is HRTF. 131 if (m_panningModel == Panner::PanningModelHRTF && !listener()->isHRTFDatabaseLoaded()) { 132 if (context()->isOfflineContext()) { 133 listener()->waitForHRTFDatabaseLoaderThreadCompletion(); 134 } else { 135 destination->zero(); 136 return; 137 } 138 } 139 140 // Apply the panning effect. 141 double azimuth; 142 double elevation; 143 azimuthElevation(&azimuth, &elevation); 144 145 m_panner->pan(azimuth, elevation, source, destination, framesToProcess); 146 147 // Get the distance and cone gain. 148 float totalGain = distanceConeGain(); 149 150 // Snap to desired gain at the beginning. 151 if (m_lastGain == -1.0) 152 m_lastGain = totalGain; 153 154 // Apply gain in-place with de-zippering. 155 destination->copyWithGainFrom(*destination, &m_lastGain, totalGain); 156 } else { 157 // Too bad - The tryLock() failed. 158 // We must be in the middle of changing the properties of the panner or the listener. 159 destination->zero(); 160 } 161} 162 163void PannerNode::initialize() 164{ 165 if (isInitialized()) 166 return; 167 168 m_panner = Panner::create(m_panningModel, sampleRate(), listener()->hrtfDatabaseLoader()); 169 listener()->addPanner(this); 170 171 AudioNode::initialize(); 172} 173 174void PannerNode::uninitialize() 175{ 176 if (!isInitialized()) 177 return; 178 179 m_panner.clear(); 180 listener()->removePanner(this); 181 182 AudioNode::uninitialize(); 183} 184 185AudioListener* PannerNode::listener() 186{ 187 return context()->listener(); 188} 189 190String PannerNode::panningModel() const 191{ 192 switch (m_panningModel) { 193 case Panner::PanningModelEqualPower: 194 return "equalpower"; 195 case Panner::PanningModelHRTF: 196 return "HRTF"; 197 default: 198 ASSERT_NOT_REACHED(); 199 return "HRTF"; 200 } 201} 202 203void PannerNode::setPanningModel(const String& model) 204{ 205 if (model == "equalpower") 206 setPanningModel(Panner::PanningModelEqualPower); 207 else if (model == "HRTF") 208 setPanningModel(Panner::PanningModelHRTF); 209} 210 211bool PannerNode::setPanningModel(unsigned model) 212{ 213 switch (model) { 214 case Panner::PanningModelEqualPower: 215 case Panner::PanningModelHRTF: 216 if (!m_panner.get() || model != m_panningModel) { 217 // This synchronizes with process(). 218 MutexLocker processLocker(m_processLock); 219 m_panner = Panner::create(model, sampleRate(), listener()->hrtfDatabaseLoader()); 220 m_panningModel = model; 221 } 222 break; 223 default: 224 ASSERT_NOT_REACHED(); 225 return false; 226 } 227 228 return true; 229} 230 231String PannerNode::distanceModel() const 232{ 233 switch (const_cast<PannerNode*>(this)->m_distanceEffect.model()) { 234 case DistanceEffect::ModelLinear: 235 return "linear"; 236 case DistanceEffect::ModelInverse: 237 return "inverse"; 238 case DistanceEffect::ModelExponential: 239 return "exponential"; 240 default: 241 ASSERT_NOT_REACHED(); 242 return "inverse"; 243 } 244} 245 246void PannerNode::setDistanceModel(const String& model) 247{ 248 if (model == "linear") 249 setDistanceModel(DistanceEffect::ModelLinear); 250 else if (model == "inverse") 251 setDistanceModel(DistanceEffect::ModelInverse); 252 else if (model == "exponential") 253 setDistanceModel(DistanceEffect::ModelExponential); 254} 255 256bool PannerNode::setDistanceModel(unsigned model) 257{ 258 switch (model) { 259 case DistanceEffect::ModelLinear: 260 case DistanceEffect::ModelInverse: 261 case DistanceEffect::ModelExponential: 262 if (model != m_distanceModel) { 263 // This synchronizes with process(). 264 MutexLocker processLocker(m_processLock); 265 m_distanceEffect.setModel(static_cast<DistanceEffect::ModelType>(model), true); 266 m_distanceModel = model; 267 } 268 break; 269 default: 270 ASSERT_NOT_REACHED(); 271 return false; 272 } 273 274 return true; 275} 276 277void PannerNode::setRefDistance(double distance) 278{ 279 if (refDistance() == distance) 280 return; 281 282 // This synchronizes with process(). 283 MutexLocker processLocker(m_processLock); 284 m_distanceEffect.setRefDistance(distance); 285 markPannerAsDirty(PannerNode::DistanceConeGainDirty); 286} 287 288void PannerNode::setMaxDistance(double distance) 289{ 290 if (maxDistance() == distance) 291 return; 292 293 // This synchronizes with process(). 294 MutexLocker processLocker(m_processLock); 295 m_distanceEffect.setMaxDistance(distance); 296 markPannerAsDirty(PannerNode::DistanceConeGainDirty); 297} 298 299void PannerNode::setRolloffFactor(double factor) 300{ 301 if (rolloffFactor() == factor) 302 return; 303 304 // This synchronizes with process(). 305 MutexLocker processLocker(m_processLock); 306 m_distanceEffect.setRolloffFactor(factor); 307 markPannerAsDirty(PannerNode::DistanceConeGainDirty); 308} 309 310void PannerNode::setConeInnerAngle(double angle) 311{ 312 if (coneInnerAngle() == angle) 313 return; 314 315 // This synchronizes with process(). 316 MutexLocker processLocker(m_processLock); 317 m_coneEffect.setInnerAngle(angle); 318 markPannerAsDirty(PannerNode::DistanceConeGainDirty); 319} 320 321void PannerNode::setConeOuterAngle(double angle) 322{ 323 if (coneOuterAngle() == angle) 324 return; 325 326 // This synchronizes with process(). 327 MutexLocker processLocker(m_processLock); 328 m_coneEffect.setOuterAngle(angle); 329 markPannerAsDirty(PannerNode::DistanceConeGainDirty); 330} 331 332void PannerNode::setConeOuterGain(double angle) 333{ 334 if (coneOuterGain() == angle) 335 return; 336 337 // This synchronizes with process(). 338 MutexLocker processLocker(m_processLock); 339 m_coneEffect.setOuterGain(angle); 340 markPannerAsDirty(PannerNode::DistanceConeGainDirty); 341} 342 343void PannerNode::setPosition(float x, float y, float z) 344{ 345 FloatPoint3D position = FloatPoint3D(x, y, z); 346 347 if (m_position == position) 348 return; 349 350 // This synchronizes with process(). 351 MutexLocker processLocker(m_processLock); 352 m_position = position; 353 markPannerAsDirty(PannerNode::AzimuthElevationDirty | PannerNode::DistanceConeGainDirty | PannerNode::DopplerRateDirty); 354} 355 356void PannerNode::setOrientation(float x, float y, float z) 357{ 358 FloatPoint3D orientation = FloatPoint3D(x, y, z); 359 360 if (m_orientation == orientation) 361 return; 362 363 // This synchronizes with process(). 364 MutexLocker processLocker(m_processLock); 365 m_orientation = orientation; 366 markPannerAsDirty(PannerNode::DistanceConeGainDirty); 367} 368 369void PannerNode::setVelocity(float x, float y, float z) 370{ 371 FloatPoint3D velocity = FloatPoint3D(x, y, z); 372 373 if (m_velocity == velocity) 374 return; 375 376 // This synchronizes with process(). 377 MutexLocker processLocker(m_processLock); 378 m_velocity = velocity; 379 markPannerAsDirty(PannerNode::DopplerRateDirty); 380} 381 382void PannerNode::calculateAzimuthElevation(double* outAzimuth, double* outElevation) 383{ 384 double azimuth = 0.0; 385 386 // Calculate the source-listener vector 387 FloatPoint3D listenerPosition = listener()->position(); 388 FloatPoint3D sourceListener = m_position - listenerPosition; 389 390 // normalize() does nothing if the length of |sourceListener| is zero. 391 sourceListener.normalize(); 392 393 // Align axes 394 FloatPoint3D listenerFront = listener()->orientation(); 395 FloatPoint3D listenerUp = listener()->upVector(); 396 FloatPoint3D listenerRight = listenerFront.cross(listenerUp); 397 listenerRight.normalize(); 398 399 FloatPoint3D listenerFrontNorm = listenerFront; 400 listenerFrontNorm.normalize(); 401 402 FloatPoint3D up = listenerRight.cross(listenerFrontNorm); 403 404 float upProjection = sourceListener.dot(up); 405 406 FloatPoint3D projectedSource = sourceListener - upProjection * up; 407 projectedSource.normalize(); 408 409 azimuth = 180.0 * acos(projectedSource.dot(listenerRight)) / piDouble; 410 fixNANs(azimuth); // avoid illegal values 411 412 // Source in front or behind the listener 413 double frontBack = projectedSource.dot(listenerFrontNorm); 414 if (frontBack < 0.0) 415 azimuth = 360.0 - azimuth; 416 417 // Make azimuth relative to "front" and not "right" listener vector 418 if ((azimuth >= 0.0) && (azimuth <= 270.0)) 419 azimuth = 90.0 - azimuth; 420 else 421 azimuth = 450.0 - azimuth; 422 423 // Elevation 424 double elevation = 90.0 - 180.0 * acos(sourceListener.dot(up)) / piDouble; 425 fixNANs(elevation); // avoid illegal values 426 427 if (elevation > 90.0) 428 elevation = 180.0 - elevation; 429 else if (elevation < -90.0) 430 elevation = -180.0 - elevation; 431 432 if (outAzimuth) 433 *outAzimuth = azimuth; 434 if (outElevation) 435 *outElevation = elevation; 436} 437 438double PannerNode::calculateDopplerRate() 439{ 440 double dopplerShift = 1.0; 441 double dopplerFactor = listener()->dopplerFactor(); 442 443 if (dopplerFactor > 0.0) { 444 double speedOfSound = listener()->speedOfSound(); 445 446 const FloatPoint3D &sourceVelocity = m_velocity; 447 const FloatPoint3D &listenerVelocity = listener()->velocity(); 448 449 // Don't bother if both source and listener have no velocity 450 bool sourceHasVelocity = !sourceVelocity.isZero(); 451 bool listenerHasVelocity = !listenerVelocity.isZero(); 452 453 if (sourceHasVelocity || listenerHasVelocity) { 454 // Calculate the source to listener vector 455 FloatPoint3D listenerPosition = listener()->position(); 456 FloatPoint3D sourceToListener = m_position - listenerPosition; 457 458 double sourceListenerMagnitude = sourceToListener.length(); 459 460 if (!sourceListenerMagnitude) { 461 // Source and listener are at the same position. Skip the computation of the doppler 462 // shift, and just return the cached value. 463 dopplerShift = m_cachedDopplerRate; 464 } else { 465 double listenerProjection = sourceToListener.dot(listenerVelocity) / sourceListenerMagnitude; 466 double sourceProjection = sourceToListener.dot(sourceVelocity) / sourceListenerMagnitude; 467 468 listenerProjection = -listenerProjection; 469 sourceProjection = -sourceProjection; 470 471 double scaledSpeedOfSound = speedOfSound / dopplerFactor; 472 listenerProjection = std::min(listenerProjection, scaledSpeedOfSound); 473 sourceProjection = std::min(sourceProjection, scaledSpeedOfSound); 474 475 dopplerShift = ((speedOfSound - dopplerFactor * listenerProjection) / (speedOfSound - dopplerFactor * sourceProjection)); 476 fixNANs(dopplerShift); // avoid illegal values 477 478 // Limit the pitch shifting to 4 octaves up and 3 octaves down. 479 if (dopplerShift > 16.0) 480 dopplerShift = 16.0; 481 else if (dopplerShift < 0.125) 482 dopplerShift = 0.125; 483 } 484 } 485 } 486 487 return dopplerShift; 488} 489 490float PannerNode::calculateDistanceConeGain() 491{ 492 FloatPoint3D listenerPosition = listener()->position(); 493 494 double listenerDistance = m_position.distanceTo(listenerPosition); 495 double distanceGain = m_distanceEffect.gain(listenerDistance); 496 double coneGain = m_coneEffect.gain(m_position, m_orientation, listenerPosition); 497 498 return float(distanceGain * coneGain); 499} 500 501void PannerNode::azimuthElevation(double* outAzimuth, double* outElevation) 502{ 503 ASSERT(context()->isAudioThread()); 504 505 if (isAzimuthElevationDirty()) { 506 calculateAzimuthElevation(&m_cachedAzimuth, &m_cachedElevation); 507 m_isAzimuthElevationDirty = false; 508 } 509 510 *outAzimuth = m_cachedAzimuth; 511 *outElevation = m_cachedElevation; 512} 513 514double PannerNode::dopplerRate() 515{ 516 ASSERT(context()->isAudioThread()); 517 518 if (isDopplerRateDirty()) { 519 m_cachedDopplerRate = calculateDopplerRate(); 520 m_isDopplerRateDirty = false; 521 } 522 523 return m_cachedDopplerRate; 524} 525 526float PannerNode::distanceConeGain() 527{ 528 ASSERT(context()->isAudioThread()); 529 530 if (isDistanceConeGainDirty()) { 531 m_cachedDistanceConeGain = calculateDistanceConeGain(); 532 m_isDistanceConeGainDirty = false; 533 } 534 535 return m_cachedDistanceConeGain; 536} 537 538void PannerNode::markPannerAsDirty(unsigned dirty) 539{ 540 if (dirty & PannerNode::AzimuthElevationDirty) 541 m_isAzimuthElevationDirty = true; 542 543 if (dirty & PannerNode::DistanceConeGainDirty) 544 m_isDistanceConeGainDirty = true; 545 546 if (dirty & PannerNode::DopplerRateDirty) 547 m_isDopplerRateDirty = true; 548} 549 550void PannerNode::notifyAudioSourcesConnectedToNode(AudioNode* node, HashMap<AudioNode*, bool>& visitedNodes) 551{ 552 ASSERT(node); 553 if (!node) 554 return; 555 556 // First check if this node is an AudioBufferSourceNode. If so, let it know about us so that doppler shift pitch can be taken into account. 557 if (node->nodeType() == NodeTypeAudioBufferSource) { 558 AudioBufferSourceNode* bufferSourceNode = static_cast<AudioBufferSourceNode*>(node); 559 bufferSourceNode->setPannerNode(this); 560 } else { 561 // Go through all inputs to this node. 562 for (unsigned i = 0; i < node->numberOfInputs(); ++i) { 563 AudioNodeInput* input = node->input(i); 564 565 // For each input, go through all of its connections, looking for AudioBufferSourceNodes. 566 for (unsigned j = 0; j < input->numberOfRenderingConnections(); ++j) { 567 AudioNodeOutput* connectedOutput = input->renderingOutput(j); 568 AudioNode* connectedNode = connectedOutput->node(); 569 HashMap<AudioNode*, bool>::iterator iterator = visitedNodes.find(connectedNode); 570 571 // If we've seen this node already, we don't need to process it again. Otherwise, 572 // mark it as visited and recurse through the node looking for sources. 573 if (iterator == visitedNodes.end()) { 574 visitedNodes.set(connectedNode, true); 575 notifyAudioSourcesConnectedToNode(connectedNode, visitedNodes); // recurse 576 } 577 } 578 } 579 } 580} 581 582void PannerNode::trace(Visitor* visitor) 583{ 584 visitor->trace(m_panner); 585 AudioNode::trace(visitor); 586} 587 588} // namespace blink 589 590#endif // ENABLE(WEB_AUDIO) 591