HA Announcement: play sound, run TTS - cuts early

Hello everybody.

I read a lot of topics and troubleshoots about voice and sound announcements, and still cannot figure out why HA cuts the last 1-2 seconds every time for TTS or just a short sound effect.

I’ve added a pretty dumb workaround for TTS: a short word or letter after the main message, so it cuts irrelevant info. But I cannot avoid the sound effect to be cut too early.

To double-check everything, I now just run Action directly, without playing anything else in HA or Music HA, but still it cuts early:

action: media_player.play_media
data:
  media:
    media_content_id: media-source://media_source/local/HA_Media/Email-02.wav
    media_content_type: audio/x-wav
  announce: true
target:
  entity_id: media_player.raspberrypi_5

Here is my setup for Snapcast. It’s a pretty basic setup.
I’m using three USB sound cards and three instances with systemd on my RPi5.

Snapcast Multiroom setup

I’ve tried to wait for a few seconds in automation\scripts and alsy I’ve tried this: {{ is_state('media_player.raspberrypi_5', 'idle') }}

I can see the states and statuses of all players, and everything looks fine; there are no concurrent plays. I also restarted everything and recreated HA Music and Snapcast integrations in HA - no luck yet.

This premature cut is always there. I remember it started a few years ago when I used HA Announcements first, and it’s still here. I’ve changed hardware, setups and everything during this time, but no luck - it’s a long-running issue and no ideas left in my mind.

Hello Ole D,

You must be sending a command or something is sent to the speaker before it finished. Add a short delay after you send the message.

Nothing watches for the end of the message, so you need to stop HA from sending anything else too soon. Longer messages will need more delay.

To completely isolate this case, I now only troubleshoot at the player that is not busy with any tasks, with an empty queue and so on.

Moreover, I’m calling the action directly, in dev tools:

action: media_player.play_media
data:
  media:
    media_content_id: media-source://media_source/local/HA_Media/Email-02.wav
    media_content_type: audio/x-wav
  announce: true
target:
  device_id: b3edda5e47dc9d0be44755d8ad67439b

Probable solution:

I almost probably figured it out, by playing with buffer - smaller buffer - better playing short files, but too short is about <150ms sounds pretty bad.

Snapserver config: /etc/snapserver.conf

# Buffer [ms]
# The end-to-end latency, from capturing a sample on the server until the sample is played-out on the client
buffer = 250

Confirmed with TTS and buffer 350. It now reads everything and do not cut anything at the end of the text.

Saved my setup here: simple_setup.md

Cutting down on the buffer is not the best idea.
The buffer is there to handle delays in the sound and it might work when you test it, but then later you might be moving files over the network or your TV buffers a just started TV show and then the buffer will be too small.

Also remember that your devices might do other stuff or some of them might be slower, so a larger buffer might be necessary on those than the ones you test on.

Changing the buffer completely fixed the cutting issue.

There is no problem with changing the buffer length if you know what you want to achieve; it will chunk the media more often if needed.

However, the overall snapcast and TTS issue persists, and it’s now not related to a buffer, but to the stream itself, and only for TTS.

Music and all notification sounds are fine.

Error: Unable to create stream - No free port found?

UPD: Crappy workaround I’m trying now - somehow catch the error in the script and restart the failed Snapcast client (or server) and retry TTS after. Yet I don’t know how to catch this error in Script execution.

# shell_command: https://www.home-assistant.io/integrations/shell_command
shell_command:
  # Reboot
  reboot: "ssh -o UserKnownHostsFile=/config/.ssh/known_hosts -i /config/.ssh/id_rsa [email protected] 'sudo reboot now'"
  # Restart snapcast
  service_restart_snapcast_server: "ssh -o UserKnownHostsFile=/config/.ssh/known_hosts -i /config/.ssh/id_rsa [email protected] 'sudo systemctl restart snapserver'"
  service_restart_snapcast_clients: "ssh -o UserKnownHostsFile=/config/.ssh/known_hosts -i /config/.ssh/id_rsa [email protected] 'sudo systemctl restart snapclient snapclient_2 snapclient_3'"
  service_restart_snapcast_client_1: "ssh -o UserKnownHostsFile=/config/.ssh/known_hosts -i /config/.ssh/id_rsa [email protected] 'sudo systemctl restart snapclient'"
  service_restart_snapcast_client_2: "ssh -o UserKnownHostsFile=/config/.ssh/known_hosts -i /config/.ssh/id_rsa [email protected] 'sudo systemctl restart snapclient_2'"
  service_restart_snapcast_client_3: "ssh -o UserKnownHostsFile=/config/.ssh/known_hosts -i /config/.ssh/id_rsa [email protected] 'sudo systemctl restart snapclient_3'"

