1 # Copyright (c) 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 import logging 6 import re 7 import subprocess 8 9 from autotest_lib.client.common_lib import error 10 from autotest_lib.client.common_lib import utils 11 from autotest_lib.client.cros.audio import cmd_utils 12 13 14 ACONNECT_PATH = '/usr/bin/aconnect' 15 ARECORD_PATH = '/usr/bin/arecord' 16 APLAY_PATH = '/usr/bin/aplay' 17 AMIXER_PATH = '/usr/bin/amixer' 18 CARD_NUM_RE = re.compile(r'(\d+) \[.*\]:') 19 CLIENT_NUM_RE = re.compile(r'client (\d+):') 20 DEV_NUM_RE = re.compile(r'.* \[.*\], device (\d+):') 21 CONTROL_NAME_RE = re.compile(r"name='(.*)'") 22 SCONTROL_NAME_RE = re.compile(r"Simple mixer control '(.*)'") 23 24 CARD_PREF_RECORD_DEV_IDX = { 25 'bxtda7219max': 3, 26 } 27 28 def _get_format_args(channels, bits, rate): 29 args = ['-c', str(channels)] 30 args += ['-f', 'S%d_LE' % bits] 31 args += ['-r', str(rate)] 32 return args 33 34 35 def get_num_soundcards(): 36 '''Returns the number of soundcards. 37 38 Number of soundcards is parsed from /proc/asound/cards. 39 Sample content: 40 41 0 [PCH ]: HDA-Intel - HDA Intel PCH 42 HDA Intel PCH at 0xef340000 irq 103 43 1 [NVidia ]: HDA-Intel - HDA NVidia 44 HDA NVidia at 0xef080000 irq 36 45 ''' 46 47 card_id = None 48 with open('/proc/asound/cards', 'r') as f: 49 for line in f: 50 match = CARD_NUM_RE.search(line) 51 if match: 52 card_id = int(match.group(1)) 53 if card_id is None: 54 return 0 55 else: 56 return card_id + 1 57 58 59 def _get_soundcard_controls(card_id): 60 '''Gets the controls for a soundcard. 61 62 @param card_id: Soundcard ID. 63 @raise RuntimeError: If failed to get soundcard controls. 64 65 Controls for a soundcard is retrieved by 'amixer controls' command. 66 amixer output format: 67 68 numid=32,iface=CARD,name='Front Headphone Jack' 69 numid=28,iface=CARD,name='Front Mic Jack' 70 numid=1,iface=CARD,name='HDMI/DP,pcm=3 Jack' 71 numid=8,iface=CARD,name='HDMI/DP,pcm=7 Jack' 72 73 Controls with iface=CARD are parsed from the output and returned in a set. 74 ''' 75 76 cmd = [AMIXER_PATH, '-c', str(card_id), 'controls'] 77 p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) 78 output, _ = p.communicate() 79 if p.wait() != 0: 80 raise RuntimeError('amixer command failed') 81 82 controls = set() 83 for line in output.splitlines(): 84 if not 'iface=CARD' in line: 85 continue 86 match = CONTROL_NAME_RE.search(line) 87 if match: 88 controls.add(match.group(1)) 89 return controls 90 91 92 def _get_soundcard_scontrols(card_id): 93 '''Gets the simple mixer controls for a soundcard. 94 95 @param card_id: Soundcard ID. 96 @raise RuntimeError: If failed to get soundcard simple mixer controls. 97 98 Simple mixer controls for a soundcard is retrieved by 'amixer scontrols' 99 command. amixer output format: 100 101 Simple mixer control 'Master',0 102 Simple mixer control 'Headphone',0 103 Simple mixer control 'Speaker',0 104 Simple mixer control 'PCM',0 105 106 Simple controls are parsed from the output and returned in a set. 107 ''' 108 109 cmd = [AMIXER_PATH, '-c', str(card_id), 'scontrols'] 110 p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) 111 output, _ = p.communicate() 112 if p.wait() != 0: 113 raise RuntimeError('amixer command failed') 114 115 scontrols = set() 116 for line in output.splitlines(): 117 match = SCONTROL_NAME_RE.findall(line) 118 if match: 119 scontrols.add(match[0]) 120 return scontrols 121 122 123 def get_first_soundcard_with_control(cname, scname): 124 '''Returns the soundcard ID with matching control name. 125 126 @param cname: Control name to look for. 127 @param scname: Simple control name to look for. 128 ''' 129 130 cpat = re.compile(r'\b%s\b' % cname, re.IGNORECASE) 131 scpat = re.compile(r'\b%s\b' % scname, re.IGNORECASE) 132 for card_id in xrange(get_num_soundcards()): 133 for pat, func in [(cpat, _get_soundcard_controls), 134 (scpat, _get_soundcard_scontrols)]: 135 if any(pat.search(c) for c in func(card_id)): 136 return card_id 137 return None 138 139 140 def get_soundcard_names(): 141 '''Returns a dictionary of card names, keyed by card number.''' 142 143 cmd = "alsa_helpers -l" 144 try: 145 output = utils.system_output(command=cmd, retain_output=True) 146 except error.CmdError: 147 raise RuntimeError('alsa_helpers -l failed to return card names') 148 149 return dict((index, name) for index, name in ( 150 line.split(',') for line in output.splitlines())) 151 152 153 def get_default_playback_device(): 154 '''Gets the first playback device. 155 156 Returns the first playback device or None if it fails to find one. 157 ''' 158 159 card_id = get_first_soundcard_with_control(cname='Headphone Jack', 160 scname='Headphone') 161 if card_id is None: 162 return None 163 return 'plughw:%d' % card_id 164 165 def get_record_card_name(card_idx): 166 '''Gets the recording sound card name for given card idx. 167 168 Returns the card name inside the square brackets of arecord output lines. 169 ''' 170 card_name_re = re.compile(r'card %d: .*?\[(.*?)\]' % card_idx) 171 cmd = [ARECORD_PATH, '-l'] 172 p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) 173 output, _ = p.communicate() 174 if p.wait() != 0: 175 raise RuntimeError('arecord -l command failed') 176 177 for line in output.splitlines(): 178 match = card_name_re.search(line) 179 if match: 180 return match.group(1) 181 return None 182 183 184 def get_record_device_supported_channels(device): 185 '''Gets the supported channels for the record device. 186 187 @param device: The device to record the audio. E.g. hw:0,1 188 189 Returns the supported values in integer in a list for the device. 190 If the value doesn't exist or the command fails, return None. 191 ''' 192 cmd = "alsa_helpers --device %s --get_capture_channels" % device 193 try: 194 output = utils.system_output(command=cmd, retain_output=True) 195 except error.CmdError: 196 logging.error("Fail to get supported channels for %s", device) 197 return None 198 199 supported_channels = output.splitlines() 200 if not supported_channels: 201 logging.error("Supported channels are empty for %s", device) 202 return None 203 return [int(i) for i in supported_channels] 204 205 206 def get_default_record_device(): 207 '''Gets the first record device. 208 209 Returns the first record device or None if it fails to find one. 210 ''' 211 212 card_id = get_first_soundcard_with_control(cname='Mic Jack', scname='Mic') 213 if card_id is None: 214 return None 215 216 card_name = get_record_card_name(card_id) 217 if CARD_PREF_RECORD_DEV_IDX.has_key(card_name): 218 return 'plughw:%d,%d' % (card_id, CARD_PREF_RECORD_DEV_IDX[card_name]) 219 220 # Get first device id of this card. 221 cmd = [ARECORD_PATH, '-l'] 222 p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) 223 output, _ = p.communicate() 224 if p.wait() != 0: 225 raise RuntimeError('arecord -l command failed') 226 227 dev_id = 0 228 for line in output.splitlines(): 229 if 'card %d:' % card_id in line: 230 match = DEV_NUM_RE.search(line) 231 if match: 232 dev_id = int(match.group(1)) 233 break 234 return 'plughw:%d,%d' % (card_id, dev_id) 235 236 237 def _get_sysdefault(cmd): 238 p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) 239 output, _ = p.communicate() 240 if p.wait() != 0: 241 raise RuntimeError('%s failed' % cmd) 242 243 for line in output.splitlines(): 244 if 'sysdefault' in line: 245 return line 246 return None 247 248 249 def get_sysdefault_playback_device(): 250 '''Gets the sysdefault device from aplay -L output.''' 251 252 return _get_sysdefault([APLAY_PATH, '-L']) 253 254 255 def get_sysdefault_record_device(): 256 '''Gets the sysdefault device from arecord -L output.''' 257 258 return _get_sysdefault([ARECORD_PATH, '-L']) 259 260 261 def playback(*args, **kwargs): 262 '''A helper funciton to execute playback_cmd. 263 264 @param kwargs: kwargs passed to playback_cmd. 265 ''' 266 cmd_utils.execute(playback_cmd(*args, **kwargs)) 267 268 269 def playback_cmd( 270 input, duration=None, channels=2, bits=16, rate=48000, device=None): 271 '''Plays the given input audio by the ALSA utility: 'aplay'. 272 273 @param input: The input audio to be played. 274 @param duration: The length of the playback (in seconds). 275 @param channels: The number of channels of the input audio. 276 @param bits: The number of bits of each audio sample. 277 @param rate: The sampling rate. 278 @param device: The device to play the audio on. E.g. hw:0,1 279 @raise RuntimeError: If no playback device is available. 280 ''' 281 args = [APLAY_PATH] 282 if duration is not None: 283 args += ['-d', str(duration)] 284 args += _get_format_args(channels, bits, rate) 285 if device is None: 286 device = get_default_playback_device() 287 if device is None: 288 raise RuntimeError('no playback device') 289 else: 290 device = "plug%s" % device 291 args += ['-D', device] 292 args += [input] 293 return args 294 295 296 def record(*args, **kwargs): 297 '''A helper function to execute record_cmd. 298 299 @param kwargs: kwargs passed to record_cmd. 300 ''' 301 cmd_utils.execute(record_cmd(*args, **kwargs)) 302 303 304 def record_cmd( 305 output, duration=None, channels=1, bits=16, rate=48000, device=None): 306 '''Records the audio to the specified output by ALSA utility: 'arecord'. 307 308 @param output: The filename where the recorded audio will be stored to. 309 @param duration: The length of the recording (in seconds). 310 @param channels: The number of channels of the recorded audio. 311 @param bits: The number of bits of each audio sample. 312 @param rate: The sampling rate. 313 @param device: The device used to recorded the audio from. E.g. hw:0,1 314 @raise RuntimeError: If no record device is available. 315 ''' 316 args = [ARECORD_PATH] 317 if duration is not None: 318 args += ['-d', str(duration)] 319 args += _get_format_args(channels, bits, rate) 320 if device is None: 321 device = get_default_record_device() 322 if device is None: 323 raise RuntimeError('no record device') 324 else: 325 device = "plug%s" % device 326 args += ['-D', device] 327 args += [output] 328 return args 329 330 331 def mixer_cmd(card_id, cmd): 332 '''Executes amixer command. 333 334 @param card_id: Soundcard ID. 335 @param cmd: Amixer command to execute. 336 @raise RuntimeError: If failed to execute command. 337 338 Amixer command like ['set', 'PCM', '2dB+'] with card_id 1 will be executed 339 as: 340 amixer -c 1 set PCM 2dB+ 341 342 Command output will be returned if any. 343 ''' 344 345 cmd = [AMIXER_PATH, '-c', str(card_id)] + cmd 346 p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) 347 output, _ = p.communicate() 348 if p.wait() != 0: 349 raise RuntimeError('amixer command failed') 350 return output 351 352 353 def get_num_seq_clients(): 354 '''Returns the number of seq clients. 355 356 The number of clients is parsed from aconnect -io. 357 This is run as the chronos user to catch permissions problems. 358 Sample content: 359 360 client 0: 'System' [type=kernel] 361 0 'Timer ' 362 1 'Announce ' 363 client 14: 'Midi Through' [type=kernel] 364 0 'Midi Through Port-0' 365 366 @raise RuntimeError: If no seq device is available. 367 ''' 368 cmd = [ACONNECT_PATH, '-io'] 369 output = cmd_utils.execute(cmd, stdout=subprocess.PIPE, run_as='chronos') 370 num_clients = 0 371 for line in output.splitlines(): 372 match = CLIENT_NUM_RE.match(line) 373 if match: 374 num_clients += 1 375 return num_clients 376 377 def convert_device_name(cras_device_name): 378 '''Converts cras device name to alsa device name. 379 380 @returns: alsa device name that can be passed to aplay -D or arecord -D. 381 For example, if cras_device_name is "kbl_r5514_5663_max: :0,1", 382 this function will return "hw:0,1". 383 ''' 384 tokens = cras_device_name.split(":") 385 return "hw:%s" % tokens[2] 386