def do_spectral_analysis(self, check_quality=False): """Gets the spectral_analysis result. @param check_quality: Check quality of each channel. """ self.has_data() for channel_idx in xrange(self._raw_data.channel): signal = self._raw_data.channel_data[channel_idx] max_abs = max(numpy.abs(signal)) logging.debug('Channel %d max abs signal: %f', channel_idx, max_abs) if max_abs == 0: logging.info('No data on channel %d, skip this channel', channel_idx) continue saturate_value = audio_data.get_maximum_value_from_sample_format( self._raw_data.sample_format) normalized_signal = audio_analysis.normalize_signal( signal, saturate_value) logging.debug('saturate_value: %f', saturate_value) logging.debug('max signal after normalized: %f', max(normalized_signal)) spectral = audio_analysis.spectral_analysis( normalized_signal, self._rate) logging.info('Channel %d spectral:\n%s', channel_idx, pprint.pformat(spectral)) if check_quality: quality = audio_quality_measurement.quality_measurement( signal=normalized_signal, rate=self._rate, dominant_frequency=spectral[0][0]) logging.info('Channel %d quality:\n%s', channel_idx, pprint.pformat(quality))
def testNotMeaningfulData(self): """Checks that sepectral analysis handles un-meaningful data.""" rate = 48000 length_in_secs = 0.5 samples = length_in_secs * rate noise_amplitude = audio_analysis.MEANINGFUL_RMS_THRESHOLD * 0.5 noise = numpy.random.standard_normal(samples) * noise_amplitude results = audio_analysis.spectral_analysis(noise, rate) self.assertEqual([(0, 0)], results)
def testNotMeaningfulData(self): """Checks that sepectral analysis rejects not meaningful data.""" rate = 48000 length_in_secs = 0.5 samples = length_in_secs * rate noise_amplitude = audio_analysis.MEANINGFUL_RMS_THRESHOLD * 0.5 noise = numpy.random.standard_normal(samples) * noise_amplitude with self.assertRaises(audio_analysis.RMSTooSmallError): results = audio_analysis.spectral_analysis(noise, rate)
def testSpectralAnalysisRealData(self): """This unittest checks the spectral analysis works on real data.""" binary = open('client/cros/audio/test_data/1k_2k.raw', 'r').read() data = audio_data.AudioRawData(binary, 2, 'S32_LE') saturate_value = audio_data.get_maximum_value_from_sample_format( 'S32_LE') golden_frequency = [1000, 2000] for channel in [0, 1]: normalized_signal = audio_analysis.normalize_signal( data.channel_data[channel],saturate_value) spectral = audio_analysis.spectral_analysis( normalized_signal, 48000, 0.02) logging.debug('channel %s: %s', channel, spectral) self.assertTrue(abs(spectral[0][0] - golden_frequency[channel]) < 5, 'Dominant frequency is not correct')
def testSpectralAnalysis(self): rate = 48000 length_in_secs = 0.5 freq_1 = 490.0 freq_2 = 60.0 coeff_1 = 1 coeff_2 = 0.3 samples = length_in_secs * rate noise = numpy.random.standard_normal(samples) * 0.005 x = numpy.linspace(0.0, (samples - 1) * 1.0 / rate, samples) y = (coeff_1 * numpy.sin(freq_1 * 2.0 * numpy.pi * x) + coeff_2 * numpy.sin(freq_2 * 2.0 * numpy.pi * x)) + noise results = audio_analysis.spectral_analysis(y, rate) # Results should contains # [(490, 1*k), (60, 0.3*k), (0, 0.1*k)] where 490Hz is the dominant # frequency with coefficient 1, 60Hz is the second dominant frequency # with coefficient 0.3, 0Hz is from Gaussian noise with coefficient # around 0.1. The k constant is resulted from window function. logging.debug('Results: %s', results) self.assertTrue(abs(results[0][0]-freq_1) < 1) self.assertTrue(abs(results[1][0]-freq_2) < 1) self.assertTrue( abs(results[0][1] / results[1][1] - coeff_1 / coeff_2) < 0.01)
def do_spectral_analysis(self, ignore_high_freq, check_quality, quality_params): """Gets the spectral_analysis result. @param ignore_high_freq: Ignore high frequencies above this threshold. @param check_quality: Check quality of each channel. @param quality_params: A QualityParams object for quality measurement. """ self.has_data() for channel_idx in xrange(self._raw_data.channel): signal = self._raw_data.channel_data[channel_idx] max_abs = max(numpy.abs(signal)) logging.debug('Channel %d max abs signal: %f', channel_idx, max_abs) if max_abs == 0: logging.info('No data on channel %d, skip this channel', channel_idx) continue saturate_value = audio_data.get_maximum_value_from_sample_format( self._raw_data.sample_format) normalized_signal = audio_analysis.normalize_signal( signal, saturate_value) logging.debug('saturate_value: %f', saturate_value) logging.debug('max signal after normalized: %f', max(normalized_signal)) spectral = audio_analysis.spectral_analysis( normalized_signal, self._rate) logging.debug('Channel %d spectral:\n%s', channel_idx, pprint.pformat(spectral)) # Ignore high frequencies above the threshold. spectral = [(f, c) for (f, c) in spectral if f < ignore_high_freq] logging.info( 'Channel %d spectral after ignoring high frequencies ' 'above %f:\n%s', channel_idx, ignore_high_freq, pprint.pformat(spectral)) if check_quality: quality = audio_quality_measurement.quality_measurement( signal=normalized_signal, rate=self._rate, dominant_frequency=spectral[0][0], block_size_secs=quality_params.block_size_secs, frequency_error_threshold=quality_params. frequency_error_threshold, delay_amplitude_threshold=quality_params. delay_amplitude_threshold, noise_amplitude_threshold=quality_params. noise_amplitude_threshold, burst_amplitude_threshold=quality_params. burst_amplitude_threshold) logging.debug('Channel %d quality:\n%s', channel_idx, pprint.pformat(quality)) self._quality_result.append(quality) self._spectrals.append(spectral)
def quality_measurement( signal, rate, dominant_frequency=None, block_size_secs=DEFAULT_BLOCK_SIZE_SECS, frequency_error_threshold=DEFAULT_FREQUENCY_ERROR, delay_amplitude_threshold=DEFAULT_DELAY_AMPLITUDE_THRESHOLD, noise_amplitude_threshold=DEFAULT_NOISE_AMPLITUDE_THRESHOLD, burst_amplitude_threshold=DEFAULT_BURST_AMPLITUDE_THRESHOLD, volume_changing_amplitude_threshold=DEFAULT_VOLUME_CHANGE_AMPLITUDE): """Detects several artifacts and estimates the noise level. This method detects artifact before playing, after playing, and delay during playing. Also, it estimates the noise level of the signal. To avoid the influence of noise, it calculates amplitude and frequency block by block. @param signal: A list of numbers for one-channel PCM data. The data should be normalized to [-1,1]. @param rate: Sampling rate @param dominant_frequency: Dominant frequency of signal. Set None to recalculate the frequency in this function. @param block_size_secs: Block size in seconds. The measurement will be done block-by-block using average amplitude and frequency in each block to avoid noise. @param frequency_error_threshold: Ref to DEFAULT_FREQUENCY_ERROR. @param delay_amplitude_threshold: If the average amplitude of a block is lower than average amplitude of the wave times delay_amplitude_threshold, it will be considered as delay. Also refer to delay_detection and DEFAULT_DELAY_AMPLITUDE_THRESHOLD. @param noise_amplitude_threshold: If the average amplitude of a block is higher than average amplitude of the wave times noise_amplitude_threshold, it will be considered as noise before/after playback. Also refer to noise_detection and DEFAULT_NOISE_AMPLITUDE_THRESHOLD. @param burst_amplitude_threshold: If the average amplitude of a block is higher than average amplitude of its left block and its right block times burst_amplitude_threshold. It will be considered as a burst. Also refer to burst_detection and DEFAULT_BURST_AMPLITUDE_THRESHOLD. @param volume_changing_amplitude_threshold: If the average amplitude of right block is higher or lower than that of left one times this value, it will be considered as a volume change. Also refer to changing_volume_detection and DEFAULT_VOLUME_CHANGE_AMPLITUDE @returns: A dictoinary of detection/estimation: {'artifacts': {'noise_before_playback': [(time_1, duration_1), (time_2, duration_2), ...], 'noise_after_playback': [(time_1, duration_1), (time_2, duration_2), ...], 'delay_during_playback': [(time_1, duration_1), (time_2, duration_2), ...], 'burst_during_playback': [time_1, time_2, ...] }, 'volume_changes': [(time_1, flag_1), (time_2, flag_2), ...], 'equivalent_noise_level': level } where durations and time points are in seconds. And, equivalence_noise_level is the quotient of noise and wave which refers to DEFAULT_STANDARD_NOISE. volume_changes is a list of tuples containing time stamps and decreasing/increasing flags for volume change events. """ # Calculates the block size, from seconds to samples. block_size = int(block_size_secs * rate) signal = numpy.concatenate((numpy.zeros(int(rate * APPEND_ZEROS_SECS)), signal, numpy.zeros(int(rate * APPEND_ZEROS_SECS)))) signal = numpy.array(signal, dtype=float) length = len(signal) # Calculates the amplitude and frequency. amplitude, frequency = hilbert_analysis(signal, rate, block_size) # Finds the dominant frequency. if not dominant_frequency: dominant_frequency = audio_analysis.spectral_analysis(signal, rate)[0][0] # Finds the array which contains absolute difference between dominant # frequency and frequency at each time point. frequency_delta = abs(frequency - dominant_frequency) # Computes average amplitude of each type of block res = find_block_average_value(amplitude, block_size * 2, block_size) left_block_amplitude, right_block_amplitude, block_amplitude = res # Computes average absolute difference of frequency and dominant frequency # of the block of each index _, _, block_frequency_delta = find_block_average_value(frequency_delta, block_size * 2, block_size) # Finds start and end index of sine wave. start_index, end_index = find_start_end_index(dominant_frequency, block_frequency_delta, block_size, frequency_error_threshold) if start_index > end_index: raise SineWaveNotFound('No sine wave found in signal') logging.debug('Found sine wave: start: %s, end: %s', float(start_index) / rate - APPEND_ZEROS_SECS, float(end_index) / rate - APPEND_ZEROS_SECS) sum_of_amplitude = float(sum(amplitude[start_index:end_index])) # Finds average amplitude of sine wave. average_amplitude = sum_of_amplitude / (end_index - start_index) # Finds noise before and/or after playback. noise_before_playing, noise_after_playing = noise_detection( start_index, end_index, block_amplitude, average_amplitude, rate, noise_amplitude_threshold) # Finds delay during playback. delays = delay_detection(start_index, end_index, block_amplitude, average_amplitude, dominant_frequency, rate, left_block_amplitude, right_block_amplitude, block_frequency_delta, delay_amplitude_threshold, frequency_error_threshold) # Finds burst during playback. burst_time_points = burst_detection(start_index, end_index, block_amplitude, average_amplitude, dominant_frequency, rate, left_block_amplitude, right_block_amplitude, block_frequency_delta, burst_amplitude_threshold, frequency_error_threshold) # Finds volume changing during playback. volume_changes = changing_volume_detection( start_index, end_index, average_amplitude, rate, left_block_amplitude, right_block_amplitude, volume_changing_amplitude_threshold) # Calculates the average teager value. teager_value = average_teager_value(signal[start_index:end_index], average_amplitude) # Finds out the noise level. noise = noise_level(average_amplitude, dominant_frequency, rate, teager_value) return {'artifacts': {'noise_before_playback': noise_before_playing, 'noise_after_playback': noise_after_playing, 'delay_during_playback': delays, 'burst_during_playback': burst_time_points }, 'volume_changes': volume_changes, 'equivalent_noise_level': noise }
def testEmptyData(self): """Checks that sepectral analysis rejects empty data.""" with self.assertRaises(audio_analysis.EmptyDataError): results = audio_analysis.spectral_analysis([], 100)
def check_recorded_frequency( golden_file, recorder, second_peak_ratio=_DEFAULT_SECOND_PEAK_RATIO, frequency_diff_threshold=DEFAULT_FREQUENCY_DIFF_THRESHOLD, ignore_frequencies=None, check_anomaly=False, check_artifacts=False, mute_durations=None, volume_changes=None, tolerant_noise_level=DEFAULT_TOLERANT_NOISE_LEVEL): """Checks if the recorded data contains sine tone of golden frequency. @param golden_file: An AudioTestData object that serves as golden data. @param recorder: An AudioWidget used in the test to record data. @param second_peak_ratio: The test fails when the second dominant frequency has coefficient larger than this ratio of the coefficient of first dominant frequency. @param frequency_diff_threshold: The maximum difference between estimated frequency of test signal and golden frequency. This value should be small for signal passed through line. @param ignore_frequencies: A list of frequencies to be ignored. The component in the spectral with frequency too close to the frequency in the list will be ignored. The comparison of frequencies uses frequency_diff_threshold as well. @param check_anomaly: True to check anomaly in the signal. @param check_artifacts: True to check artifacts in the signal. @param mute_durations: Each duration of mute in seconds in the signal. @param volume_changes: A list containing alternative -1 for decreasing volume and +1 for increasing volume. @param tolerant_noise_level: The maximum noise level can be tolerated @returns: A list containing tuples of (dominant_frequency, coefficient) for valid channels. Coefficient can be a measure of signal magnitude on that dominant frequency. Invalid channels where golden_channel is None are ignored. @raises error.TestFail if the recorded data does not contain sine tone of golden frequency. """ if not ignore_frequencies: ignore_frequencies = [] # Also ignore harmonics of ignore frequencies. ignore_frequencies_harmonics = [] for ignore_freq in ignore_frequencies: ignore_frequencies_harmonics += [ignore_freq * n for n in xrange(1, 4)] data_format = recorder.data_format recorded_data = audio_data.AudioRawData( binary=recorder.get_binary(), channel=data_format['channel'], sample_format=data_format['sample_format']) errors = [] dominant_spectrals = [] for test_channel, golden_channel in enumerate(recorder.channel_map): if golden_channel is None: logging.info('Skipped channel %d', test_channel) continue signal = recorded_data.channel_data[test_channel] saturate_value = audio_data.get_maximum_value_from_sample_format( data_format['sample_format']) logging.debug('Channel %d max signal: %f', test_channel, max(signal)) normalized_signal = audio_analysis.normalize_signal( signal, saturate_value) logging.debug('saturate_value: %f', saturate_value) logging.debug('max signal after normalized: %f', max(normalized_signal)) spectral = audio_analysis.spectral_analysis(normalized_signal, data_format['rate']) logging.debug('spectral: %s', spectral) if not spectral: errors.append('Channel %d: Can not find dominant frequency.' % test_channel) golden_frequency = golden_file.frequencies[golden_channel] logging.debug('Checking channel %s spectral %s against frequency %s', test_channel, spectral, golden_frequency) dominant_frequency = spectral[0][0] if (abs(dominant_frequency - golden_frequency) > frequency_diff_threshold): errors.append( 'Channel %d: Dominant frequency %s is away from golden %s' % (test_channel, dominant_frequency, golden_frequency)) if check_anomaly: detected_anomaly = audio_analysis.anomaly_detection( signal=normalized_signal, rate=data_format['rate'], freq=golden_frequency) if detected_anomaly: errors.append( 'Channel %d: Detect anomaly near these time: %s' % (test_channel, detected_anomaly)) else: logging.info( 'Channel %d: Quality is good as there is no anomaly', test_channel) if check_artifacts or mute_durations or volume_changes: result = audio_quality_measurement.quality_measurement( normalized_signal, data_format['rate'], dominant_frequency=dominant_frequency) logging.debug('Quality measurement result:\n%s', pprint.pformat(result)) if check_artifacts: if len(result['artifacts']['noise_before_playback']) > 0: errors.append( 'Channel %d: Detects artifacts before playing near' ' these time and duration: %s' % (test_channel, str(result['artifacts']['noise_before_playback']))) if len(result['artifacts']['noise_after_playback']) > 0: errors.append( 'Channel %d: Detects artifacts after playing near' ' these time and duration: %s' % (test_channel, str(result['artifacts']['noise_after_playback']))) if mute_durations: delays = result['artifacts']['delay_during_playback'] delay_durations = [] for x in delays: delay_durations.append(x[1]) mute_matched, delay_matched = longest_common_subsequence( mute_durations, delay_durations, DEFAULT_EQUIVALENT_THRESHOLD) # updated delay list new_delays = [ delays[i] for i in delay_matched if not delay_matched[i] ] result['artifacts']['delay_during_playback'] = new_delays unmatched_mutes = [ mute_durations[i] for i in mute_matched if not mute_matched[i] ] if len(unmatched_mutes) > 0: errors.append('Channel %d: Unmatched mute duration: %s' % (test_channel, unmatched_mutes)) if check_artifacts: if len(result['artifacts']['delay_during_playback']) > 0: errors.append( 'Channel %d: Detects delay during playing near' ' these time and duration: %s' % (test_channel, result['artifacts']['delay_during_playback'])) if len(result['artifacts']['burst_during_playback']) > 0: errors.append( 'Channel %d: Detects burst/pop near these time: %s' % (test_channel, result['artifacts']['burst_during_playback'])) if result['equivalent_noise_level'] > tolerant_noise_level: errors.append( 'Channel %d: noise level is higher than tolerant' ' noise level: %f > %f' % (test_channel, result['equivalent_noise_level'], tolerant_noise_level)) if volume_changes: matched = True volume_changing = result['volume_changes'] if len(volume_changing) != len(volume_changes): matched = False else: for i in xrange(len(volume_changing)): if volume_changing[i][1] != volume_changes[i]: matched = False break if not matched: errors.append( 'Channel %d: volume changing is not as expected, ' 'found changing time and events are: %s while ' 'expected changing events are %s' % (test_channel, volume_changing, volume_changes)) # Filter out the harmonics resulted from imperfect sin wave. # This list is different for different channels. harmonics = [dominant_frequency * n for n in xrange(2, 10)] def should_be_ignored(frequency): """Checks if frequency is close to any frequency in ignore list. The ignore list is harmonics of frequency to be ignored (like power noise), plus harmonics of dominant frequencies, plus DC. @param frequency: The frequency to be tested. @returns: True if the frequency should be ignored. False otherwise. """ for ignore_frequency in (ignore_frequencies_harmonics + harmonics + [0.0]): if (abs(frequency - ignore_frequency) < frequency_diff_threshold): logging.debug('Ignore frequency: %s', frequency) return True # Checks DC is small enough. for freq, coeff in spectral: if freq < _DC_FREQ_THRESHOLD and coeff > _DC_COEFF_THRESHOLD: errors.append('Channel %d: Found large DC coefficient: ' '(%f Hz, %f)' % (test_channel, freq, coeff)) # Filter out the frequencies to be ignored. spectral = [x for x in spectral if not should_be_ignored(x[0])] if len(spectral) > 1: first_coeff = spectral[0][1] second_coeff = spectral[1][1] if second_coeff > first_coeff * second_peak_ratio: errors.append( 'Channel %d: Found large second dominant frequencies: ' '%s' % (test_channel, spectral)) dominant_spectrals.append(spectral[0]) if errors: raise error.TestFail(', '.join(errors)) return dominant_spectrals
def check_recorded_frequency( golden_file, recorder, second_peak_ratio=DEFAULT_SECOND_PEAK_RATIO, frequency_diff_threshold=DEFAULT_FREQUENCY_DIFF_THRESHOLD, ignore_frequencies=None, check_anomaly=False): """Checks if the recorded data contains sine tone of golden frequency. @param golden_file: An AudioTestData object that serves as golden data. @param recorder: An AudioWidget used in the test to record data. @param second_peak_ratio: The test fails when the second dominant frequency has coefficient larger than this ratio of the coefficient of first dominant frequency. @param frequency_diff_threshold: The maximum difference between estimated frequency of test signal and golden frequency. This value should be small for signal passed through line. @param ignore_frequencies: A list of frequencies to be ignored. The component in the spectral with frequency too close to the frequency in the list will be ignored. The comparison of frequencies uses frequency_diff_threshold as well. @param check_anomaly: True to check anomaly in the signal. @raises error.TestFail if the recorded data does not contain sine tone of golden frequency. """ data_format = recorder.data_format recorded_data = audio_data.AudioRawData( binary=recorder.get_binary(), channel=data_format['channel'], sample_format=data_format['sample_format']) errors = [] for test_channel, golden_channel in enumerate(recorder.channel_map): if golden_channel is None: logging.info('Skipped channel %d', test_channel) continue signal = recorded_data.channel_data[test_channel] saturate_value = audio_data.get_maximum_value_from_sample_format( data_format['sample_format']) normalized_signal = audio_analysis.normalize_signal( signal, saturate_value) spectral = audio_analysis.spectral_analysis(normalized_signal, data_format['rate']) if not spectral: errors.append('Channel %d: Can not find dominant frequency.' % test_channel) golden_frequency = golden_file.frequencies[golden_channel] logging.debug('Checking channel %s spectral %s against frequency %s', test_channel, spectral, golden_frequency) dominant_frequency = spectral[0][0] if (abs(dominant_frequency - golden_frequency) > frequency_diff_threshold): errors.append( 'Channel %d: Dominant frequency %s is away from golden %s' % (test_channel, dominant_frequency, golden_frequency)) if check_anomaly: detected_anomaly = audio_analysis.anomaly_detection( signal=normalized_signal, rate=data_format['rate'], freq=golden_frequency) if detected_anomaly: errors.append( 'Channel %d: Detect anomaly near these time: %s' % (test_channel, detected_anomaly)) else: logging.info( 'Channel %d: Quality is good as there is no anomaly', test_channel) def should_be_ignored(frequency): """Checks if frequency is close to any frequency in ignore list. @param frequency: The frequency to be tested. @returns: True if the frequency should be ignored. False otherwise. """ for ignore_frequency in ignore_frequencies: if (abs(frequency - ignore_frequency) < frequency_diff_threshold): logging.debug('Ignore frequency: %s', frequency) return True # Filter out the frequencies to be ignored. if ignore_frequencies: spectral = [x for x in spectral if not should_be_ignored(x[0])] if len(spectral) > 1: first_coeff = spectral[0][1] second_coeff = spectral[1][1] if second_coeff > first_coeff * second_peak_ratio: errors.append( 'Channel %d: Found large second dominant frequencies: ' '%s' % (test_channel, spectral)) if errors: raise error.TestFail(', '.join(errors))