Round time to sample frame
authorcommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 27 Jan 2012 23:31:35 +0000 (23:31 +0000)
committercommit-queue@webkit.org <commit-queue@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 27 Jan 2012 23:31:35 +0000 (23:31 +0000)
https://bugs.webkit.org/show_bug.cgi?id=76659

Source/WebCore:

Three major changes:

1. Modify timeToSampleFrame to round the time to the sample frame
and add support for Visual Studio to round the time in the same
way as on gcc. (The issue is that Visual Studio uses x87
instructions with 80-bit floats.)  This makes Visual Studio more
consistent with the results with gcc.

2. Change the current time variable from keeping time in seconds
to keeping time in sample frames.  This minimizes rounding except
when needed for the Javascript API.

3. Update AudioBufferSourceNode::process to use samples (instead
of time) as much as possible to reduce round-off effects.

Patch by Raymond Toy <rtoy@google.com> on 2012-01-27
Reviewed by Kenneth Russell.

Tests: Added note-grain-on test to exercise precise
timing. Existing tests (gain and audiobuffersource-playbackrate)
also cover some of these changes, and the equalpower panner test is
modified to enable the tests for the offset of the impulses.

* platform/audio/AudioUtilities.cpp:
(WebCore::AudioUtilities::timeToSampleFrame): Moved from
AudioParamTimeLine and slightly modified, and updated to round
operations consistently.  Add special flags for Visual Studio to
generate code with rounding that is consistent with gcc.
* platform/audio/AudioUtilities.h: Declare new function.
* webaudio/AudioBufferSourceNode.cpp:
(WebCore::AudioBufferSourceNode::process): Use new functions to
convert time to sample frame.  Update code to use integer
arithmetic as much as possible.
(WebCore::AudioBufferSourceNode::renderFromBuffer): Use
timeToSampleFrame to convert time to sample frame.
* webaudio/AudioContext.h: Define new currentSample method to get
the current sample.
* webaudio/AudioDestinationNode.cpp:
(WebCore::AudioDestinationNode::provideInput): Use new function to
convert sample frame to time.  Update
* webaudio/AudioDestiationNode.h: Rename m_currentTime to
m_currentSample, add method to return current Sample.  Update
currentTime() method to compute time from the current sample.
* webaudio/AudioParamTimeline.cpp:
(WebCore::AudioParamTimeline::valuesForTimeRangeImpl): Remove
timeToSampleFrame and use new function in AudioUtilities to
convert time to sample frame.

LayoutTests:

Patch by Raymond Toy <rtoy@google.com> on 2012-01-27
Reviewed by Kenneth Russell.

* webaudio/audiobuffersource-playbackrate-expected.wav: Updated.
* webaudio/gain-expected.wav: Updated.
* webaudio/resources/audio-testing.js:
(timeToSampleFrame): Utility to convert time to sample frame, to
be consistent with the actual webaudio implementation.
* webaudio/resources/panner-model-testing.js:
(checkResult): Enable the tests for the offset of the impulses.
Also fix a bug in printing the time where the max error was
found.
* webaudio/note-grain-on-timing.html: New test for noteGrainOn.
Also tests the new currentSample implemenation and
roundToSampleFrame.
* webaudio/note-grain-on-timing-expected.txt: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@106162 268f45cc-cd09-0410-ab3c-d52691b4dbfc

16 files changed:
LayoutTests/ChangeLog
LayoutTests/webaudio/audiobuffersource-playbackrate-expected.wav
LayoutTests/webaudio/gain-expected.wav
LayoutTests/webaudio/note-grain-on-timing-expected.txt [new file with mode: 0644]
LayoutTests/webaudio/note-grain-on-timing.html [new file with mode: 0644]
LayoutTests/webaudio/panner-equalpower-expected.txt
LayoutTests/webaudio/resources/audio-testing.js
LayoutTests/webaudio/resources/panner-model-testing.js
Source/WebCore/ChangeLog
Source/WebCore/platform/audio/AudioUtilities.cpp
Source/WebCore/platform/audio/AudioUtilities.h
Source/WebCore/webaudio/AudioBufferSourceNode.cpp
Source/WebCore/webaudio/AudioContext.h
Source/WebCore/webaudio/AudioDestinationNode.cpp
Source/WebCore/webaudio/AudioDestinationNode.h
Source/WebCore/webaudio/AudioParamTimeline.cpp

