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