1# Lint as: python2, python3
2# Copyright 2014 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""This module provides the audio widgets used in audio tests."""
7
8from __future__ import absolute_import
9from __future__ import division
10from __future__ import print_function
11
12import abc
13import copy
14import logging
15import os
16import tempfile
17
18from autotest_lib.client.cros.audio import audio_data
19from autotest_lib.client.cros.audio import audio_test_data
20from autotest_lib.client.cros.audio import sox_utils
21from autotest_lib.client.cros.chameleon import audio_test_utils
22from autotest_lib.client.cros.chameleon import chameleon_audio_ids as ids
23from autotest_lib.client.cros.chameleon import chameleon_port_finder
24import six
25
26_CHAMELEON_FILE_PATH = os.path.join(os.path.dirname(__file__))
27
28class AudioWidget(object):
29    """
30    This class abstracts an audio widget in audio test framework. A widget
31    is identified by its audio port. The handler passed in at __init__ will
32    handle action on the audio widget.
33
34    Properties:
35        audio_port: The AudioPort this AudioWidget resides in.
36        handler: The handler that handles audio action on the widget. It is
37                  actually a (Chameleon/Cros)(Input/Output)WidgetHandler object.
38
39    """
40    def __init__(self, audio_port, handler):
41        """Initializes an AudioWidget on a AudioPort.
42
43        @param audio_port: An AudioPort object.
44        @param handler: A WidgetHandler object which handles action on the widget.
45
46        """
47        self.audio_port = audio_port
48        self.handler = handler
49
50
51    @property
52    def port_id(self):
53        """Port id of this audio widget.
54
55        @returns: A string. The port id defined in chameleon_audio_ids for this
56                  audio widget.
57        """
58        return self.audio_port.port_id
59
60
61class AudioInputWidget(AudioWidget):
62    """
63    This class abstracts an audio input widget. This class provides the audio
64    action that is available on an input audio port.
65
66    Properties:
67        _remote_rec_path: The path to the recorded file on the remote host.
68        _rec_binary: The recorded binary data.
69        _rec_format: The recorded data format. A dict containing
70                     file_type: 'raw' or 'wav'.
71                     sample_format: 'S32_LE' for 32-bit signed integer in
72                                    little-endian. Refer to aplay manpage for
73                                    other formats.
74                     channel: channel number.
75                     rate: sampling rate.
76
77        _channel_map: A list containing current channel map. Checks docstring
78                      of channel_map method for details.
79
80    """
81    def __init__(self, *args, **kwargs):
82        """Initializes an AudioInputWidget."""
83        super(AudioInputWidget, self).__init__(*args, **kwargs)
84        self._remote_rec_path = None
85        self._rec_binary = None
86        self._rec_format = None
87        self._channel_map = None
88        self._init_channel_map_without_link()
89
90
91    def start_recording(self, pinned=False, block_size=None):
92        """Starts recording.
93
94        @param pinned: Pins the audio to the input device.
95        @param block_size: The number for frames per callback.
96
97        """
98        self._remote_rec_path = None
99        self._rec_binary = None
100        self._rec_format = None
101        node_type = None
102        if pinned:
103            node_type = audio_test_utils.cros_port_id_to_cras_node_type(
104                    self.port_id)
105
106        self.handler.start_recording(node_type=node_type, block_size=block_size)
107
108
109    def stop_recording(self, pinned=False):
110        """Stops recording.
111
112        @param pinned: Stop the recording on the pinned input device.
113                       False means to stop the active selected one.
114
115        """
116        node_type = None
117        if pinned:
118            node_type = audio_test_utils.cros_port_id_to_cras_node_type(
119                    self.port_id)
120
121        self._remote_rec_path, self._rec_format = self.handler.stop_recording(
122                node_type=node_type)
123
124
125    def start_listening(self):
126        """Starts listening."""
127        self._remote_rec_path = None
128        self._rec_binary = None
129        self._rec_format = None
130        self.handler.start_listening()
131
132
133    def stop_listening(self):
134        """Stops listening."""
135        self._remote_rec_path, self._rec_format = self.handler.stop_listening()
136
137
138    def read_recorded_binary(self):
139        """Gets recorded file from handler and fills _rec_binary."""
140        self._rec_binary = self.handler.get_recorded_binary(
141                self._remote_rec_path, self._rec_format)
142
143
144    def save_file(self, file_path):
145        """Saves recorded data to a file.
146
147        @param file_path: The path to save the file.
148
149        """
150        with open(file_path, 'wb') as f:
151            logging.debug('Saving recorded raw file to %s', file_path)
152            f.write(self._rec_binary)
153
154        wav_file_path = file_path + '.wav'
155        logging.debug('Saving recorded wav file to %s', wav_file_path)
156        sox_utils.convert_raw_file(
157                path_src=file_path,
158                channels_src=self._channel,
159                rate_src=self._sampling_rate,
160                bits_src=self._sample_size_bits,
161                path_dst=wav_file_path)
162
163
164    def get_binary(self):
165        """Gets recorded binary data.
166
167        @returns: The recorded binary data.
168
169        """
170        return self._rec_binary
171
172
173    @property
174    def data_format(self):
175        """The recorded data format.
176
177        @returns: The recorded data format.
178
179        """
180        return self._rec_format
181
182
183    @property
184    def channel_map(self):
185        """The recorded data channel map.
186
187        @returns: The recorded channel map. A list containing channel mapping.
188                  E.g. [1, 0, None, None, None, None, None, None] means
189                  channel 0 of recorded data should be mapped to channel 1 of
190                  data played to the recorder. Channel 1 of recorded data should
191                  be mapped to channel 0 of data played to recorder.
192                  Channel 2 to 7 of recorded data should be ignored.
193
194        """
195        return self._channel_map
196
197
198    @channel_map.setter
199    def channel_map(self, new_channel_map):
200        """Sets channel map.
201
202        @param new_channel_map: A list containing new channel map.
203
204        """
205        self._channel_map = copy.deepcopy(new_channel_map)
206
207
208    def _init_channel_map_without_link(self):
209        """Initializes channel map without WidgetLink.
210
211        WidgetLink sets channel map to a sink widget when the link combines
212        a source widget to a sink widget. For simple cases like internal
213        microphone on Cros device, or Mic port on Chameleon, the audio signal
214        is over the air, so we do not use link to combine the source to
215        the sink. We just set a default channel map in this case.
216
217        """
218        if self.port_id in [ids.ChameleonIds.MIC, ids.CrosIds.INTERNAL_MIC]:
219            self._channel_map = [0]
220
221
222    @property
223    def _sample_size_bytes(self):
224        """Gets sample size in bytes of recorded data."""
225        return audio_data.SAMPLE_FORMATS[
226                self._rec_format['sample_format']]['size_bytes']
227
228
229    @property
230    def _sample_size_bits(self):
231        """Gets sample size in bits of recorded data."""
232        return self._sample_size_bytes * 8
233
234
235    @property
236    def _channel(self):
237        """Gets number of channels of recorded data."""
238        return self._rec_format['channel']
239
240
241    @property
242    def _sampling_rate(self):
243        """Gets sampling rate of recorded data."""
244        return self._rec_format['rate']
245
246
247    def remove_head(self, duration_secs):
248        """Removes a duration of recorded data from head.
249
250        @param duration_secs: The duration in seconds to be removed from head.
251
252        """
253        offset = int(self._sampling_rate * duration_secs *
254                     self._sample_size_bytes * self._channel)
255        self._rec_binary = self._rec_binary[offset:]
256
257
258    def lowpass_filter(self, frequency):
259        """Passes the recorded data to a lowpass filter.
260
261        @param frequency: The 3dB frequency of lowpass filter.
262
263        """
264        with tempfile.NamedTemporaryFile(
265                prefix='original_') as original_file:
266            with tempfile.NamedTemporaryFile(
267                    prefix='filtered_') as filtered_file:
268
269                original_file.write(self._rec_binary)
270                original_file.flush()
271
272                sox_utils.lowpass_filter(
273                        original_file.name, self._channel,
274                        self._sample_size_bits, self._sampling_rate,
275                        filtered_file.name, frequency)
276
277                self._rec_binary = filtered_file.read()
278
279
280class AudioOutputWidget(AudioWidget):
281    """
282    This class abstracts an audio output widget. This class provides the audio
283    action that is available on an output audio port.
284
285    """
286    def __init__(self, *args, **kwargs):
287        """Initializes an AudioOutputWidget."""
288        super(AudioOutputWidget, self).__init__(*args, **kwargs)
289        self._remote_playback_path = None
290
291
292    def set_playback_data(self, test_data):
293        """Sets data to play.
294
295        Sets the data to play in the handler and gets the remote file path.
296
297        @param test_data: An AudioTestData object.
298
299        @returns: path to the remote playback data
300
301        """
302        self._remote_playback_path = self.handler.set_playback_data(test_data)
303
304        return self._remote_playback_path
305
306    def start_playback(self, blocking=False, pinned=False, block_size=None):
307        """Starts playing audio specified in previous set_playback_data call.
308
309        @param blocking: Blocks this call until playback finishes.
310        @param pinned: Pins the audio to the active output device.
311        @param block_size: The number for frames per callback.
312
313        """
314        node_type = None
315        if pinned:
316            node_type = audio_test_utils.cros_port_id_to_cras_node_type(
317                    self.port_id)
318
319        self.handler.start_playback(
320                self._remote_playback_path, blocking, node_type=node_type,
321                block_size=block_size)
322
323    def start_playback_with_path(self, remote_playback_path, blocking=False):
324        """Starts playing audio specified in previous set_playback_data call
325           and the remote_playback_path returned by set_playback_data function.
326
327        @param remote_playback_path: Path returned by set_playback_data.
328        @param blocking: Blocks this call until playback finishes.
329
330        """
331        self.handler.start_playback(remote_playback_path, blocking)
332
333
334    def stop_playback(self):
335        """Stops playing audio."""
336        self.handler.stop_playback()
337
338
339class WidgetHandler(six.with_metaclass(abc.ABCMeta, object)):
340    """This class abstracts handler for basic actions on widget."""
341
342    @abc.abstractmethod
343    def plug(self):
344        """Plug this widget."""
345        pass
346
347
348    @abc.abstractmethod
349    def unplug(self):
350        """Unplug this widget."""
351        pass
352
353
354class ChameleonWidgetHandler(WidgetHandler):
355    """
356    This class abstracts a Chameleon audio widget handler.
357
358    Properties:
359        interface: A string that represents the interface name on
360                   Chameleon, e.g. 'HDMI', 'LineIn', 'LineOut'.
361        scale: The scale is the scaling factor to be applied on the data of the
362               widget before playing or after recording.
363        _chameleon_board: A ChameleonBoard object to control Chameleon.
364        _port: A ChameleonPort object to control port on Chameleon.
365
366    """
367    # The mic port on chameleon has a small gain. We need to scale
368    # the recorded value up, otherwise, the recorded value will be
369    # too small and will be falsely judged as not meaningful in the
370    # processing, even when the recorded audio is clear.
371    _DEFAULT_MIC_SCALE = 50.0
372
373    def __init__(self, chameleon_board, interface):
374        """Initializes a ChameleonWidgetHandler.
375
376        @param chameleon_board: A ChameleonBoard object.
377        @param interface: A string that represents the interface name on
378                          Chameleon, e.g. 'HDMI', 'LineIn', 'LineOut'.
379
380        """
381        self.interface = interface
382        self._chameleon_board = chameleon_board
383        self._port = self._find_port(interface)
384        self.scale = None
385        self._init_scale_without_link()
386
387
388    @abc.abstractmethod
389    def _find_port(self, interface):
390        """Finds the port by interface."""
391        pass
392
393
394    def plug(self):
395        """Plugs this widget."""
396        self._port.plug()
397
398
399    def unplug(self):
400        """Unplugs this widget."""
401        self._port.unplug()
402
403
404    def _init_scale_without_link(self):
405        """Initializes scale for widget handler not used with link.
406
407        Audio widget link sets scale when it connects two audio widgets.
408        For audio widget not used with link, e.g. Mic on Chameleon, we set
409        a default scale here.
410
411        """
412        if self.interface == 'Mic':
413            self.scale = self._DEFAULT_MIC_SCALE
414
415
416class ChameleonInputWidgetHandler(ChameleonWidgetHandler):
417    """
418    This class abstracts a Chameleon audio input widget handler.
419
420    """
421    def start_recording(self, **kargs):
422        """Starts recording.
423
424        @param kargs: Other arguments that Chameleon doesn't support.
425
426        """
427        self._port.start_capturing_audio()
428
429
430    def stop_recording(self, **kargs):
431        """Stops recording.
432
433        Gets remote recorded path and format from Chameleon. The format can
434        then be used in get_recorded_binary()
435
436        @param kargs: Other arguments that Chameleon doesn't support.
437
438        @returns: A tuple (remote_path, data_format) for recorded data.
439                  Refer to stop_capturing_audio call of ChameleonAudioInput.
440
441        """
442        return self._port.stop_capturing_audio()
443
444
445    def get_recorded_binary(self, remote_path, record_format):
446        """Gets remote recorded file binary.
447
448        Reads file from Chameleon host and handles scale if needed.
449
450        @param remote_path: The path to the recorded file on Chameleon.
451        @param record_format: The recorded data format. A dict containing
452                     file_type: 'raw' or 'wav'.
453                     sample_format: 'S32_LE' for 32-bit signed integer in
454                                    little-endian. Refer to aplay manpage for
455                                    other formats.
456                     channel: channel number.
457                     rate: sampling rate.
458
459        @returns: The recorded binary.
460
461        """
462        with tempfile.NamedTemporaryFile(prefix='recorded_') as f:
463            self._chameleon_board.host.get_file(remote_path, f.name)
464
465            # Handles scaling using audio_test_data.
466            test_data = audio_test_data.AudioTestData(record_format, f.name)
467            converted_test_data = test_data.convert(record_format, self.scale)
468            try:
469                return converted_test_data.get_binary()
470            finally:
471                converted_test_data.delete()
472
473
474    def _find_port(self, interface):
475        """Finds a Chameleon audio port by interface(port name).
476
477        @param interface: string, the interface. e.g: HDMI.
478
479        @returns: A ChameleonPort object.
480
481        @raises: ValueError if port is not connected.
482
483        """
484        finder = chameleon_port_finder.ChameleonAudioInputFinder(
485                self._chameleon_board)
486        chameleon_port = finder.find_port(interface)
487        if not chameleon_port:
488            raise ValueError(
489                    'Port %s is not connected to Chameleon' % interface)
490        return chameleon_port
491
492
493class ChameleonHDMIInputWidgetHandlerError(Exception):
494    """Error in ChameleonHDMIInputWidgetHandler."""
495
496
497class ChameleonHDMIInputWidgetHandler(ChameleonInputWidgetHandler):
498    """This class abstracts a Chameleon HDMI audio input widget handler."""
499    _EDID_FILE_PATH = os.path.join(
500        _CHAMELEON_FILE_PATH, 'test_data/edids/HDMI_DELL_U2410.txt')
501
502    def __init__(self, chameleon_board, interface, display_facade):
503        """Initializes a ChameleonHDMIInputWidgetHandler.
504
505        @param chameleon_board: Pass to ChameleonInputWidgetHandler.
506        @param interface: Pass to ChameleonInputWidgetHandler.
507        @param display_facade: A DisplayFacadeRemoteAdapter to access
508                               Cros device display functionality.
509
510        """
511        super(ChameleonHDMIInputWidgetHandler, self).__init__(
512              chameleon_board, interface)
513        self._display_facade = display_facade
514        self._hdmi_video_port = None
515
516        self._find_video_port()
517
518
519    def _find_video_port(self):
520        """Finds HDMI as a video port."""
521        finder = chameleon_port_finder.ChameleonVideoInputFinder(
522                self._chameleon_board, self._display_facade)
523        self._hdmi_video_port = finder.find_port(self.interface)
524        if not self._hdmi_video_port:
525            raise ChameleonHDMIInputWidgetHandlerError(
526                    'Can not find HDMI port, perhaps HDMI is not connected?')
527
528
529    def set_edid_for_audio(self):
530        """Sets the EDID suitable for audio test."""
531        self._hdmi_video_port.set_edid_from_file(self._EDID_FILE_PATH)
532
533
534    def restore_edid(self):
535        """Restores the original EDID."""
536        self._hdmi_video_port.restore_edid()
537
538
539class ChameleonOutputWidgetHandler(ChameleonWidgetHandler):
540    """
541    This class abstracts a Chameleon audio output widget handler.
542
543    """
544    def __init__(self, *args, **kwargs):
545        """Initializes an ChameleonOutputWidgetHandler."""
546        super(ChameleonOutputWidgetHandler, self).__init__(*args, **kwargs)
547        self._test_data_for_chameleon_format = None
548
549
550    def set_playback_data(self, test_data):
551        """Sets data to play.
552
553        Handles scale if needed. Creates a path and sends the scaled data to
554        Chameleon at that path.
555
556        @param test_data: An AudioTestData object.
557
558        @return: The remote data path on Chameleon.
559
560        """
561        self._test_data_for_chameleon_format = test_data.data_format
562        return self._scale_and_send_playback_data(test_data)
563
564
565    def _scale_and_send_playback_data(self, test_data):
566        """Sets data to play on Chameleon.
567
568        Creates a path and sends the scaled test data to Chameleon at that path.
569
570        @param test_data: An AudioTestData object.
571
572        @return: The remote data path on Chameleon.
573
574        """
575        test_data_for_chameleon = test_data.convert(
576                self._test_data_for_chameleon_format, self.scale)
577
578        try:
579            with tempfile.NamedTemporaryFile(prefix='audio_') as f:
580                self._chameleon_board.host.send_file(
581                        test_data_for_chameleon.path, f.name)
582            return f.name
583        finally:
584            test_data_for_chameleon.delete()
585
586
587    def start_playback(self, path, blocking=False, **kargs):
588        """Starts playback.
589
590        @param path: The path to the file to play on Chameleon.
591        @param blocking: Blocks this call until playback finishes.
592        @param kargs: Other arguments that Chameleon doesn't support.
593
594        @raises: NotImplementedError if blocking is True.
595        """
596        if blocking:
597            raise NotImplementedError(
598                    'Blocking playback on chameleon is not supported')
599
600        self._port.start_playing_audio(
601                path, self._test_data_for_chameleon_format)
602
603
604    def stop_playback(self):
605        """Stops playback."""
606        self._port.stop_playing_audio()
607
608
609    def _find_port(self, interface):
610        """Finds a Chameleon audio port by interface(port name).
611
612        @param interface: string, the interface. e.g: LineOut.
613
614        @returns: A ChameleonPort object.
615
616        @raises: ValueError if port is not connected.
617
618        """
619        finder = chameleon_port_finder.ChameleonAudioOutputFinder(
620                self._chameleon_board)
621        chameleon_port = finder.find_port(interface)
622        if not chameleon_port:
623            raise ValueError(
624                    'Port %s is not connected to Chameleon' % interface)
625        return chameleon_port
626
627
628class ChameleonLineOutOutputWidgetHandler(ChameleonOutputWidgetHandler):
629    """
630    This class abstracts a Chameleon usb audio output widget handler.
631
632    """
633
634    _DEFAULT_DATA_FORMAT = dict(file_type='raw',
635                                sample_format='S32_LE',
636                                channel=8,
637                                rate=48000)
638
639    def set_playback_data(self, test_data):
640        """Sets data to play.
641
642        Handles scale if needed. Creates a path and sends the scaled data to
643        Chameleon at that path.
644
645        @param test_data: An AudioTestData object.
646
647        @return: The remote data path on Chameleon.
648
649        """
650        self._test_data_for_chameleon_format = self._DEFAULT_DATA_FORMAT
651        return self._scale_and_send_playback_data(test_data)
652
653
654
655class CrosWidgetHandler(WidgetHandler):
656    """
657    This class abstracts a Cros device audio widget handler.
658
659    Properties:
660        _audio_facade: An AudioFacadeRemoteAdapter to access Cros device
661                       audio functionality.
662
663    """
664    def __init__(self, audio_facade):
665        """Initializes a CrosWidgetHandler.
666
667        @param audio_facade: An AudioFacadeRemoteAdapter to access Cros device
668                             audio functionality.
669
670        """
671        self._audio_facade = audio_facade
672
673    def plug(self):
674        """Plugs this widget."""
675        logging.info('CrosWidgetHandler: plug')
676
677    def unplug(self):
678        """Unplugs this widget."""
679        logging.info('CrosWidgetHandler: unplug')
680
681
682class CrosInputWidgetHandlerError(Exception):
683    """Error in CrosInputWidgetHandler."""
684
685
686class CrosInputWidgetHandler(CrosWidgetHandler):
687    """
688    This class abstracts a Cros device audio input widget handler.
689
690    """
691    _DEFAULT_DATA_FORMAT = dict(file_type='raw',
692                                sample_format='S16_LE',
693                                channel=1,
694                                rate=48000)
695    _recording_on = None
696    _SELECTED = "Selected"
697
698    def start_recording(self, node_type=None, block_size=None):
699        """Starts recording audio.
700
701        @param node_type: A Cras node type defined in cras_utils.CRAS_NODE_TYPES
702        @param block_size: The number for frames per callback.
703
704        @raises: CrosInputWidgetHandlerError if a recording was already started.
705        """
706        if self._recording_on:
707            raise CrosInputWidgetHandlerError(
708                    "A recording was already started on %s." %
709                    self._recording_on)
710
711        self._recording_on = node_type if node_type else self._SELECTED
712        self._audio_facade.start_recording(self._DEFAULT_DATA_FORMAT, node_type,
713                                           block_size)
714
715
716    def stop_recording(self, node_type=None):
717        """Stops recording audio.
718
719        @param node_type: A Cras node type defined in cras_utils.CRAS_NODE_TYPES
720
721        @returns:
722            A tuple (remote_path, format).
723                remote_path: The path to the recorded file on Cros device.
724                format: A dict containing:
725                    file_type: 'raw'.
726                    sample_format: 'S16_LE' for 16-bit signed integer in
727                                   little-endian.
728                    channel: channel number.
729                    rate: sampling rate.
730
731        @raises: CrosInputWidgetHandlerError if no corresponding responding
732        device could be stopped.
733        """
734        if self._recording_on is None:
735            raise CrosInputWidgetHandlerError("No recording was started.")
736
737        if node_type is None and self._recording_on != self._SELECTED:
738            raise CrosInputWidgetHandlerError(
739                    "No recording on selected device.")
740
741        if node_type and node_type != self._recording_on:
742            raise CrosInputWidgetHandlerError(
743                    "No recording was started on %s." % node_type)
744
745        self._recording_on = None
746        return (self._audio_facade.stop_recording(node_type=node_type),
747                self._DEFAULT_DATA_FORMAT)
748
749
750    def get_recorded_binary(self, remote_path, record_format):
751        """Gets remote recorded file binary.
752
753        Gets and reads recorded file from Cros device.
754
755        @param remote_path: The path to the recorded file on Cros device.
756        @param record_format: The recorded data format. A dict containing
757                     file_type: 'raw' or 'wav'.
758                     sample_format: 'S32_LE' for 32-bit signed integer in
759                                    little-endian. Refer to aplay manpage for
760                                    other formats.
761                     channel: channel number.
762                     rate: sampling rate.
763
764        @returns: The recorded binary.
765
766        @raises: CrosInputWidgetHandlerError if record_format is not correct.
767        """
768        if record_format != self._DEFAULT_DATA_FORMAT:
769            raise CrosInputWidgetHandlerError(
770                    'Record format %r is not valid' % record_format)
771
772        with tempfile.NamedTemporaryFile(prefix='recorded_') as f:
773            self._audio_facade.get_recorded_file(remote_path, f.name)
774            return open(f.name).read()
775
776
777class CrosUSBInputWidgetHandler(CrosInputWidgetHandler):
778    """
779    This class abstracts a Cros device audio input widget handler.
780
781    """
782    _DEFAULT_DATA_FORMAT = dict(file_type='raw',
783                                sample_format='S16_LE',
784                                channel=2,
785                                rate=48000)
786
787
788class CrosHotwordingWidgetHandler(CrosInputWidgetHandler):
789    """
790    This class abstracts a Cros device audio input widget handler on hotwording.
791
792    """
793    _DEFAULT_DATA_FORMAT = dict(file_type='raw',
794                                sample_format='S16_LE',
795                                channel=1,
796                                rate=16000)
797
798    def __init__(self, audio_facade, system_facade):
799        """Initializes a CrosWidgetHandler.
800
801        @param audio_facade: An AudioFacadeRemoteAdapter to access Cros device
802                             audio functionality.
803        @param system_facade: A SystemFacadeRemoteAdapter to access Cros device
804                             system functionality.
805
806        """
807        super(CrosHotwordingWidgetHandler, self).__init__(
808                audio_facade)
809        self._system_facade = system_facade
810
811
812    def start_listening(self):
813        """Start listening to hotword."""
814        self._audio_facade.start_listening(self._DEFAULT_DATA_FORMAT)
815
816
817    def stop_listening(self):
818        """Stops listening to hotword."""
819        return self._audio_facade.stop_listening(), self._DEFAULT_DATA_FORMAT
820
821
822class CrosOutputWidgetHandlerError(Exception):
823    """The error in CrosOutputWidgetHandler."""
824    pass
825
826
827class CrosOutputWidgetHandler(CrosWidgetHandler):
828    """
829    This class abstracts a Cros device audio output widget handler.
830
831    """
832    _DEFAULT_DATA_FORMAT = dict(file_type='raw',
833                                sample_format='S16_LE',
834                                channel=2,
835                                rate=48000)
836
837    def set_playback_data(self, test_data):
838        """Sets data to play.
839
840        @param test_data: An AudioTestData object.
841
842        @returns: The remote file path on Cros device.
843
844        """
845        # TODO(cychiang): Do format conversion on Cros device if this is
846        # needed.
847        if test_data.data_format != self._DEFAULT_DATA_FORMAT:
848            raise CrosOutputWidgetHandlerError(
849                    'File format conversion for cros device is not supported.')
850        return self._audio_facade.set_playback_file(test_data.path)
851
852
853    def start_playback(self, path, blocking=False, node_type=None,
854                       block_size=None):
855        """Starts playing audio.
856
857        @param path: The path to the file to play on Cros device.
858        @param blocking: Blocks this call until playback finishes.
859        @param node_type: A Cras node type defined in cras_utils.CRAS_NODE_TYPES
860        @param block_size: The number for frames per callback.
861
862        """
863        self._audio_facade.playback(path, self._DEFAULT_DATA_FORMAT, blocking,
864                node_type, block_size)
865
866    def stop_playback(self):
867        """Stops playing audio."""
868        self._audio_facade.stop_playback()
869
870
871class PeripheralWidgetHandler(object):
872    """
873    This class abstracts an action handler on peripheral.
874    Currently, as there is no action to take on the peripheral speaker and mic,
875    this class serves as a place-holder.
876
877    """
878    pass
879
880
881class PeripheralWidget(AudioWidget):
882    """
883    This class abstracts a peripheral widget which only acts passively like
884    peripheral speaker or microphone, or acts transparently like bluetooth
885    module on audio board which relays the audio siganl between Chameleon board
886    and Cros device. This widget does not provide playback/record function like
887    AudioOutputWidget or AudioInputWidget. The main purpose of this class is
888    an identifier to find the correct AudioWidgetLink to do the real work.
889    """
890    pass
891