Skip to content

Commit

Permalink
Begin work on docstrings
Browse files Browse the repository at this point in the history
  • Loading branch information
nocarryr committed Jun 16, 2024
1 parent 8a64edb commit d1bb9b7
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 10 deletions.
48 changes: 46 additions & 2 deletions src/lupy/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@

@dataclass
class Coeff:
b: FloatArray
a: FloatArray
"""Digital filter coefficients
"""
b: FloatArray #: Numerator of the filter
a: FloatArray #: Denominator of the filter
_sos: FloatArray|None = None

@property
def sos(self) -> FloatArray:
"""Array of second-order sections calculated from the filter's transfer
function form
"""
s = self._sos
if s is None:
s = self._sos = signal.tf2sos(self.b, self.a)
Expand All @@ -28,10 +33,13 @@ def sos(self) -> FloatArray:
b = np.array([1.53512485958697, -2.69169618940638, 1.19839281085285]),
a = np.array([1.0, -1.69065929318241, 0.73248077421585]),
)
"""Stage 1 (high-shelf) of the pre-filter defined in :term:`BS 1770` table 1"""

HP_COEFF = Coeff(
b = np.array([1.0, -2.0, 1.0]),
a = np.array([1.0, -1.99004745483398, 0.99007225036621]),
)
"""Stage 2 (high-pass) of the pre-filter defined in :term:`BS 1770` table 2"""

# BS-1771 coefficients for decimated 320 samples/s
# (128 samples per 400ms block)
Expand All @@ -42,9 +50,20 @@ def sos(self) -> FloatArray:


class Filter:
"""Multi-channel filter that tracks the filter conditions between calls
The filter (defined by :attr:`coeff`) is applied by calling a :class:`Filter`
instance directly.
"""
sos_zi: FloatArray
"""The filter conditions"""

coeff: Coeff
"""The filter coefficients"""

num_channels: int
"""Number of audio channels to filter"""

def __init__(self, coeff: Coeff, num_channels: int = 1):
self.coeff = coeff
zi = signal.sosfilt_zi(coeff.sos)
Expand All @@ -70,16 +89,41 @@ def _sos(self, x: FloatArray) -> FloatArray:
return y

def __call__(self, x: FloatArray) -> FloatArray:
"""Apply the filter defined by :attr:`coeff` and return the result
Arguments:
x: The input data with shape ``(num_channels, n)`` where *n* is the
length of the input data for each channel
"""
return self._sos(x)


class FilterGroup:
"""Apply multiple :class:`filters <Filter>` in series
Arguments:
*coeff: The :class:`coefficients <Coeff>` to construct each :class:`Filter`
num_channels: Number of channels to filter. This will also be set on
the constructed :class:`filters <Filter>`
"""

num_channels: int
"""Number of audio channels to filter"""

def __init__(self, *coeff: Coeff, num_channels: int = 1):
self.num_channels = num_channels
self._filters = [Filter(c, num_channels) for c in coeff]

def __call__(self, x: FloatArray) -> FloatArray:
"""Apply the filters in series and return the result
Arguments:
x: The input data with shape ``(num_channels, n)`` where *n* is the
length of the input data for each channel
"""
y = x
for filt in self._filters:
y = filt(y)
Expand Down
50 changes: 46 additions & 4 deletions src/lupy/meter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@
__all__ = ('Meter',)

class Meter:
"""
"""

block_size: int
"""The number of input samples per call to :meth:`write`"""

num_channels: int
"""Number of audio channels"""

sampler: Sampler
"""The :class:`Sampler` instance to buffer input data"""

processor: BlockProcessor
"""The :class:`BlockProcessor` to perform the calulations"""

sample_rate: int = 48000
def __init__(self, block_size: int, num_channels: int) -> None:
self.block_size = block_size
Expand All @@ -19,10 +34,17 @@ def __init__(self, block_size: int, num_channels: int) -> None:
num_channels=num_channels,
gate_size=self.sampler.gate_size,
)

def can_write(self) -> bool:
"""Whether there is enough room on the internal buffer for at least
one call to :meth:`write`
"""
return self.sampler.can_write()

def can_process(self) -> bool:
"""Whether there are enough samples in the internal buffer for at least
one call to :meth:`process`
"""
return self.sampler.can_read()

