Home | History | Annotate | Download | only in multimedia
      1 # Copyright 2014 The Chromium OS 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 """Facade to access the audio-related functionality."""
      6 
      7 import functools
      8 import glob
      9 import logging
     10 import numpy as np
     11 import os
     12 import tempfile
     13 
     14 from autotest_lib.client.cros import constants
     15 from autotest_lib.client.cros.audio import audio_helper
     16 from autotest_lib.client.cros.audio import cmd_utils
     17 from autotest_lib.client.cros.audio import cras_dbus_utils
     18 from autotest_lib.client.cros.audio import cras_utils
     19 from autotest_lib.client.cros.audio import alsa_utils
     20 from autotest_lib.client.cros.multimedia import audio_extension_handler
     21 
     22 
     23 class AudioFacadeNativeError(Exception):
     24     """Error in AudioFacadeNative."""
     25     pass
     26 
     27 
     28 def check_arc_resource(func):
     29     """Decorator function for ARC related functions in AudioFacadeNative."""
     30     @functools.wraps(func)
     31     def wrapper(instance, *args, **kwargs):
     32         """Wrapper for the methods to check _arc_resource.
     33 
     34         @param instance: Object instance.
     35 
     36         @raises: AudioFacadeNativeError if there is no ARC resource.
     37 
     38         """
     39         if not instance._arc_resource:
     40             raise AudioFacadeNativeError('There is no ARC resource.')
     41         return func(instance, *args, **kwargs)
     42     return wrapper
     43 
     44 
     45 def file_contains_all_zeros(path):
     46     """Reads a file and checks whether the file contains all zeros."""
     47     with open(path) as f:
     48         binary = f.read()
     49         # Assume data is in 16 bit signed int format. The real format
     50         # does not matter though since we only care if there is nonzero data.
     51         np_array = np.fromstring(binary, dtype='<i2')
     52         return not np.any(np_array)
     53 
     54 
     55 class AudioFacadeNative(object):
     56     """Facede to access the audio-related functionality.
     57 
     58     The methods inside this class only accept Python native types.
     59 
     60     """
     61     _CAPTURE_DATA_FORMATS = [
     62             dict(file_type='raw', sample_format='S16_LE',
     63                  channel=1, rate=48000),
     64             dict(file_type='raw', sample_format='S16_LE',
     65                  channel=2, rate=48000)]
     66 
     67     _PLAYBACK_DATA_FORMAT = dict(
     68             file_type='raw', sample_format='S16_LE', channel=2, rate=48000)
     69 
     70     _LISTEN_DATA_FORMATS = [
     71             dict(file_type='raw', sample_format='S16_LE',
     72                  channel=1, rate=16000)]
     73 
     74     def __init__(self, resource, arc_resource=None):
     75         """Initializes an audio facade.
     76 
     77         @param resource: A FacadeResource object.
     78         @param arc_resource: An ArcResource object.
     79 
     80         """
     81         self._resource = resource
     82         self._listener = None
     83         self._recorder = None
     84         self._player = None
     85         self._counter = None
     86         self._loaded_extension_handler = None
     87         self._arc_resource = arc_resource
     88 
     89 
     90     @property
     91     def _extension_handler(self):
     92         """Multimedia test extension handler."""
     93         if not self._loaded_extension_handler:
     94             extension = self._resource.get_extension(
     95                     constants.AUDIO_TEST_EXTENSION)
     96             logging.debug('Loaded extension: %s', extension)
     97             self._loaded_extension_handler = (
     98                     audio_extension_handler.AudioExtensionHandler(extension))
     99         return self._loaded_extension_handler
    100 
    101 
    102     def get_audio_devices(self):
    103         """Returns the audio devices from chrome.audio API.
    104 
    105         @returns: Checks docstring of get_audio_devices of AudioExtensionHandler.
    106 
    107         """
    108         return self._extension_handler.get_audio_devices()
    109 
    110 
    111     def set_chrome_active_volume(self, volume):
    112         """Sets the active audio output volume using chrome.audio API.
    113 
    114         @param volume: Volume to set (0~100).
    115 
    116         """
    117         self._extension_handler.set_active_volume(volume)
    118 
    119 
    120     def set_chrome_mute(self, mute):
    121         """Mutes the active audio output using chrome.audio API.
    122 
    123         @param mute: True to mute. False otherwise.
    124 
    125         """
    126         self._extension_handler.set_mute(mute)
    127 
    128 
    129     def get_chrome_active_volume_mute(self):
    130         """Gets the volume state of active audio output using chrome.audio API.
    131 
    132         @param returns: A tuple (volume, mute), where volume is 0~100, and mute
    133                         is True if node is muted, False otherwise.
    134 
    135         """
    136         return self._extension_handler.get_active_volume_mute()
    137 
    138 
    139     def set_chrome_active_node_type(self, output_node_type, input_node_type):
    140         """Sets active node type through chrome.audio API.
    141 
    142         The node types are defined in cras_utils.CRAS_NODE_TYPES.
    143         The current active node will be disabled first if the new active node
    144         is different from the current one.
    145 
    146         @param output_node_type: A node type defined in
    147                                  cras_utils.CRAS_NODE_TYPES. None to skip.
    148         @param input_node_type: A node type defined in
    149                                  cras_utils.CRAS_NODE_TYPES. None to skip
    150 
    151         """
    152         if output_node_type:
    153             node_id = cras_utils.get_node_id_from_node_type(
    154                     output_node_type, False)
    155             self._extension_handler.set_active_node_id(node_id)
    156         if input_node_type:
    157             node_id = cras_utils.get_node_id_from_node_type(
    158                     input_node_type, True)
    159             self._extension_handler.set_active_node_id(node_id)
    160 
    161 
    162     def check_audio_stream_at_selected_device(self):
    163         """Checks the audio output is at expected node"""
    164         output_device_name = cras_utils.get_selected_output_device_name()
    165         output_device_type = cras_utils.get_selected_output_device_type()
    166         logging.info("Output device name is %s", output_device_name)
    167         logging.info("Output device type is %s", output_device_type)
    168         alsa_utils.check_audio_stream_at_selected_device(output_device_name,
    169                                                          output_device_type)
    170 
    171 
    172     def cleanup(self):
    173         """Clean up the temporary files."""
    174         for path in glob.glob('/tmp/playback_*'):
    175             os.unlink(path)
    176 
    177         for path in glob.glob('/tmp/capture_*'):
    178             os.unlink(path)
    179 
    180         for path in glob.glob('/tmp/listen_*'):
    181             os.unlink(path)
    182 
    183         if self._recorder:
    184             self._recorder.cleanup()
    185         if self._player:
    186             self._player.cleanup()
    187         if self._listener:
    188             self._listener.cleanup()
    189 
    190         if self._arc_resource:
    191             self._arc_resource.cleanup()
    192 
    193 
    194     def playback(self, file_path, data_format, blocking=False):
    195         """Playback a file.
    196 
    197         @param file_path: The path to the file.
    198         @param data_format: A dict containing data format including
    199                             file_type, sample_format, channel, and rate.
    200                             file_type: file type e.g. 'raw' or 'wav'.
    201                             sample_format: One of the keys in
    202                                            audio_data.SAMPLE_FORMAT.
    203                             channel: number of channels.
    204                             rate: sampling rate.
    205         @param blocking: Blocks this call until playback finishes.
    206 
    207         @returns: True.
    208 
    209         @raises: AudioFacadeNativeError if data format is not supported.
    210 
    211         """
    212         logging.info('AudioFacadeNative playback file: %r. format: %r',
    213                      file_path, data_format)
    214 
    215         if data_format != self._PLAYBACK_DATA_FORMAT:
    216             raise AudioFacadeNativeError(
    217                     'data format %r is not supported' % data_format)
    218 
    219         self._player = Player()
    220         self._player.start(file_path, blocking)
    221 
    222         return True
    223 
    224 
    225     def stop_playback(self):
    226         """Stops playback process."""
    227         self._player.stop()
    228 
    229 
    230     def start_recording(self, data_format):
    231         """Starts recording an audio file.
    232 
    233         Currently the format specified in _CAPTURE_DATA_FORMATS is the only
    234         formats.
    235 
    236         @param data_format: A dict containing:
    237                             file_type: 'raw'.
    238                             sample_format: 'S16_LE' for 16-bit signed integer in
    239                                            little-endian.
    240                             channel: channel number.
    241                             rate: sampling rate.
    242 
    243 
    244         @returns: True
    245 
    246         @raises: AudioFacadeNativeError if data format is not supported.
    247 
    248         """
    249         logging.info('AudioFacadeNative record format: %r', data_format)
    250 
    251         if data_format not in self._CAPTURE_DATA_FORMATS:
    252             raise AudioFacadeNativeError(
    253                     'data format %r is not supported' % data_format)
    254 
    255         self._recorder = Recorder()
    256         self._recorder.start(data_format)
    257 
    258         return True
    259 
    260 
    261     def stop_recording(self):
    262         """Stops recording an audio file.
    263 
    264         @returns: The path to the recorded file.
    265                   None if capture device is not functional.
    266 
    267         """
    268         self._recorder.stop()
    269         if file_contains_all_zeros(self._recorder.file_path):
    270             logging.error('Recorded file contains all zeros. '
    271                           'Capture device is not functional')
    272             return None
    273         return self._recorder.file_path
    274 
    275 
    276     def start_listening(self, data_format):
    277         """Starts listening to hotword for a given format.
    278 
    279         Currently the format specified in _CAPTURE_DATA_FORMATS is the only
    280         formats.
    281 
    282         @param data_format: A dict containing:
    283                             file_type: 'raw'.
    284                             sample_format: 'S16_LE' for 16-bit signed integer in
    285                                            little-endian.
    286                             channel: channel number.
    287                             rate: sampling rate.
    288 
    289 
    290         @returns: True
    291 
    292         @raises: AudioFacadeNativeError if data format is not supported.
    293 
    294         """
    295         logging.info('AudioFacadeNative record format: %r', data_format)
    296 
    297         if data_format not in self._LISTEN_DATA_FORMATS:
    298             raise AudioFacadeNativeError(
    299                     'data format %r is not supported' % data_format)
    300 
    301         self._listener = Listener()
    302         self._listener.start(data_format)
    303 
    304         return True
    305 
    306 
    307     def stop_listening(self):
    308         """Stops listening to hotword.
    309 
    310         @returns: The path to the recorded file.
    311                   None if hotwording is not functional.
    312 
    313         """
    314         self._listener.stop()
    315         if file_contains_all_zeros(self._listener.file_path):
    316             logging.error('Recorded file contains all zeros. '
    317                           'Hotwording device is not functional')
    318             return None
    319         return self._listener.file_path
    320 
    321 
    322     def set_selected_output_volume(self, volume):
    323         """Sets the selected output volume.
    324 
    325         @param volume: the volume to be set(0-100).
    326 
    327         """
    328         cras_utils.set_selected_output_node_volume(volume)
    329 
    330 
    331     def set_input_gain(self, gain):
    332         """Sets the system capture gain.
    333 
    334         @param gain: the capture gain in db*100 (100 = 1dB)
    335 
    336         """
    337         cras_utils.set_capture_gain(gain)
    338 
    339 
    340     def set_selected_node_types(self, output_node_types, input_node_types):
    341         """Set selected node types.
    342 
    343         The node types are defined in cras_utils.CRAS_NODE_TYPES.
    344 
    345         @param output_node_types: A list of output node types.
    346                                   None to skip setting.
    347         @param input_node_types: A list of input node types.
    348                                  None to skip setting.
    349 
    350         """
    351         cras_utils.set_selected_node_types(output_node_types, input_node_types)
    352 
    353 
    354     def get_selected_node_types(self):
    355         """Gets the selected output and input node types.
    356 
    357         @returns: A tuple (output_node_types, input_node_types) where each
    358                   field is a list of selected node types defined in
    359                   cras_utils.CRAS_NODE_TYPES.
    360 
    361         """
    362         return cras_utils.get_selected_node_types()
    363 
    364 
    365     def get_plugged_node_types(self):
    366         """Gets the plugged output and input node types.
    367 
    368         @returns: A tuple (output_node_types, input_node_types) where each
    369                   field is a list of plugged node types defined in
    370                   cras_utils.CRAS_NODE_TYPES.
    371 
    372         """
    373         return cras_utils.get_plugged_node_types()
    374 
    375 
    376     def dump_diagnostics(self, file_path):
    377         """Dumps audio diagnostics results to a file.
    378 
    379         @param file_path: The path to dump results.
    380 
    381         @returns: True
    382 
    383         """
    384         with open(file_path, 'w') as f:
    385             f.write(audio_helper.get_audio_diagnostics())
    386         return True
    387 
    388 
    389     def start_counting_signal(self, signal_name):
    390         """Starts counting DBus signal from Cras.
    391 
    392         @param signal_name: Signal of interest.
    393 
    394         """
    395         if self._counter:
    396             raise AudioFacadeNativeError('There is an ongoing counting.')
    397         self._counter = cras_dbus_utils.CrasDBusBackgroundSignalCounter()
    398         self._counter.start(signal_name)
    399 
    400 
    401     def stop_counting_signal(self):
    402         """Stops counting DBus signal from Cras.
    403 
    404         @returns: Number of signals starting from last start_counting_signal
    405                   call.
    406 
    407         """
    408         if not self._counter:
    409             raise AudioFacadeNativeError('Should start counting signal first')
    410         result = self._counter.stop()
    411         self._counter = None
    412         return result
    413 
    414 
    415     def wait_for_unexpected_nodes_changed(self, timeout_secs):
    416         """Waits for unexpected nodes changed signal.
    417 
    418         @param timeout_secs: Timeout in seconds for waiting.
    419 
    420         """
    421         cras_dbus_utils.wait_for_unexpected_nodes_changed(timeout_secs)
    422 
    423 
    424     @check_arc_resource
    425     def start_arc_recording(self):
    426         """Starts recording using microphone app in container."""
    427         self._arc_resource.microphone.start_microphone_app()
    428 
    429 
    430     @check_arc_resource
    431     def stop_arc_recording(self):
    432         """Checks the recording is stopped and gets the recorded path.
    433 
    434         The recording duration of microphone app is fixed, so this method just
    435         copies the recorded result from container to a path on Cros device.
    436 
    437         """
    438         _, file_path = tempfile.mkstemp(prefix='capture_', suffix='.amr-nb')
    439         self._arc_resource.microphone.stop_microphone_app(file_path)
    440         return file_path
    441 
    442 
    443     @check_arc_resource
    444     def set_arc_playback_file(self, file_path):
    445         """Copies the audio file to be played into container.
    446 
    447         User should call this method to put the file into container before
    448         calling start_arc_playback.
    449 
    450         @param file_path: Path to the file to be played on Cros host.
    451 
    452         @returns: Path to the file in container.
    453 
    454         """
    455         return self._arc_resource.play_music.set_playback_file(file_path)
    456 
    457 
    458     @check_arc_resource
    459     def start_arc_playback(self, path):
    460         """Start playback through Play Music app.
    461 
    462         Before calling this method, user should call set_arc_playback_file to
    463         put the file into container.
    464 
    465         @param path: Path to the file in container.
    466 
    467         """
    468         self._arc_resource.play_music.start_playback(path)
    469 
    470 
    471     @check_arc_resource
    472     def stop_arc_playback(self):
    473         """Stop playback through Play Music app."""
    474         self._arc_resource.play_music.stop_playback()
    475 
    476 
    477 class RecorderError(Exception):
    478     """Error in Recorder."""
    479     pass
    480 
    481 
    482 class Recorder(object):
    483     """The class to control recording subprocess.
    484 
    485     Properties:
    486         file_path: The path to recorded file. It should be accessed after
    487                    stop() is called.
    488 
    489     """
    490     def __init__(self):
    491         """Initializes a Recorder."""
    492         _, self.file_path = tempfile.mkstemp(prefix='capture_', suffix='.raw')
    493         self._capture_subprocess = None
    494 
    495 
    496     def start(self, data_format):
    497         """Starts recording.
    498 
    499         Starts recording subprocess. It can be stopped by calling stop().
    500 
    501         @param data_format: A dict containing:
    502                             file_type: 'raw'.
    503                             sample_format: 'S16_LE' for 16-bit signed integer in
    504                                            little-endian.
    505                             channel: channel number.
    506                             rate: sampling rate.
    507 
    508         @raises: RecorderError: If recording subprocess is terminated
    509                  unexpectedly.
    510 
    511         """
    512         self._capture_subprocess = cmd_utils.popen(
    513                 cras_utils.capture_cmd(
    514                         capture_file=self.file_path, duration=None,
    515                         channels=data_format['channel'],
    516                         rate=data_format['rate']))
    517 
    518 
    519     def stop(self):
    520         """Stops recording subprocess."""
    521         if self._capture_subprocess.poll() is None:
    522             self._capture_subprocess.terminate()
    523         else:
    524             raise RecorderError(
    525                     'Recording process was terminated unexpectedly.')
    526 
    527 
    528     def cleanup(self):
    529         """Cleanup the resources.
    530 
    531         Terminates the recording process if needed.
    532 
    533         """
    534         if self._capture_subprocess and self._capture_subprocess.poll() is None:
    535             self._capture_subprocess.terminate()
    536 
    537 
    538 class PlayerError(Exception):
    539     """Error in Player."""
    540     pass
    541 
    542 
    543 class Player(object):
    544     """The class to control audio playback subprocess.
    545 
    546     Properties:
    547         file_path: The path to the file to play.
    548 
    549     """
    550     def __init__(self):
    551         """Initializes a Player."""
    552         self._playback_subprocess = None
    553 
    554 
    555     def start(self, file_path, blocking):
    556         """Starts recording.
    557 
    558         Starts recording subprocess. It can be stopped by calling stop().
    559 
    560         @param file_path: The path to the file.
    561         @param blocking: Blocks this call until playback finishes.
    562 
    563         """
    564         self._playback_subprocess = cras_utils.playback(
    565                 blocking, playback_file=file_path)
    566 
    567 
    568     def stop(self):
    569         """Stops playback subprocess."""
    570         cmd_utils.kill_or_log_returncode(self._playback_subprocess)
    571 
    572 
    573     def cleanup(self):
    574         """Cleanup the resources.
    575 
    576         Terminates the playback process if needed.
    577 
    578         """
    579         self.stop()
    580 
    581 
    582 class ListenerError(Exception):
    583     """Error in Listener."""
    584     pass
    585 
    586 
    587 class Listener(object):
    588     """The class to control listening subprocess.
    589 
    590     Properties:
    591         file_path: The path to recorded file. It should be accessed after
    592                    stop() is called.
    593 
    594     """
    595     def __init__(self):
    596         """Initializes a Listener."""
    597         _, self.file_path = tempfile.mkstemp(prefix='listen_', suffix='.raw')
    598         self._capture_subprocess = None
    599 
    600 
    601     def start(self, data_format):
    602         """Starts listening.
    603 
    604         Starts listening subprocess. It can be stopped by calling stop().
    605 
    606         @param data_format: A dict containing:
    607                             file_type: 'raw'.
    608                             sample_format: 'S16_LE' for 16-bit signed integer in
    609                                            little-endian.
    610                             channel: channel number.
    611                             rate: sampling rate.
    612 
    613         @raises: ListenerError: If listening subprocess is terminated
    614                  unexpectedly.
    615 
    616         """
    617         self._capture_subprocess = cmd_utils.popen(
    618                 cras_utils.listen_cmd(
    619                         capture_file=self.file_path, duration=None,
    620                         channels=data_format['channel'],
    621                         rate=data_format['rate']))
    622 
    623 
    624     def stop(self):
    625         """Stops listening subprocess."""
    626         if self._capture_subprocess.poll() is None:
    627             self._capture_subprocess.terminate()
    628         else:
    629             raise ListenerError(
    630                     'Listening process was terminated unexpectedly.')
    631 
    632 
    633     def cleanup(self):
    634         """Cleanup the resources.
    635 
    636         Terminates the listening process if needed.
    637 
    638         """
    639         if self._capture_subprocess and self._capture_subprocess.poll() is None:
    640             self._capture_subprocess.terminate()
    641