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