Feat: Mutichannel Parametric Equalizer (#2031)
authorMaxim Raznatovski <nda.mr43@gmail.com>
Thu, 13 Mar 2025 17:35:56 +0000 (18:35 +0100)
committerGitHub <noreply@github.com>
Thu, 13 Mar 2025 17:35:56 +0000 (18:35 +0100)
* refactor: simply settings preamp for PEQs

* fix: check for null

* feat: add support for using a PEQ bands channel

* refactor: add ALL channel

* feat: add multichannel preamp support

* fix: muted channels when per channel preamp not set

music_assistant/helpers/dsp.py

index 5195bd6482f102ded0c8ed7fd53a1227d9deee26..1d741fb1953846a1ca0ed4fef713959b99ba64ec 100644 (file)
@@ -3,6 +3,7 @@
 import math
 
 from music_assistant_models.dsp import (
+    AudioChannel,
     DSPFilter,
     ParametricEQBandType,
     ParametricEQFilter,
@@ -24,14 +25,35 @@ def filter_to_ffmpeg_params(dsp_filter: DSPFilter, input_format: AudioFormat) ->
         List of FFmpeg filter parameter strings
     """
     filter_params = []
-    preamp = 0
 
     if isinstance(dsp_filter, ParametricEQFilter):
-        if dsp_filter.preamp:
-            preamp = dsp_filter.preamp
+        has_per_channel_preamp = any(value != 0 for value in dsp_filter.per_channel_preamp.values())
+        if dsp_filter.preamp and dsp_filter.preamp != 0 and not has_per_channel_preamp:
+            filter_params.append(f"volume={dsp_filter.preamp}dB")
+        # "volume" is handled for the whole audio stream only, so we'll use the pan filter instead
+        elif has_per_channel_preamp:
+            channel_config = []
+            all_channels = [AudioChannel.FL, AudioChannel.FR]
+            for channel_id in all_channels:
+                # Get gain for this channel, default to 0 if not specified
+                gain_db = dsp_filter.per_channel_preamp.get(channel_id, 0)
+                # Apply both the overall preamp and the per-channel preamp
+                total_gain_db = dsp_filter.preamp + gain_db
+                if total_gain_db != 0:
+                    # Convert dB to linear gain
+                    gain = 10 ** (total_gain_db / 20)
+                    channel_config.append(f"{channel_id}={gain}*{channel_id}")
+                else:
+                    channel_config.append(f"{channel_id}={channel_id}")
+
+            # Could potentially also be expanded for more than 2 channels
+            filter_params.append("pan=stereo|" + "|".join(channel_config))
         for b in dsp_filter.bands:
             if not b.enabled:
                 continue
+            channels = ""
+            if b.channel != AudioChannel.ALL:
+                channels = f":c={b.channel}"
             # From https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html
 
             f_s = input_format.sample_rate
@@ -51,7 +73,9 @@ def filter_to_ffmpeg_params(dsp_filter: DSPFilter, input_format: AudioFormat) ->
                 a1 = -2 * math.cos(w_0)
                 a2 = 1 - alpha / a
 
-                filter_params.append(f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}")
+                filter_params.append(
+                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
+                )
             elif b.type == ParametricEQBandType.LOW_SHELF:
                 b0 = a * ((a + 1) - (a - 1) * math.cos(w_0) + 2 * math.sqrt(a) * alpha)
                 b1 = 2 * a * ((a - 1) - (a + 1) * math.cos(w_0))
@@ -60,7 +84,9 @@ def filter_to_ffmpeg_params(dsp_filter: DSPFilter, input_format: AudioFormat) ->
                 a1 = -2 * ((a - 1) + (a + 1) * math.cos(w_0))
                 a2 = (a + 1) + (a - 1) * math.cos(w_0) - 2 * math.sqrt(a) * alpha
 
-                filter_params.append(f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}")
+                filter_params.append(
+                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
+                )
             elif b.type == ParametricEQBandType.HIGH_SHELF:
                 b0 = a * ((a + 1) + (a - 1) * math.cos(w_0) + 2 * math.sqrt(a) * alpha)
                 b1 = -2 * a * ((a - 1) + (a + 1) * math.cos(w_0))
@@ -69,7 +95,9 @@ def filter_to_ffmpeg_params(dsp_filter: DSPFilter, input_format: AudioFormat) ->
                 a1 = 2 * ((a - 1) - (a + 1) * math.cos(w_0))
                 a2 = (a + 1) - (a - 1) * math.cos(w_0) - 2 * math.sqrt(a) * alpha
 
-                filter_params.append(f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}")
+                filter_params.append(
+                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
+                )
             elif b.type == ParametricEQBandType.HIGH_PASS:
                 b0 = (1 + math.cos(w_0)) / 2
                 b1 = -(1 + math.cos(w_0))
@@ -78,7 +106,9 @@ def filter_to_ffmpeg_params(dsp_filter: DSPFilter, input_format: AudioFormat) ->
                 a1 = -2 * math.cos(w_0)
                 a2 = 1 - alpha
 
-                filter_params.append(f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}")
+                filter_params.append(
+                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
+                )
             elif b.type == ParametricEQBandType.LOW_PASS:
                 b0 = (1 - math.cos(w_0)) / 2
                 b1 = 1 - math.cos(w_0)
@@ -87,7 +117,9 @@ def filter_to_ffmpeg_params(dsp_filter: DSPFilter, input_format: AudioFormat) ->
                 a1 = -2 * math.cos(w_0)
                 a2 = 1 - alpha
 
-                filter_params.append(f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}")
+                filter_params.append(
+                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
+                )
             elif b.type == ParametricEQBandType.NOTCH:
                 b0 = 1
                 b1 = -2 * math.cos(w_0)
@@ -96,7 +128,9 @@ def filter_to_ffmpeg_params(dsp_filter: DSPFilter, input_format: AudioFormat) ->
                 a1 = -2 * math.cos(w_0)
                 a2 = 1 - alpha
 
-                filter_params.append(f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}")
+                filter_params.append(
+                    f"biquad=b0={b0}:b1={b1}:b2={b2}:a0={a0}:a1={a1}:a2={a2}{channels}"
+                )
     if isinstance(dsp_filter, ToneControlFilter):
         # A basic 3-band equalizer
         if dsp_filter.bass_level != 0:
@@ -112,7 +146,4 @@ def filter_to_ffmpeg_params(dsp_filter: DSPFilter, input_format: AudioFormat) ->
                 f"equalizer=frequency=9000:width=18000:width_type=h:gain={dsp_filter.treble_level}"
             )
 
-    if preamp != 0:
-        filter_params.insert(0, f"volume={preamp}dB")
-
     return filter_params