From 06af18b84e7dfaad95e3b55dda22ec1ddad27050 Mon Sep 17 00:00:00 2001 From: maxtim Date: Fri, 29 Dec 2023 09:22:43 -0500 Subject: [PATCH] feat(playout): configure device for alsa and pulseaudio system outputs (#2654) ### Description Add hardware configuration to liquidsoap so that users may set hardware output in config.yml. --------- Co-authored-by: jo --- docker/config.template.yml | 4 ++ docker/config.yml | 4 ++ docker/example/config.yml | 4 ++ docs/admin-manual/configuration.md | 6 ++- installer/config.yml | 4 ++ legacy/application/configs/conf.php | 1 + .../liquidsoap/templates/outputs.liq.j2 | 31 +++++++++-- .../__snapshots__/entrypoint_test.ambr | 54 ++++++++++++++++++- playout/tests/liquidsoap/fixtures/__init__.py | 11 ++++ shared/libretime_shared/config/_models.py | 41 ++++++++++++-- 10 files changed, 149 insertions(+), 11 deletions(-) diff --git a/docker/config.template.yml b/docker/config.template.yml index a970dece1..3bc76ce1f 100644 --- a/docker/config.template.yml +++ b/docker/config.template.yml @@ -326,3 +326,7 @@ stream: # > must be one of (alsa, ao, oss, portaudio, pulseaudio) # > default is pulseaudio kind: pulseaudio + + # System output device. + # > only available for kind=(alsa, pulseaudio) + device: diff --git a/docker/config.yml b/docker/config.yml index 50ed98bc3..1f48968c6 100644 --- a/docker/config.yml +++ b/docker/config.yml @@ -326,3 +326,7 @@ stream: # > must be one of (alsa, ao, oss, portaudio, pulseaudio) # > default is pulseaudio kind: pulseaudio + + # System output device. + # > only available for kind=(alsa, pulseaudio) + device: diff --git a/docker/example/config.yml b/docker/example/config.yml index 07056660e..394fbb2d7 100644 --- a/docker/example/config.yml +++ b/docker/example/config.yml @@ -326,3 +326,7 @@ stream: # > must be one of (alsa, ao, oss, portaudio, pulseaudio) # > default is pulseaudio kind: pulseaudio + + # System output device. + # > only available for kind=(alsa, pulseaudio) + device: diff --git a/docs/admin-manual/configuration.md b/docs/admin-manual/configuration.md index 06bca4c39..a0f36a8ba 100644 --- a/docs/admin-manual/configuration.md +++ b/docs/admin-manual/configuration.md @@ -526,11 +526,15 @@ stream: system: - # Whether the output is enabled. # > default is false - enabled: false + enabled: true # System output kind. # > must be one of (alsa, ao, oss, portaudio, pulseaudio) # > default is pulseaudio kind: "pulseaudio" + + # System output device. + # > only available for kind=(alsa, pulseaudio) + device: "alsa_output.pci-0000_00_1f.3-platform-skl_hda_dsp_generic.HiFi__hw_sofhdadsp__sink" ``` ## LDAP diff --git a/installer/config.yml b/installer/config.yml index ef0a5893a..0f215c2c3 100644 --- a/installer/config.yml +++ b/installer/config.yml @@ -326,3 +326,7 @@ stream: # > must be one of (alsa, ao, oss, portaudio, pulseaudio) # > default is pulseaudio kind: pulseaudio + + # System output device. + # > only available for kind=(alsa, pulseaudio) + device: diff --git a/legacy/application/configs/conf.php b/legacy/application/configs/conf.php index 67dab1555..229b85d08 100644 --- a/legacy/application/configs/conf.php +++ b/legacy/application/configs/conf.php @@ -211,6 +211,7 @@ class Schema implements ConfigurationInterface /* */->validate()->ifNotInArray(["alsa", "ao", "oss", "portaudio", "pulseaudio"]) /* */->thenInvalid('invalid stream.outputs.system.kind %s') /* */->end()->end() + /* */->scalarNode('device')->end() /**/->end()->end()->end() ->end()->end() diff --git a/playout/libretime_playout/liquidsoap/templates/outputs.liq.j2 b/playout/libretime_playout/liquidsoap/templates/outputs.liq.j2 index 06b21efc1..e8e79f53a 100644 --- a/playout/libretime_playout/liquidsoap/templates/outputs.liq.j2 +++ b/playout/libretime_playout/liquidsoap/templates/outputs.liq.j2 @@ -117,20 +117,41 @@ output.shoutcast( ) {%- endmacro -%} -{% for output in config.stream.outputs.system -%} -{% if output.enabled -%} -# {{ output.kind.value }}:{{ loop.index }} +{#- + Build a system output configuration. +#} +{%- macro output_system(output_id, output) -%} +# {{ output.kind.value }}:{{ output_id }} %ifndef output.{{ output.kind.value }} log("output.{{ output.kind.value }} is not defined!") %endif %ifdef output.{{ output.kind.value }} -output.{{ output.kind.value }}(id="{{ output.kind.value }}:{{ loop.index }}", s) +output.{{ output.kind.value }}( + id="{{ output.kind.value }}:{{ output_id }}", +{%- if output.kind.value == "alsa" %} +{%- if output.device is not none %} + device="{{ output.device }}", +{%- endif %} +{%- elif output.kind.value == "pulseaudio" %} +{%- if output.device is not none %} + device="{{ output.device }}", +{%- endif %} +{%- endif %} + s +) %endif +{%- endmacro -%} + +{# ############################### #} + +{%- for output in config.stream.outputs.system -%} +{% if output.enabled -%} +{{ output_system(loop.index, output) }} {% endif -%} {% endfor -%} -{% for output in config.stream.outputs.icecast -%} +{%- for output in config.stream.outputs.icecast -%} {% if output.enabled -%} {{ output_icecast(loop.index, output) }} diff --git a/playout/tests/liquidsoap/__snapshots__/entrypoint_test.ambr b/playout/tests/liquidsoap/__snapshots__/entrypoint_test.ambr index 4735502f4..3c7f66ef8 100644 --- a/playout/tests/liquidsoap/__snapshots__/entrypoint_test.ambr +++ b/playout/tests/liquidsoap/__snapshots__/entrypoint_test.ambr @@ -315,7 +315,10 @@ log("output.pulseaudio is not defined!") %endif %ifdef output.pulseaudio - output.pulseaudio(id="pulseaudio:1", s) + output.pulseaudio( + id="pulseaudio:1", + s + ) %endif @@ -355,6 +358,55 @@ %include "/fake/1.4/ls_script.liq" + # pulseaudio:1 + %ifndef output.pulseaudio + log("output.pulseaudio is not defined!") + %endif + %ifdef output.pulseaudio + output.pulseaudio( + id="pulseaudio:1", + device="alsa_output.pci-0000_00_sink", + s + ) + %endif + + + + gateway("started") + + ''' +# --- +# name: test_generate_entrypoint[stream_config7-1.4] + ''' + # THIS FILE IS AUTO GENERATED. PLEASE DO NOT EDIT! + ########################################################### + # The ignore() lines are to squash unused variable warnings + + # Inputs + input_main_mount = "main" + input_main_port = 8001 + input_main_secure = false + input_show_mount = "show" + input_show_port = 8002 + input_show_secure = false + + # Settings + set("log.file.path", "/var/log/radio.log") + + set("server.telnet", true) + set("server.telnet.bind_addr", "127.0.0.1") + set("server.telnet.port", 1234) + + set("harbor.bind_addrs", ["0.0.0.0"]) + + station_name = interactive.string("station_name", "LibreTime") + + message_offline = interactive.string("message_offline", "LibreTime - offline") + message_format = interactive.string("message_format", "0") + input_fade_transition = interactive.float("input_fade_transition", 0.0) + + %include "/fake/1.4/ls_script.liq" + gateway("started") diff --git a/playout/tests/liquidsoap/fixtures/__init__.py b/playout/tests/liquidsoap/fixtures/__init__.py index c12166fa8..df832f0ef 100644 --- a/playout/tests/liquidsoap/fixtures/__init__.py +++ b/playout/tests/liquidsoap/fixtures/__init__.py @@ -92,6 +92,17 @@ TEST_STREAM_CONFIGS: List[Config] = [ "system": [{"enabled": True, "kind": "pulseaudio"}], } ), + make_config_with_stream( + outputs={ + "system": [ + { + "enabled": True, + "kind": "pulseaudio", + "device": "alsa_output.pci-0000_00_sink", + } + ], + } + ), make_config_with_stream( outputs={ "system": [{"enabled": False, "kind": "alsa"}], diff --git a/shared/libretime_shared/config/_models.py b/shared/libretime_shared/config/_models.py index 877b3e1ad..47cc56c19 100644 --- a/shared/libretime_shared/config/_models.py +++ b/shared/libretime_shared/config/_models.py @@ -216,7 +216,7 @@ class ShoutcastOutput(BaseModel): mobile: bool = False -class SystemOutputKind(str, Enum): +class SystemOutput(str, Enum): ALSA = "alsa" AO = "ao" OSS = "oss" @@ -224,16 +224,49 @@ class SystemOutputKind(str, Enum): PULSEAUDIO = "pulseaudio" -class SystemOutput(BaseModel): +class BaseSystemOutput(BaseModel): enabled: bool = False - kind: SystemOutputKind = SystemOutputKind.PULSEAUDIO + + +class ALSASystemOutput(BaseSystemOutput): + kind: Literal[SystemOutput.ALSA] = SystemOutput.ALSA + device: Optional[str] = None + + +class AOSystemOutput(BaseSystemOutput): + kind: Literal[SystemOutput.AO] = SystemOutput.AO + + +class OSSSystemOutput(BaseSystemOutput): + kind: Literal[SystemOutput.OSS] = SystemOutput.OSS + + +class PortAudioSystemOutput(BaseSystemOutput): + kind: Literal[SystemOutput.PORTAUDIO] = SystemOutput.PORTAUDIO + + +class PulseAudioSystemOutput(BaseSystemOutput): + kind: Literal[SystemOutput.PULSEAUDIO] = SystemOutput.PULSEAUDIO + device: Optional[str] = None + + +AnySystemOutput = Annotated[ + Union[ + ALSASystemOutput, + AOSystemOutput, + OSSSystemOutput, + PortAudioSystemOutput, + PulseAudioSystemOutput, + ], + Field(discriminator="kind", default=SystemOutput.PULSEAUDIO), +] # pylint: disable=too-few-public-methods class Outputs(BaseModel): icecast: List[IcecastOutput] = Field([], max_length=3) shoutcast: List[ShoutcastOutput] = Field([], max_length=1) - system: List[SystemOutput] = Field([], max_length=1) + system: List[AnySystemOutput] = Field([], max_length=1) @property def merged(self) -> List[Union[IcecastOutput, ShoutcastOutput]]: