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 re
6import shlex
7
8from autotest_lib.client.cros.audio import cmd_utils
9
10
11ARECORD_PATH = '/usr/bin/arecord'
12APLAY_PATH = '/usr/bin/aplay'
13AMIXER_PATH = '/usr/bin/amixer'
14CARD_NUM_RE = re.compile('(\d+) \[.*\]:.*')
15DEV_NUM_RE = re.compile('.* \[.*\], device (\d+):.*')
16CONTROL_NAME_RE = re.compile("name='(.*)'")
17SCONTROL_NAME_RE = re.compile("Simple mixer control '(.*)'")
18
19
20def _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
27def 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
51def _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
84def _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
115def 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
132def 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
145def 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
172def _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
184def get_sysdefault_playback_device():
185    '''Gets the sysdefault device from aplay -L output.'''
186
187    return _get_sysdefault(APLAY_PATH + ' -L')
188
189
190def get_sysdefault_record_device():
191    '''Gets the sysdefault device from arecord -L output.'''
192
193    return _get_sysdefault(ARECORD_PATH + ' -L')
194
195
196def 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
204def 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
229def 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
237def 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
262def 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