def write(
Expand All @@ -31,11 +53,26 @@ def write(
process: bool = True,
process_all: bool = True
) -> None:
"""Store input data into the internal buffer
The input data must be of shape ``(num_channels, block_size)``
"""
self.sampler.write(samples)
if process and self.can_process():
self.process(process_all=process_all)

def process(self, process_all: bool = True) -> None:
"""Process the samples for at least one :term:`gating block`
Arguments:
process_all: If ``True`` (the default), the :meth:`~.sampling.Sampler.read`
method of the :attr:`sampler` will be called and the data passed to the
:meth:`~.processing.BlockProcessor.process_block` method on the
:attr:`processor` repeatedly until there are no
:term:`gating block` samples available.
Otherwise, only one call to each will be performed.
"""
def _do_process():
samples = self.sampler.read()
self.processor.process_block(samples)
Expand All @@ -45,25 +82,30 @@ def _do_process():
else:
_do_process()


@property
def block_data(self):
def block_data(self) -> MeterArray:
"""A structured array of measurement values with
dtype :obj:`~.types.MeterDtype`
"""
return self.processor.block_data

@property
def momentary_lkfs(self) -> Float1dArray:
"""Short-term loudness for each 100ms block, averaged over 400ms
""":term:`Momentary Loudness` for each 100ms block, averaged over 400ms
(not gated)
"""
return self.processor.momentary_lkfs

@property
def short_term_lkfs(self) -> Float1dArray:
"""Short-term loudness for each 100ms block, averaged over 3 seconds
""":term:`Short-Term Loudness` for each 100ms block, averaged over 3 seconds
(not gated)
"""
return self.processor.short_term_lkfs

@property
def t(self) -> Float1dArray:
"""The measurement time for each element in :attr:`short_term_lkfs`
and :attr:`momentary_lkfs`
"""
return self.processor.t
31 changes: 29 additions & 2 deletions src/lupy/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,21 @@ def from_lk_log10(


class BlockProcessor:
"""Process audio samples and store the resulting loudness data
"""

num_channels: int
"""Number of audio channels"""

gate_size: int
"""The length of one :term:`gating block` in samples"""

integrated_lkfs: Floating
"""The current :term:`Integrated Loudness`"""

lra: float
"""The current :term:`Loudness Range`"""

sample_rate = 48000
MAX_BLOCKS = 36000 # <- 14400 seconds (4 hours) / .4 (400 milliseconds)
_channel_weights = np.array([1, 1, 1, 1.41, 1.41])
Expand Down Expand Up @@ -99,19 +114,22 @@ def __init__(self, num_channels: int, gate_size: int):

@property
def block_data(self) -> MeterArray:
"""A structured array of measurement values with
dtype :obj:`~.types.MeterDtype`
"""
return self._block_data[:self.block_index]

@property
def momentary_lkfs(self) -> Float1dArray:
"""Short-term loudness for each 100ms block, averaged over 400ms
""":term:`Momentary Loudness` for each 100ms block, averaged over 400ms
(not gated)
"""
# return self._momentary_lkfs[:self.block_index]
return self.block_data['m']

@property
def short_term_lkfs(self) -> Float1dArray:
"""Short-term loudness for each 100ms block, averaged over 3 seconds
""":term:`Short-Term Loudness` for each 100ms block, averaged over 3 seconds
(not gated)
"""
return self.block_data['s']
Expand All @@ -127,6 +145,9 @@ def short_term_lkfs(self) -> Float1dArray:

@property
def t(self) -> Float1dArray:
"""The measurement time for each element in :attr:`short_term_lkfs`
and :attr:`momentary_lkfs`
"""
return self.block_data['t']
# return self._t[:self.block_index]

Expand All @@ -138,6 +159,8 @@ def Zij(self) -> Float1dArray:
return self._Zij[:,:self.block_index]

def reset(self) -> None:
"""Reset all measurement data
"""
self.block_data['m'][:] = 0
self.block_data['s'][:] = 0
self._Zij[:] = 0
Expand Down Expand Up @@ -272,6 +295,10 @@ def _calc_lra(self):
self.lra = lo_hi[1] - lo_hi[0]

def process_block(self, samples: Float2dArray):
"""Process one :term:`gating block`
Input data must be of shape ``(num_channels, gate_size)``
"""
assert samples.shape == (self.num_channels, self.gate_size)

tg = 1 / self.gate_size
Expand Down
Loading

0 comments on commit d1bb9b7

Please sign in to comment.