Home | History | Annotate | Download | only in webaudio
      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/ScriptExecutionContext.h"
     32 #include "core/platform/audio/AudioBus.h"
     33 #include "core/platform/audio/HRTFPanner.h"
     34 #include "modules/webaudio/AudioBufferSourceNode.h"
     35 #include "modules/webaudio/AudioContext.h"
     36 #include "modules/webaudio/AudioNodeInput.h"
     37 #include "modules/webaudio/AudioNodeOutput.h"
     38 #include "wtf/MathExtras.h"
     39 
     40 using namespace std;
     41 
     42 namespace WebCore {
     43 
     44 static void fixNANs(double &x)
     45 {
     46     if (std::isnan(x) || std::isinf(x))
     47         x = 0.0;
     48 }
     49 
     50 PannerNode::PannerNode(AudioContext* context, float sampleRate)
     51     : AudioNode(context, sampleRate)
     52     , m_panningModel(Panner::PanningModelHRTF)
     53     , m_lastGain(-1.0)
     54     , m_connectionCount(0)
     55 {
     56     ScriptWrappable::init(this);
     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 
     77 PannerNode::~PannerNode()
     78 {
     79     uninitialize();
     80 }
     81 
     82 void 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 
     96 void 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 
    136 void 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 
    143 void 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 
    153 void PannerNode::uninitialize()
    154 {
    155     if (!isInitialized())
    156         return;
    157 
    158     m_panner.clear();
    159     AudioNode::uninitialize();
    160 }
    161 
    162 AudioListener* PannerNode::listener()
    163 {
    164     return context()->listener();
    165 }
    166 
    167 String 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 
    182 void 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 
    194 bool 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 
    219 String 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 
    234 void 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 
    246 bool 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 
    261 void 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 
    325 float 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 
    373 float 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 
    390 void 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 = static_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