使用 ALSA 在启动时运行 systemd 服务

使用 ALSA 在启动时运行 systemd 服务

我正在尝试启动一个 python systemd 服务,该服务在 Raspberry Pi 启动时使用 ALSA 声音。

这是 /etc/systemd/system 中的 Talkie.service 文件:

Description=bondz-client
# Requires=sys-devices-platform-soc-soc:sound-sound-card1-controlC1.device

[Service]
User=user
Group=user
Type=simple
ExecStart=/usr/bin/python3 /home/romaing/Documents/Talkie_basic_sound_test.py
WorkingDirectory=/home/romaing/Documents/

StandardOutput=append:/var/log/bondz.log
StandardError=append:/var/log/bondz.log

[Install]
WantedBy=sound.target

如果我手动启动服务,systemctl它工作正常,但启动时我在日志中收到此错误(并且 systemd 停止)以下是日志文件内容:

2024-01-14 16:30:23,363 |Player| INFO:Device count =  0... 
2024-01-14 16:30:23,364 |Player| INFO:Playing file tests/myrecording.wav... 
2024-01-14 16:30:23,403 |Player| ERROR:Error playing tests/myrecording.wav 
Traceback (most recent call last):
  File "/home/romaing/Documents/Talkie_basic_sound_test.py", line 71, in playWaveFile
    playStream = audio.open(
  File "/usr/local/lib/python3.9/dist-packages/pyaudio/__init__.py", line 639, in open
    stream = PyAudio.Stream(self, *args, **kwargs)
  File "/usr/local/lib/python3.9/dist-packages/pyaudio/__init__.py", line 441, in __init__
    self._stream = pa.open(**arguments)
OSError: [Errno -9996] Invalid output device (no default output device)

这是一个循环声音的简化 python 代码:

from Player import Player
import io
import wave
import logging
import traceback
import pyaudio

player = Player()
logger = logging.getLogger("Player")
audio = pyaudio.PyAudio()
logging.basicConfig(
    format="%(asctime)s |%(name)s| %(levelname)s:%(message)s", level=logging.INFO
)



def play():
    playWaveFile("tests/myrecording.wav", play)


def playWaveFile(filepath, callback):
    # info = audio.get_default_output_device_info()
    logger.info(f"Device count =  {audio.get_device_count()}... ")
    logger.info(f"Playing file {filepath}... ")
    try:
        wave_file = wave.open(filepath, "rb")

        playStream = audio.open(
            format=audio.get_format_from_width(wave_file.getsampwidth()),
            channels=wave_file.getnchannels(),
            rate=wave_file.getframerate(),
            output=True,
        )
        data = wave_file.readframes(1024)
        while data:
            playStream.write(data)
            data = wave_file.readframes(1024)
        # Cleanup
        playStream.stop_stream()
        playStream.close()
        logger.info(f"Playing {filepath} done.")
        callback()
    except Exception as e:
        logger.error(f"Error playing {filepath} ")
        traceback.print_exc()
        # self._statusManager.set_app_status(Status.ERROR)


play()

我想服务启动时声卡没有加载,但我认为 sound.target 可以解决它,但它没有......作为这个主题的初学者,任何帮助将不胜感激。

我正在 Raspbian 11(牛眼)上运行。

这是输出aplay -l

**** List of PLAYBACK Hardware Devices ****
card 0: vc4hdmi [vc4-hdmi], device 0: MAI PCM i2s-hifi-0 [MAI PCM i2s-hifi-0]
  Subdevices: 1/1
  Subdevice #0: subdevice #0
card 1: seeed2micvoicec [seeed-2mic-voicecard], device 0: bcm2835-i2s-wm8960-hifi wm8960-hifi-0 [bcm2835-i2s-wm8960-hifi wm8960-hifi-0]
  Subdevices: 1/1
  Subdevice #0: subdevice #0

卡 1 是我需要使用的卡。

答案1

首先,预计您不能“保留”/“延迟”服务启动,After=sound.target因为sound.target“启动单元树”中不存在该服务。

systemd(可以)仅将一个单元与另一个单元排序(如果它们在树中),即确定性地定义为在启动过程中的某个时刻被拉入(由也如此定义的其他一些单元)。

sound.target事实并非如此。何时以及是否会完全启动取决于声卡何时以及是否出现(即,被检测/枚举)。显而易见的是,systemd 在启动启动过程时并不(至少不一定)知道是否存在声卡设备单元。

然而,“无效”排序并不能阻止一个单元被拉入(被其他单元拉入,就像在本例中一样multi-user.target)。


确保声卡出现后单元/服务启动的可能唯一正确方法是(仅)创建它WantedBy=sound.target。确保在[Install]文件部分下进行更改后执行禁用-启用循环,否则它将不会生效。

因为当 systemd“开始”启动目标时,声卡已经可用,因此您After=sound.target根本不需要。 (希望在通常情况下拥有它是无害的。然而,恕我直言,排序机制有些脆弱。我强烈建议不要订购,除非您确认确实有必要。)


注意,sincesound.target会被拉入一次A(即任何)声卡出现。因此,如果您需要确保在您自己的设备/服务启动时特定的声卡可用,sound.target则不适合您,除非您知道您想要的声卡实际上总是首先出现。

在这种情况下,您可能需要求助于类似于拉入的 udev 规则sound.target,您可以在其中与 eg 匹配ENV{ID_PATH}


请注意,在许多发行版中,不在该audio组中的非 root 用户无权访问声音设备。您可以使用 检查实际所有权/权限ls -l /dev/snd/

然而,systemd 有某种“规范/用户友好性的技巧”,称为uaccessA,),这将为已登录的用户在声音设备的开发节点上动态添加 ACL。(它可能比这更复杂,但我不熟悉所有会话和席位的内容,并且细节与OP。)

因为你有类似的东西:

User=my-username
Group=

my-username在您的服务文件中,如果不在组中,则进程可能无法访问声音设备audio,除非您使用该用户登录。 (或者即使是这样;我还没有真正检查过Group=(empty)是什么。)

因此,请确保my-username获得许可(rw?)/dev/snd/*。将其添加到audio组中,和/或将Group=或设置SupplementaryGroups=audio。无论哪种方法有效并且适合您。

也不要假设audio组是您的系统/发行版中的一个东西。先检查一下。

答案2

我认为您对 的依赖sound.target是正确的(尽管我也会添加Requires=sound.target,因为它确实需要健全的支持)。我不完全确定问题出在哪里——也许某些 ALSA 组件需要一些时间才能解决——但有一些简单的方法可以解决它。

允许您的服务自动重新启动

不要手动重新启动服务,而是将其配置为在发生故障时自动重新启动:

Description=My Talkie App
After=network.target sound.target

[Service]
User=my-username
Group=
Type=simple
ExecStart=/usr/bin/python3 /home/romaing/Documents/Talkie.py
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

这将尝试在失败时每 5 秒重新启动一次。

使用 ExecStartPre 操作等待声音设备

或者,您可以阻止服务,直到所需的音频设备可用为止。就像是:

Description=My Talkie App
After=network.target sound.target

[Service]
User=my-username
Group=
Type=simple
ExecStartPre=/usr/bin/timeout 300 /bin/sh -c 'while ! amixer -D sysdefault:CARD=USB > /dev/null 2>&1; do sleep 1; done'
ExecStart=/usr/bin/python3 /home/romaing/Documents/Talkie.py

[Install]
WantedBy=multi-user.target

这将等待最多 5 分钟,由 标识的卡sysdefault:CARD=USB才可用。

相关内容