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