This commit is contained in:
Steven Dan
2025-12-11 09:43:42 +08:00
commit d8b2974133
1822 changed files with 280037 additions and 0 deletions

View 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

View 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())
)

View 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)

View 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)

View 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 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
)

View 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)

View 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,
]

View 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.")

View 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)

View 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)

View 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)

View File

@@ -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
)

View 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)

View 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)

View 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("_")
}