From 13ecd3edb5ba6ae3f314791bd2ad02b41198e2cc Mon Sep 17 00:00:00 2001 From: Paul Adenot Date: Thu, 30 Jul 2015 13:51:57 +0200 Subject: [PATCH] Bug 901633 - Part 1 - Implement a generic audio packetizer. r=jesup --- dom/media/AudioPacketizer.h | 186 ++++++++++++++++++ .../compiledtest/TestAudioPacketizer.cpp | 172 ++++++++++++++++ dom/media/compiledtest/moz.build | 3 +- dom/media/moz.build | 1 + 4 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 dom/media/AudioPacketizer.h create mode 100644 dom/media/compiledtest/TestAudioPacketizer.cpp diff --git a/dom/media/AudioPacketizer.h b/dom/media/AudioPacketizer.h new file mode 100644 index 00000000000..18adedf2424 --- /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 +#include +#include +#include + +// 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 +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 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(mStorage.get() + ReadIndex(), + out, + samplesNeeded); + } else { + uint32_t firstPartLength = mLength - ReadIndex(); + uint32_t secondPartLength = samplesNeeded - firstPartLength; + ConvertAudioSamples(mStorage.get() + ReadIndex(), + out, + firstPartLength); + ConvertAudioSamples(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 mStorage; + // Length of the buffer, in samples + uint32_t mLength; +}; + +} // mozilla + +#endif // AudioPacketizer_h_ diff --git a/dom/media/compiledtest/TestAudioPacketizer.cpp b/dom/media/compiledtest/TestAudioPacketizer.cpp new file mode 100644 index 00000000000..d70ef8fca23 --- /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 +#include +#include "../AudioPacketizer.h" + +using namespace mozilla; + +template +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(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 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 ap(441, channels); + int16_t seqEnd = 0; + for (int16_t i = 0; i < 10; i++) { + AutoBuffer 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 ap(441, channels); + int16_t seqEnd = 0; + for (int16_t i = 0; i < 10; i++) { + AutoBuffer b(441 * channels); + AutoBuffer 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 ap(441, channels); + int16_t prevEnd = 0; + int16_t prevSeq = 0; + for (int16_t i = 0; i < 10; i++) { + AutoBuffer b(480 * channels); + AutoBuffer 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 ap(441, channels); + AutoBuffer 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(((2 << 14) * sine(outPhase)))); + } + outPhase++; + } + delete [] packet; + } + } + } + } + + printf("OK\n"); + return 0; +} diff --git a/dom/media/compiledtest/moz.build b/dom/media/compiledtest/moz.build index 2f9549279db..985ce6a8f1b 100644 --- a/dom/media/compiledtest/moz.build +++ b/dom/media/compiledtest/moz.build @@ -6,7 +6,8 @@ GeckoCppUnitTests([ 'TestAudioBuffers', - 'TestAudioMixer' + 'TestAudioMixer', + 'TestAudioPacketizer' ]) LOCAL_INCLUDES += [ diff --git a/dom/media/moz.build b/dom/media/moz.build index af2e22fa7a8..c4e07581a09 100644 --- a/dom/media/moz.build +++ b/dom/media/moz.build @@ -101,6 +101,7 @@ EXPORTS += [ 'AudioChannelFormat.h', 'AudioCompactor.h', 'AudioMixer.h', + 'AudioPacketizer.h', 'AudioSampleFormat.h', 'AudioSegment.h', 'AudioStream.h',