Skip to content

ALSA stream stays RUNNING indefinitely after audio stops #233

Closed

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

Activity

  1. balloob commented on Apr 22, 2026

    @balloob
    Contributor

    It is expected that we keep playing silence until we receive a stream end, because a stream is active. If the queue is finished, I would expect the server to send a stream end.

  2. chatziko commented on Apr 30, 2026

    @chatziko
    Contributor

    I have the same issue. When I press the "pause" button on MA (or the "Stop playback" menu), sendspin daemon shows:

    DEBUG:aiosendspin.client.client:Stream ended for roles: all
    

    which suggests that the stream ended. But ALSA stream remains active.

    Clearing the MA queue after this has no effect.

    Similarly, if I start a queue with a single song and let it finish, the exact same behavior happens.

  3. added a commit that references this issue on May 3, 2026
  4. balloob commented on May 3, 2026

    @balloob
    Contributor

    Fixed in #241

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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