fix: apply replay gain preferences on scheduled files (#2945)

### Description

The replay gain preferences are applied in the legacy code, but the
playout code was missing this feature. The replay gain was not applied
when playout fetched the schedules.


37d1a7685e/legacy/application/models/Schedule.php (L881-L886)
This commit is contained in:
Jonas L 2024-02-08 20:29:10 +01:00 committed by GitHub
parent 37d1a7685e
commit 35d0dec4a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 112 additions and 43 deletions

View File

@ -18,12 +18,13 @@ class StreamPreferences(BaseModel):
input_fade_transition: float input_fade_transition: float
message_format: MessageFormatKind message_format: MessageFormatKind
message_offline: str message_offline: str
replay_gain_enabled: bool
replay_gain_offset: float
# input_auto_switch_off: bool # input_auto_switch_off: bool
# input_auto_switch_on: bool # input_auto_switch_on: bool
# input_main_user: str # input_main_user: str
# input_main_password: str # input_main_password: str
# replay_gain_enabled: bool
# replay_gain_offset: float
# track_fade_in: float # track_fade_in: float
# track_fade_out: float # track_fade_out: float
# track_fade_transition: float # track_fade_transition: float
@ -82,6 +83,8 @@ class Preference(models.Model):
int(entries.get("stream_label_format") or 0) int(entries.get("stream_label_format") or 0)
), ),
message_offline=entries.get("off_air_meta") or "Offline", message_offline=entries.get("off_air_meta") or "Offline",
replay_gain_enabled=entries.get("enable_replay_gain") == "1",
replay_gain_offset=float(entries.get("replay_gain_modifier") or 0.0),
) )
@classmethod @classmethod

View File

@ -6,6 +6,8 @@ class StreamPreferencesSerializer(serializers.Serializer):
input_fade_transition = serializers.FloatField(read_only=True) input_fade_transition = serializers.FloatField(read_only=True)
message_format = serializers.IntegerField(read_only=True) message_format = serializers.IntegerField(read_only=True)
message_offline = serializers.CharField(read_only=True) message_offline = serializers.CharField(read_only=True)
replay_gain_enabled = serializers.BooleanField(read_only=True)
replay_gain_offset = serializers.FloatField(read_only=True)
# pylint: disable=abstract-method # pylint: disable=abstract-method

View File

@ -16,6 +16,8 @@ def test_preference_get_stream_preferences(db):
"input_fade_transition": 0.0, "input_fade_transition": 0.0,
"message_format": 0, "message_format": 0,
"message_offline": "LibreTime - offline", "message_offline": "LibreTime - offline",
"replay_gain_enabled": True,
"replay_gain_offset": 0.0,
} }

View File

@ -9,6 +9,8 @@ def test_stream_preferences_get(db, api_client: APIClient):
"input_fade_transition": 0.0, "input_fade_transition": 0.0,
"message_format": 0, "message_format": 0,
"message_offline": "LibreTime - offline", "message_offline": "LibreTime - offline",
"replay_gain_enabled": True,
"replay_gain_offset": 0.0,
} }

View File

@ -19,6 +19,8 @@ class StreamPreferencesView(views.APIView):
"input_fade_transition", "input_fade_transition",
"message_format", "message_format",
"message_offline", "message_offline",
"replay_gain_enabled",
"replay_gain_offset",
} }
) )
) )

View File

@ -7535,10 +7535,19 @@ components:
message_offline: message_offline:
type: string type: string
readOnly: true readOnly: true
replay_gain_enabled:
type: boolean
readOnly: true
replay_gain_offset:
type: number
format: double
readOnly: true
required: required:
- input_fade_transition - input_fade_transition
- message_format - message_format
- message_offline - message_offline
- replay_gain_enabled
- replay_gain_offset
StreamState: StreamState:
type: object type: object
properties: properties:

View File

@ -17,6 +17,8 @@ class StreamPreferences(BaseModel):
input_fade_transition: float input_fade_transition: float
message_format: MessageFormatKind message_format: MessageFormatKind
message_offline: str message_offline: str
replay_gain_enabled: bool
replay_gain_offset: float
class StreamState(BaseModel): class StreamState(BaseModel):

View File

