OLD | NEW |
| (Empty) |
1 // Copyright 2013 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include <stddef.h> | |
6 | |
7 #include <ctime> | |
8 | |
9 #include "base/command_line.h" | |
10 #include "base/files/file_enumerator.h" | |
11 #include "base/files/file_util.h" | |
12 #include "base/files/scoped_temp_dir.h" | |
13 #include "base/macros.h" | |
14 #include "base/process/launch.h" | |
15 #include "base/process/process.h" | |
16 #include "base/scoped_native_library.h" | |
17 #include "base/strings/string_number_conversions.h" | |
18 #include "base/strings/string_util.h" | |
19 #include "base/strings/stringprintf.h" | |
20 #include "base/strings/utf_string_conversions.h" | |
21 #include "build/build_config.h" | |
22 #include "chrome/browser/media/webrtc_browsertest_audio.h" | |
23 #include "chrome/browser/media/webrtc_browsertest_base.h" | |
24 #include "chrome/browser/media/webrtc_browsertest_common.h" | |
25 #include "chrome/browser/profiles/profile.h" | |
26 #include "chrome/browser/ui/browser.h" | |
27 #include "chrome/browser/ui/browser_tabstrip.h" | |
28 #include "chrome/browser/ui/tabs/tab_strip_model.h" | |
29 #include "chrome/common/chrome_paths.h" | |
30 #include "chrome/common/chrome_switches.h" | |
31 #include "chrome/test/base/ui_test_utils.h" | |
32 #include "content/public/common/content_switches.h" | |
33 #include "content/public/test/browser_test_utils.h" | |
34 #include "media/base/audio_parameters.h" | |
35 #include "media/base/media_switches.h" | |
36 #include "net/test/embedded_test_server/embedded_test_server.h" | |
37 #include "testing/perf/perf_test.h" | |
38 | |
39 namespace { | |
40 | |
41 static const base::FilePath::CharType kReferenceFile[] = | |
42 FILE_PATH_LITERAL("speech_44kHz_16bit_stereo.wav"); | |
43 | |
44 // The javascript will load the reference file relative to its location, | |
45 // which is in /webrtc on the web server. The files we are looking for are in | |
46 // webrtc/resources in the chrome/test/data folder. | |
47 static const char kReferenceFileRelativeUrl[] = | |
48 "resources/speech_44kHz_16bit_stereo.wav"; | |
49 | |
50 static const char kWebRtcAudioTestHtmlPage[] = | |
51 "/webrtc/webrtc_audio_quality_test.html"; | |
52 | |
53 // For the AGC test, there are 6 speech segments split on silence. If one | |
54 // segment is significantly different in length compared to the same segment in | |
55 // the reference file, there's something fishy going on. | |
56 const int kMaxAgcSegmentDiffMs = | |
57 #if defined(OS_MACOSX) | |
58 // Something is different on Mac; http://crbug.com/477653. | |
59 600; | |
60 #else | |
61 200; | |
62 #endif | |
63 | |
64 #if defined(OS_LINUX) || defined(OS_MACOSX) | |
65 #define MAYBE_WebRtcAudioQualityBrowserTest WebRtcAudioQualityBrowserTest | |
66 #else | |
67 // Not implemented on Android, ChromeOS etc. | |
68 // Currently fails on Windows bots. http://crbug.com/642294. | |
69 #define MAYBE_WebRtcAudioQualityBrowserTest DISABLED_WebRtcAudioQualityBrowserTe
st | |
70 #endif | |
71 | |
72 } // namespace | |
73 | |
74 // Test we can set up a WebRTC call and play audio through it. | |
75 // | |
76 // If you're not a googler and want to run this test, you need to provide a | |
77 // pesq binary for your platform (and sox.exe on windows). Read more on how | |
78 // resources are managed in chrome/test/data/webrtc/resources/README. | |
79 // | |
80 // This test will only work on machines that have been configured to record | |
81 // their own input. | |
82 // | |
83 // On Linux: | |
84 // 1. # sudo apt-get install pavucontrol sox | |
85 // 2. For the user who will run the test: # pavucontrol | |
86 // 3. In a separate terminal, # arecord dummy | |
87 // 4. In pavucontrol, go to the recording tab. | |
88 // 5. For the ALSA plugin [aplay]: ALSA Capture from, change from <x> to | |
89 // <Monitor of x>, where x is whatever your primary sound device is called. | |
90 // 6. Try launching chrome as the target user on the target machine, try | |
91 // playing, say, a YouTube video, and record with # arecord -f dat tmp.dat. | |
92 // Verify the recording with aplay (should have recorded what you played | |
93 // from chrome). | |
94 // | |
95 // Note: the volume for ALL your input devices will be forced to 100% by | |
96 // running this test on Linux. | |
97 // | |
98 // On Mac: | |
99 // TODO(phoglund): download sox from gs instead. | |
100 // 1. Get SoundFlower: http://rogueamoeba.com/freebies/soundflower/download.php | |
101 // 2. Install it + reboot. | |
102 // 3. Install MacPorts (http://www.macports.org/). | |
103 // 4. Install sox: sudo port install sox. | |
104 // 5. (For Chrome bots) Ensure sox and rec are reachable from the env the test | |
105 // executes in (sox and rec tends to install in /opt/, which generally isn't | |
106 // in the Chrome bots' env). For instance, run | |
107 // sudo ln -s /opt/local/bin/rec /usr/local/bin/rec | |
108 // sudo ln -s /opt/local/bin/sox /usr/local/bin/sox | |
109 // 6. In Sound Preferences, set both input and output to Soundflower (2ch). | |
110 // Note: You will no longer hear audio on this machine, and it will no | |
111 // longer use any built-in mics. | |
112 // 7. Try launching chrome as the target user on the target machine, try | |
113 // playing, say, a YouTube video, and record with 'rec test.wav trim 0 5'. | |
114 // Stop the video in chrome and try playing back the file; you should hear | |
115 // a recording of the video (note; if you play back on the target machine | |
116 // you must revert the changes in step 3 first). | |
117 // | |
118 // On Windows 7: | |
119 // 1. Control panel > Sound > Manage audio devices. | |
120 // 2. In the recording tab, right-click in an empty space in the pane with the | |
121 // devices. Tick 'show disabled devices'. | |
122 // 3. You should see a 'stero mix' device - this is what your speakers output. | |
123 // Right click > Properties. | |
124 // 4. In the Listen tab for the mix device, check the 'listen to this device' | |
125 // checkbox. Ensure the mix device is the default recording device. | |
126 // 5. Launch chrome and try playing a video with sound. You should see | |
127 // in the volume meter for the mix device. Configure the mix device to have | |
128 // 50 / 100 in level. Also go into the playback tab, right-click Speakers, | |
129 // and set that level to 50 / 100. Otherwise you will get distortion in | |
130 // the recording. | |
131 class MAYBE_WebRtcAudioQualityBrowserTest : public WebRtcTestBase { | |
132 public: | |
133 MAYBE_WebRtcAudioQualityBrowserTest() {} | |
134 void SetUpInProcessBrowserTestFixture() override { | |
135 DetectErrorsInJavaScript(); // Look for errors in our rather complex js. | |
136 } | |
137 | |
138 void SetUpCommandLine(base::CommandLine* command_line) override { | |
139 EXPECT_FALSE(command_line->HasSwitch( | |
140 switches::kUseFakeUIForMediaStream)); | |
141 | |
142 // The WebAudio-based tests don't care what devices are available to | |
143 // getUserMedia, and the getUserMedia-based tests will play back a file | |
144 // through the fake device using using --use-file-for-fake-audio-capture. | |
145 command_line->AppendSwitch(switches::kUseFakeDeviceForMediaStream); | |
146 | |
147 // Add loopback interface such that there is always connectivity. | |
148 command_line->AppendSwitch(switches::kAllowLoopbackInPeerConnection); | |
149 } | |
150 | |
151 void ConfigureFakeDeviceToPlayFile(const base::FilePath& wav_file_path) { | |
152 base::CommandLine::ForCurrentProcess()->AppendSwitchNative( | |
153 switches::kUseFileForFakeAudioCapture, | |
154 wav_file_path.value() + FILE_PATH_LITERAL("%noloop")); | |
155 } | |
156 | |
157 void AddAudioFileToWebAudio(const std::string& input_file_relative_url, | |
158 content::WebContents* tab_contents) { | |
159 // This calls into webaudio.js. | |
160 EXPECT_EQ("ok-added", ExecuteJavascript( | |
161 "addAudioFile('" + input_file_relative_url + "')", tab_contents)); | |
162 } | |
163 | |
164 void PlayAudioFileThroughWebAudio(content::WebContents* tab_contents) { | |
165 EXPECT_EQ("ok-playing", ExecuteJavascript("playAudioFile()", tab_contents)); | |
166 } | |
167 | |
168 content::WebContents* OpenPageWithoutGetUserMedia(const char* url) { | |
169 chrome::AddTabAt(browser(), GURL(), -1, true); | |
170 ui_test_utils::NavigateToURL( | |
171 browser(), embedded_test_server()->GetURL(url)); | |
172 content::WebContents* tab = | |
173 browser()->tab_strip_model()->GetActiveWebContents(); | |
174 | |
175 // Prepare the peer connections manually in this test since we don't add | |
176 // getUserMedia-derived media streams in this test like the other tests. | |
177 EXPECT_EQ("ok-peerconnection-created", | |
178 ExecuteJavascript("preparePeerConnection()", tab)); | |
179 return tab; | |
180 } | |
181 | |
182 void MuteMediaElement(const std::string& element_id, | |
183 content::WebContents* tab_contents) { | |
184 EXPECT_EQ("ok-muted", ExecuteJavascript( | |
185 "setMediaElementMuted('" + element_id + "', true)", tab_contents)); | |
186 } | |
187 | |
188 protected: | |
189 void TestAutoGainControl(const base::FilePath::StringType& reference_filename, | |
190 const std::string& constraints, | |
191 const std::string& perf_modifier); | |
192 void SetupAndRecordAudioCall(const base::FilePath& reference_file, | |
193 const base::FilePath& recording, | |
194 const std::string& constraints, | |
195 const base::TimeDelta recording_time); | |
196 void TestWithFakeDeviceGetUserMedia(const std::string& constraints, | |
197 const std::string& perf_modifier); | |
198 }; | |
199 | |
200 namespace { | |
201 | |
202 class AudioRecorder { | |
203 public: | |
204 AudioRecorder() {} | |
205 ~AudioRecorder() {} | |
206 | |
207 // Starts the recording program for the specified duration. Returns true | |
208 // on success. We record in 16-bit 44.1 kHz Stereo (mostly because that's | |
209 // what SoundRecorder.exe will give us and we can't change that). | |
210 bool StartRecording(base::TimeDelta recording_time, | |
211 const base::FilePath& output_file) { | |
212 EXPECT_FALSE(recording_application_.IsValid()) | |
213 << "Tried to record, but is already recording."; | |
214 | |
215 int duration_sec = static_cast<int>(recording_time.InSeconds()); | |
216 base::CommandLine command_line(base::CommandLine::NO_PROGRAM); | |
217 | |
218 #if defined(OS_WIN) | |
219 // This disable is required to run SoundRecorder.exe on 64-bit Windows | |
220 // from a 32-bit binary. We need to load the wow64 disable function from | |
221 // the DLL since it doesn't exist on Windows XP. | |
222 base::ScopedNativeLibrary kernel32_lib(base::FilePath(L"kernel32")); | |
223 if (kernel32_lib.is_valid()) { | |
224 typedef BOOL (WINAPI* Wow64DisableWow64FSRedirection)(PVOID*); | |
225 Wow64DisableWow64FSRedirection wow_64_disable_wow_64_fs_redirection; | |
226 wow_64_disable_wow_64_fs_redirection = | |
227 reinterpret_cast<Wow64DisableWow64FSRedirection>( | |
228 kernel32_lib.GetFunctionPointer( | |
229 "Wow64DisableWow64FsRedirection")); | |
230 if (wow_64_disable_wow_64_fs_redirection != NULL) { | |
231 PVOID* ignored = NULL; | |
232 wow_64_disable_wow_64_fs_redirection(ignored); | |
233 } | |
234 } | |
235 | |
236 char duration_in_hms[128] = {0}; | |
237 struct tm duration_tm = {0}; | |
238 duration_tm.tm_sec = duration_sec; | |
239 EXPECT_NE(0u, strftime(duration_in_hms, arraysize(duration_in_hms), | |
240 "%H:%M:%S", &duration_tm)); | |
241 | |
242 command_line.SetProgram( | |
243 base::FilePath(FILE_PATH_LITERAL("SoundRecorder.exe"))); | |
244 command_line.AppendArg("/FILE"); | |
245 command_line.AppendArgPath(output_file); | |
246 command_line.AppendArg("/DURATION"); | |
247 command_line.AppendArg(duration_in_hms); | |
248 #elif defined(OS_MACOSX) | |
249 command_line.SetProgram(base::FilePath("rec")); | |
250 command_line.AppendArg("-b"); | |
251 command_line.AppendArg("16"); | |
252 command_line.AppendArg("-q"); | |
253 command_line.AppendArgPath(output_file); | |
254 command_line.AppendArg("trim"); | |
255 command_line.AppendArg("0"); | |
256 command_line.AppendArg(base::IntToString(duration_sec)); | |
257 #else | |
258 command_line.SetProgram(base::FilePath("arecord")); | |
259 command_line.AppendArg("-d"); | |
260 command_line.AppendArg(base::IntToString(duration_sec)); | |
261 command_line.AppendArg("-f"); | |
262 command_line.AppendArg("cd"); | |
263 command_line.AppendArg("-c"); | |
264 command_line.AppendArg("2"); | |
265 command_line.AppendArgPath(output_file); | |
266 #endif | |
267 | |
268 DVLOG(0) << "Running " << command_line.GetCommandLineString(); | |
269 recording_application_ = | |
270 base::LaunchProcess(command_line, base::LaunchOptions()); | |
271 return recording_application_.IsValid(); | |
272 } | |
273 | |
274 // Joins the recording program. Returns true on success. | |
275 bool WaitForRecordingToEnd() { | |
276 int exit_code = -1; | |
277 recording_application_.WaitForExit(&exit_code); | |
278 return exit_code == 0; | |
279 } | |
280 private: | |
281 base::Process recording_application_; | |
282 }; | |
283 | |
284 bool ForceMicrophoneVolumeTo100Percent() { | |
285 #if defined(OS_WIN) | |
286 // Note: the force binary isn't in tools since it's one of our own. | |
287 base::CommandLine command_line(test::GetReferenceFilesDir().Append( | |
288 FILE_PATH_LITERAL("force_mic_volume_max.exe"))); | |
289 DVLOG(0) << "Running " << command_line.GetCommandLineString(); | |
290 std::string result; | |
291 if (!base::GetAppOutput(command_line, &result)) { | |
292 LOG(ERROR) << "Failed to set source volume: output was " << result; | |
293 return false; | |
294 } | |
295 #elif defined(OS_MACOSX) | |
296 base::CommandLine command_line( | |
297 base::FilePath(FILE_PATH_LITERAL("osascript"))); | |
298 command_line.AppendArg("-e"); | |
299 command_line.AppendArg("set volume input volume 100"); | |
300 command_line.AppendArg("-e"); | |
301 command_line.AppendArg("set volume output volume 85"); | |
302 | |
303 std::string result; | |
304 if (!base::GetAppOutput(command_line, &result)) { | |
305 LOG(ERROR) << "Failed to set source volume: output was " << result; | |
306 return false; | |
307 } | |
308 #else | |
309 // Just force the volume of, say the first 5 devices. A machine will rarely | |
310 // have more input sources than that. This is way easier than finding the | |
311 // input device we happen to be using. | |
312 for (int device_index = 0; device_index < 5; ++device_index) { | |
313 std::string result; | |
314 const std::string kHundredPercentVolume = "65536"; | |
315 base::CommandLine command_line(base::FilePath(FILE_PATH_LITERAL("pacmd"))); | |
316 command_line.AppendArg("set-source-volume"); | |
317 command_line.AppendArg(base::IntToString(device_index)); | |
318 command_line.AppendArg(kHundredPercentVolume); | |
319 DVLOG(0) << "Running " << command_line.GetCommandLineString(); | |
320 if (!base::GetAppOutput(command_line, &result)) { | |
321 LOG(ERROR) << "Failed to set source volume: output was " << result; | |
322 return false; | |
323 } | |
324 } | |
325 #endif | |
326 return true; | |
327 } | |
328 | |
329 // Sox is the "Swiss army knife" of audio processing. We mainly use it for | |
330 // silence trimming. See http://sox.sourceforge.net. | |
331 base::CommandLine MakeSoxCommandLine() { | |
332 #if defined(OS_WIN) | |
333 base::FilePath sox_path = test::GetToolForPlatform("sox"); | |
334 if (!base::PathExists(sox_path)) { | |
335 LOG(ERROR) << "Missing sox.exe binary in " << sox_path.value() | |
336 << "; you may have to provide this binary yourself."; | |
337 return base::CommandLine(base::CommandLine::NO_PROGRAM); | |
338 } | |
339 base::CommandLine command_line(sox_path); | |
340 #else | |
341 // TODO(phoglund): call checked-in sox rather than system sox on mac/linux. | |
342 // Same for rec invocations on Mac, above. | |
343 base::CommandLine command_line(base::FilePath(FILE_PATH_LITERAL("sox"))); | |
344 #endif | |
345 return command_line; | |
346 } | |
347 | |
348 // Removes silence from beginning and end of the |input_audio_file| and writes | |
349 // the result to the |output_audio_file|. Returns true on success. | |
350 bool RemoveSilence(const base::FilePath& input_file, | |
351 const base::FilePath& output_file) { | |
352 // SOX documentation for silence command: http://sox.sourceforge.net/sox.html | |
353 // To remove the silence from both beginning and end of the audio file, we | |
354 // call sox silence command twice: once on normal file and again on its | |
355 // reverse, then we reverse the final output. | |
356 // Silence parameters are (in sequence): | |
357 // ABOVE_PERIODS: The period for which silence occurs. Value 1 is used for | |
358 // silence at beginning of audio. | |
359 // DURATION: the amount of time in seconds that non-silence must be detected | |
360 // before sox stops trimming audio. | |
361 // THRESHOLD: value used to indicate what sample value is treats as silence. | |
362 const char* kAbovePeriods = "1"; | |
363 const char* kDuration = "2"; | |
364 const char* kTreshold = "1.5%"; | |
365 | |
366 base::CommandLine command_line = MakeSoxCommandLine(); | |
367 if (command_line.GetProgram().empty()) | |
368 return false; | |
369 command_line.AppendArgPath(input_file); | |
370 command_line.AppendArgPath(output_file); | |
371 command_line.AppendArg("silence"); | |
372 command_line.AppendArg(kAbovePeriods); | |
373 command_line.AppendArg(kDuration); | |
374 command_line.AppendArg(kTreshold); | |
375 command_line.AppendArg("reverse"); | |
376 command_line.AppendArg("silence"); | |
377 command_line.AppendArg(kAbovePeriods); | |
378 command_line.AppendArg(kDuration); | |
379 command_line.AppendArg(kTreshold); | |
380 command_line.AppendArg("reverse"); | |
381 | |
382 DVLOG(0) << "Running " << command_line.GetCommandLineString(); | |
383 std::string result; | |
384 bool ok = base::GetAppOutput(command_line, &result); | |
385 DVLOG(0) << "Output was:\n\n" << result; | |
386 return ok; | |
387 } | |
388 | |
389 // Looks for 0.2 second audio segments surrounded by silences under 0.3% audio | |
390 // power and splits the input file on those silences. Output files are written | |
391 // according to the output file template (e.g. /tmp/out.wav writes | |
392 // /tmp/out001.wav, /tmp/out002.wav, etc if there are two silence-padded | |
393 // regions in the file). The silences between speech segments must be at | |
394 // least 500 ms for this to be reliable. | |
395 bool SplitFileOnSilence(const base::FilePath& input_file, | |
396 const base::FilePath& output_file_template) { | |
397 base::CommandLine command_line = MakeSoxCommandLine(); | |
398 if (command_line.GetProgram().empty()) | |
399 return false; | |
400 | |
401 // These are experimentally determined and work on the files we use. | |
402 const char* kAbovePeriods = "1"; | |
403 const char* kUnderPeriods = "1"; | |
404 const char* kDuration = "0.2"; | |
405 const char* kTreshold = "0.5%"; | |
406 command_line.AppendArgPath(input_file); | |
407 command_line.AppendArgPath(output_file_template); | |
408 command_line.AppendArg("silence"); | |
409 command_line.AppendArg(kAbovePeriods); | |
410 command_line.AppendArg(kDuration); | |
411 command_line.AppendArg(kTreshold); | |
412 command_line.AppendArg(kUnderPeriods); | |
413 command_line.AppendArg(kDuration); | |
414 command_line.AppendArg(kTreshold); | |
415 command_line.AppendArg(":"); | |
416 command_line.AppendArg("newfile"); | |
417 command_line.AppendArg(":"); | |
418 command_line.AppendArg("restart"); | |
419 | |
420 DVLOG(0) << "Running " << command_line.GetCommandLineString(); | |
421 std::string result; | |
422 bool ok = base::GetAppOutput(command_line, &result); | |
423 DVLOG(0) << "Output was:\n\n" << result; | |
424 return ok; | |
425 } | |
426 | |
427 bool CanParseAsFloat(const std::string& value) { | |
428 return atof(value.c_str()) != 0 || value == "0"; | |
429 } | |
430 | |
431 // Runs PESQ to compare |reference_file| to a |actual_file|. The |sample_rate| | |
432 // can be either 16000 or 8000. | |
433 // | |
434 // PESQ is only mono-aware, so the files should preferably be recorded in mono. | |
435 // Furthermore it expects the file to be 16 rather than 32 bits, even though | |
436 // 32 bits might work. The audio bandwidth of the two files should be the same | |
437 // e.g. don't compare a 32 kHz file to a 8 kHz file. | |
438 // | |
439 // The raw score in MOS is written to |raw_mos|, whereas the MOS-LQO score is | |
440 // written to mos_lqo. The scores are returned as floats in string form (e.g. | |
441 // "3.145", etc). Returns true on success. | |
442 bool RunPesq(const base::FilePath& reference_file, | |
443 const base::FilePath& actual_file, | |
444 int sample_rate, std::string* raw_mos, std::string* mos_lqo) { | |
445 // PESQ will break if the paths are too long (!). | |
446 EXPECT_LT(reference_file.value().length(), 128u); | |
447 EXPECT_LT(actual_file.value().length(), 128u); | |
448 | |
449 base::FilePath pesq_path = test::GetToolForPlatform("pesq"); | |
450 if (!base::PathExists(pesq_path)) { | |
451 LOG(ERROR) << "Missing PESQ binary in " << pesq_path.value() | |
452 << "; you may have to provide this binary yourself."; | |
453 return false; | |
454 } | |
455 | |
456 base::CommandLine command_line(pesq_path); | |
457 command_line.AppendArg(base::StringPrintf("+%d", sample_rate)); | |
458 command_line.AppendArgPath(reference_file); | |
459 command_line.AppendArgPath(actual_file); | |
460 | |
461 DVLOG(0) << "Running " << command_line.GetCommandLineString(); | |
462 std::string result; | |
463 if (!base::GetAppOutput(command_line, &result)) { | |
464 LOG(ERROR) << "Failed to run PESQ."; | |
465 return false; | |
466 } | |
467 DVLOG(0) << "Output was:\n\n" << result; | |
468 | |
469 const std::string result_anchor = "Prediction (Raw MOS, MOS-LQO): = "; | |
470 std::size_t anchor_pos = result.find(result_anchor); | |
471 if (anchor_pos == std::string::npos) { | |
472 LOG(ERROR) << "PESQ was not able to compute a score; we probably recorded " | |
473 << "only silence. Please check the output/input volume levels."; | |
474 return false; | |
475 } | |
476 | |
477 // There are two tab-separated numbers on the format x.xxx, e.g. 5 chars each. | |
478 std::size_t first_number_pos = anchor_pos + result_anchor.length(); | |
479 *raw_mos = result.substr(first_number_pos, 5); | |
480 EXPECT_TRUE(CanParseAsFloat(*raw_mos)) << "Failed to parse raw MOS number."; | |
481 *mos_lqo = result.substr(first_number_pos + 5 + 1, 5); | |
482 EXPECT_TRUE(CanParseAsFloat(*mos_lqo)) << "Failed to parse MOS LQO number."; | |
483 | |
484 return true; | |
485 } | |
486 | |
487 base::FilePath CreateTemporaryWaveFile() { | |
488 base::FilePath filename; | |
489 EXPECT_TRUE(base::CreateTemporaryFile(&filename)); | |
490 base::FilePath wav_filename = | |
491 filename.AddExtension(FILE_PATH_LITERAL(".wav")); | |
492 EXPECT_TRUE(base::Move(filename, wav_filename)); | |
493 return wav_filename; | |
494 } | |
495 | |
496 void DeleteFileUnlessTestFailed(const base::FilePath& path, bool recursive) { | |
497 if (::testing::Test::HasFailure()) | |
498 printf("Test failed; keeping recording(s) at\n\t%" PRFilePath ".\n", | |
499 path.value().c_str()); | |
500 else | |
501 EXPECT_TRUE(base::DeleteFile(path, recursive)); | |
502 } | |
503 | |
504 std::vector<base::FilePath> ListWavFilesInDir(const base::FilePath& dir) { | |
505 base::FileEnumerator files(dir, false, base::FileEnumerator::FILES, | |
506 FILE_PATH_LITERAL("*.wav")); | |
507 | |
508 std::vector<base::FilePath> result; | |
509 for (base::FilePath name = files.Next(); !name.empty(); name = files.Next()) | |
510 result.push_back(name); | |
511 return result; | |
512 } | |
513 | |
514 // Splits |to_split| into sub-files based on silence. The file you use must have | |
515 // at least 500 ms periods of silence between speech segments for this to be | |
516 // reliable. | |
517 void SplitFileOnSilenceIntoDir(const base::FilePath& to_split, | |
518 const base::FilePath& workdir) { | |
519 // First trim beginning and end since they are tricky for the splitter. | |
520 base::FilePath trimmed_audio = CreateTemporaryWaveFile(); | |
521 | |
522 ASSERT_TRUE(RemoveSilence(to_split, trimmed_audio)); | |
523 DVLOG(0) << "Trimmed silence: " << trimmed_audio.value() << std::endl; | |
524 | |
525 ASSERT_TRUE(SplitFileOnSilence( | |
526 trimmed_audio, workdir.Append(FILE_PATH_LITERAL("output.wav")))); | |
527 DeleteFileUnlessTestFailed(trimmed_audio, false); | |
528 } | |
529 | |
530 // Computes the difference between the actual and reference segment. A positive | |
531 // number x means the actual file is x dB stronger than the reference. | |
532 float AnalyzeOneSegment(const base::FilePath& ref_segment, | |
533 const base::FilePath& actual_segment, | |
534 int segment_number) { | |
535 media::AudioParameters ref_parameters; | |
536 media::AudioParameters actual_parameters; | |
537 float ref_energy = | |
538 test::ComputeAudioEnergyForWavFile(ref_segment, &ref_parameters); | |
539 float actual_energy = | |
540 test::ComputeAudioEnergyForWavFile(actual_segment, &actual_parameters); | |
541 | |
542 base::TimeDelta difference_in_length = ref_parameters.GetBufferDuration() - | |
543 actual_parameters.GetBufferDuration(); | |
544 | |
545 EXPECT_LE(difference_in_length, | |
546 base::TimeDelta::FromMilliseconds(kMaxAgcSegmentDiffMs)) | |
547 << "Segments differ " << difference_in_length.InMilliseconds() << " ms " | |
548 << "in length for segment " << segment_number << "; we're likely " | |
549 << "comparing unrelated segments or silence splitting is busted."; | |
550 | |
551 return actual_energy - ref_energy; | |
552 } | |
553 | |
554 std::string MakeTraceName(const base::FilePath& ref_filename, | |
555 size_t segment_number) { | |
556 std::string ascii_filename; | |
557 #if defined(OS_WIN) | |
558 ascii_filename = base::WideToUTF8(ref_filename.BaseName().value()); | |
559 #else | |
560 ascii_filename = ref_filename.BaseName().value(); | |
561 #endif | |
562 return base::StringPrintf( | |
563 "%s_segment_%d", ascii_filename.c_str(), (int)segment_number); | |
564 } | |
565 | |
566 void AnalyzeSegmentsAndPrintResult( | |
567 const std::vector<base::FilePath>& ref_segments, | |
568 const std::vector<base::FilePath>& actual_segments, | |
569 const base::FilePath& reference_file, | |
570 const std::string& perf_modifier) { | |
571 ASSERT_GT(ref_segments.size(), 0u) | |
572 << "Failed to split reference file on silence; sox is likely broken."; | |
573 ASSERT_EQ(ref_segments.size(), actual_segments.size()) | |
574 << "The recording did not result in the same number of audio segments " | |
575 << "after on splitting on silence; WebRTC must have deformed the audio " | |
576 << "too much."; | |
577 | |
578 for (size_t i = 0; i < ref_segments.size(); i++) { | |
579 float difference_in_decibel = AnalyzeOneSegment(ref_segments[i], | |
580 actual_segments[i], | |
581 i); | |
582 std::string trace_name = MakeTraceName(reference_file, i); | |
583 perf_test::PrintResult("agc_energy_diff", perf_modifier, trace_name, | |
584 difference_in_decibel, "dB", false); | |
585 } | |
586 } | |
587 | |
588 void ComputeAndPrintPesqResults(const base::FilePath& reference_file, | |
589 const base::FilePath& recording, | |
590 const std::string& perf_modifier) { | |
591 base::FilePath trimmed_reference = CreateTemporaryWaveFile(); | |
592 base::FilePath trimmed_recording = CreateTemporaryWaveFile(); | |
593 | |
594 ASSERT_TRUE(RemoveSilence(reference_file, trimmed_reference)); | |
595 ASSERT_TRUE(RemoveSilence(recording, trimmed_recording)); | |
596 | |
597 std::string raw_mos; | |
598 std::string mos_lqo; | |
599 bool succeeded = RunPesq(trimmed_reference, trimmed_recording, 16000, | |
600 &raw_mos, &mos_lqo); | |
601 EXPECT_TRUE(succeeded) << "Failed to run PESQ."; | |
602 if (succeeded) { | |
603 perf_test::PrintResult( | |
604 "audio_pesq", perf_modifier, "raw_mos", raw_mos, "score", true); | |
605 perf_test::PrintResult( | |
606 "audio_pesq", perf_modifier, "mos_lqo", mos_lqo, "score", true); | |
607 } | |
608 | |
609 DeleteFileUnlessTestFailed(trimmed_reference, false); | |
610 DeleteFileUnlessTestFailed(trimmed_recording, false); | |
611 } | |
612 | |
613 } // namespace | |
614 | |
615 // Sets up a two-way WebRTC call and records its output to |recording|, using | |
616 // getUserMedia. | |
617 // | |
618 // |reference_file| should have at least five seconds of silence in the | |
619 // beginning: otherwise all the reference audio will not be picked up by the | |
620 // recording. Note that the reference file will start playing as soon as the | |
621 // audio device is up following the getUserMedia call in the left tab. The time | |
622 // it takes to negotiate a call isn't deterministic, but five seconds should be | |
623 // plenty of time. Similarly, the recording time should be enough to catch the | |
624 // whole reference file. If you then silence-trim the reference file and actual | |
625 // file, you should end up with two time-synchronized files. | |
626 void MAYBE_WebRtcAudioQualityBrowserTest::SetupAndRecordAudioCall( | |
627 const base::FilePath& reference_file, | |
628 const base::FilePath& recording, | |
629 const std::string& constraints, | |
630 const base::TimeDelta recording_time) { | |
631 ASSERT_TRUE(embedded_test_server()->Start()); | |
632 ASSERT_TRUE(test::HasReferenceFilesInCheckout()); | |
633 ASSERT_TRUE(ForceMicrophoneVolumeTo100Percent()); | |
634 | |
635 ConfigureFakeDeviceToPlayFile(reference_file); | |
636 | |
637 // Create a two-way call. Mute one of the receivers though; that way it will | |
638 // be receiving audio bytes, but we will not be playing out of both elements. | |
639 GURL test_page = embedded_test_server()->GetURL(kWebRtcAudioTestHtmlPage); | |
640 content::WebContents* left_tab = | |
641 OpenPageAndGetUserMediaInNewTabWithConstraints(test_page, constraints); | |
642 SetupPeerconnectionWithLocalStream(left_tab); | |
643 MuteMediaElement("remote-view", left_tab); | |
644 | |
645 content::WebContents* right_tab = | |
646 OpenPageAndGetUserMediaInNewTabWithConstraints(test_page, constraints); | |
647 SetupPeerconnectionWithLocalStream(right_tab); | |
648 | |
649 AudioRecorder recorder; | |
650 ASSERT_TRUE(recorder.StartRecording(recording_time, recording)); | |
651 | |
652 NegotiateCall(left_tab, right_tab); | |
653 | |
654 ASSERT_TRUE(recorder.WaitForRecordingToEnd()); | |
655 DVLOG(0) << "Done recording to " << recording.value() << std::endl; | |
656 | |
657 HangUp(left_tab); | |
658 } | |
659 | |
660 void MAYBE_WebRtcAudioQualityBrowserTest::TestWithFakeDeviceGetUserMedia( | |
661 const std::string& constraints, | |
662 const std::string& perf_modifier) { | |
663 if (OnWin8()) { | |
664 // http://crbug.com/379798. | |
665 LOG(ERROR) << "This test is not implemented for Windows XP/Win8."; | |
666 return; | |
667 } | |
668 | |
669 base::FilePath reference_file = | |
670 test::GetReferenceFilesDir().Append(kReferenceFile); | |
671 base::FilePath recording = CreateTemporaryWaveFile(); | |
672 | |
673 ASSERT_NO_FATAL_FAILURE(SetupAndRecordAudioCall( | |
674 reference_file, recording, constraints, | |
675 base::TimeDelta::FromSeconds(30))); | |
676 | |
677 ComputeAndPrintPesqResults(reference_file, recording, perf_modifier); | |
678 DeleteFileUnlessTestFailed(recording, false); | |
679 } | |
680 | |
681 IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcAudioQualityBrowserTest, | |
682 MANUAL_TestCallQualityWithAudioFromFakeDevice) { | |
683 TestWithFakeDeviceGetUserMedia(kAudioOnlyCallConstraints, "_getusermedia"); | |
684 } | |
685 | |
686 IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcAudioQualityBrowserTest, | |
687 MANUAL_TestCallQualityWithAudioFromWebAudio) { | |
688 if (OnWin8()) { | |
689 // http://crbug.com/379798. | |
690 LOG(ERROR) << "This test is not implemented for Windows XP/Win8."; | |
691 return; | |
692 } | |
693 ASSERT_TRUE(test::HasReferenceFilesInCheckout()); | |
694 ASSERT_TRUE(embedded_test_server()->Start()); | |
695 | |
696 ASSERT_TRUE(ForceMicrophoneVolumeTo100Percent()); | |
697 | |
698 content::WebContents* left_tab = | |
699 OpenPageWithoutGetUserMedia(kWebRtcAudioTestHtmlPage); | |
700 content::WebContents* right_tab = | |
701 OpenPageWithoutGetUserMedia(kWebRtcAudioTestHtmlPage); | |
702 | |
703 AddAudioFileToWebAudio(kReferenceFileRelativeUrl, left_tab); | |
704 | |
705 NegotiateCall(left_tab, right_tab); | |
706 | |
707 base::FilePath recording = CreateTemporaryWaveFile(); | |
708 | |
709 // Note: the sound clip is 21.6 seconds: record for 25 seconds to get some | |
710 // safety margins on each side. | |
711 AudioRecorder recorder; | |
712 ASSERT_TRUE(recorder.StartRecording(base::TimeDelta::FromSeconds(25), | |
713 recording)); | |
714 | |
715 PlayAudioFileThroughWebAudio(left_tab); | |
716 | |
717 ASSERT_TRUE(recorder.WaitForRecordingToEnd()); | |
718 DVLOG(0) << "Done recording to " << recording.value() << std::endl; | |
719 | |
720 HangUp(left_tab); | |
721 | |
722 // Compare with the reference file on disk (this is the same file we played | |
723 // through WebAudio earlier). | |
724 base::FilePath reference_file = | |
725 test::GetReferenceFilesDir().Append(kReferenceFile); | |
726 ComputeAndPrintPesqResults(reference_file, recording, "_webaudio"); | |
727 } | |
728 | |
729 /** | |
730 * The auto gain control test plays a file into the fake microphone. Then it | |
731 * sets up a one-way WebRTC call with audio only and records Chrome's output on | |
732 * the receiving side using the audio loopback provided by the quality test | |
733 * (see the class comments for more details). | |
734 * | |
735 * Then both the recording and reference file are split on silence. This creates | |
736 * a number of segments with speech in them. The reason for this is to provide | |
737 * a kind of synchronization mechanism so the start of each speech segment is | |
738 * compared to the start of the corresponding speech segment. This is because we | |
739 * will experience inevitable clock drift between the system clock (which runs | |
740 * the fake microphone) and the sound card (which runs play-out). Effectively | |
741 * re-synchronizing on each segment mitigates this. | |
742 * | |
743 * The silence splitting is inherently sensitive to the sound file we run on. | |
744 * Therefore the reference file must have at least 500 ms of pure silence | |
745 * between speech segments; the test will fail if the output produces more | |
746 * segments than the reference. | |
747 * | |
748 * The test reports the difference in decibel between the reference and output | |
749 * file per 10 ms interval in each speech segment. A value of 6 means the | |
750 * output was 6 dB louder than the reference, presumably because the AGC applied | |
751 * gain to the signal. | |
752 * | |
753 * The test only exercises digital AGC for now. | |
754 * | |
755 * We record in CD format here (44.1 kHz) because that's what the fake input | |
756 * device currently supports, and we want to be able to compare directly. See | |
757 * http://crbug.com/421054. | |
758 */ | |
759 void MAYBE_WebRtcAudioQualityBrowserTest::TestAutoGainControl( | |
760 const base::FilePath::StringType& reference_filename, | |
761 const std::string& constraints, | |
762 const std::string& perf_modifier) { | |
763 if (OnWin8()) { | |
764 // http://crbug.com/379798. | |
765 LOG(ERROR) << "This test is not implemented for Windows XP/Win8."; | |
766 return; | |
767 } | |
768 base::FilePath reference_file = | |
769 test::GetReferenceFilesDir().Append(reference_filename); | |
770 base::FilePath recording = CreateTemporaryWaveFile(); | |
771 | |
772 ASSERT_NO_FATAL_FAILURE(SetupAndRecordAudioCall( | |
773 reference_file, recording, constraints, | |
774 base::TimeDelta::FromSeconds(30))); | |
775 | |
776 base::ScopedTempDir split_ref_files; | |
777 ASSERT_TRUE(split_ref_files.CreateUniqueTempDir()); | |
778 ASSERT_NO_FATAL_FAILURE( | |
779 SplitFileOnSilenceIntoDir(reference_file, split_ref_files.path())); | |
780 std::vector<base::FilePath> ref_segments = | |
781 ListWavFilesInDir(split_ref_files.path()); | |
782 | |
783 base::ScopedTempDir split_actual_files; | |
784 ASSERT_TRUE(split_actual_files.CreateUniqueTempDir()); | |
785 ASSERT_NO_FATAL_FAILURE( | |
786 SplitFileOnSilenceIntoDir(recording, split_actual_files.path())); | |
787 | |
788 // Keep the recording and split files if the analysis fails. | |
789 base::FilePath actual_files_dir = split_actual_files.Take(); | |
790 std::vector<base::FilePath> actual_segments = | |
791 ListWavFilesInDir(actual_files_dir); | |
792 | |
793 AnalyzeSegmentsAndPrintResult( | |
794 ref_segments, actual_segments, reference_file, perf_modifier); | |
795 | |
796 DeleteFileUnlessTestFailed(recording, false); | |
797 DeleteFileUnlessTestFailed(actual_files_dir, true); | |
798 } | |
799 | |
800 // The AGC should apply non-zero gain here. | |
801 IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcAudioQualityBrowserTest, | |
802 MANUAL_TestAutoGainControlOnLowAudio) { | |
803 ASSERT_NO_FATAL_FAILURE(TestAutoGainControl( | |
804 kReferenceFile, kAudioOnlyCallConstraints, "_with_agc")); | |
805 } | |
806 | |
807 // The test is failing on the Win7 bot. | |
808 // http://crbug.com/625808#c23 | |
809 #if defined(OS_WIN) | |
810 #define MAYBE_MANUAL_TestAutoGainIsOffWithAudioProcessingOff\ | |
811 DISABLED_TestAutoGainIsOffWithAudioProcessingOff | |
812 #else | |
813 #define MAYBE_MANUAL_TestAutoGainIsOffWithAudioProcessingOff\ | |
814 MANUAL_TestAutoGainIsOffWithAudioProcessingOff | |
815 #endif | |
816 // Since the AGC is off here there should be no gain at all. | |
817 IN_PROC_BROWSER_TEST_F(MAYBE_WebRtcAudioQualityBrowserTest, | |
818 MAYBE_MANUAL_TestAutoGainIsOffWithAudioProcessingOff) { | |
819 const char* kAudioCallWithoutAudioProcessing = | |
820 "{audio: { mandatory: { echoCancellation: false } } }"; | |
821 ASSERT_NO_FATAL_FAILURE(TestAutoGainControl( | |
822 kReferenceFile, kAudioCallWithoutAudioProcessing, "_no_agc")); | |
823 } | |
OLD | NEW |