Commit 40c0897e authored by Yuri Wiitala's avatar Yuri Wiitala Committed by Commit Bot

Add DelayBuffer utility class for later loopback functionality.

The DelayBuffer records and maintains a recent history of an audio
signal, then allows read-back of any part of the recording. While this
looks a lot like "yet another FIFO," it is not because the reads will
typically be somewhere in the middle, not the end, of the circular
queue.

This will be used in a later change to support loopback functionality in
the Audio Service.

Bug: 824019
Change-Id: I5321724ac3a55929318d411a08d33344a39c2272
Reviewed-on: https://chromium-review.googlesource.com/1029617Reviewed-by: default avatarOlga Sharonova <olka@chromium.org>
Reviewed-by: default avatarXiangjun Zhang <xjz@chromium.org>
Reviewed-by: default avatarYuri Wiitala <miu@chromium.org>
Commit-Queue: Yuri Wiitala <miu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#556334}
parent 5bab4996
...@@ -32,6 +32,8 @@ source_set("lib") { ...@@ -32,6 +32,8 @@ source_set("lib") {
sources = [ sources = [
"debug_recording.cc", "debug_recording.cc",
"debug_recording.h", "debug_recording.h",
"delay_buffer.cc",
"delay_buffer.h",
"group_coordinator.cc", "group_coordinator.cc",
"group_coordinator.h", "group_coordinator.h",
"group_member.h", "group_member.h",
...@@ -74,6 +76,7 @@ source_set("tests") { ...@@ -74,6 +76,7 @@ source_set("tests") {
sources = [ sources = [
"debug_recording_unittest.cc", "debug_recording_unittest.cc",
"delay_buffer_unittest.cc",
"group_coordinator_unittest.cc", "group_coordinator_unittest.cc",
"input_stream_unittest.cc", "input_stream_unittest.cc",
"local_muter_unittest.cc", "local_muter_unittest.cc",
......
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/audio/delay_buffer.h"
#include <algorithm>
#include <utility>
#include "base/numerics/safe_conversions.h"
#include "media/base/audio_bus.h"
#include "media/base/vector_math.h"
namespace audio {
DelayBuffer::DelayBuffer(int history_size) : history_size_(history_size) {}
DelayBuffer::~DelayBuffer() = default;
void DelayBuffer::Write(FrameTicks position,
const media::AudioBus& input_bus,
double volume) {
DCHECK(chunks_.empty() || chunks_.back().GetEndPosition() <= position);
// Prune-out the oldest InputChunks, but ensure that this DelayBuffer is
// maintaining at least |history_size_| frames in total when this method
// returns (i.e., after the current chunk is inserted).
const FrameTicks prune_position =
position + input_bus.frames() - history_size_;
while (!chunks_.empty() &&
chunks_.front().GetEndPosition() <= prune_position) {
chunks_.pop_front();
}
// Make a copy of the AudioBus for later consumption. Apply the volume setting
// by scaling the audio signal during the copy.
auto copy = media::AudioBus::Create(input_bus.channels(), input_bus.frames());
for (int ch = 0; ch < input_bus.channels(); ++ch) {
media::vector_math::FMUL(input_bus.channel(ch), volume, input_bus.frames(),
copy->channel(ch));
}
chunks_.emplace_back(position, std::move(copy));
}
void DelayBuffer::Read(FrameTicks from,
int frames_to_read,
media::AudioBus* output_bus) {
DCHECK_LE(frames_to_read, output_bus->frames());
// Remove all of the oldest chunks until the one in front contains the |from|
// position (or is the first chunk after it).
while (!chunks_.empty() && chunks_.front().GetEndPosition() <= from) {
chunks_.pop_front();
}
// Loop, transferring data from each InputChunk to the output AudioBus until
// the requested number of frames have been read.
for (int frames_remaining = frames_to_read; frames_remaining > 0;) {
const int dest_offset = frames_to_read - frames_remaining;
// If attempting to read past the end of the recorded signal, zero-pad the
// rest of the output and return.
if (chunks_.empty()) {
output_bus->ZeroFramesPartial(dest_offset, frames_remaining);
return;
}
const InputChunk& chunk = chunks_.front();
// This is the offset to the frame within the chunk's AudioBus that
// corresponds to the offset in the output AudioBus. If this calculated
// value is out-of-range, there is a gap (i.e., a missing piece of audio
// signal) in the recording.
const int source_offset =
base::saturated_cast<int>(from + dest_offset - chunk.position);
if (source_offset < 0) {
// There is a gap in the recording. Fill zeroes in the corresponding part
// of the output.
const int frames_to_zero_fill = (source_offset + frames_remaining <= 0)
? frames_remaining
: -source_offset;
output_bus->ZeroFramesPartial(dest_offset, frames_to_zero_fill);
frames_remaining -= frames_to_zero_fill;
continue;
}
DCHECK_LE(source_offset, chunk.bus->frames());
// Copy some or all of the frames in the current chunk to the output; the
// lesser of: a) the frames available in the chunk, or b) the frames
// remaining to output.
const int frames_to_copy_from_chunk = chunk.bus->frames() - source_offset;
if (frames_to_copy_from_chunk <= frames_remaining) {
chunk.bus->CopyPartialFramesTo(source_offset, frames_to_copy_from_chunk,
dest_offset, output_bus);
frames_remaining -= frames_to_copy_from_chunk;
chunks_.pop_front(); // All frames from this chunk have been consumed.
} else {
chunk.bus->CopyPartialFramesTo(source_offset, frames_remaining,
dest_offset, output_bus);
return; // The |output_bus| has been fully populated.
}
}
}
DelayBuffer::FrameTicks DelayBuffer::GetBeginPosition() const {
return chunks_.empty() ? 0 : chunks_.front().position;
}
DelayBuffer::FrameTicks DelayBuffer::GetEndPosition() const {
return chunks_.empty() ? 0 : chunks_.back().GetEndPosition();
}
DelayBuffer::InputChunk::InputChunk(FrameTicks p,
std::unique_ptr<media::AudioBus> b)
: position(p), bus(std::move(b)) {}
DelayBuffer::InputChunk::InputChunk(DelayBuffer::InputChunk&&) = default;
DelayBuffer::InputChunk& DelayBuffer::InputChunk::operator=(
DelayBuffer::InputChunk&&) = default;
DelayBuffer::InputChunk::~InputChunk() = default;
DelayBuffer::FrameTicks DelayBuffer::InputChunk::GetEndPosition() const {
return position + bus->frames();
}
} // namespace audio
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SERVICES_AUDIO_DELAY_BUFFER_H_
#define SERVICES_AUDIO_DELAY_BUFFER_H_
#include <memory>
#include "base/containers/circular_deque.h"
#include "base/macros.h"
namespace media {
class AudioBus;
} // namespace media
namespace audio {
// Records and maintains a recent history of an audio signal, then allows
// read-back starting from part of the recording. While this looks a lot like a
// FIFO, it is not because Read() will typically not be reading from one end of
// a queue.
//
// The audio format is the same throughout all operations, as this DelayBuffer
// does not resample or remix the audio. Also, for absolute precision, it uses
// frame counts to track the timing of the audio recorded and read.
//
// The typical use case is the loopback audio system: In this scenario, the
// service has an audio output stream running for local playback, and the
// stream's audio is timed to play back in the near future (usually, 1 ms to 20
// ms, depending on the platform). When loopback is active, that audio will be
// copied into this DelayBuffer via calls to Write(). Then, the loopback audio
// stream implementation will Read() the audio at a time in the recent past
// (approximately 20 ms before "now," but this will vary slightly). Because of
// clock drift concerns, the loopback implementation will slightly compress/
// stretch the audio signal it pulls out of this buffer, to maintain
// synchronization, and this will cause it to vary the number of frames read for
// each successive Read() call.
class DelayBuffer {
public:
// Use sample counts as a measure of audio signal position.
using FrameTicks = int64_t;
// Construct a DelayBuffer that keeps at least |history_size| un-read frames
// recorded.
explicit DelayBuffer(int history_size);
~DelayBuffer();
// Inserts a copy of the given audio into the buffer. |position| must be
// monotonically increasing, and the audio must not overlap any
// previously-written audio. The length of the |input_bus| may vary, but the
// channel layout may not. The given |volume| will be used to scale the audio
// during the copy to the internal buffer.
void Write(FrameTicks position,
const media::AudioBus& input_bus,
double volume);
// Reads audio from the buffer, starting at the given |from| position, which
// must not overlap any previously-read audio. |frames_to_read| is the number
// of frames to read, which may be any amount less than or equal to the size
// of the |output_bus|. No part of the |output_bus| beyond the first
// |frames_to_read| will be modified. If there are gaps (i.e., missing pieces)
// of the recording, zeros will be filled in the output.
void Read(FrameTicks from, int frames_to_read, media::AudioBus* output_bus);
// Returns the current buffered range of the recording, ala the usual C++
// [begin,end) semantics.
FrameTicks GetBeginPosition() const;
FrameTicks GetEndPosition() const;
private:
struct InputChunk {
// The position of the first frame in this chunk.
FrameTicks position;
// The storage for the audio frames.
std::unique_ptr<media::AudioBus> bus;
// Constructor for an InputChunk with data.
InputChunk(FrameTicks p, std::unique_ptr<media::AudioBus> b);
// Move constructor/assignment.
InputChunk(InputChunk&& other);
InputChunk& operator=(InputChunk&& other);
~InputChunk();
// Returns the position just after the last frame's position.
FrameTicks GetEndPosition() const;
private:
DISALLOW_COPY_AND_ASSIGN(InputChunk);
};
// The minimum number of un-read frames that must be kept.
const int history_size_;
// A queue storing each chunk of recorded audio. The elements in the queue are
// always in-order, chronologically increasing by InputChunk::position, and do
// not overlap.
base::circular_deque<InputChunk> chunks_;
DISALLOW_COPY_AND_ASSIGN(DelayBuffer);
};
} // namespace audio
#endif // SERVICES_AUDIO_DELAY_BUFFER_H_
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/audio/delay_buffer.h"
#include <algorithm>
#include "media/base/audio_bus.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace audio {
namespace {
constexpr int kChannels = 1;
constexpr int kMaxFrames = 32;
#define EXPECT_BUS_VALUES_EQ(bus, begin, end, value) \
{ \
const auto IsValue = [](float x) { return x == (value); }; \
EXPECT_TRUE(std::all_of((bus)->channel(0) + (begin), \
(bus)->channel(0) + (end) - (begin), IsValue)); \
}
TEST(DelayBufferTest, RecordsAMaximumNumberOfFrames) {
DelayBuffer buffer(kMaxFrames);
ASSERT_EQ(buffer.GetBeginPosition(), buffer.GetEndPosition());
constexpr int frames_per_bus = kMaxFrames / 4;
const auto bus = media::AudioBus::Create(kChannels, frames_per_bus);
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 1.0);
// Fill the buffer.
DelayBuffer::FrameTicks position = 0;
for (int i = 0; i < 4; ++i) {
buffer.Write(position, *bus, 1.0);
position += frames_per_bus;
EXPECT_EQ(0, buffer.GetBeginPosition());
EXPECT_EQ(position, buffer.GetEndPosition());
}
// Writing just one more bus should cause the leading frames to be dropped.
buffer.Write(position, *bus, 1.0);
position += frames_per_bus;
EXPECT_EQ(position - kMaxFrames, buffer.GetBeginPosition());
EXPECT_EQ(position, buffer.GetEndPosition());
// Now, simulate a gap in the recording by recording the next bus late.
position += frames_per_bus * 2;
buffer.Write(position, *bus, 1.0);
position += frames_per_bus;
EXPECT_EQ(position - kMaxFrames, buffer.GetBeginPosition());
EXPECT_EQ(position, buffer.GetEndPosition());
}
TEST(DelayBufferTest, ReadsSilenceIfNothingWasRecorded) {
DelayBuffer buffer(kMaxFrames);
ASSERT_EQ(buffer.GetBeginPosition(), buffer.GetEndPosition());
DelayBuffer::FrameTicks position = 0;
constexpr int frames_per_bus = kMaxFrames / 4;
const auto bus = media::AudioBus::Create(kChannels, frames_per_bus);
for (int i = 0; i < 10; ++i) {
// Set data in the bus to confirm it is all going to be overwritten.
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 1.0);
buffer.Read(position, frames_per_bus, bus.get());
EXPECT_EQ(buffer.GetBeginPosition(), buffer.GetEndPosition());
EXPECT_BUS_VALUES_EQ(bus, 0, frames_per_bus, 0.0);
position += frames_per_bus;
}
}
TEST(DelayBufferTest, ReadsSilenceIfOutsideRecordedRange) {
DelayBuffer buffer(kMaxFrames);
ASSERT_EQ(buffer.GetBeginPosition(), buffer.GetEndPosition());
constexpr int frames_per_bus = kMaxFrames / 4;
const auto bus = media::AudioBus::Create(kChannels, frames_per_bus);
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 1.0);
// Fill the buffer.
DelayBuffer::FrameTicks position = 0;
for (int i = 0; i < 4; ++i) {
buffer.Write(position, *bus, 1.0);
position += frames_per_bus;
}
EXPECT_EQ(0, buffer.GetBeginPosition());
EXPECT_EQ(position, buffer.GetEndPosition());
// Read before the begin position and expect to get silence.
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 0.5);
buffer.Read(-kMaxFrames, frames_per_bus, bus.get());
EXPECT_BUS_VALUES_EQ(bus, 0, frames_per_bus, 0.0);
// Read at a position one before the begin position. Expect the first sample
// to be 0.0, and the rest 1.0.
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 0.5);
buffer.Read(buffer.GetBeginPosition() - 1, frames_per_bus, bus.get());
EXPECT_EQ(0.0, bus->channel(0)[0]);
EXPECT_BUS_VALUES_EQ(bus, 1, frames_per_bus - 1, 1.0);
// Read at a position where the last sample should be 0.0 and the rest 1.0.
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 0.5);
buffer.Read(buffer.GetEndPosition() - frames_per_bus + 1, frames_per_bus,
bus.get());
EXPECT_BUS_VALUES_EQ(bus, 0, frames_per_bus - 1, 1.0);
EXPECT_EQ(0.0, bus->channel(0)[frames_per_bus - 1]);
// Read after the end position and expect to get silence.
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 0.5);
buffer.Read(kMaxFrames, frames_per_bus, bus.get());
EXPECT_BUS_VALUES_EQ(bus, 0, frames_per_bus, 0.0);
}
TEST(DelayBufferTest, ReadsGapsInRecording) {
DelayBuffer buffer(kMaxFrames);
ASSERT_EQ(buffer.GetBeginPosition(), buffer.GetEndPosition());
constexpr int frames_per_bus = kMaxFrames / 4;
const auto bus = media::AudioBus::Create(kChannels, frames_per_bus);
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 1.0);
// Fill the buffer, but with a gap in the third quarter of it.
DelayBuffer::FrameTicks record_position = 0;
for (int i = 0; i < 4; ++i) {
if (i != 2) {
buffer.Write(record_position, *bus, 1.0);
}
record_position += frames_per_bus;
}
EXPECT_EQ(0, buffer.GetBeginPosition());
EXPECT_EQ(record_position, buffer.GetEndPosition());
// Read through the whole range, but offset by one frame early. Confirm the
// silence gap appears in the right place.
DelayBuffer::FrameTicks read_position = -1;
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 0.5);
buffer.Read(read_position, frames_per_bus, bus.get());
read_position += frames_per_bus;
EXPECT_EQ(0.0, bus->channel(0)[0]);
EXPECT_BUS_VALUES_EQ(bus, 1, frames_per_bus - 1, 1.0);
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 0.5);
buffer.Read(read_position, frames_per_bus, bus.get());
read_position += frames_per_bus;
EXPECT_BUS_VALUES_EQ(bus, 0, frames_per_bus, 1.0);
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 0.5);
buffer.Read(read_position, frames_per_bus, bus.get());
read_position += frames_per_bus;
EXPECT_EQ(1.0, bus->channel(0)[0]);
// The gap begins.
EXPECT_BUS_VALUES_EQ(bus, 1, frames_per_bus - 1, 0.0);
std::fill(bus->channel(0), bus->channel(0) + frames_per_bus, 0.5);
buffer.Read(read_position, frames_per_bus, bus.get());
read_position += frames_per_bus;
EXPECT_EQ(0.0, bus->channel(0)[0]);
// The gap ends.
EXPECT_BUS_VALUES_EQ(bus, 1, frames_per_bus - 1, 1.0);
}
} // namespace
} // namespace audio
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment