Bug 1153783 - Implement the `detune` AudioParam for the AudioBufferSourceNode. r=ehsan
authorPaul Adenot <paul@paul.cx>
Tue, 14 Apr 2015 17:03:50 +0200
changeset 257920 06ecc22ef9d57ad56783e25c9586b12f62b33ac2
parent 257919 7bb9166310a2cbfd8106c3d7bbda6755ab77286b
child 257921 060479d9d865696417121cf906ade90e848ca925
push id8007
push userraliiev@mozilla.com
push dateMon, 11 May 2015 19:23:16 +0000
treeherdermozilla-aurora@e2ce1aac996e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersehsan
bugs1153783
milestone40.0a1
Bug 1153783 - Implement the `detune` AudioParam for the AudioBufferSourceNode. r=ehsan
dom/media/webaudio/AudioBufferSourceNode.cpp
dom/media/webaudio/AudioBufferSourceNode.h
dom/media/webaudio/test/mochitest.ini
dom/media/webaudio/test/test_audioBufferSourceNodeRate.html
dom/media/webaudio/test/webaudio.js
dom/webidl/AudioBufferSourceNode.webidl
--- a/dom/media/webaudio/AudioBufferSourceNode.cpp
+++ b/dom/media/webaudio/AudioBufferSourceNode.cpp
@@ -9,38 +9,41 @@
 #include "mozilla/dom/AudioParam.h"
 #include "mozilla/FloatingPoint.h"
 #include "nsMathUtils.h"
 #include "AudioNodeEngine.h"
 #include "AudioNodeStream.h"
 #include "AudioDestinationNode.h"
 #include "AudioParamTimeline.h"
 #include <limits>
+#include <algorithm>
 
 namespace mozilla {
 namespace dom {
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(AudioBufferSourceNode)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioBufferSourceNode)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mBuffer)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlaybackRate)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mDetune)
   if (tmp->Context()) {
     // AudioNode's Unlink implementation disconnects us from the graph
     // too, but we need to do this right here to make sure that
     // UnregisterAudioBufferSourceNode can properly untangle us from
     // the possibly connected PannerNodes.
     tmp->DisconnectFromGraph();
     tmp->Context()->UnregisterAudioBufferSourceNode(tmp);
   }
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(AudioNode)
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AudioBufferSourceNode, AudioNode)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBuffer)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlaybackRate)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDetune)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(AudioBufferSourceNode)
 NS_INTERFACE_MAP_END_INHERITING(AudioNode)
 
 NS_IMPL_ADDREF_INHERITED(AudioBufferSourceNode, AudioNode)
 NS_IMPL_RELEASE_INHERITED(AudioBufferSourceNode, AudioNode)
 
@@ -59,17 +62,19 @@ public:
     mStart(0.0), mBeginProcessing(0),
     mStop(STREAM_TIME_MAX),
     mResampler(nullptr), mRemainingResamplerTail(0),
     mBufferEnd(0),
     mLoopStart(0), mLoopEnd(0),
     mBufferSampleRate(0), mBufferPosition(0), mChannels(0),
     mDopplerShift(1.0f),
     mDestination(static_cast<AudioNodeStream*>(aDestination->Stream())),
-    mPlaybackRateTimeline(1.0f), mLoop(false)
+    mPlaybackRateTimeline(1.0f),
+    mDetuneTimeline(0.0f),
+    mLoop(false)
   {}
 
   ~AudioBufferSourceNodeEngine()
   {
     if (mResampler) {
       speex_resampler_destroy(mResampler);
     }
   }
@@ -83,16 +88,20 @@ public:
                                     const dom::AudioParamTimeline& aValue,
                                     TrackRate aSampleRate) override
   {
     switch (aIndex) {
     case AudioBufferSourceNode::PLAYBACKRATE:
       mPlaybackRateTimeline = aValue;
       WebAudioUtils::ConvertAudioParamToTicks(mPlaybackRateTimeline, mSource, mDestination);
       break;
+    case AudioBufferSourceNode::DETUNE:
+      mDetuneTimeline = aValue;
+      WebAudioUtils::ConvertAudioParamToTicks(mDetuneTimeline, mSource, mDestination);
+      break;
     default:
       NS_ERROR("Bad AudioBufferSourceNodeEngine TimelineParameter");
     }
   }
   virtual void SetStreamTimeParameter(uint32_t aIndex, StreamTime aParam) override
   {
     switch (aIndex) {
     case AudioBufferSourceNode::STOP: mStop = aParam; break;
@@ -221,17 +230,17 @@ public:
       float* baseChannelData = static_cast<float*>(const_cast<void*>(aOutput->mChannelData[i]));
       memcpy(baseChannelData + aOffsetWithinBlock,
              mBuffer->GetData(i) + mBufferPosition,
              aNumberOfFrames * sizeof(float));
     }
   }
 
   // Resamples input data to an output buffer, according to |mBufferSampleRate| and
