# Lint as: python2, python3 # Copyright (c) 2013 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. from __future__ import absolute_import from __future__ import division from __future__ import print_function import logging import re import subprocess from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib import utils from autotest_lib.client.cros.audio import cmd_utils from six.moves import range ACONNECT_PATH = '/usr/bin/aconnect' ARECORD_PATH = '/usr/bin/arecord' APLAY_PATH = '/usr/bin/aplay' AMIXER_PATH = '/usr/bin/amixer' CARD_NUM_RE = re.compile(r'(\d+) \[.*\]:') CLIENT_NUM_RE = re.compile(r'client (\d+):') DEV_NUM_RE = re.compile(r'.* \[.*\], device (\d+):') CONTROL_NAME_RE = re.compile(r"name='(.*)'") SCONTROL_NAME_RE = re.compile(r"Simple mixer control '(.*)'") AUDIO_DEVICE_STATUS_CMD = 'cat /proc/asound/card%s/pcm%sp/sub0/status' OUTPUT_DEVICE_CMD = 'cras_test_client --dump_audio_thread | grep "Output dev:"' CARD_PREF_RECORD_DEV_IDX = { 'bxtda7219max': 3, } def _get_format_args(channels, bits, rate): args = ['-c', str(channels)] args += ['-f', 'S%d_LE' % bits] args += ['-r', str(rate)] return args def get_num_soundcards(): '''Returns the number of soundcards. Number of soundcards is parsed from /proc/asound/cards. Sample content: 0 [PCH ]: HDA-Intel - HDA Intel PCH HDA Intel PCH at 0xef340000 irq 103 1 [NVidia ]: HDA-Intel - HDA NVidia HDA NVidia at 0xef080000 irq 36 ''' card_id = None with open('/proc/asound/cards', 'r') as f: for line in f: match = CARD_NUM_RE.search(line) if match: card_id = int(match.group(1)) if card_id is None: return 0 else: return card_id + 1 def _get_soundcard_controls(card_id): '''Gets the controls for a soundcard. @param card_id: Soundcard ID. @raise RuntimeError: If failed to get soundcard controls. Controls for a soundcard is retrieved by 'amixer controls' command. amixer output format: numid=32,iface=CARD,name='Front Headphone Jack' numid=28,iface=CARD,name='Front Mic Jack' numid=1,iface=CARD,name='HDMI/DP,pcm=3 Jack' numid=8,iface=CARD,name='HDMI/DP,pcm=7 Jack' Controls with iface=CARD are parsed from the output and returned in a set. ''' cmd = [AMIXER_PATH, '-c', str(card_id), 'controls'] p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) output, _ = p.communicate() if p.wait() != 0: raise RuntimeError('amixer command failed') controls = set() for line in output.splitlines(): if not 'iface=CARD' in line: continue match = CONTROL_NAME_RE.search(line) if match: controls.add(match.group(1)) return controls def _get_soundcard_scontrols(card_id): '''Gets the simple mixer controls for a soundcard. @param card_id: Soundcard ID. @raise RuntimeError: If failed to get soundcard simple mixer controls. # TODO b:169251326 terms below are set outside of this codebase # and should be updated when possible. ("Master" -> "Main") Simple mixer controls for a soundcard is retrieved by 'amixer scontrols' command. amixer output format: Simple mixer control 'Master',0 Simple mixer control 'Headphone',0 Simple mixer control 'Speaker',0 Simple mixer control 'PCM',0 Simple controls are parsed from the output and returned in a set. ''' cmd = [AMIXER_PATH, '-c', str(card_id), 'scontrols'] p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) output, _ = p.communicate() if p.wait() != 0: raise RuntimeError('amixer command failed') scontrols = set() for line in output.splitlines(): match = SCONTROL_NAME_RE.findall(line) if match: scontrols.add(match[0]) return scontrols def get_first_soundcard_with_control(cname, scname): '''Returns the soundcard ID with matching control name. @param cname: Control name to look for. @param scname: Simple control name to look for. ''' cpat = re.compile(r'\b%s\b' % cname, re.IGNORECASE) scpat = re.compile(r'\b%s\b' % scname, re.IGNORECASE) for card_id in range(get_num_soundcards()): for pat, func in [(cpat, _get_soundcard_controls), (scpat, _get_soundcard_scontrols)]: if any(pat.search(c) for c in func(card_id)): return card_id return None def get_soundcard_names(): '''Returns a dictionary of card names, keyed by card number.''' cmd = "alsa_helpers -l" try: output = utils.system_output(command=cmd, retain_output=True) except error.CmdError: raise RuntimeError('alsa_helpers -l failed to return card names') return dict((index, name) for index, name in ( line.split(',') for line in output.splitlines())) def get_default_playback_device(): '''Gets the first playback device. Returns the first playback device or None if it fails to find one. ''' card_id = get_first_soundcard_with_control(cname='Headphone Jack', scname='Headphone') if card_id is None: return None return 'plughw:%d' % card_id def get_record_card_name(card_idx): '''Gets the recording sound card name for given card idx. Returns the card name inside the square brackets of arecord output lines. ''' card_name_re = re.compile(r'card %d: .*?\[(.*?)\]' % card_idx) cmd = [ARECORD_PATH, '-l'] p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) output, _ = p.communicate() if p.wait() != 0: raise RuntimeError('arecord -l command failed') for line in output.splitlines(): match = card_name_re.search(line) if match: return match.group(1) return None def get_record_device_supported_channels(device): '''Gets the supported channels for the record device. @param device: The device to record the audio. E.g. hw:0,1 Returns the supported values in integer in a list for the device. If the value doesn't exist or the command fails, return None. ''' cmd = "alsa_helpers --device %s --get_capture_channels" % device try: output = utils.system_output(command=cmd, retain_output=True) except error.CmdError: logging.error("Fail to get supported channels for %s", device) return None supported_channels = output.splitlines() if not supported_channels: logging.error("Supported channels are empty for %s", device) return None return [int(i) for i in supported_channels] def get_default_record_device(): '''Gets the first record device. Returns the first record device or None if it fails to find one. ''' card_id = get_first_soundcard_with_control(cname='Mic Jack', scname='Mic') if card_id is None: return None card_name = get_record_card_name(card_id) if card_name in CARD_PREF_RECORD_DEV_IDX: return 'plughw:%d,%d' % (card_id, CARD_PREF_RECORD_DEV_IDX[card_name]) # Get first device id of this card. cmd = [ARECORD_PATH, '-l'] p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) output, _ = p.communicate() if p.wait() != 0: raise RuntimeError('arecord -l command failed') dev_id = 0 for line in output.splitlines(): if 'card %d:' % card_id in line: match = DEV_NUM_RE.search(line) if match: dev_id = int(match.group(1)) break return 'plughw:%d,%d' % (card_id, dev_id) def _get_sysdefault(cmd): p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) output, _ = p.communicate() if p.wait() != 0: raise RuntimeError('%s failed' % cmd) for line in output.splitlines(): if 'sysdefault' in line: return line return None def get_sysdefault_playback_device(): '''Gets the sysdefault device from aplay -L output.''' return _get_sysdefault([APLAY_PATH, '-L']) def get_sysdefault_record_device(): '''Gets the sysdefault device from arecord -L output.''' return _get_sysdefault([ARECORD_PATH, '-L']) def playback(*args, **kwargs): '''A helper funciton to execute playback_cmd. @param kwargs: kwargs passed to playback_cmd. ''' cmd_utils.execute(playback_cmd(*args, **kwargs)) def playback_cmd( input, duration=None, channels=2, bits=16, rate=48000, device=None): '''Plays the given input audio by the ALSA utility: 'aplay'. @param input: The input audio to be played. @param duration: The length of the playback (in seconds). @param channels: The number of channels of the input audio. @param bits: The number of bits of each audio sample. @param rate: The sampling rate. @param device: The device to play the audio on. E.g. hw:0,1 @raise RuntimeError: If no playback device is available. ''' args = [APLAY_PATH] if duration is not None: args += ['-d', str(duration)] args += _get_format_args(channels, bits, rate) if device is None: device = get_default_playback_device() if device is None: raise RuntimeError('no playback device') else: device = "plug%s" % device args += ['-D', device] args += [input] return args def record(*args, **kwargs): '''A helper function to execute record_cmd. @param kwargs: kwargs passed to record_cmd. ''' cmd_utils.execute(record_cmd(*args, **kwargs)) def record_cmd( output, duration=None, channels=1, bits=16, rate=48000, device=None): '''Records the audio to the specified output by ALSA utility: 'arecord'. @param output: The filename where the recorded audio will be stored to. @param duration: The length of the recording (in seconds). @param channels: The number of channels of the recorded audio. @param bits: The number of bits of each audio sample. @param rate: The sampling rate. @param device: The device used to recorded the audio from. E.g. hw:0,1 @raise RuntimeError: If no record device is available. ''' args = [ARECORD_PATH] if duration is not None: args += ['-d', str(duration)] args += _get_format_args(channels, bits, rate) if device is None: device = get_default_record_device() if device is None: raise RuntimeError('no record device') else: device = "plug%s" % device args += ['-D', device] args += [output] return args def mixer_cmd(card_id, cmd): '''Executes amixer command. @param card_id: Soundcard ID. @param cmd: Amixer command to execute. @raise RuntimeError: If failed to execute command. Amixer command like ['set', 'PCM', '2dB+'] with card_id 1 will be executed as: amixer -c 1 set PCM 2dB+ Command output will be returned if any. ''' cmd = [AMIXER_PATH, '-c', str(card_id)] + cmd p = cmd_utils.popen(cmd, stdout=subprocess.PIPE) output, _ = p.communicate() if p.wait() != 0: raise RuntimeError('amixer command failed') return output def get_num_seq_clients(): '''Returns the number of seq clients. The number of clients is parsed from aconnect -io. This is run as the chronos user to catch permissions problems. Sample content: client 0: 'System' [type=kernel] 0 'Timer ' 1 'Announce ' client 14: 'Midi Through' [type=kernel] 0 'Midi Through Port-0' @raise RuntimeError: If no seq device is available. ''' cmd = [ACONNECT_PATH, '-io'] output = cmd_utils.execute(cmd, stdout=subprocess.PIPE, run_as='chronos') num_clients = 0 for line in output.splitlines(): match = CLIENT_NUM_RE.match(line) if match: num_clients += 1 return num_clients def convert_device_name(cras_device_name): '''Converts cras device name to alsa device name. @returns: alsa device name that can be passed to aplay -D or arecord -D. For example, if cras_device_name is "kbl_r5514_5663_max: :0,1", this function will return "hw:0,1". ''' tokens = cras_device_name.split(":") return "hw:%s" % tokens[2] def check_audio_stream_at_selected_device(device_name, device_type): """Checks the audio output at expected node @param device_name: Audio output device name, Ex: kbl_r5514_5663_max: :0,1 @param device_type: Audio output device type, Ex: INTERNAL_SPEAKER """ if device_type == 'BLUETOOTH': output_device_output = utils.system_output(OUTPUT_DEVICE_CMD).strip() bt_device = output_device_output.split('Output dev:')[1].strip() if bt_device != device_name: raise error.TestFail("Audio is not routing through expected node") logging.info('Audio is routing through %s', bt_device) else: card_device_search = re.search(r':(\d),(\d)', device_name) if card_device_search: card_num = card_device_search.group(1) device_num = card_device_search.group(2) logging.debug("Sound card number is %s", card_num) logging.debug("Device number is %s", device_num) if card_num is None or device_num is None: raise error.TestError("Audio device name is not in expected format") device_status_output = utils.system_output(AUDIO_DEVICE_STATUS_CMD % (card_num, device_num)) logging.debug("Selected output device status is %s", device_status_output) if 'RUNNING' in device_status_output: logging.info("Audio is routing through expected node!") elif 'closed' in device_status_output: raise error.TestFail("Audio is not routing through expected audio " "node!") else: raise error.TestError("Audio routing error! Device may be " "preparing")