Home | History | Annotate | Download | only in media
      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 <ctime>
      6 
      7 #include "base/command_line.h"
      8 #include "base/file_util.h"
      9 #include "base/path_service.h"
     10 #include "base/process/launch.h"
     11 #include "base/scoped_native_library.h"
     12 #include "base/strings/stringprintf.h"
     13 #include "base/win/windows_version.h"
     14 #include "chrome/browser/media/webrtc_browsertest_base.h"
     15 #include "chrome/browser/media/webrtc_browsertest_common.h"
     16 #include "chrome/browser/profiles/profile.h"
     17 #include "chrome/browser/ui/browser.h"
     18 #include "chrome/browser/ui/browser_tabstrip.h"
     19 #include "chrome/browser/ui/tabs/tab_strip_model.h"
     20 #include "chrome/common/chrome_paths.h"
     21 #include "chrome/common/chrome_switches.h"
     22 #include "chrome/test/base/ui_test_utils.h"
     23 #include "chrome/test/ui/ui_test.h"
     24 #include "content/public/test/browser_test_utils.h"
     25 #include "net/test/embedded_test_server/embedded_test_server.h"
     26 #include "testing/perf/perf_test.h"
     27 
     28 static const base::FilePath::CharType kReferenceFile[] =
     29 #if defined (OS_WIN)
     30     FILE_PATH_LITERAL("pyauto_private/webrtc/human-voice-win.wav");
     31 #else
     32     FILE_PATH_LITERAL("pyauto_private/webrtc/human-voice-linux.wav");
     33 #endif
     34 
     35 // The javascript will load the reference file relative to its location,
     36 // which is in /webrtc on the web server. Therefore, prepend a '..' traversal.
     37 static const char kReferenceFileRelativeUrl[] =
     38 #if defined (OS_WIN)
     39     "../pyauto_private/webrtc/human-voice-win.wav";
     40 #else
     41     "../pyauto_private/webrtc/human-voice-linux.wav";
     42 #endif
     43 
     44 static const base::FilePath::CharType kToolsPath[] =
     45     FILE_PATH_LITERAL("pyauto_private/media/tools");
     46 
     47 static const char kMainWebrtcTestHtmlPage[] =
     48     "/webrtc/webrtc_audio_quality_test.html";
     49 
     50 static base::FilePath GetTestDataDir() {
     51   base::FilePath source_dir;
     52   PathService::Get(chrome::DIR_TEST_DATA, &source_dir);
     53   return source_dir;
     54 }
     55 
     56 // Test we can set up a WebRTC call and play audio through it.
     57 //
     58 // You must have the src-internal solution in your .gclient to put the required
     59 // pyauto_private directory into chrome/test/data/.
     60 //
     61 // This test will only work on machines that have been configured to record
     62 // their own input.
     63 //
     64 // On Linux:
     65 // 1. # sudo apt-get install pavucontrol
     66 // 2. For the user who will run the test: # pavucontrol
     67 // 3. In a separate terminal, # arecord dummy
     68 // 4. In pavucontrol, go to the recording tab.
     69 // 5. For the ALSA plug-in [aplay]: ALSA Capture from, change from <x> to
     70 //    <Monitor of x>, where x is whatever your primary sound device is called.
     71 // 6. Try launching chrome as the target user on the target machine, try
     72 //    playing, say, a YouTube video, and record with # arecord -f dat tmp.dat.
     73 //    Verify the recording with aplay (should have recorded what you played
     74 //    from chrome).
     75 //
     76 // Note: the volume for ALL your input devices will be forced to 100% by
     77 //       running this test on Linux.
     78 //
     79 // On Windows 7:
     80 // 1. Control panel > Sound > Manage audio devices.
     81 // 2. In the recording tab, right-click in an empty space in the pane with the
     82 //    devices. Tick 'show disabled devices'.
     83 // 3. You should see a 'stero mix' device - this is what your speakers output.
     84 //    Right click > Properties.
     85 // 4. In the Listen tab for the mix device, check the 'listen to this device'
     86 //    checkbox. Ensure the mix device is the default recording device.
     87 // 5. Launch chrome and try playing a video with sound. You should see
     88 //    in the volume meter for the mix device. Configure the mix device to have
     89 //    50 / 100 in level. Also go into the playback tab, right-click Speakers,
     90 //    and set that level to 50 / 100. Otherwise you will get distortion in
     91 //    the recording.
     92 class WebrtcAudioQualityBrowserTest : public WebRtcTestBase {
     93  public:
     94   virtual void SetUpInProcessBrowserTestFixture() OVERRIDE {
     95     PeerConnectionServerRunner::KillAllPeerConnectionServersOnCurrentSystem();
     96     DetectErrorsInJavaScript();  // Look for errors in our rather complex js.
     97   }
     98 
     99   virtual void SetUpCommandLine(CommandLine* command_line) OVERRIDE {
    100     // This test expects real device handling and requires a real webcam / audio
    101     // device; it will not work with fake devices.
    102     EXPECT_FALSE(command_line->HasSwitch(
    103         switches::kUseFakeDeviceForMediaStream));
    104     EXPECT_FALSE(command_line->HasSwitch(
    105         switches::kUseFakeUIForMediaStream));
    106   }
    107 
    108   bool HasAllRequiredResources() {
    109     base::FilePath reference_file = GetTestDataDir().Append(kReferenceFile);
    110     if (!base::PathExists(reference_file)) {
    111       LOG(ERROR) << "Cannot find the reference file to be used for audio "
    112           << "quality comparison: " << reference_file.value();
    113       return false;
    114     }
    115     return true;
    116   }
    117 
    118   void AddAudioFile(const std::string& input_file_relative_url,
    119                     content::WebContents* tab_contents) {
    120     EXPECT_EQ("ok-added", ExecuteJavascript(
    121         "addAudioFile('" + input_file_relative_url + "')", tab_contents));
    122   }
    123 
    124   void PlayAudioFile(content::WebContents* tab_contents) {
    125     EXPECT_EQ("ok-playing", ExecuteJavascript("playAudioFile()", tab_contents));
    126   }
    127 
    128   void EstablishCall(content::WebContents* from_tab,
    129                      content::WebContents* to_tab) {
    130     EXPECT_EQ("ok-negotiating",
    131               ExecuteJavascript("negotiateCall()", from_tab));
    132 
    133     // Ensure the call gets up on both sides.
    134     EXPECT_TRUE(PollingWaitUntil("getPeerConnectionReadyState()",
    135                                  "active", from_tab));
    136     EXPECT_TRUE(PollingWaitUntil("getPeerConnectionReadyState()",
    137                                  "active", to_tab));
    138   }
    139 
    140   void HangUp(content::WebContents* from_tab) {
    141     EXPECT_EQ("ok-call-hung-up", ExecuteJavascript("hangUp()", from_tab));
    142   }
    143 
    144   void WaitUntilHangupVerified(content::WebContents* tab_contents) {
    145     EXPECT_TRUE(PollingWaitUntil("getPeerConnectionReadyState()",
    146                                  "no-peer-connection", tab_contents));
    147   }
    148 
    149   base::FilePath CreateTemporaryWaveFile() {
    150     base::FilePath filename;
    151     EXPECT_TRUE(base::CreateTemporaryFile(&filename));
    152     base::FilePath wav_filename =
    153         filename.AddExtension(FILE_PATH_LITERAL(".wav"));
    154     EXPECT_TRUE(base::Move(filename, wav_filename));
    155     return wav_filename;
    156   }
    157 
    158   PeerConnectionServerRunner peerconnection_server_;
    159 };
    160 
    161 class AudioRecorder {
    162  public:
    163   AudioRecorder(): recording_application_(base::kNullProcessHandle) {}
    164   ~AudioRecorder() {}
    165 
    166   // Starts the recording program for the specified duration. Returns true
    167   // on success.
    168   bool StartRecording(int duration_sec, const base::FilePath& output_file,
    169                       bool mono) {
    170     EXPECT_EQ(base::kNullProcessHandle, recording_application_)
    171         << "Tried to record, but is already recording.";
    172 
    173     CommandLine command_line(CommandLine::NO_PROGRAM);
    174 #if defined(OS_WIN)
    175     // This disable is required to run SoundRecorder.exe on 64-bit Windows
    176     // from a 32-bit binary. We need to load the wow64 disable function from
    177     // the DLL since it doesn't exist on Windows XP.
    178     // TODO(phoglund): find some cleaner solution than using SoundRecorder.exe.
    179     base::ScopedNativeLibrary kernel32_lib(base::FilePath(L"kernel32"));
    180     if (kernel32_lib.is_valid()) {
    181       typedef BOOL (WINAPI* Wow64DisableWow64FSRedirection)(PVOID*);
    182       Wow64DisableWow64FSRedirection wow_64_disable_wow_64_fs_redirection;
    183       wow_64_disable_wow_64_fs_redirection =
    184           reinterpret_cast<Wow64DisableWow64FSRedirection>(
    185               kernel32_lib.GetFunctionPointer(
    186                   "Wow64DisableWow64FsRedirection"));
    187       if (wow_64_disable_wow_64_fs_redirection != NULL) {
    188         PVOID* ignored = NULL;
    189         wow_64_disable_wow_64_fs_redirection(ignored);
    190       }
    191     }
    192 
    193     char duration_in_hms[128] = {0};
    194     struct tm duration_tm = {0};
    195     duration_tm.tm_sec = duration_sec;
    196     EXPECT_NE(0u, strftime(duration_in_hms, arraysize(duration_in_hms),
    197                            "%H:%M:%S", &duration_tm));
    198 
    199     command_line.SetProgram(
    200         base::FilePath(FILE_PATH_LITERAL("SoundRecorder.exe")));
    201     command_line.AppendArg("/FILE");
    202     command_line.AppendArgPath(output_file);
    203     command_line.AppendArg("/DURATION");
    204     command_line.AppendArg(duration_in_hms);
    205 #else
    206     int num_channels = mono ? 1 : 2;
    207     command_line.SetProgram(base::FilePath("arecord"));
    208     command_line.AppendArg("-d");
    209     command_line.AppendArg(base::StringPrintf("%d", duration_sec));
    210     command_line.AppendArg("-f");
    211     command_line.AppendArg("dat");
    212     command_line.AppendArg("-c");
    213     command_line.AppendArg(base::StringPrintf("%d", num_channels));
    214     command_line.AppendArgPath(output_file);
    215 #endif
    216 
    217     VLOG(0) << "Running " << command_line.GetCommandLineString();
    218     return base::LaunchProcess(command_line, base::LaunchOptions(),
    219                                &recording_application_);
    220   }
    221 
    222   // Joins the recording program. Returns true on success.
    223   bool WaitForRecordingToEnd() {
    224     int exit_code = -1;
    225     base::WaitForExitCode(recording_application_, &exit_code);
    226     return exit_code == 0;
    227   }
    228  private:
    229   base::ProcessHandle recording_application_;
    230 };
    231 
    232 bool ForceMicrophoneVolumeTo100Percent() {
    233 #if defined(OS_WIN)
    234   CommandLine command_line(GetTestDataDir().Append(kToolsPath).Append(
    235       FILE_PATH_LITERAL("force_mic_volume_max.exe")));
    236   VLOG(0) << "Running " << command_line.GetCommandLineString();
    237   std::string result;
    238   if (!base::GetAppOutput(command_line, &result)) {
    239     LOG(ERROR) << "Failed to set source volume: output was " << result;
    240     return false;
    241   }
    242 #else
    243   // Just force the volume of, say the first 5 devices. A machine will rarely
    244   // have more input sources than that. This is way easier than finding the
    245   // input device we happen to be using.
    246   for (int device_index = 0; device_index < 5; ++device_index) {
    247     std::string result;
    248     const std::string kHundredPercentVolume = "65536";
    249     CommandLine command_line(base::FilePath(FILE_PATH_LITERAL("pacmd")));
    250     command_line.AppendArg("set-source-volume");
    251     command_line.AppendArg(base::StringPrintf("%d", device_index));
    252     command_line.AppendArg(kHundredPercentVolume);
    253     VLOG(0) << "Running " << command_line.GetCommandLineString();
    254     if (!base::GetAppOutput(command_line, &result)) {
    255       LOG(ERROR) << "Failed to set source volume: output was " << result;
    256       return false;
    257     }
    258   }
    259 #endif
    260   return true;
    261 }
    262 
    263 // Removes silence from beginning and end of the |input_audio_file| and writes
    264 // the result to the |output_audio_file|. Returns true on success.
    265 bool RemoveSilence(const base::FilePath& input_file,
    266                    const base::FilePath& output_file) {
    267   // SOX documentation for silence command: http://sox.sourceforge.net/sox.html
    268   // To remove the silence from both beginning and end of the audio file, we
    269   // call sox silence command twice: once on normal file and again on its
    270   // reverse, then we reverse the final output.
    271   // Silence parameters are (in sequence):
    272   // ABOVE_PERIODS: The period for which silence occurs. Value 1 is used for
    273   //                 silence at beginning of audio.
    274   // DURATION: the amount of time in seconds that non-silence must be detected
    275   //           before sox stops trimming audio.
    276   // THRESHOLD: value used to indicate what sample value is treates as silence.
    277   const char* kAbovePeriods = "1";
    278   const char* kDuration = "2";
    279   const char* kTreshold = "5%";
    280 
    281 #if defined(OS_WIN)
    282   CommandLine command_line(GetTestDataDir().Append(kToolsPath).Append(
    283       FILE_PATH_LITERAL("sox.exe")));
    284 #else
    285   CommandLine command_line(base::FilePath(FILE_PATH_LITERAL("sox")));
    286 #endif
    287   command_line.AppendArgPath(input_file);
    288   command_line.AppendArgPath(output_file);
    289   command_line.AppendArg("silence");
    290   command_line.AppendArg(kAbovePeriods);
    291   command_line.AppendArg(kDuration);
    292   command_line.AppendArg(kTreshold);
    293   command_line.AppendArg("reverse");
    294   command_line.AppendArg("silence");
    295   command_line.AppendArg(kAbovePeriods);
    296   command_line.AppendArg(kDuration);
    297   command_line.AppendArg(kTreshold);
    298   command_line.AppendArg("reverse");
    299 
    300   VLOG(0) << "Running " << command_line.GetCommandLineString();
    301   std::string result;
    302   bool ok = base::GetAppOutput(command_line, &result);
    303   VLOG(0) << "Output was:\n\n" << result;
    304   return ok;
    305 }
    306 
    307 bool CanParseAsFloat(const std::string& value) {
    308   return atof(value.c_str()) != 0 || value == "0";
    309 }
    310 
    311 // Runs PESQ to compare |reference_file| to a |actual_file|. The |sample_rate|
    312 // can be either 16000 or 8000.
    313 //
    314 // PESQ is only mono-aware, so the files should preferably be recorded in mono.
    315 // Furthermore it expects the file to be 16 rather than 32 bits, even though
    316 // 32 bits might work. The audio bandwidth of the two files should be the same
    317 // e.g. don't compare a 32 kHz file to a 8 kHz file.
    318 //
    319 // The raw score in MOS is written to |raw_mos|, whereas the MOS-LQO score is
    320 // written to mos_lqo. The scores are returned as floats in string form (e.g.
    321 // "3.145", etc). Returns true on success.
    322 bool RunPesq(const base::FilePath& reference_file,
    323              const base::FilePath& actual_file,
    324              int sample_rate, std::string* raw_mos, std::string* mos_lqo) {
    325   // PESQ will break if the paths are too long (!).
    326   EXPECT_LT(reference_file.value().length(), 128u);
    327   EXPECT_LT(actual_file.value().length(), 128u);
    328 
    329 #if defined(OS_WIN)
    330   base::FilePath pesq_path =
    331       GetTestDataDir().Append(kToolsPath).Append(FILE_PATH_LITERAL("pesq.exe"));
    332 #else
    333   base::FilePath pesq_path =
    334       GetTestDataDir().Append(kToolsPath).Append(FILE_PATH_LITERAL("pesq"));
    335 #endif
    336 
    337   if (!base::PathExists(pesq_path)) {
    338     LOG(ERROR) << "Missing PESQ binary in " << pesq_path.value();
    339     return false;
    340   }
    341 
    342   CommandLine command_line(pesq_path);
    343   command_line.AppendArg(base::StringPrintf("+%d", sample_rate));
    344   command_line.AppendArgPath(reference_file);
    345   command_line.AppendArgPath(actual_file);
    346 
    347   VLOG(0) << "Running " << command_line.GetCommandLineString();
    348   std::string result;
    349   if (!base::GetAppOutput(command_line, &result)) {
    350     LOG(ERROR) << "Failed to run PESQ.";
    351     return false;
    352   }
    353   VLOG(0) << "Output was:\n\n" << result;
    354 
    355   const std::string result_anchor = "Prediction (Raw MOS, MOS-LQO):  = ";
    356   std::size_t anchor_pos = result.find(result_anchor);
    357   if (anchor_pos == std::string::npos) {
    358     LOG(ERROR) << "PESQ was not able to compute a score; we probably recorded "
    359         << "only silence.";
    360     return false;
    361   }
    362 
    363   // There are two tab-separated numbers on the format x.xxx, e.g. 5 chars each.
    364   std::size_t first_number_pos = anchor_pos + result_anchor.length();
    365   *raw_mos = result.substr(first_number_pos, 5);
    366   EXPECT_TRUE(CanParseAsFloat(*raw_mos)) << "Failed to parse raw MOS number.";
    367   *mos_lqo = result.substr(first_number_pos + 5 + 1, 5);
    368   EXPECT_TRUE(CanParseAsFloat(*mos_lqo)) << "Failed to parse MOS LQO number.";
    369 
    370   return true;
    371 }
    372 
    373 #if defined(OS_LINUX) || defined(OS_WIN)
    374 // Only implemented on Linux and Windows for now.
    375 #define MAYBE_MANUAL_TestAudioQuality MANUAL_TestAudioQuality
    376 #else
    377 #define MAYBE_MANUAL_TestAudioQuality DISABLED_MANUAL_TestAudioQuality
    378 #endif
    379 
    380 IN_PROC_BROWSER_TEST_F(WebrtcAudioQualityBrowserTest,
    381                        MAYBE_MANUAL_TestAudioQuality) {
    382 #if defined(OS_WIN)
    383   if (base::win::GetVersion() < base::win::VERSION_VISTA) {
    384     // It would take work to implement this on XP; not prioritized right now.
    385     LOG(ERROR) << "This test is not implemented for Windows XP.";
    386     return;
    387   }
    388 #endif
    389   ASSERT_TRUE(HasAllRequiredResources());
    390   ASSERT_TRUE(embedded_test_server()->InitializeAndWaitUntilReady());
    391   ASSERT_TRUE(peerconnection_server_.Start());
    392 
    393   ASSERT_TRUE(ForceMicrophoneVolumeTo100Percent());
    394 
    395   ui_test_utils::NavigateToURL(
    396       browser(), embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage));
    397   content::WebContents* left_tab =
    398       browser()->tab_strip_model()->GetActiveWebContents();
    399 
    400   chrome::AddTabAt(browser(), GURL(), -1, true);
    401   content::WebContents* right_tab =
    402       browser()->tab_strip_model()->GetActiveWebContents();
    403   ui_test_utils::NavigateToURL(
    404       browser(), embedded_test_server()->GetURL(kMainWebrtcTestHtmlPage));
    405 
    406   ConnectToPeerConnectionServer("peer 1", left_tab);
    407   ConnectToPeerConnectionServer("peer 2", right_tab);
    408 
    409   EXPECT_EQ("ok-peerconnection-created",
    410             ExecuteJavascript("preparePeerConnection()", left_tab));
    411 
    412   AddAudioFile(kReferenceFileRelativeUrl, left_tab);
    413 
    414   EstablishCall(left_tab, right_tab);
    415 
    416   // Note: the media flow isn't necessarily established on the connection just
    417   // because the ready state is ok on both sides. We sleep a bit between call
    418   // establishment and playing to avoid cutting of the beginning of the audio
    419   // file.
    420   SleepInJavascript(left_tab, 2000);
    421 
    422   base::FilePath recording = CreateTemporaryWaveFile();
    423 
    424   // Note: the sound clip is about 10 seconds: record for 15 seconds to get some
    425   // safety margins on each side.
    426   AudioRecorder recorder;
    427   static int kRecordingTimeSeconds = 15;
    428   ASSERT_TRUE(recorder.StartRecording(kRecordingTimeSeconds, recording, true));
    429 
    430   PlayAudioFile(left_tab);
    431 
    432   ASSERT_TRUE(recorder.WaitForRecordingToEnd());
    433   VLOG(0) << "Done recording to " << recording.value() << std::endl;
    434 
    435   HangUp(left_tab);
    436   WaitUntilHangupVerified(left_tab);
    437   WaitUntilHangupVerified(right_tab);
    438 
    439   base::FilePath trimmed_recording = CreateTemporaryWaveFile();
    440 
    441   ASSERT_TRUE(RemoveSilence(recording, trimmed_recording));
    442   VLOG(0) << "Trimmed silence: " << trimmed_recording.value() << std::endl;
    443 
    444   std::string raw_mos;
    445   std::string mos_lqo;
    446   base::FilePath reference_file_in_test_dir =
    447       GetTestDataDir().Append(kReferenceFile);
    448   ASSERT_TRUE(RunPesq(reference_file_in_test_dir, trimmed_recording, 16000,
    449                       &raw_mos, &mos_lqo));
    450 
    451   perf_test::PrintResult("audio_pesq", "", "raw_mos", raw_mos, "score", true);
    452   perf_test::PrintResult("audio_pesq", "", "mos_lqo", mos_lqo, "score", true);
    453 
    454   EXPECT_TRUE(base::DeleteFile(recording, false));
    455   EXPECT_TRUE(base::DeleteFile(trimmed_recording, false));
    456 
    457   ASSERT_TRUE(peerconnection_server_.Stop());
    458 }
    459