Hardware Configuration

I have two Audio Science ASI6044 cards installed to an Ubuntu 20.04 server. This gives me 16 mono channels out, or 8 stereo channels. How can I configure the LibreTime to use a specific output? (I have installed the official driver from AudioScience, which recommends ALSA. I can only select output type in the settings page.) Note that I’m not afraid of tweaking database entries, config settings, etc.

So maybe a good starting point might be; what is the mechanism that outputs audio to the hardware device? Is that liquidsoap?

Currently the system outputs configuration are really basic, and does not provide any options.

I expect this to change once we merged feat: use file based stream configuration by jooola · Pull Request #1986 · libretime/libretime · GitHub which should allow to add options more easily.
This is for no power users though.

You could dig into the liquidsoap scripts and add you own output by hand.

I am once again exploring this idea, except I am using docker containers rather than installing it to the system. Ideally, I’d like to have six containers running at the same time. Is it yet possible to define an alsa output? Using mplayer, I’d define alsa:device=hw=2.0.0 or alsa:device=hw=3.0.1, etc. Is such a mechanism possible here?

I’ve read through that tutorial but I’m not seeing where I would specify which output for which socket, assuming I would create a different socket per output/container.

I was reading through the source code and found that the system outputs are generated by a Jinja2 template

outputs.liq.j2:

{% for output in config.stream.outputs.system -%}
{% if output.enabled -%}
# {{ output.kind.value }}:{{ loop.index }}
%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)
%endif

{% endif -%}
{% endfor -%}

According to the liquidsoap docs:

output.alsa

Output the source’s stream to an ALSA output device.

Type:

