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 glob
      8 import logging
      9 import multiprocessing
     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 class AudioFacadeNative(object):
     27     """Facede to access the audio-related functionality.
     28 
     29     The methods inside this class only accept Python native types.
     30 
     31     """
     32     _CAPTURE_DATA_FORMATS = [
     33             dict(file_type='raw', sample_format='S16_LE',
     34                  channel=1, rate=48000),
     35             dict(file_type='raw', sample_format='S16_LE',
     36                  channel=2, rate=48000)]
     37 
     38     _PLAYBACK_DATA_FORMAT = dict(
     39             file_type='raw', sample_format='S16_LE', channel=2, rate=48000)
     40 
     41     def __init__(self, resource):
     42         """Initializes an audio facade.
     43 
     44         @param resource: A FacadeResource object.
     45 
     46         """
     47         self._resource = resource
     48         self._recorder = None
     49         self._counter = None
     50         self._extension_handler = None
     51         self._create_extension_handler()
     52 
     53 
     54     def _create_extension_handler(self):
     55         """Loads multimedia test extension and creates extension handler."""
     56         extension = self._resource.get_extension(
     57                 constants.MULTIMEDIA_TEST_EXTENSION)
     58         logging.debug('Loaded extension: %s', extension)
     59         self._extension_handler = audio_extension_handler.AudioExtensionHandler(
     60                 extension)
     61 
     62 
     63     def get_audio_info(self):
     64         """Returns the audio info from chrome.audio API.
     65 
     66         @returns: Checks docstring of get_audio_info of AudioExtensionHandler.
     67 
     68         """
     69         return self._extension_handler.get_audio_info()
     70 
     71 
     72     def set_chrome_active_volume(self, volume):
     73         """Sets the active audio output volume using chrome.audio API.
     74 
     75         @param volume: Volume to set (0~100).
     76 
     77         """
     78         self._extension_handler.set_active_volume(volume)
     79 
     80 
     81     def set_chrome_mute(self, mute):
     82         """Mutes the active audio output using chrome.audio API.
     83 
     84         @param mute: True to mute. False otherwise.
     85 
     86         """
     87         self._extension_handler.set_mute(mute)
     88 
     89 
     90     def get_chrome_active_volume_mute(self):
     91         """Gets the volume state of active audio output using chrome.audio API.
     92 
     93         @param returns: A tuple (volume, mute), where volume is 0~100, and mute
     94                         is True if node is muted, False otherwise.
     95 
     96         """
     97         return self._extension_handler.get_active_volume_mute()
     98 
     99 
    100     def set_chrome_active_node_type(self, output_node_type, input_node_type):
    101         """Sets active node type through chrome.audio API.
    102 
    103         The node types are defined in cras_utils.CRAS_NODE_TYPES.
    104         The current active node will be disabled first if the new active node
    105         is different from the current one.
    106 
    107         @param output_node_type: A node type defined in
    108                                  cras_utils.CRAS_NODE_TYPES. None to skip.
    109         @param input_node_type: A node type defined in
    110                                  cras_utils.CRAS_NODE_TYPES. None to skip
    111 
    112         """
    113         if output_node_type:
    114             node_id = cras_utils.get_node_id_from_node_type(
    115                     output_node_type, False)
    116             self._extension_handler.set_active_node_id(node_id)
    117         if input_node_type:
    118             node_id = cras_utils.get_node_id_from_node_type(
    119                     input_node_type, True)
    120             self._extension_handler.set_active_node_id(node_id)
    121 
    122 
    123     def cleanup(self):
    124         """Clean up the temporary files."""
    125         for path in glob.glob('/tmp/playback_*'):
    126             os.unlink(path)
    127 
    128         for path in glob.glob('/tmp/capture_*'):
    129             os.unlink(path)
    130 
    131 
    132     def playback(self, file_path, data_format, blocking=False):
    133         """Playback a file.
    134 
    135         @param file_path: The path to the file.
    136         @param data_format: A dict containing data format including
    137                             file_type, sample_format, channel, and rate.
    138                             file_type: file type e.g. 'raw' or 'wav'.
    139                             sample_format: One of the keys in
    140                                            audio_data.SAMPLE_FORMAT.
    141                             channel: number of channels.
    142                             rate: sampling rate.
    143         @param blocking: Blocks this call until playback finishes.
    144 
    145         @returns: True.
    146 
    147         @raises: AudioFacadeNativeError if data format is not supported.
    148 
    149         """
    150         logging.info('AudioFacadeNative playback file: %r. format: %r',
    151                      file_path, data_format)
    152 
    153         if data_format != self._PLAYBACK_DATA_FORMAT:
    154             raise AudioFacadeNativeError(
    155                     'data format %r is not supported' % data_format)
    156 
    157         def _playback():
    158             """Playback using cras utility."""
    159             cras_utils.playback(playback_file=file_path)
    160 
    161         if blocking:
    162             _playback()
    163         else:
    164             p = multiprocessing.Process(target=_playback)
    165             p.daemon = True
    166             p.start()
    167 
    168         return True
    169 
    170 
    171     def start_recording(self, data_format):
    172         """Starts recording an audio file.
    173 
    174         Currently the format specified in _CAPTURE_DATA_FORMATS is the only
    175         formats.
    176 
    177         @param data_format: A dict containing:
    178                             file_type: 'raw'.
    179                             sample_format: 'S16_LE' for 16-bit signed integer in
    180                                            little-endian.
    181                             channel: channel number.
    182                             rate: sampling rate.
    183 
    184 
    185         @returns: True
    186 
    187         @raises: AudioFacadeNativeError if data format is not supported.
    188 
    189         """
    190         logging.info('AudioFacadeNative record format: %r', data_format)
    191 
    192         if data_format not in self._CAPTURE_DATA_FORMATS:
    193             raise AudioFacadeNativeError(
    194                     'data format %r is not supported' % data_format)
    195 
    196         self._recorder = Recorder()
    197         self._recorder.start(data_format)
    198 
    199         return True
    200 
    201 
    202     def stop_recording(self):
    203         """Stops recording an audio file.
    204 
    205         @returns: The path to the recorded file.
    206 
    207         """
    208         self._recorder.stop()
    209         return self._recorder.file_path
    210 
    211 
    212     def set_selected_output_volume(self, volume):
    213         """Sets the selected output volume.
    214 
    215         @param volume: the volume to be set(0-100).
    216 
    217         """
    218         cras_utils.set_selected_output_node_volume(volume)
    219 
    220 
    221     def set_selected_node_types(self, output_node_types, input_node_types):
    222         """Set selected node types.
    223 
    224         The node types are defined in cras_utils.CRAS_NODE_TYPES.
    225 
    226         @param output_node_types: A list of output node types.
    227                                   None to skip setting.
    228         @param input_node_types: A list of input node types.
    229                                  None to skip setting.
    230 
    231         """
    232         cras_utils.set_selected_node_types(output_node_types, input_node_types)
    233 
    234 
    235     def get_selected_node_types(self):
    236         """Gets the selected output and input node types.
    237 
    238         @returns: A tuple (output_node_types, input_node_types) where each
    239                   field is a list of selected node types defined in
    240                   cras_utils.CRAS_NODE_TYPES.
    241 
    242         """
    243         return cras_utils.get_selected_node_types()
    244 
    245 
    246     def get_plugged_node_types(self):
    247         """Gets the plugged output and input node types.
    248 
    249         @returns: A tuple (output_node_types, input_node_types) where each
    250                   field is a list of plugged node types defined in
    251                   cras_utils.CRAS_NODE_TYPES.
    252 
    253         """
    254         return cras_utils.get_plugged_node_types()
    255 
    256 
    257     def dump_diagnostics(self, file_path):
    258         """Dumps audio diagnostics results to a file.
    259 
    260         @param file_path: The path to dump results.
    261 
    262         @returns: True
    263 
    264         """
    265         with open(file_path, 'w') as f:
    266             f.write(audio_helper.get_audio_diagnostics())
    267         return True
    268 
    269 
    270     def start_counting_signal(self, signal_name):
    271         """Starts counting DBus signal from Cras.
    272 
    273         @param signal_name: Signal of interest.
    274 
    275         """
    276         if self._counter:
    277             raise AudioFacadeNativeError('There is an ongoing counting.')
    278         self._counter = cras_dbus_utils.CrasDBusBackgroundSignalCounter()
    279         self._counter.start(signal_name)
    280 
    281 
    282     def stop_counting_signal(self):
    283         """Stops counting DBus signal from Cras.
    284 
    285         @returns: Number of signals starting from last start_counting_signal
    286                   call.
    287 
    288         """
    289         if not self._counter:
    290             raise AudioFacadeNativeError('Should start counting signal first')
    291         result = self._counter.stop()
    292         self._counter = None
    293         return result
    294 
    295 
    296     def wait_for_unexpected_nodes_changed(self, timeout_secs):
    297         """Waits for unexpected nodes changed signal.
    298 
    299         @param timeout_secs: Timeout in seconds for waiting.
    300 
    301         """
    302         cras_dbus_utils.wait_for_unexpected_nodes_changed(timeout_secs)
    303 
    304 
    305 
    306 class RecorderError(Exception):
    307     """Error in Recorder."""
    308     pass
    309 
    310 
    311 class Recorder(object):
    312     """The class to control recording subprocess.
    313 
    314     Properties:
    315         file_path: The path to recorded file. It should be accessed after
    316                    stop() is called.
    317 
    318     """
    319     def __init__(self):
    320         """Initializes a Recorder."""
    321         _, self.file_path = tempfile.mkstemp(prefix='capture_', suffix='.raw')
    322         self._capture_subprocess = None
    323 
    324 
    325     def start(self, data_format):
    326         """Starts recording.
    327 
    328         Starts recording subprocess. It can be stopped by calling stop().
    329 
    330         @param data_format: A dict containing:
    331                             file_type: 'raw'.
    332                             sample_format: 'S16_LE' for 16-bit signed integer in
    333                                            little-endian.
    334                             channel: channel number.
    335                             rate: sampling rate.
    336 
    337         @raises: RecorderError: If recording subprocess is terminated
    338                  unexpectedly.
    339 
    340         """
    341         self._capture_subprocess = cmd_utils.popen(
    342                 cras_utils.capture_cmd(
    343                         capture_file=self.file_path, duration=None,
    344                         channels=data_format['channel'],
    345                         rate=data_format['rate']))
    346 
    347 
    348     def stop(self):
    349         """Stops recording subprocess."""
    350         if self._capture_subprocess.poll() is None:
    351             self._capture_subprocess.terminate()
    352         else:
    353             raise RecorderError(
    354                     'Recording process was terminated unexpectedly.')
    355