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