blob: 39543a3e35f70a53cb08d42f6a3f1a80885a8b98 [file] [log] [blame]
Mark De Ruyterc5ffb422019-01-22 15:46:39 -08001#!/usr/bin/env python3
global edgec044d5b2018-10-12 17:17:30 +05302#
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
17import logging
Qic8f9e102020-01-02 10:50:39 -080018import numpy
global edgec044d5b2018-10-12 17:17:30 +053019import os
Qic8f9e102020-01-02 10:50:39 -080020import scipy.io.wavfile as sciwav
global edgec044d5b2018-10-12 17:17:30 +053021
Xianyuan Jia63751fb2020-11-17 00:07:40 +000022from acts_contrib.test_utils.coex.audio_capture_device import AudioCaptureBase
23from acts_contrib.test_utils.coex.audio_capture_device import CaptureAudioOverAdb
24from acts_contrib.test_utils.coex.audio_capture_device import CaptureAudioOverLocal
25from acts_contrib.test_utils.audio_analysis_lib import audio_analysis
26from acts_contrib.test_utils.audio_analysis_lib.check_quality import quality_analysis
global edgec044d5b2018-10-12 17:17:30 +053027
Aidan Holloway-bidwell70c902d2019-01-16 11:49:26 -080028ANOMALY_DETECTION_BLOCK_SIZE = audio_analysis.ANOMALY_DETECTION_BLOCK_SIZE
29ANOMALY_GROUPING_TOLERANCE = audio_analysis.ANOMALY_GROUPING_TOLERANCE
30PATTERN_MATCHING_THRESHOLD = audio_analysis.PATTERN_MATCHING_THRESHOLD
Aidan Holloway-bidwell07a759c2018-11-30 16:52:55 -080031ANALYSIS_FILE_TEMPLATE = "audio_analysis_%s.txt"
global edgec044d5b2018-10-12 17:17:30 +053032bits_per_sample = 32
33
34
sairam0a4627402019-11-19 17:33:27 -080035def get_audio_capture_device(ad, audio_params):
sairamcd7a86c2019-09-18 11:30:25 -070036 """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:
sairam0a4627402019-11-19 17:33:27 -080043 ad: Android Device object.
44 audio_params: object containing variables to record audio.
sairamcd7a86c2019-09-18 11:30:25 -070045
46 Returns:
47 Object of the audio capture device.
48
49 Raises:
50 ValueError if audio_params['type'] is not "AndroidDevice" or
51 "Local".
sairamcd7a86c2019-09-18 11:30:25 -070052 """
sairamcd7a86c2019-09-18 11:30:25 -070053
54 if audio_params['type'] == 'AndroidDevice':
sairam0a4627402019-11-19 17:33:27 -080055 return CaptureAudioOverAdb(ad, audio_params)
56
sairamcd7a86c2019-09-18 11:30:25 -070057 elif audio_params['type'] == 'Local':
58 return CaptureAudioOverLocal(audio_params)
sairam0a4627402019-11-19 17:33:27 -080059
sairamcd7a86c2019-09-18 11:30:25 -070060 else:
61 raise ValueError('Unrecognized audio capture device '
62 '%s' % audio_params['type'])
63
64
gobaledge71a8e5b2019-02-28 18:54:44 +053065class FileNotFound(Exception):
66 """Raises Exception if file is not present"""
67
sairamcd7a86c2019-09-18 11:30:25 -070068
sairamdd9a0752019-12-16 06:35:27 -080069class AudioCaptureResult(AudioCaptureBase):
Qic8f9e102020-01-02 10:50:39 -080070 def __init__(self, path, audio_params=None):
sairamdd9a0752019-12-16 06:35:27 -080071 """Initializes Audio Capture Result class.
Globaledge731dd672019-03-14 17:02:06 +053072
73 Args:
sairamdd9a0752019-12-16 06:35:27 -080074 path: Path of audio capture result.
Globaledge731dd672019-03-14 17:02:06 +053075 """
sairamdd9a0752019-12-16 06:35:27 -080076 super().__init__()
77 self.path = path
Qic8f9e102020-01-02 10:50:39 -080078 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 edgec044d5b2018-10-12 17:17:30 +053083
Aidan Holloway-bidwell5d55c132019-01-18 17:53:23 -080084 def THDN(self, win_size=None, step_size=None, q=1, freq=None):
Aidan Holloway-bidwell07a759c2018-11-30 16:52:55 -080085 """Calculate THD+N value for most recently recorded file.
Aidan Holloway-bidwell5d55c132019-01-18 17:53:23 -080086
Aidan Holloway-bidwell07a759c2018-11-30 16:52:55 -080087 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-bidwell5d55c132019-01-18 17:53:23 -080095 freq: the fundamental frequency to remove from the signal. If none,
96 the fundamental frequency will be determined using FFT.
Aidan Holloway-bidwell07a759c2018-11-30 16:52:55 -080097 Returns:
Aidan Holloway-bidwell5d55c132019-01-18 17:53:23 -080098 channel_results (list): THD+N value for each channel's signal.
99 List index corresponds to channel index.
Aidan Holloway-bidwell07a759c2018-11-30 16:52:55 -0800100 """
Aidan Holloway-bidwell5d55c132019-01-18 17:53:23 -0800101 if not (win_size and step_size):
sairamdd9a0752019-12-16 06:35:27 -0800102 return audio_analysis.get_file_THDN(filename=self.path,
Aidan Holloway-bidwell5d55c132019-01-18 17:53:23 -0800103 q=q,
104 freq=freq)
Aidan Holloway-bidwell07a759c2018-11-30 16:52:55 -0800105 else:
sairamdd9a0752019-12-16 06:35:27 -0800106 return audio_analysis.get_file_max_THDN(filename=self.path,
Aidan Holloway-bidwell5d55c132019-01-18 17:53:23 -0800107 step_size=step_size,
108 window_size=win_size,
109 q=q,
110 freq=freq)
Aidan Holloway-bidwell07a759c2018-11-30 16:52:55 -0800111
Qic8f9e102020-01-02 10:50:39 -0800112 def detect_anomalies(self,
113 freq=None,
Aidan Holloway-bidwell5d55c132019-01-18 17:53:23 -0800114 block_size=ANOMALY_DETECTION_BLOCK_SIZE,
Aidan Holloway-bidwell70c902d2019-01-16 11:49:26 -0800115 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-bidwell5d55c132019-01-18 17:53:23 -0800124 freq (int|float): fundamental frequency of the signal. All other
125 frequencies are noise. If None, will be calculated with FFT.
Aidan Holloway-bidwell70c902d2019-01-16 11:49:26 -0800126 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-bidwell5d55c132019-01-18 17:53:23 -0800133 channel_results (list): anomaly durations for each channel's signal.
134 List index corresponds to channel index.
Aidan Holloway-bidwell70c902d2019-01-16 11:49:26 -0800135 """
Qic8f9e102020-01-02 10:50:39 -0800136 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-bidwell70c902d2019-01-16 11:49:26 -0800141
sairamdd9a0752019-12-16 06:35:27 -0800142 @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-bidwelle5882a52019-08-14 12:27:29 -0700149
Qic8f9e102020-01-02 10:50:39 -0800150 def audio_quality_analysis(self):
gobaledge71a8e5b2019-02-28 18:54:44 +0530151 """Measures audio quality based on the audio file given as input.
152
gobaledge71a8e5b2019-02-28 18:54:44 +0530153 Returns:
154 analysis_path on success.
155 """
sairamdd9a0752019-12-16 06:35:27 -0800156 analysis_path = self.analysis_path % self.analysis_fileno
157 if not os.path.exists(self.path):
gobaledge71a8e5b2019-02-28 18:54:44 +0530158 raise FileNotFound("Recorded file not found")
global edgec044d5b2018-10-12 17:17:30 +0530159 try:
Qic8f9e102020-01-02 10:50:39 -0800160 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 edgec044d5b2018-10-12 17:17:30 +0530166 except Exception as err:
167 logging.exception("Failed to analyze raw audio: %s" % err)
168 return analysis_path
gobaledge71a8e5b2019-02-28 18:54:44 +0530169
Qic8f9e102020-01-02 10:50:39 -0800170 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)