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