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