-  // the playbackRate.
+  // the playbackRate/detune.
   // The number of frames consumed/produced depends on the amount of space
   // remaining in both the input and output buffer, and the playback rate (that
   // is, the ratio between the output samplerate and the input samplerate).
   void CopyFromInputBufferWithResampling(AudioNodeStream* aStream,
                                          AudioChunk* aOutput,
                                          uint32_t aChannels,
                                          uint32_t* aOffsetWithinBlock,
                                          StreamTime* aCurrentPosition,
@@ -392,40 +401,49 @@ public:
         *aCurrentPosition += numFrames;
         mBufferPosition += numFrames;
       } else {
         CopyFromInputBufferWithResampling(aStream, aOutput, aChannels, aOffsetWithinBlock, aCurrentPosition, aBufferMax);
       }
     }
   }
 
-  int32_t ComputeFinalOutSampleRate(float aPlaybackRate)
+  int32_t ComputeFinalOutSampleRate(float aPlaybackRate, float aDetune)
   {
+    float computedPlaybackRate = aPlaybackRate * pow(2, aDetune / 1200.f);
     // Make sure the playback rate and the doppler shift are something
     // our resampler can work with.
     int32_t rate = WebAudioUtils::
       TruncateFloatToInt<int32_t>(mSource->SampleRate() /
-                                  (aPlaybackRate * mDopplerShift));
+                                  (computedPlaybackRate * mDopplerShift));
     return rate ? rate : mBufferSampleRate;
   }
 
   void UpdateSampleRateIfNeeded(uint32_t aChannels)
   {
     float playbackRate;
+    float detune;
 
     if (mPlaybackRateTimeline.HasSimpleValue()) {
       playbackRate = mPlaybackRateTimeline.GetValue();
     } else {
       playbackRate = mPlaybackRateTimeline.GetValueAtTime(mSource->GetCurrentPosition());
     }
+    if (mDetuneTimeline.HasSimpleValue()) {
+      detune = mDetuneTimeline.GetValue();
+    } else {
+      detune = mDetuneTimeline.GetValueAtTime(mSource->GetCurrentPosition());
+    }
     if (playbackRate <= 0 || mozilla::IsNaN(playbackRate)) {
       playbackRate = 1.0f;
     }
 
-    int32_t outRate = ComputeFinalOutSampleRate(playbackRate);
+    detune = std::min(std::max(-1200.f, detune), 1200.f);
+
+    int32_t outRate = ComputeFinalOutSampleRate(playbackRate, detune);
     UpdateResampler(outRate, aChannels);
   }
 
   virtual void ProcessBlock(AudioNodeStream* aStream,
                             const AudioChunk& aInput,
                             AudioChunk* aOutput,
                             bool* aFinished) override
   {
@@ -435,19 +453,16 @@ public:
     }
 
     uint32_t channels = mBuffer->GetChannels();
     if (!channels) {
       aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
       return;
     }
 
-    // WebKit treats the playbackRate as a k-rate parameter in their code,
-    // despite the spec saying that it should be an a-rate parameter. We treat
-    // it as k-rate. Spec bug: https://www.w3.org/Bugs/Public/show_bug.cgi?id=21592
     UpdateSampleRateIfNeeded(channels);
 
     uint32_t written = 0;
     StreamTime streamPosition = aStream->GetCurrentPosition();
     while (written < WEBAUDIO_BLOCK_SIZE) {
       if (mStop != STREAM_TIME_MAX &&
           streamPosition >= mStop) {
         FillWithZeroes(aOutput, channels, &written, &streamPosition, STREAM_TIME_MAX);
@@ -483,16 +498,17 @@ public:
     }
   }
 
   virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override
   {
     // Not owned:
     // - mBuffer - shared w/ AudioNode
     // - mPlaybackRateTimeline - shared w/ AudioNode
+    // - mDetuneTimeline - shared w/ AudioNode
 
     size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
 
     // NB: We need to modify speex if we want the full memory picture, internal
     //     fields that need measuring noted below.
     // - mResampler->mem
     // - mResampler->sinc_table
     // - mResampler->last_sample
@@ -525,28 +541,30 @@ public:
   int32_t mLoopEnd;
   int32_t mBufferSampleRate;
   int32_t mBufferPosition;
   uint32_t mChannels;
   float mDopplerShift;
   AudioNodeStream* mDestination;
   AudioNodeStream* mSource;
   AudioParamTimeline mPlaybackRateTimeline;
+  AudioParamTimeline mDetuneTimeline;
   bool mLoop;
 };
 
 AudioBufferSourceNode::AudioBufferSourceNode(AudioContext* aContext)
   : AudioNode(aContext,
               2,
               ChannelCountMode::Max,
               ChannelInterpretation::Speakers)
   , mLoopStart(0.0)
   , mLoopEnd(0.0)
   // mOffset and mDuration are initialized in Start().
   , mPlaybackRate(new AudioParam(this, SendPlaybackRateToStream, 1.0f))