index cc9cfc9..5ab59b3 100644 (file)
@@ -1,3 +1,24 @@
+2012-01-27  Raymond Toy  <rtoy@google.com>
+
+        Round time to sample frame
+        https://bugs.webkit.org/show_bug.cgi?id=76659
+
+        Reviewed by Kenneth Russell.
+
+        * webaudio/audiobuffersource-playbackrate-expected.wav: Updated.
+        * webaudio/gain-expected.wav: Updated.
+        * webaudio/resources/audio-testing.js:
+        (timeToSampleFrame): Utility to convert time to sample frame, to
+        be consistent with the actual webaudio implementation.
+        * webaudio/resources/panner-model-testing.js:
+        (checkResult): Enable the tests for the offset of the impulses.
+        Also fix a bug in printing the time where the max error was
+        found. 
+        * webaudio/note-grain-on-timing.html: New test for noteGrainOn.
+        Also tests the new currentSample implemenation and
+        roundToSampleFrame.
+        * webaudio/note-grain-on-timing-expected.txt: Added.
+
 2012-01-27  Ken Buchanan  <kenrb@chromium.org>
 
         Crash in updateFirstLetter() from unnecessary anonymous block
index 3a59166..b925763 100644 (file)
Binary files a/LayoutTests/webaudio/audiobuffersource-playbackrate-expected.wav and b/LayoutTests/webaudio/audiobuffersource-playbackrate-expected.wav differ
index b7888cf..b445bd8 100644 (file)
Binary files a/LayoutTests/webaudio/gain-expected.wav and b/LayoutTests/webaudio/gain-expected.wav differ
diff --git a/LayoutTests/webaudio/note-grain-on-timing-expected.txt b/LayoutTests/webaudio/note-grain-on-timing-expected.txt
new file mode 100644 (file)
index 0000000..c524f82
--- /dev/null
@@ -0,0 +1,12 @@
+Test timing of noteGrainOn.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+PASS Found all 100 pulses.
+PASS All 100 square pulses started at the correct time.
+PASS All 100 square pulses ended at the correct time.
+PASS noteGrainOn timing tests passed.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/webaudio/note-grain-on-timing.html b/LayoutTests/webaudio/note-grain-on-timing.html
new file mode 100644 (file)
index 0000000..f33af09
--- /dev/null
@@ -0,0 +1,204 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+  <head>
+    <script src="resources/audio-testing.js"></script>
+    <script src="../fast/js/resources/js-test-pre.js"></script>
+  </head>
+
+  <body>
+    <div id="description"></div>
+    <div id="console"></div>
+
+    <script>
+      description("Test timing of noteGrainOn.");
+
+      var sampleRate = 44100.0;
+
+      // HRTF extra frames.  This is a magic constant currently in
+      // AudioBufferSourceNode::process that always extends the
+      // duration by this number of samples.  See bug 77224
+      // (https://bugs.webkit.org/show_bug.cgi?id=77224).
+      var extraFramesHRTF = 512;
+      
+      // How many square pulses to play.
+      var numberOfTests = 100;
+
+      // Duration of the square pulse to be played
+      var duration = 0.01;
+
+      // Time step between the start of each square pulse.  We need to
+      // add a little bit of silence so we can detect pulse boundaries
+      // and also account for the extra frames for HRTF.
+      var timeStep = duration + .005 + extraFramesHRTF / sampleRate;
+
+      // Time step between the grain start for each square pulse.
+      var grainOffsetStep = 0.005;
+
+      // How long to render to cover all of the pulses.
+      var renderTime = (numberOfTests + 1) * timeStep;
+
+      var context;
+      var squarePulseBuffer;
+      var renderedData;
+
+      function createSquarePulse(context) {
+          // Create a square pulse that is long enough so that all the
+          // possible grain offsets still results in a square pulse of
+          // of the requested duration.  (The extra 1 is for any
+          // round-off.) 
+
+          var pulseLength = Math.floor(1 + extraFramesHRTF + sampleRate * (numberOfTests * grainOffsetStep + duration));
+
+          squarePulseBuffer = context.createBuffer(2, pulseLength, sampleRate);
+          var data = squarePulseBuffer.getChannelData(0);
+          for (var k = 0; k < pulseLength; ++k) {
+              data[k] = 1;
+          }
+      }
+
+      function trueGrainLength(grainOffset, duration) {
+          var startFrame = timeToSampleFrame(grainOffset, sampleRate);
+          var endFrame = timeToSampleFrame(grainOffset + duration, sampleRate);
+
+          return endFrame - startFrame;
+      }
+      
+      function checkResult(event) {
+          var buffer = event.renderedBuffer;
+          renderedData = buffer.getChannelData(0);
+          var nSamples = renderedData.length;
+
+          var success = true;
+          var errorCountStart = 0;
+          var errorCountEnd = 0;
+      
+          var startTime = [];
+          var endTime = [];
+          var lookForStart = true;
+      
+          // Look through the rendered data to find the start and stop
+          // times of each pulse.
+          for (var k = 0; k < nSamples; ++k) {
+              if (lookForStart) {
+                  // Find a non-zero point and record it.  We're not
+                  // concerned with the value in this test, only that
+                  // the pulse started here.  Other tests should cover
+                  // this case.
+                  if (renderedData[k] > 0) {
+                      startTime.push(k);
+                      lookForStart = false;
+                  }
+              } else {
+                  // Find a zero and record it.
+                  if (renderedData[k] == 0) {
+                      endTime.push(k);
+                      lookForStart = true;
+                  }
+              }
+          }
+
+          if (startTime.length != endTime.length) {
+              testFailed("Could not find the beginning or end of a square pulse.");
+              success = false;
+          }
+
+          if (startTime.length == numberOfTests && endTime.length == numberOfTests) {
+              testPassed("Found all " + numberOfTests + " pulses.");
+          } else {
+              testFailed("Did not find all " + numberOfTests + " pulses.");
+          }
+
+          // Examine the start and stop times to see if they match our
+          // expectations.
+          for (var k = 0; k < startTime.length; ++k) {
+              var expectedStart = timeToSampleFrame(k * timeStep, sampleRate);
+              // The end point is the duration, plus the extra frames
+              // for HRTF.
+              var expectedEnd = extraFramesHRTF + expectedStart + trueGrainLength(k * grainOffsetStep, duration);
+
+              if (startTime[k] != expectedStart) {
+                  testFailed("Pulse " + k + " started at " + startTime[k] + " but expected at " + expectedStart);
+                  ++errorCountStart;
+                  success = false;
+              }
+
+              if (endTime[k] != expectedEnd) {
+                  testFailed("Pulse " + k + " ended at " + endTime[k] + " but expected at " + expectedEnd);
+                  ++errorCountEnd;
+                  success = false;
+              }
+          }
+
+          if (!errorCountStart) {
+              if (startTime.length == numberOfTests) {
+                  testPassed("All " + numberOfTests + " square pulses started at the correct time.");
+              } else {
+                  testFailed("All pulses started at the correct time, but only " + startTime.length + " pulses found.");
+                  success = false;
+              }
+          } else {
+              testFailed(errorCountStart + " out of " + numberOfTests + " square pulses started at the wrong time.");
+              success = false;
+          }
+
+          if (!errorCountEnd) {
+              if (endTime.length == numberOfTests) {
+                  testPassed("All " + numberOfTests + " square pulses ended at the correct time.");
+              } else {
+                  testFailed("All pulses ended at the correct time, but only " + endTime.length + " pulses found.");
+                  success = false;
+              }
+          } else {
+              testFailed(errorCountEnd + " out of " + numberOfTests + " square pulses ended at the wrong time.");
+              success = false;
+          }
+
+      
+          if (success) {
+              testPassed("noteGrainOn timing tests passed.");
+          } else {
+              testFailed("noteGrainOn timing tests failed.");
+          }
+
+          finishJSTest();
+      }
+
+      function playNote(time, grainOffset, duration) {
+          var bufferSource = context.createBufferSource();
+          bufferSource.buffer = squarePulseBuffer;
+          bufferSource.connect(context.destination);
+          // We're only testing that the source starts and ends at a
+          // particular time.  See bug 77225
+          // (https://bugs.webkit.org/show_bug.cgi?id=77225).
+          bufferSource.noteGrainOn(time, grainOffset, duration);
+      }
+
+      function runTest() {
+          if (window.layoutTestController) {
+              layoutTestController.dumpAsText();
+              layoutTestController.waitUntilDone();
+          }
+
+          window.jsTestIsAsync = true;
+
+          // Create offline audio context.
+          context = new webkitAudioContext(2, sampleRate * renderTime, sampleRate);
+          createSquarePulse(context);    
+
+          for (var i = 0; i < numberOfTests; ++i) {
+              var timeOffset = timeStep * i;
+              playNote(timeOffset, i * grainOffsetStep, duration);
+          }
+
+          context.oncomplete = checkResult;
+          context.startRendering();
+      }
+      
+      runTest();
+      successfullyParsed = true;
+
+    </script>
+
+    <script src="../fast/js/resources/js-test-post.js"></script>
+  </body>
+</html>
index ddd5ced..cbda404 100644 (file)
@@ -3,6 +3,7 @@ Test equal-power panner model of AudioPannerNode.
 On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
 
 PASS Number of impulses matches the number of panner nodes.
