Feature request: keep the ALSA output running briefly between streams (--keep-output-alive <s>)

オープン
#109 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
42/100
issue の種類
機能追加
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
cpp

調査の方向性

Start with alsa_sink.h and alsa_sink.cpp, then trace the existing fixed-volume option through CLI parsing, configuration, and sendspin-cli.conf.example. Use the attached patch and the modeled parse/config tests as references, and run the build-armv6 container with SENDSPIN_CLI_WERROR=ON. Done means the ALSA keep-alive behavior, timeout, CLI/config bounds, and no-op behavior for other backends are implemented and all 122 tests pass.

索引モデルが issue の本文から書いたものです。

説明

Problem

With an HDMI (or S/PDIF) receiver behind the player, the start of every stream is cut off.
At each stream end and start, AlsaAudioSink stops the PCM:

  • clear() calls snd_pcm_drop() + snd_pcm_prepare()
  • configure() does the same when the next stream has the same format

The PCM then sits in PREPARED (or in XRUN, if the ring runs dry before clear() arrives)
until the next stream writes. While it does, no audio packets go out over HDMI, so the receiver
drops the input and has to re-lock when audio comes back. That takes about 2-3 s on an Onkyo
TX-NR626. For Music Assistant announcements (TTS) this is a real problem: MA ends the music
stream, plays the announcement as its own stream and then resumes the music, so the receiver
re-locks twice, and most of a short announcement is lost. A pre-announce chime is too short
to cover the re-lock.

Setup: Raspberry Pi Zero W, sendspin-cli 0.4.0 (linux-armv6), Music Assistant 2.10.8.
Output is onkyo_hdmi, a copy of the hdmi:CARD=vc4hdmi,DEV=0 chain in asound.conf with the
hw rate pinned to 48 kHz, plus audio-format = flac:48000:24:2,pcm:48000:24:2, so the format
never changes between streams and only the same-format path below is involved.

Measured by polling /proc/asound/card0/pcm0p/sub0/status every 250 ms during
radio -> TTS -> radio:

RUNNING   (radio)
PREPARED  ~1 s     <- stream end, clear()
RUNNING   (TTS, ~6 s)
XRUN      ~0.3 s   <- ring ran dry before the stream-end message
PREPARED  ~2 s
RUNNING   (radio)

Workarounds that do not work

Both try to keep the device busy from outside the player. Each puts a layer between the player
and the device that reports snd_pcm_delay() too coarsely or too jittery for the 5 ms
HARD_SYNC_THRESHOLD_US. The result is a constant Lost sync / hard-sync loop, which you hear
as choppy audio:

  • snd-aloop + alsaloop feeding HDMI. The loopback pointer moves in jiffies, and the Pi
    kernel runs HZ=100. Result: Lost sync (~90 ms off) about every second, with any
    alsaloop latency or sync mode.
  • System-mode PulseAudio with module-suspend-on-idle unloaded (-o pulse). The same
    Lost sync storm, see #32, plus 100 % CPU on a Pi Zero.

So this has to happen inside the sink, where the delay reported to the sync task comes straight
from the device.

Proposal

An opt-in option --keep-output-alive <seconds> (config key keep-output-alive), 0-3600,
default 0 (off), ALSA backend only. Other backends get a no-op AudioSink::set_keep_alive().
The goal is only to bridge the switch from one stream to the next, not to keep the receiver
awake forever: a few seconds (e.g. keep-output-alive = 10) are enough.

When enabled:

  1. clear() and the same-format path in configure() no longer drop the PCM. If it is
    running, they rewind the queued audio with snd_pcm_rewind(), keeping one period in front
    of the hardware pointer as a DMA safety margin. Then they write a short run of silence right
    away. If the rewind fails, the queued tail simply plays out.
  2. poll() (main loop, ~10 ms) tops the ring up with silence to about 2 periods once write()
    has not delivered real audio for 60 ms. It uses try_lock on device_mutex_: a write()
    blocked in snd_pcm_wait() holds the mutex, so a stream that is running is never touched
    and the main loop never stalls. The same path re-prepares an XRUN (e.g. the ring ran dry
    at the end of a stream before clear()), so the signal does not drop there either.
  3. Time limit. Every real write() and every same-format configure() (re)starts a window
    of <seconds>; a clear() restarts it while it is still open. When it runs out, the PCM is
    dropped and prepared exactly as without the option, so the receiver sees the signal end and
    its auto standby still works. After that, clear() behaves as without the option, and the
    next stream starts the PCM again and opens a new window.
  4. Sync is not touched: the keep-alive silence is just queued frames, and the next real
    write() measures it through snd_pcm_delay() as usual.

Results

A patch against v0.4.0 (about 170 added lines in alsa_sink.{h,cpp}, plus option plumbing, help
text, sendspin-cli.conf.example and parse/config tests modeled on --fixed-volume) is
attached. It builds with SENDSPIN_CLI_WERROR=ON in the build-armv6 container, and all
122 tests pass. clear() and configure() are unchanged on current main, so it should apply
there with little or no change.

On the Pi Zero W above, with keep-output-alive = 10:

  • HDMI PCM RUNNING without a break through radio -> TTS -> radio (polled every 250 ms)
  • the receiver no longer re-locks, and TTS announcements play from the first word
  • no Lost sync during the switches; CPU the same as without the option (~75 % idle)
  • after a pause the PCM went from RUNNING to PREPARED exactly 10 s after the last audio
    (so the receiver can go to standby), and the next stream started normally

Open questions

  • Name and unit: is keep-output-alive <seconds> fine, or would you prefer milliseconds or an
    output-level setting?
  • Is a hard cap (3600 s) sensible, or should there be a "forever" value for setups that want
    it? I left that out on purpose so that receivers can always go to standby.
  • PortAudio, Pulse and PipeWire are left out on purpose. Pulse and PipeWire already have their
    own idle/suspend handling.

keep-output-alive-v0.4.0.patch.txt

主要言語
C++
スター
18
フォーク
7
平均マージ
1日 12時間
マージ済み PR(30日)
26

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

Sendspin/sendspin-cpp-cli のほかの issue

Sendspin/sendspin-cpp-cli の issue をすべて見る

似ている issue

C++ の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。