+  , mDetune(new AudioParam(this, SendDetuneToStream, 0.0f))
   , mLoop(false)
   , mStartCalled(false)
   , mStopped(false)
 {
   AudioBufferSourceNodeEngine* engine = new AudioBufferSourceNodeEngine(this, aContext->Destination());
   mStream = aContext->Graph()->CreateAudioNodeStream(engine, MediaStreamGraph::SOURCE_STREAM);
   engine->SetSourceStream(static_cast<AudioNodeStream*>(mStream.get()));
   mStream->AddMainThreadListener(this);
@@ -563,16 +581,17 @@ size_t
 AudioBufferSourceNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
 {
   size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
   if (mBuffer) {
     amount += mBuffer->SizeOfIncludingThis(aMallocSizeOf);
   }
 
   amount += mPlaybackRate->SizeOfIncludingThis(aMallocSizeOf);
+  amount += mDetune->SizeOfIncludingThis(aMallocSizeOf);
   return amount;
 }
 
 size_t
 AudioBufferSourceNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const
 {
   return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
 }
@@ -728,16 +747,23 @@ AudioBufferSourceNode::NotifyMainThreadS
 void
 AudioBufferSourceNode::SendPlaybackRateToStream(AudioNode* aNode)
 {
   AudioBufferSourceNode* This = static_cast<AudioBufferSourceNode*>(aNode);
   SendTimelineParameterToStream(This, PLAYBACKRATE, *This->mPlaybackRate);
 }
 
 void
+AudioBufferSourceNode::SendDetuneToStream(AudioNode* aNode)
+{
+  AudioBufferSourceNode* This = static_cast<AudioBufferSourceNode*>(aNode);
+  SendTimelineParameterToStream(This, DETUNE, *This->mDetune);
+}
+
+void
 AudioBufferSourceNode::SendDopplerShiftToStream(double aDopplerShift)
 {
   SendDoubleParameterToStream(DOPPLERSHIFT, aDopplerShift);
 }
 
 void
 AudioBufferSourceNode::SendLoopParametersToStream()
 {
--- a/dom/media/webaudio/AudioBufferSourceNode.h
+++ b/dom/media/webaudio/AudioBufferSourceNode.h
@@ -54,16 +54,20 @@ public:
     mBuffer = aBuffer;
     SendBufferParameterToStream(aCx);
     SendLoopParametersToStream();
   }
   AudioParam* PlaybackRate() const
   {
     return mPlaybackRate;
   }
+  AudioParam* Detune() const
+  {
+    return mDetune;
+  }
   bool Loop() const
   {
     return mLoop;
   }
   void SetLoop(bool aLoop)
   {
     mLoop = aLoop;
     SendLoopParametersToStream();
@@ -118,31 +122,34 @@ private:
     BUFFERSTART,
     // BUFFEREND is the sum of "offset" and "duration" passed to start(),
     // multiplied by buffer.sampleRate, or the size of the buffer, if smaller.
     BUFFEREND,
     LOOP,
     LOOPSTART,
     LOOPEND,
     PLAYBACKRATE,
+    DETUNE,
     DOPPLERSHIFT
   };
 
   void SendLoopParametersToStream();
   void SendBufferParameterToStream(JSContext* aCx);
   void SendOffsetAndDurationParametersToStream(AudioNodeStream* aStream);
   static void SendPlaybackRateToStream(AudioNode* aNode);
+  static void SendDetuneToStream(AudioNode* aNode);
 
 private:
   double mLoopStart;
   double mLoopEnd;
   double mOffset;
   double mDuration;
   nsRefPtr<AudioBuffer> mBuffer;
   nsRefPtr<AudioParam> mPlaybackRate;
+  nsRefPtr<AudioParam> mDetune;
   bool mLoop;
   bool mStartCalled;
   bool mStopped;
 };
 
 }
 }
 
--- a/dom/media/webaudio/test/mochitest.ini
+++ b/dom/media/webaudio/test/mochitest.ini
@@ -38,16 +38,17 @@ support-files =
 [test_audioBufferSourceNodeLoopStartEndSame.html]
 [test_audioBufferSourceNodeNeutered.html]
 skip-if = (toolkit == 'android' && (processor == 'x86' || debug)) || os == 'win' # bug 1127845, bug 1138468
 [test_audioBufferSourceNodeNoStart.html]
 [test_audioBufferSourceNodeNullBuffer.html]
 [test_audioBufferSourceNodeOffset.html]
 skip-if = (toolkit == 'gonk') || (toolkit == 'android') || debug #bug 906752
 [test_audioBufferSourceNodePassThrough.html]
