From 6e2f97a6809d339cd4da754e46e625dc451a5fb0 Mon Sep 17 00:00:00 2001 From: wantysal Date: Fri, 15 Dec 2023 15:09:20 +0100 Subject: [PATCH 01/17] [CC] import correciton --- mosqito/__init__.py | 2 ++ mosqito/sound_level_meter/noct_spectrum/noct_synthesis.py | 1 + 2 files changed, 3 insertions(+) diff --git a/mosqito/__init__.py b/mosqito/__init__.py index 625c5802..5d99164c 100644 --- a/mosqito/__init__.py +++ b/mosqito/__init__.py @@ -1,4 +1,6 @@ from mosqito.sound_level_meter.noct_spectrum.noct_spectrum import noct_spectrum +from mosqito.sound_level_meter.noct_spectrum.noct_synthesis import noct_synthesis +from mosqito.sound_level_meter.spectrum import spectrum from mosqito.sq_metrics.loudness.loudness_ecma.loudness_ecma import loudness_ecma from mosqito.sq_metrics.loudness.loudness_zwst.loudness_zwst import loudness_zwst diff --git a/mosqito/sound_level_meter/noct_spectrum/noct_synthesis.py b/mosqito/sound_level_meter/noct_spectrum/noct_synthesis.py index 4bead4e1..c4436bab 100644 --- a/mosqito/sound_level_meter/noct_spectrum/noct_synthesis.py +++ b/mosqito/sound_level_meter/noct_spectrum/noct_synthesis.py @@ -11,6 +11,7 @@ def noct_synthesis(spectrum, freqs, fmin, fmax, n=3, G=10, fr=1000): """Adapt input spectrum to nth-octave band spectrum + Convert the input spectrum to third-octave band spectrum between "fc_min" and "fc_max". Parameters From 18e1665e13c7a7b4b5539cb375ae2f7ff1a1c25f Mon Sep 17 00:00:00 2001 From: wantysal Date: Fri, 15 Dec 2023 15:10:39 +0100 Subject: [PATCH 02/17] [NF] computation of frequency band levels from a full spectrum (for SII use) Provided lower and upper limits of each frequency band, the corresponding level is computed as the log sum of the values inside --- .../band_spectrum_synthesis.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 mosqito/sound_level_meter/band_spectrum_synthesis.py diff --git a/mosqito/sound_level_meter/band_spectrum_synthesis.py b/mosqito/sound_level_meter/band_spectrum_synthesis.py new file mode 100644 index 00000000..d827e24a --- /dev/null +++ b/mosqito/sound_level_meter/band_spectrum_synthesis.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +# Standard library import +from numpy import array, concatenate, zeros, log10, power, argmin, split + +def band_spectrum_synthesis(spectrum, freqs, fmin, fmax): + """Adapt input spectrum to frequency band levels + + Convert the input spectrum to frequency band spectrum + between "fmin" and "fmax". + + Parameters + ---------- + spectrum : numpy.ndarray + amplitude rms of the one-sided spectrum of the signal, size (nperseg, nseg). + freqs : list + List of input frequency , size (nperseg) or (nperseg, nseg). + fmin : float + Min frequency band [Hz]. + fmax : float + Max frequency band [Hz]. + n : int + Number of bands pr octave. + G : int + System for specifying the exact geometric mean frequencies. + Can be base 2 or base 10. + fr : int + Reference frequency. Shall be set to 1 kHz for audible frequency + range, to 1 Hz for infrasonic range (f < 20 Hz) and to 1 MHz for + ultrasonic range (f > 31.5 kHz). + Outputs + ------- + spec : numpy.ndarray + Third octave band spectrum of signal sig [dB re.2e-5 Pa], size (nbands, nseg). + fpref : numpy.ndarray + Corresponding preferred third octave band center frequencies, size (nbands). + """ + + # Find the lower and upper index of each band + idx_low = argmin(abs(freqs[:,None] - fmin), axis=0) + idx_up = argmin(abs(freqs[:,None] - fmax), axis=0) + idx = concatenate((idx_low, [idx_up[-1]])) + + # Split the given spectrum in several bands + bands = array(split(spectrum, idx), dtype=object)[1:-1] + + # Compute the bands level + band_spectrum = zeros((len(bands))) + i = 0 + for s in bands: + band_spectrum[i] = 10*log10(sum(power(10,s/10))) + i += 1 + + return band_spectrum, (fmin+fmax)/2 + + + + + + From db76373ef4376948de1df346479188116b902978 Mon Sep 17 00:00:00 2001 From: wantysal Date: Fri, 15 Dec 2023 15:11:16 +0100 Subject: [PATCH 03/17] [CC] data from ANSI S3.5 standard for SII computation --- .../_band_procedure_data.py | 618 ++++++++++++++++++ .../speech_intelligibility/_speech_data.py | 382 +++++++++++ 2 files changed, 1000 insertions(+) create mode 100644 mosqito/sq_metrics/speech_intelligibility/_band_procedure_data.py create mode 100644 mosqito/sq_metrics/speech_intelligibility/_speech_data.py diff --git a/mosqito/sq_metrics/speech_intelligibility/_band_procedure_data.py b/mosqito/sq_metrics/speech_intelligibility/_band_procedure_data.py new file mode 100644 index 00000000..967a3ac7 --- /dev/null +++ b/mosqito/sq_metrics/speech_intelligibility/_band_procedure_data.py @@ -0,0 +1,618 @@ +from numpy import array + +def _get_critical_band_data(): + CENTER_FREQUENCIES = array( + [ + 150, + 250, + 350, + 450, + 570, + 700, + 840, + 1000, + 1170, + 1370, + 1600, + 1850, + 2150, + 2500, + 2900, + 3400, + 4000, + 4800, + 5800, + 7000, + 8500 + ] + ) + + LOWER_FREQUENCIES = array( + [ + 100, + 200, + 300, + 400, + 510, + 630, + 770, + 920, + 1080, + 1270, + 1480, + 1720, + 2000, + 2320, + 2700, + 3150, + 3700, + 4400, + 5300, + 6400, + 7700 + ] + ) + + UPPER_FREQUENCIES = array( + [ + 200, + 300, + 400, + 510, + 630, + 770, + 920, + 1080, + 1270, + 1480, + 1720, + 2000, + 2320, + 2700, + 3150, + 3700, + 4400, + 5300, + 6400, + 7700, + 9500 + ] + ) + + IMPORTANCE = array( + [ + 0.0103, + 0.0261, + 0.0419, + 0.0577, + 0.0577, + 0.0577, + 0.0577, + 0.0577, + 0.0577, + 0.0577, + 0.0577, + 0.0577, + 0.0577, + 0.0577, + 0.0577, + 0.0577, + 0.0577, + 0.0460, + 0.0343, + 0.0226, + 0.0110 + ] + ) + + STANDARD_SPEECH_SPECTRUM_NORMAL = array( + [ + 31.44, + 34.75, + 34.14, + 34.58, + 33.17, + 30.64, + 27.59, + 25.01, + 23.52, + 22.28, + 20.15, + 18.29, + 16.37, + 13.80, + 12.21, + 11.09, + 9.33, + 5.84, + 3.47, + 1.78, + -0.14 + ] + ) + + REFERENCE_INTERNAL_NOISE_SPECTRUM = array( + [ + 1.50, + -3.90, + -7.20, + -8.90, + -10.30, + -11.40, + -12.00, + -12.50, + -13.20, + -14.00, + -15.40, + -16.90, + -18.80, + -21.20, + -23.20, + -24.90, + -25.90, + -24.20, + -19.00, + -11.70, + -6.00 + ] + ) + + FREEFIELD2EARDRUM_TRANSFER_FUNCTION = array( + [ + 0.60, + 1.00, + 1.40, + 1.40, + 1.90, + 2.80, + 3.00, + 2.60, + 2.60, + 3.60, + 6.10, + 10.50, + 13.80, + 16.80, + 15.80, + 14.90, + 14.30, + 12.40, + 7.90, + 4.30, + 0.50 + ] + ) + return CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL + +def _get_equal_critical_band_data(): + CENTER_FREQUENCIES = array( + [ + 350, + 450, + 570, + 700, + 840, + 1000, + 1170, + 1370, + 1600, + 1850, + 2150, + 2500, + 2900, + 3400, + 4000, + 4800, + 5800, + ] + ) + + LOWER_FREQUENCIES = array( + [ + 300, + 400, + 510, + 630, + 770, + 920, + 1080, + 1270, + 1480, + 1720, + 2000, + 2320, + 2700, + 3150, + 3700, + 4400, + 5300, + ] + ) + + UPPER_FREQUENCIES = array( + [ + 400, + 510, + 630, + 770, + 920, + 1080, + 1270, + 1480, + 1720, + 2000, + 2320, + 2700, + 3150, + 3700, + 4400, + 5300, + 6400 + ] + ) + + IMPORTANCE = array( + [ + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + 0.0588, + ] + ) + + STANDARD_SPEECH_SPECTRUM_NORMAL = array( + [ + 34.14, + 34.58, + 33.17, + 30.64, + 27.59, + 25.01, + 23.52, + 22.28, + 20.15, + 18.29, + 16.37, + 13.80, + 12.21, + 11.09, + 9.33, + 5.84, + 3.47, + ] + ) + + + REFERENCE_INTERNAL_NOISE_SPECTRUM = array( + [ + -7.20, + -8.90, + -10.30, + -11.40, + -12.00, + -12.50, + -13.20, + -14.00, + -15.40, + -16.90, + -18.80, + -21.20, + -23.20, + -24.90, + -25.90, + -24.20, + -19.00, + ] + ) + + FREEFIELD2EARDRUM_TRANSFER_FUNCTION = array( + [ + 1.40, + 1.40, + 1.90, + 2.80, + 3.00, + 2.60, + 2.60, + 3.60, + 6.10, + 10.50, + 13.80, + 16.80, + 15.80, + 14.90, + 14.30, + 12.40, + 7.90, + ] + ) + return CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL + +def _get_octave_band_data(): + CENTER_FREQUENCIES = array( + [ + 250, + 500, + 1000, + 2000, + 4000, + 8000, + ] + ) + + LOWER_FREQUENCIES = array( + [ + 177, + 355, + 710, + 1420, + 2840, + 5680, + ] + ) + + UPPER_FREQUENCIES = array( + [ + 355, + 710, + 1420, + 2840, + 5680, + 11360, + ] + ) + + BANDWIDTH_ADJUSTEMENT = array( + [ + 22.48, + 25.48, + 28.48, + 31.48, + 34.48, + 37.48, + ] + ) + + + IMPORTANCE = array( + [ + 0.0617, + 0.1671, + 0.2373, + 0.2648, + 0.2142, + 0.0549 + ] + ) + + STANDARD_SPEECH_SPECTRUM_NORMAL = array( + [ + 34.75, + 34.27, + 25.01, + 17.32, + 9.33, + 1.13 + ] + ) + + REFERENCE_INTERNAL_NOISE_SPECTRUM = array( + [ + -3.90, + -9.70, + -12.50, + -17.70, + -25.90, + -7.10 + ] + ) + + FREEFIELD2EARDRUM_TRANSFER_FUNCTION = array( + [ + 1.00, + 1.80, + 2.60, + 12.00, + 14.30, + 1.80 + ] + ) + return CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, BANDWIDTH_ADJUSTEMENT, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL + +def _get_third_octave_band_data(): + CENTER_FREQUENCIES = array( + [ + 160, + 200, + 250, + 315, + 400, + 500, + 630, + 800, + 1000, + 1250, + 1600, + 2000, + 2500, + 3150, + 4000, + 5000, + 6300, + 8000 + ] + ) + + LOWER_FREQUENCIES = array( + [ + 141, + 178, + 224, + 282, + 355, + 447, + 562, + 708, + 891, + 1122, + 1413, + 1778, + 2239, + 2818, + 3548, + 4467, + 5623, + 7079, + ] + ) + + UPPER_FREQUENCIES = array( + [ + 178, + 224, + 282, + 355, + 447, + 562, + 708, + 891, + 1122, + 1413, + 1778, + 2239, + 2818, + 3548, + 4467, + 5623, + 7079, + 8913, + ] + ) + + BANDWIDTH_ADJUSTEMENT = array( + [ + 15.65, + 16.65, + 17.65, + 18.65, + 19.65, + 20.65, + 21.65, + 22.65, + 23.65, + 24.65, + 25.65, + 26.65, + 27.65, + 28.65, + 29.65, + 30.65, + 31.65, + 32.65, + ] + ) + + IMPORTANCE = array( + [ + 0.0083, + 0.0095, + 0.0150, + 0.0289, + 0.0440, + 0.0578, + 0.0653, + 0.0711, + 0.0818, + 0.0844, + 0.0882, + 0.0898, + 0.0868, + 0.0844, + 0.0771, + 0.0527, + 0.0364, + 0.0185, + ] + ) + + STANDARD_SPEECH_SPECTRUM_NORMAL = array ( + [ + 32.41, + 34.48, + 34.75, + 33.98, + 34.59, + 34.27, + 32.06, + 28.30, + 25.01, + 23.00, + 20.15, + 17.32, + 13.18, + 11.55, + 9.33, + 5.31, + 2.59, + 1.13 ] + ) + + REFERENCE_INTERNAL_NOISE_SPECTRUM = array( + [ + 0.60, + -1.70, + -3.90, + -6.10, + -8.20, + -9.70, + -10.80, + -11.90, + -12.50, + -13.50, + -15.40, + -17.70, + -21.20, + -24.20, + -25.90, + -23.60, + -15.80, + -7.10 + ] + ) + + FREEFIELD2EARDRUM_TRANSFER_FUNCTION = array( + [ + 0.00, + 0.50, + 1.00, + 1.40, + 1.50, + 1.80, + 2.40, + 3.10, + 2.60, + 3.00, + 6.10, + 12.00, + 16.80, + 15.00, + 14.30, + 10.70, + 6.40, + 1.80, + ] + ) + return CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, BANDWIDTH_ADJUSTEMENT, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL diff --git a/mosqito/sq_metrics/speech_intelligibility/_speech_data.py b/mosqito/sq_metrics/speech_intelligibility/_speech_data.py new file mode 100644 index 00000000..671ce96b --- /dev/null +++ b/mosqito/sq_metrics/speech_intelligibility/_speech_data.py @@ -0,0 +1,382 @@ +from numpy import array + +def _get_critical_band_speech_data(speech_level): + + if speech_level == "normal": + SPEECH_SPECTRUM = array( + [ + 31.44, + 34.75, + 34.14, + 34.58, + 33.17, + 30.64, + 27.59, + 25.01, + 23.52, + 22.28, + 20.15, + 18.29, + 16.37, + 13.80, + 12.21, + 11.09, + 9.33, + 5.84, + 3.47, + 1.78, + -0.14 + ]) + SPEECH_LEVEL = 62.35 + elif speech_level == "raised": + SPEECH_SPECTRUM = array( + [ + 34.06, + 38.98, + 38.62, + 39.84, + 39.44, + 37.99, + 35.85, + 33.86, + 32.56, + 30.91, + 28.58, + 26.37, + 24.34, + 22.35, + 21.04, + 19.56, + 16.78, + 12.14, + 9.04, + 6.36, + 3.44 + ]) + SPEECH_LEVEL = 68.34 + elif speech_level == "loud": + + SPEECH_SPECTRUM = array( + [ + 34.21, + 41.55, + 43.68, + 44.08, + 45.34, + 45.22, + 43.60, + 42.16, + 41.07, + 39.68, + 37.70, + 35.62, + 33.17, + 30.98, + 29.01, + 27.71, + 25.41, + 19.20, + 15.37, + 12.61, + 9.62 + ]) + SPEECH_LEVEL = 74.85 + elif speech_level == "shout": + SPEECH_SPECTRUM = array( + [ + 28.69, + 42.50, + 47.14, + 48.46, + 50.17, + 51.68, + 51.43, + 51.31, + 49.40, + 49.03, + 47.65, + 45.47, + 43.13, + 40.80, + 39.15, + 37.30, + 34.41, + 29.01, + 25.17, + 22.08, + 18.76 + ]) + SPEECH_LEVEL = 82.30 + else: + raise ValueError("Error: available speech levels are {'normal', 'raised', 'loud', 'shout'}") + + return SPEECH_SPECTRUM, SPEECH_LEVEL + +def _get_equal_critical_band_speech_data(speech_level): + if speech_level == "normal": + SPEECH_SPECTRUM = array( + [ + 34.14, + 34.58, + 33.17, + 30.64, + 27.59, + 25.01, + 23.52, + 22.28, + 20.15, + 18.29, + 16.37, + 13.80, + 12.21, + 11.09, + 9.33, + 5.84, + 3.47, + ] + ) + SPEECH_LEVEL = 62.35 + elif speech_level == "raised": + SPEECH_SPECTRUM = array( + [ + 38.62, + 39.84, + 39.44, + 37.99, + 35.85, + 33.86, + 32.56, + 30.91, + 28.58, + 26.37, + 24.34, + 22.35, + 21.04, + 19.56, + 16.78, + 12.14, + 9.04, + ] + ) + SPEECH_LEVEL = 68.34 + elif speech_level == "loud": + SPEECH_SPECTRUM = array( + [ + 43.68, + 44.08, + 45.34, + 45.22, + 43.60, + 42.16, + 41.07, + 39.68, + 37.70, + 35.62, + 33.17, + 30.98, + 29.01, + 27.71, + 25.41, + 19.20, + 15.37, + ] + ) + SPEECH_LEVEL = 74.85 + elif speech_level == "shout": + SPEECH_SPECTRUM = array( + [ + 47.14, + 48.46, + 50.17, + 51.68, + 51.43, + 51.31, + 49.40, + 49.03, + 47.65, + 45.47, + 43.13, + 40.80, + 39.15, + 37.30, + 34.41, + 29.01, + 25.17, + ] + ) + SPEECH_LEVEL = 82.30 + else: + raise ValueError("Error: Available speech levels are {'normal', 'raised', 'loud', 'shout'}") + + return SPEECH_SPECTRUM, SPEECH_LEVEL + +def _get_octave_band_speech_data(speech_level): + + if speech_level == "normal": + SPEECH_SPECTRUM = array( + [ + 34.75, + 34.27, + 25.01, + 17.32, + 9.33, + 1.13 + ] + ) + SPEECH_LEVEL = 62.35 + elif speech_level == "raised": + SPEECH_SPECTRUM = array( + [ + 38.98, + 40.15, + 33.86, + 25.32, + 16.78, + 5.07 + ] + ) + + SPEECH_LEVEL = 68.34 + elif speech_level == "loud": + SPEECH_SPECTRUM = array( + [ + 41.55, + 44.85, + 42.16, + 34.39, + 25.41, + 11.39 + ] + ) + SPEECH_LEVEL = 74.85 + elif speech_level == "shout": + SPEECH_SPECTRUM = array( + [ + 42.50, + 49.24, + 51.31, + 44.32, + 34.41, + 20.72 + ] + ) + SPEECH_LEVEL = 82.30 + else: + raise ValueError("Error: Available speech levels are {'normal', 'raised', 'loud', 'shout'}") + + return SPEECH_SPECTRUM, SPEECH_LEVEL + +def _get_third_octave_band_speech_data(speech_level): + if speech_level == "normal": + SPEECH_SPECTRUM = array( + [ + 32.41, + 34.48, + 34.75, + 33.98, + 34.59, + 34.27, + 32.06, + 28.30, + 25.01, + 23.00, + 20.15, + 17.32, + 13.18, + 11.55, + 9.33, + 5.31, + 2.59, + 1.13 ] + ) + SPEECH_LEVEL = 62.35 + elif speech_level == "raised": + SPEECH_SPECTRUM = array( + [ + 33.81, + 33.92, + 38.98, + 38.57, + 39.11, + 40.15, + 38.78, + 36.37, + 33.86, + 31.89, + 28.58, + 25.32, + 22.35, + 20.15, + 16.78, + 11.47, + 7.67, + 5.07 + ] + ) + SPEECH_LEVEL = 68.34 + elif speech_level == "loud": + SPEECH_SPECTRUM = array( + [ + 35.29, + 37.76, + 41.55, + 43.78, + 43.40, + 44.85, + 45.55, + 44.05, + 42.16, + 40.53, + 37.70, + 34.39, + 30.98, + 28.21, + 25.41, + 18.35, + 13.87, + 11.39, + ] + ) + SPEECH_LEVEL = 74.85 + elif speech_level == "shout": + SPEECH_SPECTRUM = array( + [ + 30.77, + 36.65, + 42.50, + 46.51, + 47.40, + 49.24, + 51.21, + 51.44, + 51.31, + 49.63, + 47.65, + 44.32, + 40.80, + 38.13, + 34.41, + 28.24, + 23.45, + 20.72, + ] + ) + SPEECH_LEVEL = 82.30 + else: + raise ValueError("Error: Available speech levels are {'normal', 'raised', 'loud', 'shout'}") + + return SPEECH_SPECTRUM, SPEECH_LEVEL + +if __name__ == "__main__": + + speech_level = "normal" + speech_spectrum, _ = _get_critical_band_speech_data(speech_level) + print(len(speech_spectrum)) + speech_spectrum, _ = _get_equal_critical_band_speech_data(speech_level) + print(len(speech_spectrum)) + speech_spectrum, _ = _get_third_octave_band_speech_data(speech_level) + print(len(speech_spectrum)) + speech_spectrum, _ = _get_octave_band_speech_data(speech_level) + print(len(speech_spectrum)) + + print('done') From 5e83a36f07420774aac868450d6b6f119bee3b33 Mon Sep 17 00:00:00 2001 From: wantysal Date: Fri, 15 Dec 2023 15:11:41 +0100 Subject: [PATCH 04/17] [NF] SII core computation from ANSI S3.5 --- .../speech_intelligibility/_main_sii.py | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 mosqito/sq_metrics/speech_intelligibility/_main_sii.py diff --git a/mosqito/sq_metrics/speech_intelligibility/_main_sii.py b/mosqito/sq_metrics/speech_intelligibility/_main_sii.py new file mode 100644 index 00000000..1f085911 --- /dev/null +++ b/mosqito/sq_metrics/speech_intelligibility/_main_sii.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- + + +from numpy import array, zeros, log10, maximum, where, sum + +from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data +from mosqito.sq_metrics.speech_intelligibility._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data +from mosqito.utils.LTQ import LTQ +from mosqito.utils.conversion import freq2bark + + +def _main_sii(method, speech_spectrum, noise_spectrum, threshold): + """Calculate core speech intelligibility index + + This function computes SII values based on ANSI S3.5 standard. + + Parameters + ---------- + method: {"critical", "equally_critical", "third_octave", "octave"} + Type of frequency band to be used for the calculation. + speech_spectrum : array_like + Speech spectrum [dB ref. 2e-5 Pa] with same size as the chosen method frequency axis. + noise_spectrum : array_like + Noise spectrum [dB ref. 2e-5 Pa] with same size as the chosen method frequency axis. + threshold : array_like or 'zwicker' + Threshold of hearing [dB ref. 2e-5 Pa] with same size as the chosen method frequency axis, or 'zwicker' to use the standard threshold. + Default to None sets the threshold to zeros on each frequency band. + + Returns + ------- + sii: numpy.ndarray + Overall SII value. + specific_sii: numpy.ndarray + Specific SII values along the frequency axis. + freq_axis: numpy.ndarray + Frequency axis corresponding to the chosen method. + """ + + if (method!='critical') & (method!='equally_critical') & (method!='third_octave') & (method!='octave'): + raise ValueError('Method should be within {"critical", "equally_critical", "third_octave", "octave"}.') + + # Get band data according to the chosen method + if method == 'critical': + CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL = _get_critical_band_data() + elif method == 'equally_critical': + CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL = _get_equal_critical_band_data() + elif method == 'third_octave': + CENTER_FREQUENCIES, _, _, BANDWIDTH_ADJUSTEMENT, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL = _get_third_octave_band_data() + elif method == 'octave': + CENTER_FREQUENCIES, _, _, BANDWIDTH_ADJUSTEMENT, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL = _get_octave_band_data() + nbands = len(CENTER_FREQUENCIES) + + if threshold is None: + T = zeros((nbands)) + elif threshold == 'zwicker': + T = LTQ(freq2bark(CENTER_FREQUENCIES)) + else: + T = array(threshold) + + # dB bandwidth adjustement + if (method == 'critical_bands') or (method == 'equal_critical_bands'): + noise_spectrum -= 10 * log10(UPPER_FREQUENCIES - LOWER_FREQUENCIES) + elif (method == 'octave_bands') or (method == 'third_octave_bands'): + noise_spectrum -= BANDWIDTH_ADJUSTEMENT + + + if method == 'octave': + Z = noise_spectrum + else: + V = speech_spectrum - 24 + B = maximum(noise_spectrum, V) + if method == 'third_octave': + C = -80 + 0.6 * (B + 10*log10(CENTER_FREQUENCIES)-6.353) + Z = zeros((nbands)) + for i in range(nbands): + s = 0 + for k in range(i-1): + s += 10**(0.1*B[k] + 3.32 * C[k] * log10(0.89 * CENTER_FREQUENCIES[i] / CENTER_FREQUENCIES[k])) + Z[i] = 10 * log10(10**(0.1*noise_spectrum[i]) + s) + else: + C = -80 + 0.6 * (B + 10*log10(UPPER_FREQUENCIES - LOWER_FREQUENCIES)) + Z = zeros((nbands)) + for i in range(nbands): + s = 0 + for k in range(i-1): + s += 10**(0.1*B[k] + 3.32 * C[k] * log10(CENTER_FREQUENCIES[i] / UPPER_FREQUENCIES[k])) + + Z[i] = 10 * log10(10**(0.1*noise_spectrum[i]) + s) + # 4.3.2.4 + Z[0] = B[0] + + # STEP 4 + X = REFERENCE_INTERNAL_NOISE_SPECTRUM + T + + # STEP 5 + D = maximum(Z, X) + + # STEP 6 + L = 1 - (speech_spectrum - STANDARD_SPEECH_SPECTRUM_NORMAL ) + L[where(L>1)] = 1 + + # STEP 7 + K = (speech_spectrum - D + 15)/30 + K[where(K>1)] = 1 + K[where(K<0)] = 0 + A = L * K + + # STEP 8 + SII = sum(IMPORTANCE * A) + SII_specific = A + + return SII, SII_specific, CENTER_FREQUENCIES + + From 1d15b87f26efe15628b499d6a2668c57c175b676 Mon Sep 17 00:00:00 2001 From: wantysal Date: Fri, 15 Dec 2023 15:12:12 +0100 Subject: [PATCH 05/17] [NF] function to compute SII for a time signal --- .../sq_metrics/speech_intelligibility/sii.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 mosqito/sq_metrics/speech_intelligibility/sii.py diff --git a/mosqito/sq_metrics/speech_intelligibility/sii.py b/mosqito/sq_metrics/speech_intelligibility/sii.py new file mode 100644 index 00000000..a2972e99 --- /dev/null +++ b/mosqito/sq_metrics/speech_intelligibility/sii.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +from numpy import array, zeros + +from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data +from mosqito.sq_metrics.speech_intelligibility._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data +from mosqito.sq_metrics.speech_intelligibility._main_sii import _main_sii +from mosqito.sound_level_meter.spectrum import spectrum +from mosqito.sound_level_meter.band_spectrum_synthesis import band_spectrum_synthesis + + +def sii(noise, fs, method, speech_level, threshold=None): + """Calculate speech intelligibility index + + This function computes SII values for a noise time signal according to ANSI S3.5 standard. + + Parameters + ---------- + noise : array_like + Noise time signal in [Pa]. + fs: float + Sampling frequency of the input noise signal. + method: {"critical", "equally_critical", "third_octave", "octave"} + Type of frequency band to be used for the calculation. + speech_level : {'normal', 'raised', 'loud', 'shout'} + Speech level to assess, the corresponding speech spectrum defined in the standard is used for calculation. + threshold : array_like or 'zwicker' + Threshold of hearing [dB ref. 2e-5 Pa] with same size as the chosen method frequency axis, or 'zwicker' to use the standard threshold. + Default to None sets the threshold to zeros on each frequency band. + + Returns + ------- + sii: numpy.ndarray + Overall SII value. + specific_sii: numpy.ndarray + Specific SII values along the frequency axis. + freq_axis: numpy.ndarray + Frequency axis corresponding to the chosen method. + + Examples + -------- + .. plot:: + :include-source: + + >>> from mosqito.sq_metrics.speech_intelligibility import sii + >>> import matplotlib.pyplot as plt + >>> import numpy as np + >>> fs=48000 + >>> d=0.2 + >>> dB=90 + >>> time = np.arange(0, d, 1/fs) + >>> f = 50 + >>> stimulus = np.sin(2 * np.pi * f * time) * np.sin(np.pi * f * time) + np.sin(10 * np.pi * f * time) + np.sin(100 * np.pi * f * time) + >>> rms = np.sqrt(np.mean(np.power(stimulus, 2))) + >>> ampl = 0.00002 * np.power(10, dB / 20) / rms + >>> stimulus = stimulus * ampl + >>> SII, SII_spec, freq_axis = sii(stimulus, fs, method='critical', speech_level='normal') + >>> plt.plot(freq_axis, SII_spec) + >>> plt.xlabel("Frequency [Hz]") + >>> plt.ylabel("Specific value ") + >>> plt.title("Speech Intelligibility Index = " + f"{SII:.2f}") + + """ + + if (method!='critical') & (method!='equally_critical') & (method!='third_octave') & (method!='octave'): + raise ValueError('Method should be within {"critical", "equally_critical", "third_octave", "octave"}.') + + if (speech_level!='normal') & (speech_level!='raised') & (speech_level!='loud') & (speech_level!='shout'): + raise ValueError('Speech level should be within {"normal", "raised", "loud", "shout"} to use the corresponding standard data.') + + # Get standard speech spectrum + if method == 'critical': + speech_spectrum, speech_level = _get_critical_band_speech_data(speech_level) + CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, _, _, _ = _get_critical_band_data() + elif method == 'equally_critical': + speech_spectrum, speech_level = _get_equal_critical_band_speech_data(speech_level) + CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, _, _, _ = _get_equal_critical_band_data() + elif method == 'third_octave': + speech_spectrum, speech_level = _get_third_octave_band_speech_data(speech_level) + CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, _, _, _, _ = _get_third_octave_band_data() + elif method == 'octave': + speech_spectrum, speech_level = _get_octave_band_speech_data(speech_level) + CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, _, _, _, _, = _get_octave_band_data() + + # Compute noise spectrum in dB + spec, freqs = spectrum(noise, fs, nfft="default", window="blackman", db=True) + noise_spectrum, _ = band_spectrum_synthesis(spec, freqs, LOWER_FREQUENCIES, UPPER_FREQUENCIES) + + SII, SII_specific, freq_axis = _main_sii(method, speech_spectrum, noise_spectrum, threshold) + + return SII, SII_specific, freq_axis + +if __name__ == "__main__": + import matplotlib.pyplot as plt + import numpy as np + fs=48000 + d=0.2 + dB=90 + time = np.arange(0, d, 1/fs) + f = 50 + stimulus = np.sin(2 * np.pi * f * time) * np.sin(np.pi * f * time) + np.sin(10 * np.pi * f * time) + np.sin(100 * np.pi * f * time) + rms = np.sqrt(np.mean(np.power(stimulus, 2))) + ampl = 0.00002 * np.power(10, dB / 20) / rms + stimulus = stimulus * ampl + SII, SII_spec, freq_axis = sii(stimulus, fs, method='critical', speech_level='normal') + plt.plot(freq_axis, SII_spec) + plt.xlabel("Frequency [Hz]") + plt.ylabel("Specific value ") + plt.title("Speech Intelligibility Index = " + f"{SII:.2f}") + + plt.show(block=True) + From 2f9dfe8cc061c6c921d4f45267ebec3330099563 Mon Sep 17 00:00:00 2001 From: wantysal Date: Fri, 15 Dec 2023 15:12:34 +0100 Subject: [PATCH 06/17] [NF] function to compute SII for a noise spectrum in dB --- .../speech_intelligibility/sii_freq.py | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 mosqito/sq_metrics/speech_intelligibility/sii_freq.py diff --git a/mosqito/sq_metrics/speech_intelligibility/sii_freq.py b/mosqito/sq_metrics/speech_intelligibility/sii_freq.py new file mode 100644 index 00000000..59a9c72e --- /dev/null +++ b/mosqito/sq_metrics/speech_intelligibility/sii_freq.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- + +from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data +from mosqito.sq_metrics.speech_intelligibility._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data +from mosqito.sq_metrics.speech_intelligibility._main_sii import _main_sii + +from mosqito.sound_level_meter.band_spectrum_synthesis import band_spectrum_synthesis + +def sii_freq(spectrum, freqs, method, speech_level, threshold=None): + """Calculate speech intelligibility index + + This function computes SII values for a noise spectrum in dB according to ANSI S3.5 standard. + + Parameters + ---------- + spectrum : array_like + Noise spectrum [dB ref. 2e-5 Pa]. + freqs: array_like + Frequency axis [Hz] of the spectrum. + method: {"critical", "equally_critical", "third_octave", "octave"} + Type of frequency band to be used for the calculation. + speech_level : {'normal', 'raised', 'loud', 'shout'} + Speech level to assess, the corresponding speech spectrum defined in the standard is used for calculation. + threshold : array_like or 'zwicker' + Threshold of hearing [dB ref. 2e-5 Pa] with same size as the chosen method frequency axis, or 'zwicker' to use the standard threshold. + Default to None sets the threshold to zeros on each frequency band. + + Returns + ------- + sii: numpy.ndarray + Overall SII value. + specific_sii: numpy.ndarray + Specific SII values along the frequency axis. + freq_axis: numpy.ndarray + Frequency axis corresponding to the chosen method. + + See also + -------- + sii : speech intelligibility with a time signal as background noise + sii_level : speech intelligibility with an overall SPL level as background noise + + Examples + -------- + .. plot:: + :include-source: + + >>> from mosqito.sq_metrics.speech_intelligibility import sii_freq + >>> from mosqito.sound_level_meter.spectrum import spectrum + >>> import matplotlib.pyplot as plt + >>> import numpy as np + >>> fs=48000 + >>> d=0.2 + >>> dB=60 + >>> time = np.arange(0, d, 1/fs) + >>> f = 50 + >>> stimulus = np.sin(2 * np.pi * f * time) * np.sin(np.pi * f * time) + np.sin(10 * np.pi * f * time) + np.sin(100 * np.pi * f * time) + >>> rms = np.sqrt(np.mean(np.power(stimulus, 2))) + >>> ampl = 0.00002 * np.power(10, dB / 20) / rms + >>> stimulus = stimulus * ampl + >>> spec, freqs = spectrum(stimulus, fs, db=False) + >>> SII, SII_spec, freq_axis = sii_freq(spec, freqs, method='critical', speech_level='normal') + >>> plt.plot(freq_axis, SII_spec) + >>> plt.xlabel("Frequency [Hz]") + >>> plt.ylabel("Specific value ") + >>> plt.title("Speech Intelligibility Index = " + f"{SII:.2f}") + + """ + + if (method!='critical') & (method!='equally_critical') & (method!='third_octave') & (method!='octave'): + raise ValueError('Method should be within {"critical", "equally_critical", "third_octave", "octave"}.') + + if (speech_level!='normal') & (speech_level!='raised') & (speech_level!='loud') & (speech_level!='shout'): + raise ValueError('Speech level should be within {"normal", "raised", "loud", "shout"} to use the corresponding standard data.') + + # Get standard speech spectrum + if method == 'critical': + speech_spectrum, speech_level = _get_critical_band_speech_data(speech_level) + CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, _, _, _ = _get_critical_band_data() + elif method == 'equally_critical': + speech_spectrum, speech_level = _get_equal_critical_band_speech_data(speech_level) + CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, _, _, _ = _get_equal_critical_band_data() + elif method == 'third_octave': + speech_spectrum, speech_level = _get_third_octave_band_speech_data(speech_level) + CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, _, _, _, _ = _get_third_octave_band_data() + elif method == 'octave': + speech_spectrum, speech_level = _get_octave_band_speech_data(speech_level) + CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, _, _, _, _, = _get_octave_band_data() + nbands = len(speech_spectrum) + + if (len(spectrum) != nbands) or (freqs != CENTER_FREQUENCIES): + noise_spectrum,_ = band_spectrum_synthesis(spectrum, freqs, LOWER_FREQUENCIES, UPPER_FREQUENCIES) + + SII, SII_specific, freq_axis = _main_sii(method, speech_spectrum, noise_spectrum, threshold) + + return SII, SII_specific, freq_axis + +if __name__ == "__main__": + from mosqito.sound_level_meter.spectrum import spectrum + import matplotlib.pyplot as plt + import numpy as np + fs=48000 + d=0.2 + dB=60 + time = np.arange(0, d, 1/fs) + f = 50 + stimulus = np.sin(2 * np.pi * f * time) + np.sin(2 * np.pi * f * time) * np.sin(np.pi * f * time) + np.sin(10 * np.pi * f * time) + rms = np.sqrt(np.mean(np.power(stimulus, 2))) + ampl = 0.00002 * np.power(10, dB / 20) / rms + stimulus = stimulus * ampl + spec, freqs = spectrum(stimulus, fs, db=False) + SII, SII_spec, freq_axis = sii_freq(spec, freqs, method='critical', speech_level='normal') + plt.plot(freq_axis, SII_spec) + plt.xlabel("Frequency band [Bark]") + plt.ylabel("Specific loudness [Sone/Bark]") + plt.title("Speech Intelligibility Index = " + f"{SII:.2f}") + + plt.show(block=True) + + From 85188bef08452b7d3e077ffde85451b4768e9e8e Mon Sep 17 00:00:00 2001 From: wantysal Date: Fri, 15 Dec 2023 15:12:58 +0100 Subject: [PATCH 07/17] [NF] function to compute SII with an overall noise level --- .../speech_intelligibility/sii_level.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 mosqito/sq_metrics/speech_intelligibility/sii_level.py diff --git a/mosqito/sq_metrics/speech_intelligibility/sii_level.py b/mosqito/sq_metrics/speech_intelligibility/sii_level.py new file mode 100644 index 00000000..84ee46d6 --- /dev/null +++ b/mosqito/sq_metrics/speech_intelligibility/sii_level.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +from numpy import ones, log10, power + +from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data +from mosqito.sq_metrics.speech_intelligibility._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data +from mosqito.sq_metrics.speech_intelligibility._main_sii import _main_sii +from mosqito.utils.LTQ import LTQ +from mosqito.utils.conversion import freq2bark + + +def sii_level(noise_level, method, speech_level, threshold=None): + """Calculate speech intelligibility index + + This function computes SII values for an overall noise level in dB according to ANSI S3.5 standard. + + Parameters + ---------- + noise_level : float + Overall noise level in [dB ref. 2e-5 Pa]. This value is used to create a uniform noise spectrum. + method: {"critical", "equally_critical", "third_octave", "octave"} + Type of frequency band to be used for the calculation. + speech_level : {'normal', 'raised', 'loud', 'shout'} + Speech level to assess, the corresponding speech spectrum defined in the standard is used for calculation. + threshold : array_like or 'zwicker' + Threshold of hearing [dB ref. 2e-5 Pa] with same size as the chosen method frequency axis, or 'zwicker' to use the standard threshold. + Default to None sets the threshold to zeros on each frequency band. + + Returns + ------- + sii: numpy.ndarray + Overall SII value. + specific_sii: numpy.ndarray + Specific SII values along the frequency axis. + freq_axis: numpy.ndarray + Frequency axis corresponding to the chosen method. + + Examples + -------- + .. plot:: + :include-source: + """ + + if (method!='critical') & (method!='equally_critical') & (method!='third_octave') & (method!='octave'): + raise ValueError('Method should be within {"critical", "equally_critical", "third_octave", "octave"}.') + + if (speech_level!='normal') & (speech_level!='raised') & (speech_level!='loud') & (speech_level!='shout'): + raise ValueError('Speech level should be within {"normal", "raised", "loud", "shout"} to use the corresponding standard data.') + + # Get standard speech spectrum + if method == 'critical': + speech_spectrum, speech_level = _get_critical_band_speech_data(speech_level) + elif method == 'equally_critical': + speech_spectrum, speech_level = _get_equal_critical_band_speech_data(speech_level) + elif method == 'third_octave': + speech_spectrum, speech_level = _get_third_octave_band_speech_data(speech_level) + elif method == 'octave': + speech_spectrum, speech_level = _get_octave_band_speech_data(speech_level) + nbands = len(speech_spectrum) + + # Create noise spectrum as uniform spectrum from overall level + band_level = 10 * log10(power(10, noise_level/10)/nbands) + noise_spectrum = ones((nbands)) * band_level + + # Compute SII + SII, SII_specific, freq_axis = _main_sii(method, speech_spectrum, noise_spectrum, threshold) + + return SII, SII_specific, freq_axis + +if __name__ == "__main__": + import matplotlib.pyplot as plt + import numpy as np + fs=48000 + d=0.2 + dB=90 + time = np.arange(0, d, 1/fs) + f = 50 + stimulus = np.sin(2 * np.pi * f * time) * np.sin(np.pi * f * time) + np.sin(10 * np.pi * f * time) + np.sin(100 * np.pi * f * time) + rms = np.sqrt(np.mean(np.power(stimulus, 2))) + ampl = 0.00002 * np.power(10, dB / 20) / rms + stimulus = stimulus * ampl + speech_level = 'raised' + SII, SII_spec, freq_axis = sii_level(60, method='critical', speech_level=speech_level, threshold='zwicker') + plt.plot(freq_axis, SII_spec) + plt.xlabel("Frequency [Hz]") + plt.ylabel("Specific value ") + plt.title("Speech Intelligibility Index = " + f"{SII:.2f} \n Speech level: " + speech_level) + + plt.show(block=True) + From 45a9842e737e573452af45efbe1f7ef795c1a58f Mon Sep 17 00:00:00 2001 From: wantysal Date: Fri, 15 Dec 2023 15:33:58 +0100 Subject: [PATCH 08/17] [WiP] validation script for sii computation --- .../speech_intelligibility/validation_sii.py | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 validations/sq_metrics/speech_intelligibility/validation_sii.py diff --git a/validations/sq_metrics/speech_intelligibility/validation_sii.py b/validations/sq_metrics/speech_intelligibility/validation_sii.py new file mode 100644 index 00000000..76895a3e --- /dev/null +++ b/validations/sq_metrics/speech_intelligibility/validation_sii.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +try: + import matplotlib.pyplot as plt +except ImportError: + raise RuntimeError( + "In order to perform this validation you need the 'matplotlib' package." + ) + +# Third party imports +import numpy as np + +# Local application imports +from mosqito.sq_metrics import sii_freq + + +# Reference values from ANSI S3.5 standard +reference = np.empty((3)) + +reference[0] = {"spectrum": "input/broadband_250.wav", + "method": "critica", + "speech_level": "normal", + "SII": 0.67, +} + + +def validation_sii(noise): + """Test function for the script sii_freq + + Test function for the script sharpness_din with .wav filesas input. + The input files are provided by DIN 45692_2009E + The compliance is assessed according to chapter 6 of the standard. + One .png compliance plot is generated. + + Parameters + ---------- + None + + Outputs + ------- + None + """ + + SII = np.zeros((len(noise))) + reference = np.zeros((len(noise))) + + for i in range(len(noise)): + # Compute SII + SII[i], _, _ = sii_freq(reference[i]["spectrum"], reference[i]["method"], reference[i]["speech_level"] ) + + # Load reference value + reference[i] = reference[i]["SII"] + + + _check_compliance(SII, reference) + + +def _check_compliance(sharpness, reference): + """Check the compliance of sharpness calc. to ANSI S3.5 + + The compliance is assessed with an absolute 1% tolerance. + One .png compliance plot is generated. + + + Parameters + ---------- + sharpness : numpy.array + computed sharpness values + reference : numpy.array + reference sharpness values + + + Outputs + ------- + tst : bool + Compliance to the reference data + """ + plt.figure() + + # Frequency bark axis + barks = np.arange(2.5, len(sharpness) + 2.5, 1) + + # Test for DIN 45692_2009E comformance (chapter 6) + S = sharpness + tstS = (S >= np.amin([reference * 0.99, reference - 0.01], axis=0)).all() and ( + S <= np.amax([reference * 1.01, reference + 0.01], axis=0) + ).all() + + # Tolerance curves definition + tol_low = np.amin([reference * 0.99, reference - 0.01], axis=0) + tol_high = np.amax([reference * 1.01, reference + 0.01], axis=0) + + # Plot tolerance curves + plt.plot( + barks, tol_low, color="red", linestyle="solid", label="tolerance", linewidth=1 + ) + plt.plot(barks, tol_high, color="red", linestyle="solid", linewidth=1) + + if tstS: + plt.text( + 0.5, + 0.5, + "Test passed ", + horizontalalignment="center", + verticalalignment="center", + transform=plt.gca().transAxes, + bbox=dict(facecolor="green", alpha=0.3), + ) + + else: + plt.text( + 0.5, + 0.5, + "Test not passed", + horizontalalignment="center", + verticalalignment="center", + transform=plt.gca().transAxes, + bbox=dict(facecolor="red", alpha=0.3), + ) + + # Plot the calculated sharpness + plt.plot(barks, sharpness, label="MOSQITO") + plt.title("Speech intelligibility index for the 3 test signals", fontsize=10) + plt.legend() + plt.xlabel("Center frequency [bark]") + plt.ylabel("Sharpness, [acum]") + + plt.savefig( + "output/" + + "validation_sii_" + + ".png", + format="png", + ) + plt.clf() + + +# test de la fonction +if __name__ == "__main__": + # generate compliance plot + validation_sii() From 2bfb731c0ea0f6f45f05d3eab6e74ac62b366815 Mon Sep 17 00:00:00 2001 From: wantysal Date: Fri, 15 Dec 2023 15:36:50 +0100 Subject: [PATCH 09/17] [CC] import SII functions from sq_metrics_folder --- mosqito/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mosqito/__init__.py b/mosqito/__init__.py index 5d99164c..e171c616 100644 --- a/mosqito/__init__.py +++ b/mosqito/__init__.py @@ -29,6 +29,10 @@ from mosqito.sq_metrics.sharpness.sharpness_din.sharpness_din_perseg import sharpness_din_perseg from mosqito.sq_metrics.sharpness.sharpness_din.sharpness_din_freq import sharpness_din_freq +from mosqito.sq_metrics.speech_intelligibility.sii import sii +from mosqito.sq_metrics.speech_intelligibility.sii_freq import sii_freq +from mosqito.sq_metrics.speech_intelligibility.sii_level import sii_level + from mosqito.sq_metrics.loudness.utils.sone_to_phon import sone_to_phon from mosqito.utils.isoclose import isoclose from mosqito.utils.load import load From d72b4d157ca40f1bdc93521f1a54382c18471022 Mon Sep 17 00:00:00 2001 From: wantysal Date: Fri, 15 Dec 2023 16:20:49 +0100 Subject: [PATCH 10/17] [WiP] started scripts to test SII implementation --- pytest.ini | 1 + .../speech_intelligibility/test_sii.py | 143 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 tests/sq_metrics/speech_intelligibility/test_sii.py diff --git a/pytest.ini b/pytest.ini index 219bb06f..0ff8bb55 100644 --- a/pytest.ini +++ b/pytest.ini @@ -12,6 +12,7 @@ markers = pr_st: marks tests related to stationary prominence ratio pr_tv: marks tests related to time-varying prominence ratio pr_freq: marks tests related to prominence ratio from a spectrum + sii: marks tests related to speech intelligibility noct_spectrum: marks tests related to n_octave spectra computation noct_synthesis: marks test related to n_octave spectra adaptation utils: marks tests related to utils functions \ No newline at end of file diff --git a/tests/sq_metrics/speech_intelligibility/test_sii.py b/tests/sq_metrics/speech_intelligibility/test_sii.py new file mode 100644 index 00000000..ab835b32 --- /dev/null +++ b/tests/sq_metrics/speech_intelligibility/test_sii.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- + +import numpy as np +from scipy.fft import fft, fftfreq + +# Optional package import +try: + import pytest +except ImportError: + raise RuntimeError( + "In order to perform the tests you need the 'pytest' package.") +try: + from SciDataTool import DataLinspace, DataTime +except ImportError: + raise RuntimeError( + "In order to handle Data objects you need the 'SciDataTool' package." + ) + +# Local application imports +from mosqito.utils import load +from mosqito.sq_metrics import sii, sii_freq, sii_level + + +@pytest.fixture +def test_signal(): + # Input signal from DIN 45692_2009E + sig, fs = load("tests/input/broadband_570.wav", wav_calib=1) + sig_dict = { + "signal": sig, + "fs": fs, + "S_din": 2.85, + } + return sig_dict + + +@pytest.mark.sii # to skip or run sharpness test +def test_sii(test_signal): + """Test function for the sharpness calculation of an audio signal + The input signals come from DIN 45692_2009E. The compliance is assessed + according to chapter 6 of the standard. + Parameters + ---------- + None + Outputs + ------- + None + """ + + # Input signal + sig = test_signal["signal"] + fs = test_signal["fs"] + + # Compute sharpness + SII, _, _ = sii(sig, fs, method, 'normal') + SII, _, _ = sii(sig, fs, method, 'raised') + SII, _, _ = sii(sig, fs, method, 'loud') + SII, _, _ = sii(sig, fs, method, 'shout') + + + + +@pytest.mark.sii # to skip or run sharpness test +def test_sii_freq(): + """Test function for the sharpness calculation of an time-varying audio signal. + + Parameters + ---------- + None + Outputs + ------- + None + """ + + # Input signal + sig = test_spectrum + + # Compute sharpness + SII, _, _ = sii(spec, freqs, method, 'normal') + SII, _, _ = sii(spec, freqs, method, 'raised') + SII, _, _ = sii(spec, freqs, method, 'loud') + SII, _, _ = sii(spec, freqs, method, 'shout') + + # Check that the value is within the desired values +/- 1% + np.testing.assert_allclose(SII, test_signal["SII"], rtol=0.05) + + + +@pytest.mark.sii +def test_sii_level(test_signal): + + # Compute sharpness + SII, _, _ = sii(60, method, 'normal') + SII, _, _ = sii(60, method, 'raised') + SII, _, _ = sii(60, method, 'loud') + SII, _, _ = sii(60, method, 'shout') + +def check_compliance(S, signal): + """Check the comppiance of loudness calc. to ISO 532-1 + + The compliance is assessed according to chapter 6 of the + standard DIN 45692_2009E. + + Parameters + ---------- + S : float + computed sharpness value + signal : dict + {"data file" : + "S" : + } + + Outputs + ------- + tst : bool + Compliance to the reference data + """ + + # Load reference value + ref = signal["S"] + + # Test for DIN 45692_2009E comformance (chapter 6) + tst = (S >= np.amax([ref * 0.95, ref - 0.05], axis=0)).all() and ( + S <= np.amin([ref * 1.05, ref + 0.05], axis=0) + ).all() + + return tst + + +# test de la fonction +if __name__ == "__main__": + # Reproduce the code from the fixture + # Input signal from ANSI S3.5 + sig, fs = load("tests/input/broadband_570.wav", wav_calib=1) + test_signal = { + "signal": sig, + "fs": fs, + "S_din": 2.85, + } + test_spectrum = [] + + test_sii(test_signal) + test_sii_freq(test_spectrum) + test_sii_level() From 436fe4b79fe144e5a45aa2354937773dcbfd2bd8 Mon Sep 17 00:00:00 2001 From: wantysal Date: Tue, 19 Dec 2023 17:11:08 +0100 Subject: [PATCH 11/17] [CC] validation plot running --- .../speech_intelligibility/_main_sii.py | 6 +- .../output/validation_sii.png | Bin 0 -> 48067 bytes .../speech_intelligibility/validation_sii.py | 87 +++++------------- 3 files changed, 28 insertions(+), 65 deletions(-) create mode 100644 validations/sq_metrics/speech_intelligibility/output/validation_sii.png diff --git a/mosqito/sq_metrics/speech_intelligibility/_main_sii.py b/mosqito/sq_metrics/speech_intelligibility/_main_sii.py index 1f085911..9de2630d 100644 --- a/mosqito/sq_metrics/speech_intelligibility/_main_sii.py +++ b/mosqito/sq_metrics/speech_intelligibility/_main_sii.py @@ -63,7 +63,7 @@ def _main_sii(method, speech_spectrum, noise_spectrum, threshold): elif (method == 'octave_bands') or (method == 'third_octave_bands'): noise_spectrum -= BANDWIDTH_ADJUSTEMENT - + # STEP 3 if method == 'octave': Z = noise_spectrum else: @@ -96,7 +96,7 @@ def _main_sii(method, speech_spectrum, noise_spectrum, threshold): D = maximum(Z, X) # STEP 6 - L = 1 - (speech_spectrum - STANDARD_SPEECH_SPECTRUM_NORMAL ) + L = 1 - (speech_spectrum - STANDARD_SPEECH_SPECTRUM_NORMAL -10)/160 L[where(L>1)] = 1 # STEP 7 @@ -107,7 +107,7 @@ def _main_sii(method, speech_spectrum, noise_spectrum, threshold): # STEP 8 SII = sum(IMPORTANCE * A) - SII_specific = A + SII_specific = IMPORTANCE * A return SII, SII_specific, CENTER_FREQUENCIES diff --git a/validations/sq_metrics/speech_intelligibility/output/validation_sii.png b/validations/sq_metrics/speech_intelligibility/output/validation_sii.png new file mode 100644 index 0000000000000000000000000000000000000000..b1d832b9f0a63be99b37929a2ce27912ed18de05 GIT binary patch literal 48067 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 np.amin([reference * 0.99, reference - 0.01], axis=0)).all() and ( - S <= np.amax([reference * 1.01, reference + 0.01], axis=0) + tstS = (SII_spec >= amin([reference["SII_spec"] * 0.99, reference["SII_spec"] - 0.01], axis=0)).all() and ( + SII_spec <= amax([reference["SII_spec"] * 1.01, reference["SII_spec"] + 0.01], axis=0) ).all() # Tolerance curves definition - tol_low = np.amin([reference * 0.99, reference - 0.01], axis=0) - tol_high = np.amax([reference * 1.01, reference + 0.01], axis=0) + tol_low = amin([reference["SII_spec"] * 0.99, reference["SII_spec"] - 0.01], axis=0) + tol_high = amax([reference["SII_spec"] * 1.01, reference["SII_spec"] + 0.01], axis=0) # Plot tolerance curves plt.plot( - barks, tol_low, color="red", linestyle="solid", label="tolerance", linewidth=1 + freqs, tol_low, color="red", linestyle="solid", label="tolerance", linewidth=1 ) - plt.plot(barks, tol_high, color="red", linestyle="solid", linewidth=1) + plt.plot(freqs, tol_high, color="red", linestyle="solid", linewidth=1) if tstS: plt.text( @@ -119,16 +84,14 @@ def _check_compliance(sharpness, reference): ) # Plot the calculated sharpness - plt.plot(barks, sharpness, label="MOSQITO") - plt.title("Speech intelligibility index for the 3 test signals", fontsize=10) + plt.plot(freqs, SII_spec, label="MOSQITO") + plt.title("Speech intelligibility index = "+f"{SII:.3f}"+"\n Octave band procedure", fontsize=10) plt.legend() - plt.xlabel("Center frequency [bark]") - plt.ylabel("Sharpness, [acum]") - + plt.xlabel("Center frequency [Hz]") + plt.ylabel("Specific SII") plt.savefig( - "output/" - + "validation_sii_" - + ".png", + "validations/sq_metrics/speech_intelligibility/output/" + + "validation_sii.png", format="png", ) plt.clf() @@ -137,4 +100,4 @@ def _check_compliance(sharpness, reference): # test de la fonction if __name__ == "__main__": # generate compliance plot - validation_sii() + validation_sii(reference) From c9d93a212a2193b2daf396a1a4807f8902344a3f Mon Sep 17 00:00:00 2001 From: wantysal Date: Wed, 20 Dec 2023 09:31:04 +0100 Subject: [PATCH 12/17] [CC] validation of the third octave band procedure according to the standard --- .../speech_intelligibility/_main_sii.py | 13 +- .../speech_intelligibility/validation_sii.py | 175 +++++++++++++++++- 2 files changed, 176 insertions(+), 12 deletions(-) diff --git a/mosqito/sq_metrics/speech_intelligibility/_main_sii.py b/mosqito/sq_metrics/speech_intelligibility/_main_sii.py index 9de2630d..c9e3226a 100644 --- a/mosqito/sq_metrics/speech_intelligibility/_main_sii.py +++ b/mosqito/sq_metrics/speech_intelligibility/_main_sii.py @@ -45,9 +45,9 @@ def _main_sii(method, speech_spectrum, noise_spectrum, threshold): elif method == 'equally_critical': CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL = _get_equal_critical_band_data() elif method == 'third_octave': - CENTER_FREQUENCIES, _, _, BANDWIDTH_ADJUSTEMENT, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL = _get_third_octave_band_data() + CENTER_FREQUENCIES, _, _, _, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL = _get_third_octave_band_data() elif method == 'octave': - CENTER_FREQUENCIES, _, _, BANDWIDTH_ADJUSTEMENT, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL = _get_octave_band_data() + CENTER_FREQUENCIES, _, _, _, IMPORTANCE, REFERENCE_INTERNAL_NOISE_SPECTRUM, STANDARD_SPEECH_SPECTRUM_NORMAL = _get_octave_band_data() nbands = len(CENTER_FREQUENCIES) if threshold is None: @@ -60,8 +60,6 @@ def _main_sii(method, speech_spectrum, noise_spectrum, threshold): # dB bandwidth adjustement if (method == 'critical_bands') or (method == 'equal_critical_bands'): noise_spectrum -= 10 * log10(UPPER_FREQUENCIES - LOWER_FREQUENCIES) - elif (method == 'octave_bands') or (method == 'third_octave_bands'): - noise_spectrum -= BANDWIDTH_ADJUSTEMENT # STEP 3 if method == 'octave': @@ -74,8 +72,8 @@ def _main_sii(method, speech_spectrum, noise_spectrum, threshold): Z = zeros((nbands)) for i in range(nbands): s = 0 - for k in range(i-1): - s += 10**(0.1*B[k] + 3.32 * C[k] * log10(0.89 * CENTER_FREQUENCIES[i] / CENTER_FREQUENCIES[k])) + for k in range(i): + s += 10**(0.1*(B[k] + 3.32 * C[k] * log10(0.89 * CENTER_FREQUENCIES[i] / CENTER_FREQUENCIES[k]))) Z[i] = 10 * log10(10**(0.1*noise_spectrum[i]) + s) else: C = -80 + 0.6 * (B + 10*log10(UPPER_FREQUENCIES - LOWER_FREQUENCIES)) @@ -83,8 +81,7 @@ def _main_sii(method, speech_spectrum, noise_spectrum, threshold): for i in range(nbands): s = 0 for k in range(i-1): - s += 10**(0.1*B[k] + 3.32 * C[k] * log10(CENTER_FREQUENCIES[i] / UPPER_FREQUENCIES[k])) - + s += 10**(0.1*(B[k] + 3.32 * C[k] * log10(CENTER_FREQUENCIES[i] / CENTER_FREQUENCIES[k]))) Z[i] = 10 * log10(10**(0.1*noise_spectrum[i]) + s) # 4.3.2.4 Z[0] = B[0] diff --git a/validations/sq_metrics/speech_intelligibility/validation_sii.py b/validations/sq_metrics/speech_intelligibility/validation_sii.py index f9a29044..fe5d8306 100644 --- a/validations/sq_metrics/speech_intelligibility/validation_sii.py +++ b/validations/sq_metrics/speech_intelligibility/validation_sii.py @@ -8,13 +8,20 @@ ) # Third party imports -from numpy import array, empty, amin, amax +from numpy import array, empty, amin, amax, zeros, log10, maximum, float64 + + +from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data +from mosqito.utils.LTQ import LTQ +from mosqito.utils.conversion import freq2bark # Local application imports from mosqito.sq_metrics.speech_intelligibility._main_sii import _main_sii # Reference values from ANSI S3.5 standard -reference = {"noise_spectrum": array([70, 65, 45, 25, 1, -15]), +reference = empty(2, dtype=dict) + +reference[0] = {"noise_spectrum": array([70, 65, 45, 25, 1, -15]), "speech_spectrum": array([50, 40, 40, 30, 20, 0]), "freq_axis": array([250, 500, 1000, 2000, 4000, 8000]), "method": "octave", @@ -23,7 +30,73 @@ } -def validation_sii(reference): +reference[1] = {"noise_spectrum": array([ + 40, + 30, + 20, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 ] +), + "speech_spectrum": array([ + 54, + 54, + 54, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 ] +), + "freq_axis": array( [ + 160, + 200, + 250, + 315, + 400, + 500, + 630, + 800, + 1000, + 1250, + 1600, + 2000, + 2500, + 3150, + 4000, + 5000, + 6300, + 8000 + ] +), + "method": "third_octave", + "SII_spec": array([40,34.66,25.04]), +} + + +def validation_sii_octave(reference): """Test function for the script sii_freq Test function for the script sharpness_din with .wav filesas input. @@ -95,9 +168,103 @@ def validation_sii(reference): format="png", ) plt.clf() + +def validation_sii_third_octave(reference): + """Test function for the script sii_freq + + Test function for the script sharpness_din with .wav filesas input. + The input files are provided by DIN 45692_2009E + The compliance is assessed according to chapter 6 of the standard. + One .png compliance plot is generated. + + Parameters + ---------- + None + + Outputs + ------- + None + """ + + noise_spectrum = reference["noise_spectrum"] + speech_spectrum = reference["speech_spectrum"] + + # Get band data for third-octave procedure + CENTER_FREQUENCIES, _, _, BANDWIDTH_ADJUSTEMENT, _, _, _ = _get_third_octave_band_data() + nbands = len(CENTER_FREQUENCIES) + + T = zeros((nbands)) + + # STEP 3 + V = speech_spectrum - 24 + B = maximum(noise_spectrum, V) + C = -80 + 0.6 * (B + 10*log10(CENTER_FREQUENCIES)-6.353) + Z = zeros((nbands)) + for i in range(nbands): + s = 0 + for k in range(i): + s += 10**(0.1*(B[k] + 3.32 * C[k] * log10(0.89 * CENTER_FREQUENCIES[i] / CENTER_FREQUENCIES[k]))) + Z[i] = 10 * log10(10**(0.1*noise_spectrum[i]) + s) + # 4.3.2.4 + Z[0] = B[0] + + plt.figure() + + # Frequency bark axis + freqs = reference["freq_axis"] + + tstS = (Z[:3] >= amin([reference["SII_spec"] * 0.99, reference["SII_spec"] - 0.01], axis=0)).all() and ( + Z[:3] <= amax([reference["SII_spec"] * 1.01, reference["SII_spec"] + 0.01], axis=0) + ).all() + + # Tolerance curves definition + tol_low = amin([reference["SII_spec"] * 0.99, reference["SII_spec"] - 0.01], axis=0) + tol_high = amax([reference["SII_spec"] * 1.01, reference["SII_spec"] + 0.01], axis=0) + + # Plot tolerance curves + plt.plot( + freqs[:3], tol_low, color="red", linestyle="solid", label="tolerance", linewidth=1 + ) + plt.plot(freqs[:3], tol_high, color="red", linestyle="solid", linewidth=1) + + if tstS: + plt.text( + 0.5, + 0.5, + "Test passed ", + horizontalalignment="center", + verticalalignment="center", + transform=plt.gca().transAxes, + bbox=dict(facecolor="green", alpha=0.3), + ) + + else: + plt.text( + 0.5, + 0.5, + "Test not passed", + horizontalalignment="center", + verticalalignment="center", + transform=plt.gca().transAxes, + bbox=dict(facecolor="red", alpha=0.3), + ) + + # Plot the calculated sharpness + plt.plot(freqs[:3], Z[:3], label="MOSQITO") + plt.title("Equivalent noise spectrum \n Third-octave band procedure", fontsize=10) + plt.legend() + plt.xlabel("Center frequency [Hz]") + plt.ylabel("Amplitude [dB]") + plt.savefig( + "validations/sq_metrics/speech_intelligibility/output/" + + "validation_equivalent_noise_spectrum.png", + format="png", + ) + plt.clf() # test de la fonction if __name__ == "__main__": # generate compliance plot - validation_sii(reference) + validation_sii_octave(reference[0]) + validation_sii_third_octave(reference[1]) From 8644a7dd92889f10a83958d4f5ae91b390b48525 Mon Sep 17 00:00:00 2001 From: wantysal Date: Wed, 20 Dec 2023 09:33:56 +0100 Subject: [PATCH 13/17] [CC] docstring update --- .../speech_intelligibility/validation_sii.py | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/validations/sq_metrics/speech_intelligibility/validation_sii.py b/validations/sq_metrics/speech_intelligibility/validation_sii.py index fe5d8306..831431d0 100644 --- a/validations/sq_metrics/speech_intelligibility/validation_sii.py +++ b/validations/sq_metrics/speech_intelligibility/validation_sii.py @@ -10,12 +10,8 @@ # Third party imports from numpy import array, empty, amin, amax, zeros, log10, maximum, float64 - -from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data -from mosqito.utils.LTQ import LTQ -from mosqito.utils.conversion import freq2bark - # Local application imports +from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_third_octave_band_data from mosqito.sq_metrics.speech_intelligibility._main_sii import _main_sii # Reference values from ANSI S3.5 standard @@ -97,20 +93,13 @@ def validation_sii_octave(reference): - """Test function for the script sii_freq + """Test function for the script _main_sii with octave band procedure - Test function for the script sharpness_din with .wav filesas input. - The input files are provided by DIN 45692_2009E - The compliance is assessed according to chapter 6 of the standard. + Test function for the script sii_freq with reference arrays as input. + The input files are provided by ANSI S3.5-1997. + The compliance is assessed according with a 1% tolerance. One .png compliance plot is generated. - Parameters - ---------- - None - - Outputs - ------- - None """ # Compute SII SII, SII_spec, _ = _main_sii(reference["method"], reference["speech_spectrum"], reference["noise_spectrum"], threshold=None) @@ -170,20 +159,13 @@ def validation_sii_octave(reference): plt.clf() def validation_sii_third_octave(reference): - """Test function for the script sii_freq + """Test function for the script _main_sii with third octave band procedure - Test function for the script sharpness_din with .wav filesas input. - The input files are provided by DIN 45692_2009E - The compliance is assessed according to chapter 6 of the standard. + Test function for the script sii_freq with reference arrays as input. + The input files are provided by ANSI S3.5-1997. + The compliance is assessed according with a 1% tolerance. One .png compliance plot is generated. - Parameters - ---------- - None - - Outputs - ------- - None """ noise_spectrum = reference["noise_spectrum"] From 14043d80e621e8db95d7650c4acdbbe93aebbfc5 Mon Sep 17 00:00:00 2001 From: wantysal Date: Wed, 20 Dec 2023 10:58:59 +0100 Subject: [PATCH 14/17] [CC] all tests running + docstring cleaned --- mosqito/__init__.py | 9 +- mosqito/sq_metrics/__init__.py | 4 + .../sq_metrics/speech_intelligibility/sii.py | 21 --- .../speech_intelligibility/sii_freq.py | 25 +--- .../speech_intelligibility/sii_level.py | 41 +++--- .../sq_metrics/roughness/test_roughness_dw.py | 14 +- .../speech_intelligibility/test_sii.py | 129 ++++++++++-------- .../validation_equivalent_noise_spectrum.png | Bin 0 -> 43952 bytes 8 files changed, 110 insertions(+), 133 deletions(-) create mode 100644 validations/sq_metrics/speech_intelligibility/output/validation_equivalent_noise_spectrum.png diff --git a/mosqito/__init__.py b/mosqito/__init__.py index e171c616..aa3d4cc7 100644 --- a/mosqito/__init__.py +++ b/mosqito/__init__.py @@ -42,8 +42,9 @@ # Colors and linestyles COLORS = [ "#69c3c5", - "#9969c4", - "#c46b69", - "#95c469", - "#2a6c6e", + "#ffd788", + "#ff8b88", + "#7894cf", + "#228080", + "#a8e2e2" ] diff --git a/mosqito/sq_metrics/__init__.py b/mosqito/sq_metrics/__init__.py index fa83fc72..e7344c3a 100644 --- a/mosqito/sq_metrics/__init__.py +++ b/mosqito/sq_metrics/__init__.py @@ -32,3 +32,7 @@ from mosqito.sq_metrics.sharpness.sharpness_din.sharpness_din_freq import sharpness_din_freq from mosqito.sq_metrics.loudness.utils.sone_to_phon import sone_to_phon + +from mosqito.sq_metrics.speech_intelligibility.sii import sii +from mosqito.sq_metrics.speech_intelligibility.sii_freq import sii_freq +from mosqito.sq_metrics.speech_intelligibility.sii_level import sii_level diff --git a/mosqito/sq_metrics/speech_intelligibility/sii.py b/mosqito/sq_metrics/speech_intelligibility/sii.py index a2972e99..9080401e 100644 --- a/mosqito/sq_metrics/speech_intelligibility/sii.py +++ b/mosqito/sq_metrics/speech_intelligibility/sii.py @@ -89,24 +89,3 @@ def sii(noise, fs, method, speech_level, threshold=None): SII, SII_specific, freq_axis = _main_sii(method, speech_spectrum, noise_spectrum, threshold) return SII, SII_specific, freq_axis - -if __name__ == "__main__": - import matplotlib.pyplot as plt - import numpy as np - fs=48000 - d=0.2 - dB=90 - time = np.arange(0, d, 1/fs) - f = 50 - stimulus = np.sin(2 * np.pi * f * time) * np.sin(np.pi * f * time) + np.sin(10 * np.pi * f * time) + np.sin(100 * np.pi * f * time) - rms = np.sqrt(np.mean(np.power(stimulus, 2))) - ampl = 0.00002 * np.power(10, dB / 20) / rms - stimulus = stimulus * ampl - SII, SII_spec, freq_axis = sii(stimulus, fs, method='critical', speech_level='normal') - plt.plot(freq_axis, SII_spec) - plt.xlabel("Frequency [Hz]") - plt.ylabel("Specific value ") - plt.title("Speech Intelligibility Index = " + f"{SII:.2f}") - - plt.show(block=True) - diff --git a/mosqito/sq_metrics/speech_intelligibility/sii_freq.py b/mosqito/sq_metrics/speech_intelligibility/sii_freq.py index 59a9c72e..0413a4b1 100644 --- a/mosqito/sq_metrics/speech_intelligibility/sii_freq.py +++ b/mosqito/sq_metrics/speech_intelligibility/sii_freq.py @@ -87,33 +87,14 @@ def sii_freq(spectrum, freqs, method, speech_level, threshold=None): CENTER_FREQUENCIES, LOWER_FREQUENCIES, UPPER_FREQUENCIES, _, _, _, _, = _get_octave_band_data() nbands = len(speech_spectrum) - if (len(spectrum) != nbands) or (freqs != CENTER_FREQUENCIES): + if (len(spectrum) != nbands) or (freqs != CENTER_FREQUENCIES).any(): noise_spectrum,_ = band_spectrum_synthesis(spectrum, freqs, LOWER_FREQUENCIES, UPPER_FREQUENCIES) + else: + noise_spectrum = spectrum SII, SII_specific, freq_axis = _main_sii(method, speech_spectrum, noise_spectrum, threshold) return SII, SII_specific, freq_axis -if __name__ == "__main__": - from mosqito.sound_level_meter.spectrum import spectrum - import matplotlib.pyplot as plt - import numpy as np - fs=48000 - d=0.2 - dB=60 - time = np.arange(0, d, 1/fs) - f = 50 - stimulus = np.sin(2 * np.pi * f * time) + np.sin(2 * np.pi * f * time) * np.sin(np.pi * f * time) + np.sin(10 * np.pi * f * time) - rms = np.sqrt(np.mean(np.power(stimulus, 2))) - ampl = 0.00002 * np.power(10, dB / 20) / rms - stimulus = stimulus * ampl - spec, freqs = spectrum(stimulus, fs, db=False) - SII, SII_spec, freq_axis = sii_freq(spec, freqs, method='critical', speech_level='normal') - plt.plot(freq_axis, SII_spec) - plt.xlabel("Frequency band [Bark]") - plt.ylabel("Specific loudness [Sone/Bark]") - plt.title("Speech Intelligibility Index = " + f"{SII:.2f}") - - plt.show(block=True) diff --git a/mosqito/sq_metrics/speech_intelligibility/sii_level.py b/mosqito/sq_metrics/speech_intelligibility/sii_level.py index 84ee46d6..90123083 100644 --- a/mosqito/sq_metrics/speech_intelligibility/sii_level.py +++ b/mosqito/sq_metrics/speech_intelligibility/sii_level.py @@ -39,6 +39,26 @@ def sii_level(noise_level, method, speech_level, threshold=None): -------- .. plot:: :include-source: + + >>> import matplotlib.pyplot as plt + >>> import numpy as np + >>> from mosqito.sq_metrics.speech_intelligibility import sii_level + >>> fs=48000 + >>> d=0.2 + >>> dB=90 + >>> time = np.arange(0, d, 1/fs) + >>> f = 50 + >>> stimulus = np.sin(2 * np.pi * f * time) * np.sin(np.pi * f * time) + np.sin(10 * np.pi * f * time) + np.sin(100 * np.pi * f * time) + >>> rms = np.sqrt(np.mean(np.power(stimulus, 2))) + >>> ampl = 0.00002 * np.power(10, dB / 20) / rms + >>> stimulus = stimulus * ampl + >>> speech_level = 'raised' + >>> SII, SII_spec, freq_axis = sii_level(60, method='critical', speech_level=speech_level, threshold='zwicker') + >>> plt.plot(freq_axis, SII_spec) + >>> plt.xlabel("Frequency [Hz]") + >>> plt.ylabel("Specific value ") + >>> plt.title("Speech Intelligibility Index = " + f"{SII:.2f} \n Speech level: " + speech_level) + """ if (method!='critical') & (method!='equally_critical') & (method!='third_octave') & (method!='octave'): @@ -67,24 +87,3 @@ def sii_level(noise_level, method, speech_level, threshold=None): return SII, SII_specific, freq_axis -if __name__ == "__main__": - import matplotlib.pyplot as plt - import numpy as np - fs=48000 - d=0.2 - dB=90 - time = np.arange(0, d, 1/fs) - f = 50 - stimulus = np.sin(2 * np.pi * f * time) * np.sin(np.pi * f * time) + np.sin(10 * np.pi * f * time) + np.sin(100 * np.pi * f * time) - rms = np.sqrt(np.mean(np.power(stimulus, 2))) - ampl = 0.00002 * np.power(10, dB / 20) / rms - stimulus = stimulus * ampl - speech_level = 'raised' - SII, SII_spec, freq_axis = sii_level(60, method='critical', speech_level=speech_level, threshold='zwicker') - plt.plot(freq_axis, SII_spec) - plt.xlabel("Frequency [Hz]") - plt.ylabel("Specific value ") - plt.title("Speech Intelligibility Index = " + f"{SII:.2f} \n Speech level: " + speech_level) - - plt.show(block=True) - diff --git a/tests/sq_metrics/roughness/test_roughness_dw.py b/tests/sq_metrics/roughness/test_roughness_dw.py index 35c9e0cb..d8642572 100644 --- a/tests/sq_metrics/roughness/test_roughness_dw.py +++ b/tests/sq_metrics/roughness/test_roughness_dw.py @@ -20,7 +20,7 @@ # Local application imports from mosqito.sq_metrics import roughness_dw, roughness_dw_freq from tests.sq_metrics.roughness.signals_test_generation import signal_test - +from mosqito.sound_level_meter.spectrum import spectrum @pytest.mark.roughness_dw # to skip or run only Daniel and Weber roughness tests def test_roughness_dw(): @@ -134,20 +134,14 @@ def test_roughness_dw_freq(): ------- None """ - + fs = 44100 # Stimulus generation stimulus, _ = signal_test( - fc=1000, fmod=70, mdepth=1, fs=44100, d=0.2, dB=60) + fc=1000, fmod=70, mdepth=1, fs=fs, d=0.2, dB=60) # conversion into frequency domain n = len(stimulus) - - # Creation of the spectrum by FFT using the Blackman window - spec = fft(stimulus )[0:n//2] * 1.42 - # Highest frequency - nMax = round(n / 2) - # Frequency axis in Hertz - freqs = np.arange(1, nMax + 1, 1) * (44100 / n) + spec, freqs = spectrum(stimulus, fs, nfft="default", window="blackman", db=False) # Roughness calculation roughness, _, _ = roughness_dw_freq(spec, freqs) diff --git a/tests/sq_metrics/speech_intelligibility/test_sii.py b/tests/sq_metrics/speech_intelligibility/test_sii.py index ab835b32..484b7863 100644 --- a/tests/sq_metrics/speech_intelligibility/test_sii.py +++ b/tests/sq_metrics/speech_intelligibility/test_sii.py @@ -1,7 +1,9 @@ # -*- coding: utf-8 -*- import numpy as np -from scipy.fft import fft, fftfreq +from numpy import array, interp, linspace, concatenate, flip + +from scipy.fft import fft, fftfreq, ifft # Optional package import try: @@ -18,20 +20,30 @@ # Local application imports from mosqito.utils import load -from mosqito.sq_metrics import sii, sii_freq, sii_level +from mosqito import sii, sii_freq, sii_level +from mosqito.sq_metrics.speech_intelligibility._main_sii import _main_sii @pytest.fixture def test_signal(): - # Input signal from DIN 45692_2009E - sig, fs = load("tests/input/broadband_570.wav", wav_calib=1) - sig_dict = { + + spec = array([70, 65, 45, 25, 1, -15]) + freqs = array([250, 500, 1000, 2000, 4000, 8000]) + fs = 44100 + n = int(44100 * 0.2) + f = linspace(0,fs//2,2*n) + sig = ifft(interp(f, freqs, spec, left=0, right=0)).real[4410:13230] + test_signal = {"noise_spectrum": spec, + "speech_spectrum": array([50, 40, 40, 30, 20, 0]), + "freq_axis": freqs, "signal": sig, "fs": fs, - "S_din": 2.85, - } - return sig_dict + "method": "octave", + "SII": 0.504, + "SII_spec": array([0, 0, 0.08, 0.17, 0.21, 0.04]), + } + return test_signal @pytest.mark.sii # to skip or run sharpness test def test_sii(test_signal): @@ -51,16 +63,14 @@ def test_sii(test_signal): fs = test_signal["fs"] # Compute sharpness - SII, _, _ = sii(sig, fs, method, 'normal') - SII, _, _ = sii(sig, fs, method, 'raised') - SII, _, _ = sii(sig, fs, method, 'loud') - SII, _, _ = sii(sig, fs, method, 'shout') + SII, _, _ = sii(sig, fs, 'third_octave', 'raised') + SII, _, _ = sii(sig, fs, 'critical', 'loud') + SII, _, _ = sii(sig, fs, 'equally_critical', 'shout') + SII, _, _ = sii(sig, fs, 'octave', 'normal') - - @pytest.mark.sii # to skip or run sharpness test -def test_sii_freq(): +def test_sii_freq(test_signal): """Test function for the sharpness calculation of an time-varying audio signal. Parameters @@ -72,55 +82,56 @@ def test_sii_freq(): """ # Input signal - sig = test_spectrum - + spec = test_signal["noise_spectrum"] + freqs = test_signal["freq_axis"] # Compute sharpness - SII, _, _ = sii(spec, freqs, method, 'normal') - SII, _, _ = sii(spec, freqs, method, 'raised') - SII, _, _ = sii(spec, freqs, method, 'loud') - SII, _, _ = sii(spec, freqs, method, 'shout') - - # Check that the value is within the desired values +/- 1% - np.testing.assert_allclose(SII, test_signal["SII"], rtol=0.05) - - - + SII, _, _ = sii_freq(spec, freqs, "critical", 'loud') + SII, _, _ = sii_freq(spec, freqs, "equally_critical", 'raised') + SII, _, _ = sii_freq(spec, freqs, "third_octave", 'shout') + SII, _, _ = sii_freq(spec, freqs, "octave", 'normal') + @pytest.mark.sii -def test_sii_level(test_signal): +def test_sii_level(): # Compute sharpness - SII, _, _ = sii(60, method, 'normal') - SII, _, _ = sii(60, method, 'raised') - SII, _, _ = sii(60, method, 'loud') - SII, _, _ = sii(60, method, 'shout') + SII, _, _ = sii_level(60, 'critical', 'normal') + SII, _, _ = sii_level(60, 'equally_critical', 'raised') + SII, _, _ = sii_level(60, 'octave', 'loud') + SII, _, _ = sii_level(60, 'third_octave', 'shout') + + +@pytest.mark.sii # to skip or run sharpness test +def test_main_sii(test_signal): + """Test function for the sharpness calculation of an time-varying audio signal. + + """ + + SII, SII_spec, _ = _main_sii(test_signal["method"], test_signal["speech_spectrum"], test_signal["noise_spectrum"], threshold=None) + + assert check_compliance(SII, test_signal["SII"]) -def check_compliance(S, signal): - """Check the comppiance of loudness calc. to ISO 532-1 +def check_compliance(SII, reference): + """Check the compliance of SII with ANSI S3.5 - The compliance is assessed according to chapter 6 of the - standard DIN 45692_2009E. + The compliance is assessed according to ANSI S3.5 annex A Parameters ---------- - S : float + SII : float computed sharpness value - signal : dict - {"data file" : - "S" : - } - + reference : float + reference value + Outputs ------- tst : bool Compliance to the reference data """ - # Load reference value - ref = signal["S"] # Test for DIN 45692_2009E comformance (chapter 6) - tst = (S >= np.amax([ref * 0.95, ref - 0.05], axis=0)).all() and ( - S <= np.amin([ref * 1.05, ref + 0.05], axis=0) + tst = (SII >= np.amax([reference * 0.999, reference - 0.01], axis=0)).all() and ( + SII <= np.amin([reference * 1.01, reference + 0.01], axis=0) ).all() return tst @@ -128,16 +139,24 @@ def check_compliance(S, signal): # test de la fonction if __name__ == "__main__": - # Reproduce the code from the fixture - # Input signal from ANSI S3.5 - sig, fs = load("tests/input/broadband_570.wav", wav_calib=1) - test_signal = { + + spec = array([70, 65, 45, 25, 1, -15]) + freqs = array([250, 500, 1000, 2000, 4000, 8000]) + fs = 44100 + n = int(44100 * 0.2) + f = linspace(0,fs//2,2*n) + sig = ifft(interp(f, freqs, spec, left=0, right=0)).real[4410:13230] + sig = {"noise_spectrum": spec, + "speech_spectrum": array([50, 40, 40, 30, 20, 0]), + "freq_axis": freqs, "signal": sig, "fs": fs, - "S_din": 2.85, - } - test_spectrum = [] + "method": "octave", + "SII": 0.504, + "SII_spec": array([0, 0, 0.08, 0.17, 0.21, 0.04]), + } - test_sii(test_signal) - test_sii_freq(test_spectrum) + test_sii(sig) + test_sii_freq(sig) test_sii_level() + test_main_sii(sig) diff --git a/validations/sq_metrics/speech_intelligibility/output/validation_equivalent_noise_spectrum.png b/validations/sq_metrics/speech_intelligibility/output/validation_equivalent_noise_spectrum.png new file mode 100644 index 0000000000000000000000000000000000000000..61a5306883d26ab972e7185013fcb64a0ef11d6a GIT binary patch literal 43952 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 literal 0 HcmV?d00001 From d24981df6dda71c4cde5982b054d4645659183bf Mon Sep 17 00:00:00 2001 From: wantysal Date: Wed, 20 Dec 2023 11:10:46 +0100 Subject: [PATCH 15/17] [BC] issue 70 solved --- mosqito/sq_metrics/loudness/loudness_zwst/_main_loudness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mosqito/sq_metrics/loudness/loudness_zwst/_main_loudness.py b/mosqito/sq_metrics/loudness/loudness_zwst/_main_loudness.py index 411ff7c1..9945d1c4 100644 --- a/mosqito/sq_metrics/loudness/loudness_zwst/_main_loudness.py +++ b/mosqito/sq_metrics/loudness/loudness_zwst/_main_loudness.py @@ -226,7 +226,7 @@ def _main_loudness(spec_third, field_type): # taking into account the dependance of absolute threshold # within this critical band korry = 0.4 + 0.32 * nm[0] ** 0.2 - nm[0, korry <= 1] *= korry + nm[0, korry <= 1] *= korry[korry <= 1] # nm[:, -1] = 0 # if spec_third.ndim == 1 or spec_third.shape[1] == 1: # # This is only for test_loudness_zwicker_3oct because only one array of one col is given and this routine needs 2 or more From af68d182c06928039eae4ebc6d257e4b5865aa15 Mon Sep 17 00:00:00 2001 From: wantysal Date: Thu, 21 Dec 2023 11:51:07 +0100 Subject: [PATCH 16/17] [CC] corrections following review before merge * precision of the standard name in the functions * input checks in the freq_band_synthesis to prevent error linked to small spectrum * all improts updated --- mosqito/__init__.py | 6 +-- mosqito/sound_level_meter/__init__.py | 3 +- ...um_synthesis.py => freq_band_synthesis.py} | 20 ++++++-- mosqito/sq_metrics/__init__.py | 6 +-- .../{ => sii_ansi}/_band_procedure_data.py | 1 + .../{ => sii_ansi}/_main_sii.py | 4 +- .../{ => sii_ansi}/_speech_data.py | 3 +- .../{sii.py => sii_ansi/sii_ansi.py} | 14 +++--- .../sii_ansi_freq.py} | 20 ++++---- .../sii_ansi_level.py} | 15 +++--- pytest.ini | 2 +- .../{test_sii.py => test_sii_ansi.py} | 49 ++++++++----------- .../speech_intelligibility/validation_sii.py | 4 +- 13 files changed, 77 insertions(+), 70 deletions(-) rename mosqito/sound_level_meter/{band_spectrum_synthesis.py => freq_band_synthesis.py} (64%) rename mosqito/sq_metrics/speech_intelligibility/{ => sii_ansi}/_band_procedure_data.py (99%) rename mosqito/sq_metrics/speech_intelligibility/{ => sii_ansi}/_main_sii.py (91%) rename mosqito/sq_metrics/speech_intelligibility/{ => sii_ansi}/_speech_data.py (99%) rename mosqito/sq_metrics/speech_intelligibility/{sii.py => sii_ansi/sii_ansi.py} (82%) rename mosqito/sq_metrics/speech_intelligibility/{sii_freq.py => sii_ansi/sii_ansi_freq.py} (80%) rename mosqito/sq_metrics/speech_intelligibility/{sii_level.py => sii_ansi/sii_ansi_level.py} (81%) rename tests/sq_metrics/speech_intelligibility/{test_sii.py => test_sii_ansi.py} (68%) diff --git a/mosqito/__init__.py b/mosqito/__init__.py index aa3d4cc7..f9836f17 100644 --- a/mosqito/__init__.py +++ b/mosqito/__init__.py @@ -29,9 +29,9 @@ from mosqito.sq_metrics.sharpness.sharpness_din.sharpness_din_perseg import sharpness_din_perseg from mosqito.sq_metrics.sharpness.sharpness_din.sharpness_din_freq import sharpness_din_freq -from mosqito.sq_metrics.speech_intelligibility.sii import sii -from mosqito.sq_metrics.speech_intelligibility.sii_freq import sii_freq -from mosqito.sq_metrics.speech_intelligibility.sii_level import sii_level +from mosqito.sq_metrics.speech_intelligibility.sii_ansi.sii_ansi import sii_ansi +from mosqito.sq_metrics.speech_intelligibility.sii_ansi.sii_ansi_freq import sii_ansi_freq +from mosqito.sq_metrics.speech_intelligibility.sii_ansi.sii_ansi_level import sii_ansi_level from mosqito.sq_metrics.loudness.utils.sone_to_phon import sone_to_phon from mosqito.utils.isoclose import isoclose diff --git a/mosqito/sound_level_meter/__init__.py b/mosqito/sound_level_meter/__init__.py index e87ee5a5..e266e4e3 100644 --- a/mosqito/sound_level_meter/__init__.py +++ b/mosqito/sound_level_meter/__init__.py @@ -1,3 +1,4 @@ from mosqito.sound_level_meter.noct_spectrum.noct_spectrum import noct_spectrum from mosqito.sound_level_meter.noct_spectrum.noct_synthesis import noct_synthesis - +from mosqito.sound_level_meter.spectrum import spectrum +from mosqito.sound_level_meter.freq_band_synthesis import freq_band_synthesis diff --git a/mosqito/sound_level_meter/band_spectrum_synthesis.py b/mosqito/sound_level_meter/freq_band_synthesis.py similarity index 64% rename from mosqito/sound_level_meter/band_spectrum_synthesis.py rename to mosqito/sound_level_meter/freq_band_synthesis.py index d827e24a..48ee836f 100644 --- a/mosqito/sound_level_meter/band_spectrum_synthesis.py +++ b/mosqito/sound_level_meter/freq_band_synthesis.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # Standard library import -from numpy import array, concatenate, zeros, log10, power, argmin, split +from numpy import array, concatenate, zeros, log10, power, argmin, split, arange, interp, iscomplex -def band_spectrum_synthesis(spectrum, freqs, fmin, fmax): +def freq_band_synthesis(spectrum, freqs, fmin, fmax): """Adapt input spectrum to frequency band levels Convert the input spectrum to frequency band spectrum @@ -12,7 +12,7 @@ def band_spectrum_synthesis(spectrum, freqs, fmin, fmax): Parameters ---------- spectrum : numpy.ndarray - amplitude rms of the one-sided spectrum of the signal, size (nperseg, nseg). + One-sided spectrum of the signal in [dB], size (nperseg, nseg). freqs : list List of input frequency , size (nperseg) or (nperseg, nseg). fmin : float @@ -35,6 +35,20 @@ def band_spectrum_synthesis(spectrum, freqs, fmin, fmax): fpref : numpy.ndarray Corresponding preferred third octave band center frequencies, size (nbands). """ + if iscomplex(spectrum).any(): + raise ValueError('Input spectrum must be in dB, no complex value allowed.') + + if (fmin.min() < freqs.min()): + print("[WARNING] Input spectrum minimum frequency if higher than fmin. Empty values will be filled with 0.") + df = freqs[1] - freqs[0] + spectrum = interp(arange(fmin.min(),fmax.max()+df, df), freqs, spectrum) + freqs = arange(fmin.min(),fmax.max()+df, df) + + if (fmax.max() > freqs.max()): + print("[WARNING] Input spectrum maximum frequency if lower than fmax. Empty values will be filled with 0.") + df = freqs[1] - freqs[0] + spectrum = interp(arange(fmin.min(),fmax.max()+df, df), freqs, spectrum) + freqs = arange(fmin.min(),fmax.max()+df, df) # Find the lower and upper index of each band idx_low = argmin(abs(freqs[:,None] - fmin), axis=0) diff --git a/mosqito/sq_metrics/__init__.py b/mosqito/sq_metrics/__init__.py index e7344c3a..b9af14fc 100644 --- a/mosqito/sq_metrics/__init__.py +++ b/mosqito/sq_metrics/__init__.py @@ -33,6 +33,6 @@ from mosqito.sq_metrics.loudness.utils.sone_to_phon import sone_to_phon -from mosqito.sq_metrics.speech_intelligibility.sii import sii -from mosqito.sq_metrics.speech_intelligibility.sii_freq import sii_freq -from mosqito.sq_metrics.speech_intelligibility.sii_level import sii_level +from mosqito.sq_metrics.speech_intelligibility.sii_ansi.sii_ansi import sii_ansi +from mosqito.sq_metrics.speech_intelligibility.sii_ansi.sii_ansi_freq import sii_ansi_freq +from mosqito.sq_metrics.speech_intelligibility.sii_ansi.sii_ansi_level import sii_ansi_level diff --git a/mosqito/sq_metrics/speech_intelligibility/_band_procedure_data.py b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/_band_procedure_data.py similarity index 99% rename from mosqito/sq_metrics/speech_intelligibility/_band_procedure_data.py rename to mosqito/sq_metrics/speech_intelligibility/sii_ansi/_band_procedure_data.py index 967a3ac7..6681d347 100644 --- a/mosqito/sq_metrics/speech_intelligibility/_band_procedure_data.py +++ b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/_band_procedure_data.py @@ -1,6 +1,7 @@ from numpy import array def _get_critical_band_data(): + """ See § 3.4 of the standard ANSI S3.5. """ CENTER_FREQUENCIES = array( [ 150, diff --git a/mosqito/sq_metrics/speech_intelligibility/_main_sii.py b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/_main_sii.py similarity index 91% rename from mosqito/sq_metrics/speech_intelligibility/_main_sii.py rename to mosqito/sq_metrics/speech_intelligibility/sii_ansi/_main_sii.py index c9e3226a..7db8d4c5 100644 --- a/mosqito/sq_metrics/speech_intelligibility/_main_sii.py +++ b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/_main_sii.py @@ -3,8 +3,8 @@ from numpy import array, zeros, log10, maximum, where, sum -from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data -from mosqito.sq_metrics.speech_intelligibility._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data from mosqito.utils.LTQ import LTQ from mosqito.utils.conversion import freq2bark diff --git a/mosqito/sq_metrics/speech_intelligibility/_speech_data.py b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/_speech_data.py similarity index 99% rename from mosqito/sq_metrics/speech_intelligibility/_speech_data.py rename to mosqito/sq_metrics/speech_intelligibility/sii_ansi/_speech_data.py index 671ce96b..2d521e9c 100644 --- a/mosqito/sq_metrics/speech_intelligibility/_speech_data.py +++ b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/_speech_data.py @@ -1,7 +1,8 @@ from numpy import array def _get_critical_band_speech_data(speech_level): - + """ See § 3.4 of the standard ANSI S3.5. """ + if speech_level == "normal": SPEECH_SPECTRUM = array( [ diff --git a/mosqito/sq_metrics/speech_intelligibility/sii.py b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/sii_ansi.py similarity index 82% rename from mosqito/sq_metrics/speech_intelligibility/sii.py rename to mosqito/sq_metrics/speech_intelligibility/sii_ansi/sii_ansi.py index 9080401e..cc207c9a 100644 --- a/mosqito/sq_metrics/speech_intelligibility/sii.py +++ b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/sii_ansi.py @@ -2,14 +2,14 @@ from numpy import array, zeros -from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data -from mosqito.sq_metrics.speech_intelligibility._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data -from mosqito.sq_metrics.speech_intelligibility._main_sii import _main_sii +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._main_sii import _main_sii from mosqito.sound_level_meter.spectrum import spectrum -from mosqito.sound_level_meter.band_spectrum_synthesis import band_spectrum_synthesis +from mosqito.sound_level_meter.freq_band_synthesis import freq_band_synthesis -def sii(noise, fs, method, speech_level, threshold=None): +def sii_ansi(noise, fs, method, speech_level, threshold=None): """Calculate speech intelligibility index This function computes SII values for a noise time signal according to ANSI S3.5 standard. @@ -21,7 +21,7 @@ def sii(noise, fs, method, speech_level, threshold=None): fs: float Sampling frequency of the input noise signal. method: {"critical", "equally_critical", "third_octave", "octave"} - Type of frequency band to be used for the calculation. + Type of frequency band to be used for the calculation. See § 3.4 of the standard. speech_level : {'normal', 'raised', 'loud', 'shout'} Speech level to assess, the corresponding speech spectrum defined in the standard is used for calculation. threshold : array_like or 'zwicker' @@ -84,7 +84,7 @@ def sii(noise, fs, method, speech_level, threshold=None): # Compute noise spectrum in dB spec, freqs = spectrum(noise, fs, nfft="default", window="blackman", db=True) - noise_spectrum, _ = band_spectrum_synthesis(spec, freqs, LOWER_FREQUENCIES, UPPER_FREQUENCIES) + noise_spectrum, _ = freq_band_synthesis(spec, freqs, LOWER_FREQUENCIES, UPPER_FREQUENCIES) SII, SII_specific, freq_axis = _main_sii(method, speech_spectrum, noise_spectrum, threshold) diff --git a/mosqito/sq_metrics/speech_intelligibility/sii_freq.py b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/sii_ansi_freq.py similarity index 80% rename from mosqito/sq_metrics/speech_intelligibility/sii_freq.py rename to mosqito/sq_metrics/speech_intelligibility/sii_ansi/sii_ansi_freq.py index 0413a4b1..3942084d 100644 --- a/mosqito/sq_metrics/speech_intelligibility/sii_freq.py +++ b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/sii_ansi_freq.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- -from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data -from mosqito.sq_metrics.speech_intelligibility._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data -from mosqito.sq_metrics.speech_intelligibility._main_sii import _main_sii +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._main_sii import _main_sii -from mosqito.sound_level_meter.band_spectrum_synthesis import band_spectrum_synthesis +from mosqito.sound_level_meter.freq_band_synthesis import freq_band_synthesis -def sii_freq(spectrum, freqs, method, speech_level, threshold=None): +def sii_ansi_freq(spectrum, freqs, method, speech_level, threshold=None): """Calculate speech intelligibility index This function computes SII values for a noise spectrum in dB according to ANSI S3.5 standard. @@ -18,7 +18,7 @@ def sii_freq(spectrum, freqs, method, speech_level, threshold=None): freqs: array_like Frequency axis [Hz] of the spectrum. method: {"critical", "equally_critical", "third_octave", "octave"} - Type of frequency band to be used for the calculation. + Type of frequency band to be used for the calculation. See § 3.4 of the standard. speech_level : {'normal', 'raised', 'loud', 'shout'} Speech level to assess, the corresponding speech spectrum defined in the standard is used for calculation. threshold : array_like or 'zwicker' @@ -44,7 +44,7 @@ def sii_freq(spectrum, freqs, method, speech_level, threshold=None): .. plot:: :include-source: - >>> from mosqito.sq_metrics.speech_intelligibility import sii_freq + >>> from mosqito.sq_metrics import sii_ansi_freq >>> from mosqito.sound_level_meter.spectrum import spectrum >>> import matplotlib.pyplot as plt >>> import numpy as np @@ -57,7 +57,7 @@ def sii_freq(spectrum, freqs, method, speech_level, threshold=None): >>> rms = np.sqrt(np.mean(np.power(stimulus, 2))) >>> ampl = 0.00002 * np.power(10, dB / 20) / rms >>> stimulus = stimulus * ampl - >>> spec, freqs = spectrum(stimulus, fs, db=False) + >>> spec, freqs = spectrum(stimulus, fs, db=True) >>> SII, SII_spec, freq_axis = sii_freq(spec, freqs, method='critical', speech_level='normal') >>> plt.plot(freq_axis, SII_spec) >>> plt.xlabel("Frequency [Hz]") @@ -88,7 +88,7 @@ def sii_freq(spectrum, freqs, method, speech_level, threshold=None): nbands = len(speech_spectrum) if (len(spectrum) != nbands) or (freqs != CENTER_FREQUENCIES).any(): - noise_spectrum,_ = band_spectrum_synthesis(spectrum, freqs, LOWER_FREQUENCIES, UPPER_FREQUENCIES) + noise_spectrum,_ = freq_band_synthesis(spectrum, freqs, LOWER_FREQUENCIES, UPPER_FREQUENCIES) else: noise_spectrum = spectrum @@ -96,5 +96,3 @@ def sii_freq(spectrum, freqs, method, speech_level, threshold=None): return SII, SII_specific, freq_axis - - diff --git a/mosqito/sq_metrics/speech_intelligibility/sii_level.py b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/sii_ansi_level.py similarity index 81% rename from mosqito/sq_metrics/speech_intelligibility/sii_level.py rename to mosqito/sq_metrics/speech_intelligibility/sii_ansi/sii_ansi_level.py index 90123083..d7fa0bd7 100644 --- a/mosqito/sq_metrics/speech_intelligibility/sii_level.py +++ b/mosqito/sq_metrics/speech_intelligibility/sii_ansi/sii_ansi_level.py @@ -2,24 +2,23 @@ from numpy import ones, log10, power -from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data -from mosqito.sq_metrics.speech_intelligibility._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data -from mosqito.sq_metrics.speech_intelligibility._main_sii import _main_sii -from mosqito.utils.LTQ import LTQ -from mosqito.utils.conversion import freq2bark +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._band_procedure_data import _get_critical_band_data, _get_equal_critical_band_data, _get_octave_band_data, _get_third_octave_band_data +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._speech_data import _get_critical_band_speech_data, _get_equal_critical_band_speech_data, _get_octave_band_speech_data, _get_third_octave_band_speech_data +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._main_sii import _main_sii -def sii_level(noise_level, method, speech_level, threshold=None): +def sii_ansi_level(noise_level, method, speech_level, threshold=None): """Calculate speech intelligibility index This function computes SII values for an overall noise level in dB according to ANSI S3.5 standard. + This value is used to create a uniform noise spectrum, with the same deduced level on each frequency band. Parameters ---------- noise_level : float - Overall noise level in [dB ref. 2e-5 Pa]. This value is used to create a uniform noise spectrum. + Overall noise level in [dB ref. 2e-5 Pa]. method: {"critical", "equally_critical", "third_octave", "octave"} - Type of frequency band to be used for the calculation. + Type of frequency band to be used for the calculation. See § 3.4 of the standard. speech_level : {'normal', 'raised', 'loud', 'shout'} Speech level to assess, the corresponding speech spectrum defined in the standard is used for calculation. threshold : array_like or 'zwicker' diff --git a/pytest.ini b/pytest.ini index 0ff8bb55..e3cfc75d 100644 --- a/pytest.ini +++ b/pytest.ini @@ -12,7 +12,7 @@ markers = pr_st: marks tests related to stationary prominence ratio pr_tv: marks tests related to time-varying prominence ratio pr_freq: marks tests related to prominence ratio from a spectrum - sii: marks tests related to speech intelligibility + sii_ansi: marks tests related to speech intelligibility noct_spectrum: marks tests related to n_octave spectra computation noct_synthesis: marks test related to n_octave spectra adaptation utils: marks tests related to utils functions \ No newline at end of file diff --git a/tests/sq_metrics/speech_intelligibility/test_sii.py b/tests/sq_metrics/speech_intelligibility/test_sii_ansi.py similarity index 68% rename from tests/sq_metrics/speech_intelligibility/test_sii.py rename to tests/sq_metrics/speech_intelligibility/test_sii_ansi.py index 484b7863..7d777dad 100644 --- a/tests/sq_metrics/speech_intelligibility/test_sii.py +++ b/tests/sq_metrics/speech_intelligibility/test_sii_ansi.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- import numpy as np -from numpy import array, interp, linspace, concatenate, flip +from numpy import array, interp, linspace -from scipy.fft import fft, fftfreq, ifft +from scipy.fft import ifft # Optional package import try: @@ -11,17 +11,10 @@ except ImportError: raise RuntimeError( "In order to perform the tests you need the 'pytest' package.") -try: - from SciDataTool import DataLinspace, DataTime -except ImportError: - raise RuntimeError( - "In order to handle Data objects you need the 'SciDataTool' package." - ) # Local application imports -from mosqito.utils import load -from mosqito import sii, sii_freq, sii_level -from mosqito.sq_metrics.speech_intelligibility._main_sii import _main_sii +from mosqito import sii_ansi, sii_ansi_freq, sii_ansi_level +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._main_sii import _main_sii @pytest.fixture @@ -45,7 +38,7 @@ def test_signal(): return test_signal -@pytest.mark.sii # to skip or run sharpness test +@pytest.mark.sii_ansi # to skip or run sharpness test def test_sii(test_signal): """Test function for the sharpness calculation of an audio signal The input signals come from DIN 45692_2009E. The compliance is assessed @@ -63,13 +56,13 @@ def test_sii(test_signal): fs = test_signal["fs"] # Compute sharpness - SII, _, _ = sii(sig, fs, 'third_octave', 'raised') - SII, _, _ = sii(sig, fs, 'critical', 'loud') - SII, _, _ = sii(sig, fs, 'equally_critical', 'shout') - SII, _, _ = sii(sig, fs, 'octave', 'normal') + SII, _, _ = sii_ansi(sig, fs, 'third_octave', 'raised') + SII, _, _ = sii_ansi(sig, fs, 'critical', 'loud') + SII, _, _ = sii_ansi(sig, fs, 'equally_critical', 'shout') + SII, _, _ = sii_ansi(sig, fs, 'octave', 'normal') -@pytest.mark.sii # to skip or run sharpness test +@pytest.mark.sii_ansi # to skip or run sharpness test def test_sii_freq(test_signal): """Test function for the sharpness calculation of an time-varying audio signal. @@ -85,28 +78,28 @@ def test_sii_freq(test_signal): spec = test_signal["noise_spectrum"] freqs = test_signal["freq_axis"] # Compute sharpness - SII, _, _ = sii_freq(spec, freqs, "critical", 'loud') - SII, _, _ = sii_freq(spec, freqs, "equally_critical", 'raised') - SII, _, _ = sii_freq(spec, freqs, "third_octave", 'shout') - SII, _, _ = sii_freq(spec, freqs, "octave", 'normal') + SII, _, _ = sii_ansi_freq(spec, freqs, "critical", 'loud') + SII, _, _ = sii_ansi_freq(spec, freqs, "equally_critical", 'raised') + SII, _, _ = sii_ansi_freq(spec, freqs, "third_octave", 'shout') + SII, _, _ = sii_ansi_freq(spec, freqs, "octave", 'normal') -@pytest.mark.sii +@pytest.mark.sii_ansi def test_sii_level(): # Compute sharpness - SII, _, _ = sii_level(60, 'critical', 'normal') - SII, _, _ = sii_level(60, 'equally_critical', 'raised') - SII, _, _ = sii_level(60, 'octave', 'loud') - SII, _, _ = sii_level(60, 'third_octave', 'shout') + SII, _, _ = sii_ansi_level(60, 'critical', 'normal') + SII, _, _ = sii_ansi_level(60, 'equally_critical', 'raised') + SII, _, _ = sii_ansi_level(60, 'octave', 'loud') + SII, _, _ = sii_ansi_level(60, 'third_octave', 'shout') -@pytest.mark.sii # to skip or run sharpness test +@pytest.mark.sii_ansi # to skip or run sharpness test def test_main_sii(test_signal): """Test function for the sharpness calculation of an time-varying audio signal. """ - SII, SII_spec, _ = _main_sii(test_signal["method"], test_signal["speech_spectrum"], test_signal["noise_spectrum"], threshold=None) + SII, _, _ = _main_sii(test_signal["method"], test_signal["speech_spectrum"], test_signal["noise_spectrum"], threshold=None) assert check_compliance(SII, test_signal["SII"]) diff --git a/validations/sq_metrics/speech_intelligibility/validation_sii.py b/validations/sq_metrics/speech_intelligibility/validation_sii.py index 831431d0..b502db9d 100644 --- a/validations/sq_metrics/speech_intelligibility/validation_sii.py +++ b/validations/sq_metrics/speech_intelligibility/validation_sii.py @@ -11,8 +11,8 @@ from numpy import array, empty, amin, amax, zeros, log10, maximum, float64 # Local application imports -from mosqito.sq_metrics.speech_intelligibility._band_procedure_data import _get_third_octave_band_data -from mosqito.sq_metrics.speech_intelligibility._main_sii import _main_sii +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._band_procedure_data import _get_third_octave_band_data +from mosqito.sq_metrics.speech_intelligibility.sii_ansi._main_sii import _main_sii # Reference values from ANSI S3.5 standard reference = empty(2, dtype=dict) From 68e961b8177755bb3c7fd629b6dcf963ec992087 Mon Sep 17 00:00:00 2001 From: wantysal Date: Thu, 21 Dec 2023 12:07:45 +0100 Subject: [PATCH 17/17] [CC] version number upgraded for pip --- CITATION.cff | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index dd17979e..3e58188f 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -4,9 +4,9 @@ authors: - family-names: Green Forge Coop given-names: title: MOSQITO -version: 1.0.8 +version: 1.1 doi: 10.5281/zenodo.6675733 -date-released: 2022-06-21 +date-released: 2023-12-21 keywords: - audio - python diff --git a/setup.py b/setup.py index 3db69d9e..24552bbc 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ import setuptools # /!\ update before a release -MoSQITo_VERSION = "1.0.8" +MoSQITo_VERSION = "1.1.0" # MoSQITo description with open("README.md", "r", encoding="utf-8") as fh: