Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(67)

Side by Side Diff: modules/audio_processing/test/py_quality_assessment/quality_assessment/eval_scores.py

Issue 3010413002: Total Harmonic Distorsion plus noise (THD+n) score in APM-QA. (Closed)
Patch Set: merge Created 3 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. 1 # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
2 # 2 #
3 # Use of this source code is governed by a BSD-style license 3 # Use of this source code is governed by a BSD-style license
4 # that can be found in the LICENSE file in the root of the source 4 # that can be found in the LICENSE file in the root of the source
5 # tree. An additional intellectual property rights grant can be found 5 # tree. An additional intellectual property rights grant can be found
6 # in the file PATENTS. All contributing project authors may 6 # in the file PATENTS. All contributing project authors may
7 # be found in the AUTHORS file in the root of the source tree. 7 # be found in the AUTHORS file in the root of the source tree.
8 8
9 """Evaluation score abstract class and implementations. 9 """Evaluation score abstract class and implementations.
10 """ 10 """
11 11
12 from __future__ import division 12 from __future__ import division
13 import logging 13 import logging
14 import os 14 import os
15 import re 15 import re
16 import subprocess 16 import subprocess
17 import sys
18
19 try:
20 import numpy as np
21 except ImportError:
22 logging.critical('Cannot import the third-party Python package numpy')
23 sys.exit(1)
17 24
18 from . import data_access 25 from . import data_access
19 from . import exceptions 26 from . import exceptions
20 from . import signal_processing 27 from . import signal_processing
21 28
22 29
23 class EvaluationScore(object): 30 class EvaluationScore(object):
24 31
25 NAME = None 32 NAME = None
26 REGISTERED_CLASSES = {} 33 REGISTERED_CLASSES = {}
27 34
28 def __init__(self, score_filename_prefix): 35 def __init__(self, score_filename_prefix):
29 self._score_filename_prefix = score_filename_prefix 36 self._score_filename_prefix = score_filename_prefix
37 self._input_signal_metadata = None
30 self._reference_signal = None 38 self._reference_signal = None
31 self._reference_signal_filepath = None 39 self._reference_signal_filepath = None
32 self._tested_signal = None 40 self._tested_signal = None
33 self._tested_signal_filepath = None 41 self._tested_signal_filepath = None
34 self._output_filepath = None 42 self._output_filepath = None
35 self._score = None 43 self._score = None
36 44
37 @classmethod 45 @classmethod
38 def RegisterClass(cls, class_to_register): 46 def RegisterClass(cls, class_to_register):
39 """Registers an EvaluationScore implementation. 47 """Registers an EvaluationScore implementation.
40 48
41 Decorator to automatically register the classes that extend EvaluationScore. 49 Decorator to automatically register the classes that extend EvaluationScore.
42 Example usage: 50 Example usage:
43 51
44 @EvaluationScore.RegisterClass 52 @EvaluationScore.RegisterClass
45 class AudioLevelScore(EvaluationScore): 53 class AudioLevelScore(EvaluationScore):
46 pass 54 pass
47 """ 55 """
48 cls.REGISTERED_CLASSES[class_to_register.NAME] = class_to_register 56 cls.REGISTERED_CLASSES[class_to_register.NAME] = class_to_register
49 return class_to_register 57 return class_to_register
50 58
51 @property 59 @property
52 def output_filepath(self): 60 def output_filepath(self):
53 return self._output_filepath 61 return self._output_filepath
54 62
55 @property 63 @property
56 def score(self): 64 def score(self):
57 return self._score 65 return self._score
58 66
67 def SetInputSignalMetadata(self, metadata):
68 """Sets input signal metadata.
69
70 Args:
71 metadata: dict instance.
72 """
73 self._input_signal_metadata = metadata
74
59 def SetReferenceSignalFilepath(self, filepath): 75 def SetReferenceSignalFilepath(self, filepath):
60 """ Sets the path to the audio track used as reference signal. 76 """Sets the path to the audio track used as reference signal.
61 77
62 Args: 78 Args:
63 filepath: path to the reference audio track. 79 filepath: path to the reference audio track.
64 """ 80 """
65 self._reference_signal_filepath = filepath 81 self._reference_signal_filepath = filepath
66 82
67 def SetTestedSignalFilepath(self, filepath): 83 def SetTestedSignalFilepath(self, filepath):
68 """ Sets the path to the audio track used as test signal. 84 """Sets the path to the audio track used as test signal.
69 85
70 Args: 86 Args:
71 filepath: path to the test audio track. 87 filepath: path to the test audio track.
72 """ 88 """
73 self._tested_signal_filepath = filepath 89 self._tested_signal_filepath = filepath
74 90
75 def Run(self, output_path): 91 def Run(self, output_path):
76 """Extracts the score for the set test data pair. 92 """Extracts the score for the set test data pair.
77 93
78 Args: 94 Args:
(...skipping 156 matching lines...) Expand 10 before | Expand all | Expand 10 after
235 data.append(re.split(r'\t+', line)) 251 data.append(re.split(r'\t+', line))
236 252
237 # Two rows expected (header and values). 253 # Two rows expected (header and values).
238 assert len(data) == 2, 'Cannot parse POLQA output' 254 assert len(data) == 2, 'Cannot parse POLQA output'
239 number_of_fields = len(data[0]) 255 number_of_fields = len(data[0])
240 assert number_of_fields == len(data[1]) 256 assert number_of_fields == len(data[1])
241 257
242 # Build and return a dictionary with field names (header) as keys and the 258 # Build and return a dictionary with field names (header) as keys and the
243 # corresponding field values as values. 259 # corresponding field values as values.
244 return {data[0][index]: data[1][index] for index in range(number_of_fields)} 260 return {data[0][index]: data[1][index] for index in range(number_of_fields)}
261
262
263 @EvaluationScore.RegisterClass
264 class TotalHarmonicDistorsionScore(EvaluationScore):
265 """Total harmonic distorsion plus noise score.
266
267 Total harmonic distorsion plus noise score.
268 See "https://en.wikipedia.org/wiki/Total_harmonic_distortion#THD.2BN".
269
270 Unit: -.
271 Ideal: 0.
272 Worst case: +inf
273 """
274
275 NAME = 'thd'
276
277 def __init__(self, score_filename_prefix):
278 EvaluationScore.__init__(self, score_filename_prefix)
279 self._input_frequency = None
280
281 def _Run(self, output_path):
282 # TODO(aleloi): Integrate changes made locally.
283 self._CheckInputSignal()
284
285 self._LoadTestedSignal()
286 if self._tested_signal.channels != 1:
287 raise exceptions.EvaluationScoreException(
288 'unsupported number of channels')
289 samples = signal_processing.SignalProcessingUtils.AudioSegmentToRawData(
290 self._tested_signal)
291
292 # Init.
293 num_samples = len(samples)
294 duration = len(self._tested_signal) / 1000.0
295 scaling = 2.0 / num_samples
296 max_freq = self._tested_signal.frame_rate / 2
297 f0_freq = float(self._input_frequency)
298 t = np.linspace(0, duration, num_samples)
299
300 # Analyze harmonics.
301 b_terms = []
302 n = 1
303 while f0_freq * n < max_freq:
304 x_n = np.sum(samples * np.sin(2.0 * np.pi * n * f0_freq * t)) * scaling
305 y_n = np.sum(samples * np.cos(2.0 * np.pi * n * f0_freq * t)) * scaling
306 b_terms.append(np.sqrt(x_n**2 + y_n**2))
307 n += 1
308
309 output_without_fundamental = samples - b_terms[0] * np.sin(
310 2.0 * np.pi * f0_freq * t)
311 distortion_and_noise = np.sqrt(np.sum(
312 output_without_fundamental**2) * np.pi * scaling)
313
314 # TODO(alessiob): Fix or remove if not needed.
315 # thd = np.sqrt(np.sum(b_terms[1:]**2)) / b_terms[0]
316
317 # TODO(alessiob): Check the range of |thd_plus_noise| and update the class
318 # docstring above if accordingly.
319 thd_plus_noise = distortion_and_noise / b_terms[0]
320
321 self._score = thd_plus_noise
322 self._SaveScore()
323
324 def _CheckInputSignal(self):
325 # Check input signal and get properties.
326 try:
327 if self._input_signal_metadata['signal'] != 'pure_tone':
328 raise exceptions.EvaluationScoreException(
329 'The THD score requires a pure tone as input signal')
330 self._input_frequency = self._input_signal_metadata['frequency']
331 if self._input_signal_metadata['test_data_gen_name'] != 'identity' or (
332 self._input_signal_metadata['test_data_gen_config'] != 'default'):
333 raise exceptions.EvaluationScoreException(
334 'The THD score cannot be used with any test data generator other '
335 'than "identity"')
336 except TypeError:
337 raise exceptions.EvaluationScoreException(
338 'The THD score requires an input signal with associated metadata')
339 except KeyError:
340 raise exceptions.EvaluationScoreException(
341 'Invalid input signal metadata to compute the THD score')
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698