@ -64,15 +64,11 @@ def get_schedule(api_client: ApiClient) -> Events:
if show["live_enabled"]: if show["live_enabled"]:
show_instance["starts_at"] = event_isoparse(show_instance["starts_at"]) show_instance["starts_at"] = event_isoparse(show_instance["starts_at"])
show_instance["ends_at"] = event_isoparse(show_instance["ends_at"]) show_instance["ends_at"] = event_isoparse(show_instance["ends_at"])
generate_live_events( generate_live_events(events, show_instance, stream_preferences)
events,
show_instance,
stream_preferences.input_fade_transition,
)
if item["file"]: if item["file"]:
file = api_client.get_file(item["file"]).json() file = api_client.get_file(item["file"]).json()
generate_file_events(events, item, file, show) generate_file_events(events, item, file, show, stream_preferences)
elif item["stream"]: elif item["stream"]:
webstream = api_client.get_webstream(item["stream"]).json() webstream = api_client.get_webstream(item["stream"]).json()
@ -84,9 +80,9 @@ def get_schedule(api_client: ApiClient) -> Events:
def generate_live_events( def generate_live_events(
events: Events, events: Events,
show_instance: dict, show_instance: dict,
input_fade_transition: float, stream_preferences: StreamPreferences,
): ):
transition = timedelta(seconds=input_fade_transition) transition = timedelta(seconds=stream_preferences.input_fade_transition)
switch_off = show_instance["ends_at"] - transition switch_off = show_instance["ends_at"] - transition
kick_out = show_instance["ends_at"] kick_out = show_instance["ends_at"]
@ -118,6 +114,7 @@ def generate_file_events(
schedule: dict, schedule: dict,
file: dict, file: dict,
show: dict, show: dict,
stream_preferences: StreamPreferences,
): ):
""" """
Generate events for a scheduled file. Generate events for a scheduled file.
@ -143,6 +140,15 @@ def generate_file_events(
replay_gain=file["replay_gain"], replay_gain=file["replay_gain"],
filesize=file["size"], filesize=file["size"],
) )
if event.replay_gain is None:
event.replay_gain = 0.0
if stream_preferences.replay_gain_enabled:
event.replay_gain += stream_preferences.replay_gain_offset
else:
event.replay_gain = None
insert_event(events, event.start_key, event) insert_event(events, event.start_key, event)

View File

@ -1,6 +1,7 @@
import pytest import pytest
from libretime_playout.config import Config from libretime_playout.config import Config
from libretime_playout.liquidsoap.models import StreamPreferences
@pytest.fixture() @pytest.fixture()
@ -36,3 +37,14 @@ def config():
}, },
} }
) )
@pytest.fixture()
def stream_preferences():
return StreamPreferences(
input_fade_transition=0.0,
message_format=0,
message_offline="LibreTime - offline",
replay_gain_enabled=True,
replay_gain_offset=-3.5,
)

View File

@ -21,7 +21,12 @@ from .fixtures import TEST_STREAM_CONFIGS, make_config_with_stream
"stream_config", "stream_config",
TEST_STREAM_CONFIGS, TEST_STREAM_CONFIGS,
) )
def test_generate_entrypoint(stream_config: Config, version, snapshot): def test_generate_entrypoint(
stream_config: Config,
stream_preferences: StreamPreferences,
version,
snapshot,
):
with mock.patch( with mock.patch(
"libretime_playout.liquidsoap.entrypoint.here", "libretime_playout.liquidsoap.entrypoint.here",
Path("/fake"), Path("/fake"),
@ -29,11 +34,7 @@ def test_generate_entrypoint(stream_config: Config, version, snapshot):
found = generate_entrypoint( found = generate_entrypoint(
log_filepath=Path("/var/log/radio.log"), log_filepath=Path("/var/log/radio.log"),
config=stream_config, config=stream_config,
preferences=StreamPreferences( preferences=stream_preferences,
input_fade_transition=0.0,
message_format=0,
message_offline="LibreTime - offline",
),
info=Info( info=Info(
station_name="LibreTime", station_name="LibreTime",
), ),
@ -51,7 +52,11 @@ def test_generate_entrypoint(stream_config: Config, version, snapshot):
"stream_config", "stream_config",
TEST_STREAM_CONFIGS, TEST_STREAM_CONFIGS,
) )
def test_liquidsoap_syntax(tmp_path: Path, stream_config): def test_liquidsoap_syntax(
tmp_path: Path,
stream_config: Config,
stream_preferences: StreamPreferences,
):
entrypoint_filepath = tmp_path / "radio.liq" entrypoint_filepath = tmp_path / "radio.liq"
log_filepath = tmp_path / "radio.log" log_filepath = tmp_path / "radio.log"
@ -59,11 +64,7 @@ def test_liquidsoap_syntax(tmp_path: Path, stream_config):
generate_entrypoint( generate_entrypoint(
log_filepath=log_filepath, log_filepath=log_filepath,
config=stream_config, config=stream_config,
preferences=StreamPreferences( preferences=stream_preferences,
input_fade_transition=0.0,
message_format=0,
message_offline="LibreTime - offline",
),
info=Info( info=Info(
station_name="LibreTime", station_name="LibreTime",
), ),
@ -79,7 +80,10 @@ def test_liquidsoap_syntax(tmp_path: Path, stream_config):
LIQ_VERSION >= (2, 0, 0), LIQ_VERSION >= (2, 0, 0),
reason="unsupported liquidsoap >= 2.0.0", reason="unsupported liquidsoap >= 2.0.0",
) )
def test_liquidsoap_unsupported_output_aac(tmp_path: Path): def test_liquidsoap_unsupported_output_aac(
tmp_path: Path,
stream_preferences: StreamPreferences,
):
entrypoint_filepath = tmp_path / "radio.liq" entrypoint_filepath = tmp_path / "radio.liq"
log_filepath = tmp_path / "radio.log" log_filepath = tmp_path / "radio.log"
@ -98,11 +102,7 @@ def test_liquidsoap_unsupported_output_aac(tmp_path: Path):
] ]
} }
), ),
preferences=StreamPreferences( preferences=stream_preferences,
input_fade_transition=0.0,
message_format=0,
message_offline="LibreTime - offline",
),
info=Info( info=Info(
station_name="LibreTime", station_name="LibreTime",
), ),

