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 re
      6 import shlex
      7 
      8 from autotest_lib.client.cros.audio import cmd_utils
      9 
     10 
     11 ARECORD_PATH = '/usr/bin/arecord'
     12 APLAY_PATH = '/usr/bin/aplay'
     13 AMIXER_PATH = '/usr/bin/amixer'
     14 CARD_NUM_RE = re.compile('(\d+) \[.*\]:.*')
     15 DEV_NUM_RE = re.compile('.* \[.*\], device (\d+):.*')
     16 CONTROL_NAME_RE = re.compile("name='(.*)'")
     17 SCONTROL_NAME_RE = re.compile("Simple mixer control '(.*)'")
     18 
     19 
     20 def _get_format_args(channels, bits, rate):
     21     args = ['-c', str(channels)]
     22     args += ['-f', 'S%d_LE' % bits]
     23     args += ['-r', str(rate)]
     24     return args
     25 
     26 
     27 def get_num_soundcards():
     28     '''Returns the number of soundcards.
     29 
     30     Number of soundcards is parsed from /proc/asound/cards.
     31     Sample content:
     32 
     33       0 [PCH            ]: HDA-Intel - HDA Intel PCH
     34                            HDA Intel PCH at 0xef340000 irq 103
     35       1 [NVidia         ]: HDA-Intel - HDA NVidia
     36                            HDA NVidia at 0xef080000 irq 36
     37     '''
     38 
     39     card_id = None
     40     with open('/proc/asound/cards', 'r') as f:
     41         for line in f:
     42             match = CARD_NUM_RE.search(line)
     43             if match:
     44                 card_id = int(match.group(1))
     45     if card_id is None:
     46         return 0
     47     else:
     48         return card_id + 1
     49 
     50 
     51 def _get_soundcard_controls(card_id):
     52     '''Gets the controls for a soundcard.
     53 
     54     @param card_id: Soundcard ID.
     55     @raise RuntimeError: If failed to get soundcard controls.
     56 
     57     Controls for a soundcard is retrieved by 'amixer controls' command.
     58     amixer output format:
     59 
     60       numid=32,iface=CARD,name='Front Headphone Jack'
     61       numid=28,iface=CARD,name='Front Mic Jack'
     62       numid=1,iface=CARD,name='HDMI/DP,pcm=3 Jack'
     63       numid=8,iface=CARD,name='HDMI/DP,pcm=7 Jack'
     64 
     65     Controls with iface=CARD are parsed from the output and returned in a set.
     66     '''
     67 
     68     cmd = AMIXER_PATH + ' -c %d controls' % card_id
     69     p = cmd_utils.popen(shlex.split(cmd), stdout=cmd_utils.PIPE)
     70     output, _ = p.communicate()
     71     if p.wait() != 0:
     72         raise RuntimeError('amixer command failed')
     73 
     74     controls = set()
     75     for line in output.splitlines():
     76         if not 'iface=CARD' in line:
     77             continue
     78         match = CONTROL_NAME_RE.search(line)
     79         if match:
     80             controls.add(match.group(1))
     81     return controls
     82 
     83 
     84 def _get_soundcard_scontrols(card_id):
     85     '''Gets the simple mixer controls for a soundcard.
     86 
     87     @param card_id: Soundcard ID.
     88     @raise RuntimeError: If failed to get soundcard simple mixer controls.
     89 
     90     Simple mixer controls for a soundcard is retrieved by 'amixer scontrols'
     91     command.  amixer output format:
     92 
     93       Simple mixer control 'Master',0
     94       Simple mixer control 'Headphone',0
     95       Simple mixer control 'Speaker',0
     96       Simple mixer control 'PCM',0
     97 
     98     Simple controls are parsed from the output and returned in a set.
     99     '''
    100 
    101     cmd = AMIXER_PATH + ' -c %d scontrols' % card_id
    102     p = cmd_utils.popen(shlex.split(cmd), stdout=cmd_utils.PIPE)
    103     output, _ = p.communicate()
    104     if p.wait() != 0:
    105         raise RuntimeError('amixer command failed')
    106 
    107     scontrols = set()
    108     for line in output.splitlines():
    109         match = SCONTROL_NAME_RE.findall(line)
    110         if match:
    111             scontrols.add(match[0])
    112     return scontrols
    113 
    114 
    115 def get_first_soundcard_with_control(cname, scname):
    116     '''Returns the soundcard ID with matching control name.
    117 
    118     @param cname: Control name to look for.
    119     @param scname: Simple control name to look for.
    120     '''
    121 
    122     cpat = re.compile(r'\b%s\b' % cname, re.IGNORECASE)
    123     scpat = re.compile(r'\b%s\b' % scname, re.IGNORECASE)
    124     for card_id in xrange(get_num_soundcards()):
    125         for pat, func in [(cpat, _get_soundcard_controls),
    126                           (scpat, _get_soundcard_scontrols)]:
    127             if any(pat.search(c) for c in func(card_id)):
    128                 return card_id
    129     return None
    130 
    131 
    132 def get_default_playback_device():
    133     '''Gets the first playback device.
    134 
    135     Returns the first playback device or None if it fails to find one.
    136     '''
    137 
    138     card_id = get_first_soundcard_with_control(cname='Headphone Jack',
    139                                                scname='Headphone')
    140     if card_id is None:
    141         return None
    142     return 'plughw:%d' % card_id
    143 
    144 
    145 def get_default_record_device():
    146     '''Gets the first record device.
    147 
    148     Returns the first record device or None if it fails to find one.
    149     '''
    150 
    151     card_id = get_first_soundcard_with_control(cname='Mic Jack', scname='Mic')
    152     if card_id is None:
    153         return None
    154 
    155     # Get first device id of this card.
    156     cmd = ARECORD_PATH + ' -l'
    157     p = cmd_utils.popen(shlex.split(cmd), stdout=cmd_utils.PIPE)
    158     output, _ = p.communicate()
    159     if p.wait() != 0:
    160         raise RuntimeError('arecord -l command failed')
    161 
    162     dev_id = 0
    163     for line in output.splitlines():
    164         if 'card %d:' % card_id in line:
    165             match = DEV_NUM_RE.search(line)
    166             if match:
    167                 dev_id = int(match.group(1))
    168                 break
    169     return 'plughw:%d,%d' % (card_id, dev_id)
    170 
    171 
    172 def _get_sysdefault(cmd):
    173     p = cmd_utils.popen(shlex.split(cmd), stdout=cmd_utils.PIPE)
    174     output, _ = p.communicate()
    175     if p.wait() != 0:
    176         raise RuntimeError('%s failed' % cmd)
    177 
    178     for line in output.splitlines():
    179         if 'sysdefault' in line:
    180             return line
    181     return None
    182 
    183 
    184 def get_sysdefault_playback_device():
    185     '''Gets the sysdefault device from aplay -L output.'''
    186 
    187     return _get_sysdefault(APLAY_PATH + ' -L')
    188 
    189 
    190 def get_sysdefault_record_device():
    191     '''Gets the sysdefault device from arecord -L output.'''
    192 
    193     return _get_sysdefault(ARECORD_PATH + ' -L')
    194 
    195 
    196 def playback(*args, **kwargs):
    197     '''A helper funciton to execute playback_cmd.
    198 
    199     @param kwargs: kwargs passed to playback_cmd.
    200     '''
    201     cmd_utils.execute(playback_cmd(*args, **kwargs))
    202 
    203 
    204 def playback_cmd(
    205         input, duration=None, channels=2, bits=16, rate=48000, device=None):
    206     '''Plays the given input audio by the ALSA utility: 'aplay'.
    207 
    208     @param input: The input audio to be played.
    209     @param duration: The length of the playback (in seconds).
    210     @param channels: The number of channels of the input audio.
    211     @param bits: The number of bits of each audio sample.
    212     @param rate: The sampling rate.
    213     @param device: The device to play the audio on.
    214     @raise RuntimeError: If no playback device is available.
    215     '''
    216     args = [APLAY_PATH]
    217     if duration is not None:
    218         args += ['-d', str(duration)]
    219     args += _get_format_args(channels, bits, rate)
    220     if device is None:
    221         device = get_default_playback_device()
    222         if device is None:
    223             raise RuntimeError('no playback device')
    224     args += ['-D', device]
    225     args += [input]
    226     return args
    227 
    228 
    229 def record(*args, **kwargs):
    230     '''A helper function to execute record_cmd.
    231 
    232     @param kwargs: kwargs passed to record_cmd.
    233     '''
    234     cmd_utils.execute(record_cmd(*args, **kwargs))
    235 
    236 
    237 def record_cmd(
    238         output, duration=None, channels=1, bits=16, rate=48000, device=None):
    239     '''Records the audio to the specified output by ALSA utility: 'arecord'.
    240 
    241     @param output: The filename where the recorded audio will be stored to.
    242     @param duration: The length of the recording (in seconds).
    243     @param channels: The number of channels of the recorded audio.
    244     @param bits: The number of bits of each audio sample.
    245     @param rate: The sampling rate.
    246     @param device: The device used to recorded the audio from.
    247     @raise RuntimeError: If no record device is available.
    248     '''
    249     args = [ARECORD_PATH]
    250     if duration is not None:
    251         args += ['-d', str(duration)]
    252     args += _get_format_args(channels, bits, rate)
    253     if device is None:
    254         device = get_default_record_device()
    255         if device is None:
    256             raise RuntimeError('no record device')
    257     args += ['-D', device]
    258     args += [output]
    259     return args
    260 
    261 
    262 def mixer_cmd(card_id, cmd):
    263     '''Executes amixer command.
    264 
    265     @param card_id: Soundcard ID.
    266     @param cmd: Amixer command to execute.
    267     @raise RuntimeError: If failed to execute command.
    268 
    269     Amixer command like "set PCM 2dB+" with card_id 1 will be executed as:
    270         amixer -c 1 set PCM 2dB+
    271 
    272     Command output will be returned if any.
    273     '''
    274 
    275     cmd = AMIXER_PATH + ' -c %d ' % card_id + cmd
    276     p = cmd_utils.popen(shlex.split(cmd), stdout=cmd_utils.PIPE)
    277     output, _ = p.communicate()
    278     if p.wait() != 0:
    279         raise RuntimeError('amixer command failed')
    280     return output
    281