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