init
This commit is contained in:
17
lib_audio_dsp/python/audio_dsp/models/__init__.py
Normal file
17
lib_audio_dsp/python/audio_dsp/models/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""The pydantic models of the DSP Stages."""
|
||||
|
||||
from .signal_chain import VolumeControl, FixedGain, Fork, Mixer, Delay
|
||||
|
||||
from .cascaded_biquads import ParametricEq8b, ParametricEq16b, NthOrderFilter
|
||||
from .reverb import ReverbPlateStereo
|
||||
from .envelope_detector import EnvelopeDetectorPeak, EnvelopeDetectorRMS
|
||||
from .noise_suppressor_expander import NoiseSuppressorExpander
|
||||
from .biquad import Biquad
|
||||
from .limiter import LimiterRMS, LimiterPeak, HardLimiterPeak
|
||||
from .noise_gate import NoiseGate
|
||||
from .compressor import CompressorRMS
|
||||
from .compressor_sidechain import CompressorSidechain
|
||||
from .fir import FirDirect
|
||||
from .graphic_eq import GraphicEq10b
|
||||
63
lib_audio_dsp/python/audio_dsp/models/biquad.py
Normal file
63
lib_audio_dsp/python/audio_dsp/models/biquad.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Models for biquad filter stages."""
|
||||
|
||||
from typing import Literal
|
||||
from pydantic import Field
|
||||
|
||||
from audio_dsp.models.stage import StageModel, StageParameters
|
||||
from audio_dsp.models.fields import BIQUAD_TYPES, biquad_bypass
|
||||
|
||||
|
||||
class BiquadParameters(StageParameters):
|
||||
"""Parameters for a biquad filter.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
filter_type : audio_dsp.models.fields.BIQUAD_TYPES
|
||||
The parameters of the type of biquad filter to use (e.g., biquad_lowpass, biquad_highpass, etc.)
|
||||
|
||||
"""
|
||||
|
||||
filter_type: BIQUAD_TYPES = Field(
|
||||
default=biquad_bypass(),
|
||||
description="Type of biquad filter to implement and it's parameters.",
|
||||
)
|
||||
|
||||
|
||||
class Biquad(StageModel):
|
||||
"""A single biquad filter stage.
|
||||
|
||||
A biquad filter is a second-order recursive filter that can implement various
|
||||
filter types like lowpass, highpass, bandpass, etc. This stage implements a
|
||||
single biquad section with slew rate limiting to prevent audio artifacts
|
||||
when parameters are changed.
|
||||
"""
|
||||
|
||||
op_type: Literal["Biquad"] = "Biquad"
|
||||
parameters: BiquadParameters = Field(
|
||||
default_factory=lambda: BiquadParameters(filter_type=biquad_bypass())
|
||||
)
|
||||
|
||||
|
||||
class BiquadSlewParameters(BiquadParameters):
|
||||
"""Parameters for a slewing biquad filter."""
|
||||
|
||||
slew_shift: int = Field(
|
||||
default=6, ge=0, lt=31, description="The shift value used in the exponential slew."
|
||||
)
|
||||
|
||||
|
||||
class BiquadSlew(StageModel):
|
||||
"""A single biquad filter stage.
|
||||
|
||||
A biquad filter is a second-order recursive filter that can implement various
|
||||
filter types like lowpass, highpass, bandpass, etc. This stage implements a
|
||||
single biquad section with slew rate limiting to prevent audio artifacts
|
||||
when parameters are changed.
|
||||
"""
|
||||
|
||||
op_type: Literal["BiquadSlew"] = "BiquadSlew"
|
||||
parameters: BiquadSlewParameters = Field(
|
||||
default_factory=lambda: BiquadSlewParameters(filter_type=biquad_bypass())
|
||||
)
|
||||
119
lib_audio_dsp/python/audio_dsp/models/cascaded_biquads.py
Normal file
119
lib_audio_dsp/python/audio_dsp/models/cascaded_biquads.py
Normal file
@@ -0,0 +1,119 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Pydantic models of the cascaded biquad DSP Stages."""
|
||||
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from annotated_types import Len
|
||||
from pydantic import Field
|
||||
from pydantic.json_schema import SkipJsonSchema
|
||||
|
||||
from audio_dsp.models.fields import BIQUAD_TYPES, biquad_bypass, DEFAULT_FILTER_FREQ
|
||||
|
||||
from .stage import StageModel, StageParameters
|
||||
|
||||
|
||||
def _8biquads():
|
||||
return [biquad_bypass() for _ in range(8)]
|
||||
|
||||
|
||||
def _16biquads():
|
||||
return [biquad_bypass() for _ in range(16)]
|
||||
|
||||
|
||||
CASCADED_BIQUADS_8 = Annotated[list[BIQUAD_TYPES], Len(8)]
|
||||
|
||||
|
||||
class CascadedBiquadsParameters(StageParameters):
|
||||
"""Parameters for CascadedBiquad Stage.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
filters : list[BIQUAD_TYPES]
|
||||
A list of BiquadParameters to update the cascaded biquads with.
|
||||
"""
|
||||
|
||||
filters: CASCADED_BIQUADS_8 = Field(default_factory=_8biquads, max_length=8)
|
||||
|
||||
|
||||
class NthOrderFilterParameters(StageParameters):
|
||||
"""Parameters for NthOrderFilter Stage."""
|
||||
|
||||
type: Literal["bypass", "highpass", "lowpass"] = Field(
|
||||
default="bypass",
|
||||
description="Type of filter to implement. Can be 'bypass', 'highpass', or 'lowpass'.",
|
||||
)
|
||||
filter: Literal["butterworth"] = Field(
|
||||
default="butterworth",
|
||||
description="Class of filter to use. Currently only 'butterworth' is supported.",
|
||||
)
|
||||
order: Literal[2, 4, 6, 8, 10, 12, 14, 16] = Field(
|
||||
default=2, description="The order of the filter. Must be even and less than 16."
|
||||
)
|
||||
filter_freq: float = DEFAULT_FILTER_FREQ(
|
||||
description="-3dB cutoff frequency of the filter in Hz."
|
||||
)
|
||||
|
||||
|
||||
class CascadedBiquads(StageModel):
|
||||
"""8 cascaded biquad filters. This allows up to 8 second order
|
||||
biquad filters to be run in series.
|
||||
|
||||
This can be used for either:
|
||||
|
||||
- an Nth order filter built out of cascaded second order sections
|
||||
- a parametric EQ, where several biquad filters are used at once.
|
||||
"""
|
||||
|
||||
op_type: Literal["CascadedBiquads"] = "CascadedBiquads"
|
||||
parameters: CascadedBiquadsParameters | NthOrderFilterParameters = Field(
|
||||
default=CascadedBiquadsParameters()
|
||||
)
|
||||
|
||||
|
||||
class CascadedBiquads16Parameters(StageParameters):
|
||||
"""Parameters for CascadedBiquad16 Stage."""
|
||||
|
||||
filters: Annotated[list[BIQUAD_TYPES], Len(16)] = Field(
|
||||
default_factory=_16biquads, max_length=16
|
||||
)
|
||||
|
||||
|
||||
class CascadedBiquads16(StageModel):
|
||||
"""8 cascaded biquad filters. This allows up to 8 second order
|
||||
biquad filters to be run in series.
|
||||
|
||||
This can be used for either:
|
||||
|
||||
- an Nth order filter built out of cascaded second order sections
|
||||
- a parametric EQ, where several biquad filters are used at once.
|
||||
"""
|
||||
|
||||
op_type: Literal["CascadedBiquads16"] = "CascadedBiquads16"
|
||||
parameters: CascadedBiquads16Parameters = Field(default_factory=CascadedBiquads16Parameters)
|
||||
|
||||
|
||||
class ParametricEq8b(CascadedBiquads):
|
||||
"""Pydantic model of the ParametricEq8b Stage."""
|
||||
|
||||
op_type: Literal["ParametricEq8b"] = "ParametricEq8b" # pyright: ignore override
|
||||
|
||||
|
||||
class ParametricEq16b(CascadedBiquads16):
|
||||
"""Pydantic model of the ParametricEq16b Stage."""
|
||||
|
||||
op_type: Literal["ParametricEq16b"] = "ParametricEq16b" # pyright: ignore override
|
||||
|
||||
|
||||
class NthOrderFilter(StageModel):
|
||||
"""8 cascaded biquad filters. This allows up to 8 second order
|
||||
biquad filters to be run in series.
|
||||
|
||||
This can be used for either:
|
||||
|
||||
- an Nth order filter built out of cascaded second order sections
|
||||
- a parametric EQ, where several biquad filters are used at once.
|
||||
"""
|
||||
|
||||
op_type: Literal["NthOrderFilter"] = "NthOrderFilter"
|
||||
parameters: NthOrderFilterParameters = Field(default_factory=NthOrderFilterParameters)
|
||||
45
lib_audio_dsp/python/audio_dsp/models/compressor.py
Normal file
45
lib_audio_dsp/python/audio_dsp/models/compressor.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Models for compressor stages."""
|
||||
|
||||
from typing import Literal
|
||||
from pydantic import Field
|
||||
|
||||
from audio_dsp.models.stage import StageModel, StageParameters
|
||||
from audio_dsp.models.fields import DEFAULT_ATTACK_T, DEFAULT_RELEASE_T, DEFAULT_COMPRESSOR_RATIO
|
||||
from audio_dsp.models.fields import DEFAULT_THRESHOLD_DB, DEFAULT_RMS_THRESHOLD_DB
|
||||
|
||||
|
||||
class CompressorParameters(StageParameters):
|
||||
"""Parameters for compressor stage."""
|
||||
|
||||
ratio: float = DEFAULT_COMPRESSOR_RATIO(
|
||||
description="Compression ratio applied when detect signal exceeds threshold"
|
||||
)
|
||||
threshold_db: float = DEFAULT_RMS_THRESHOLD_DB(
|
||||
description="Level in dB above which compression occurs"
|
||||
)
|
||||
attack_t: float = DEFAULT_ATTACK_T(
|
||||
description="Time in seconds for compressor to start compressing"
|
||||
)
|
||||
release_t: float = DEFAULT_RELEASE_T(
|
||||
description="Time in seconds for signal to return to original level"
|
||||
)
|
||||
|
||||
|
||||
class CompressorRMS(StageModel):
|
||||
"""Compressor stage based on RMS envelope of input signal.
|
||||
|
||||
When the RMS envelope of the signal exceeds the threshold, the signal
|
||||
amplitude is reduced by the compression ratio. The threshold sets the value
|
||||
above which compression occurs. The ratio sets how much the signal is
|
||||
compressed. A ratio of 1 results in no compression, while a ratio of
|
||||
infinity results in the same behavior as a limiter.
|
||||
|
||||
The attack time sets how fast the compressor starts compressing. The release
|
||||
time sets how long the signal takes to ramp up to its original level after
|
||||
the envelope is below the threshold.
|
||||
"""
|
||||
|
||||
op_type: Literal["CompressorRMS"] = "CompressorRMS"
|
||||
parameters: CompressorParameters = Field(default_factory=CompressorParameters)
|
||||
@@ -0,0 +1,98 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Models for sidechain compressor stages."""
|
||||
|
||||
from typing import Literal
|
||||
from pydantic import Field
|
||||
|
||||
from audio_dsp.models.stage import (
|
||||
StageModel,
|
||||
StageParameters,
|
||||
NodePlacement,
|
||||
Placement_2i1o,
|
||||
Placement_4i2o,
|
||||
)
|
||||
from audio_dsp.models.fields import (
|
||||
DEFAULT_ATTACK_T,
|
||||
DEFAULT_RELEASE_T,
|
||||
DEFAULT_COMPRESSOR_RATIO,
|
||||
DEFAULT_THRESHOLD_DB,
|
||||
DEFAULT_RMS_THRESHOLD_DB,
|
||||
)
|
||||
|
||||
|
||||
class CompressorSidechainParameters(StageParameters):
|
||||
"""Parameters for sidechain compressor stage."""
|
||||
|
||||
ratio: float = DEFAULT_COMPRESSOR_RATIO(
|
||||
description="Compression ratio applied when detect signal exceeds threshold"
|
||||
)
|
||||
threshold_db: float = DEFAULT_RMS_THRESHOLD_DB(
|
||||
description="Level in dB above which compression occurs"
|
||||
)
|
||||
attack_t: float = DEFAULT_ATTACK_T(
|
||||
description="Time in seconds for compressor to start compressing"
|
||||
)
|
||||
release_t: float = DEFAULT_RELEASE_T(
|
||||
description="Time in seconds for signal to return to original level"
|
||||
)
|
||||
|
||||
|
||||
class CompressorSidechainPlacement(Placement_2i1o):
|
||||
"""Node placement for sidechain compressor.
|
||||
|
||||
Requires exactly 2 inputs:
|
||||
- Input 0: Signal to be compressed
|
||||
- Input 1: Detect signal used to control compression
|
||||
|
||||
Produces exactly 1 output:
|
||||
- Output 0: Compressed version of input 0
|
||||
"""
|
||||
|
||||
|
||||
class CompressorSidechain(StageModel[CompressorSidechainPlacement]):
|
||||
"""Sidechain compressor stage based on RMS envelope of detect signal.
|
||||
|
||||
This stage requires exactly 2 input channels:
|
||||
1. The signal to be compressed
|
||||
2. The detect signal that controls the compression
|
||||
|
||||
When the RMS envelope of the detect signal exceeds the threshold, the
|
||||
processed signal amplitude is reduced by the compression ratio. The threshold
|
||||
sets the value above which compression occurs. The ratio sets how much the
|
||||
signal is compressed. A ratio of 1 results in no compression, while a ratio
|
||||
of infinity results in the same behavior as a limiter.
|
||||
|
||||
The attack time sets how fast the compressor starts compressing. The release
|
||||
time sets how long the signal takes to ramp up to its original level after
|
||||
the envelope is below the threshold.
|
||||
"""
|
||||
|
||||
op_type: Literal["CompressorSidechain"] = "CompressorSidechain"
|
||||
parameters: CompressorSidechainParameters = Field(
|
||||
default_factory=CompressorSidechainParameters
|
||||
)
|
||||
|
||||
|
||||
class CompressorSidechainStereo(StageModel[Placement_4i2o]):
|
||||
"""Sidechain compressor stage based on RMS envelope of detect signal.
|
||||
|
||||
This stage requires exactly 2 input channels:
|
||||
1. The signal to be compressed
|
||||
2. The detect signal that controls the compression
|
||||
|
||||
When the RMS envelope of the detect signal exceeds the threshold, the
|
||||
processed signal amplitude is reduced by the compression ratio. The threshold
|
||||
sets the value above which compression occurs. The ratio sets how much the
|
||||
signal is compressed. A ratio of 1 results in no compression, while a ratio
|
||||
of infinity results in the same behavior as a limiter.
|
||||
|
||||
The attack time sets how fast the compressor starts compressing. The release
|
||||
time sets how long the signal takes to ramp up to its original level after
|
||||
the envelope is below the threshold.
|
||||
"""
|
||||
|
||||
op_type: Literal["CompressorSidechainStereo"] = "CompressorSidechainStereo"
|
||||
parameters: CompressorSidechainParameters = Field(
|
||||
default_factory=CompressorSidechainParameters
|
||||
)
|
||||
66
lib_audio_dsp/python/audio_dsp/models/envelope_detector.py
Normal file
66
lib_audio_dsp/python/audio_dsp/models/envelope_detector.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Copyright 2024-2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Envelope detector Stages measure how the average or peak amplitude of
|
||||
a signal varies over time.
|
||||
"""
|
||||
|
||||
from typing import Literal, Tuple
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from audio_dsp.models.stage import StageModel, StageParameters, NodePlacement
|
||||
from audio_dsp.models.fields import DEFAULT_ATTACK_T, DEFAULT_RELEASE_T
|
||||
|
||||
|
||||
class EnvelopeDetectorPlacement(NodePlacement):
|
||||
"""Graph placement for an Envelope Stage. This stage has no outputs."""
|
||||
|
||||
input: list[Tuple[str, int]] = Field(
|
||||
default=[],
|
||||
description="Set of input edges.",
|
||||
)
|
||||
|
||||
|
||||
class EnvelopeDetectorParameters(StageParameters):
|
||||
"""Parameters for an EnvelopeDetector Stage."""
|
||||
|
||||
attack_t: float = DEFAULT_ATTACK_T()
|
||||
release_t: float = DEFAULT_RELEASE_T()
|
||||
|
||||
|
||||
class EnvelopeDetectorPeak(StageModel[EnvelopeDetectorPlacement]):
|
||||
"""
|
||||
A stage with no outputs that measures the signal peak envelope.
|
||||
|
||||
The current envelope of the signal can be read out using this stage's
|
||||
``envelope`` control.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dsp_block : :class:`audio_dsp.dsp.drc.drc.envelope_detector_peak`
|
||||
The DSP block class; see :ref:`EnvelopeDetectorPeak`
|
||||
for implementation details.
|
||||
|
||||
"""
|
||||
|
||||
op_type: Literal["EnvelopeDetectorPeak"] = "EnvelopeDetectorPeak"
|
||||
parameters: EnvelopeDetectorParameters = Field(default_factory=EnvelopeDetectorParameters)
|
||||
|
||||
|
||||
class EnvelopeDetectorRMS(StageModel[EnvelopeDetectorPlacement]):
|
||||
"""
|
||||
A stage with no outputs that measures the signal RMS envelope.
|
||||
|
||||
The current envelope of the signal can be read out using this stage's
|
||||
``envelope`` control.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
dsp_block : :class:`audio_dsp.dsp.drc.drc.envelope_detector_rms`
|
||||
The DSP block class; see :ref:`EnvelopeDetectorRMS`
|
||||
for implementation details.
|
||||
|
||||
"""
|
||||
|
||||
op_type: Literal["EnvelopeDetectorRMS"] = "EnvelopeDetectorRMS"
|
||||
parameters: EnvelopeDetectorParameters = Field(default_factory=EnvelopeDetectorParameters)
|
||||
197
lib_audio_dsp/python/audio_dsp/models/fields.py
Normal file
197
lib_audio_dsp/python/audio_dsp/models/fields.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Pydantic models of the different biquad types."""
|
||||
|
||||
from audio_dsp.models.stage import StageParameters
|
||||
from functools import partial
|
||||
from pydantic import BaseModel, RootModel, Field, create_model
|
||||
from typing import Literal, Annotated, List, Union
|
||||
from annotated_types import Len
|
||||
from audio_dsp.dsp.generic import HEADROOM_DB, MIN_SIG_DB, Q_SIG
|
||||
from audio_dsp.dsp import utils
|
||||
|
||||
|
||||
def _ws(locals):
|
||||
"""
|
||||
Without self.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
locals : dict
|
||||
a dictionary
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
l with the entry "self" removed
|
||||
"""
|
||||
return {k: v for k, v in locals.items() if k != "self"}
|
||||
|
||||
|
||||
DEFAULT_Q = partial(Field, default=0.707, gt=0, le=10, description="Q factor of the filter.")
|
||||
DEFAULT_FILTER_FREQ = partial(
|
||||
Field, default=1000, gt=0, lt=24000, description="Frequency of the filter in Hz."
|
||||
)
|
||||
DEFAULT_BW = partial(
|
||||
Field, default=1, gt=0, le=10, description="Bandwidth of the filter in octaves."
|
||||
)
|
||||
DEFAULT_BOOST_DB = partial(
|
||||
Field, default=0.0, ge=-24, le=24, description="Gain of the filter in dB."
|
||||
)
|
||||
|
||||
DEFAULT_GAIN_DB = partial(
|
||||
Field, default=0.0, ge=MIN_SIG_DB, le=HEADROOM_DB, description="Gain of the stage in dB."
|
||||
)
|
||||
|
||||
DEFAULT_ATTACK_T = partial(
|
||||
Field, default=0.01, gt=0, le=1, description="Attack time of the stage in seconds."
|
||||
)
|
||||
DEFAULT_RELEASE_T = partial(
|
||||
Field, default=0.2, gt=0, le=5, description="Release time of the stage in seconds."
|
||||
)
|
||||
DEFAULT_COMPRESSOR_RATIO = partial(
|
||||
Field, default=4.0, gt=1, le=100, description="Compression ratio of the stage."
|
||||
)
|
||||
DEFAULT_THRESHOLD_DB = DEFAULT_GAIN_DB
|
||||
|
||||
RMS_HEADROOM_DB = utils.db_pow((utils.Q_max(31) + 1) / utils.Q_max(Q_SIG))
|
||||
MIN_RMS_SIG_DB = utils.db_pow(1 / 2**Q_SIG)
|
||||
|
||||
DEFAULT_RMS_THRESHOLD_DB = partial(
|
||||
Field,
|
||||
default=0.0,
|
||||
ge=MIN_RMS_SIG_DB,
|
||||
le=RMS_HEADROOM_DB,
|
||||
description="Threshold of the stage in dB.",
|
||||
)
|
||||
|
||||
|
||||
class biquad_allpass(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to allpass."""
|
||||
|
||||
type: Literal["allpass"] = "allpass"
|
||||
filter_freq: float = DEFAULT_FILTER_FREQ()
|
||||
q_factor: float = DEFAULT_Q()
|
||||
|
||||
|
||||
class biquad_bandpass(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to bandpass."""
|
||||
|
||||
type: Literal["bandpass"] = "bandpass"
|
||||
filter_freq: float = DEFAULT_FILTER_FREQ()
|
||||
bw: float = DEFAULT_BW()
|
||||
|
||||
|
||||
class biquad_bandstop(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to bandstop."""
|
||||
|
||||
type: Literal["bandstop"] = "bandstop"
|
||||
filter_freq: float = DEFAULT_FILTER_FREQ()
|
||||
bw: float = DEFAULT_BW()
|
||||
|
||||
|
||||
class biquad_bypass(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to bypass."""
|
||||
|
||||
type: Literal["bypass"] = "bypass"
|
||||
|
||||
|
||||
class biquad_constant_q(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to constant_q."""
|
||||
|
||||
type: Literal["constant_q"] = "constant_q"
|
||||
filter_freq: float = DEFAULT_FILTER_FREQ()
|
||||
q_factor: float = DEFAULT_Q()
|
||||
boost_db: float = DEFAULT_BOOST_DB()
|
||||
|
||||
|
||||
class biquad_gain(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to gain."""
|
||||
|
||||
type: Literal["gain"] = "gain"
|
||||
gain_db: float = DEFAULT_GAIN_DB()
|
||||
|
||||
|
||||
class biquad_highpass(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to highpass."""
|
||||
|
||||
type: Literal["highpass"] = "highpass"
|
||||
filter_freq: float = DEFAULT_FILTER_FREQ()
|
||||
q_factor: float = DEFAULT_Q()
|
||||
|
||||
|
||||
class biquad_highshelf(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to highshelf."""
|
||||
|
||||
type: Literal["highshelf"] = "highshelf"
|
||||
filter_freq: float = DEFAULT_FILTER_FREQ()
|
||||
q_factor: float = DEFAULT_Q()
|
||||
boost_db: float = DEFAULT_BOOST_DB()
|
||||
|
||||
|
||||
class biquad_linkwitz(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to linkwitz."""
|
||||
|
||||
type: Literal["linkwitz"] = "linkwitz"
|
||||
f0: float = DEFAULT_FILTER_FREQ()
|
||||
q0: float = DEFAULT_Q()
|
||||
fp: float = DEFAULT_FILTER_FREQ()
|
||||
qp: float = DEFAULT_Q()
|
||||
|
||||
|
||||
class biquad_lowpass(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to lowpass."""
|
||||
|
||||
type: Literal["lowpass"] = "lowpass"
|
||||
filter_freq: float = DEFAULT_FILTER_FREQ()
|
||||
q_factor: float = DEFAULT_Q()
|
||||
|
||||
|
||||
class biquad_lowshelf(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to lowshelf."""
|
||||
|
||||
type: Literal["lowshelf"] = "lowshelf"
|
||||
filter_freq: float = DEFAULT_FILTER_FREQ()
|
||||
q_factor: float = DEFAULT_Q()
|
||||
boost_db: float = DEFAULT_BOOST_DB()
|
||||
|
||||
|
||||
class biquad_mute(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to mute."""
|
||||
|
||||
type: Literal["mute"] = "mute"
|
||||
|
||||
|
||||
class biquad_notch(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to notch."""
|
||||
|
||||
type: Literal["notch"] = "notch"
|
||||
filter_freq: float = DEFAULT_FILTER_FREQ()
|
||||
q_factor: float = DEFAULT_Q()
|
||||
|
||||
|
||||
class biquad_peaking(StageParameters):
|
||||
"""Parameters for a Biquad Stage configured to peaking."""
|
||||
|
||||
type: Literal["peaking"] = "peaking"
|
||||
filter_freq: float = DEFAULT_FILTER_FREQ()
|
||||
q_factor: float = DEFAULT_Q()
|
||||
boost_db: float = DEFAULT_BOOST_DB()
|
||||
|
||||
|
||||
BIQUAD_TYPES = Union[
|
||||
biquad_allpass,
|
||||
biquad_bandpass,
|
||||
biquad_bandstop,
|
||||
biquad_bypass,
|
||||
biquad_constant_q,
|
||||
biquad_gain,
|
||||
biquad_highpass,
|
||||
biquad_highshelf,
|
||||
biquad_linkwitz,
|
||||
biquad_lowpass,
|
||||
biquad_lowshelf,
|
||||
biquad_mute,
|
||||
biquad_notch,
|
||||
biquad_peaking,
|
||||
]
|
||||
21
lib_audio_dsp/python/audio_dsp/models/fir.py
Normal file
21
lib_audio_dsp/python/audio_dsp/models/fir.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""FIR model definitions."""
|
||||
|
||||
from audio_dsp.models.stage import StageModel, StageConfig
|
||||
from pydantic import Field, field_validator, model_validator
|
||||
from typing import Literal
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class FirConfig(StageConfig):
|
||||
"""Compile time configuration for a FIR Stage."""
|
||||
|
||||
coeffs_path: Path = Field(description="Path to filter coefficients file.")
|
||||
|
||||
|
||||
class FirDirect(StageModel):
|
||||
"""FIR filter stage using direct form implementation."""
|
||||
|
||||
op_type: Literal["FirDirect"] = "FirDirect"
|
||||
config: FirConfig = Field(..., description="FIR configuration.")
|
||||
49
lib_audio_dsp/python/audio_dsp/models/graphic_eq.py
Normal file
49
lib_audio_dsp/python/audio_dsp/models/graphic_eq.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Pydantic models for signal chain DSP Stages."""
|
||||
|
||||
from typing import Literal, Annotated
|
||||
from annotated_types import Len
|
||||
|
||||
from pydantic import Field, field_validator, model_validator
|
||||
|
||||
from audio_dsp.models.stage import (
|
||||
NodePlacement,
|
||||
StageModel,
|
||||
StageParameters,
|
||||
Placement_2i1o,
|
||||
Placement_Ni1o,
|
||||
Placement_4i2o,
|
||||
)
|
||||
from audio_dsp.models.fields import DEFAULT_GAIN_DB
|
||||
|
||||
|
||||
GEQ_GAIN = Field(ge=-24, le=24, description="Gain of the band in dB.")
|
||||
|
||||
|
||||
class GraphicEq10bParameters(StageParameters):
|
||||
"""
|
||||
Parameters for a 10-band graphic equalizer.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
gains_db: (list[float])
|
||||
Gain values (in dB) for each of the 10 frequency bands.
|
||||
- Each value must be between -24 dB and +24 dB.
|
||||
- The list must have exactly 10 elements.
|
||||
"""
|
||||
|
||||
gains_db: Annotated[
|
||||
list[Annotated[float, GEQ_GAIN]],
|
||||
Len(10),
|
||||
] = Field(default_factory=lambda: [0.0] * 10)
|
||||
|
||||
|
||||
class GraphicEq10b(StageModel):
|
||||
"""
|
||||
This stage implements a Graphic EQ with 10 bands.
|
||||
Each band can be adjusted with a gain value ranging from -24 dB to +24 dB.
|
||||
"""
|
||||
|
||||
op_type: Literal["GraphicEq10b"] = "GraphicEq10b"
|
||||
parameters: GraphicEq10bParameters = Field(default_factory=GraphicEq10bParameters)
|
||||
98
lib_audio_dsp/python/audio_dsp/models/limiter.py
Normal file
98
lib_audio_dsp/python/audio_dsp/models/limiter.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Models for limiter stages."""
|
||||
|
||||
from typing import Literal
|
||||
from pydantic import Field
|
||||
|
||||
from audio_dsp.models.stage import StageModel, StageParameters
|
||||
from audio_dsp.models.fields import (
|
||||
DEFAULT_ATTACK_T,
|
||||
DEFAULT_RELEASE_T,
|
||||
DEFAULT_THRESHOLD_DB,
|
||||
DEFAULT_RMS_THRESHOLD_DB,
|
||||
)
|
||||
|
||||
|
||||
class LimiterParameters(StageParameters):
|
||||
"""Parameters for limiter stage."""
|
||||
|
||||
threshold_db: float = DEFAULT_THRESHOLD_DB(
|
||||
description="Level in dB above which limiting occurs"
|
||||
)
|
||||
attack_t: float = DEFAULT_ATTACK_T(description="Time in seconds for limiter to start limiting")
|
||||
release_t: float = DEFAULT_RELEASE_T(
|
||||
description="Time in seconds for signal to return to original level"
|
||||
)
|
||||
|
||||
|
||||
class LimiterRMSParameters(LimiterParameters):
|
||||
"""Parameters for RMS limiter stage."""
|
||||
|
||||
threshold_db: float = DEFAULT_RMS_THRESHOLD_DB(
|
||||
description="Level in dB above which limiting occurs"
|
||||
)
|
||||
|
||||
|
||||
class LimiterRMS(StageModel):
|
||||
"""Limiter stage based on RMS value of signal.
|
||||
|
||||
When the RMS envelope of the signal exceeds the threshold, the signal
|
||||
amplitude is reduced. The threshold sets the value above which limiting
|
||||
occurs.
|
||||
|
||||
The attack time sets how fast the limiter starts limiting. The release
|
||||
time sets how long the signal takes to ramp up to its original level after
|
||||
the envelope is below the threshold.
|
||||
"""
|
||||
|
||||
op_type: Literal["LimiterRMS"] = "LimiterRMS"
|
||||
parameters: LimiterRMSParameters = Field(default_factory=LimiterRMSParameters)
|
||||
|
||||
|
||||
class LimiterPeak(StageModel):
|
||||
"""Limiter stage based on peak value of signal.
|
||||
|
||||
When the peak envelope of the signal exceeds the threshold, the signal
|
||||
amplitude is reduced. The threshold sets the value above which limiting
|
||||
occurs.
|
||||
|
||||
The attack time sets how fast the limiter starts limiting. The release
|
||||
time sets how long the signal takes to ramp up to its original level after
|
||||
the envelope is below the threshold.
|
||||
"""
|
||||
|
||||
op_type: Literal["LimiterPeak"] = "LimiterPeak"
|
||||
parameters: LimiterParameters = Field(default_factory=LimiterParameters)
|
||||
|
||||
|
||||
class HardLimiterPeak(StageModel):
|
||||
"""Hard limiter stage based on peak value of signal.
|
||||
|
||||
When the peak envelope of the signal exceeds the threshold, the signal
|
||||
amplitude is reduced. If the signal still exceeds the threshold, it is
|
||||
clipped. The peak envelope of the signal may never exceed the threshold.
|
||||
|
||||
The threshold sets the value above which limiting/clipping occurs. The
|
||||
attack time sets how fast the limiter starts limiting. The release time
|
||||
sets how long the signal takes to ramp up to its original level after
|
||||
the envelope is below the threshold.
|
||||
"""
|
||||
|
||||
op_type: Literal["HardLimiterPeak"] = "HardLimiterPeak"
|
||||
parameters: LimiterParameters = Field(default_factory=LimiterParameters)
|
||||
|
||||
|
||||
class ClipperParameters(StageParameters):
|
||||
"""Parameters for clipper stage."""
|
||||
|
||||
threshold_db: float = DEFAULT_THRESHOLD_DB(
|
||||
description="Level in dB above which clipping occurs"
|
||||
)
|
||||
|
||||
|
||||
class Clipper(StageModel):
|
||||
"""Clipper stage model for limiting signal amplitude."""
|
||||
|
||||
op_type: Literal["Clipper"] = "Clipper"
|
||||
parameters: ClipperParameters = Field(default_factory=ClipperParameters)
|
||||
41
lib_audio_dsp/python/audio_dsp/models/noise_gate.py
Normal file
41
lib_audio_dsp/python/audio_dsp/models/noise_gate.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Models for noise gate stages."""
|
||||
|
||||
from typing import Literal
|
||||
from pydantic import Field
|
||||
|
||||
from audio_dsp.models.stage import StageModel, StageParameters
|
||||
from audio_dsp.models.fields import DEFAULT_ATTACK_T, DEFAULT_RELEASE_T, DEFAULT_COMPRESSOR_RATIO
|
||||
from audio_dsp.models.fields import DEFAULT_THRESHOLD_DB
|
||||
|
||||
|
||||
class NoiseGateParameters(StageParameters):
|
||||
"""Parameters for noise gate stage."""
|
||||
|
||||
threshold_db: float = DEFAULT_THRESHOLD_DB(
|
||||
default=-35,
|
||||
description="Level in dB below which the gate begins to close",
|
||||
)
|
||||
attack_t: float = DEFAULT_ATTACK_T(
|
||||
default=0.005,
|
||||
description="Time in seconds for gate to open when signal exceeds threshold",
|
||||
)
|
||||
release_t: float = DEFAULT_RELEASE_T(
|
||||
default=0.12,
|
||||
description="Time in seconds for gate to close when signal falls below threshold",
|
||||
)
|
||||
|
||||
|
||||
class NoiseGate(StageModel):
|
||||
"""Noise gate stage for removing low-level signals.
|
||||
|
||||
Attenuates signals that fall below a threshold, useful for removing
|
||||
background noise during silent passages. When the signal falls below
|
||||
the threshold, the gain is reduced to 0 over the release time. When
|
||||
the signal rises above the threshold, the gain is increased to 1 over
|
||||
the attack time.
|
||||
"""
|
||||
|
||||
op_type: Literal["NoiseGate"] = "NoiseGate"
|
||||
parameters: NoiseGateParameters = Field(default_factory=NoiseGateParameters)
|
||||
@@ -0,0 +1,55 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Models for noise suppressor expander stages."""
|
||||
|
||||
from typing import Literal
|
||||
from pydantic import Field
|
||||
|
||||
from audio_dsp.models.stage import StageModel, StageParameters
|
||||
from audio_dsp.models.fields import (
|
||||
DEFAULT_ATTACK_T,
|
||||
DEFAULT_RELEASE_T,
|
||||
DEFAULT_COMPRESSOR_RATIO,
|
||||
DEFAULT_THRESHOLD_DB,
|
||||
)
|
||||
|
||||
|
||||
class NoiseSuppressorExpanderParameters(StageParameters):
|
||||
"""Parameters for noise suppressor expander stage."""
|
||||
|
||||
ratio: float = DEFAULT_COMPRESSOR_RATIO(
|
||||
default=3,
|
||||
description="Expansion ratio applied when signal falls below threshold",
|
||||
)
|
||||
threshold_db: float = DEFAULT_THRESHOLD_DB(
|
||||
default=-35, description="Level in dB below which expansion occurs"
|
||||
)
|
||||
attack_t: float = DEFAULT_ATTACK_T(
|
||||
default=0.005,
|
||||
description="Time in seconds for expander to start expanding",
|
||||
)
|
||||
release_t: float = DEFAULT_RELEASE_T(
|
||||
default=0.12,
|
||||
description="Time in seconds for signal to return to original level",
|
||||
)
|
||||
|
||||
|
||||
class NoiseSuppressorExpander(StageModel):
|
||||
"""Noise suppressor expander stage.
|
||||
|
||||
A noise suppressor that reduces the level of an audio signal when it falls below a threshold. This is also known as an expander.
|
||||
|
||||
When the signal envelope falls below the threshold, the gain applied
|
||||
to the signal is reduced relative to the expansion ratio over the
|
||||
release time. When the envelope returns above the threshold, the
|
||||
gain applied to the signal is increased to 1 over the attack time.
|
||||
|
||||
The initial state of the noise suppressor is with the suppression
|
||||
off; this models a full scale signal having been present before
|
||||
t = 0.
|
||||
"""
|
||||
|
||||
op_type: Literal["NoiseSuppressorExpander"] = "NoiseSuppressorExpander"
|
||||
parameters: NoiseSuppressorExpanderParameters = Field(
|
||||
default_factory=NoiseSuppressorExpanderParameters
|
||||
)
|
||||
212
lib_audio_dsp/python/audio_dsp/models/reverb.py
Normal file
212
lib_audio_dsp/python/audio_dsp/models/reverb.py
Normal file
@@ -0,0 +1,212 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Pydantic models for reverb DSP Stages."""
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from audio_dsp.models.stage import (
|
||||
StageConfig,
|
||||
StageModel,
|
||||
StageParameters,
|
||||
MonoPlacement,
|
||||
StereoPlacement,
|
||||
NodePlacement,
|
||||
)
|
||||
|
||||
|
||||
class ReverbBaseParameters(StageParameters):
|
||||
"""Parameters for all Reverb Stages."""
|
||||
|
||||
predelay: float = Field(
|
||||
default=15, ge=0, le=30, description="Set the predelay in milliseconds."
|
||||
)
|
||||
pregain: float = Field(
|
||||
default=0.015,
|
||||
ge=0,
|
||||
le=1,
|
||||
description="It is not advised to increase this value above the "
|
||||
"default 0.015, as it can result in saturation inside "
|
||||
"the reverb delay lines.",
|
||||
)
|
||||
wet_dry_mix: float = Field(
|
||||
default=0.5,
|
||||
ge=0,
|
||||
le=1,
|
||||
description=(
|
||||
"The mix between the wet and dry signal. When the mix is 0, the output signal is fully dry, "
|
||||
"when 1, the output signal is fully wet."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ReverbStereoBaseParameters(ReverbBaseParameters):
|
||||
"""Parameters for all stereo Reverb Stages."""
|
||||
|
||||
width: float = Field(
|
||||
default=1.0,
|
||||
ge=0,
|
||||
le=1,
|
||||
description=(
|
||||
"How much stereo separation there is between the left and "
|
||||
"right channels. Setting width to 0 will yield a mono signal, "
|
||||
"whilst setting width to 1 will yield the most stereo "
|
||||
"separation."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ReverbBaseConfig(StageConfig):
|
||||
"""Compile time configuration for a ReverbRoom Stage."""
|
||||
|
||||
max_predelay: float = Field(
|
||||
default=30, description="Set the maximum predelay in milliseconds."
|
||||
)
|
||||
|
||||
|
||||
class _ReverbBaseModel[Placement: NodePlacement](StageModel[Placement]):
|
||||
"""
|
||||
The base class for reverb stages, containing pre delays, and wet/dry
|
||||
mixes and pregain.
|
||||
"""
|
||||
|
||||
# op_type: is not defined as this Stage cannot be pipelined
|
||||
|
||||
|
||||
class ReverbRoomConfig(ReverbBaseConfig):
|
||||
"""Compile time configuration for a ReverbRoom Stage."""
|
||||
|
||||
max_room_size: float = Field(
|
||||
default=1.0,
|
||||
gt=0,
|
||||
le=4,
|
||||
description=(
|
||||
"Sets the maximum room size for this reverb. The"
|
||||
" ``room_size`` parameter sets the fraction of this value actually used at any given time."
|
||||
" For optimal memory usage, max_room_size should be set so that the longest reverb tail"
|
||||
" occurs when ``room_size=1.0``."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ReverbRoomParameters(ReverbBaseParameters):
|
||||
"""Parameters for a ReverbRoom Stage."""
|
||||
|
||||
damping: float = Field(
|
||||
default=0.4,
|
||||
ge=0,
|
||||
le=1,
|
||||
description="This controls how much high frequency attenuation "
|
||||
"is in the room. Higher values yield shorter "
|
||||
"reverberation times at high frequencies. Range: 0 to 1",
|
||||
)
|
||||
decay: float = Field(
|
||||
default=0.5,
|
||||
ge=0,
|
||||
le=1,
|
||||
description="This sets how reverberant the room is. Higher "
|
||||
"values will give a longer reverberation time for "
|
||||
"a given room size. Range: 0 to 1",
|
||||
)
|
||||
room_size: float = Field(
|
||||
default=1.0,
|
||||
ge=0,
|
||||
le=1,
|
||||
description="This sets how reverberant the room is. Higher "
|
||||
"values will give a longer reverberation time for "
|
||||
"a given room size. Range: 0 to 1",
|
||||
)
|
||||
|
||||
|
||||
class ReverbRoomStereoConfig(ReverbRoomConfig):
|
||||
"""Compile time configuration for a ReverbRoomStereo Stage."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReverbRoomStereoParameters(ReverbStereoBaseParameters, ReverbRoomParameters):
|
||||
"""Parameters for a ReverbRoomStereo Stage."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReverbPlateStereoConfig(ReverbBaseConfig):
|
||||
"""Compile time configuration for a ReverbPlateStereo Stage."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ReverbPlateStereoParameters(ReverbStereoBaseParameters):
|
||||
"""Parameters for a ReverbPlateStereo Stage."""
|
||||
|
||||
pregain: float = Field(
|
||||
default=0.5,
|
||||
ge=0,
|
||||
le=1,
|
||||
description="It is not advised to increase this value above the "
|
||||
"default 0.5, as it can result in saturation inside the reverb delay lines.",
|
||||
)
|
||||
damping: float = Field(
|
||||
default=0.75,
|
||||
ge=0,
|
||||
le=1,
|
||||
description="This controls how much high frequency attenuation is in the room. Higher "
|
||||
"values yield shorter reverberation times at high frequencies. Range: 0 to 1",
|
||||
)
|
||||
decay: float = Field(
|
||||
default=0.4,
|
||||
ge=0,
|
||||
le=1,
|
||||
description="This sets how reverberant the room is. Higher "
|
||||
"values will give a longer reverberation time for "
|
||||
"a given room size. Range: 0 to 1",
|
||||
)
|
||||
early_diffusion: float = Field(
|
||||
default=0.75,
|
||||
ge=0,
|
||||
le=1,
|
||||
description="Sets how much diffusion is present in the first part of the reverberation. Range: 0 to 1",
|
||||
)
|
||||
late_diffusion: float = Field(
|
||||
default=0.7,
|
||||
ge=0,
|
||||
le=1,
|
||||
description="Sets how much diffusion is present in the latter part of the reverberation. Range: 0 to 1",
|
||||
)
|
||||
bandwidth: float = Field(
|
||||
default=8000,
|
||||
ge=0,
|
||||
le=24000,
|
||||
description="Sets the low pass cutoff frequency of the reverb input.",
|
||||
)
|
||||
|
||||
|
||||
class ReverbRoom(_ReverbBaseModel[MonoPlacement]):
|
||||
"""Mono Reverb room model."""
|
||||
|
||||
op_type: Literal["ReverbRoom"] = "ReverbRoom"
|
||||
parameters: ReverbRoomParameters = Field(default_factory=ReverbRoomParameters)
|
||||
config: ReverbRoomConfig = Field(default_factory=ReverbRoomConfig)
|
||||
|
||||
|
||||
class ReverbRoomStereo(_ReverbBaseModel[StereoPlacement]):
|
||||
"""Stereo Reverb room model."""
|
||||
|
||||
op_type: Literal["ReverbRoomStereo"] = "ReverbRoomStereo"
|
||||
parameters: ReverbRoomStereoParameters = Field(default_factory=ReverbRoomStereoParameters)
|
||||
config: ReverbRoomStereoConfig = Field(default_factory=ReverbRoomStereoConfig)
|
||||
|
||||
|
||||
class ReverbPlateStereo(_ReverbBaseModel[StereoPlacement]):
|
||||
"""
|
||||
The stereo room plate stage. This is based on Dattorro's 1997
|
||||
paper. This reverb consists of 4 allpass filters for input diffusion,
|
||||
followed by a figure of 8 reverb tank of allpasses, low-pass filters,
|
||||
and delays. The output is taken from multiple taps in the delay lines
|
||||
to get a desirable echo density.
|
||||
"""
|
||||
|
||||
op_type: Literal["ReverbPlateStereo"] = "ReverbPlateStereo"
|
||||
parameters: ReverbPlateStereoParameters = Field(default_factory=ReverbPlateStereoParameters)
|
||||
config: ReverbPlateStereoConfig = Field(default_factory=ReverbPlateStereoConfig)
|
||||
258
lib_audio_dsp/python/audio_dsp/models/signal_chain.py
Normal file
258
lib_audio_dsp/python/audio_dsp/models/signal_chain.py
Normal file
@@ -0,0 +1,258 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Pydantic models for signal chain DSP Stages."""
|
||||
|
||||
from typing import Literal, Tuple
|
||||
|
||||
from pydantic import Field, field_validator, model_validator
|
||||
|
||||
from audio_dsp.models.stage import (
|
||||
NodePlacement,
|
||||
StageConfig,
|
||||
StageModel,
|
||||
StageParameters,
|
||||
Placement_2i1o,
|
||||
Placement_Ni1o,
|
||||
Placement_4i2o,
|
||||
)
|
||||
from audio_dsp.models.fields import DEFAULT_GAIN_DB
|
||||
import annotated_types
|
||||
|
||||
|
||||
class Bypass(StageModel):
|
||||
"""
|
||||
This stage implements a bypass. The input signal is passed through
|
||||
unchanged.
|
||||
"""
|
||||
|
||||
op_type: Literal["Bypass"] = "Bypass"
|
||||
|
||||
|
||||
class ForkConfig(StageConfig):
|
||||
"""Compile time configuration for a Fork Stage."""
|
||||
|
||||
count: int = Field(default=1)
|
||||
|
||||
|
||||
class ForkPlacement(NodePlacement, extra="forbid"):
|
||||
"""Graph placement for a Fork Stage."""
|
||||
|
||||
input: list[Tuple[str, int]] = Field(
|
||||
default=[], description="List of input edges.", min_length=1
|
||||
)
|
||||
|
||||
|
||||
class Fork(StageModel[ForkPlacement]):
|
||||
"""Forks the input signal into multiple outputs."""
|
||||
|
||||
op_type: Literal["Fork"] = "Fork"
|
||||
config: ForkConfig = Field(default_factory=ForkConfig)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def check_fork(self):
|
||||
"""Check that the fork has been validly connected."""
|
||||
in_len = len(self.placement.input)
|
||||
# out_len = len(self.placement.output)
|
||||
|
||||
# if out_len / in_len != self.config.count:
|
||||
# if out_len / in_len == out_len // in_len:
|
||||
# self.config.count = out_len // in_len
|
||||
# else:
|
||||
# raise ValueError("number of fork outputs not a multiple of inputs")
|
||||
return self
|
||||
|
||||
|
||||
class MixerParameters(StageParameters):
|
||||
"""Parameters for Mixer Stage."""
|
||||
|
||||
gain_db: float = DEFAULT_GAIN_DB(default=-6)
|
||||
|
||||
|
||||
class Mixer(StageModel[Placement_Ni1o]):
|
||||
"""
|
||||
Mixes the input signals together. The mixer can be used to add signals together, or to attenuate the input signals.
|
||||
It must have exactly one output.
|
||||
"""
|
||||
|
||||
op_type: Literal["Mixer"] = "Mixer"
|
||||
parameters: MixerParameters = Field(default_factory=MixerParameters)
|
||||
|
||||
|
||||
class Adder(StageModel[Placement_Ni1o]):
|
||||
"""
|
||||
Add the input signals together. The adder can be used to add signals
|
||||
together. It must have exactly one output.
|
||||
"""
|
||||
|
||||
op_type: Literal["Adder"] = "Adder"
|
||||
|
||||
|
||||
class Subtractor(StageModel[Placement_2i1o]):
|
||||
"""
|
||||
Subtract the input signals. The subtractor can be used to subtract
|
||||
signals together. It must have exactly one output.
|
||||
"""
|
||||
|
||||
op_type: Literal["Subtractor"] = "Subtractor"
|
||||
|
||||
|
||||
class FixedGainParameters(StageParameters):
|
||||
"""Parameters for FixedGain Stage."""
|
||||
|
||||
gain_db: float = DEFAULT_GAIN_DB()
|
||||
|
||||
|
||||
class FixedGain(StageModel):
|
||||
"""
|
||||
This stage implements a fixed gain. The input signal is multiplied
|
||||
by a gain. If the gain is changed at runtime, pops and clicks may
|
||||
occur.
|
||||
|
||||
If the gain needs to be changed at runtime, use a
|
||||
:class:`VolumeControl` stage instead.
|
||||
"""
|
||||
|
||||
op_type: Literal["FixedGain"] = "FixedGain"
|
||||
parameters: FixedGainParameters = Field(default_factory=FixedGainParameters)
|
||||
|
||||
|
||||
class VolumeControlParameters(StageParameters):
|
||||
"""Parameters for VolumeControl Stage."""
|
||||
|
||||
gain_db: float = DEFAULT_GAIN_DB()
|
||||
mute_state: int = Field(
|
||||
default=0,
|
||||
ge=0,
|
||||
le=1,
|
||||
description=("The mute state of the VolumeControl: 0: unmuted, 1: muted."),
|
||||
)
|
||||
|
||||
|
||||
class VolumeControl(StageModel):
|
||||
"""
|
||||
This stage implements a volume control. The input signal is
|
||||
multiplied by a gain. The gain can be changed at runtime. To avoid
|
||||
pops and clicks during gain changes, a slew is applied to the gain
|
||||
update. The stage can be muted and unmuted at runtime.
|
||||
"""
|
||||
|
||||
op_type: Literal["VolumeControl"] = "VolumeControl"
|
||||
parameters: VolumeControlParameters = Field(default_factory=VolumeControlParameters)
|
||||
|
||||
|
||||
class SwitchParameters(StageParameters):
|
||||
"""Parameters for Switch Stage."""
|
||||
|
||||
position: int = Field(
|
||||
default=0,
|
||||
ge=0,
|
||||
description="Switch position. This changes the output signal to the input[index]",
|
||||
)
|
||||
|
||||
|
||||
class Switch(StageModel[Placement_Ni1o]):
|
||||
"""
|
||||
Switch the input to one of the outputs. The switch can be used to
|
||||
select between different signals.
|
||||
|
||||
"""
|
||||
|
||||
op_type: Literal["Switch"] = "Switch"
|
||||
parameters: SwitchParameters = Field(default_factory=SwitchParameters)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def set_max_outputs(self):
|
||||
"""Set the maximum number os switch positions."""
|
||||
max_val = len(self.placement.input)
|
||||
type(self.parameters).model_fields["position"].metadata.append(
|
||||
annotated_types.Le(max_val - 1)
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class SwitchSlew(Switch):
|
||||
"""
|
||||
Switch the input to one of the outputs with slew. The switch can be used to
|
||||
select between different signals.
|
||||
|
||||
"""
|
||||
|
||||
op_type: Literal["SwitchSlew"] = "SwitchSlew" # pyright: ignore
|
||||
|
||||
|
||||
class SwitchStereo(StageModel):
|
||||
"""
|
||||
Switch the input to one of the stereo pairs of outputs. The switch
|
||||
can be used to select between different stereo signal pairs. The
|
||||
inputs should be passed in pairs, e.g. ``[0_L, 0_R, 1_L, 1_R, ...]``.
|
||||
Setting the switch position will output the nth pair.
|
||||
|
||||
"""
|
||||
|
||||
op_type: Literal["SwitchStereo"] = "SwitchStereo"
|
||||
parameters: SwitchParameters = Field(default_factory=SwitchParameters)
|
||||
|
||||
|
||||
class DelayConfig(StageConfig):
|
||||
"""Configuration for delay stage.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
max_delay: Maximum delay length in samples
|
||||
units: Units for delay values, either "samples" or "seconds"
|
||||
"""
|
||||
|
||||
max_delay: float = Field(default=1024, gt=0, description="Maximum delay length in units")
|
||||
units: Literal["samples", "s", "ms"] = Field(
|
||||
default="samples", description="Units for maximum delay values"
|
||||
)
|
||||
|
||||
|
||||
class DelayParameters(StageParameters):
|
||||
"""Parameters for delay stage."""
|
||||
|
||||
delay: float = Field(
|
||||
default=0, ge=0, description="Current delay length in the configured units"
|
||||
)
|
||||
|
||||
|
||||
class Delay(StageModel):
|
||||
"""Delay stage for delaying input signals.
|
||||
|
||||
Delays the input signal by a specified amount. The maximum delay is set at
|
||||
compile time via config, and the runtime delay can be set between 0 and max_delay.
|
||||
The delay can be specified in either samples or seconds.
|
||||
"""
|
||||
|
||||
op_type: Literal["Delay"] = "Delay"
|
||||
parameters: DelayParameters = Field(default_factory=DelayParameters)
|
||||
config: DelayConfig = Field(default_factory=DelayConfig)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def set_max_delay(self):
|
||||
"""Set the maximum delay value based on the configuration."""
|
||||
max_val = self.config.max_delay
|
||||
type(self.parameters).model_fields["delay"].metadata.append(annotated_types.Le(max_val))
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class CrossfaderParameters(StageParameters):
|
||||
"""Parameters for crossfader stage."""
|
||||
|
||||
mix: float = Field(default=0.5, le=1, ge=0, description="Set the mix of the crossfader")
|
||||
|
||||
|
||||
class Crossfader(StageModel[Placement_2i1o]):
|
||||
"""Crossfader stage model."""
|
||||
|
||||
op_type: Literal["Crossfader"] = "Crossfader"
|
||||
parameters: CrossfaderParameters = Field(default_factory=CrossfaderParameters)
|
||||
|
||||
|
||||
class CrossfaderStereo(StageModel[Placement_4i2o]):
|
||||
"""Stereo Crossfader stage model."""
|
||||
|
||||
op_type: Literal["CrossfaderStereo"] = "CrossfaderStereo"
|
||||
parameters: CrossfaderParameters = Field(default_factory=CrossfaderParameters)
|
||||
127
lib_audio_dsp/python/audio_dsp/models/stage.py
Normal file
127
lib_audio_dsp/python/audio_dsp/models/stage.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# Copyright 2025 XMOS LIMITED.
|
||||
# This Software is subject to the terms of the XMOS Public Licence: Version 1.
|
||||
"""Generic pydantic models for DSP Stages."""
|
||||
|
||||
from typing import Type, Union, Optional, Any, Tuple
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
|
||||
class edgeProducerBaseModel(BaseModel):
|
||||
"""The pydantic model defining an edge producer (e.g. DSP Stage)."""
|
||||
|
||||
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||
|
||||
|
||||
class _GlobalStageModels:
|
||||
"""Class to hold some globals."""
|
||||
|
||||
stages = []
|
||||
|
||||
|
||||
class StageConfig(BaseModel, extra="ignore"):
|
||||
"""The pydantic model defining the compile-time configurable configuration of a DSP Stage."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StageParameters(BaseModel, extra="ignore"):
|
||||
"""The pydantic model defining the runtime configurable cparameters of a DSP Stage."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NodePlacement(BaseModel, extra="forbid"):
|
||||
"""The pydantic model that defines the placement of a DSP Stage in the graph.
|
||||
|
||||
By default this expects inputs and outputs for each stage.
|
||||
This may be subclassed for custom placement behaviour.
|
||||
"""
|
||||
|
||||
name: str
|
||||
input: list[Tuple[str, int]] = Field(
|
||||
default=[],
|
||||
description="List of input edges.",
|
||||
)
|
||||
thread: int = Field(ge=0, lt=5)
|
||||
|
||||
# @field_validator("input", "output", mode="before")
|
||||
# def _single_to_list(cls, value: Union[int, list]) -> list:
|
||||
# if isinstance(value, list):
|
||||
# return value
|
||||
# else:
|
||||
# return [value]
|
||||
|
||||
|
||||
class MonoPlacement(NodePlacement):
|
||||
"""The placement of a mono stage that must have 1 input and 1 output."""
|
||||
|
||||
input: list[Tuple[str, int]] = Field(
|
||||
default=[],
|
||||
description="List of input edges.",
|
||||
min_length=1,
|
||||
max_length=1,
|
||||
)
|
||||
|
||||
|
||||
class StereoPlacement(NodePlacement):
|
||||
"""The placement of a stereo stage that must have 2 inputs and 2 outputs."""
|
||||
|
||||
input: list[Tuple[str, int]] = Field(
|
||||
default=[],
|
||||
description="List of input edges.",
|
||||
min_length=2,
|
||||
max_length=2,
|
||||
)
|
||||
|
||||
|
||||
class Placement_2i1o(NodePlacement):
|
||||
"""The placement of a stage that must have 2 inputs and 1 outputs."""
|
||||
|
||||
input: list[Tuple[str, int]] = Field(
|
||||
default=[],
|
||||
description="List of input edges.",
|
||||
min_length=2,
|
||||
max_length=2,
|
||||
)
|
||||
|
||||
|
||||
class Placement_4i2o(NodePlacement):
|
||||
"""The placement of a stage that must have 2 inputs and 1 outputs."""
|
||||
|
||||
input: list[Tuple[str, int]] = Field(
|
||||
default=[],
|
||||
description="List of input edges.",
|
||||
min_length=4,
|
||||
max_length=4,
|
||||
)
|
||||
|
||||
|
||||
class Placement_Ni1o(NodePlacement, extra="forbid"):
|
||||
"""Graph placement for a Stage that takes many input and one output."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class StageModel[Placement: NodePlacement](edgeProducerBaseModel):
|
||||
"""A generic pydantic model of a DSP Stage.
|
||||
|
||||
Stages should subclass this and define their op_type, parameters (optional),
|
||||
compile-time config (optional), and specific placement requirements (optional).
|
||||
"""
|
||||
|
||||
placement: Placement
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
"""Add all subclasses of StageModel to a global list for querying."""
|
||||
super().__init_subclass__()
|
||||
_GlobalStageModels.stages.append(cls)
|
||||
|
||||
|
||||
def all_models() -> dict[str, Type[StageModel]]:
|
||||
"""Get a dict containing all stages in scope."""
|
||||
return {
|
||||
s.__name__: s
|
||||
for s in _GlobalStageModels.stages
|
||||
if "op_type" in s.model_fields and not s.__name__.startswith("_")
|
||||
}
|
||||
Reference in New Issue
Block a user