Mark De Ruyter | c5ffb42 | 2019-01-22 15:46:39 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
global edge | c044d5b | 2018-10-12 17:17:30 +0530 | [diff] [blame] | 2 | # |
| 3 | # Copyright (C) 2018 The Android Open Source Project |
| 4 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| 6 | # use this file except in compliance with the License. You may obtain a copy of |
| 7 | # the License at |
| 8 | # |
| 9 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | # |
| 11 | # Unless required by applicable law or agreed to in writing, software |
| 12 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 13 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 14 | # License for the specific language governing permissions and limitations under |
| 15 | # the License. |
| 16 | |
| 17 | import logging |
Qi | c8f9e10 | 2020-01-02 10:50:39 -0800 | [diff] [blame] | 18 | import numpy |
global edge | c044d5b | 2018-10-12 17:17:30 +0530 | [diff] [blame] | 19 | import os |
Qi | c8f9e10 | 2020-01-02 10:50:39 -0800 | [diff] [blame] | 20 | import scipy.io.wavfile as sciwav |
global edge | c044d5b | 2018-10-12 17:17:30 +0530 | [diff] [blame] | 21 | |
Xianyuan Jia | 63751fb | 2020-11-17 00:07:40 +0000 | [diff] [blame^] | 22 | from acts_contrib.test_utils.coex.audio_capture_device import AudioCaptureBase |
| 23 | from acts_contrib.test_utils.coex.audio_capture_device import CaptureAudioOverAdb |
| 24 | from acts_contrib.test_utils.coex.audio_capture_device import CaptureAudioOverLocal |
| 25 | from acts_contrib.test_utils.audio_analysis_lib import audio_analysis |
| 26 | from acts_contrib.test_utils.audio_analysis_lib.check_quality import quality_analysis |
global edge | c044d5b | 2018-10-12 17:17:30 +0530 | [diff] [blame] | 27 | |
Aidan Holloway-bidwell | 70c902d | 2019-01-16 11:49:26 -0800 | [diff] [blame] | 28 | ANOMALY_DETECTION_BLOCK_SIZE = audio_analysis.ANOMALY_DETECTION_BLOCK_SIZE |
| 29 | ANOMALY_GROUPING_TOLERANCE = audio_analysis.ANOMALY_GROUPING_TOLERANCE |
| 30 | PATTERN_MATCHING_THRESHOLD = audio_analysis.PATTERN_MATCHING_THRESHOLD |
Aidan Holloway-bidwell | 07a759c | 2018-11-30 16:52:55 -0800 | [diff] [blame] | 31 | ANALYSIS_FILE_TEMPLATE = "audio_analysis_%s.txt" |
global edge | c044d5b | 2018-10-12 17:17:30 +0530 | [diff] [blame] | 32 | bits_per_sample = 32 |
| 33 | |
| 34 | |
sairam | 0a462740 | 2019-11-19 17:33:27 -0800 | [diff] [blame] | 35 | def get_audio_capture_device(ad, audio_params): |
sairam | cd7a86c | 2019-09-18 11:30:25 -0700 | [diff] [blame] | 36 | """Gets the device object of the audio capture device connected to server. |
| 37 | |
| 38 | The audio capture device returned is specified by the audio_params |
| 39 | within user_params. audio_params must specify a "type" field, that |
| 40 | is either "AndroidDevice" or "Local" |
| 41 | |
| 42 | Args: |
sairam | 0a462740 | 2019-11-19 17:33:27 -0800 | [diff] [blame] | 43 | ad: Android Device object. |
| 44 | audio_params: object containing variables to record audio. |
sairam | cd7a86c | 2019-09-18 11:30:25 -0700 | [diff] [blame] | 45 | |
| 46 | Returns: |
| 47 | Object of the audio capture device. |
| 48 | |
| 49 | Raises: |
| 50 | ValueError if audio_params['type'] is not "AndroidDevice" or |
| 51 | "Local". |
sairam | cd7a86c | 2019-09-18 11:30:25 -0700 | [diff] [blame] | 52 | """ |
sairam | cd7a86c | 2019-09-18 11:30:25 -0700 | [diff] [blame] | 53 | |
| 54 | if audio_params['type'] == 'AndroidDevice': |
sairam | 0a462740 | 2019-11-19 17:33:27 -0800 | [diff] [blame] | 55 | return CaptureAudioOverAdb(ad, audio_params) |
| 56 | |
sairam | cd7a86c | 2019-09-18 11:30:25 -0700 | [diff] [blame] | 57 | elif audio_params['type'] == 'Local': |
| 58 | return CaptureAudioOverLocal(audio_params) |
sairam | 0a462740 | 2019-11-19 17:33:27 -0800 | [diff] [blame] | 59 | |
sairam | cd7a86c | 2019-09-18 11:30:25 -0700 | [diff] [blame] | 60 | else: |
| 61 | raise ValueError('Unrecognized audio capture device ' |
| 62 | '%s' % audio_params['type']) |
| 63 | |
| 64 | |
gobaledge | 71a8e5b | 2019-02-28 18:54:44 +0530 | [diff] [blame] | 65 | class FileNotFound(Exception): |
| 66 | """Raises Exception if file is not present""" |
| 67 | |
sairam | cd7a86c | 2019-09-18 11:30:25 -0700 | [diff] [blame] | 68 | |
sairam | dd9a075 | 2019-12-16 06:35:27 -0800 | [diff] [blame] | 69 | class AudioCaptureResult(AudioCaptureBase): |
Qi | c8f9e10 | 2020-01-02 10:50:39 -0800 | [diff] [blame] | 70 | def __init__(self, path, audio_params=None): |
sairam | dd9a075 | 2019-12-16 06:35:27 -0800 | [diff] [blame] | 71 | """Initializes Audio Capture Result class. |
Globaledge | 731dd67 | 2019-03-14 17:02:06 +0530 | [diff] [blame] | 72 | |
| 73 | Args: |
sairam | dd9a075 | 2019-12-16 06:35:27 -0800 | [diff] [blame] | 74 | path: Path of audio capture result. |
Globaledge | 731dd67 | 2019-03-14 17:02:06 +0530 | [diff] [blame] | 75 | """ |
sairam | dd9a075 | 2019-12-16 06:35:27 -0800 | [diff] [blame] | 76 | super().__init__() |
| 77 | self.path = path |
Qi | c8f9e10 | 2020-01-02 10:50:39 -0800 | [diff] [blame] | 78 | self.audio_params = audio_params |
| 79 | self.analysis_path = os.path.join(self.log_path, |
| 80 | ANALYSIS_FILE_TEMPLATE) |
| 81 | if self.audio_params: |
| 82 | self._trim_wave_file() |
global edge | c044d5b | 2018-10-12 17:17:30 +0530 | [diff] [blame] | 83 | |
Aidan Holloway-bidwell | 5d55c13 | 2019-01-18 17:53:23 -0800 | [diff] [blame] | 84 | def THDN(self, win_size=None, step_size=None, q=1, freq=None): |
Aidan Holloway-bidwell | 07a759c | 2018-11-30 16:52:55 -0800 | [diff] [blame] | 85 | """Calculate THD+N value for most recently recorded file. |
Aidan Holloway-bidwell | 5d55c13 | 2019-01-18 17:53:23 -0800 | [diff] [blame] | 86 | |
Aidan Holloway-bidwell | 07a759c | 2018-11-30 16:52:55 -0800 | [diff] [blame] | 87 | Args: |
| 88 | win_size: analysis window size (must be less than length of |
| 89 | signal). Used with step size to analyze signal piece by |
| 90 | piece. If not specified, entire signal will be analyzed. |
| 91 | step_size: number of samples to move window per-analysis. If not |
| 92 | specified, entire signal will be analyzed. |
| 93 | q: quality factor for the notch filter used to remove fundamental |
| 94 | frequency from signal to isolate noise. |
Aidan Holloway-bidwell | 5d55c13 | 2019-01-18 17:53:23 -0800 | [diff] [blame] | 95 | freq: the fundamental frequency to remove from the signal. If none, |
| 96 | the fundamental frequency will be determined using FFT. |
Aidan Holloway-bidwell | 07a759c | 2018-11-30 16:52:55 -0800 | [diff] [blame] | 97 | Returns: |
Aidan Holloway-bidwell | 5d55c13 | 2019-01-18 17:53:23 -0800 | [diff] [blame] | 98 | channel_results (list): THD+N value for each channel's signal. |
| 99 | List index corresponds to channel index. |
Aidan Holloway-bidwell | 07a759c | 2018-11-30 16:52:55 -0800 | [diff] [blame] | 100 | """ |
Aidan Holloway-bidwell | 5d55c13 | 2019-01-18 17:53:23 -0800 | [diff] [blame] | 101 | if not (win_size and step_size): |
sairam | dd9a075 | 2019-12-16 06:35:27 -0800 | [diff] [blame] | 102 | return audio_analysis.get_file_THDN(filename=self.path, |
Aidan Holloway-bidwell | 5d55c13 | 2019-01-18 17:53:23 -0800 | [diff] [blame] | 103 | q=q, |
| 104 | freq=freq) |
Aidan Holloway-bidwell | 07a759c | 2018-11-30 16:52:55 -0800 | [diff] [blame] | 105 | else: |
sairam | dd9a075 | 2019-12-16 06:35:27 -0800 | [diff] [blame] | 106 | return audio_analysis.get_file_max_THDN(filename=self.path, |
Aidan Holloway-bidwell | 5d55c13 | 2019-01-18 17:53:23 -0800 | [diff] [blame] | 107 | step_size=step_size, |
| 108 | window_size=win_size, |
| 109 | q=q, |
| 110 | freq=freq) |
Aidan Holloway-bidwell | 07a759c | 2018-11-30 16:52:55 -0800 | [diff] [blame] | 111 | |
Qi | c8f9e10 | 2020-01-02 10:50:39 -0800 | [diff] [blame] | 112 | def detect_anomalies(self, |
| 113 | freq=None, |
Aidan Holloway-bidwell | 5d55c13 | 2019-01-18 17:53:23 -0800 | [diff] [blame] | 114 | block_size=ANOMALY_DETECTION_BLOCK_SIZE, |
Aidan Holloway-bidwell | 70c902d | 2019-01-16 11:49:26 -0800 | [diff] [blame] | 115 | threshold=PATTERN_MATCHING_THRESHOLD, |
| 116 | tolerance=ANOMALY_GROUPING_TOLERANCE): |
| 117 | """Detect anomalies in most recently recorded file. |
| 118 | |
| 119 | An anomaly is defined as a sample in a recorded sine wave that differs |
| 120 | by at least the value defined by the threshold parameter from a golden |
| 121 | generated sine wave of the same amplitude, sample rate, and frequency. |
| 122 | |
| 123 | Args: |
Aidan Holloway-bidwell | 5d55c13 | 2019-01-18 17:53:23 -0800 | [diff] [blame] | 124 | freq (int|float): fundamental frequency of the signal. All other |
| 125 | frequencies are noise. If None, will be calculated with FFT. |
Aidan Holloway-bidwell | 70c902d | 2019-01-16 11:49:26 -0800 | [diff] [blame] | 126 | block_size (int): the number of samples to analyze at a time in the |
| 127 | anomaly detection algorithm. |
| 128 | threshold (float): the threshold of the correlation index to |
| 129 | determine if two sample values match. |
| 130 | tolerance (float): the sample tolerance for anomaly time values |
| 131 | to be grouped as the same anomaly |
| 132 | Returns: |
Aidan Holloway-bidwell | 5d55c13 | 2019-01-18 17:53:23 -0800 | [diff] [blame] | 133 | channel_results (list): anomaly durations for each channel's signal. |
| 134 | List index corresponds to channel index. |
Aidan Holloway-bidwell | 70c902d | 2019-01-16 11:49:26 -0800 | [diff] [blame] | 135 | """ |
Qi | c8f9e10 | 2020-01-02 10:50:39 -0800 | [diff] [blame] | 136 | return audio_analysis.get_file_anomaly_durations(filename=self.path, |
| 137 | freq=freq, |
| 138 | block_size=block_size, |
| 139 | threshold=threshold, |
| 140 | tolerance=tolerance) |
Aidan Holloway-bidwell | 70c902d | 2019-01-16 11:49:26 -0800 | [diff] [blame] | 141 | |
sairam | dd9a075 | 2019-12-16 06:35:27 -0800 | [diff] [blame] | 142 | @property |
| 143 | def analysis_fileno(self): |
| 144 | """Returns the file number to dump audio analysis results.""" |
| 145 | counter = 0 |
| 146 | while os.path.exists(self.analysis_path % counter): |
| 147 | counter += 1 |
| 148 | return counter |
Aidan Holloway-bidwell | e5882a5 | 2019-08-14 12:27:29 -0700 | [diff] [blame] | 149 | |
Qi | c8f9e10 | 2020-01-02 10:50:39 -0800 | [diff] [blame] | 150 | def audio_quality_analysis(self): |
gobaledge | 71a8e5b | 2019-02-28 18:54:44 +0530 | [diff] [blame] | 151 | """Measures audio quality based on the audio file given as input. |
| 152 | |
gobaledge | 71a8e5b | 2019-02-28 18:54:44 +0530 | [diff] [blame] | 153 | Returns: |
| 154 | analysis_path on success. |
| 155 | """ |
sairam | dd9a075 | 2019-12-16 06:35:27 -0800 | [diff] [blame] | 156 | analysis_path = self.analysis_path % self.analysis_fileno |
| 157 | if not os.path.exists(self.path): |
gobaledge | 71a8e5b | 2019-02-28 18:54:44 +0530 | [diff] [blame] | 158 | raise FileNotFound("Recorded file not found") |
global edge | c044d5b | 2018-10-12 17:17:30 +0530 | [diff] [blame] | 159 | try: |
Qi | c8f9e10 | 2020-01-02 10:50:39 -0800 | [diff] [blame] | 160 | quality_analysis(filename=self.path, |
| 161 | output_file=analysis_path, |
| 162 | bit_width=bits_per_sample, |
| 163 | rate=self.audio_params["sample_rate"], |
| 164 | channel=self.audio_params["channel"], |
| 165 | spectral_only=False) |
global edge | c044d5b | 2018-10-12 17:17:30 +0530 | [diff] [blame] | 166 | except Exception as err: |
| 167 | logging.exception("Failed to analyze raw audio: %s" % err) |
| 168 | return analysis_path |
gobaledge | 71a8e5b | 2019-02-28 18:54:44 +0530 | [diff] [blame] | 169 | |
Qi | c8f9e10 | 2020-01-02 10:50:39 -0800 | [diff] [blame] | 170 | def _trim_wave_file(self): |
| 171 | """Trim wave files. |
| 172 | |
| 173 | """ |
| 174 | original_record_file_name = 'original_' + os.path.basename(self.path) |
| 175 | original_record_file_path = os.path.join(os.path.dirname(self.path), |
| 176 | original_record_file_name) |
| 177 | os.rename(self.path, original_record_file_path) |
| 178 | fs, data = sciwav.read(original_record_file_path) |
| 179 | trim_start = self.audio_params['trim_start'] |
| 180 | trim_end = self.audio_params['trim_end'] |
| 181 | trim = numpy.array([[trim_start, trim_end]]) |
| 182 | trim = trim * fs |
| 183 | new_wave_file_list = [] |
| 184 | for elem in trim: |
| 185 | # To check start and end doesn't exceed raw data dimension |
| 186 | start_read = min(elem[0], data.shape[0] - 1) |
| 187 | end_read = min(elem[1], data.shape[0] - 1) |
| 188 | new_wave_file_list.extend(data[start_read:end_read]) |
| 189 | new_wave_file = numpy.array(new_wave_file_list) |
| 190 | |
| 191 | sciwav.write(self.path, fs, new_wave_file) |