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 '(.*)'")
23
24CARD_PREF_RECORD_DEV_IDX = {
25    'bxtda7219max': 3,
26}
27
28def _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
35def 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
59def _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
92def _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
123def 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
140def 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
153def 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
165def 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
184def 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
206def 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
237def _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
249def get_sysdefault_playback_device():
250    '''Gets the sysdefault device from aplay -L output.'''
251
252    return _get_sysdefault([APLAY_PATH, '-L'])
253
254
255def get_sysdefault_record_device():
256    '''Gets the sysdefault device from arecord -L output.'''
257
258    return _get_sysdefault([ARECORD_PATH, '-L'])
259
260
261def 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
269def 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
296def 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
304def 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
331def 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
353def 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
377def 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