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