Home | History | Annotate | Download | only in media
      1 #!/usr/bin/env python
      2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """Audio tools for recording and analyzing audio.
      7 
      8 The audio tools provided here are mainly to:
      9 - record playing audio.
     10 - remove silence from beginning and end of audio file.
     11 - compare audio files using PESQ tool.
     12 
     13 The tools are supported on Windows and Linux.
     14 """
     15 
     16 import commands
     17 import ctypes
     18 import logging
     19 import os
     20 import re
     21 import subprocess
     22 import sys
     23 import threading
     24 import time
     25 
     26 import pyauto_media
     27 import pyauto
     28 
     29 
     30 _TOOLS_PATH = os.path.abspath(os.path.join(pyauto.PyUITest.DataDir(),
     31                                            'pyauto_private', 'media', 'tools'))
     32 
     33 WINDOWS = 'win32' in sys.platform
     34 if WINDOWS:
     35   _PESQ_PATH = os.path.join(_TOOLS_PATH, 'pesq.exe')
     36   _SOX_PATH = os.path.join(_TOOLS_PATH, 'sox.exe')
     37   _AUDIO_RECORDER = r'SoundRecorder.exe'
     38   _FORCE_MIC_VOLUME_MAX_UTIL = os.path.join(_TOOLS_PATH,
     39                                             r'force_mic_volume_max.exe')
     40 else:
     41   _PESQ_PATH = os.path.join(_TOOLS_PATH, 'pesq')
     42   _SOX_PATH = commands.getoutput('which sox')
     43   _AUDIO_RECORDER = commands.getoutput('which arecord')
     44   _PACMD_PATH = commands.getoutput('which pacmd')
     45 
     46 
     47 class AudioRecorderThread(threading.Thread):
     48   """A thread that records audio out of the default audio output."""
     49 
     50   def __init__(self, duration, output_file, record_mono=False):
     51     threading.Thread.__init__(self)
     52     self.error = ''
     53     self._duration = duration
     54     self._output_file = output_file
     55     self._record_mono = record_mono
     56 
     57   def run(self):
     58     """Starts audio recording."""
     59     if WINDOWS:
     60       if self._record_mono:
     61         logging.error("Mono recording not supported on Windows yet!")
     62 
     63       duration = time.strftime('%H:%M:%S', time.gmtime(self._duration))
     64       cmd = [_AUDIO_RECORDER, '/FILE', self._output_file, '/DURATION',
     65              duration]
     66       # This is needed to run SoundRecorder.exe on Win-64 using Python-32 bit.
     67       ctypes.windll.kernel32.Wow64DisableWow64FsRedirection(
     68           ctypes.byref(ctypes.c_long()))
     69     else:
     70       num_channels = 1 if self._record_mono else 2
     71       cmd = [_AUDIO_RECORDER, '-d', self._duration, '-f', 'dat', '-c',
     72              str(num_channels), self._output_file]
     73 
     74     cmd = [str(s) for s in cmd]
     75     logging.debug('Running command: %s', ' '.join(cmd))
     76     returncode = subprocess.call(cmd, stdout=subprocess.PIPE,
     77                                  stderr=subprocess.PIPE)
     78     if returncode != 0:
     79       self.error = 'Failed to record audio.'
     80     else:
     81       logging.debug('Finished recording audio into %s.', self._output_file)
     82 
     83 
     84 def RunPESQ(audio_file_ref, audio_file_test, sample_rate=16000):
     85   """Runs PESQ to compare audio test file to a reference audio file.
     86 
     87   Args:
     88     audio_file_ref: The reference audio file used by PESQ.
     89     audio_file_test: The audio test file to compare.
     90     sample_rate: Sample rate used by PESQ algorithm, possible values are only
     91         8000 or 16000.
     92 
     93   Returns:
     94     A tuple of float values representing PESQ scores of the audio_file_ref and
     95     audio_file_test consecutively.
     96   """
     97   # Work around a bug in PESQ when the ref file path is > 128 chars. PESQ will
     98   # compute an incorrect score then (!), and the relative path to the ref file
     99   # should be a lot shorter than the absolute one.
    100   audio_file_ref = os.path.relpath(audio_file_ref)
    101   cmd = [_PESQ_PATH, '+%d' % sample_rate, audio_file_ref, audio_file_test]
    102   logging.debug('Running command: %s', ' '.join(cmd))
    103   p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    104   output, error = p.communicate()
    105   if p.returncode != 0:
    106     logging.error('Error running pesq: %s\n%s', output, error)
    107   # Last line of PESQ output shows the results.  Example:
    108   # P.862 Prediction (Raw MOS, MOS-LQO):  = 4.180    4.319
    109   result = re.search('Prediction.*= (\d{1}\.\d{3})\t(\d{1}\.\d{3})',
    110                      output)
    111   if not result or len(result.groups()) != 2:
    112     return None
    113   return (float(result.group(1)), float(result.group(2)))
    114 
    115 
    116 def RemoveSilence(input_audio_file, output_audio_file):
    117   """Removes silence from beginning and end of the input_audio_file.
    118 
    119   Args:
    120     input_audio_file: The audio file to remove silence from.
    121     output_audio_file: The audio file to save the output audio.
    122   """
    123   # SOX documentation for silence command: http://sox.sourceforge.net/sox.html
    124   # To remove the silence from both beginning and end of the audio file, we call
    125   # sox silence command twice: once on normal file and again on its reverse,
    126   # then we reverse the final output.
    127   # Silence parameters are (in sequence):
    128   # ABOVE_PERIODS: The period for which silence occurs. Value 1 is used for
    129   #                 silence at beginning of audio.
    130   # DURATION: the amount of time in seconds that non-silence must be detected
    131   #           before sox stops trimming audio.
    132   # THRESHOLD: value used to indicate what sample value is treates as silence.
    133   ABOVE_PERIODS = '1'
    134   DURATION = '2'
    135   THRESHOLD = '5%'
    136 
    137   cmd = [_SOX_PATH, input_audio_file, output_audio_file, 'silence',
    138          ABOVE_PERIODS, DURATION, THRESHOLD, 'reverse', 'silence',
    139          ABOVE_PERIODS, DURATION, THRESHOLD, 'reverse']
    140   logging.debug('Running command: %s', ' '.join(cmd))
    141   p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    142   output, error = p.communicate()
    143   if p.returncode != 0:
    144     logging.error('Error removing silence from audio: %s\n%s', output, error)
    145 
    146 
    147 def ForceMicrophoneVolumeTo100Percent():
    148   if WINDOWS:
    149     # The volume max util is implemented in WebRTC in
    150     # webrtc/tools/force_mic_volume_max/force_mic_volume_max.cc.
    151     if not os.path.exists(_FORCE_MIC_VOLUME_MAX_UTIL):
    152       raise Exception('Missing required binary %s.' %
    153                       _FORCE_MIC_VOLUME_MAX_UTIL)
    154     cmd = [_FORCE_MIC_VOLUME_MAX_UTIL]
    155   else:
    156     # The recording device id is machine-specific. We assume here it is called
    157     # Monitor of render (which corresponds to the id render.monitor). You can
    158     # list the available recording devices with pacmd list-sources.
    159     RECORDING_DEVICE_ID = 'render.monitor'
    160     HUNDRED_PERCENT_VOLUME = '65536'
    161     cmd = [_PACMD_PATH, 'set-source-volume', RECORDING_DEVICE_ID,
    162            HUNDRED_PERCENT_VOLUME]
    163 
    164   logging.debug('Running command: %s', ' '.join(cmd))
    165   p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    166   output, error = p.communicate()
    167   if p.returncode != 0:
    168     logging.error('Error forcing mic volume to 100%%: %s\n%s', output, error)
    169