UPD: No luck! I’ve solved the cutting issue, but cannot catch, reproduce and solve the issue with players in automation. It just randomly happens.

Error: Unable to create stream - No free port found?

logs

2025-10-13 22:16:26.205 WARNING (MainThread) [music_assistant.snapcast] {'code': None, 'message': 'Server not connected'}
2025-10-13 22:16:26.205 ERROR (MainThread) [music_assistant.webserver] Error handling message: players/cmd/play_announcement: Unable to create stream - No free port found?
2025-10-13 22:16:31.215 INFO (MainThread) [music_assistant.snapcast] Stopping, built-in Snapserver

To recreate: Failed to perform the action tts.speak. Unable to create stream - No free port found?

Run multiple times:

action: tts.speak
data:
  cache: true
  media_player_entity_id: media_player.raspberrypi_5
  message: MESSAGE
  language: LANG
target:
  entity_id: tts.piper
  • media_player.raspberrypi_5 is muscis assistant audio device based on RPI5 snapcast server with USB sound card.

UPD: Unwilling to deep dive into the code to find the root cause of this constantly recurring issue, I ended up with automation to restart HA Music and all related modules hourly at idle or unavailability.

Summary
alias: Snapcast clients restart
description: Service routine - restart Snapcast when idle or unavailable
triggers:
  - trigger: state
    entity_id:
      - media_player.bedroom
    to: idle
    for:
      hours: 1
      minutes: 0
      seconds: 0
    id: bedroom_idle
  - trigger: state
    entity_id:
      - media_player.kitchen
    to: idle
    for:
      hours: 1
      minutes: 0
      seconds: 0
    id: kitchen_idle
  - trigger: state
    entity_id:
      - media_player.bathroom
    to: idle
    for:
      hours: 1
      minutes: 0
      seconds: 0
    id: bathroom_idle
  - trigger: state
    entity_id:
      - media_player.bedroom
    to: unknown
    id: bedroom_unknown
    for:
      hours: 0
      minutes: 5
      seconds: 0
  - trigger: state
    entity_id:
      - media_player.kitchen
    to: unknown
    id: kitchen_unknown
    for:
      hours: 0
      minutes: 5
      seconds: 0
  - trigger: state
    entity_id:
      - media_player.bathroom
    to: unknown
    id: bathroom_unknown
    for:
      hours: 0
      minutes: 5
      seconds: 0
  - trigger: state
    entity_id:
      - media_player.bedroom
    to: unavailable
    id: bedroom_unavailable
    for:
      hours: 0
      minutes: 5
      seconds: 0
  - trigger: state
    entity_id:
      - media_player.kitchen
    to: unavailable
    id: kitchen_unavailable
    for:
      hours: 0
      minutes: 5
      seconds: 0
  - trigger: state
    entity_id:
      - media_player.bathroom
    to: unavailable
    id: bathroom_unavailable
    for:
      hours: 0
      minutes: 5
      seconds: 0
  - trigger: template
    value_template: >-
      {{ 

      states('media_player.bathroom')  == 'idle' or
      states('media_player.bathroom') == 'off' 

      and

      states('media_player.bedroom')  == 'idle' or
      states('media_player.bedroom') == 'off' 

      and

      states('media_player.kitchen')  == 'idle' or
      states('media_player.kitchen') == 'off' 

      }}
    for:
      hours: 1
      minutes: 0
      seconds: 0
    id: all_music_assistant_idle
