Skip to content

ALSA stream stays RUNNING indefinitely after audio stops #233

@tobsch

Description

@tobsch

Problem

When sendspin stops receiving audio from the server, the ALSA PCM stream remains in RUNNING state indefinitely, outputting silence. This makes it impossible for external tools to determine whether sendspin is actually playing audio by examining /proc/asound/<card>/pcm*/sub*/status.

Root Cause

Two issues combine:

  1. PortAudio MMAP + silence padding: PortAudio opens ALSA in MMAP_INTERLEAVED mode with stop_threshold = boundary and silence_size = boundary. The DMA engine never auto-stops.

  2. Callback fills silence when queue is empty: In audio.py, _read_input_frames_bulk() pads with silence when the queue drains. Since the callback always returns a fully filled buffer, PortAudio never detects an underflow, and the stream runs forever.

After audio stops, the ALSA stream shows:

state: RUNNING
appl_ptr: 0          (never advances — PortAudio MMAP quirk)
hw_ptr: 43088418     (advances at 48kHz forever)
avail_max: 43104802  (grows unboundedly)

Impact

  • Power management: Tools monitoring /proc/asound/ status to control amplifier power (relay on/off) cannot distinguish idle sendspin from active playback. Amplifiers either stay on forever or incorrectly turn off during playback because the monitoring has to ignore sendspin streams entirely.
  • Resource waste: DMA engine runs continuously outputting silence to USB hardware, consuming USB bandwidth and CPU for callback invocations.

Observed Behavior

10:27:45 Stream started with codec pcm
10:28:19 Audio underflow → clear → Stream STARTED (restarted)
         ... no more audio from server ...
12:43:00 Stream still RUNNING (2+ hours of silence, hw_ptr=43M, appl_ptr=0)

No stream_end event was received from the server after 10:28:19. The callback continued to fill silence indefinitely.

Suggested Fix

When the audio callback detects that no real audio data has been available for a configurable timeout (e.g., 5–10 seconds of consecutive silence fills), call stream.stop() to release the ALSA device. The stream would restart on the next submit() call when new audio arrives — this path already exists in the code (audio.py, around line 1229).

Alternatively, ensure the server always sends a stream_end event when playback stops, so _on_stream_end()player.clear()stream.stop() is triggered reliably.

Environment

  • sendspin 7.0.0
  • Raspberry Pi 5 (aarch64)
  • USB audio: Wondom GAB8 8-channel amplifiers (period_size=2048, buffer_size=16384)
  • PortAudio V19.6.0-devel, MMAP_INTERLEAVED access
  • lox-audioserver (beta-latest) as sendspin server

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions