| OLD | NEW |
| (Empty) |
| 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) | |
| OLD | NEW |