Skip to content

Commit

Permalink
Refactor: Move DSP filter to ffmpeg arg conversion to it's own module
Browse files Browse the repository at this point in the history
  • Loading branch information
maximmaxim345 committed Dec 14, 2024
1 parent 44be66f commit 5608b6a
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 97 deletions.
100 changes: 3 additions & 97 deletions music_assistant/helpers/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import asyncio
import logging
import math
import os
import re
import struct
Expand All @@ -15,7 +14,6 @@

import aiofiles
from aiohttp import ClientTimeout
from music_assistant_models.dsp import ParametricEQBandType, ParametricEQFilter, ToneControlFilter
from music_assistant_models.enums import ContentType, MediaType, StreamType, VolumeNormalizationMode
from music_assistant_models.errors import (
InvalidDataError,
Expand All @@ -38,6 +36,7 @@
from music_assistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads
from music_assistant.helpers.util import clean_stream_title

from .dsp import filter_to_ffmpeg_params
from .ffmpeg import FFMpeg, get_ffmpeg_stream
from .playlists import IsHLSPlaylist, PlaylistItem, fetch_playlist, parse_m3u
from .process import AsyncProcess, check_output, communicate
Expand Down Expand Up @@ -850,101 +849,8 @@ def get_player_filter_params(
if not f.enabled:
continue

if isinstance(f, ParametricEQFilter):
for b in f.bands:
if not b.enabled:
continue
# From https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html

f_s = input_format.sample_rate
f_0 = b.frequency
db_gain = b.gain
q = b.q

a = math.sqrt(10 ** (db_gain / 20))
w_0 = 2 * math.pi * f_0 / f_s
alpha = math.sin(w_0) / (2 * q)

if b.type == ParametricEQBandType.PEAK:
b0 = 1 + alpha * a
b1 = -2 * math.cos(w_0)
b2 = 1 - alpha * a
a0 = 1 + alpha / a
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}"
)
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))
b2 = a * ((a + 1) - (a - 1) * math.cos(w_0) - 2 * math.sqrt(a) * alpha)
a0 = (a + 1) + (a - 1) * math.cos(w_0) + 2 * math.sqrt(a) * alpha
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}"
)
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))
b2 = a * ((a + 1) + (a - 1) * math.cos(w_0) - 2 * math.sqrt(a) * alpha)
a0 = (a + 1) - (a - 1) * math.cos(w_0) + 2 * math.sqrt(a) * alpha
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}"
)
elif b.type == ParametricEQBandType.HIGH_PASS:
b0 = (1 + math.cos(w_0)) / 2
b1 = -(1 + math.cos(w_0))
b2 = (1 + math.cos(w_0)) / 2
a0 = 1 + alpha
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}"
)
elif b.type == ParametricEQBandType.LOW_PASS:
b0 = (1 - math.cos(w_0)) / 2
b1 = 1 - math.cos(w_0)
b2 = (1 - math.cos(w_0)) / 2
a0 = 1 + alpha
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}"
)
elif b.type == ParametricEQBandType.NOTCH:
b0 = 1
b1 = -2 * math.cos(w_0)
b2 = 1
a0 = 1 + alpha
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}"
)
if isinstance(f, ToneControlFilter):
# A basic 3-band equalizer
if f.bass_level != 0:
filter_params.append(
f"equalizer=frequency=100:width=200:width_type=h:gain={f.bass_level}"
)
if f.mid_level != 0:
filter_params.append(
f"equalizer=frequency=900:width=1800:width_type=h:gain={f.mid_level}"
)
if f.treble_level != 0:
filter_params.append(
f"equalizer=frequency=9000:width=18000:width_type=h:gain={f.treble_level}"
)
# Apply filter
filter_params.extend(filter_to_ffmpeg_params(f, input_format))

# Apply output gain
if dsp.output_gain != 0:
Expand Down
112 changes: 112 additions & 0 deletions music_assistant/helpers/dsp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Helper functions for DSP filters."""

import math

from music_assistant_models.dsp import (
DSPFilter,
ParametricEQBandType,
ParametricEQFilter,
ToneControlFilter,
)
from music_assistant_models.streamdetails import AudioFormat

# ruff: noqa: PLR0915


def filter_to_ffmpeg_params(dsp_filter: DSPFilter, input_format: AudioFormat) -> list[str]:
"""Convert a DSP filter model to FFmpeg filter parameters.
Args:
dsp_filter: DSP filter configuration (ParametricEQ or ToneControl)
input_format: Audio format containing sample rate
Returns:
List of FFmpeg filter parameter strings
"""
filter_params = []

if isinstance(dsp_filter, ParametricEQFilter):
for b in dsp_filter.bands:
if not b.enabled:
continue
# From https://webaudio.github.io/Audio-EQ-Cookbook/audio-eq-cookbook.html

f_s = input_format.sample_rate
f_0 = b.frequency
db_gain = b.gain
q = b.q

a = math.sqrt(10 ** (db_gain / 20))
w_0 = 2 * math.pi * f_0 / f_s
alpha = math.sin(w_0) / (2 * q)

if b.type == ParametricEQBandType.PEAK:
b0 = 1 + alpha * a
b1 = -2 * math.cos(w_0)
b2 = 1 - alpha * a
a0 = 1 + alpha / a
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}")
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))
b2 = a * ((a + 1) - (a - 1) * math.cos(w_0) - 2 * math.sqrt(a) * alpha)
a0 = (a + 1) + (a - 1) * math.cos(w_0) + 2 * math.sqrt(a) * alpha
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}")
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))
b2 = a * ((a + 1) + (a - 1) * math.cos(w_0) - 2 * math.sqrt(a) * alpha)
a0 = (a + 1) - (a - 1) * math.cos(w_0) + 2 * math.sqrt(a) * alpha
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}")
elif b.type == ParametricEQBandType.HIGH_PASS:
b0 = (1 + math.cos(w_0)) / 2
b1 = -(1 + math.cos(w_0))
b2 = (1 + math.cos(w_0)) / 2
a0 = 1 + alpha
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}")
elif b.type == ParametricEQBandType.LOW_PASS:
b0 = (1 - math.cos(w_0)) / 2
b1 = 1 - math.cos(w_0)
b2 = (1 - math.cos(w_0)) / 2
a0 = 1 + alpha
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}")
elif b.type == ParametricEQBandType.NOTCH:
b0 = 1
b1 = -2 * math.cos(w_0)
b2 = 1
a0 = 1 + alpha
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}")
if isinstance(dsp_filter, ToneControlFilter):
# A basic 3-band equalizer
if dsp_filter.bass_level != 0:
filter_params.append(
f"equalizer=frequency=100:width=200:width_type=h:gain={dsp_filter.bass_level}"
)
if dsp_filter.mid_level != 0:
filter_params.append(
f"equalizer=frequency=900:width=1800:width_type=h:gain={dsp_filter.mid_level}"
)
if dsp_filter.treble_level != 0:
filter_params.append(
f"equalizer=frequency=9000:width=18000:width_type=h:gain={dsp_filter.treble_level}"
)

return filter_params

0 comments on commit 5608b6a

Please sign in to comment.