View File

@ -4,6 +4,7 @@ from datetime import datetime
import pytest import pytest
from libretime_api_client.v2 import ApiClient from libretime_api_client.v2 import ApiClient
from libretime_playout.liquidsoap.models import StreamPreferences
from libretime_playout.player.events import ( from libretime_playout.player.events import (
ActionEvent, ActionEvent,
EventKind, EventKind,
@ -271,13 +272,13 @@ SCHEDULE = [
] ]
def test_generate_live_events(): def test_generate_live_events(stream_preferences: StreamPreferences):
show_instance_3 = SHOW_INSTANCE_3.copy() show_instance_3 = SHOW_INSTANCE_3.copy()
show_instance_3["starts_at"] = event_isoparse(show_instance_3["starts_at"]) show_instance_3["starts_at"] = event_isoparse(show_instance_3["starts_at"])
show_instance_3["ends_at"] = event_isoparse(show_instance_3["ends_at"]) show_instance_3["ends_at"] = event_isoparse(show_instance_3["ends_at"])
result = {} result = {}
generate_live_events(result, show_instance_3, 0.0) generate_live_events(result, show_instance_3, stream_preferences)
assert result == { assert result == {
"2022-09-05-13-00-00": ActionEvent( "2022-09-05-13-00-00": ActionEvent(
start=datetime(2022, 9, 5, 13, 0), start=datetime(2022, 9, 5, 13, 0),
@ -288,7 +289,8 @@ def test_generate_live_events():
} }
result = {} result = {}
generate_live_events(result, show_instance_3, 2.0) stream_preferences.input_fade_transition = 2.0
generate_live_events(result, show_instance_3, stream_preferences)
assert result == { assert result == {
"2022-09-05-12-59-58": ActionEvent( "2022-09-05-12-59-58": ActionEvent(
start=datetime(2022, 9, 5, 12, 59, 58), start=datetime(2022, 9, 5, 12, 59, 58),
@ -305,13 +307,13 @@ def test_generate_live_events():
} }
def test_generate_file_events(): def test_generate_file_events(stream_preferences: StreamPreferences):
schedule_1 = SCHEDULE_1.copy() schedule_1 = SCHEDULE_1.copy()
schedule_1["starts_at"] = event_isoparse(schedule_1["starts_at"]) schedule_1["starts_at"] = event_isoparse(schedule_1["starts_at"])
schedule_1["ends_at"] = event_isoparse(schedule_1["ends_at"]) schedule_1["ends_at"] = event_isoparse(schedule_1["ends_at"])
result = {} result = {}
generate_file_events(result, schedule_1, FILE_2, SHOW_1) generate_file_events(result, schedule_1, FILE_2, SHOW_1, stream_preferences)
assert result == { assert result == {
"2022-09-05-11-00-00": FileEvent( "2022-09-05-11-00-00": FileEvent(
start=datetime(2022, 9, 5, 11, 0), start=datetime(2022, 9, 5, 11, 0),
@ -328,7 +330,32 @@ def test_generate_file_events():
track_title="My Friend the Forest", track_title="My Friend the Forest",
artist_name="Nils Frahm", artist_name="Nils Frahm",
mime="audio/flac", mime="audio/flac",
replay_gain=11.46, replay_gain=11.46 - 3.5,
filesize=10000,
file_ready=False,
)
}
result = {}
stream_preferences.replay_gain_enabled = False
generate_file_events(result, schedule_1, FILE_2, SHOW_1, stream_preferences)
assert result == {
"2022-09-05-11-00-00": FileEvent(
start=datetime(2022, 9, 5, 11, 0),
end=datetime(2022, 9, 5, 11, 5, 2),
type=EventKind.FILE,
row_id=1,
uri=None,
id=2,
show_name="Show 1",
fade_in=500.0,
fade_out=500.0,
cue_in=13.7008,
cue_out=315.845,
track_title="My Friend the Forest",
artist_name="Nils Frahm",
mime="audio/flac",
replay_gain=None,
filesize=10000, filesize=10000,
file_ready=False, file_ready=False,
) )
@ -398,6 +425,8 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient):
"input_fade_transition": 2.0, "input_fade_transition": 2.0,
"message_format": 0, "message_format": 0,
"message_offline": "", "message_offline": "",
"replay_gain_enabled": True,
"replay_gain_offset": -3.5,
}, },
) )
@ -434,7 +463,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient):
track_title="My Friend the Forest", track_title="My Friend the Forest",
artist_name="Nils Frahm", artist_name="Nils Frahm",
mime="audio/flac", mime="audio/flac",
replay_gain=11.46, replay_gain=11.46 - 3.5,
filesize=10000, filesize=10000,
file_ready=False, file_ready=False,
), ),
@ -453,7 +482,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient):
track_title="#2", track_title="#2",
artist_name="Nils Frahm", artist_name="Nils Frahm",
mime="audio/flac", mime="audio/flac",
replay_gain=-1.65, replay_gain=-1.65 - 3.5,
filesize=10000, filesize=10000,
file_ready=False, file_ready=False,
), ),
@ -472,7 +501,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient):
track_title="Democracy Now! 2022-09-05 Monday", track_title="Democracy Now! 2022-09-05 Monday",
artist_name="Democracy Now! Audio", artist_name="Democracy Now! Audio",
mime="audio/mp3", mime="audio/mp3",
replay_gain=-1.39, replay_gain=-1.39 - 3.5,
filesize=10000, filesize=10000,
file_ready=False, file_ready=False,
), ),
@ -491,7 +520,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient):
track_title="#2", track_title="#2",
artist_name="Nils Frahm", artist_name="Nils Frahm",
mime="audio/flac", mime="audio/flac",
replay_gain=-1.65, replay_gain=-1.65 - 3.5,
filesize=10000, filesize=10000,
file_ready=False, file_ready=False,
), ),
@ -546,7 +575,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient):
track_title="All Melody", track_title="All Melody",
artist_name="Nils Frahm", artist_name="Nils Frahm",
mime="audio/flac", mime="audio/flac",
replay_gain=-2.13, replay_gain=-2.13 - 3.5,
filesize=10000, filesize=10000,
file_ready=False, file_ready=False,
), ),
@ -565,7 +594,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient):
track_title="My Friend the Forest", track_title="My Friend the Forest",
artist_name="Nils Frahm", artist_name="Nils Frahm",
mime="audio/flac", mime="audio/flac",
replay_gain=11.46, replay_gain=11.46 - 3.5,
filesize=10000, filesize=10000,
file_ready=False, file_ready=False,
), ),
@ -584,7 +613,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient):
track_title="The Dane", track_title="The Dane",
artist_name="Nils Frahm", artist_name="Nils Frahm",
mime="audio/flac", mime="audio/flac",
replay_gain=4.52, replay_gain=4.52 - 3.5,
filesize=10000, filesize=10000,
file_ready=False, file_ready=False,
), ),
@ -615,7 +644,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient):
track_title="My Friend the Forest", track_title="My Friend the Forest",
artist_name="Nils Frahm", artist_name="Nils Frahm",
mime="audio/flac", mime="audio/flac",
replay_gain=11.46, replay_gain=11.46 - 3.5,
filesize=10000, filesize=10000,
file_ready=False, file_ready=False,
), ),
@ -634,7 +663,7 @@ def test_get_schedule(schedule, requests_mock, api_client: ApiClient):
track_title="#2", track_title="#2",
artist_name="Nils Frahm", artist_name="Nils Frahm",
mime="audio/flac", mime="audio/flac",
replay_gain=-1.65, replay_gain=-1.65 - 3.5,
filesize=10000, filesize=10000,
file_ready=False, file_ready=False,
), ),