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.
6 """Audio tools for recording and analyzing audio.
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.
13 The tools are supported on Windows and Linux.
30 _TOOLS_PATH = os.path.abspath(os.path.join(pyauto.PyUITest.DataDir(),
31 'pyauto_private', 'media', 'tools'))
33 WINDOWS = 'win32' in sys.platform
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')
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')
47 class AudioRecorderThread(threading.Thread):
48 """A thread that records audio out of the default audio output."""
50 def __init__(self, duration, output_file, record_mono=False):
51 threading.Thread.__init__(self)
53 self._duration = duration
54 self._output_file = output_file
55 self._record_mono = record_mono
58 """Starts audio recording."""
61 logging.error("Mono recording not supported on Windows yet!")
63 duration = time.strftime('%H:%M:%S', time.gmtime(self._duration))
64 cmd = [_AUDIO_RECORDER, '/FILE', self._output_file, '/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()))
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]
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)
79 self.error = 'Failed to record audio.'
81 logging.debug('Finished recording audio into %s.', self._output_file)
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.
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
94 A tuple of float values representing PESQ scores of the audio_file_ref and
95 audio_file_test consecutively.
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})',
111 if not result or len(result.groups()) != 2:
113 return (float(result.group(1)), float(result.group(2)))
116 def RemoveSilence(input_audio_file, output_audio_file):
117 """Removes silence from beginning and end of the input_audio_file.
120 input_audio_file: The audio file to remove silence from.
121 output_audio_file: The audio file to save the output audio.
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.
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)
147 def ForceMicrophoneVolumeTo100Percent():
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]
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]
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)