conditions: []
actions:
  - choose:
      - conditions:
          - condition: trigger
            id:
              - bedroom_idle
              - bedroom_unknown
              - bedroom_unavailable
        sequence:
          - action: shell_command.service_restart_snapcast_client_2
            metadata: {}
            data: {}
          - action: homeassistant.reload_config_entry
            target:
              device_id:
                - dfed91884a924a58a427a508cc06a8ed
            data: {}
      - conditions:
          - condition: trigger
            id:
              - kitchen_idle
              - kitchen_unknown
              - kitchen_unavailable
        sequence:
          - action: shell_command.service_restart_snapcast_client_3
            metadata: {}
            data: {}
          - action: homeassistant.reload_config_entry
            target:
              device_id:
                - b3edda5e47dc9d0be44755d8ad67439b
            data: {}
      - conditions:
          - condition: trigger
            id:
              - bathroom_idle
              - bathroom_unknown
              - bathroom_unavailable
        sequence:
          - action: shell_command.service_restart_snapcast_clients
            metadata: {}
            data: {}
          - action: homeassistant.reload_config_entry
            target:
              device_id:
                - 35fe0ea1be42e3be79649813d25517c2
            data: {}
      - conditions:
          - condition: trigger
            id:
              - all_music_assistant_idle
        sequence:
          - sequence:
              - action: shell_command.service_restart_snapcast_server
                metadata: {}
                data: {}
              - action: shell_command.service_restart_snapcast_clients
                metadata: {}
                data: {}
              - action: shell_command.service_restart_snapcast_client_2
                metadata: {}
                data: {}
              - action: shell_command.service_restart_snapcast_client_3
                metadata: {}
                data: {}
          - action: hassio.addon_restart
            metadata: {}
            data:
              addon: d5369777_music_assistant
          - action: hassio.addon_restart
            metadata: {}
            data:
              addon: core_piper
          - action: homeassistant.reload_config_entry
            target:
              device_id:
                - b3edda5e47dc9d0be44755d8ad67439b
                - dfed91884a924a58a427a508cc06a8ed
                - 35fe0ea1be42e3be79649813d25517c2
            data: {}
          - action: persistent_notification.create
            metadata: {}
            data:
              title: Snapcast {{ trigger.id }}
              message: Restarting MA, Piper addons
            enabled: false
mode: queued
max: 10

And a script which will use a while loop (10 cycles limit) to force restart all modules when notification and TTS play cannot play or scripts for TTS or Notification are stuck or ended with an error.

I have a similar setup and the same issue. I think the root cause is this:

  • when MA starts playing an announcement, it creates a new Snapcast stream and switches the group to it. This happens immediately, clearing the buffer, so there is now a delay (unknown to MA) before the buffer fills again and the announcement starts actually playing.
  • MA now waits for the duration of the announcement.
  • MA switches the group back to the original stream (either for music, or the default silent stream). This also happens immediately and clears the buffer, which still contains the last part of the announcement.

Overall this means that there is $BUFFER_LENGTH silent delay before the announcement and $BUFFER_LENGTH is cut off the end of the announcement.

I think we would need to fix this issue on Snapcast’s part - introduce the ability to switch streams without clearing the buffer.

1 Like

So I studies Snapcast source code and I think I found the root cause.

  • The snapserver encodes and sends audio to clients as fast as possible, there are no large internal buffers there.
  • The playback delay is controlled entirely by snapclient. It receives WireChunk and CodecHeader messages. WireChunk messages contain the timestamped encoded audio data and are added to a buffer for later playback after a delay. CodecHeader messages are processed immediately and clear the internal buffer and reset the player state.
  • The issue is that the codec is set per snapcast stream and not globally. Each stream has its own encoder instance. When MA switches stream, a new CodecHeader message is sent and processed immediately and that clears the buffer in the client.

On MA’s end, we could inject the announcement into the currently playing stream’s audio data and not create a new stream. But this would mean that when multiple players are grouped together, the announcement could be send only to the entire group, not to individual players thereof.

On Snapcast’s end, I came up with two solutions:

  • Either modify the snapserver, so it shares the encoder between multiple streams if they have same parameters (codec, sample rate & format etc.). When switching streams, switch the encoder input instead of the encoder output.
    Current situation (switching encoder output):


    Desired situation (shared encoder, switching its input):

  • Modify snapclient so it doesn’t reset the player when receiving the CodecHeader message, but just starts decoding the new data and queuing it to the player like normal. This would be only possible if the decoded data is compatible (same sample rate & format), otherwise it would behave like currently.

1 Like

I do not think you can do that.
Snapcast delays single players stream to make them play in sync, so I think you will get in fight what that functionality.

The delay is added by snapclient, not snapserver. Snapserver sends audio data as fast as it can.

I’ve made an issue at the HA repo for MA:

I’ll add a link to this thread and your comments to make the picture full.