OLD | NEW |
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 Loading... |
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') |
OLD | NEW |