├── .github └── workflows │ ├── publish-to-test-pypi.yml │ └── tests.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pyloudnorm ├── __init__.py ├── iirfilter.py ├── meter.py ├── normalize.py └── util.py ├── pyproject.toml ├── requirements.txt ├── setup.py └── tests ├── data ├── 1770-2_Comp_18LKFS_FrequencySweep.wav ├── 1770-2_Comp_23LKFS_10000Hz_2ch.wav ├── 1770-2_Comp_23LKFS_1000Hz_2ch.wav ├── 1770-2_Comp_23LKFS_100Hz_2ch.wav ├── 1770-2_Comp_23LKFS_2000Hz_2ch.wav ├── 1770-2_Comp_23LKFS_25Hz_2ch.wav ├── 1770-2_Comp_23LKFS_500Hz_2ch.wav ├── 1770-2_Comp_24LKFS_10000Hz_2ch.wav ├── 1770-2_Comp_24LKFS_1000Hz_2ch.wav ├── 1770-2_Comp_24LKFS_100Hz_2ch.wav ├── 1770-2_Comp_24LKFS_2000Hz_2ch.wav ├── 1770-2_Comp_24LKFS_25Hz_2ch.wav ├── 1770-2_Comp_24LKFS_500Hz_2ch.wav ├── 1770-2_Comp_AbsGateTest.wav ├── 1770-2_Comp_RelGateTest.wav ├── 1770-2_Conf_Mono_Voice+Music-23LKFS.wav ├── 1770-2_Conf_Mono_Voice+Music-24LKFS.wav ├── 1770-2_Conf_Stereo_VinL+R-23LKFS.wav ├── 1770-2_Conf_Stereo_VinL+R-24LKFS.wav ├── piano.wav └── sine_1000.wav └── test_loudness.py /.github/workflows/publish-to-test-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python distributions to PyPI and TestPyPI 2 | 3 | on: [push, workflow_dispatch] 4 | 5 | jobs: 6 | build-n-publish: 7 | name: Publish Python distributions to PyPI and TestPyPI 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - name: Set up Python 3.7 12 | uses: actions/setup-python@v1 13 | with: 14 | python-version: 3.7 15 | - name: Install pypa/build 16 | run: >- 17 | python -m 18 | pip install 19 | build 20 | --user 21 | - name: Build a binary wheel and a source tarball 22 | run: >- 23 | python -m 24 | build 25 | --sdist 26 | --wheel 27 | --outdir dist/ 28 | . 29 | - name: Publish distribution to Test PyPI 30 | if: startsWith(github.ref, 'refs/tags') 31 | uses: pypa/gh-action-pypi-publish@release/v1 32 | with: 33 | password: ${{ secrets.TEST_PYPI_API_TOKEN }} 34 | repository_url: https://test.pypi.org/legacy/ 35 | - name: Publish distribution to PyPI 36 | if: startsWith(github.ref, 'refs/tags') 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | with: 39 | password: ${{ secrets.PYPI_API_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: pyloudnorm 2 | 3 | on: [push, pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: ["3.7", "3.8", "3.9", "3.10"] 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install . 23 | sudo apt-get install libsndfile1-dev 24 | pip install pytest soundfile 25 | - name: Test with pytest 26 | run: | 27 | pytest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build 2 | build/ 3 | dist/ 4 | *.egg-info 5 | 6 | # junk 7 | .DS_Store 8 | 9 | # dev 10 | __pycache__ 11 | .vscode 12 | env/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | notifications: 4 | email: false 5 | 6 | python: 7 | - 2.7 8 | - 3.5 9 | - 3.6 10 | 11 | install: 12 | - pip install -r requirements.txt 13 | - pip install -e . 14 | 15 | script: 16 | - pytest -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Christian Steinmetz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyloudnorm [![Build Status](https://travis-ci.org/csteinmetz1/pyloudnorm.svg?branch=master)](https://travis-ci.org/csteinmetz1/pyloudnorm) ![Zenodo](https://zenodo.org/badge/DOI/10.5281/zenodo.3551801.svg) 2 | Flexible audio loudness meter in Python. 3 | 4 | Implementation of [ITU-R BS.1770-4](https://www.itu.int/dms_pubrec/itu-r/rec/bs/R-REC-BS.1770-4-201510-I!!PDF-E.pdf).
5 | Allows control over gating block size and frequency weighting filters for additional control. 6 | 7 | For full details on the implementation see our [paper](https://csteinmetz1.github.io/pyloudnorm-eval/paper/pyloudnorm_preprint.pdf) with a summary in our [AES presentation video](https://www.youtube.com/watch?v=krSJpQ3d4gE). 8 | 9 | ## Installation 10 | You can install with pip as follows 11 | ``` 12 | pip install pyloudnorm 13 | ``` 14 | 15 | For the latest releases always install from the GitHub repo 16 | ``` 17 | pip install git+https://github.com/csteinmetz1/pyloudnorm 18 | ``` 19 | ## Usage 20 | 21 | ### Find the loudness of an audio file 22 | It's easy to measure the loudness of a wav file. 23 | Here we use PySoundFile to read a .wav file as an ndarray. 24 | ```python 25 | import soundfile as sf 26 | import pyloudnorm as pyln 27 | 28 | data, rate = sf.read("test.wav") # load audio (with shape (samples, channels)) 29 | meter = pyln.Meter(rate) # create BS.1770 meter 30 | loudness = meter.integrated_loudness(data) # measure loudness 31 | ``` 32 | 33 | ### Loudness normalize and peak normalize audio files 34 | Methods are included to normalize audio files to desired peak values or desired loudness. 35 | ```python 36 | import soundfile as sf 37 | import pyloudnorm as pyln 38 | 39 | data, rate = sf.read("test.wav") # load audio 40 | 41 | # peak normalize audio to -1 dB 42 | peak_normalized_audio = pyln.normalize.peak(data, -1.0) 43 | 44 | # measure the loudness first 45 | meter = pyln.Meter(rate) # create BS.1770 meter 46 | loudness = meter.integrated_loudness(data) 47 | 48 | # loudness normalize audio to -12 dB LUFS 49 | loudness_normalized_audio = pyln.normalize.loudness(data, loudness, -12.0) 50 | ``` 51 | 52 | ### Advanced operation 53 | A number of alternate weighting filters are available, as well as the ability to adjust the analysis block size. 54 | Examples are shown below. 55 | ```python 56 | import soundfile as sf 57 | import pyloudnorm as pyln 58 | from pyloudnorm import IIRfilter 59 | 60 | data, rate = sf.read("test.wav") # load audio 61 | 62 | # block size 63 | meter1 = pyln.Meter(rate) # 400ms block size 64 | meter2 = pyln.Meter(rate, block_size=0.200) # 200ms block size 65 | 66 | # filter classes 67 | meter3 = pyln.Meter(rate) # BS.1770 meter 68 | meter4 = pyln.Meter(rate, filter_class="DeMan") # fully compliant filters 69 | meter5 = pyln.Meter(rate, filter_class="Fenton/Lee 1") # low complexity improvement by Fenton and Lee 70 | meter6 = pyln.Meter(rate, filter_class="Fenton/Lee 2") # higher complexity improvement by Fenton and Lee 71 | meter7 = pyln.Meter(rate, filter_class="Dash et al.") # early modification option 72 | 73 | # create your own IIR filters 74 | my_high_pass = IIRfilter(0.0, 0.5, 20.0, rate, 'high_pass') 75 | my_high_shelf = IIRfilter(2.0, 0.7, 1525.0, rate, 'high_shelf') 76 | 77 | # create a meter initialized without filters 78 | meter8 = pyln.Meter(rate, filter_class="custom") 79 | 80 | # load your filters into the meter 81 | meter8._filters = {'my_high_pass' : my_high_pass, 'my_high_shelf' : my_high_shelf} 82 | 83 | ``` 84 | 85 | ## Dependencies 86 | - **SciPy** ([https://www.scipy.org/](https://www.scipy.org/)) 87 | - **NumPy** ([http://www.numpy.org/](http://www.numpy.org/)) 88 | 89 | 90 | ## Citation 91 | If you use pyloudnorm in your work please consider citing us. 92 | ``` 93 | @inproceedings{steinmetz2021pyloudnorm, 94 | title={pyloudnorm: {A} simple yet flexible loudness meter in Python}, 95 | author={Steinmetz, Christian J. and Reiss, Joshua D.}, 96 | booktitle={150th AES Convention}, 97 | year={2021}} 98 | ``` 99 | 100 | ## References 101 | 102 | > Ian Dash, Luis Miranda, and Densil Cabrera, "[Multichannel Loudness Listening Test](http://www.aes.org/e-lib/browse.cfm?elib=14581)," 103 | > 124th International Convention of the Audio Engineering Society, May 2008 104 | 105 | > Pedro D. Pestana and Álvaro Barbosa, "[Accuracy of ITU-R BS.1770 Algorithm in Evaluating Multitrack Material](http://www.aes.org/e-lib/online/browse.cfm?elib=16608)," 106 | > 133rd International Convention of the Audio Engineering Society, October 2012 107 | 108 | > Pedro D. Pestana, Josh D. Reiss, and Álvaro Barbosa, "[Loudness Measurement of Multitrack Audio Content Using Modifications of ITU-R BS.1770](http://www.aes.org/e-lib/browse.cfm?elib=16714)," 109 | > 134th International Convention of the Audio Engineering Society, May 2013 110 | 111 | > Steven Fenton and Hyunkook Lee, "[Alternative Weighting Filters for Multi-Track Program Loudness Measurement](http://www.aes.org/e-lib/browse.cfm?elib=19215)," 112 | > 143rd International Convention of the Audio Engineering Society, October 2017 113 | 114 | > Brecht De Man, "[Evaluation of Implementations of the EBU R128 Loudness Measurement](http://www.aes.org/e-lib/browse.cfm?elib=19790)," 115 | > 145th International Convention of the Audio Engineering Society, October 2018. 116 | 117 | ## Tensorized/Differentiable Implementations 118 | 119 | For use in differentiable contexts, such as part of a loss function, there are the following implementations: 120 | - PyTorch: [Descript Inc.'s `audiotools`](https://github.com/descriptinc/audiotools/blob/master/audiotools/core/loudness.py) 121 | - Jax: [jaxloudnorm](https://github.com/boris-kuz/jaxloudnorm) 122 | -------------------------------------------------------------------------------- /pyloudnorm/__init__.py: -------------------------------------------------------------------------------- 1 | """Top-level module for pyloudnorm""" 2 | 3 | # Import pyloudnorm sub-modules 4 | from .meter import Meter, IIRfilter 5 | from . import util 6 | from . import normalize -------------------------------------------------------------------------------- /pyloudnorm/iirfilter.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | import scipy.signal 3 | import numpy as np 4 | 5 | class IIRfilter(object): 6 | """ IIR Filter object to pre-filtering 7 | 8 | This class allows for the generation of various IIR filters 9 | in order to apply different frequency weighting to audio data 10 | before measuring the loudness. 11 | 12 | Parameters 13 | ---------- 14 | G : float 15 | Gain of the filter in dB. 16 | Q : float 17 | Q of the filter. 18 | fc : float 19 | Center frequency of the shelf in Hz. 20 | rate : float 21 | Sampling rate in Hz. 22 | filter_type: str 23 | Shape of the filter. 24 | """ 25 | 26 | def __init__(self, G, Q, fc, rate, filter_type, passband_gain=1.0): 27 | self.G = G 28 | self.Q = Q 29 | self.fc = fc 30 | self.rate = rate 31 | self.filter_type = filter_type 32 | self.passband_gain = passband_gain 33 | 34 | def __str__(self): 35 | filter_info = dedent(""" 36 | ------------------------------ 37 | type: {type} 38 | ------------------------------ 39 | Gain = {G} dB 40 | Q factor = {Q} 41 | Center freq. = {fc} Hz 42 | Sample rate = {rate} Hz 43 | Passband gain = {passband_gain} dB 44 | ------------------------------ 45 | b0 = {_b0} 46 | b1 = {_b1} 47 | b2 = {_b2} 48 | a0 = {_a0} 49 | a1 = {_a1} 50 | a2 = {_a2} 51 | ------------------------------ 52 | """.format(type = self.filter_type, 53 | G=self.G, Q=self.Q, fc=self.fc, rate=self.rate, 54 | passband_gain=self.passband_gain, 55 | _b0=self.b[0], _b1=self.b[1], _b2=self.b[2], 56 | _a0=self.a[0], _a1=self.a[1], _a2=self.a[2])) 57 | 58 | return filter_info 59 | 60 | def generate_coefficients(self): 61 | """ Generates biquad filter coefficients using instance filter parameters. 62 | 63 | This method is called whenever an IIRFilter is instantiated and then sets 64 | the coefficients for the filter instance. 65 | 66 | Design of the 'standard' filter types are based upon the equations 67 | presented by RBJ in the "Cookbook formulae for audio equalizer biquad 68 | filter coefficients" which can be found at the link below. 69 | 70 | http://shepazu.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html 71 | 72 | Additional filter designs are also available. Brecht DeMan found that 73 | the coefficients generated by the RBJ filters do not directly match 74 | the coefficients provided in the ITU specification. For full compliance 75 | use the 'DeMan' filters below when constructing filters. Details on his 76 | work can be found at the GitHub repository below. 77 | 78 | https://github.com/BrechtDeMan/loudness.py 79 | 80 | Returns 81 | ------- 82 | b : ndarray 83 | Numerator filter coefficients stored as [b0, b1, b2] 84 | a : ndarray 85 | Denominator filter coefficients stored as [a0, a1, a2] 86 | """ 87 | A = 10**(self.G/40.0) 88 | w0 = 2.0 * np.pi * (self.fc / self.rate) 89 | alpha = np.sin(w0) / (2.0 * self.Q) 90 | 91 | if self.filter_type == 'high_shelf': 92 | b0 = A * ( (A+1) + (A-1) * np.cos(w0) + 2 * np.sqrt(A) * alpha ) 93 | b1 = -2 * A * ( (A-1) + (A+1) * np.cos(w0) ) 94 | b2 = A * ( (A+1) + (A-1) * np.cos(w0) - 2 * np.sqrt(A) * alpha ) 95 | a0 = (A+1) - (A-1) * np.cos(w0) + 2 * np.sqrt(A) * alpha 96 | a1 = 2 * ( (A-1) - (A+1) * np.cos(w0) ) 97 | a2 = (A+1) - (A-1) * np.cos(w0) - 2 * np.sqrt(A) * alpha 98 | elif self.filter_type == 'low_shelf': 99 | b0 = A * ( (A+1) - (A-1) * np.cos(w0) + 2 * np.sqrt(A) * alpha ) 100 | b1 = 2 * A * ( (A-1) - (A+1) * np.cos(w0) ) 101 | b2 = A * ( (A+1) - (A-1) * np.cos(w0) - 2 * np.sqrt(A) * alpha ) 102 | a0 = (A+1) + (A-1) * np.cos(w0) + 2 * np.sqrt(A) * alpha 103 | a1 = -2 * ( (A-1) + (A+1) * np.cos(w0) ) 104 | a2 = (A+1) + (A-1) * np.cos(w0) - 2 * np.sqrt(A) * alpha 105 | elif self.filter_type == 'high_pass': 106 | b0 = (1 + np.cos(w0))/2 107 | b1 = -(1 + np.cos(w0)) 108 | b2 = (1 + np.cos(w0))/2 109 | a0 = 1 + alpha 110 | a1 = -2 * np.cos(w0) 111 | a2 = 1 - alpha 112 | elif self.filter_type == 'low_pass': 113 | b0 = (1 - np.cos(w0))/2 114 | b1 = (1 - np.cos(w0)) 115 | b2 = (1 - np.cos(w0))/2 116 | a0 = 1 + alpha 117 | a1 = -2 * np.cos(w0) 118 | a2 = 1 - alpha 119 | elif self.filter_type == 'peaking': 120 | b0 = 1 + alpha * A 121 | b1 = -2 * np.cos(w0) 122 | b2 = 1 - alpha * A 123 | a0 = 1 + alpha / A 124 | a1 = -2 * np.cos(w0) 125 | a2 = 1 - alpha / A 126 | elif self.filter_type == 'notch': 127 | b0 = 1 128 | b1 = -2 * np.cos(w0) 129 | b2 = 1 130 | a0 = 1 + alpha 131 | a1 = -2 * np.cos(w0) 132 | a2 = 1 - alpha 133 | elif self.filter_type == 'high_shelf_DeMan': 134 | K = np.tan(np.pi * self.fc / self.rate) 135 | Vh = np.power(10.0, self.G / 20.0) 136 | Vb = np.power(Vh, 0.499666774155) 137 | a0_ = 1.0 + K / self.Q + K * K 138 | b0 = (Vh + Vb * K / self.Q + K * K) / a0_ 139 | b1 = 2.0 * (K * K - Vh) / a0_ 140 | b2 = (Vh - Vb * K / self.Q + K * K) / a0_ 141 | a0 = 1.0 142 | a1 = 2.0 * (K * K - 1.0) / a0_ 143 | a2 = (1.0 - K / self.Q + K * K) / a0_ 144 | elif self.filter_type == 'high_pass_DeMan': 145 | K = np.tan(np.pi * self.fc / self.rate) 146 | a0 = 1.0 147 | a1 = 2.0 * (K * K - 1.0) / (1.0 + K / self.Q + K * K) 148 | a2 = (1.0 - K / self.Q + K * K) / (1.0 + K / self.Q + K * K) 149 | b0 = 1.0 150 | b1 = -2.0 151 | b2 = 1.0 152 | else: 153 | raise ValueError("Invalid filter type", self.filter_type) 154 | 155 | return np.array([b0, b1, b2])/a0, np.array([a0, a1, a2])/a0 156 | 157 | def apply_filter(self, data): 158 | """ Apply the IIR filter to an input signal. 159 | 160 | Params 161 | ------- 162 | data : ndarrary 163 | Input audio data. 164 | 165 | Returns 166 | ------- 167 | filtered_signal : ndarray 168 | Filtered input audio. 169 | """ 170 | return self.passband_gain * scipy.signal.lfilter(self.b, self.a, data) 171 | 172 | @property 173 | def a(self): 174 | return self.generate_coefficients()[1] 175 | 176 | @property 177 | def b(self): 178 | return self.generate_coefficients()[0] 179 | 180 | -------------------------------------------------------------------------------- /pyloudnorm/meter.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | import numpy as np 3 | from . import util 4 | from .iirfilter import IIRfilter 5 | 6 | class Meter(object): 7 | """ Meter object which defines how the meter operates 8 | 9 | Defaults to the algorithm defined in ITU-R BS.1770-4. 10 | 11 | Parameters 12 | ---------- 13 | rate : float 14 | Sampling rate in Hz. 15 | filter_class : str 16 | Class of weighting filter used. 17 | - 'K-weighting' 18 | - 'Fenton/Lee 1' 19 | - 'Fenton/Lee 2' 20 | - 'Dash et al.' 21 | - 'DeMan' 22 | block_size : float 23 | Gating block size in seconds. 24 | """ 25 | 26 | def __init__(self, rate, filter_class="K-weighting", block_size=0.400): 27 | self.rate = rate 28 | self.filter_class = filter_class 29 | self.block_size = block_size 30 | 31 | def integrated_loudness(self, data): 32 | """ Measure the integrated gated loudness of a signal. 33 | 34 | Uses the weighting filters and block size defined by the meter 35 | the integrated loudness is measured based upon the gating algorithm 36 | defined in the ITU-R BS.1770-4 specification. 37 | 38 | Input data must have shape (samples, ch) or (samples,) for mono audio. 39 | Supports up to 5 channels and follows the channel ordering: 40 | [Left, Right, Center, Left surround, Right surround] 41 | 42 | Params 43 | ------- 44 | data : ndarray 45 | Input multichannel audio data. 46 | 47 | Returns 48 | ------- 49 | LUFS : float 50 | Integrated gated loudness of the input measured in dB LUFS. 51 | """ 52 | input_data = data.copy() 53 | util.valid_audio(input_data, self.rate, self.block_size) 54 | 55 | if input_data.ndim == 1: 56 | input_data = np.reshape(input_data, (input_data.shape[0], 1)) 57 | 58 | numChannels = input_data.shape[1] 59 | numSamples = input_data.shape[0] 60 | 61 | # Apply frequency weighting filters - account for the acoustic response of the head and auditory system 62 | for (filter_class, filter_stage) in self._filters.items(): 63 | for ch in range(numChannels): 64 | input_data[:,ch] = filter_stage.apply_filter(input_data[:,ch]) 65 | 66 | G = [1.0, 1.0, 1.0, 1.41, 1.41] # channel gains 67 | T_g = self.block_size # 400 ms gating block standard 68 | Gamma_a = -70.0 # -70 LKFS = absolute loudness threshold 69 | overlap = 0.75 # overlap of 75% of the block duration 70 | step = 1.0 - overlap # step size by percentage 71 | 72 | T = numSamples / self.rate # length of the input in seconds 73 | numBlocks = int(np.round(((T - T_g) / (T_g * step)))+1) # total number of gated blocks (see end of eq. 3) 74 | j_range = np.arange(0, numBlocks) # indexed list of total blocks 75 | z = np.zeros(shape=(numChannels,numBlocks)) # instantiate array - trasponse of input 76 | 77 | for i in range(numChannels): # iterate over input channels 78 | for j in j_range: # iterate over total frames 79 | l = int(T_g * (j * step ) * self.rate) # lower bound of integration (in samples) 80 | u = int(T_g * (j * step + 1) * self.rate) # upper bound of integration (in samples) 81 | # caluate mean square of the filtered for each block (see eq. 1) 82 | z[i,j] = (1.0 / (T_g * self.rate)) * np.sum(np.square(input_data[l:u,i])) 83 | 84 | with warnings.catch_warnings(): 85 | warnings.simplefilter("ignore", category=RuntimeWarning) 86 | # loudness for each jth block (see eq. 4) 87 | l = [-0.691 + 10.0 * np.log10(np.sum([G[i] * z[i,j] for i in range(numChannels)])) for j in j_range] 88 | 89 | # find gating block indices above absolute threshold 90 | J_g = [j for j,l_j in enumerate(l) if l_j >= Gamma_a] 91 | 92 | with warnings.catch_warnings(): 93 | warnings.simplefilter("ignore", category=RuntimeWarning) 94 | # calculate the average of z[i,j] as show in eq. 5 95 | z_avg_gated = [np.mean([z[i,j] for j in J_g]) for i in range(numChannels)] 96 | # calculate the relative threshold value (see eq. 6) 97 | Gamma_r = -0.691 + 10.0 * np.log10(np.sum([G[i] * z_avg_gated[i] for i in range(numChannels)])) - 10.0 98 | 99 | # find gating block indices above relative and absolute thresholds (end of eq. 7) 100 | J_g = [j for j,l_j in enumerate(l) if (l_j > Gamma_r and l_j > Gamma_a)] 101 | with warnings.catch_warnings(): 102 | warnings.simplefilter("ignore", category=RuntimeWarning) 103 | # calculate the average of z[i,j] as show in eq. 7 with blocks above both thresholds 104 | z_avg_gated = np.nan_to_num(np.array([np.mean([z[i,j] for j in J_g]) for i in range(numChannels)])) 105 | 106 | # calculate final loudness gated loudness (see eq. 7) 107 | with np.errstate(divide='ignore'): 108 | LUFS = -0.691 + 10.0 * np.log10(np.sum([G[i] * z_avg_gated[i] for i in range(numChannels)])) 109 | 110 | return LUFS 111 | 112 | @property 113 | def filter_class(self): 114 | return self._filter_class 115 | 116 | @filter_class.setter 117 | def filter_class(self, value): 118 | self._filters = {} # reset (clear) filters 119 | self._filter_class = value 120 | if self._filter_class == "K-weighting": 121 | self._filters['high_shelf'] = IIRfilter(4.0, 1/np.sqrt(2), 1500.0, self.rate, 'high_shelf') 122 | self._filters['high_pass'] = IIRfilter(0.0, 0.5, 38.0, self.rate, 'high_pass') 123 | elif self._filter_class == "Fenton/Lee 1": 124 | self._filters['high_shelf'] = IIRfilter(5.0, 1/np.sqrt(2), 1500.0, self.rate, 'high_shelf') 125 | self._filters['high_pass'] = IIRfilter(0.0, 0.5, 130.0, self.rate, 'high_pass') 126 | self._filters['peaking'] = IIRfilter(0.0, 1/np.sqrt(2), 500.0, self.rate, 'peaking') 127 | elif self._filter_class == "Fenton/Lee 2": # not yet implemented 128 | self._filters['high_self'] = IIRfilter(4.0, 1/np.sqrt(2), 1500.0, self.rate, 'high_shelf') 129 | self._filters['high_pass'] = IIRfilter(0.0, 0.5, 38.0, self.rate, 'high_pass') 130 | elif self._filter_class == "Dash et al.": 131 | self._filters['high_pass'] = IIRfilter(0.0, 0.375, 149.0, self.rate, 'high_pass') 132 | self._filters['peaking'] = IIRfilter(-2.93820927, 1.68878655, 1000.0, self.rate, 'peaking') 133 | elif self._filter_class == "DeMan": 134 | self._filters['high_shelf_DeMan'] = IIRfilter(3.99984385397, 0.7071752369554193, 1681.9744509555319, self.rate, 'high_shelf_DeMan') 135 | self._filters['high_pass_DeMan'] = IIRfilter(0.0, 0.5003270373253953, 38.13547087613982, self.rate, 'high_pass_DeMan') 136 | elif self._filter_class == "custom": 137 | pass 138 | else: 139 | raise ValueError("Invalid filter class:", self._filter_class) 140 | -------------------------------------------------------------------------------- /pyloudnorm/normalize.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | import numpy as np 3 | 4 | 5 | def peak(data, target): 6 | """ Peak normalize a signal. 7 | 8 | Normalize an input signal to a user specifed peak amplitude. 9 | 10 | Params 11 | ------- 12 | data : ndarray 13 | Input multichannel audio data. 14 | target : float 15 | Desired peak amplitude in dB. 16 | 17 | Returns 18 | ------- 19 | output : ndarray 20 | Peak normalized output data. 21 | """ 22 | # find the amplitude of the largest peak 23 | current_peak = np.max(np.abs(data)) 24 | 25 | # calculate the gain needed to scale to the desired peak level 26 | gain = np.power(10.0, target/20.0) / current_peak 27 | output = gain * data 28 | 29 | # check for potentially clipped samples 30 | if np.max(np.abs(output)) >= 1.0: 31 | warnings.warn("Possible clipped samples in output.") 32 | 33 | return output 34 | 35 | def loudness(data, input_loudness, target_loudness): 36 | """ Loudness normalize a signal. 37 | 38 | Normalize an input signal to a user loudness in dB LKFS. 39 | 40 | Params 41 | ------- 42 | data : ndarray 43 | Input multichannel audio data. 44 | input_loudness : float 45 | Loudness of the input in dB LUFS. 46 | target_loudness : float 47 | Target loudness of the output in dB LUFS. 48 | 49 | Returns 50 | ------- 51 | output : ndarray 52 | Loudness normalized output data. 53 | """ 54 | # calculate the gain needed to scale to the desired loudness level 55 | delta_loudness = target_loudness - input_loudness 56 | gain = np.power(10.0, delta_loudness/20.0) 57 | 58 | output = gain * data 59 | 60 | # check for potentially clipped samples 61 | if np.max(np.abs(output)) >= 1.0: 62 | warnings.warn("Possible clipped samples in output.") 63 | 64 | return output 65 | -------------------------------------------------------------------------------- /pyloudnorm/util.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def valid_audio(data, rate, block_size): 4 | """ Validate input audio data. 5 | 6 | Ensure input is numpy array of floating point data bewteen -1 and 1 7 | 8 | Params 9 | ------- 10 | data : ndarray 11 | Input audio data 12 | rate : int 13 | Sampling rate of the input audio in Hz 14 | block_size : int 15 | Analysis block size in seconds 16 | 17 | Returns 18 | ------- 19 | valid : bool 20 | True if valid audio 21 | 22 | """ 23 | if not isinstance(data, np.ndarray): 24 | raise ValueError("Data must be of type numpy.ndarray.") 25 | 26 | if not np.issubdtype(data.dtype, np.floating): 27 | raise ValueError("Data must be floating point.") 28 | 29 | if data.ndim == 2 and data.shape[1] > 5: 30 | raise ValueError("Audio must have five channels or less.") 31 | 32 | if data.shape[0] < block_size * rate: 33 | raise ValueError("Audio must have length greater than the block size.") 34 | 35 | return True -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pyloudnorm" 3 | version = "0.1.1" 4 | description = "Implementation of ITU-R BS.1770-4 loudness algorithm in Python." 5 | authors = [ 6 | { name = "Christian Steinmetz" }, 7 | { email = "c.j.steinmetz@qmul.ac.uk" }, 8 | ] 9 | 10 | [build-system] 11 | # Minimum requirements for the build system to execute. 12 | requires = ["setuptools>=58.0", "wheel", "attrs"] 13 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | scipy>=1.0.1 2 | numpy>=1.14.2 3 | matplotlib>=2.1.1 4 | pysoundfile>=0.9.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from setuptools import setup, find_packages 3 | 4 | NAME = "pyloudnorm" 5 | DESCRIPTION = "Implementation of ITU-R BS.1770-4 loudness algorithm in Python" 6 | URL = "https://github.com/csteinmetz1/pyloudnorm" 7 | EMAIL = "c.j.steinmetz@qmul.ac.uk" 8 | AUTHOR = "Christian Steinmetz" 9 | REQUIRES_PYTHON = ">=3.0" 10 | VERSION = "0.1.1" 11 | 12 | HERE = Path(__file__).parent 13 | 14 | try: 15 | with open(HERE / "README.md", encoding="utf-8") as f: 16 | long_description = "\n" + f.read() 17 | except FileNotFoundError: 18 | long_description = DESCRIPTION 19 | 20 | setup( 21 | name=NAME, 22 | version=VERSION, 23 | description=DESCRIPTION, 24 | long_description=long_description, 25 | long_description_content_type="text/markdown", 26 | author=AUTHOR, 27 | author_email=EMAIL, 28 | python_requires=REQUIRES_PYTHON, 29 | url=URL, 30 | packages=["pyloudnorm"], 31 | install_requires=["scipy>=1.0.1", "numpy>=1.14.2"], 32 | include_package_data=True, 33 | license="MIT", 34 | classifiers=[ 35 | "Programming Language :: Python :: 3", 36 | "License :: OSI Approved :: MIT License", 37 | "Operating System :: OS Independent", 38 | "Topic :: Multimedia :: Sound/Audio", 39 | "Topic :: Scientific/Engineering", 40 | ], 41 | ) -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_18LKFS_FrequencySweep.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_18LKFS_FrequencySweep.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_23LKFS_10000Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_23LKFS_10000Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_23LKFS_1000Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_23LKFS_1000Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_23LKFS_100Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_23LKFS_100Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_23LKFS_2000Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_23LKFS_2000Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_23LKFS_25Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_23LKFS_25Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_23LKFS_500Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_23LKFS_500Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_24LKFS_10000Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_24LKFS_10000Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_24LKFS_1000Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_24LKFS_1000Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_24LKFS_100Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_24LKFS_100Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_24LKFS_2000Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_24LKFS_2000Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_24LKFS_25Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_24LKFS_25Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_24LKFS_500Hz_2ch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_24LKFS_500Hz_2ch.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_AbsGateTest.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_AbsGateTest.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Comp_RelGateTest.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Comp_RelGateTest.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Conf_Mono_Voice+Music-23LKFS.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Conf_Mono_Voice+Music-23LKFS.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Conf_Mono_Voice+Music-24LKFS.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Conf_Mono_Voice+Music-24LKFS.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Conf_Stereo_VinL+R-23LKFS.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Conf_Stereo_VinL+R-23LKFS.wav -------------------------------------------------------------------------------- /tests/data/1770-2_Conf_Stereo_VinL+R-24LKFS.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/1770-2_Conf_Stereo_VinL+R-24LKFS.wav -------------------------------------------------------------------------------- /tests/data/piano.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/piano.wav -------------------------------------------------------------------------------- /tests/data/sine_1000.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csteinmetz1/pyloudnorm/a741692b186dbb1ca5ae69562d3e4354bc3e761f/tests/data/sine_1000.wav -------------------------------------------------------------------------------- /tests/test_loudness.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pyloudnorm as pyln 3 | import soundfile as sf 4 | import numpy as np 5 | 6 | def test_integrated_loudness(): 7 | 8 | data, rate = sf.read("tests/data/sine_1000.wav") 9 | meter = pyln.Meter(rate) 10 | loudness = meter.integrated_loudness(data) 11 | 12 | assert np.isclose(loudness, -3.0523438444331137) 13 | 14 | def test_peak_normalize(): 15 | 16 | data = np.array(0.5) 17 | norm = pyln.normalize.peak(data, 0.0) 18 | 19 | assert np.isclose(norm, 1.0) 20 | 21 | def test_loudness_normalize(): 22 | 23 | data, rate = sf.read("tests/data/sine_1000.wav") 24 | meter = pyln.Meter(rate) 25 | loudness = meter.integrated_loudness(data) 26 | norm = pyln.normalize.loudness(data, loudness, -6.0) 27 | loudness = meter.integrated_loudness(norm) 28 | 29 | assert np.isclose(loudness, -6.0) 30 | 31 | def test_rel_gate_test(): 32 | 33 | data, rate = sf.read("tests/data/1770-2_Comp_RelGateTest.wav") 34 | meter = pyln.Meter(rate) 35 | loudness = meter.integrated_loudness(data) 36 | 37 | targetLoudness = -10.0 38 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 39 | 40 | def test_abs_gate_test(): 41 | 42 | data, rate = sf.read("tests/data/1770-2_Comp_AbsGateTest.wav") 43 | meter = pyln.Meter(rate) 44 | loudness = meter.integrated_loudness(data) 45 | 46 | targetLoudness = -69.5 47 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 48 | 49 | def test_24LKFS_25Hz_2ch(): 50 | 51 | data, rate = sf.read("tests/data/1770-2_Comp_24LKFS_25Hz_2ch.wav") 52 | meter = pyln.Meter(rate) 53 | loudness = meter.integrated_loudness(data) 54 | 55 | targetLoudness = -24.0 56 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 57 | 58 | def test_24LKFS_100Hz_2ch(): 59 | 60 | data, rate = sf.read("tests/data/1770-2_Comp_24LKFS_100Hz_2ch.wav") 61 | meter = pyln.Meter(rate) 62 | loudness = meter.integrated_loudness(data) 63 | 64 | targetLoudness = -24.0 65 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 66 | 67 | def test_24LKFS_500Hz_2ch(): 68 | 69 | data, rate = sf.read("tests/data/1770-2_Comp_24LKFS_500Hz_2ch.wav") 70 | meter = pyln.Meter(rate) 71 | loudness = meter.integrated_loudness(data) 72 | 73 | 74 | targetLoudness = -24.0 75 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 76 | 77 | def test_24LKFS_1000Hz_2ch(): 78 | 79 | data, rate = sf.read("tests/data/1770-2_Comp_24LKFS_1000Hz_2ch.wav") 80 | meter = pyln.Meter(rate) 81 | loudness = meter.integrated_loudness(data) 82 | 83 | targetLoudness = -24.0 84 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 85 | 86 | def test_24LKFS_2000Hz_2ch(): 87 | 88 | data, rate = sf.read("tests/data/1770-2_Comp_24LKFS_2000Hz_2ch.wav") 89 | meter = pyln.Meter(rate) 90 | loudness = meter.integrated_loudness(data) 91 | 92 | targetLoudness = -24.0 93 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 94 | 95 | def test_24LKFS_10000Hz_2ch(): 96 | 97 | data, rate = sf.read("tests/data/1770-2_Comp_24LKFS_10000Hz_2ch.wav") 98 | meter = pyln.Meter(rate) 99 | loudness = meter.integrated_loudness(data) 100 | 101 | targetLoudness = -24.0 102 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 103 | 104 | def test_23LKFS_25Hz_2ch(): 105 | 106 | data, rate = sf.read("tests/data/1770-2_Comp_23LKFS_25Hz_2ch.wav") 107 | meter = pyln.Meter(rate) 108 | loudness = meter.integrated_loudness(data) 109 | 110 | targetLoudness = -23.0 111 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 112 | 113 | def test_23LKFS_100Hz_2ch(): 114 | 115 | data, rate = sf.read("tests/data/1770-2_Comp_23LKFS_100Hz_2ch.wav") 116 | meter = pyln.Meter(rate) 117 | loudness = meter.integrated_loudness(data) 118 | 119 | targetLoudness = -23.0 120 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 121 | 122 | def test_23LKFS_500Hz_2ch(): 123 | 124 | data, rate = sf.read("tests/data/1770-2_Comp_23LKFS_500Hz_2ch.wav") 125 | meter = pyln.Meter(rate) 126 | loudness = meter.integrated_loudness(data) 127 | 128 | targetLoudness = -23.0 129 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 130 | 131 | def test_23LKFS_1000Hz_2ch(): 132 | 133 | data, rate = sf.read("tests/data/1770-2_Comp_23LKFS_1000Hz_2ch.wav") 134 | meter = pyln.Meter(rate) 135 | loudness = meter.integrated_loudness(data) 136 | 137 | targetLoudness = -23.0 138 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 139 | 140 | def test_23LKFS_2000Hz_2ch(): 141 | 142 | data, rate = sf.read("tests/data/1770-2_Comp_23LKFS_2000Hz_2ch.wav") 143 | meter = pyln.Meter(rate) 144 | loudness = meter.integrated_loudness(data) 145 | 146 | targetLoudness = -23.0 147 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 148 | 149 | def test_23LKFS_10000Hz_2ch(): 150 | 151 | data, rate = sf.read("tests/data/1770-2_Comp_23LKFS_10000Hz_2ch.wav") 152 | meter = pyln.Meter(rate) 153 | loudness = meter.integrated_loudness(data) 154 | 155 | targetLoudness = -23.0 156 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 157 | 158 | def test_18LKFS_frequency_sweep(): 159 | 160 | data, rate = sf.read("tests/data/1770-2_Comp_18LKFS_FrequencySweep.wav") 161 | meter = pyln.Meter(rate) 162 | loudness = meter.integrated_loudness(data) 163 | 164 | targetLoudness = -18.0 165 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 166 | 167 | def test_conf_stereo_vinL_R_23LKFS(): 168 | 169 | data, rate = sf.read("tests/data/1770-2_Conf_Stereo_VinL+R-23LKFS.wav") 170 | meter = pyln.Meter(rate) 171 | loudness = meter.integrated_loudness(data) 172 | 173 | targetLoudness = -23.0 174 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 175 | 176 | def test_conf_monovoice_music_24LKFS(): 177 | 178 | data, rate = sf.read("tests/data/1770-2_Conf_Mono_Voice+Music-24LKFS.wav") 179 | meter = pyln.Meter(rate) 180 | loudness = meter.integrated_loudness(data) 181 | 182 | targetLoudness = -24.0 183 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 184 | 185 | def conf_monovoice_music_24LKFS(): 186 | 187 | data, rate = sf.read("tests/data/1770-2_Conf_Mono_Voice+Music-24LKFS.wav") 188 | meter = pyln.Meter(rate) 189 | loudness = meter.integrated_loudness(data) 190 | 191 | targetLoudness = -24.0 192 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 193 | 194 | def test_conf_monovoice_music_23LKFS(): 195 | 196 | data, rate = sf.read("tests/data/1770-2_Conf_Mono_Voice+Music-23LKFS.wav") 197 | meter = pyln.Meter(rate) 198 | loudness = meter.integrated_loudness(data) 199 | 200 | targetLoudness = -23.0 201 | assert targetLoudness - 0.1 <= loudness <= targetLoudness + 0.1 --------------------------------------------------------------------------------