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