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#include "PannerNode.h"
27
28#if ENABLE(WEB_AUDIO)
29
30#include "AudioBufferSourceNode.h"
31#include "AudioBus.h"
32#include "AudioContext.h"
33#include "AudioNodeInput.h"
34#include "AudioNodeOutput.h"
35#include "HRTFPanner.h"
36#include "ScriptExecutionContext.h"
37#include <wtf/IsoMallocInlines.h>
38#include <wtf/MathExtras.h>
39
40namespace WebCore {
41
42WTF_MAKE_ISO_ALLOCATED_IMPL(PannerNode);
43
44static void fixNANs(double &x)
45{
46 if (std::isnan(x) || std::isinf(x))
47 x = 0.0;
48}
49
50PannerNode::PannerNode(AudioContext& context, float sampleRate)
51 : AudioNode(context, sampleRate)
52 , m_panningModel(PanningModelType::HRTF)
53 , m_lastGain(-1.0)
54 , m_connectionCount(0)
55{
56 setNodeType(NodeTypePanner);
57
58 // Load the HRTF database asynchronously so we don't block the Javascript thread while creating the HRTF database.
59 m_hrtfDatabaseLoader = HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary(context.sampleRate());
60
61 addInput(std::make_unique<AudioNodeInput>(this));
62 addOutput(std::make_unique<AudioNodeOutput>(this, 2));
63
64 // Node-specific default mixing rules.
65 m_channelCount = 2;
66 m_channelCountMode = ClampedMax;
67 m_channelInterpretation = AudioBus::Speakers;
68
69 m_distanceGain = AudioParam::create(context, "distanceGain", 1.0, 0.0, 1.0);
70 m_coneGain = AudioParam::create(context, "coneGain", 1.0, 0.0, 1.0);
71
72 m_position = FloatPoint3D(0, 0, 0);
73 m_orientation = FloatPoint3D(1, 0, 0);
74 m_velocity = FloatPoint3D(0, 0, 0);
75
76 initialize();
77}
78
79PannerNode::~PannerNode()
80{
81 uninitialize();
82}
83
84void PannerNode::pullInputs(size_t framesToProcess)
85{
86 // We override pullInputs(), so we can detect new AudioSourceNodes which have connected to us when new connections are made.
87 // These AudioSourceNodes need to be made aware of our existence in order to handle doppler shift pitch changes.
88 if (m_connectionCount != context().connectionCount()) {
89 m_connectionCount = context().connectionCount();
90
91 // Recursively go through all nodes connected to us.
92 HashSet<AudioNode*> visitedNodes;
93 notifyAudioSourcesConnectedToNode(this, visitedNodes);
94 }
95
96 AudioNode::pullInputs(framesToProcess);
97}
98
99void PannerNode::process(size_t framesToProcess)
100{
101 AudioBus* destination = output(0)->bus();
102
103 if (!isInitialized() || !input(0)->isConnected() || !m_panner.get()) {
104 destination->zero();
105 return;
106 }
107
108 AudioBus* source = input(0)->bus();
109 if (!source) {
110 destination->zero();
111 return;
112 }
113
114 // HRTFDatabase should be loaded before proceeding for offline audio context when panningModel() is "HRTF".
115 if (panningModel() == PanningModelType::HRTF && !m_hrtfDatabaseLoader->isLoaded()) {
116 if (context().isOfflineContext())
117 m_hrtfDatabaseLoader->waitForLoaderThreadCompletion();
118 else {
119 destination->zero();
120 return;
121 }
122 }
123
124 // The audio thread can't block on this lock, so we use std::try_to_lock instead.
125 std::unique_lock<Lock> lock(m_pannerMutex, std::try_to_lock);
126 if (!lock.owns_lock()) {
127 // Too bad - The try_lock() failed. We must be in the middle of changing the panner.
128 destination->zero();
129 return;
130 }
131
132 // Apply the panning effect.
133 double azimuth;
134 double elevation;
135 getAzimuthElevation(&azimuth, &elevation);
136 m_panner->pan(azimuth, elevation, source, destination, framesToProcess);
137
138 // Get the distance and cone gain.
139 double totalGain = distanceConeGain();
140
141 // Snap to desired gain at the beginning.
142 if (m_lastGain == -1.0)
143 m_lastGain = totalGain;
144
145 // Apply gain in-place with de-zippering.
146 destination->copyWithGainFrom(*destination, &m_lastGain, totalGain);
147}
148
149void PannerNode::reset()
150{
151 m_lastGain = -1.0; // force to snap to initial gain
152 if (m_panner.get())
153 m_panner->reset();
154}
155
156void PannerNode::initialize()
157{
158 if (isInitialized())
159 return;
160
161 m_panner = Panner::create(m_panningModel, sampleRate(), m_hrtfDatabaseLoader.get());
162
163 AudioNode::initialize();
164}
165
166void PannerNode::uninitialize()
167{
168 if (!isInitialized())
169 return;
170
171 m_panner = nullptr;
172 AudioNode::uninitialize();
173}
174
175AudioListener* PannerNode::listener()
176{
177 return context().listener();
178}
179
180void PannerNode::setPanningModel(PanningModelType model)
181{
182 if (!m_panner.get() || model != m_panningModel) {
183 // This synchronizes with process().
184 std::lock_guard<Lock> lock(m_pannerMutex);
185
186 m_panner = Panner::create(model, sampleRate(), m_hrtfDatabaseLoader.get());
187 m_panningModel = model;
188 }
189}
190
191DistanceModelType PannerNode::distanceModel() const
192{
193 return const_cast<PannerNode*>(this)->m_distanceEffect.model();
194}
195
196void PannerNode::setDistanceModel(DistanceModelType model)
197{
198 m_distanceEffect.setModel(model, true);
199}
200
201void PannerNode::getAzimuthElevation(double* outAzimuth, double* outElevation)
202{
203 // FIXME: we should cache azimuth and elevation (if possible), so we only re-calculate if a change has been made.
204
205 double azimuth = 0.0;
206
207 // Calculate the source-listener vector
208 FloatPoint3D listenerPosition = listener()->position();
209 FloatPoint3D sourceListener = m_position - listenerPosition;
210
211 if (sourceListener.isZero()) {
212 // degenerate case if source and listener are at the same point
213 *outAzimuth = 0.0;
214 *outElevation = 0.0;
215 return;
216 }
217
218 sourceListener.normalize();
219
220 // Align axes
221 FloatPoint3D listenerFront = listener()->orientation();
222 FloatPoint3D listenerUp = listener()->upVector();
223 FloatPoint3D listenerRight = listenerFront.cross(listenerUp);
224 listenerRight.normalize();
225
226 FloatPoint3D listenerFrontNorm = listenerFront;
227 listenerFrontNorm.normalize();
228
229 FloatPoint3D up = listenerRight.cross(listenerFrontNorm);
230
231 float upProjection = sourceListener.dot(up);
232
233 FloatPoint3D projectedSource = sourceListener - upProjection * up;
234 projectedSource.normalize();
235
236 azimuth = 180.0 * acos(projectedSource.dot(listenerRight)) / piDouble;
237 fixNANs(azimuth); // avoid illegal values
238
239 // Source in front or behind the listener
240 double frontBack = projectedSource.dot(listenerFrontNorm);
241 if (frontBack < 0.0)
242 azimuth = 360.0 - azimuth;
243
244 // Make azimuth relative to "front" and not "right" listener vector
245 if ((azimuth >= 0.0) && (azimuth <= 270.0))
246 azimuth = 90.0 - azimuth;
247 else
248 azimuth = 450.0 - azimuth;
249
250 // Elevation
251 double elevation = 90.0 - 180.0 * acos(sourceListener.dot(up)) / piDouble;
252 fixNANs(elevation); // avoid illegal values
253
254 if (elevation > 90.0)
255 elevation = 180.0 - elevation;
256 else if (elevation < -90.0)
257 elevation = -180.0 - elevation;
258
259 if (outAzimuth)
260 *outAzimuth = azimuth;
261 if (outElevation)
262 *outElevation = elevation;
263}
264
265float PannerNode::dopplerRate()
266{
267 double dopplerShift = 1.0;
268
269 // FIXME: optimize for case when neither source nor listener has changed...
270 double dopplerFactor = listener()->dopplerFactor();
271
272 if (dopplerFactor > 0.0) {
273 double speedOfSound = listener()->speedOfSound();
274
275 const FloatPoint3D &sourceVelocity = m_velocity;
276 const FloatPoint3D &listenerVelocity = listener()->velocity();
277
278 // Don't bother if both source and listener have no velocity
279 bool sourceHasVelocity = !sourceVelocity.isZero();
280 bool listenerHasVelocity = !listenerVelocity.isZero();
281
282 if (sourceHasVelocity || listenerHasVelocity) {
283 // Calculate the source to listener vector
284 FloatPoint3D listenerPosition = listener()->position();
285 FloatPoint3D sourceToListener = m_position - listenerPosition;
286
287 double sourceListenerMagnitude = sourceToListener.length();
288
289 double listenerProjection = sourceToListener.dot(listenerVelocity) / sourceListenerMagnitude;
290 double sourceProjection = sourceToListener.dot(sourceVelocity) / sourceListenerMagnitude;
291
292 listenerProjection = -listenerProjection;
293 sourceProjection = -sourceProjection;
294
295 double scaledSpeedOfSound = speedOfSound / dopplerFactor;
296 listenerProjection = std::min(listenerProjection, scaledSpeedOfSound);
297 sourceProjection = std::min(sourceProjection, scaledSpeedOfSound);
298
299 dopplerShift = ((speedOfSound - dopplerFactor * listenerProjection) / (speedOfSound - dopplerFactor * sourceProjection));
300 fixNANs(dopplerShift); // avoid illegal values
301
302 // Limit the pitch shifting to 4 octaves up and 3 octaves down.
303 if (dopplerShift > 16.0)
304 dopplerShift = 16.0;
305 else if (dopplerShift < 0.125)
306 dopplerShift = 0.125;
307 }
308 }
309
310 return static_cast<float>(dopplerShift);
311}
312
313float PannerNode::distanceConeGain()
314{
315 FloatPoint3D listenerPosition = listener()->position();
316
317 double listenerDistance = m_position.distanceTo(listenerPosition);
318 double distanceGain = m_distanceEffect.gain(listenerDistance);
319
320 m_distanceGain->setValue(static_cast<float>(distanceGain));
321
322 // FIXME: could optimize by caching coneGain
323 double coneGain = m_coneEffect.gain(m_position, m_orientation, listenerPosition);
324
325 m_coneGain->setValue(static_cast<float>(coneGain));
326
327 return float(distanceGain * coneGain);
328}
329
330void PannerNode::notifyAudioSourcesConnectedToNode(AudioNode* node, HashSet<AudioNode*>& visitedNodes)
331{
332 ASSERT(node);
333 if (!node)
334 return;
335
336 // 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.
337 if (node->nodeType() == NodeTypeAudioBufferSource) {
338 AudioBufferSourceNode* bufferSourceNode = reinterpret_cast<AudioBufferSourceNode*>(node);
339 bufferSourceNode->setPannerNode(this);
340 } else {
341 // Go through all inputs to this node.
342 for (unsigned i = 0; i < node->numberOfInputs(); ++i) {
343 AudioNodeInput* input = node->input(i);
344
345 // For each input, go through all of its connections, looking for AudioBufferSourceNodes.
346 for (unsigned j = 0; j < input->numberOfRenderingConnections(); ++j) {
347 AudioNodeOutput* connectedOutput = input->renderingOutput(j);
348 AudioNode* connectedNode = connectedOutput->node();
349 if (visitedNodes.contains(connectedNode))
350 continue;
351
352 visitedNodes.add(connectedNode);
353 notifyAudioSourcesConnectedToNode(connectedNode, visitedNodes);
354 }
355 }
356 }
357}
358
359} // namespace WebCore
360
361#endif // ENABLE(WEB_AUDIO)
362