Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Models required for a Configurable DSP with Parametric Equalizer #18

Merged
merged 4 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions music_assistant_models/dsp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""All DSP (Digital Signal Processing) related models."""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import StrEnum
from typing import Literal

from mashumaro import DataClassDictMixin

# ruff: noqa: S105


class DSPFilterType(StrEnum):
"""Enum of all supported DSP Filter Types."""

PARAMETRIC_EQ = "parametric_eq"
TONE_CONTROL = "tone_control"


@dataclass
class DSPFilterBase(DataClassDictMixin):
"""Base model for all DSP filters."""

# Enable/disable this filter
enabled: bool

def validate(self) -> None:
"""Validate the DSP filter."""


class ParametricEQBandType(StrEnum):
"""Enum for Parametric EQ band types."""

PEAK = "peak"
HIGH_SHELF = "high_shelf"
LOW_SHELF = "low_shelf"
HIGH_PASS = "high_pass"
LOW_PASS = "low_pass"
NOTCH = "notch"
UNKNOWN = "unknown"

@classmethod
def _missing_(cls, _: object) -> ParametricEQBandType:
"""Set default enum member if an unknown value is provided."""
return cls.UNKNOWN


@dataclass
class ParametricEQBand(DataClassDictMixin):
"""Model for a single Parametric EQ band."""

# Center frequency of the band in Hz
frequency: float = 1000.0
# Q factor, changes the bandwidth of the band
q: float = 1.0
# Gain in dB, can be negative or positive
gain: float = 0.0
# Equalizer band type, changes the behavior of the band
type: ParametricEQBandType = ParametricEQBandType.PEAK
# Enable/disable the band
enabled: bool = True


@dataclass
class ParametricEQFilter(DSPFilterBase):
"""Model for a Parametric EQ filter."""

type: Literal[DSPFilterType.PARAMETRIC_EQ] = DSPFilterType.PARAMETRIC_EQ
bands: list[ParametricEQBand] = field(default_factory=list)

def validate(self) -> None:
"""Validate the Parametric EQ filter."""
# Validate bands
for band in self.bands:
if not 0.0 < band.frequency <= 100000.0:
raise ValueError("Band frequency must be in the range 0.0 to 100000.0 Hz")
if not 0.01 <= band.q <= 100.0:
raise ValueError("Band Q factor must be in the range 0.01 to 100.0")
if not -60.0 <= band.gain <= 60.0:
raise ValueError("Band gain must be in the range -60.0 to 60.0 dB")


@dataclass
class ToneControlFilter(DSPFilterBase):
"""Model for a Tone Control filter."""

type: Literal[DSPFilterType.TONE_CONTROL] = DSPFilterType.TONE_CONTROL
# Bass level in dB, can be negative or positive
bass_level: float = 0.0
# Mid level in dB, can be negative or positive
mid_level: float = 0.0
# Treble level in dB, can be negative or positive
treble_level: float = 0.0

def validate(self) -> None:
"""Validate the Tone Control filter."""
# Validate bass level
if not -60.0 <= self.bass_level <= 60.0:
raise ValueError("Bass level must be in the range -60.0 to 60.0 dB")
# Validate mid level
if not -60.0 <= self.mid_level <= 60.0:
raise ValueError("Mid level must be in the range -60.0 to 60.0 dB")
# Validate treble level
if not -60.0 <= self.treble_level <= 60.0:
raise ValueError("Treble level must be in the range -60.0 to 60.0 dB")


# Type alias for all possible DSP filters
DSPFilter = ParametricEQFilter | ToneControlFilter


@dataclass
class DSPConfig(DataClassDictMixin):
"""Model for a complete DSP configuration."""

# Enable/disable the complete DSP configuration, including input/output stages
enabled: bool = True
# List of DSP filters that are applied in order
filters: list[DSPFilter] = field(default_factory=list)
# Input gain in dB, will be applied before any other DSP filters
input_gain: float = 0.0
# Output gain in dB, will be applied after all other DSP filters
output_gain: float = 0.0
# Enable/disable the default output limiter, will be applied after all other DSP effects
# to prevent clipping
output_limiter: bool = True

def validate(self) -> None:
"""Validate the DSP configuration."""
# Validate input gain
if not -60.0 <= self.input_gain <= 60.0:
raise ValueError("Input gain must be in the range -60.0 to 60.0 dB")
# Validate output gain
if not -60.0 <= self.output_gain <= 60.0:
raise ValueError("Output gain must be in the range -60.0 to 60.0 dB")
# Validate filters
for f in self.filters:
f.validate()
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ asyncio_mode = "auto"
ignore = [
"ANN002", # Just annoying, not really useful
"ANN003", # Just annoying, not really useful
"ANN101", # Self... explanatory
"ANN401", # Opinioated warning on disallowing dynamically typed expressions
"D203", # Conflicts with other rules
"D213", # Conflicts with other rules
Expand Down
Loading