| #!/usr/bin/env python3 |
| # |
| # Copyright (C) 2019 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| # use this file except in compliance with the License. You may obtain a copy of |
| # the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| # License for the specific language governing permissions and limitations under |
| # the License. |
| """Stream music through connected device from phone test implementation.""" |
| import acts |
| import os |
| import shutil |
| import time |
| |
| import acts_contrib.test_utils.coex.audio_test_utils as atu |
| import acts_contrib.test_utils.bt.bt_test_utils as btutils |
| from acts import asserts |
| from acts_contrib.test_utils.abstract_devices.bluetooth_handsfree_abstract_device import BluetoothHandsfreeAbstractDeviceFactory as bt_factory |
| from acts_contrib.test_utils.bt.BluetoothBaseTest import BluetoothBaseTest |
| |
| PHONE_MUSIC_FILE_DIRECTORY = '/sdcard/Music' |
| INIT_ATTEN = 0 |
| WAIT_TIME = 1 |
| |
| |
| class A2dpBaseTest(BluetoothBaseTest): |
| """Stream audio file over desired Bluetooth codec configurations. |
| |
| Audio file should be a sine wave. Other audio files will not work for the |
| test analysis metrics. |
| |
| Device under test is Android phone, connected to headset with a controller |
| that can generate a BluetoothHandsfreeAbstractDevice from test_utils. |
| abstract_devices.bluetooth_handsfree_abstract_device. |
| BuetoothHandsfreeAbstractDeviceFactory. |
| """ |
| def setup_class(self): |
| |
| super().setup_class() |
| self.dut = self.android_devices[0] |
| req_params = ['audio_params', 'music_files'] |
| #'audio_params' is a dict, contains the audio device type, audio streaming |
| #settings such as volumn, duration, audio recording parameters such as |
| #channel, sampling rate/width, and thdn parameters for audio processing |
| self.unpack_userparams(req_params) |
| # Find music file and push it to the dut |
| music_src = self.music_files[0] |
| music_dest = PHONE_MUSIC_FILE_DIRECTORY |
| success = self.dut.push_system_file(music_src, music_dest) |
| if success: |
| self.music_file = os.path.join(PHONE_MUSIC_FILE_DIRECTORY, |
| os.path.basename(music_src)) |
| # Initialize media_control class |
| self.media = btutils.MediaControlOverSl4a(self.dut, self.music_file) |
| # Set attenuator to minimum attenuation |
| if hasattr(self, 'attenuators'): |
| self.attenuator = self.attenuators[0] |
| self.attenuator.set_atten(INIT_ATTEN) |
| # Create the BTOE(Bluetooth-Other-End) device object |
| bt_devices = self.user_params.get('bt_devices', []) |
| if bt_devices: |
| attr, idx = bt_devices.split(':') |
| self.bt_device_controller = getattr(self, attr)[int(idx)] |
| self.bt_device = bt_factory().generate(self.bt_device_controller) |
| else: |
| self.log.error('No BT devices config is provided!') |
| |
| def teardown_class(self): |
| |
| super().teardown_class() |
| if hasattr(self, 'media'): |
| self.media.stop() |
| if hasattr(self, 'attenuator'): |
| self.attenuator.set_atten(INIT_ATTEN) |
| self.dut.droid.bluetoothFactoryReset() |
| self.bt_device.reset() |
| self.bt_device.power_off() |
| btutils.disable_bluetooth(self.dut.droid) |
| |
| def setup_test(self): |
| |
| super().setup_test() |
| # Initialize audio capture devices |
| self.audio_device = atu.get_audio_capture_device( |
| self.bt_device_controller, self.audio_params) |
| # Reset BT to factory defaults |
| self.dut.droid.bluetoothFactoryReset() |
| self.bt_device.reset() |
| self.bt_device.power_on() |
| btutils.enable_bluetooth(self.dut.droid, self.dut.ed) |
| btutils.connect_phone_to_headset(self.dut, self.bt_device, 60) |
| vol = self.dut.droid.getMaxMediaVolume() * self.audio_params['volume'] |
| self.dut.droid.setMediaVolume(0) |
| time.sleep(1) |
| self.dut.droid.setMediaVolume(int(vol)) |
| |
| def teardown_test(self): |
| |
| super().teardown_test() |
| self.dut.droid.bluetoothFactoryReset() |
| self.media.stop() |
| # Set Attenuator to the initial attenuation |
| if hasattr(self, 'attenuator'): |
| self.attenuator.set_atten(INIT_ATTEN) |
| self.bt_device.reset() |
| self.bt_device.power_off() |
| btutils.disable_bluetooth(self.dut.droid) |
| |
| def play_and_record_audio(self, duration): |
| """Play and record audio for a set duration. |
| |
| Args: |
| duration: duration in seconds for music playing |
| Returns: |
| audio_captured: captured audio file path |
| """ |
| |
| self.log.info('Play and record audio for {} second'.format(duration)) |
| self.media.play() |
| proc = self.audio_device.start() |
| time.sleep(duration + WAIT_TIME) |
| proc.kill() |
| time.sleep(WAIT_TIME) |
| proc.kill() |
| audio_captured = self.audio_device.stop() |
| self.media.stop() |
| self.log.info('Audio play and record stopped') |
| asserts.assert_true(audio_captured, 'Audio not recorded') |
| return audio_captured |
| |
| def _get_bt_link_metrics(self): |
| """Get bt link metrics such as rssi and tx pwls. |
| |
| Returns: |
| rssi_master: master rssi |
| pwl_master: master tx pwl |
| rssi_slave: slave rssi |
| """ |
| |
| self.media.play() |
| # Get master rssi and power level |
| rssi_master = btutils.get_bt_metric(self.dut)['rssi'] |
| pwl_master = btutils.get_bt_metric(self.dut)['pwlv'] |
| # Get slave rssi if possible |
| if isinstance(self.bt_device_controller, |
| acts.controllers.android_device.AndroidDevice): |
| rssi_slave = btutils.get_bt_rssi(self.bt_device_controller) |
| else: |
| rssi_slave = None |
| self.media.stop() |
| return [rssi_master, pwl_master, rssi_slave] |
| |
| def run_thdn_analysis(self, audio_captured, tag): |
| """Calculate Total Harmonic Distortion plus Noise for latest recording. |
| |
| Store result in self.metrics. |
| |
| Args: |
| audio_captured: the captured audio file |
| Returns: |
| thdn: thdn value in a list |
| """ |
| # Calculate Total Harmonic Distortion + Noise |
| audio_result = atu.AudioCaptureResult(audio_captured, |
| self.audio_params) |
| thdn = audio_result.THDN(**self.audio_params['thdn_params']) |
| file_name = tag + os.path.basename(audio_result.path) |
| file_new = os.path.join(os.path.dirname(audio_result.path), file_name) |
| shutil.copyfile(audio_result.path, file_new) |
| for ch_no, t in enumerate(thdn): |
| self.log.info('THD+N for channel %s: %.4f%%' % (ch_no, t * 100)) |
| return thdn |
| |
| def run_anomaly_detection(self, audio_captured): |
| """Detect anomalies in latest recording. |
| |
| Store result in self.metrics. |
| |
| Args: |
| audio_captured: the captured audio file |
| Returns: |
| anom: anom detected in the captured file |
| """ |
| # Detect Anomalies |
| audio_result = atu.AudioCaptureResult(audio_captured) |
| anom = audio_result.detect_anomalies( |
| **self.audio_params['anomaly_params']) |
| num_anom = 0 |
| for ch_no, anomalies in enumerate(anom): |
| if anomalies: |
| for anomaly in anomalies: |
| num_anom += 1 |
| start, end = anomaly |
| self.log.warning( |
| 'Anomaly on channel {} at {}:{}. Duration ' |
| '{} sec'.format(ch_no, start // 60, start % 60, |
| end - start)) |
| else: |
| self.log.info('%i anomalies detected.' % num_anom) |
| return anom |