/
/
/
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