Home | History | Annotate | Download | only in audio
      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