+[test_audioBufferSourceNodeRate.html]
 [test_AudioContext.html]
 [test_audioDestinationNode.html]
 [test_AudioListener.html]
 [test_audioParamExponentialRamp.html]
 [test_audioParamGain.html]
 [test_audioParamLinearRamp.html]
 [test_audioParamSetCurveAtTime.html]
 [test_audioParamSetCurveAtTimeZeroDuration.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodeRate.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test AudioBufferSourceNode</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="webaudio.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var rate = 44100;
+var off = new OfflineAudioContext(1, rate, rate);
+var off2 = new OfflineAudioContext(1, rate, rate);
+
+var source = off.createBufferSource();
+var source2 = off2.createBufferSource();
+
+// a buffer of a 440Hz at half the length. If we detune by -1200 or set the
+// playbackRate to 0.5, we should get 44100 samples back with a sine at 220Hz.
+var buf = off.createBuffer(1, rate / 2, rate);
+var bufarray = buf.getChannelData(0);
+for (var i = 0; i < bufarray.length; i++) {
+  bufarray[i] = Math.sin(i * 440 * 2 * Math.PI / rate);
+}
+
+source.buffer = buf;
+source.playbackRate.value = 0.5; // 50% slowdown
+source.connect(off.destination);
+source.start(0);
+
+source2.buffer = buf;
+source2.detune.value = -1200; // one octave -> 50% slowdown
+source2.connect(off2.destination);
+source2.start(0);
+
+var finished = 0;
+function finish() {
+  finished++;
+  if (finished == 2) {
+    SimpleTest.finish();
+  }
+}
+
+off.startRendering().then((renderedPlaybackRate) => {
+  // we don't care about comparing the value here, we just want to know whether
+  // the second part is noisy.
+  var rmsValue = rms(renderedPlaybackRate, 0, 22050);
+  ok(rmsValue != 0, "Resampling happened (rms of the second part " + rmsValue + ")");
+
+  off2.startRendering().then((renderedDetune) => {
+    var rmsValue = rms(renderedDetune, 0, 22050);
+    ok(rmsValue != 0, "Resampling happened (rms of the second part " + rmsValue + ")");
+    // The two buffers should be the same: detune of -1200 is a 50% slowdown
+    compareBuffers(renderedPlaybackRate, renderedDetune);
+    SimpleTest.finish();
+  });
+});
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/media/webaudio/test/webaudio.js
+++ b/dom/media/webaudio/test/webaudio.js
@@ -88,16 +88,35 @@ function compareBuffers(got, expected) {
   }
 
   for (var i = 0; i < got.numberOfChannels; ++i) {
     compareChannels(got.getChannelData(i), expected.getChannelData(i),
                     got.length, 0, 0, true);
   }
 }
 
+/**
+ * Compute the root mean square (RMS,
+ * <http://en.wikipedia.org/wiki/Root_mean_square>) of a channel of a slice
+ * (defined by `start` and `end`) of an AudioBuffer.
+ *
+ * This is useful to detect that a buffer is noisy or silent.
+ */
+function rms(audiobuffer, channel = 0, start = 0, end = audiobuffer.length) {
+  var buffer= audiobuffer.getChannelData(channel);
+  var rms = 0;
+  for (var i = start; i < end; i++) {
+    rms += buffer[i] * buffer[i];
+  }
+
+  rms /= buffer.length;
+  rms = Math.sqrt(rms);
+  return rms;
+}
+
 function getEmptyBuffer(context, length) {
   return context.createBuffer(gTest.numberOfChannels, length, context.sampleRate);
 }
 
 /**
  * This function assumes that the test file defines a single gTest variable with
  * the following properties and methods:
  *
--- a/dom/webidl/AudioBufferSourceNode.webidl
+++ b/dom/webidl/AudioBufferSourceNode.webidl
@@ -10,16 +10,17 @@
  * liability, trademark and document use rules apply.
  */
 
 interface AudioBufferSourceNode : AudioNode {
 
     attribute AudioBuffer? buffer;
 
     readonly attribute AudioParam playbackRate;
+    readonly attribute AudioParam detune;
 
     attribute boolean loop;
     attribute double loopStart;
     attribute double loopEnd;
 
     [Throws, UnsafeInPrerendering]
     void start(optional double when = 0, optional double grainOffset = 0,
                optional double grainDuration);