1# Lint as: python2, python3 2# Copyright 2015 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 is a server side audio test using the Chameleon board.""" 7 8from __future__ import print_function 9 10import logging 11import os 12import time 13import threading 14 15from autotest_lib.client.common_lib import error 16from autotest_lib.client.cros.chameleon import audio_test_utils 17from autotest_lib.client.cros.chameleon import chameleon_audio_helper 18from autotest_lib.client.cros.chameleon import chameleon_audio_ids 19from autotest_lib.server.cros.audio import audio_test 20from autotest_lib.server.cros.multimedia import remote_facade_factory 21 22 23class audio_AudioAfterSuspend(audio_test.AudioTest): 24 """Server side audio test. 25 26 This test talks to a Chameleon board and a Cros device to verify 27 audio function of the Cros device. 28 29 """ 30 version = 1 31 DELAY_BEFORE_RECORD_SECONDS = 0.5 32 RECORD_SECONDS = 5 33 RESUME_TIMEOUT_SECS = 60 34 SHORT_WAIT = 2 35 SUSPEND_SECONDS = 40 36 37 38 def action_plug_jack(self, plug_state): 39 """Calls the audio interface API and plugs/unplugs. 40 41 @param plug_state: plug state to switch to 42 43 """ 44 logging.debug('Plugging' if plug_state else 'Unplugging') 45 jack_plugger = self.audio_board.get_jack_plugger() 46 47 # It is not required for the test to have jack plugger. 48 # We'll ignore the plug/unplug action and assume the target device 49 # is all time plugged/unplugged if there is no jack plugger. 50 if jack_plugger is None: 51 logging.debug('Jack plugger is NOT present!') 52 return 53 54 if plug_state: 55 jack_plugger.plug() 56 audio_test_utils.check_plugged_nodes_contain(self.audio_facade, 57 self.audio_nodes) 58 else: 59 jack_plugger.unplug() 60 time.sleep(self.SHORT_WAIT) 61 62 63 def action_suspend(self, suspend_time=SUSPEND_SECONDS): 64 """Calls the host method suspend. 65 66 @param suspend_time: time to suspend the device for. 67 68 """ 69 self.host.suspend(suspend_time=suspend_time) 70 71 72 def suspend_resume(self, plugged_before_suspend, plugged_after_suspend, 73 plugged_before_resume, test_case): 74 """Performs the mix of suspend/resume and plug/unplug 75 76 @param plugged_before_suspend: plug state before suspend 77 @param plugged_after_suspend: plug state after suspend 78 @param plugged_before_resume: plug state before resume 79 @param test_case: string identifying test case sequence 80 81 """ 82 83 # Suspend 84 boot_id = self.host.get_boot_id() 85 thread = threading.Thread(target=self.action_suspend) 86 thread.start() 87 try: 88 self.host.test_wait_for_sleep(3 * self.SUSPEND_SECONDS / 4) 89 except error.TestFail as ex: 90 self.errors.append("%s - %s" % (test_case, str(ex))) 91 92 # Plugged after suspended 93 self.action_plug_jack(plugged_after_suspend) 94 95 # Plugged before resumed 96 self.action_plug_jack(plugged_before_resume) 97 try: 98 self.host.test_wait_for_resume(boot_id, self.RESUME_TIMEOUT_SECS) 99 except error.TestFail as ex: 100 self.errors.append("%s - %s" % (test_case, str(ex))) 101 102 103 def check_correct_audio_node_selected(self): 104 """Checks the node selected by Cras is correct.""" 105 audio_test_utils.check_audio_nodes(self.audio_facade, self.audio_nodes) 106 107 108 def play_and_record(self, source_widget, recorder_widget): 109 """Plays and records audio 110 111 @param source_widget: widget to do the playback 112 @param recorder_widget: widget to do the recording 113 114 """ 115 audio_test_utils.dump_cros_audio_logs( 116 self.host, self.audio_facade, self.resultsdir, 117 'before_playback') 118 119 self.check_correct_audio_node_selected() 120 121 # Play, wait for some time, and then start recording. 122 # This is to avoid artifact caused by codec initialization. 123 source_widget.set_playback_data(self.golden_file) 124 logging.debug('Start playing %s', self.golden_file.path) 125 source_widget.start_playback() 126 127 time.sleep(self.DELAY_BEFORE_RECORD_SECONDS) 128 logging.debug('Start recording.') 129 recorder_widget.start_recording() 130 131 time.sleep(self.RECORD_SECONDS) 132 133 recorder_widget.stop_recording() 134 logging.debug('Stopped recording.') 135 136 audio_test_utils.dump_cros_audio_logs( 137 self.host, self.audio_facade, self.resultsdir, 138 'after_recording') 139 140 recorder_widget.read_recorded_binary() 141 142 143 def save_and_check_data(self, recorder_widget): 144 """Saves and checks the data from the recorder 145 146 @param recorder_widget: recorder widget to save data from 147 148 @returns (success, error_message): success is True if audio comparison 149 is successful, False otherwise. 150 error_message contains the error 151 message. 152 153 """ 154 recorded_file = os.path.join(self.resultsdir, "recorded.raw") 155 logging.debug('Saving recorded data to %s', recorded_file) 156 recorder_widget.save_file(recorded_file) 157 158 # Removes the beginning of recorded data. This is to avoid artifact 159 # caused by codec initialization in the beginning of 160 # recording. 161 recorder_widget.remove_head(2.0) 162 163 # Removes noise by a lowpass filter. 164 recorder_widget.lowpass_filter(self.low_pass_freq) 165 recorded_file = os.path.join(self.resultsdir, 166 "recorded_filtered.raw") 167 logging.debug('Saving filtered data to %s', recorded_file) 168 recorder_widget.save_file(recorded_file) 169 170 # Compares data by frequency and returns the result. 171 try: 172 audio_test_utils.check_recorded_frequency( 173 self.golden_file, recorder_widget, 174 second_peak_ratio=self.second_peak_ratio, 175 ignore_frequencies=self.ignore_frequencies) 176 except error.TestFail as e: 177 return (False, e) 178 179 return (True, None) 180 181 182 def run_once(self, host, audio_nodes, golden_data, 183 bind_from=None, bind_to=None, 184 source=None, recorder=None, plug_status=None, 185 blocked_boards=[]): 186 """Runs the test main workflow 187 188 @param host: A host object representing the DUT. 189 @param audio_nodes: audio nodes supposed to be selected. 190 @param golden_data: audio file and low pass filter frequency 191 the audio file should be test data defined in audio_test_data 192 @param bind_from: audio originating entity to be binded 193 should be defined in chameleon_audio_ids 194 @param bind_to: audio directed_to entity to be binded 195 should be defined in chameleon_audio_ids 196 @param source: source widget entity 197 should be defined in chameleon_audio_ids 198 @param recorder: recorder widget entity 199 should be defined in chameleon_audio_ids 200 @param plug_status: audio channel plug unplug sequence 201 @blocked_boards: boards to ignore and exit. 202 203 """ 204 if self.host.get_board().split(':')[1] in blocked_boards: 205 raise error.TestNAError('Board NOT APPLICABLE to test!') 206 if ((bind_from == chameleon_audio_ids.CrosIds.HEADPHONE or 207 bind_to == chameleon_audio_ids.CrosIds.EXTERNAL_MIC) and 208 not audio_test_utils.has_audio_jack(self.host)): 209 raise error.TestNAError( 210 'No audio jack for the DUT.' 211 ' Confirm swarming bot dimension and control file' 212 ' dependency for audio jack is matching.' 213 ' For new boards, has_audio_jack might need to be updated.' 214 ) 215 216 if (recorder == chameleon_audio_ids.CrosIds.INTERNAL_MIC and 217 not audio_test_utils.has_internal_microphone(host)): 218 return 219 220 if (source == chameleon_audio_ids.CrosIds.SPEAKER and 221 not audio_test_utils.has_internal_speaker(host)): 222 return 223 224 self.host = host 225 self.audio_nodes = audio_nodes 226 227 self.second_peak_ratio = audio_test_utils.get_second_peak_ratio( 228 source_id=source, 229 recorder_id=recorder) 230 231 self.ignore_frequencies = None 232 if (source == chameleon_audio_ids.CrosIds.SPEAKER 233 or bind_to == chameleon_audio_ids.CrosIds.EXTERNAL_MIC): 234 self.ignore_frequencies = [50, 60] 235 236 self.errors = [] 237 self.golden_file, self.low_pass_freq = golden_data 238 chameleon_board = self.host.chameleon 239 self.factory = remote_facade_factory.RemoteFacadeFactory( 240 self.host, results_dir=self.resultsdir) 241 self.audio_facade = self.factory.create_audio_facade() 242 chameleon_board.setup_and_reset(self.outputdir) 243 widget_factory = chameleon_audio_helper.AudioWidgetFactory( 244 self.factory, host) 245 246 # Two widgets are binded in the factory if necessary 247 binder_widget = None 248 bind_from_widget = None 249 bind_to_widget = None 250 if bind_from != None and bind_to != None: 251 bind_from_widget = widget_factory.create_widget(bind_from) 252 bind_to_widget = widget_factory.create_widget(bind_to) 253 binder_widget = widget_factory.create_binder(bind_from_widget, 254 bind_to_widget) 255 256 # Additional widgets that could be part of the factory 257 if source == None: 258 source_widget = bind_from_widget 259 else: 260 source_widget = widget_factory.create_widget(source) 261 if recorder == None: 262 recorder_widget = bind_to_widget 263 else: 264 recorder_widget = widget_factory.create_widget(recorder) 265 266 self.audio_board = chameleon_board.get_audio_board() 267 268 test_index = 0 269 for (plugged_before_suspend, plugged_after_suspend, plugged_before_resume, 270 plugged_after_resume) in plug_status: 271 test_index += 1 272 273 test_case = ('TEST CASE %d: %s > suspend > %s > %s > resume > %s' % 274 (test_index, 'PLUG' if plugged_before_suspend else 'UNPLUG', 275 'PLUG' if plugged_after_suspend else 'UNPLUG', 276 'PLUG' if plugged_before_resume else 'UNPLUG', 277 'PLUG' if plugged_after_resume else 'UNPLUG')) 278 logging.info(test_case) 279 280 # Plugged status before suspended 281 self.action_plug_jack(plugged_before_suspend) 282 283 self.suspend_resume(plugged_before_suspend, 284 plugged_after_suspend, 285 plugged_before_resume, 286 test_case) 287 288 # Active (plugged for external) state after resume 289 self.action_plug_jack(plugged_after_resume) 290 291 # Explicitly select the node as there is a known issue 292 # that the selected node might change after a suspension. 293 # We should remove this after the issue is addressed(crbug:987529). 294 self.audio_facade.set_selected_node_types(self.audio_nodes[0], 295 self.audio_nodes[1]) 296 297 if binder_widget != None: 298 with chameleon_audio_helper.bind_widgets(binder_widget): 299 self.play_and_record(source_widget, recorder_widget) 300 else: 301 self.play_and_record(source_widget, recorder_widget) 302 303 success, error_message = self.save_and_check_data(recorder_widget) 304 if not success: 305 self.errors.append('%s: Comparison failed: %s' % 306 (test_case, error_message)) 307 308 if self.errors: 309 raise error.TestFail('; '.join(set(self.errors))) 310