+PASS All impulses at expected offsets.
 PASS Left channel gain values are correct.
 PASS Right channel gain values are correct.
 PASS EqualPower panner test passed
index f90bf93..c428ae0 100644 (file)
@@ -119,3 +119,9 @@ function createImpulseBuffer(context, sampleFrameLength) {
 
     return audioBuffer;
 }
+
+// Convert time (in seconds) to sample frames.
+function timeToSampleFrame(time, sampleRate) {
+    return Math.floor(0.5 + time * sampleRate);
+}
+
index 83a9a08..c8e733c 100644 (file)
@@ -133,8 +133,9 @@ function checkResult(event) {
 
             // Keep track of the impulses that didn't show up where we
             // expected them to be.
-            if (k != Math.round(sampleRate * time[impulseCount])) {
-                timeErrors[timeCount] = { actual : k, expected : Math.round(sampleRate * time[impulseCount])};
+            var expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate);
+            if (k != expectedOffset) {
+                timeErrors[timeCount] = { actual : k, expected : expectedOffset};
                 ++timeCount;
             }
             ++impulseCount;
@@ -148,33 +149,30 @@ function checkResult(event) {
         success = false;
     }
 
+    if (timeErrors.length > 0) {
+        success = false;
+        testFailed(timeErrors.length + " timing errors found in " + nodesToCreate + " panner nodes.");
+        for (var k = 0; k < timeErrors.length; ++k) {
+            testFailed("Impulse at sample " + timeErrors[k].actual + " but expected " + timeErrors[k].expected);
+        }
+    } else {
+        testPassed("All impulses at expected offsets.");
+    }
+
     if (maxErrorL <= maxAllowedError) {
         testPassed("Left channel gain values are correct.");
     } else {
-        testFailed("Left channel gain values are incorrect.  Max error = " + maxErrorL + " at time " + maxErrorIndexL / sampleRate + " (threshold = " + maxAllowedError + ")");
+        testFailed("Left channel gain values are incorrect.  Max error = " + maxErrorL + " at time " + time[maxErrorIndexL] + " (threshold = " + maxAllowedError + ")");
         success = false;
     }
     
     if (maxErrorR <= maxAllowedError) {
         testPassed("Right channel gain values are correct.");
     } else {
-        testFailed("Right channel gain values are incorrect.  Max error = " + maxErrorR + " at time " + maxErrorIndexR / sampleRate + " (threshold = " + maxAllowedError + ")");
+        testFailed("Right channel gain values are incorrect.  Max error = " + maxErrorR + " at time " + time[maxErrorIndexR] + " (threshold = " + maxAllowedError + ")");
         success = false;
     }
 
-    // See bug 75996 (https://bugs.webkit.org/show_bug.cgi?id=75996)
-    // and 76073 (https://bugs.webkit.org/show_bug.cgi?id=76073).
-    // When those bugs are fixed, uncomment the if statement below to
-    // enable this check.
-
-//    if (timeErrors.length > 0) {
-//        success = false;
-//        testFailed(timeErrors.length + " timing errors found in " + nodesToCreate + " panner nodes.");
-//        for (var k = 0; k < timeErrors.length; ++k) {
-//            testFailed("Impulse at sample " + timeErrors[k].actual + " but expected " + timeErrors[k].expected);
-//        }
-//    }
-
     if (success) {
         testPassed("EqualPower panner test passed");
     } else {
index ecc4372..2ca0533 100644 (file)
@@ -1,3 +1,55 @@
+2012-01-27  Raymond Toy  <rtoy@google.com>
+
+        Round time to sample frame
+        https://bugs.webkit.org/show_bug.cgi?id=76659
+
+        Three major changes:
+
+        1. Modify timeToSampleFrame to round the time to the sample frame
+        and add support for Visual Studio to round the time in the same
+        way as on gcc. (The issue is that Visual Studio uses x87
+        instructions with 80-bit floats.)  This makes Visual Studio more
+        consistent with the results with gcc.
+
+        2. Change the current time variable from keeping time in seconds
+        to keeping time in sample frames.  This minimizes rounding except
+        when needed for the Javascript API.
+
+        3. Update AudioBufferSourceNode::process to use samples (instead
+        of time) as much as possible to reduce round-off effects.
+
+        Reviewed by Kenneth Russell.
+
+        Tests: Added note-grain-on test to exercise precise
+        timing. Existing tests (gain and audiobuffersource-playbackrate)
+        also cover some of these changes, and the equalpower panner test is
+        modified to enable the tests for the offset of the impulses.
+
+        * platform/audio/AudioUtilities.cpp:
+        (WebCore::AudioUtilities::timeToSampleFrame): Moved from
+        AudioParamTimeLine and slightly modified, and updated to round
+        operations consistently.  Add special flags for Visual Studio to
+        generate code with rounding that is consistent with gcc.
+        * platform/audio/AudioUtilities.h: Declare new function.
+        * webaudio/AudioBufferSourceNode.cpp:
+        (WebCore::AudioBufferSourceNode::process): Use new functions to
+        convert time to sample frame.  Update code to use integer
+        arithmetic as much as possible.
+        (WebCore::AudioBufferSourceNode::renderFromBuffer): Use
+        timeToSampleFrame to convert time to sample frame.
+        * webaudio/AudioContext.h: Define new currentSample method to get
+        the current sample.
+        * webaudio/AudioDestinationNode.cpp:
+        (WebCore::AudioDestinationNode::provideInput): Use new function to
+        convert sample frame to time.  Update
+        * webaudio/AudioDestiationNode.h: Rename m_currentTime to
+        m_currentSample, add method to return current Sample.  Update
+        currentTime() method to compute time from the current sample.
+        * webaudio/AudioParamTimeline.cpp:
+        (WebCore::AudioParamTimeline::valuesForTimeRangeImpl): Remove
+        timeToSampleFrame and use new function in AudioUtilities to
+        convert time to sample frame.
+
 2012-01-27  Adrienne Walker  <enne@google.com>
 
         [chromium] Don't ever skip drawing the non-composited content layer
index 1a02b7e..ff19cee 100644 (file)
@@ -55,7 +55,39 @@ float discreteTimeConstantForSampleRate(float timeConstant, float sampleRate)
     // FIXME: replace hardcode 2.718282 with M_E until the correct MathExtras.h solution is determined.
     return 1 - powf(1 / 2.718282f, 1 / (sampleRate * timeConstant));
 }
-    
+
+#if OS(WINDOWS) && COMPILER(MSVC) && !_M_IX86_FP
+// When compiling with MSVC with x87 FPU instructions using 80-bit
+// floats, we want very precise control over the arithmetic so that
+// rounding is done according to the IEEE 754 specification for
+// single- and double-precision floats. We want each operation to be
+// done with specified arithmetic precision and rounding consistent
+// with gcc, not extended to 80 bits automatically.
+//
+// These pragmas are equivalent to /fp:strict flag, but we only need
+// it for the function here.  (Using fp:strict everywhere can have
+// severe impact on floating point performance.)
+#pragma float_control(push)
+#pragma float_control(precise, on)
+#pragma fenv_access(on)
+#pragma float_control(except, on)
+#endif
+
+size_t timeToSampleFrame(double time, double sampleRate)
+{
+    // DO NOT CONSOLIDATE THESE ASSIGNMENTS INTO ONE! When compiling
+    // with Visual Studio, these assignments force the rounding of
+    // each operation according to IEEE 754, instead of leaving
+    // intermediate results in 80-bit precision which is not
+    // consistent with IEEE 754 double-precision rounding.
+    double r = time * sampleRate;
+    r += 0.5;
+    return static_cast<size_t>(r);
+}
+#if OS(WINDOWS) && COMPILER(MSVC) && !_M_IX86_FP
+// Restore normal floating-point semantics.
+#pragma float_control(pop)
+#endif
 } // AudioUtilites
 
 } // WebCore
