diff options
author | hubbe@chromium.org <hubbe@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-03-13 01:45:15 +0000 |
---|---|---|
committer | hubbe@chromium.org <hubbe@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2014-03-13 01:45:15 +0000 |
commit | e432b1f8880d04e8186a0ce6bba691aa4f5a6672 (patch) | |
tree | 81cc58da8bcfa0f65140c9a060db8b25ec7d7149 /media | |
parent | 3b93ca6e6981c54ac6d3e3eade22ffa30186678f (diff) | |
download | chromium_src-e432b1f8880d04e8186a0ce6bba691aa4f5a6672.zip chromium_src-e432b1f8880d04e8186a0ce6bba691aa4f5a6672.tar.gz chromium_src-e432b1f8880d04e8186a0ce6bba691aa4f5a6672.tar.bz2 |
Helper functions and binary for encoding timestamps into audio.
These functions and binaries will be used to test the audio mirroring
pipeline. They allow to encode and decode 16 bit numbers into a short
blip of audio that should be decodable even when compressed
and uncompressed.
Review URL: https://codereview.chromium.org/178473021
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@256728 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'media')
-rw-r--r-- | media/cast/cast.gyp | 24 | ||||
-rw-r--r-- | media/cast/cast_config.gyp | 29 | ||||
-rw-r--r-- | media/cast/test/utility/audio_utility.cc | 125 | ||||
-rw-r--r-- | media/cast/test/utility/audio_utility.h | 16 | ||||
-rw-r--r-- | media/cast/test/utility/audio_utility_unittest.cc | 45 | ||||
-rw-r--r-- | media/cast/test/utility/generate_timecode_audio.cc | 32 | ||||
-rw-r--r-- | media/cast/test/utility/utility.gyp | 16 |
7 files changed, 267 insertions, 20 deletions
diff --git a/media/cast/cast.gyp b/media/cast/cast.gyp index 02b991d..ef67982 100644 --- a/media/cast/cast.gyp +++ b/media/cast/cast.gyp @@ -8,23 +8,6 @@ 'chromium_code': 1, }, 'targets': [ - { - 'target_name': 'cast_config', - 'type': 'static_library', - 'include_dirs': [ - '<(DEPTH)/', - ], - 'dependencies': [ - '<(DEPTH)/base/base.gyp:base', - ], - 'sources': [ - 'cast_config.cc', - 'cast_config.h', - 'cast_defines.h', - 'cast_environment.cc', - 'cast_environment.h', - ], # source - }, ], # targets, 'conditions': [ ['include_tests==1', { @@ -33,7 +16,7 @@ 'target_name': 'cast_unittests', 'type': '<(gtest_target_type)', 'dependencies': [ - 'cast_config', + 'cast_config.gyp:cast_config', 'cast_receiver.gyp:cast_receiver', 'cast_sender.gyp:cast_sender', 'logging/logging.gyp:cast_log_analysis', @@ -89,6 +72,7 @@ 'test/fake_single_thread_task_runner.h', 'test/fake_video_encode_accelerator.cc', 'test/fake_video_encode_accelerator.h', + 'test/utility/audio_utility_unittest.cc', 'test/utility/barcode_unittest.cc', 'transport/cast_transport_sender_impl_unittest.cc', 'transport/pacing/mock_paced_packet_sender.cc', @@ -113,7 +97,7 @@ '<(DEPTH)/', ], 'dependencies': [ - 'cast_config', + 'cast_config.gyp:cast_config', 'logging/logging.gyp:sender_logging', '<(DEPTH)/ui/gfx/gfx.gyp:gfx', '<(DEPTH)/net/net.gyp:net_test_support', @@ -135,7 +119,7 @@ '<(DEPTH)/', ], 'dependencies': [ - 'cast_config', + 'cast_config.gyp:cast_config', '<(DEPTH)/ui/gfx/gfx.gyp:gfx', '<(DEPTH)/net/net.gyp:net_test_support', '<(DEPTH)/media/cast/cast_receiver.gyp:*', diff --git a/media/cast/cast_config.gyp b/media/cast/cast_config.gyp new file mode 100644 index 0000000..21d83c9 --- /dev/null +++ b/media/cast/cast_config.gyp @@ -0,0 +1,29 @@ +# Copyright 2014 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. + +{ + 'variables': { + 'include_tests%': 1, + 'chromium_code': 1, + }, + 'targets': [ + { + 'target_name': 'cast_config', + 'type': 'static_library', + 'include_dirs': [ + '<(DEPTH)/', + ], + 'dependencies': [ + '<(DEPTH)/base/base.gyp:base', + ], + 'sources': [ + 'cast_config.cc', + 'cast_config.h', + 'cast_defines.h', + 'cast_environment.cc', + 'cast_environment.h', + ], # source + }, + ], +} diff --git a/media/cast/test/utility/audio_utility.cc b/media/cast/test/utility/audio_utility.cc index 8477536..f8e4957 100644 --- a/media/cast/test/utility/audio_utility.cc +++ b/media/cast/test/utility/audio_utility.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include <math.h> + #include "media/cast/test/utility/audio_utility.h" #include "base/time/time.h" @@ -11,6 +13,8 @@ namespace media { namespace cast { +const double Pi = 3.14159265358979323846; + TestAudioBusFactory::TestAudioBusFactory(int num_channels, int sample_rate, float sine_wave_frequency, @@ -68,5 +72,126 @@ int CountZeroCrossings(const std::vector<int16>& samples) { return count; } +// EncodeTimestamp stores a 16-bit number as frequencies in a sample. +// Our internal code tends to work on 10ms chunks of data, and to +// make sure the decoding always work, I wanted to make sure that the +// encoded value can be decoded from 5ms of sample data, assuming a +// sampling rate of 48Khz, this turns out to be 240 samples. +// Each bit of the timestamp is stored as a frequency, where the +// frequency is bit_number * 200 Hz. We also add a 'sense' tone to +// the output, this tone is 17 * 200 = 3400Hz, and when we decode, +// we can use this tone to make sure that we aren't decoding bogus data. +// Also, we use this tone to scale our expectations in case something +// changed changed the volume of the audio. +// +// Normally, we will encode 480 samples (10ms) of data, but when we +// read it will will scan 240 samples at a time until something that +// can be decoded is found. +// +// The intention is to use these routines to encode the frame number +// that goes with each chunk of audio, so if our frame rate is +// 30Hz, we would encode 48000/30 = 1600 samples of "1", then +// 1600 samples of "2", etc. When we decode this, it is possible +// that we get a chunk of data that is spanning two frame numbers, +// so we gray-code the numbers. Since adjacent gray-coded number +// will only differ in one bit, we should never get numbers out +// of sequence when decoding, at least not by more than one. + +const double kBaseFrequency = 200; +const int kSamplingFrequency = 48000; +const size_t kNumBits = 16; +const size_t kSamplesToAnalyze = kSamplingFrequency / kBaseFrequency; +const double kSenseFrequency = kBaseFrequency * (kNumBits + 1); +const double kMinSense = 50000.0; + +bool EncodeTimestamp(uint16 timestamp, + size_t sample_offset, + std::vector<int16>* samples) { + if (samples->size() < kSamplesToAnalyze) { + return false; + } + // gray-code the number + timestamp = (timestamp >> 1) ^ timestamp; + std::vector<double> frequencies; + for (int i = 0; i < kNumBits; i++) { + if ((timestamp >> i) & 1) { + frequencies.push_back(kBaseFrequency * (i+1)); + } + } + // Carrier sense frequency + frequencies.push_back(kSenseFrequency); + for (size_t i = 0; i < samples->size(); i++) { + double ret = 0.0; + for (size_t f = 0; f < frequencies.size(); f++) { + ret += sin((i + sample_offset) * Pi * 2.0 * frequencies[f] / + kSamplingFrequency); + } + (*samples)[i] = ret * 32766 / (kNumBits + 1); + } + return true; +} + +namespace { +// We use a slow DCT here since this code is only used for testing. +// While an FFT would probably be faster, it wouldn't be a LOT +// faster since we only analyze 17 out of 120 frequencies. +// With an FFT we would verify that none of the higher frequencies +// contain a lot of energy, which would be useful in detecting +// bogus data. +double DecodeOneFrequency(const int16* samples, + size_t length, + double frequency) { + double sin_sum = 0.0; + double cos_sum = 0.0; + for (size_t i = 0; i < length; i++) { + sin_sum += samples[i] * sin(i * Pi * 2 * frequency / kSamplingFrequency); + cos_sum += samples[i] * cos(i * Pi * 2 * frequency / kSamplingFrequency); + } + return sqrt(sin_sum * sin_sum + cos_sum * cos_sum); +} +} // namespace + +// When decoding, we first check for sense frequency, then we decode +// each of the bits. Each frequency must have a strength that is similar to +// the sense frequency or to zero, or the decoding fails. If it fails, we +// move head by 60 samples and try again until we run out of samples. +bool DecodeTimestamp(const std::vector<int16>& samples, uint16* timestamp) { + for (size_t start = 0; + start + kSamplesToAnalyze <= samples.size(); + start += kSamplesToAnalyze / 4) { + double sense = DecodeOneFrequency(&samples[start], + kSamplesToAnalyze, + kSenseFrequency); + if (sense < kMinSense) continue; + bool success = true; + uint16 gray_coded = 0; + for (int bit = 0; success && bit < kNumBits; bit++) { + double signal_strength = DecodeOneFrequency( + &samples[start], + kSamplesToAnalyze, + kBaseFrequency * (bit + 1)); + if (signal_strength < sense / 4) { + // Zero bit, no action + } else if (signal_strength > sense * 0.75 && + signal_strength < sense * 1.25) { + // One bit + gray_coded |= 1 << bit; + } else { + success = false; + } + } + if (success) { + // Convert from gray-coded number to binary. + uint16 mask; + for (mask = gray_coded >> 1; mask != 0; mask = mask >> 1) { + gray_coded = gray_coded ^ mask; + } + *timestamp = gray_coded; + return true; + } + } + return false; +} + } // namespace cast } // namespace media diff --git a/media/cast/test/utility/audio_utility.h b/media/cast/test/utility/audio_utility.h index ea90e9c..7cc9b7d 100644 --- a/media/cast/test/utility/audio_utility.h +++ b/media/cast/test/utility/audio_utility.h @@ -58,6 +58,22 @@ scoped_ptr<PcmAudioFrame> ToPcmAudioFrame(const AudioBus& audio_bus, // zero. int CountZeroCrossings(const std::vector<int16>& samples); +// Encode |timestamp| into the samples pointed to by 'samples' in a way +// that should be decodable even after compressing/decompressing the audio. +// Assumes 48Khz sampling rate and needs at least 240 samples. Returns +// false if |samples| is too small. If more than 240 samples are available, +// then the timestamp will be repeated. |sample_offset| should contain how +// many samples has been encoded so far, so that we can make smooth +// transitions between encoded chunks. +// See audio_utility.cc for details on how the encoding is done. +bool EncodeTimestamp(uint16 timestamp, + size_t sample_offset, + std::vector<int16>* samples); + +// Decode a timestamp encoded with EncodeTimestamp. Returns true if a +// timestamp was found in |samples|. +bool DecodeTimestamp(const std::vector<int16>& samples, uint16* timestamp); + } // namespace cast } // namespace media diff --git a/media/cast/test/utility/audio_utility_unittest.cc b/media/cast/test/utility/audio_utility_unittest.cc new file mode 100644 index 0000000..3e6ea89 --- /dev/null +++ b/media/cast/test/utility/audio_utility_unittest.cc @@ -0,0 +1,45 @@ +// Copyright 2014 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 "media/base/video_frame.h" +#include "media/cast/test/utility/audio_utility.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace media { +namespace cast { +namespace test { +namespace { + +TEST(AudioTimestampTest, Small) { + std::vector<int16> samples(480); + for (int32 in_timestamp = 0; in_timestamp < 65536; in_timestamp += 177) { + EncodeTimestamp(in_timestamp, 0, &samples); + uint16 out_timestamp; + EXPECT_TRUE(DecodeTimestamp(samples, &out_timestamp)); + ASSERT_EQ(in_timestamp, out_timestamp); + } +} + +TEST(AudioTimestampTest, Negative) { + std::vector<int16> samples(480); + uint16 out_timestamp; + EXPECT_FALSE(DecodeTimestamp(samples, &out_timestamp)); +} + +TEST(AudioTimestampTest, CheckPhase) { + std::vector<int16> samples(4800); + EncodeTimestamp(4711, 0, &samples); + while (samples.size() > 240) { + uint16 out_timestamp; + EXPECT_TRUE(DecodeTimestamp(samples, &out_timestamp)); + ASSERT_EQ(4711, out_timestamp); + + samples.erase(samples.begin(), samples.begin() + 73); + } +} + +} // namespace +} // namespace test +} // namespace cast +} // namespace media diff --git a/media/cast/test/utility/generate_timecode_audio.cc b/media/cast/test/utility/generate_timecode_audio.cc new file mode 100644 index 0000000..27d9a32 --- /dev/null +++ b/media/cast/test/utility/generate_timecode_audio.cc @@ -0,0 +1,32 @@ +// Copyright 2014 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 <stdio.h> +#include <stdlib.h> +#include <vector> + +#include "media/cast/test/utility/audio_utility.h" + +const size_t kSamplingFrequency = 48000; + +int main(int argc, char **argv) { + if (argc < 3) { + fprintf(stderr, "Usage: %s <fps> <frames> >output.s16le\n", argv[0]); + exit(1); + } + int fps = atoi(argv[1]); + int frames = atoi(argv[2]); + std::vector<int16> samples(kSamplingFrequency / fps); + size_t num_samples = 0; + for (uint32 frame_id = 1; frame_id <= frames; frame_id++) { + CHECK(media::cast::EncodeTimestamp(frame_id, num_samples, &samples)); + num_samples += samples.size(); + for (size_t sample = 0; sample < samples.size(); sample++) { + putchar(samples[sample] & 0xff); + putchar(samples[sample] >> 8); + putchar(samples[sample] & 0xff); + putchar(samples[sample] >> 8); + } + } +} diff --git a/media/cast/test/utility/utility.gyp b/media/cast/test/utility/utility.gyp index cb1c781..035ce2c 100644 --- a/media/cast/test/utility/utility.gyp +++ b/media/cast/test/utility/utility.gyp @@ -52,5 +52,21 @@ '<(DEPTH)/media/cast/test/utility/generate_barcode_video.cc', ], }, + { + 'target_name': 'generate_timecode_audio', + 'type': 'executable', + 'include_dirs': [ + '<(DEPTH)/', + ], + 'dependencies': [ + '<(DEPTH)/media/cast/cast_config.gyp:cast_config', + '<(DEPTH)/media/cast/test/utility/utility.gyp:cast_test_utility', + '<(DEPTH)/media/cast/transport/cast_transport.gyp:cast_transport', + '<(DEPTH)/media/media.gyp:media', + ], + 'sources': [ + '<(DEPTH)/media/cast/test/utility/generate_timecode_audio.cc', + ], + }, ], } |