我正在尝试启动一个 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 有某种“规范/用户友好性的技巧”,称为uaccess
(A,乙),这将为已登录的用户在声音设备的开发节点上动态添加 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
才可用。