music-assistant-server

6.2 KBPY
dsp.py
6.2 KB152 lines • python
1"""Helper functions for DSP filters."""
2
3import math
4
5from music_assistant_models.dsp import (
6    AudioChannel,
7    DSPFilter,
8    ParametricEQBandType,
9    ParametricEQFilter,
10    ToneControlFilter,
11)
12from music_assistant_models.media_items.audio_format import AudioFormat
13
14# ruff: noqa: PLR0915
15
16
17def filter_to_ffmpeg_params(dsp_filter: DSPFilter, input_format: AudioFormat) -> list[str]:
18    """Convert a DSP filter model to FFmpeg filter parameters.
19
20    Args:
21        dsp_filter: DSP filter configuration (ParametricEQ or ToneControl)
22        input_format: Audio format containing sample rate
23
24    Returns:
25        List of FFmpeg filter parameter strings
26    """
27    filter_params = []
28
29    if isinstance(dsp_filter, ParametricEQFilter):
30        has_per_channel_preamp = any(value != 0 for value in dsp_filter.per_channel_preamp.values())
31        if dsp_filter.preamp and dsp_filter.preamp != 0 and not has_per_channel_preamp:
32            filter_params.append(f"volume={dsp_filter.preamp}dB")
33        # "volume" is handled for the whole audio stream only, so we'll use the pan filter instead
34        elif has_per_channel_preamp:
35            channel_config = []
36            all_channels = [AudioChannel.FL, AudioChannel.FR]
37            for channel_id in all_channels:
38                # Get gain for this channel, default to 0 if not specified
39                gain_db = dsp_filter.per_channel_preamp.get(channel_id, 0)
40                # Apply both the overall preamp and the per-channel preamp
41                total_gain_db = (
42                    dsp_filter.preamp + gain_db if dsp_filter.preamp is not None else gain_db
43                )
44                if total_gain_db != 0:
45                    # Convert dB to linear gain
46                    gain = 10 ** (total_gain_db / 20)
47                    channel_config.append(f"{channel_id}={gain}*{channel_id}")
48                else:
49                    channel_config.append(f"{channel_id}={channel_id}")
50
51            # Could potentially also be expanded for more than 2 channels
52            filter_params.append("pan=stereo|" + "|".join(channel_config))
53        for b in dsp_filter.bands:
54            if not b.enabled:
55                continue
56            channels = ""
57            if b.channel != AudioChannel.ALL:
58                channels = f":c={b.channel}"
59            # From https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html
60
61            f_s = input_format.sample_rate
62            f_0 = b.frequency
63            db_gain = b.gain
64            q = b.q
65
66            a = math.sqrt(10 ** (db_gain / 20))
67            w_0 = 2 * math.pi * f_0 / f_s
68            alpha = math.sin(w_0) / (2 * q)
69
70            if b.type == ParametricEQBandType.PEAK:
71                b0 = 1 + alpha * a
72                b1 = -2 * math.cos(w_0)
73                b2 = 1 - alpha * a
74                a0 = 1 + alpha / a
75                a1 = -2 * math.cos(w_0)
76                a2 = 1 - alpha / a
77
78                filter_params.append(
79                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
80                )
81            elif b.type == ParametricEQBandType.LOW_SHELF:
82                b0 = a * ((a + 1) - (a - 1) * math.cos(w_0) + 2 * math.sqrt(a) * alpha)
83                b1 = 2 * a * ((a - 1) - (a + 1) * math.cos(w_0))
84                b2 = a * ((a + 1) - (a - 1) * math.cos(w_0) - 2 * math.sqrt(a) * alpha)
85                a0 = (a + 1) + (a - 1) * math.cos(w_0) + 2 * math.sqrt(a) * alpha
86                a1 = -2 * ((a - 1) + (a + 1) * math.cos(w_0))
87                a2 = (a + 1) + (a - 1) * math.cos(w_0) - 2 * math.sqrt(a) * alpha
88
89                filter_params.append(
90                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
91                )
92            elif b.type == ParametricEQBandType.HIGH_SHELF:
93                b0 = a * ((a + 1) + (a - 1) * math.cos(w_0) + 2 * math.sqrt(a) * alpha)
94                b1 = -2 * a * ((a - 1) + (a + 1) * math.cos(w_0))
95                b2 = a * ((a + 1) + (a - 1) * math.cos(w_0) - 2 * math.sqrt(a) * alpha)
96                a0 = (a + 1) - (a - 1) * math.cos(w_0) + 2 * math.sqrt(a) * alpha
97                a1 = 2 * ((a - 1) - (a + 1) * math.cos(w_0))
98                a2 = (a + 1) - (a - 1) * math.cos(w_0) - 2 * math.sqrt(a) * alpha
99
100                filter_params.append(
101                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
102                )
103            elif b.type == ParametricEQBandType.HIGH_PASS:
104                b0 = (1 + math.cos(w_0)) / 2
105                b1 = -(1 + math.cos(w_0))
106                b2 = (1 + math.cos(w_0)) / 2
107                a0 = 1 + alpha
108                a1 = -2 * math.cos(w_0)
109                a2 = 1 - alpha
110
111                filter_params.append(
112                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
113                )
114            elif b.type == ParametricEQBandType.LOW_PASS:
115                b0 = (1 - math.cos(w_0)) / 2
116                b1 = 1 - math.cos(w_0)
117                b2 = (1 - math.cos(w_0)) / 2
118                a0 = 1 + alpha
119                a1 = -2 * math.cos(w_0)
120                a2 = 1 - alpha
121
122                filter_params.append(
123                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
124                )
125            elif b.type == ParametricEQBandType.NOTCH:
126                b0 = 1
127                b1 = -2 * math.cos(w_0)
128                b2 = 1
129                a0 = 1 + alpha
130                a1 = -2 * math.cos(w_0)
131                a2 = 1 - alpha
132
133                filter_params.append(
134                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
135                )
136    if isinstance(dsp_filter, ToneControlFilter):
137        # A basic 3-band equalizer
138        if dsp_filter.bass_level != 0:
139            filter_params.append(
140                f"equalizer=frequency=100:width=200:width_type=h:gain={dsp_filter.bass_level}"
141            )
142        if dsp_filter.mid_level != 0:
143            filter_params.append(
144                f"equalizer=frequency=900:width=1800:width_type=h:gain={dsp_filter.mid_level}"
145            )
146        if dsp_filter.treble_level != 0:
147            filter_params.append(
148                f"equalizer=frequency=9000:width=18000:width_type=h:gain={dsp_filter.treble_level}"
149            )
150
151    return filter_params
152