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