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