index c98a4c8..ed2945f 100644 (file)
@@ -37,7 +37,9 @@ float decibelsToLinear(float);
 // to reach the value 1 - 1/e (around 63.2%) given a step input response.
 // discreteTimeConstantForSampleRate() will return the discrete time-constant for the specific sampleRate.
 float discreteTimeConstantForSampleRate(float timeConstant, float sampleRate);
-    
+
+// Convert the time to a sample frame at the given sample rate.
+size_t timeToSampleFrame(double time, double sampleRate);
 } // AudioUtilites
 
 } // WebCore
index a0cb13a..db9d3ed 100644 (file)
@@ -30,6 +30,7 @@
 
 #include "AudioContext.h"
 #include "AudioNodeOutput.h"
+#include "AudioUtilities.h"
 #include "Document.h"
 #include "FloatConversion.h"
 #include "ScriptCallStack.h"
@@ -101,26 +102,27 @@ void AudioBufferSourceNode::process(size_t framesToProcess)
     // Careful - this is a tryLock() and not an autolocker, so we must unlock() before every return.
     if (m_processLock.tryLock()) {
         // Check if it's time to start playing.
-        float sampleRate = this->sampleRate();
-        double quantumStartTime = context()->currentTime();
-        double quantumEndTime = quantumStartTime + framesToProcess / sampleRate;
+        double sampleRate = this->sampleRate();
+        size_t quantumStartFrame = context()->currentSampleFrame();
+        size_t quantumEndFrame = quantumStartFrame + framesToProcess;
+        size_t startFrame = AudioUtilities::timeToSampleFrame(m_startTime, sampleRate);
+        size_t endFrame = m_endTime == UnknownTime ? 0 : AudioUtilities::timeToSampleFrame(m_endTime, sampleRate);
 
         // If we know the end time and it's already passed, then don't bother doing any more rendering this cycle.
-        if (m_endTime != UnknownTime && m_endTime <= quantumStartTime) {
+        if (m_endTime != UnknownTime && endFrame <= quantumStartFrame) {
             m_isPlaying = false;
             m_virtualReadIndex = 0;
             finish();
         }
         
-        if (!m_isPlaying || m_hasFinished || !buffer() || m_startTime >= quantumEndTime) {
+        if (!m_isPlaying || m_hasFinished || !buffer() || startFrame >= quantumEndFrame) {
             // FIXME: can optimize here by propagating silent hint instead of forcing the whole chain to process silence.
             outputBus->zero();
             m_processLock.unlock();
             return;
         }
 
-        double quantumTimeOffset = m_startTime > quantumStartTime ? m_startTime - quantumStartTime : 0;
-        size_t quantumFrameOffset = static_cast<unsigned>(quantumTimeOffset * sampleRate);
+        size_t quantumFrameOffset = startFrame > quantumStartFrame ? startFrame - quantumStartFrame : 0;
         quantumFrameOffset = min(quantumFrameOffset, framesToProcess); // clamp to valid range
         size_t bufferFramesToProcess = framesToProcess - quantumFrameOffset;
 
@@ -133,8 +135,8 @@ void AudioBufferSourceNode::process(size_t framesToProcess)
 
         // If the end time is somewhere in the middle of this time quantum, then simply zero out the
         // frames starting at the end time.
-        if (m_endTime != UnknownTime && m_endTime >= quantumStartTime && m_endTime < quantumEndTime) {
-            size_t zeroStartFrame = narrowPrecisionToFloat((m_endTime - quantumStartTime) * sampleRate);
+        if (m_endTime != UnknownTime && endFrame >= quantumStartFrame && endFrame < quantumEndFrame) {
+            size_t zeroStartFrame = endFrame - quantumStartFrame;
             size_t framesToZero = framesToProcess - zeroStartFrame;
 
             bool isSafe = zeroStartFrame < framesToProcess && framesToZero <= framesToProcess && zeroStartFrame + framesToZero <= framesToProcess;
@@ -237,8 +239,11 @@ void AudioBufferSourceNode::renderFromBuffer(AudioBus* bus, unsigned destination
 
     // Calculate the start and end frames in our buffer that we want to play.
     // If m_isGrain is true, then we will be playing a portion of the total buffer.
-    unsigned startFrame = m_isGrain ? static_cast<unsigned>(m_grainOffset * bufferSampleRate) : 0;
-    unsigned endFrame = m_isGrain ? static_cast<unsigned>(startFrame + m_grainDuration * bufferSampleRate) : bufferLength;
+    unsigned startFrame = m_isGrain ? AudioUtilities::timeToSampleFrame(m_grainOffset, bufferSampleRate) : 0;
+
+    // Avoid converting from time to sample-frames twice by computing
+    // the grain end time first before computing the sample frame.
+    unsigned endFrame = m_isGrain ? AudioUtilities::timeToSampleFrame(m_grainOffset + m_grainDuration, bufferSampleRate) : bufferLength;
     
     ASSERT(endFrame >= startFrame);
     if (endFrame < startFrame)
@@ -431,11 +436,11 @@ void AudioBufferSourceNode::noteGrainOn(double when, double grainOffset, double
     m_isGrain = true;
     m_startTime = when;
 
-    // We call floor() here since at playbackRate == 1 we don't want to go through linear interpolation
+    // We call timeToSampleFrame here since at playbackRate == 1 we don't want to go through linear interpolation
     // at a sub-sample position since it will degrade the quality.
     // When aligned to the sample-frame the playback will be identical to the PCM data stored in the buffer.
     // Since playbackRate == 1 is very common, it's worth considering quality.
-    m_virtualReadIndex = floor(m_grainOffset * buffer()->sampleRate());
+    m_virtualReadIndex = AudioUtilities::timeToSampleFrame(m_grainOffset, buffer()->sampleRate());
     
     m_isPlaying = true;
 }
index ba7c538..7911519 100644 (file)
@@ -93,6 +93,7 @@ public:
     bool hasDocument();
 
     AudioDestinationNode* destination() { return m_destinationNode.get(); }
+    size_t currentSampleFrame() { return m_destinationNode->currentSampleFrame(); }
     double currentTime() { return m_destinationNode->currentTime(); }
     float sampleRate() { return m_destinationNode->sampleRate(); }
 
index ff9ebbd..6e527bb 100644 (file)
 #include "AudioContext.h"
 #include "AudioNodeInput.h"
 #include "AudioNodeOutput.h"
+#include "AudioUtilities.h"
 #include "DenormalDisabler.h"
 
 namespace WebCore {
     
 AudioDestinationNode::AudioDestinationNode(AudioContext* context, float sampleRate)
     : AudioNode(context, sampleRate)
-    , m_currentTime(0.0)
+    , m_currentSampleFrame(0)
 {
     addInput(adoptPtr(new AudioNodeInput(this)));
     
@@ -82,8 +83,8 @@ void AudioDestinationNode::provideInput(AudioBus* destinationBus, size_t numberO
     // Let the context take care of any business at the end of each render quantum.
     context()->handlePostRenderTasks();
     
-    // Advance current time.
-    m_currentTime += numberOfFrames / sampleRate();
+    // Advance current sample-frame.
+    m_currentSampleFrame += numberOfFrames;
 }
 
 } // namespace WebCore
index d7bc7bc..581edd8 100644 (file)
@@ -41,12 +41,13 @@ public:
     
     // AudioNode   
     virtual void process(size_t) { }; // we're pulled by hardware so this is never called
-    virtual void reset() { m_currentTime = 0.0; };
+    virtual void reset() { m_currentSampleFrame = 0; };
     
     // The audio hardware calls here periodically to gets its input stream.
     virtual void provideInput(AudioBus*, size_t numberOfFrames);
 
-    double currentTime() { return m_currentTime; }
+    size_t currentSampleFrame() { return m_currentSampleFrame; }
+    double currentTime() { return currentSampleFrame() / static_cast<double>(sampleRate()); }
 
     virtual float sampleRate() const = 0;
 
@@ -55,7 +56,8 @@ public:
     virtual void startRendering() = 0;
     
 protected:
-    double m_currentTime;
+    // Counts the number of sample-frames processed by the destination.
+    size_t m_currentSampleFrame;
 };
 
 } // namespace WebCore
index 30d6192..eff1ea7 100644 (file)
@@ -157,13 +157,6 @@ float AudioParamTimeline::valuesForTimeRange(float startTime,
     return value;
 }
 
-// Returns the rounded down integer sample-frame for the time and sample-rate.
-static unsigned timeToSampleFrame(double time, float sampleRate)
-{
-    double k = 0.5 / sampleRate;
-    return static_cast<unsigned>((time + k) * sampleRate);
-}
-
 float AudioParamTimeline::valuesForTimeRangeImpl(float startTime,
                                                  float endTime,
                                                  float defaultValue,
@@ -193,7 +186,7 @@ float AudioParamTimeline::valuesForTimeRangeImpl(float startTime,
     float firstEventTime = m_events[0].time();
     if (firstEventTime > startTime) {
         float fillToTime = min(endTime, firstEventTime);
-        unsigned fillToFrame = timeToSampleFrame(fillToTime - startTime, sampleRate);
+        unsigned fillToFrame = AudioUtilities::timeToSampleFrame(fillToTime - startTime, sampleRate);
         fillToFrame = min(fillToFrame, numberOfValues);
         for (; writeIndex < fillToFrame; ++writeIndex)
             values[writeIndex] = defaultValue;
@@ -226,7 +219,7 @@ float AudioParamTimeline::valuesForTimeRangeImpl(float startTime,
         float sampleFrameTimeIncr = 1 / sampleRate;
 
         float fillToTime = min(endTime, time2);
-        unsigned fillToFrame = timeToSampleFrame(fillToTime - startTime, sampleRate);
+        unsigned fillToFrame = AudioUtilities::timeToSampleFrame(fillToTime - startTime, sampleRate);
         fillToFrame = min(fillToFrame, numberOfValues);
 
         ParamEvent::Type nextEventType = nextEvent ? static_cast<ParamEvent::Type>(nextEvent->type()) : ParamEvent::LastType /* unknown */;
@@ -313,7 +306,7 @@ float AudioParamTimeline::valuesForTimeRangeImpl(float startTime,
                     unsigned nextEventFillToFrame = fillToFrame;
                     float nextEventFillToTime = fillToTime;
                     fillToTime = min(endTime, time1 + duration);
-                    fillToFrame = timeToSampleFrame(fillToTime - startTime, sampleRate);
+                    fillToFrame = AudioUtilities::timeToSampleFrame(fillToTime - startTime, sampleRate);
                     fillToFrame = min(fillToFrame, numberOfValues);
 
                     // Index into the curve data using a floating-point value.