(?id : string, ?bufferize : bool, ?clock_safe : bool,
?device : string, ?fallible : bool,
?on_start : (() -> unit), ?on_stop : (() -> unit),
?start : bool, source(audio='#a+1, video='#b, midi='#c)) ->
active_source(audio='#a+1, video='#b, midi='#c)

So if I specified alsa in config.yml, outputs.liq.j2 would output output.alsa(id=alsa... because the output and the id are coupled. Which isn’t great. Is it possible to decouple those two variables so I can specify the correct hardware output? I haven’t figured out how the outputs.liq.j2 file interacts with the config.yml file yet.

Edit: The ID doesn’t matter so much. There would need to be an additional variable for device. The final config would look like output.alsa(id="alsa", device="hw:3,0,0") where device="hw:<card_number>,<device_number>,<subdevice_number>"

I tried tweaking the outputs.liq.j2 file and the shared/libretime_shared/config/_models.py files to accept a device field:

libretime/playout/libretime/libretime_playout/liquidsoap/templates/outputs.liq.j2

120 {% for output in config.stream.outputs.system -%}
121 {% if output.enabled -%}
122 # {{ output.kind.value }}:{{ loop.index }}
123 %ifndef output.{{ output.kind.value }}
124 log("output.{{ output.kind.value }} is not defined!")
125 %endif
126 %ifdef output.{{ output.kind.value }}
127 output.{{ output.kind.value }}(
128   id="{{ output.kind.value }}:{{ loop.index }}",
129   device="{{ output.device.value }}",
130   s
131 )
132 %endif

libretime/share/libretime_shared/config/_models.py

256 class SystemOutput(BaseModel):
257     enabled: bool = False
258     kind: SystemOutputKind = SystemOutputKind.ALSA
259     device: Optional[str] = None

This was to allow the device to be set in config.yml

    system:
      - # Whether the output is enabled.
        # > default is false
        enabled: true
        # System output kind.
        # > must be one of (alsa, ao, oss, portaudio, pulseaudio)
        # > default is alsa
        kind: alsa
        device: hw:2,0,3

Unfortunately, I think I’m missing something during the ingest or an entry to the postgres database. The resulting radio.liq file looks like:
/var/lib/docker/volumes/stat1_libretime_playout/_data/radio.liq

# alsa:1
%ifndef output.alsa
log("output.alsa is not defined!")
%endif
%ifdef output.alsa
output.alsa(
  id="alsa:1",
  device="",
  s
)
%endif

Any advice would be appreciated…

Just to update my “notes” here; I was able to make the device parameter populate the radio.liq file by removing .value:

libretime/playout/libretime/libretime_playout/liquidsoap/templates/outputs.liq.j2

120 {% for output in config.stream.outputs.system -%}
121 {% if output.enabled -%}
122 # {{ output.kind.value }}:{{ loop.index }}
123 %ifndef output.{{ output.kind.value }}
124 log("output.{{ output.kind.value }} is not defined!")
125 %endif
126 %ifdef output.{{ output.kind.value }}
127 output.{{ output.kind.value }}(
128   id="{{ output.kind.value }}:{{ loop.index }}",
129   device="{{ output.device }}",
130   s
131 )
132 %endif

At this point I am having trouble finding the correct parameter to populate the config.yml files. Some settings that I have tried:

setting result
hw:2,0,0 Thread “alsa:1” aborts with exception Alsa error: No such file or directory!
hw:2.0.0 Thread “alsa:1” aborts with exception Alsa error: Device or resource busy!
hw:2.0 Thread “alsa:1” aborts with exception Alsa error: Device or resource busy!
hw:CARD=ASI6500,DEV=0 Thread “alsa:1” aborts with exception Alsa error: Device or resource busy!

Any other variants of these settings for the other card or other outputs result in the same output here. I have attempted to add the libretime user to the audio group by adding a line in the Dockerfile:

libretime/Dockerfile

 26 # Custom user
 27 ARG USER=libretime
 28 ARG UID=1000
 29 ARG GID=1000
 30 
 31 RUN set -eux \
 32     && adduser --disabled-password --uid=$UID --gecos '' --no-create-home ${USER} \
 33     && install --directory --owner=${USER} /etc/libretime /srv/libretime \
 34     && usermod -aG audio libretime

That did not change anything at all. I may not have done that correctly.

Being able to extand the different output option was on my todo list. But I didnt find time to extend this yet. You did it the rigt way.

Maybe you should look into using/extending the pulseaudio output instead ? You probably will have a better chance to route you audio to the right place.

Aah and for docker to stream audio, you have to mount the hardward into docker or mount the socket of pulseaudio.

Look at the docs to have an example on how to set this up using docker and pulseaudio.

Despite this, I am still receiving thefollowing error when I try to navigate to [host]:[port]:

could not parse configuration: Unrecognized option "device" under ".stream.outputs.system.0". Available options are "enabled", "kind".

This made me think the issue is with the old php code, so I dug into that and found:
libretime/legacy/application/models/StreamSettings.php:

141     public static function getStreamSetting()
142     {
143         $settings = [];
144         $numStreams = MAX_NUM_STREAMS;
145         for ($streamIdx = 1; $streamIdx <= $numStreams; ++$streamIdx) {
146             $settings = array_merge($settings, self::getStreamData('s' . $streamIdx));
147         }
148         $settings['master_live_stream_port'] = self::getMasterLiveStreamPort();
149         $settings['master_live_stream_mp'] = self::getMasterLiveStreamMountPoint();
150         $settings['dj_live_stream_port'] = self::getDjLiveStreamPort();
151         $settings['dj_live_stream_mp'] = self::getDjLiveStreamMountPoint();
152         $settings['off_air_meta'] = Application_Model_Preference::getOffAirMeta();
153         $settings['icecast_vorbis_metadata'] = self::getIcecastVorbisMetadata();
154         $settings['output_sound_device'] = self::getOutputSoundDevice();
155         $settings['output_sound_device_type'] = self::getOutputSoundDeviceType();
156         $settings['output_sound_device_device'] = self::getOutputSoundDeviceDevice();
157 
158         return $settings;
159     }

[ . . . ]

235     public static function getOutputSoundDevice()
236     {
237         return Config::get('stream.outputs.system.0.enabled') ?? 'false';
238     }
239 
240     public static function getOutputSoundDeviceType()
241     {
242         return Config::get('stream.outputs.system.0.kind') ?? '';
243     }
244 
245     public static function getOutputSoundDeviceDevice()
246     {
247         return Config::get('stream.outputs.system.0.device') ?? '';
248     }
249 }

I added line 156 and lines 245 to 248. This still hasn’t worked, so I’m wondering if there is another place where I need to call the function or add something somewhere else. (getOutputSoundDeviceDevice seems like a dumb function name lol).

The main reason I’ve been focusing my effort toward ALSA is because the Audio Science cards I’m using; I’m not sure if they will play nice with PulseAudio. Although, since Pulse is just a wrapper for ALSA, it ~should~ work.

The config validation happens in 2 places, in the shared models as well as in php config schema. You need to edit both so they match.

Though the php code is not required for the radio.liq file generztion. Only the python shared models

If you want me to help you out for this, I propose that you open a Pull Request on Github so I can review and guide you through the required steps to get this working. We should also be able to make this available to other once its working!

I prefer pulseaudio also because it doesn’t lock the sound card to a specific client, going through the pulseaudio server allows you more flexibility.

I’ll check the PR tomorrow morning, I already have a small idea why it is not working.

You might need to follow the pattern using by the AudioFormat for the Output and apply to the SystemOutputs models. I’ll push changes tomorrow morning as well if you didnt catch that.

I was actually able to get the legacy code to recognize the device setting. See my PR for the changes in the code. However, I am still seeing ALSA device busy error, which means I am not mounting the device properly with docker. I am curious to see how this might run with just installing it, though.

So, when the radio.liq file is generated, that’s it, right? I mean, if you want to make a change, you would have to regenerate that file and then restart playout/liquidsoap in order to take the new config. I noticed that the settings for system output are now gone in the web gui. If we got this to work, would we be able to add this functionality back into the web gui with the addition of device? Maybe add a print out in some clever way of aplay -lL to show which device can be assigned, or maybe even format them into a dropdown or something to make it easier for users to find the device they want to use and use it.

Might be a pipe dream there, but I think worth exploring once this works.

Have a look at this guide Container sound: ALSA or Pulseaudio · mviereck/x11docker Wiki · GitHub

I think I used this guide to write: How to setup a PulseAudio output inside containers | LibreTime

I really prefer pulseaudio as you don’t lock the sound card for this single process.

Editing the config from the UI is not supported, and will not be supported. A readonly view might be considered in a future change though, but I think we should focus on the python/yaml/liquidsoap for now, and leave the UI stuff for a future change.