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