From: Maxim Raznatovski Date: Thu, 13 Mar 2025 17:35:56 +0000 (+0100) Subject: Feat: Mutichannel Parametric Equalizer (#2031) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=93842950127392ce2494eb55c7b6ba47601e7308;p=music-assistant-server.git Feat: Mutichannel Parametric Equalizer (#2031) * 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 --- diff --git a/music_assistant/helpers/dsp.py b/music_assistant/helpers/dsp.py index 5195bd64..1d741fb1 100644 --- a/music_assistant/helpers/dsp.py +++ b/music_assistant/helpers/dsp.py @@ -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