Initial push of code and tests
This commit is contained in:
parent
976023cb59
commit
c2fc68f7af
14 changed files with 1354 additions and 0 deletions
12
MANIFEST.in
Normal file
12
MANIFEST.in
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
prune *
|
||||||
|
graft tests
|
||||||
|
graft src/pyebur128
|
||||||
|
include src/lib/COPYING
|
||||||
|
include src/lib/README.md
|
||||||
|
include src/lib/ebur128/ebur128.c
|
||||||
|
include src/lib/ebur128/ebur128.h
|
||||||
|
include src/lib/ebur128/queue/sys/queue.h
|
||||||
|
|
||||||
|
include LICENSE README.md pyproject.toml setup.py setup.cfg
|
||||||
|
exclude CHANGELOG.md CONTRIBUTING.md mkdocs.yml mkdocs_macros.py
|
||||||
|
global-exclude __pycache__ *.py[cod] .*
|
16
pyproject.toml
Normal file
16
pyproject.toml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
[build-system]
|
||||||
|
requires = [
|
||||||
|
'wheel',
|
||||||
|
'setuptools',
|
||||||
|
'Cython',
|
||||||
|
'setuptools_scm[toml]',
|
||||||
|
]
|
||||||
|
build-backend = 'setuptools.build_meta'
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = '6.0'
|
||||||
|
addopts = '--cov-report=xml --cov-report=term:skip-covered --cov=pyebur128'
|
||||||
|
testpaths = ['tests']
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
write_to = 'src/pyebur128/version.py'
|
74
setup.cfg
Normal file
74
setup.cfg
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
[metadata]
|
||||||
|
name = pyebur128
|
||||||
|
version = attr: pyebur128.__version__
|
||||||
|
url = https://github.com/jodhus/pyebur128/
|
||||||
|
license = MIT
|
||||||
|
license_files = LICENSE
|
||||||
|
author = Josh Washburne
|
||||||
|
author_email = josh@jodh.us
|
||||||
|
maintainer = Josh Washburne
|
||||||
|
description = A Cython implementation of the libebur128 library for measuring audio loudness.
|
||||||
|
long_description = file: README.md
|
||||||
|
long_description_content_type = text/markdown
|
||||||
|
project_urls =
|
||||||
|
Documentation = https://github.com/jodhus/pyebur128/
|
||||||
|
Source = https://github.com/jodhus/pyebur128/
|
||||||
|
Bug Tracker = https://github.com/jodhus/pyebur128/issues/
|
||||||
|
keywords =
|
||||||
|
audio loudness
|
||||||
|
loudness range
|
||||||
|
loudness units
|
||||||
|
true peak
|
||||||
|
sample peak
|
||||||
|
relative threshold
|
||||||
|
ebu r128
|
||||||
|
itu r bs 1770
|
||||||
|
classifiers =
|
||||||
|
Development Status :: 4 - Beta
|
||||||
|
Intended Audience :: Developers
|
||||||
|
License :: OSI Approved :: MIT License
|
||||||
|
Natural Language :: English
|
||||||
|
Operating System :: MacOS :: MacOS X
|
||||||
|
Operating System :: POSIX
|
||||||
|
Operating System :: Unix
|
||||||
|
Operating System :: Microsoft :: Windows
|
||||||
|
Programming Language :: Cython
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Programming Language :: Python :: 3.6
|
||||||
|
Programming Language :: Python :: 3.7
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
|
Topic :: Software Development :: Libraries :: Python Modules
|
||||||
|
Topic :: Multimedia :: Sound/Audio
|
||||||
|
Topic :: Multimedia :: Sound/Audio :: Analysis
|
||||||
|
|
||||||
|
[options]
|
||||||
|
zip_safe = True
|
||||||
|
include_package_data = True
|
||||||
|
python_requires = >= 3.6
|
||||||
|
install_requires =
|
||||||
|
packages = find:
|
||||||
|
package_dir =
|
||||||
|
=src
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
where = src
|
||||||
|
|
||||||
|
[options.extras_require]
|
||||||
|
tests =
|
||||||
|
numpy
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
SoundFile
|
||||||
|
dev =
|
||||||
|
cython
|
||||||
|
flake8
|
||||||
|
docs =
|
||||||
|
mkdocs
|
||||||
|
mkdocs-macros-plugin
|
||||||
|
mkdocs-material
|
||||||
|
mkdocstrings
|
||||||
|
pymdown-extensions
|
||||||
|
|
||||||
|
[build_ext]
|
||||||
|
inplace=1
|
59
setup.py
Normal file
59
setup.py
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import platform
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages, Extension
|
||||||
|
from distutils.ccompiler import new_compiler
|
||||||
|
from distutils.msvccompiler import MSVCCompiler
|
||||||
|
|
||||||
|
|
||||||
|
def is_msvc():
|
||||||
|
'''Checks to see if the detected C compiler is MSVC.'''
|
||||||
|
try:
|
||||||
|
# This depends on _winreg, which is not available on not-Windows.
|
||||||
|
from distutils.msvc9compiler import MSVCCompiler as MSVC9Compiler
|
||||||
|
except ImportError:
|
||||||
|
MSVC9Compiler = None
|
||||||
|
try:
|
||||||
|
from distutils._msvccompiler import MSVCCompiler as MSVC14Compiler
|
||||||
|
except ImportError:
|
||||||
|
MSVC14Compiler = None
|
||||||
|
msvc_classes = tuple(filter(None, (MSVCCompiler,
|
||||||
|
MSVC9Compiler,
|
||||||
|
MSVC14Compiler)))
|
||||||
|
cc = new_compiler()
|
||||||
|
return isinstance(cc, msvc_classes)
|
||||||
|
|
||||||
|
|
||||||
|
macros = []
|
||||||
|
|
||||||
|
# MSVC won't use <math.h> unless this is defined.
|
||||||
|
if platform.system() == 'Windows' and is_msvc():
|
||||||
|
macros.append(('_USE_MATH_DEFINES', None))
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
Extension(
|
||||||
|
name='pyebur128.pyebur128',
|
||||||
|
sources=[
|
||||||
|
"src/pyebur128/pyebur128.pyx",
|
||||||
|
"src/lib/ebur128/ebur128.c",
|
||||||
|
],
|
||||||
|
include_dirs=[
|
||||||
|
'.',
|
||||||
|
'src/lib/ebur128',
|
||||||
|
'src/lib/ebur128/queue',
|
||||||
|
],
|
||||||
|
define_macros=macros,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from Cython.Build import cythonize
|
||||||
|
|
||||||
|
setup(
|
||||||
|
ext_modules=cythonize(
|
||||||
|
extensions,
|
||||||
|
compiler_directives={'language_level': 3, 'embedsignature': True},
|
||||||
|
),
|
||||||
|
)
|
14
src/pyebur128/__init__.py
Normal file
14
src/pyebur128/__init__.py
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
from pyebur128.pyebur128 import (
|
||||||
|
ChannelType, ErrorCode, MeasurementMode,
|
||||||
|
R128State,
|
||||||
|
get_loudness_global, get_loudness_global_multiple,
|
||||||
|
get_loudness_momentary, get_loudness_shortterm, get_loudness_window,
|
||||||
|
get_loudness_range, get_loudness_range_multiple,
|
||||||
|
get_sample_peak, get_previous_sample_peak,
|
||||||
|
get_true_peak, get_previous_true_peak,
|
||||||
|
get_relative_threshold,
|
||||||
|
get_libebur128_version
|
||||||
|
)
|
||||||
|
|
||||||
|
# Quick access to the version
|
||||||
|
from .version import version as __version__
|
140
src/pyebur128/pyebur128.pxd
Normal file
140
src/pyebur128/pyebur128.pxd
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
cdef extern from "../lib/ebur128/ebur128.h":
|
||||||
|
|
||||||
|
ctypedef enum Channel "channel":
|
||||||
|
EBUR128_UNUSED = 0
|
||||||
|
EBUR128_LEFT = 1
|
||||||
|
EBUR128_Mp030 = 1
|
||||||
|
EBUR128_RIGHT = 2
|
||||||
|
EBUR128_Mm030 = 2
|
||||||
|
EBUR128_CENTER = 3
|
||||||
|
EBUR128_Mp000 = 3
|
||||||
|
EBUR128_LEFT_SURROUND = 4
|
||||||
|
EBUR128_Mp110 = 4
|
||||||
|
EBUR128_RIGHT_SURROUND = 5
|
||||||
|
EBUR128_Mm110 = 5
|
||||||
|
EBUR128_DUAL_MONO
|
||||||
|
EBUR128_MpSC
|
||||||
|
EBUR128_MmSC
|
||||||
|
EBUR128_Mp060
|
||||||
|
EBUR128_Mm060
|
||||||
|
EBUR128_Mp090
|
||||||
|
EBUR128_Mm090
|
||||||
|
EBUR128_Mp135
|
||||||
|
EBUR128_Mm135
|
||||||
|
EBUR128_Mp180
|
||||||
|
EBUR128_Up000
|
||||||
|
EBUR128_Up030
|
||||||
|
EBUR128_Um030
|
||||||
|
EBUR128_Up045
|
||||||
|
EBUR128_Um045
|
||||||
|
EBUR128_Up090
|
||||||
|
EBUR128_Um090
|
||||||
|
EBUR128_Up110
|
||||||
|
EBUR128_Um110
|
||||||
|
EBUR128_Up135
|
||||||
|
EBUR128_Um135
|
||||||
|
EBUR128_Up180
|
||||||
|
EBUR128_Tp000
|
||||||
|
EBUR128_Bp000
|
||||||
|
EBUR128_Bp045
|
||||||
|
EBUR128_Bm045
|
||||||
|
|
||||||
|
ctypedef enum Error "error":
|
||||||
|
EBUR128_SUCCESS
|
||||||
|
EBUR128_ERROR_NOMEM
|
||||||
|
EBUR128_ERROR_INVALID_MODE
|
||||||
|
EBUR128_ERROR_INVALID_CHANNEL_INDEX
|
||||||
|
EBUR128_ERROR_NO_CHANGE
|
||||||
|
|
||||||
|
ctypedef enum Mode "mode":
|
||||||
|
EBUR128_MODE_M = (1 << 0)
|
||||||
|
EBUR128_MODE_S = (1 << 1) | EBUR128_MODE_M
|
||||||
|
EBUR128_MODE_I = (1 << 2) | EBUR128_MODE_M
|
||||||
|
EBUR128_MODE_LRA = (1 << 3) | EBUR128_MODE_S
|
||||||
|
EBUR128_MODE_SAMPLE_PEAK = (1 << 4) | EBUR128_MODE_M
|
||||||
|
EBUR128_MODE_TRUE_PEAK = (
|
||||||
|
(1 << 5) | EBUR128_MODE_M | EBUR128_MODE_SAMPLE_PEAK
|
||||||
|
)
|
||||||
|
EBUR128_MODE_HISTOGRAM = (1 << 6)
|
||||||
|
|
||||||
|
# forward declaration of ebur128_state_internal
|
||||||
|
struct ebur128_state_internal
|
||||||
|
|
||||||
|
# Contains information about the state of a loudness measurement.
|
||||||
|
# You should not need to modify this struct directly.
|
||||||
|
ctypedef struct ebur128_state:
|
||||||
|
int mode # The current mode.
|
||||||
|
unsigned int channels # The number of channels.
|
||||||
|
unsigned long samplerate # The sample rate.
|
||||||
|
ebur128_state_internal* d # Internal state.
|
||||||
|
|
||||||
|
void ebur128_get_version(int *major, int *minor, int *patch)
|
||||||
|
|
||||||
|
ebur128_state *ebur128_init(unsigned int channels,
|
||||||
|
unsigned long samplerate,
|
||||||
|
int mode)
|
||||||
|
|
||||||
|
void ebur128_destroy(ebur128_state **state)
|
||||||
|
|
||||||
|
int ebur128_set_channel(ebur128_state *state,
|
||||||
|
unsigned int channel_number,
|
||||||
|
int value)
|
||||||
|
|
||||||
|
int ebur128_change_parameters(ebur128_state *state,
|
||||||
|
unsigned int channels,
|
||||||
|
unsigned long samplerate)
|
||||||
|
|
||||||
|
int ebur128_set_max_window(ebur128_state *state, unsigned long window)
|
||||||
|
|
||||||
|
int ebur128_set_max_history(ebur128_state *state, unsigned long history)
|
||||||
|
|
||||||
|
int ebur128_add_frames_short(ebur128_state *state,
|
||||||
|
const short *source,
|
||||||
|
size_t frames)
|
||||||
|
int ebur128_add_frames_int(ebur128_state *state,
|
||||||
|
const int *source,
|
||||||
|
size_t frames)
|
||||||
|
int ebur128_add_frames_float(ebur128_state *state,
|
||||||
|
const float *source,
|
||||||
|
size_t frames)
|
||||||
|
int ebur128_add_frames_double(ebur128_state *state,
|
||||||
|
const double *source,
|
||||||
|
size_t frames)
|
||||||
|
|
||||||
|
int ebur128_loudness_global(ebur128_state *state, double *loudness)
|
||||||
|
|
||||||
|
int ebur128_loudness_global_multiple(ebur128_state **states,
|
||||||
|
size_t size,
|
||||||
|
double *loudness)
|
||||||
|
|
||||||
|
int ebur128_loudness_momentary(ebur128_state *state, double *loudness)
|
||||||
|
|
||||||
|
int ebur128_loudness_shortterm(ebur128_state *state, double *loudness)
|
||||||
|
|
||||||
|
int ebur128_loudness_window(ebur128_state *state,
|
||||||
|
unsigned long window,
|
||||||
|
double *loudness)
|
||||||
|
|
||||||
|
int ebur128_loudness_range(ebur128_state *state, double *loudness)
|
||||||
|
|
||||||
|
int ebur128_loudness_range_multiple(ebur128_state **states,
|
||||||
|
size_t size,
|
||||||
|
double *loudness)
|
||||||
|
|
||||||
|
int ebur128_sample_peak(ebur128_state *state,
|
||||||
|
unsigned int channel_number,
|
||||||
|
double *max_peak)
|
||||||
|
|
||||||
|
int ebur128_prev_sample_peak(ebur128_state *state,
|
||||||
|
unsigned int channel_number,
|
||||||
|
double *max_peak)
|
||||||
|
|
||||||
|
int ebur128_true_peak(ebur128_state *state,
|
||||||
|
unsigned int channel_number,
|
||||||
|
double *max_peak)
|
||||||
|
|
||||||
|
int ebur128_prev_true_peak(ebur128_state *state,
|
||||||
|
unsigned int channel_number,
|
||||||
|
double *max_peak)
|
||||||
|
|
||||||
|
int ebur128_relative_threshold(ebur128_state *state, double *threshold)
|
647
src/pyebur128/pyebur128.pyx
Normal file
647
src/pyebur128/pyebur128.pyx
Normal file
|
@ -0,0 +1,647 @@
|
||||||
|
import enum
|
||||||
|
|
||||||
|
cimport cython
|
||||||
|
from libc.stdlib cimport malloc, free
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelType(enum.IntEnum):
|
||||||
|
'''Use these values when setting the channel map with
|
||||||
|
R128State.set_channel(). See definitions in ITU R-REC-BS 1770-4.
|
||||||
|
'''
|
||||||
|
Unused = 0 # unused channel (for example LFE channel)
|
||||||
|
Left = 1
|
||||||
|
Mplus030 = 1 # itu M+030
|
||||||
|
Right = 2
|
||||||
|
Mminus030 = 2 # itu M-030
|
||||||
|
Center = 3
|
||||||
|
Mplus000 = 3 # itu M+000
|
||||||
|
LeftSurround = 4
|
||||||
|
Mplus110 = 4 # itu M+110
|
||||||
|
RightSuround = 5
|
||||||
|
Mminus110 = 5 # itu M-110
|
||||||
|
DualMono = 6 # a channel that is counted twice
|
||||||
|
MplusSC = 7 # itu M+SC
|
||||||
|
MminusSC = 8 # itu M-SC
|
||||||
|
Mplus060 = 9 # itu M+060
|
||||||
|
Mminus060 = 10 # itu M-060
|
||||||
|
Mplus090 = 11 # itu M+090
|
||||||
|
Mminus090 = 12 # itu M-090
|
||||||
|
Mplus135 = 13 # itu M+135
|
||||||
|
Mminus135 = 14 # itu M-135
|
||||||
|
Mplus180 = 15 # itu M+180
|
||||||
|
Uplus000 = 16 # itu U+000
|
||||||
|
Uplus030 = 17 # itu U+030
|
||||||
|
Uminus030 = 18 # itu U-030
|
||||||
|
Uplus045 = 19 # itu U+045
|
||||||
|
Uminus045 = 20 # itu U-030
|
||||||
|
Uplus090 = 21 # itu U+090
|
||||||
|
Uminus090 = 22 # itu U-090
|
||||||
|
Uplus110 = 23 # itu U+110
|
||||||
|
Uminus110 = 24 # itu U-110
|
||||||
|
Uplus135 = 25 # itu U+135
|
||||||
|
Uminus135 = 26 # itu U-135
|
||||||
|
Uplus180 = 27 # itu U+180
|
||||||
|
Tplus000 = 28 # itu T+000
|
||||||
|
Bplus000 = 29 # itu B+000
|
||||||
|
Bplus045 = 30 # itu B+045
|
||||||
|
Bminus045 = 31 # itu B-045
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorCode(enum.IntEnum):
|
||||||
|
'''Error codes returned by libebur128 functions.'''
|
||||||
|
Success = 0
|
||||||
|
OutOfMemory = 1
|
||||||
|
InvalidMode = 2
|
||||||
|
InvalidChannelIndex = 3
|
||||||
|
ValueDidNotChange = 4
|
||||||
|
|
||||||
|
|
||||||
|
class MeasurementMode(enum.IntFlag):
|
||||||
|
'''Use these values bitwise OR'd when instancing an R128State class.
|
||||||
|
Try to use the lowest possible modes that suit your needs, as performance
|
||||||
|
will be better.
|
||||||
|
'''
|
||||||
|
# can call get_loudness_momentary
|
||||||
|
ModeM = (1 << 0)
|
||||||
|
# can call get_loudness_shortterm
|
||||||
|
ModeS = (1 << 1) | ModeM
|
||||||
|
# can call get_loudness_global_* and get_relative_threshold
|
||||||
|
ModeI = (1 << 2) | ModeM
|
||||||
|
# can call get_loudness_range
|
||||||
|
ModeLRA = (1 << 3) | ModeS
|
||||||
|
# can call get_sample_peak
|
||||||
|
ModeSamplePeak = (1 << 4) | ModeM
|
||||||
|
# can call get_true_peak
|
||||||
|
ModeTruePeak = (1 << 5) | ModeM | ModeSamplePeak
|
||||||
|
# uses histogram algorithm to calculate loudness
|
||||||
|
ModeHistogram = (1 << 6)
|
||||||
|
|
||||||
|
|
||||||
|
ctypedef fused const_frames_array:
|
||||||
|
const short[::1]
|
||||||
|
const int[::1]
|
||||||
|
const float[::1]
|
||||||
|
const double[::1]
|
||||||
|
|
||||||
|
|
||||||
|
cdef class R128State:
|
||||||
|
'''This is a class representation of an EBU R128 Loudness Measurement State.
|
||||||
|
|
||||||
|
:param channels: The number of audio channels used in the measurement.
|
||||||
|
:type channels: int
|
||||||
|
:param samplerate: The samplerate in samples per second (or Hz).
|
||||||
|
:type samplerate: int
|
||||||
|
:param mode: A bitwise OR'd value from the :class:`Mode` enum. Try to use
|
||||||
|
the lowest possible modes that suit your needs, as performance will be
|
||||||
|
better.
|
||||||
|
:type mode: int
|
||||||
|
'''
|
||||||
|
|
||||||
|
# Contains information about the state of a loudness measurement.
|
||||||
|
# You should not need to modify this struct directly.
|
||||||
|
cdef ebur128_state *_state
|
||||||
|
|
||||||
|
def __cinit__(self,
|
||||||
|
unsigned int channels,
|
||||||
|
unsigned long samplerate,
|
||||||
|
int mode):
|
||||||
|
'''Initialize library state.
|
||||||
|
|
||||||
|
:raises MemoryError: If the underlying C-struct cannot be allocated in
|
||||||
|
memory.
|
||||||
|
'''
|
||||||
|
self._state = ebur128_init(channels, samplerate, mode)
|
||||||
|
if self._state == NULL:
|
||||||
|
raise MemoryError('Out of memory.')
|
||||||
|
|
||||||
|
def __dealloc__(self):
|
||||||
|
'''Destroy library state.'''
|
||||||
|
if self._state != NULL:
|
||||||
|
ebur128_destroy(&self._state)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
'''A nicer way of explaining the object.'''
|
||||||
|
obj = '<{0}: channels={1}, samplerate={2}, mode={3} at 0x{4:0{5}X}>'
|
||||||
|
return obj.format(
|
||||||
|
self.__class__.__name__,
|
||||||
|
self.channels,
|
||||||
|
self.samplerate,
|
||||||
|
self.mode.__repr__(),
|
||||||
|
id(self),
|
||||||
|
16
|
||||||
|
)
|
||||||
|
|
||||||
|
property channels:
|
||||||
|
'''The number of audio channels used in the measurement.'''
|
||||||
|
def __get__(self):
|
||||||
|
'''channels' getter'''
|
||||||
|
return self._state.channels if self._state is not NULL else None
|
||||||
|
def __set__(self, unsigned int c):
|
||||||
|
'''channels' setter'''
|
||||||
|
if self._state is not NULL:
|
||||||
|
self.change_parameters(c, self._state.samplerate)
|
||||||
|
|
||||||
|
property samplerate:
|
||||||
|
'''The samplerate in samples per second (or Hz).'''
|
||||||
|
def __get__(self):
|
||||||
|
'''samplerate's getter'''
|
||||||
|
return self._state.samplerate if self._state is not NULL else None
|
||||||
|
def __set__(self, unsigned long s):
|
||||||
|
'''samplerate's setter'''
|
||||||
|
if self._state is not NULL:
|
||||||
|
self.change_parameters(self._state.channels, s)
|
||||||
|
|
||||||
|
property mode:
|
||||||
|
''' A bitwise OR'd value from the :class:`Mode` enum. Try to use
|
||||||
|
the lowest possible modes that suit your needs, as performance will be
|
||||||
|
better.'''
|
||||||
|
def __get__(self):
|
||||||
|
'''mode's getter'''
|
||||||
|
if self._state is not NULL:
|
||||||
|
return MeasurementMode(self._state.mode)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
def __set__(self, int m):
|
||||||
|
'''mode's setter'''
|
||||||
|
if self._state is not NULL:
|
||||||
|
self._state.mode = m
|
||||||
|
|
||||||
|
def set_channel(self, unsigned int channel_number, int channel_type):
|
||||||
|
'''Sets an audio channel to a specific channel type as defined in the
|
||||||
|
:class:`ChannelType` enum.
|
||||||
|
|
||||||
|
:param channel_number: The zero-based channel index.
|
||||||
|
:type channel_number: int
|
||||||
|
:param channel_type: The channel type from :class:`ChannelType`.
|
||||||
|
:type channel_type: int
|
||||||
|
|
||||||
|
:raises IndexError: If specified channel index is out of bounds.
|
||||||
|
'''
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_set_channel(self._state,
|
||||||
|
channel_number,
|
||||||
|
channel_type)
|
||||||
|
if result == ErrorCode.InvalidChannelIndex:
|
||||||
|
raise IndexError('Channel index is out of bounds.')
|
||||||
|
|
||||||
|
def change_parameters(self,
|
||||||
|
unsigned int channels,
|
||||||
|
unsigned long samplerate):
|
||||||
|
'''Changes number of audio channels and/or the samplerate of the
|
||||||
|
loudness measurement. Returns an integer error code.
|
||||||
|
|
||||||
|
Note that the channel map will be reset when setting a different number
|
||||||
|
of channels. The current unfinished block will be lost.
|
||||||
|
|
||||||
|
:param channels: New number of audio channels.
|
||||||
|
:type channels: int
|
||||||
|
:param samplerate: The new samplerate in samples per second (or Hz).
|
||||||
|
:type samplerate: int
|
||||||
|
|
||||||
|
:raises MemoryError: If not enough memory could be allocated for the
|
||||||
|
new values.
|
||||||
|
:raises ValueError: If both new values are the same as the currently
|
||||||
|
stored values.
|
||||||
|
'''
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_change_parameters(self._state,
|
||||||
|
channels,
|
||||||
|
samplerate)
|
||||||
|
if result == ErrorCode.OutOfMemory:
|
||||||
|
raise MemoryError('Out of memory.')
|
||||||
|
elif result == ErrorCode.ValueDidNotChange:
|
||||||
|
raise ValueError('Channel numbers & sample rate have not changed.')
|
||||||
|
|
||||||
|
def set_max_window(self, unsigned long window):
|
||||||
|
'''Set the maximum duration that will be used for
|
||||||
|
:func:`~pyebur128.get_loudness_window`.
|
||||||
|
|
||||||
|
Note that this destroys the current content of the audio buffer.
|
||||||
|
|
||||||
|
:param window: The duration of the window in milliseconds (ms).
|
||||||
|
:type window: int
|
||||||
|
|
||||||
|
:raises MemoryError: If not enough memory could be allocated for the
|
||||||
|
new value.
|
||||||
|
:raises ValueError: If the new window value is the same as the
|
||||||
|
currently stored window value.
|
||||||
|
'''
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_set_max_window(self._state, window)
|
||||||
|
if result == ErrorCode.OutOfMemory:
|
||||||
|
raise MemoryError('Out of memory.')
|
||||||
|
elif result == ErrorCode.ValueDidNotChange:
|
||||||
|
raise ValueError('Maximum window duration has not changed.')
|
||||||
|
|
||||||
|
def set_max_history(self, unsigned long history):
|
||||||
|
'''Set the maximum history that will be stored for loudness integration.
|
||||||
|
More history provides more accurate results, but requires more
|
||||||
|
resources.
|
||||||
|
|
||||||
|
Applies to :func:`~pyebur128.get_loudness_range` and
|
||||||
|
:func:`~pyebur128.get_loudness_global` when ``ModeHistogram`` is
|
||||||
|
not set from :class:`pyebur128.MeasurementMode`.
|
||||||
|
|
||||||
|
Default is ULONG_MAX (at least ~50 days).
|
||||||
|
Minimum is 3000ms for ``ModeLRA`` and 400ms for ``ModeM``.
|
||||||
|
|
||||||
|
:param history: The duration of history in milliseconds (ms).
|
||||||
|
:type history: int
|
||||||
|
|
||||||
|
:raises MemoryError: If not enough memory could be allocated for the
|
||||||
|
new value.
|
||||||
|
:raises ValueError: If the new history value is the same as the
|
||||||
|
currently stored history value.
|
||||||
|
'''
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_set_max_history(self._state, history)
|
||||||
|
if result == ErrorCode.ValueDidNotChange:
|
||||||
|
raise ValueError('Maximum history duration has not changed.')
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
def add_frames(self, const_frames_array source, size_t frames):
|
||||||
|
'''Add frames to be processed.
|
||||||
|
|
||||||
|
:param source: An array of source frames. Channels must be interleaved.
|
||||||
|
:type source: New buffer protocol (PEP-3118) array of short, int, float,
|
||||||
|
or double.
|
||||||
|
:param frames: The number of frames. (Not the number of samples!)
|
||||||
|
:type frames: int
|
||||||
|
|
||||||
|
:raises MemoryError: If not enough memory could be allocated for the
|
||||||
|
new frames.
|
||||||
|
'''
|
||||||
|
cdef int result
|
||||||
|
|
||||||
|
if const_frames_array is short[::1]:
|
||||||
|
result = ebur128_add_frames_short(self._state,
|
||||||
|
&source[0],
|
||||||
|
frames)
|
||||||
|
elif const_frames_array is int[::1]:
|
||||||
|
result = ebur128_add_frames_int(self._state,
|
||||||
|
&source[0],
|
||||||
|
frames)
|
||||||
|
elif const_frames_array is float[::1]:
|
||||||
|
result = ebur128_add_frames_float(self._state,
|
||||||
|
&source[0],
|
||||||
|
frames)
|
||||||
|
elif const_frames_array is double[::1]:
|
||||||
|
result = ebur128_add_frames_double(self._state,
|
||||||
|
&source[0],
|
||||||
|
frames)
|
||||||
|
|
||||||
|
if result == ErrorCode.OutOfMemory:
|
||||||
|
raise MemoryError('Out of memory.')
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_loudness_global(R128State state):
|
||||||
|
'''Get the global integrated loudness in LUFS.
|
||||||
|
|
||||||
|
:param state: An instance of the :class:`R128State` class.
|
||||||
|
:type state: R128State
|
||||||
|
|
||||||
|
:raises ValueError: If Mode ``ModeI`` has not been set.
|
||||||
|
|
||||||
|
:return: The integrated loudness in LUFS.
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double lufs
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_loudness_global(state._state, &lufs)
|
||||||
|
if result == ErrorCode.InvalidMode:
|
||||||
|
raise ValueError('Mode "ModeI" has not been set.')
|
||||||
|
return lufs
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_loudness_global_multiple(list states):
|
||||||
|
'''Get the global integrated loudness in LUFS across multiple instances.
|
||||||
|
|
||||||
|
:param state: A list of :class:`R128State` instances.
|
||||||
|
:type state: list of R128State
|
||||||
|
|
||||||
|
:raises MemoryError: If not enough memory could be allocated for the
|
||||||
|
conversion of a Python list to a C array.
|
||||||
|
:raises ValueError: If Mode ``ModeI`` has not been set.
|
||||||
|
|
||||||
|
:return: The integrated loudness in LUFS.
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double lufs
|
||||||
|
cdef int result
|
||||||
|
cdef size_t i, num
|
||||||
|
cdef ebur128_state **state_ptrs
|
||||||
|
|
||||||
|
num = len(states)
|
||||||
|
state_ptrs = <ebur128_state**>malloc(sizeof(ebur128_state*) * num)
|
||||||
|
if state_ptrs == NULL:
|
||||||
|
raise MemoryError('Unable to allocate array of R128 states.')
|
||||||
|
|
||||||
|
for i in range(num):
|
||||||
|
state_ptrs[i] = (<R128State?>states[i])._state
|
||||||
|
|
||||||
|
result = ebur128_loudness_global_multiple(state_ptrs, num, &lufs)
|
||||||
|
free(state_ptrs)
|
||||||
|
if result == ErrorCode.InvalidMode:
|
||||||
|
raise ValueError('Mode "ModeI" has not been set.')
|
||||||
|
return lufs
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_loudness_momentary(R128State state):
|
||||||
|
'''Get the momentary loudness (last 400ms) in LUFS.
|
||||||
|
|
||||||
|
:param state: An instance of the :class:`R128State` class.
|
||||||
|
:type state: R128State
|
||||||
|
|
||||||
|
:return: The momentary loudness in LUFS.
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double lufs
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_loudness_momentary(state._state, &lufs)
|
||||||
|
return lufs
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_loudness_shortterm(R128State state):
|
||||||
|
'''Get the short-term loudness (last 3s) in LUFS.
|
||||||
|
|
||||||
|
:param state: An instance of the :class:`R128State` class.
|
||||||
|
:type state: R128State
|
||||||
|
|
||||||
|
:raises ValueError: If Mode ``ModeS`` has not been set.
|
||||||
|
|
||||||
|
:return: The short-term loudness in LUFS.
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double lufs
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_loudness_shortterm(state._state, &lufs)
|
||||||
|
if result == ErrorCode.InvalidMode:
|
||||||
|
raise ValueError('Mode "ModeS" has not been set.')
|
||||||
|
return lufs
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_loudness_window(R128State state, unsigned long window):
|
||||||
|
'''Get loudness of the specified window in LUFS.
|
||||||
|
|
||||||
|
:param state: An instance of the :class:`R128State` class.
|
||||||
|
:type state: R128State
|
||||||
|
:param window: The window size in milliseconds (ms) to calculate loudness.
|
||||||
|
:type window: int
|
||||||
|
|
||||||
|
:raises ValueError: If the new window size is larger than the current
|
||||||
|
window size stored in state.
|
||||||
|
|
||||||
|
:return: The loudness in LUFS.
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double lufs
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_loudness_window(state._state, window, &lufs)
|
||||||
|
if result == ErrorCode.InvalidMode:
|
||||||
|
msg = (
|
||||||
|
'Requested window larger than the current '
|
||||||
|
'window in the provided state.'
|
||||||
|
)
|
||||||
|
raise ValueError(msg)
|
||||||
|
return lufs
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_loudness_range(R128State state):
|
||||||
|
'''Get loudness range (LRA) of audio in LU.
|
||||||
|
|
||||||
|
Calculates loudness range according to EBU 3342.
|
||||||
|
|
||||||
|
:param state: An instance of the :class:`R128State` class.
|
||||||
|
:type state: R128State
|
||||||
|
|
||||||
|
:raises MemoryError: If not enough memory could be allocated for the
|
||||||
|
measurement.
|
||||||
|
:raises ValueError: If Mode ``ModeLRA`` has not been set.
|
||||||
|
|
||||||
|
:return: The loudness range (LRA) in LU.
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double lufs
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_loudness_range(state._state, &lufs)
|
||||||
|
if result == ErrorCode.OutOfMemory:
|
||||||
|
raise MemoryError('Memory allocation error.')
|
||||||
|
elif result == ErrorCode.InvalidMode:
|
||||||
|
raise ValueError('Mode "ModeLRA" has not been set.')
|
||||||
|
return lufs
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_loudness_range_multiple(list states):
|
||||||
|
'''Get loudness range (LRA) of audio in LU across multiple instances.
|
||||||
|
|
||||||
|
Calculates loudness range according to EBU 3342.
|
||||||
|
|
||||||
|
:param state: A list of :class:`R128State` instances.
|
||||||
|
:type state: list of R128State
|
||||||
|
|
||||||
|
:raises MemoryError: If not enough memory could be allocated for the
|
||||||
|
measurement or there was a problem with the Python list to C array
|
||||||
|
conversion.
|
||||||
|
:raises ValueError: If Mode ``ModeLRA`` has not been set.
|
||||||
|
|
||||||
|
:return: The loudness range (LRA) in LU.
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double lufs
|
||||||
|
cdef int result
|
||||||
|
cdef size_t i, num
|
||||||
|
cdef ebur128_state **state_ptrs
|
||||||
|
|
||||||
|
num = len(states)
|
||||||
|
state_ptrs = <ebur128_state**>malloc(sizeof(ebur128_state*) * num)
|
||||||
|
if state_ptrs == NULL:
|
||||||
|
raise MemoryError('Unable to allocate array of R128 states.')
|
||||||
|
|
||||||
|
for i in range(num):
|
||||||
|
state_ptrs[i] = (<R128State?>states[i])._state
|
||||||
|
|
||||||
|
result = ebur128_loudness_range_multiple(state_ptrs, num, &lufs)
|
||||||
|
free(state_ptrs)
|
||||||
|
if result == ErrorCode.OutOfMemory:
|
||||||
|
raise MemoryError('Memory allocation error.')
|
||||||
|
elif result == ErrorCode.InvalidMode:
|
||||||
|
raise ValueError('Mode "ModeLRA" has not been set.')
|
||||||
|
return lufs
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_sample_peak(R128State state, unsigned int channel_number):
|
||||||
|
'''Get maximum sample peak from all frames that have been processed.
|
||||||
|
|
||||||
|
The equation to convert to dBFS is: 20 * log10(result).
|
||||||
|
|
||||||
|
:param state: An instance of the :class:`R128State` class.
|
||||||
|
:type state: R128State
|
||||||
|
:param channel_number: The index of the channel to analyze.
|
||||||
|
:type channel_number: int
|
||||||
|
|
||||||
|
:raises ValueError: If Mode ``ModeSamplePeak`` has not been set or the
|
||||||
|
channel index is out of bounds.
|
||||||
|
|
||||||
|
:return: The maximum sample peak (1.0 is 0 dBFS).
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double max_peak
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_sample_peak(state._state, channel_number, &max_peak)
|
||||||
|
if result == ErrorCode.InvalidMode:
|
||||||
|
raise ValueError('Mode "ModeSamplePeak" has not been set.')
|
||||||
|
elif result == ErrorCode.InvalidChannelIndex:
|
||||||
|
raise ValueError('Invalid channel index provided.')
|
||||||
|
return max_peak
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_previous_sample_peak(R128State state,
|
||||||
|
unsigned int channel_number):
|
||||||
|
'''Get maximum sample peak from the last call to add_frames().
|
||||||
|
|
||||||
|
The equation to convert to dBFS is: 20 * log10(result).
|
||||||
|
|
||||||
|
:param state: An instance of the :class:`R128State` class.
|
||||||
|
:type state: R128State
|
||||||
|
:param channel_number: The index of the channel to analyze.
|
||||||
|
:type channel_number: int
|
||||||
|
|
||||||
|
:raises ValueError: If Mode ``ModeSamplePeak`` has not been set or the
|
||||||
|
channel index is out of bounds.
|
||||||
|
|
||||||
|
:return: The maximum sample peak (1.0 is 0 dBFS).
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double max_peak
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_prev_sample_peak(state._state, channel_number, &max_peak)
|
||||||
|
if result == ErrorCode.InvalidMode:
|
||||||
|
raise ValueError('Mode "ModeSamplePeak" has not been set.')
|
||||||
|
elif result == ErrorCode.InvalidChannelIndex:
|
||||||
|
raise ValueError('Invalid channel index provided.')
|
||||||
|
return max_peak
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_true_peak(R128State state, unsigned int channel_number):
|
||||||
|
'''Get maximum true peak from all frames that have been processed.
|
||||||
|
|
||||||
|
Uses an implementation defined algorithm to calculate the true peak. Do not
|
||||||
|
try to compare resulting values across different versions of the library,
|
||||||
|
as the algorithm may change.
|
||||||
|
|
||||||
|
The current implementation uses a custom polyphase FIR interpolator to
|
||||||
|
calculate true peak. Will oversample 4x for sample rates < 96000 Hz, 2x for
|
||||||
|
sample rates < 192000 Hz and leave the signal unchanged for 192000 Hz.
|
||||||
|
|
||||||
|
The equation to convert to dBTP is: 20 * log10(out)
|
||||||
|
|
||||||
|
:param state: An instance of the :class:`R128State` class.
|
||||||
|
:type state: R128State
|
||||||
|
:param channel_number: The index of the channel to analyze.
|
||||||
|
:type channel_number: int
|
||||||
|
|
||||||
|
:raises ValueError: If Mode ``ModeTruePeak`` has not been set or the
|
||||||
|
channel index is out of bounds.
|
||||||
|
|
||||||
|
:return: The maximum true peak (1.0 is 0 dBTP).
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double max_peak
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_true_peak(state._state, channel_number, &max_peak)
|
||||||
|
if result == ErrorCode.InvalidMode:
|
||||||
|
raise ValueError('Mode "ModeTruePeak" has not been set.')
|
||||||
|
elif result == ErrorCode.InvalidChannelIndex:
|
||||||
|
raise ValueError('Invalid channel index provided.')
|
||||||
|
return max_peak
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_previous_true_peak(R128State state,
|
||||||
|
unsigned int channel_number):
|
||||||
|
'''Get maximum true peak from the last call to add_frames().
|
||||||
|
|
||||||
|
Uses an implementation defined algorithm to calculate the true peak. Do not
|
||||||
|
try to compare resulting values across different versions of the library,
|
||||||
|
as the algorithm may change.
|
||||||
|
|
||||||
|
The current implementation uses a custom polyphase FIR interpolator to
|
||||||
|
calculate true peak. Will oversample 4x for sample rates < 96000 Hz, 2x for
|
||||||
|
sample rates < 192000 Hz and leave the signal unchanged for 192000 Hz.
|
||||||
|
|
||||||
|
The equation to convert to dBTP is: 20 * log10(out)
|
||||||
|
|
||||||
|
:param state: An instance of the :class:`R128State` class.
|
||||||
|
:type state: R128State
|
||||||
|
:param channel_number: The index of the channel to analyze.
|
||||||
|
:type channel_number: int
|
||||||
|
|
||||||
|
:raises ValueError: If Mode ``ModeTruePeak`` has not been set or the
|
||||||
|
channel index is out of bounds.
|
||||||
|
|
||||||
|
:return: The maximum true peak (1.0 is 0 dBTP).
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double max_peak
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_prev_true_peak(state._state, channel_number, &max_peak)
|
||||||
|
if result == ErrorCode.InvalidMode:
|
||||||
|
raise ValueError('Mode "ModeTruePeak" has not been set.')
|
||||||
|
elif result == ErrorCode.InvalidChannelIndex:
|
||||||
|
raise ValueError('Invalid channel index provided.')
|
||||||
|
return max_peak
|
||||||
|
|
||||||
|
|
||||||
|
@cython.boundscheck(False)
|
||||||
|
@cython.wraparound(False)
|
||||||
|
cpdef double get_relative_threshold(R128State state):
|
||||||
|
'''Get relative threshold in LUFS.
|
||||||
|
|
||||||
|
:param state: An instance of the :class:`R128State` class.
|
||||||
|
:type state: R128State
|
||||||
|
|
||||||
|
:raises ValueError: If Mode ``ModeI`` has not been set.
|
||||||
|
|
||||||
|
:return: The relative threshold in LUFS.
|
||||||
|
:rtype: float
|
||||||
|
'''
|
||||||
|
cdef double threshold
|
||||||
|
cdef int result
|
||||||
|
result = ebur128_relative_threshold(state._state, &threshold)
|
||||||
|
if result == ErrorCode.InvalidMode:
|
||||||
|
raise ValueError('Mode "ModeI" has not been set.')
|
||||||
|
return threshold
|
||||||
|
|
||||||
|
|
||||||
|
cpdef get_libebur128_version():
|
||||||
|
'''Gets the version number of the compiled libebur128.
|
||||||
|
|
||||||
|
:return: The major, minor, and patch numbers of the implemented libebur128
|
||||||
|
version.
|
||||||
|
:rtype: tuple of int
|
||||||
|
'''
|
||||||
|
cdef int major, minor, patch
|
||||||
|
ebur128_get_version(&major, &minor, &patch)
|
||||||
|
return major, minor, patch
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
30
tests/conftest.py
Normal file
30
tests/conftest.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import pytest
|
||||||
|
from urllib import request
|
||||||
|
from zipfile import ZipFile
|
||||||
|
|
||||||
|
|
||||||
|
states = {}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def r128_test_data(tmp_path_factory):
|
||||||
|
'''Download and extract the test WAV files.'''
|
||||||
|
|
||||||
|
# The latest data can be found here:
|
||||||
|
# https://tech.ebu.ch/publications/ebu_loudness_test_set
|
||||||
|
url = 'https://tech.ebu.ch/files/live/sites/tech/files/shared/testmaterial/ebu-loudness-test-setv05.zip' # noqa
|
||||||
|
z = tmp_path_factory.getbasetemp() / 'ebu-loudness-test-setv05.zip'
|
||||||
|
|
||||||
|
data = request.urlopen(url)
|
||||||
|
with open(z, 'wb') as fp:
|
||||||
|
fp.write(data.read())
|
||||||
|
|
||||||
|
with ZipFile(z, 'r') as zp:
|
||||||
|
zp.extractall(tmp_path_factory.getbasetemp())
|
||||||
|
|
||||||
|
return tmp_path_factory.getbasetemp()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def r128_states():
|
||||||
|
return states
|
77
tests/test_loudness_global.py
Normal file
77
tests/test_loudness_global.py
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pyebur128 import (
|
||||||
|
ChannelType, MeasurementMode, R128State,
|
||||||
|
get_loudness_global, get_loudness_global_multiple
|
||||||
|
)
|
||||||
|
import soundfile as sf
|
||||||
|
|
||||||
|
|
||||||
|
def get_single_loudness(filename):
|
||||||
|
'''Open the WAV file and get the global loudness.'''
|
||||||
|
with sf.SoundFile(filename) as wav:
|
||||||
|
state = R128State(wav.channels, wav.samplerate, MeasurementMode.ModeI)
|
||||||
|
|
||||||
|
if wav.channels == 5:
|
||||||
|
state.set_channel(0, ChannelType.Left)
|
||||||
|
state.set_channel(1, ChannelType.Right)
|
||||||
|
state.set_channel(2, ChannelType.Center)
|
||||||
|
state.set_channel(3, ChannelType.LeftSurround)
|
||||||
|
state.set_channel(4, ChannelType.RightSuround)
|
||||||
|
|
||||||
|
for sample in wav.read():
|
||||||
|
state.add_frames(sample, 1)
|
||||||
|
|
||||||
|
loudness = get_loudness_global(state)
|
||||||
|
return state, loudness
|
||||||
|
|
||||||
|
|
||||||
|
def test_loudness_global_single(r128_test_data, r128_states):
|
||||||
|
'''Test for the global loudness value of a single file.
|
||||||
|
|
||||||
|
NOTE: the tests have a second unused value of the exact expected value. We
|
||||||
|
are choosing to ignore this for now since it's overkill for most needs.
|
||||||
|
HOWEVER--passing the tests does not mean that the library is 100% EBU R 128
|
||||||
|
compliant either!
|
||||||
|
'''
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
('seq-3341-1-16bit.wav', -23.0, -2.2953556442089987e+01),
|
||||||
|
('seq-3341-2-16bit.wav', -33.0, -3.2959860397340044e+01),
|
||||||
|
('seq-3341-3-16bit-v02.wav', -23.0, -2.2995899818255047e+01),
|
||||||
|
('seq-3341-4-16bit-v02.wav', -23.0, -2.3035918615414182e+01),
|
||||||
|
('seq-3341-5-16bit-v02.wav', -23.0, -2.2949997446096436e+01),
|
||||||
|
('seq-3341-6-5channels-16bit.wav', -23.0, -2.3017157781104373e+01),
|
||||||
|
('seq-3341-6-6channels-WAVEEX-16bit.wav', -23.0, -2.3017157781104373e+01), # noqa
|
||||||
|
('seq-3341-7_seq-3342-5-24bit.wav', -23.0, -2.2980242495081757e+01),
|
||||||
|
('seq-3341-2011-8_seq-3342-6-24bit-v02.wav', -23.0, -2.3009077718930545e+01), # noqa
|
||||||
|
]
|
||||||
|
|
||||||
|
tolerance = 0.1
|
||||||
|
status_msg = '==== \'{}\': want {} \u00b1 {} ---> '
|
||||||
|
|
||||||
|
print('\n')
|
||||||
|
for test in expected:
|
||||||
|
print(status_msg.format(test[0], test[1], tolerance), end='')
|
||||||
|
result = get_single_loudness(r128_test_data / test[0])
|
||||||
|
r128_states[test[0]] = result[0]
|
||||||
|
print('got {} '.format(round(result[1], 1)), end='')
|
||||||
|
assert (round(result[1], 1) <= test[1] + tolerance and
|
||||||
|
round(result[1], 1) >= test[1] - tolerance)
|
||||||
|
print('---> PASSED!')
|
||||||
|
|
||||||
|
|
||||||
|
def test_loudness_global_multiple(r128_states):
|
||||||
|
'''Test for the global loudness value across multiple files.'''
|
||||||
|
states = list(r128_states.values())
|
||||||
|
expected = -23.18758
|
||||||
|
tolerance = 0.05
|
||||||
|
|
||||||
|
print('\n==== want {} \u00b1 {} ---> '.format(expected, tolerance), end='')
|
||||||
|
result = get_loudness_global_multiple(states)
|
||||||
|
print('got {} '.format(round(result, 5)), end='')
|
||||||
|
assert (round(result, 5) >= expected - tolerance and
|
||||||
|
round(result, 5) <= expected + tolerance)
|
||||||
|
print('---> PASSED!')
|
||||||
|
|
||||||
|
r128_states = []
|
82
tests/test_loudness_momentary.py
Normal file
82
tests/test_loudness_momentary.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pyebur128 import (
|
||||||
|
ChannelType, MeasurementMode, R128State, get_loudness_momentary
|
||||||
|
)
|
||||||
|
import soundfile as sf
|
||||||
|
|
||||||
|
|
||||||
|
def get_max_loudness_momentary(filename):
|
||||||
|
'''Open the WAV file and get the loudness in momentary (400ms) chunks.'''
|
||||||
|
with sf.SoundFile(filename) as wav:
|
||||||
|
state = R128State(wav.channels,
|
||||||
|
wav.samplerate,
|
||||||
|
MeasurementMode.ModeM)
|
||||||
|
|
||||||
|
if wav.channels == 5:
|
||||||
|
state.set_channel(0, ChannelType.Left)
|
||||||
|
state.set_channel(1, ChannelType.Right)
|
||||||
|
state.set_channel(2, ChannelType.Center)
|
||||||
|
state.set_channel(3, ChannelType.LeftSurround)
|
||||||
|
state.set_channel(4, ChannelType.RightSuround)
|
||||||
|
|
||||||
|
# 10 ms buffer / 100 Hz refresh rate as 10 Hz refresh rate fails on
|
||||||
|
# several tests.
|
||||||
|
max_momentary = float('-inf')
|
||||||
|
total_frames_read = 0
|
||||||
|
for block in wav.blocks(blocksize=int(wav.samplerate / 100)):
|
||||||
|
frames_read = len(block)
|
||||||
|
total_frames_read += frames_read
|
||||||
|
|
||||||
|
for sample in block:
|
||||||
|
state.add_frames(sample, 1)
|
||||||
|
|
||||||
|
# Invalid results before the first 400 ms.
|
||||||
|
if total_frames_read >= 4 * wav.samplerate / 10:
|
||||||
|
momentary = get_loudness_momentary(state)
|
||||||
|
max_momentary = max(momentary, max_momentary)
|
||||||
|
|
||||||
|
del state
|
||||||
|
|
||||||
|
return max_momentary
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_loudness_momentary(r128_test_data):
|
||||||
|
'''Test for the loudness value of a single file in momentary (400ms)
|
||||||
|
chunks.
|
||||||
|
'''
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
('seq-3341-13-1-24bit.wav', -23.0),
|
||||||
|
('seq-3341-13-2-24bit.wav', -23.0),
|
||||||
|
('seq-3341-13-3-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-4-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-5-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-6-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-7-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-8-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-9-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-10-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-11-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-12-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-13-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-14-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-15-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-16-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-17-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-18-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-19-24bit.wav.wav', -23.0),
|
||||||
|
('seq-3341-13-20-24bit.wav.wav', -23.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
tolerance = 0.1
|
||||||
|
status_msg = '==== \'{}\': want {} \u00b1 {} ---> '
|
||||||
|
|
||||||
|
print('\n')
|
||||||
|
for test in expected:
|
||||||
|
print(status_msg.format(test[0], test[1], tolerance), end='')
|
||||||
|
result = get_max_loudness_momentary(r128_test_data / test[0])
|
||||||
|
print('got {} '.format(round(result, 1)), end='')
|
||||||
|
assert (round(result, 1) <= test[1] + tolerance and
|
||||||
|
round(result, 1) >= test[1] - tolerance)
|
||||||
|
print('---> PASSED!')
|
60
tests/test_loudness_range.py
Normal file
60
tests/test_loudness_range.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pyebur128 import (
|
||||||
|
ChannelType, MeasurementMode, R128State, get_loudness_range
|
||||||
|
)
|
||||||
|
import soundfile as sf
|
||||||
|
|
||||||
|
|
||||||
|
def get_single_loudness_range(filename):
|
||||||
|
'''Open the WAV file and get the loudness range.'''
|
||||||
|
with sf.SoundFile(filename) as wav:
|
||||||
|
state = R128State(wav.channels,
|
||||||
|
wav.samplerate,
|
||||||
|
MeasurementMode.ModeLRA)
|
||||||
|
|
||||||
|
if wav.channels == 5:
|
||||||
|
state.set_channel(0, ChannelType.Left)
|
||||||
|
state.set_channel(1, ChannelType.Right)
|
||||||
|
state.set_channel(2, ChannelType.Center)
|
||||||
|
state.set_channel(3, ChannelType.LeftSurround)
|
||||||
|
state.set_channel(4, ChannelType.RightSuround)
|
||||||
|
|
||||||
|
for sample in wav.read():
|
||||||
|
state.add_frames(sample, 1)
|
||||||
|
|
||||||
|
loudness = get_loudness_range(state)
|
||||||
|
del state
|
||||||
|
|
||||||
|
return loudness
|
||||||
|
|
||||||
|
|
||||||
|
def test_loudness_range(r128_test_data):
|
||||||
|
'''Test for the loudness range value of a single file.
|
||||||
|
|
||||||
|
NOTE: the tests have a second unused value of the exact expected value. We
|
||||||
|
are choosing to ignore this for now since it's overkill for most needs.
|
||||||
|
HOWEVER--passing the tests does not mean that the library is 100% EBU R 128
|
||||||
|
compliant either!
|
||||||
|
'''
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
('seq-3342-1-16bit.wav', 10.0, 1.0001105488329134e+01),
|
||||||
|
('seq-3342-2-16bit.wav', 5.0, 4.9993734051522178e+00),
|
||||||
|
('seq-3342-3-16bit.wav', 20.0, 1.9995064067783115e+01),
|
||||||
|
('seq-3342-4-16bit.wav', 15.0, 1.4999273937723455e+01),
|
||||||
|
('seq-3341-7_seq-3342-5-24bit.wav', 5.0, 4.9747585878473721e+00),
|
||||||
|
('seq-3341-2011-8_seq-3342-6-24bit-v02.wav', 15.0, 1.4993650849123316e+01), # noqa
|
||||||
|
]
|
||||||
|
|
||||||
|
tolerance = 1
|
||||||
|
status_msg = '==== \'{}\': want {} \u00b1 {} ---> '
|
||||||
|
|
||||||
|
print('\n')
|
||||||
|
for test in expected:
|
||||||
|
print(status_msg.format(test[0], test[1], tolerance), end='')
|
||||||
|
result = get_single_loudness_range(r128_test_data / test[0])
|
||||||
|
print('got {} '.format(round(result, 1)), end='')
|
||||||
|
assert (round(result, 1) <= test[1] + tolerance and
|
||||||
|
round(result, 1) >= test[1] - tolerance)
|
||||||
|
print('---> PASSED!')
|
81
tests/test_loudness_shortterm.py
Normal file
81
tests/test_loudness_shortterm.py
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pyebur128 import (
|
||||||
|
ChannelType, MeasurementMode, R128State, get_loudness_shortterm
|
||||||
|
)
|
||||||
|
import soundfile as sf
|
||||||
|
|
||||||
|
|
||||||
|
def get_max_loudness_shortterm(filename):
|
||||||
|
'''Open the WAV file and get the loudness in short-term (3s) chunks.'''
|
||||||
|
with sf.SoundFile(filename) as wav:
|
||||||
|
state = R128State(wav.channels,
|
||||||
|
wav.samplerate,
|
||||||
|
MeasurementMode.ModeS)
|
||||||
|
|
||||||
|
if wav.channels == 5:
|
||||||
|
state.set_channel(0, ChannelType.Left)
|
||||||
|
state.set_channel(1, ChannelType.Right)
|
||||||
|
state.set_channel(2, ChannelType.Center)
|
||||||
|
state.set_channel(3, ChannelType.LeftSurround)
|
||||||
|
state.set_channel(4, ChannelType.RightSuround)
|
||||||
|
|
||||||
|
# 10 ms buffer / 10 Hz refresh rate.
|
||||||
|
max_shortterm = float('-inf')
|
||||||
|
total_frames_read = 0
|
||||||
|
for block in wav.blocks(blocksize=int(wav.samplerate / 10)):
|
||||||
|
frames_read = len(block)
|
||||||
|
total_frames_read += frames_read
|
||||||
|
|
||||||
|
for sample in block:
|
||||||
|
state.add_frames(sample, 1)
|
||||||
|
|
||||||
|
# Invalid results before the first 3 seconds.
|
||||||
|
if total_frames_read >= 3 * wav.samplerate:
|
||||||
|
shortterm = get_loudness_shortterm(state)
|
||||||
|
max_shortterm = max(shortterm, max_shortterm)
|
||||||
|
|
||||||
|
del state
|
||||||
|
|
||||||
|
return max_shortterm
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_loudness_shortterm(r128_test_data):
|
||||||
|
'''Test for the loudness value of a single file in short-term (3s)
|
||||||
|
chunks.
|
||||||
|
'''
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
('seq-3341-10-1-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-2-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-3-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-4-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-5-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-6-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-7-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-8-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-9-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-10-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-11-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-12-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-13-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-14-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-15-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-16-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-17-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-18-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-19-24bit.wav', -23.0),
|
||||||
|
('seq-3341-10-20-24bit.wav', -23.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
tolerance = 0.1
|
||||||
|
status_msg = '==== \'{}\': want {} \u00b1 {} ---> '
|
||||||
|
|
||||||
|
print('\n')
|
||||||
|
for test in expected:
|
||||||
|
print(status_msg.format(test[0], test[1], tolerance), end='')
|
||||||
|
result = get_max_loudness_shortterm(r128_test_data / test[0])
|
||||||
|
print('got {} '.format(round(result, 1)), end='')
|
||||||
|
assert (round(result, 1) <= test[1] + tolerance and
|
||||||
|
round(result, 1) >= test[1] - tolerance)
|
||||||
|
print('---> PASSED!')
|
62
tests/test_max_true_peak.py
Normal file
62
tests/test_max_true_peak.py
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
from math import log10
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from pyebur128 import (
|
||||||
|
ChannelType, MeasurementMode, R128State, get_true_peak
|
||||||
|
)
|
||||||
|
import soundfile as sf
|
||||||
|
|
||||||
|
|
||||||
|
def get_max_true_peak(filename):
|
||||||
|
'''Open the WAV file and get the maximum true loudness peak.'''
|
||||||
|
with sf.SoundFile(filename) as wav:
|
||||||
|
state = R128State(wav.channels,
|
||||||
|
wav.samplerate,
|
||||||
|
MeasurementMode.ModeTruePeak)
|
||||||
|
|
||||||
|
if wav.channels == 5:
|
||||||
|
state.set_channel(0, ChannelType.Left)
|
||||||
|
state.set_channel(1, ChannelType.Right)
|
||||||
|
state.set_channel(2, ChannelType.Center)
|
||||||
|
state.set_channel(3, ChannelType.LeftSurround)
|
||||||
|
state.set_channel(4, ChannelType.RightSuround)
|
||||||
|
|
||||||
|
for sample in wav.read():
|
||||||
|
state.add_frames(sample, 1)
|
||||||
|
|
||||||
|
max_true_peak = float('-inf')
|
||||||
|
for channel in range(state.channels):
|
||||||
|
true_peak = get_true_peak(state, channel)
|
||||||
|
max_true_peak = max(true_peak, max_true_peak)
|
||||||
|
del state
|
||||||
|
|
||||||
|
return 20 * log10(max_true_peak)
|
||||||
|
|
||||||
|
|
||||||
|
def test_max_true_peak(r128_test_data):
|
||||||
|
'''Test for the maximum true loudness peak value of a single file.'''
|
||||||
|
|
||||||
|
expected = [
|
||||||
|
('seq-3341-15-24bit.wav.wav', -6.0),
|
||||||
|
('seq-3341-16-24bit.wav.wav', -6.0),
|
||||||
|
('seq-3341-17-24bit.wav.wav', -6.0),
|
||||||
|
('seq-3341-18-24bit.wav.wav', -6.0),
|
||||||
|
('seq-3341-19-24bit.wav.wav', 3.0),
|
||||||
|
('seq-3341-20-24bit.wav.wav', 0.0),
|
||||||
|
('seq-3341-21-24bit.wav.wav', 0.0),
|
||||||
|
('seq-3341-22-24bit.wav.wav', 0.0),
|
||||||
|
('seq-3341-23-24bit.wav.wav', 0.0),
|
||||||
|
]
|
||||||
|
|
||||||
|
tolerance = 0.4
|
||||||
|
status_msg = '==== \'{}\': want {} \u00b1 {} ---> '
|
||||||
|
|
||||||
|
print('\n')
|
||||||
|
for test in expected:
|
||||||
|
print(status_msg.format(test[0], test[1], tolerance), end='')
|
||||||
|
result = get_max_true_peak(r128_test_data / test[0])
|
||||||
|
print('got {} '.format(round(result, 1)), end='')
|
||||||
|
assert (round(result, 1) <= test[1] + tolerance and
|
||||||
|
round(result, 1) >= test[1] - tolerance)
|
||||||
|
print('---> PASSED!')
|
Loading…
Reference in a new issue