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