Bug 901633 - Part 1 - Implement a generic audio packetizer. r=jesup
authorPaul Adenot <paul@paul.cx>
Thu, 30 Jul 2015 13:51:57 +0200
changeset 260323 5b18471f2ff788f41da56325bef5c9d6a012ec74
parent 260322 4f33bf3be1d07b080197cd8231c6365673c25fd7
child 260324 19dec0df775cde66e99c6861a858c2e7ab7ddd8c
push id29307
push userryanvm@gmail.com
push dateWed, 02 Sep 2015 01:01:53 +0000
treeherdermozilla-central@e2eb0442ece9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjesup
bugs901633
milestone43.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 901633 - Part 1 - Implement a generic audio packetizer. r=jesup
dom/media/AudioPacketizer.h
dom/media/compiledtest/TestAudioPacketizer.cpp
dom/media/compiledtest/moz.build
dom/media/moz.build
new file mode 100644
--- /dev/null
+++ b/dom/media/AudioPacketizer.h
@@ -0,0 +1,186 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioPacketizer_h_
+#define AudioPacketizer_h_
+
+#include <mozilla/PodOperations.h>
+#include <mozilla/Assertions.h>
+#include <nsAutoPtr.h>
+#include <AudioSampleFormat.h>
+
+// Enable this to warn when `Output` has been called but not enough data was
+// buffered.
+// #define LOG_PACKETIZER_UNDERRUN
+
+namespace mozilla {
+/**
+ * This class takes arbitrary input data, and returns packets of a specific
+ * size. In the process, it can convert audio samples from 16bit integers to
+ * float (or vice-versa).
+ *
+ * Input and output, as well as length units in the public interface are
+ * interleaved frames.
+ *
+ * Allocations of output buffer are performed by this class.  Buffers can simply
+ * be delete-d.  This is because packets are intended to be sent off to
+ * non-gecko code using normal pointers/length pairs
+ *
+ * The implementation uses a circular buffer using absolute virtual indices.
+ */
+template <typename InputType, typename OutputType>
+class AudioPacketizer
+{
+public:
+  AudioPacketizer(uint32_t aPacketSize, uint32_t aChannels)
+    : mPacketSize(aPacketSize)
+    , mChannels(aChannels)
+    , mReadIndex(0)
+    , mWriteIndex(0)
+    // Start off with a single packet
+    , mStorage(new InputType[aPacketSize * aChannels])
+    , mLength(aPacketSize * aChannels)
+  {
+     MOZ_ASSERT(aPacketSize > 0 && aChannels > 0,
+       "The packet size and the number of channel should be strictly positive");
+  }
+
+  void Input(InputType* aFrames, uint32_t aFrameCount)
+  {
+    uint32_t inputSamples = aFrameCount * mChannels;
+    // Need to grow the storage. This should rarely happen, if at all, once the
+    // array has the right size.
+    if (inputSamples > EmptySlots()) {
+      // Calls to Input and Output are roughtly interleaved
+      // (Input,Output,Input,Output, etc.), or balanced
+      // (Input,Input,Input,Output,Output,Output), so we update the buffer to
+      // the exact right size in order to not waste space.
+      uint32_t newLength = AvailableSamples() + inputSamples;
+      uint32_t toCopy = AvailableSamples();
+      nsAutoPtr<InputType> oldStorage = mStorage;
+      mStorage = new InputType[newLength];
+      // Copy the old data at the beginning of the new storage.
+      if (WriteIndex() >= ReadIndex()) {
+        PodCopy(mStorage.get(),
+                oldStorage.get() + ReadIndex(),
+                AvailableSamples());
+      } else {
+        uint32_t firstPartLength = mLength - ReadIndex();
+        uint32_t secondPartLength = AvailableSamples() - firstPartLength;
+        PodCopy(mStorage.get(),
+                oldStorage.get() + ReadIndex(),
+                firstPartLength);
+        PodCopy(mStorage.get() + firstPartLength,
+                oldStorage.get(),
+                secondPartLength);
+      }
+      mWriteIndex = toCopy;
+      mReadIndex = 0;
+      mLength = newLength;
+    }
+
+    if (WriteIndex() + inputSamples <= mLength) {
+      PodCopy(mStorage.get() + WriteIndex(), aFrames, aFrameCount * mChannels);
+    } else {
+      uint32_t firstPartLength = mLength - WriteIndex();
+      uint32_t secondPartLength = inputSamples - firstPartLength;
+      PodCopy(mStorage.get() + WriteIndex(), aFrames, firstPartLength);
+      PodCopy(mStorage.get(), aFrames + firstPartLength, secondPartLength);
+    }
+
+    mWriteIndex += inputSamples;
+  }
+
+  OutputType* Output()
+  {
+    uint32_t samplesNeeded = mPacketSize * mChannels;
+    OutputType* out = new OutputType[samplesNeeded];
+
+    // Under-run. Pad the end of the buffer with silence.
+    if (AvailableSamples() < samplesNeeded) {
+#ifdef LOG_PACKETIZER_UNDERRUN
+      char buf[256];
+      snprintf(buf, 256,
+               "AudioPacketizer %p underrun: available: %u, needed: %u\n",
+               this, AvailableSamples(), samplesNeeded);
+      NS_WARNING(buf);
+#endif
+      uint32_t zeros = samplesNeeded - AvailableSamples();
+      PodZero(out + AvailableSamples(), zeros);
+      samplesNeeded -= zeros;
+    }
+    if (ReadIndex() + samplesNeeded <= mLength) {
+      ConvertAudioSamples<InputType,OutputType>(mStorage.get() + ReadIndex(),
+                                                out,
+                                                samplesNeeded);
+    } else {
+      uint32_t firstPartLength = mLength - ReadIndex();
+      uint32_t secondPartLength = samplesNeeded - firstPartLength;
+      ConvertAudioSamples<InputType, OutputType>(mStorage.get() + ReadIndex(),
+                                                 out,
+                                                 firstPartLength);
+      ConvertAudioSamples<InputType, OutputType>(mStorage.get(),
+                                                 out + firstPartLength,
+                                                 secondPartLength);
+    }
+    mReadIndex += samplesNeeded;
+
+    return out;
+  }
+
+  uint32_t PacketsAvailable() const {
+    return AvailableSamples() / mChannels / mPacketSize;
+  }
+
+  bool Empty() const {
+   return mWriteIndex == mReadIndex;
+  }
+
+  bool Full() const {
+    return mWriteIndex - mReadIndex == mLength;
+  }
+
+  uint32_t PacketSize() const {
+    return mPacketSize;
+  }
+
+  uint32_t Channels() const {
+    return mChannels;
+  }
+
+private:
+  uint32_t ReadIndex() const {
+    return mReadIndex % mLength;
+  }
+
+  uint32_t WriteIndex() const {
+    return mWriteIndex % mLength;
+  }
+
+  uint32_t AvailableSamples() const {
+    return mWriteIndex - mReadIndex;
+  }
+
+  uint32_t EmptySlots() const {
+    return mLength - AvailableSamples();
+  }
+
+  // Size of one packet of audio, in frames
+  uint32_t mPacketSize;
+  // Number of channels of the stream flowing through this packetizer
+  uint32_t mChannels;
+  // Two virtual index into the buffer: the read position and the write
+  // position.
+  uint64_t mReadIndex;
+  uint64_t mWriteIndex;
+  // Storage for the samples
+  nsAutoPtr<InputType> mStorage;
+  // Length of the buffer, in samples
+  uint32_t mLength;
+};
+
+} // mozilla
+
+#endif // AudioPacketizer_h_
new file mode 100644
--- /dev/null
+++ b/dom/media/compiledtest/TestAudioPacketizer.cpp
@@ -0,0 +1,172 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stdint.h>
+#include <assert.h>
+#include "../AudioPacketizer.h"
+
+using namespace mozilla;
+
+template<typename T>
+class AutoBuffer
+{
+public:
+  AutoBuffer(size_t aLength)
+  {
+    mStorage = new T[aLength];
+  }
+  ~AutoBuffer() {
+    delete [] mStorage;
+  }
+  T* Get() {
+    return mStorage;
+  }
+private:
+  T* mStorage;
+};
+
+int16_t Sequence(int16_t* aBuffer, uint32_t aSize, uint32_t aStart = 0)
+{
+  uint32_t i;
+  for (i = 0; i < aSize; i++) {
+    aBuffer[i] = aStart + i;
+  }
+  return aStart + i;
+}
+
+void IsSequence(int16_t* aBuffer, uint32_t aSize, uint32_t aStart = 0)
+{
+  for (uint32_t i = 0; i < aSize; i++) {
+    if (aBuffer[i] != static_cast<int64_t>(aStart + i)) {
+      fprintf(stderr, "Buffer is not a sequence at offset %u\n", i);
+      assert(false);
+    }
+  }
+  assert("Buffer is a sequence.");
+}
+
+void Zero(int16_t* aBuffer, uint32_t aSize)
+{
+  for (uint32_t i = 0; i < aSize; i++) {
+    if (aBuffer[i] != 0) {
+      fprintf(stderr, "Buffer is not null at offset %u\n", i);
+      assert(false);
+    }
+  }
+}
+
+double sine(uint32_t aPhase) {
+ return sin(aPhase * 2 * M_PI * 440 / 44100);
+}
+
+int main() {
+  for (int16_t channels = 1; channels < 2; channels++) {
+    // Test that the packetizer returns zero on underrun
+    {
+      AudioPacketizer<int16_t, int16_t> ap(441, channels);
+      for (int16_t i = 0; i < 10; i++) {
+        int16_t* out = ap.Output();
+        Zero(out, 441);
+        delete out;
+      }
+    }
+    // Simple test, with input/output buffer size aligned on the packet size,
+    // alternating Input and Output calls.
+    {
+      AudioPacketizer<int16_t, int16_t> ap(441, channels);
+      int16_t seqEnd = 0;
+      for (int16_t i = 0; i < 10; i++) {
+        AutoBuffer<int16_t> b(441 * channels);
+        int16_t prevEnd = seqEnd;
+        seqEnd = Sequence(b.Get(), channels * 441, prevEnd);
+        ap.Input(b.Get(), 441);
+        int16_t* out = ap.Output();
+        IsSequence(out, 441 * channels, prevEnd);
+        delete out;
+      }
+    }
+    // Simple test, with input/output buffer size aligned on the packet size,
+    // alternating two Input and Output calls.
+    {
+      AudioPacketizer<int16_t, int16_t> ap(441, channels);
+      int16_t seqEnd = 0;
+      for (int16_t i = 0; i < 10; i++) {
+        AutoBuffer<int16_t> b(441 * channels);
+        AutoBuffer<int16_t> b1(441 * channels);
+        int16_t prevEnd0 = seqEnd;
+        seqEnd = Sequence(b.Get(), 441 * channels, prevEnd0);
+        int16_t prevEnd1 = seqEnd;
+        seqEnd = Sequence(b1.Get(), 441 * channels, seqEnd);
+        ap.Input(b.Get(), 441);
+        ap.Input(b1.Get(), 441);
+        int16_t* out = ap.Output();
+        int16_t* out2 = ap.Output();
+        IsSequence(out, 441 * channels, prevEnd0);
+        IsSequence(out2, 441 * channels, prevEnd1);
+        delete out;
+        delete out2;
+      }
+    }
+    // Input/output buffer size not aligned on the packet size,
+    // alternating two Input and Output calls.
+    {
+      AudioPacketizer<int16_t, int16_t> ap(441, channels);
+      int16_t prevEnd = 0;
+      int16_t prevSeq = 0;
+      for (int16_t i = 0; i < 10; i++) {
+        AutoBuffer<int16_t> b(480 * channels);
+        AutoBuffer<int16_t> b1(480 * channels);
+        prevSeq = Sequence(b.Get(), 480 * channels, prevSeq);
+        prevSeq = Sequence(b1.Get(), 480 * channels, prevSeq);
+        ap.Input(b.Get(), 480);
+        ap.Input(b1.Get(), 480);
+        int16_t* out = ap.Output();
+        int16_t* out2 = ap.Output();
+        IsSequence(out, 441 * channels, prevEnd);
+        prevEnd += 441 * channels;
+        IsSequence(out2, 441 * channels, prevEnd);
+        prevEnd += 441 * channels;
+        delete out;
+        delete out2;
+      }
+      printf("Available: %d\n", ap.PacketsAvailable());
+    }
+
+    // "Real-life" test case: streaming a sine wave through a packetizer, and
+    // checking that we have the right output.
+    // 128 is, for example, the size of a Web Audio API block, and 441 is the
+    // size of a webrtc.org packet when the sample rate is 44100 (10ms)
+    {
+      AudioPacketizer<int16_t, int16_t> ap(441, channels);
+      AutoBuffer<int16_t> b(128 * channels);
+      uint32_t phase = 0;
+      uint32_t outPhase = 0;
+      for (int16_t i = 0; i < 1000; i++) {
+        for (int32_t j = 0; j < 128; j++) {
+          for (int32_t c = 0; c < channels; c++) {
+            // int16_t sinewave at 440Hz/44100Hz sample rate
+            b.Get()[j * channels + c] = (2 << 14) * sine(phase);
+          }
+          phase++;
+        }
+        ap.Input(b.Get(), 128);
+        while (ap.PacketsAvailable()) {
+          int16_t* packet = ap.Output();
+          for (uint32_t k = 0; k < ap.PacketSize(); k++) {
+            for (int32_t c = 0; c < channels; c++) {
+              assert(packet[k * channels + c] ==
+                     static_cast<int16_t>(((2 << 14) * sine(outPhase))));
+            }
+            outPhase++;
+          }
+          delete [] packet;
+        }
+      }
+    }
+  }
+
+  printf("OK\n");
+  return 0;
+}
--- a/dom/media/compiledtest/moz.build
+++ b/dom/media/compiledtest/moz.build
@@ -1,17 +1,18 @@
 # -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 GeckoCppUnitTests([
     'TestAudioBuffers',
-    'TestAudioMixer'
+    'TestAudioMixer',
+    'TestAudioPacketizer'
 ])
 
 LOCAL_INCLUDES += [
     '..',
 ]
 
 USE_LIBS += [
     'lgpllibs',
--- a/dom/media/moz.build
+++ b/dom/media/moz.build
@@ -96,16 +96,17 @@ XPIDL_SOURCES += [
 XPIDL_MODULE = 'dom_media'
 
 EXPORTS += [
     'AbstractMediaDecoder.h',
     'AudioBufferUtils.h',
     'AudioChannelFormat.h',
     'AudioCompactor.h',
     'AudioMixer.h',
+    'AudioPacketizer.h',
     'AudioSampleFormat.h',
     'AudioSegment.h',
     'AudioStream.h',
     'BufferMediaResource.h',
     'CubebUtils.h',
     'DecodedStream.h',
     'DecoderTraits.h',
     'DOMMediaStream.h',