| Index: chrome/browser/media/chrome_webrtc_audio_quality_browsertest.cc
|
| diff --git a/chrome/browser/media/chrome_webrtc_audio_quality_browsertest.cc b/chrome/browser/media/chrome_webrtc_audio_quality_browsertest.cc
|
| index d59b99baa290852aea4128541af53cf8c39c5de2..bfb8cfb51ee25ae83099069703eab5d771f38999 100644
|
| --- a/chrome/browser/media/chrome_webrtc_audio_quality_browsertest.cc
|
| +++ b/chrome/browser/media/chrome_webrtc_audio_quality_browsertest.cc
|
| @@ -5,11 +5,15 @@
|
| #include <ctime>
|
|
|
| #include "base/command_line.h"
|
| +#include "base/files/file_enumerator.h"
|
| #include "base/files/file_util.h"
|
| +#include "base/files/scoped_temp_dir.h"
|
| #include "base/process/launch.h"
|
| #include "base/process/process.h"
|
| #include "base/scoped_native_library.h"
|
| +#include "base/strings/string_util.h"
|
| #include "base/strings/stringprintf.h"
|
| +#include "chrome/browser/media/webrtc_browsertest_audio.h"
|
| #include "chrome/browser/media/webrtc_browsertest_base.h"
|
| #include "chrome/browser/media/webrtc_browsertest_common.h"
|
| #include "chrome/browser/profiles/profile.h"
|
| @@ -20,6 +24,7 @@
|
| #include "chrome/common/chrome_switches.h"
|
| #include "chrome/test/base/ui_test_utils.h"
|
| #include "content/public/test/browser_test_utils.h"
|
| +#include "media/audio/audio_parameters.h"
|
| #include "media/base/media_switches.h"
|
| #include "net/test/embedded_test_server/embedded_test_server.h"
|
| #include "testing/perf/perf_test.h"
|
| @@ -149,15 +154,6 @@ class MAYBE_WebRtcAudioQualityBrowserTest : public WebRtcTestBase {
|
| EXPECT_EQ("ok-playing", ExecuteJavascript("playAudioFile()", tab_contents));
|
| }
|
|
|
| - base::FilePath CreateTemporaryWaveFile() {
|
| - base::FilePath filename;
|
| - EXPECT_TRUE(base::CreateTemporaryFile(&filename));
|
| - base::FilePath wav_filename =
|
| - filename.AddExtension(FILE_PATH_LITERAL(".wav"));
|
| - EXPECT_TRUE(base::Move(filename, wav_filename));
|
| - return wav_filename;
|
| - }
|
| -
|
| content::WebContents* OpenPageWithoutGetUserMedia(const char* url) {
|
| chrome::AddTabAt(browser(), GURL(), -1, true);
|
| ui_test_utils::NavigateToURL(
|
| @@ -171,17 +167,28 @@ class MAYBE_WebRtcAudioQualityBrowserTest : public WebRtcTestBase {
|
| ExecuteJavascript("preparePeerConnection()", tab));
|
| return tab;
|
| }
|
| +
|
| + protected:
|
| + void TestAutoGainControl(const base::FilePath::StringType& reference_filename,
|
| + const std::string& constraints,
|
| + const std::string& perf_modifier);
|
| };
|
|
|
| +namespace {
|
| +
|
| class AudioRecorder {
|
| public:
|
| AudioRecorder() {}
|
| ~AudioRecorder() {}
|
|
|
| // Starts the recording program for the specified duration. Returns true
|
| - // on success.
|
| + // on success. We record in 44.1 kHz 16-bit format unless |record_cd| is
|
| + // false, in which case we record in 48 kHz 16-bit format. We record in stereo
|
| + // unless |mono| is true.
|
| + // TODO(phoglund): make win and mac also support the record_cd parameter. Or,
|
| + // even better, make everybody use the CD format rather than DAT.
|
| bool StartRecording(int duration_sec, const base::FilePath& output_file,
|
| - bool mono) {
|
| + bool mono, bool record_cd) {
|
| EXPECT_FALSE(recording_application_.IsValid())
|
| << "Tried to record, but is already recording.";
|
|
|
| @@ -238,7 +245,10 @@ class AudioRecorder {
|
| command_line.AppendArg("-d");
|
| command_line.AppendArg(base::StringPrintf("%d", duration_sec));
|
| command_line.AppendArg("-f");
|
| - command_line.AppendArg("dat");
|
| + if (record_cd)
|
| + command_line.AppendArg("cd");
|
| + else
|
| + command_line.AppendArg("dat");
|
| command_line.AppendArg("-c");
|
| command_line.AppendArg(base::StringPrintf("%d", num_channels));
|
| command_line.AppendArgPath(output_file);
|
| @@ -304,6 +314,24 @@ bool ForceMicrophoneVolumeTo100Percent() {
|
| return true;
|
| }
|
|
|
| +// Sox is the "Swiss army knife" of audio processing. We mainly use it for
|
| +// silence trimming. See http://sox.sourceforge.net.
|
| +CommandLine MakeSoxCommandLine() {
|
| +#if defined(OS_WIN)
|
| + base::FilePath sox_path = test::GetReferenceFilesDir().Append(
|
| + FILE_PATH_LITERAL("tools/sox.exe"));
|
| + if (!base::PathExists(sox_path)) {
|
| + LOG(ERROR) << "Missing sox.exe binary in " << sox_path.value()
|
| + << "; you may have to provide this binary yourself.";
|
| + return CommandLine(CommandLine::NO_PROGRAM);
|
| + }
|
| + CommandLine command_line(sox_path);
|
| +#else
|
| + CommandLine command_line(base::FilePath(FILE_PATH_LITERAL("sox")));
|
| +#endif
|
| + return command_line;
|
| +}
|
| +
|
| // Removes silence from beginning and end of the |input_audio_file| and writes
|
| // the result to the |output_audio_file|. Returns true on success.
|
| bool RemoveSilence(const base::FilePath& input_file,
|
| @@ -317,23 +345,14 @@ bool RemoveSilence(const base::FilePath& input_file,
|
| // silence at beginning of audio.
|
| // DURATION: the amount of time in seconds that non-silence must be detected
|
| // before sox stops trimming audio.
|
| - // THRESHOLD: value used to indicate what sample value is treates as silence.
|
| + // THRESHOLD: value used to indicate what sample value is treats as silence.
|
| const char* kAbovePeriods = "1";
|
| const char* kDuration = "2";
|
| - const char* kTreshold = "5%";
|
| + const char* kTreshold = "3%";
|
|
|
| -#if defined(OS_WIN)
|
| - base::FilePath sox_path = test::GetReferenceFilesDir().Append(
|
| - FILE_PATH_LITERAL("tools/sox.exe"));
|
| - if (!base::PathExists(sox_path)) {
|
| - LOG(ERROR) << "Missing sox.exe binary in " << sox_path.value()
|
| - << "; you may have to provide this binary yourself.";
|
| + CommandLine command_line = MakeSoxCommandLine();
|
| + if (command_line.GetProgram().empty())
|
| return false;
|
| - }
|
| - CommandLine command_line(sox_path);
|
| -#else
|
| - CommandLine command_line(base::FilePath(FILE_PATH_LITERAL("sox")));
|
| -#endif
|
| command_line.AppendArgPath(input_file);
|
| command_line.AppendArgPath(output_file);
|
| command_line.AppendArg("silence");
|
| @@ -354,6 +373,43 @@ bool RemoveSilence(const base::FilePath& input_file,
|
| return ok;
|
| }
|
|
|
| +// Looks for 0.3-second silences (under 1% audio power) and splits the input
|
| +// file on those silences. Output files are written according to the output file
|
| +// template (e.g. /tmp/out.wav writes /tmp/out001.wav, /tmp/out002.wav, etc if
|
| +// there are two silence-padded regions in the file). The silences between
|
| +// speech segments must be at least 500 ms for this to be reliable.
|
| +bool SplitFileOnSilence(const base::FilePath& input_file,
|
| + const base::FilePath& output_file_template) {
|
| + CommandLine command_line = MakeSoxCommandLine();
|
| + if (command_line.GetProgram().empty())
|
| + return false;
|
| +
|
| + // These are experimentally determined and work on the files we use.
|
| + const char* kAbovePeriods = "1";
|
| + const char* kUnderPeriods = "1";
|
| + const char* kDuration = "0.3";
|
| + const char* kTreshold = "1%";
|
| + command_line.AppendArgPath(input_file);
|
| + command_line.AppendArgPath(output_file_template);
|
| + command_line.AppendArg("silence");
|
| + command_line.AppendArg(kAbovePeriods);
|
| + command_line.AppendArg(kDuration);
|
| + command_line.AppendArg(kTreshold);
|
| + command_line.AppendArg(kUnderPeriods);
|
| + command_line.AppendArg(kDuration);
|
| + command_line.AppendArg(kTreshold);
|
| + command_line.AppendArg(":");
|
| + command_line.AppendArg("newfile");
|
| + command_line.AppendArg(":");
|
| + command_line.AppendArg("restart");
|
| +
|
| + DVLOG(0) << "Running " << command_line.GetCommandLineString();
|
| + std::string result;
|
| + bool ok = base::GetAppOutput(command_line, &result);
|
| + DVLOG(0) << "Output was:\n\n" << result;
|
| + return ok;
|
| +}
|
| +
|
| bool CanParseAsFloat(const std::string& value) {
|
| return atof(value.c_str()) != 0 || value == "0";
|
| }
|
| @@ -424,6 +480,88 @@ bool RunPesq(const base::FilePath& reference_file,
|
| return true;
|
| }
|
|
|
| +base::FilePath CreateTemporaryWaveFile() {
|
| + base::FilePath filename;
|
| + EXPECT_TRUE(base::CreateTemporaryFile(&filename));
|
| + base::FilePath wav_filename =
|
| + filename.AddExtension(FILE_PATH_LITERAL(".wav"));
|
| + EXPECT_TRUE(base::Move(filename, wav_filename));
|
| + return wav_filename;
|
| +}
|
| +
|
| +std::vector<base::FilePath> ListWavFilesInDir(const base::FilePath& dir) {
|
| + base::FileEnumerator files(dir, false, base::FileEnumerator::FILES,
|
| + FILE_PATH_LITERAL("*.wav"));
|
| +
|
| + std::vector<base::FilePath> result;
|
| + for (base::FilePath name = files.Next(); !name.empty(); name = files.Next())
|
| + result.push_back(name);
|
| + return result;
|
| +}
|
| +
|
| +// Splits |to_split| into sub-files based on silence. The file you use must have
|
| +// at least 500 ms periods of silence between speech segments for this to be
|
| +// reliable.
|
| +void SplitFileOnSilenceIntoDir(const base::FilePath& to_split,
|
| + const base::FilePath& workdir) {
|
| + // First trim beginning and end since they are tricky for the splitter.
|
| + base::FilePath trimmed_audio = CreateTemporaryWaveFile();
|
| +
|
| + ASSERT_TRUE(RemoveSilence(to_split, trimmed_audio));
|
| + DVLOG(0) << "Trimmed silence: " << trimmed_audio.value() << std::endl;
|
| +
|
| + ASSERT_TRUE(SplitFileOnSilence(
|
| + trimmed_audio, workdir.Append(FILE_PATH_LITERAL("output.wav"))));
|
| + ASSERT_TRUE(base::DeleteFile(trimmed_audio, false));
|
| +}
|
| +
|
| +// Computes the difference between the actual and reference segment. A positive
|
| +// number x means the actual file is x dB stronger than the reference.
|
| +float AnalyzeOneSegment(const base::FilePath& ref_segment,
|
| + const base::FilePath& actual_segment,
|
| + int segment_number) {
|
| + media::AudioParameters ref_parameters;
|
| + media::AudioParameters actual_parameters;
|
| + float ref_energy =
|
| + test::ComputeAudioEnergyForWavFile(ref_segment, &ref_parameters);
|
| + float actual_energy =
|
| + test::ComputeAudioEnergyForWavFile(actual_segment, &actual_parameters);
|
| +
|
| + base::TimeDelta difference_in_length = ref_parameters.GetBufferDuration() -
|
| + actual_parameters.GetBufferDuration();
|
| + EXPECT_LE(difference_in_length, base::TimeDelta::FromMilliseconds(200))
|
| + << "Segments differ " << difference_in_length.InMilliseconds() << " ms "
|
| + << "in length for segment " << segment_number << "; we're likely "
|
| + << "comparing unrelated segments or silence splitting is busted.";
|
| +
|
| + return actual_energy - ref_energy;
|
| +}
|
| +
|
| +void AnalyzeSegmentsAndPrintResult(
|
| + const std::vector<base::FilePath>& ref_segments,
|
| + const std::vector<base::FilePath>& actual_segments,
|
| + const base::FilePath& reference_file,
|
| + const std::string& perf_modifier) {
|
| + ASSERT_GT(ref_segments.size(), 0u)
|
| + << "Failed to split reference file on silence; sox is likely broken.";
|
| + ASSERT_EQ(ref_segments.size(), actual_segments.size())
|
| + << "The recording did not result in the same number of audio segments "
|
| + << "after on splitting on silence; WebRTC must have deformed the audio "
|
| + << "too much.";
|
| +
|
| + for (size_t i = 0; i < ref_segments.size(); i++) {
|
| + float difference_in_decibel = AnalyzeOneSegment(ref_segments[i],
|
| + actual_segments[i],
|
| + i);
|
| + std::string trace_name = base::StringPrintf(
|
| + "%s_segment_%zu", reference_file.BaseName().value().c_str(), i);
|
| + perf_test::PrintResult("agc_energy_diff", perf_modifier, trace_name,
|
| + difference_in_decibel, "dB", false);
|
| + }
|
| +}
|
| +
|
| +} // namespace
|
| +
|
| IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcAudioQualityBrowserTest,
|
| MANUAL_TestAudioQuality) {
|
| if (OnWinXp()) {
|
| @@ -460,7 +598,8 @@ IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcAudioQualityBrowserTest,
|
| // safety margins on each side.
|
| AudioRecorder recorder;
|
| static int kRecordingTimeSeconds = 15;
|
| - ASSERT_TRUE(recorder.StartRecording(kRecordingTimeSeconds, recording, true));
|
| + ASSERT_TRUE(recorder.StartRecording(kRecordingTimeSeconds, recording,
|
| + true, false));
|
|
|
| PlayAudioFileThroughWebAudio(left_tab);
|
|
|
| @@ -488,8 +627,40 @@ IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcAudioQualityBrowserTest,
|
| EXPECT_TRUE(base::DeleteFile(trimmed_recording, false));
|
| }
|
|
|
| -IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcAudioQualityBrowserTest,
|
| - MANUAL_TestAutoGainControlIncreasesEnergyForLowAudio) {
|
| +/**
|
| + * The auto gain control test plays a file into the fake microphone. Then it
|
| + * sets up a one-way WebRTC call with audio only and records Chrome's output on
|
| + * the receiving side using the audio loopback provided by the quality test
|
| + * (see the class comments for more details).
|
| + *
|
| + * Then both the recording and reference file are split on silence. This creates
|
| + * a number of segments with speech in them. The reason for this is to provide
|
| + * a kind of synchronization mechanism so the start of each speech segment is
|
| + * compared to the start of the corresponding speech segment. This is because we
|
| + * will experience inevitable clock drift between the system clock (which runs
|
| + * the fake microphone) and the sound card (which runs play-out). Effectively
|
| + * re-synchronizing on each segment mitigates this.
|
| + *
|
| + * The silence splitting is inherently sensitive to the sound file we run on.
|
| + * Therefore the reference file must have at least 500 ms of pure silence
|
| + * between speech segments; the test will fail if the output produces more
|
| + * segments than the reference.
|
| + *
|
| + * The test reports the difference in decibel between the reference and output
|
| + * file per 10 ms interval in each speech segment. A value of 6 means the
|
| + * output was 6 dB louder than the reference, presumably because the AGC applied
|
| + * gain to the signal.
|
| + *
|
| + * The test only exercises digital AGC for now.
|
| + *
|
| + * We record in CD format here (44.1 kHz) because that's what the fake input
|
| + * device currently supports, and we want to be able to compare directly. See
|
| + * http://crbug.com/421054.
|
| + */
|
| +void MAYBE_WebRtcAudioQualityBrowserTest::TestAutoGainControl(
|
| + const base::FilePath::StringType& reference_filename,
|
| + const std::string& constraints,
|
| + const std::string& perf_modifier) {
|
| if (OnWinXp()) {
|
| LOG(ERROR) << "This test is not implemented for Windows XP.";
|
| return;
|
| @@ -504,40 +675,86 @@ IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcAudioQualityBrowserTest,
|
|
|
| ASSERT_TRUE(ForceMicrophoneVolumeTo100Percent());
|
|
|
| - ConfigureFakeDeviceToPlayFile(
|
| - test::GetReferenceFilesDir().Append(kAgcTestReferenceFile));
|
| + base::FilePath reference_file =
|
| + test::GetReferenceFilesDir().Append(reference_filename);
|
| + ConfigureFakeDeviceToPlayFile(reference_file);
|
|
|
| // Create a one-way call.
|
| GURL test_page = embedded_test_server()->GetURL(kWebRtcAudioTestHtmlPage);
|
| content::WebContents* left_tab =
|
| - OpenPageAndGetUserMediaInNewTabWithConstraints(test_page,
|
| - kAudioOnlyCallConstraints);
|
| + OpenPageAndGetUserMediaInNewTabWithConstraints(test_page, constraints);
|
| SetupPeerconnectionWithLocalStream(left_tab);
|
|
|
| content::WebContents* right_tab =
|
| OpenPageWithoutGetUserMedia(kWebRtcAudioTestHtmlPage);
|
|
|
| - NegotiateCall(left_tab, right_tab);
|
| -
|
| base::FilePath recording = CreateTemporaryWaveFile();
|
|
|
| AudioRecorder recorder;
|
| - static int kRecordingTimeSeconds = 10;
|
| - ASSERT_TRUE(recorder.StartRecording(kRecordingTimeSeconds, recording, true));
|
| + static int kRecordingTimeSeconds = 25;
|
| + ASSERT_TRUE(recorder.StartRecording(kRecordingTimeSeconds, recording, false,
|
| + true));
|
| +
|
| + NegotiateCall(left_tab, right_tab);
|
|
|
| ASSERT_TRUE(recorder.WaitForRecordingToEnd());
|
| DVLOG(0) << "Done recording to " << recording.value() << std::endl;
|
|
|
| HangUp(left_tab);
|
|
|
| - base::FilePath trimmed_recording = CreateTemporaryWaveFile();
|
| + // Call Take() on the scoped temp dirs if you want to look at the files after
|
| + // the test exits (the default is to delete the files).
|
| + base::ScopedTempDir split_ref_files;
|
| + ASSERT_TRUE(split_ref_files.CreateUniqueTempDir());
|
| + ASSERT_NO_FATAL_FAILURE(
|
| + SplitFileOnSilenceIntoDir(reference_file, split_ref_files.path()));
|
| + std::vector<base::FilePath> ref_segments =
|
| + ListWavFilesInDir(split_ref_files.path());
|
| +
|
| + base::ScopedTempDir split_actual_files;
|
| + ASSERT_TRUE(split_actual_files.CreateUniqueTempDir());
|
| + ASSERT_NO_FATAL_FAILURE(
|
| + SplitFileOnSilenceIntoDir(recording, split_actual_files.path()));
|
| + std::vector<base::FilePath> actual_segments =
|
| + ListWavFilesInDir(split_actual_files.path());
|
| +
|
| + AnalyzeSegmentsAndPrintResult(ref_segments, actual_segments, reference_file,
|
| + perf_modifier);
|
|
|
| - ASSERT_TRUE(RemoveSilence(recording, trimmed_recording));
|
| - DVLOG(0) << "Trimmed silence: " << trimmed_recording.value() << std::endl;
|
| + EXPECT_TRUE(base::DeleteFile(recording, false));
|
| +}
|
| +
|
| +// Only implemented for Linux for now.
|
| +#if defined(OS_LINUX)
|
| +#define MAYBE_MANUAL_TestAutoGainControlOnLowAudio \
|
| + MANUAL_TestAutoGainControlOnLowAudio
|
| +#else
|
| +#define MAYBE_MANUAL_TestAutoGainControlOnLowAudio \
|
| + DISABLED_MANUAL_TestAutoGainControlOnLowAudio
|
| +#endif
|
|
|
| - // TODO(phoglund): invoke bjornv's audio energy analysis tool on the trimmed
|
| - // recording and log the result.
|
| +// The AGC should apply non-zero gain here.
|
| +IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcAudioQualityBrowserTest,
|
| + MAYBE_MANUAL_TestAutoGainControlOnLowAudio) {
|
| + ASSERT_NO_FATAL_FAILURE(TestAutoGainControl(
|
| + kAgcTestReferenceFile, kAudioOnlyCallConstraints, "_with_agc"));
|
| +}
|
|
|
| - EXPECT_TRUE(base::DeleteFile(recording, false));
|
| - EXPECT_TRUE(base::DeleteFile(trimmed_recording, false));
|
| +// Only implemented for Linux for now.
|
| +#if defined(OS_LINUX)
|
| +#define MAYBE_MANUAL_TestComputeGainWithAudioProcessingOff \
|
| + MANUAL_TestComputeGainWithAudioProcessingOff
|
| +#else
|
| +#define MAYBE_MANUAL_TestComputeGainWithAudioProcessingOff \
|
| + DISABLED_MANUAL_TestComputeGainWithAudioProcessingOff
|
| +#endif
|
| +
|
| +// Since the AGC is off here there should be no gain at all.
|
| +IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcAudioQualityBrowserTest,
|
| + MAYBE_MANUAL_TestComputeGainWithAudioProcessingOff) {
|
| + const char* kAudioCallWithoutAudioProcessing =
|
| + "{audio: { mandatory: { echoCancellation: false } } }";
|
| + ASSERT_NO_FATAL_FAILURE(TestAutoGainControl(
|
| + kAgcTestReferenceFile, kAudioCallWithoutAudioProcessing, "_no_agc"));
|
| }
|
| +
|
|
|