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