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