How to loop play sound files [Solved]

Not sure if this will make it any better (actually might make it worse from the perspective of sound delays), but I think you could possibly change script.play_sound_bite_loop to this:

  play_sound_bite_loop:
    alias: Play sound bite loop
    sequence:
      - wait_template: "{{ is_state('script.play_sound_bite', 'off') }}"
      - service: script.play_sound_bite

The typical issue with getting one script to invoke another, especially in this looping scenario, is the lag between a script starting and its state in the state machine changing to ‘on’ (and the same thing goes when it’s done.) So by adding a wait_template at the beginning of the second script you can be sure that when it invokes the first script again it will be completely finished and ready to run again. No worries in the other direction because the first script takes a significant amount of time to run before getting to the last step of invoking the second script. By that time the second script is sure to be completely done and ready to run again.

Just an idea.

HI Phil,
yes, the timings are of the utmost vulnerability… I will certainly try it, though, as I ended up, the 2 scripts are essentially identical, so if the first one runs fine, the second should run equally fine?

As they do at the moment.
If your solution could help attaching the 2 just a bit better (I see your fear they don’t…will try anyway) that would be a true advantage, though I must say, other than with continuous sounding files, the connection now is rather ok on the pulsating or definite ending ones.

Will give it a go and report back, thanks for lending me your brains again!

btw I take it you meant to suggest I do this:

  play_sound_bite_loop:
    alias: Play sound bite loop
    sequence:
      - service: script.sound_bite <--- you left this out?
      - wait_template: "{{ is_state('script.play_sound_bite', 'off') }}"
      - service: script.play_sound_bite

No, I did not. The whole point is the first script does the work, and the second script is just there to run the first script again – no duplication.

FWIW, in the first script, I would move the condition step to be the first thing in the script.

O am sorry. I had that earlier, and it didn’t work out, because of the wait_template to wait for the player to be != playing. Also, by using 2 identical scripts, one is positively sure when the script hs finished, which can be a bit if a fluke now and then, using just the 1 script when timing issues arise in HA.
Maybe this script == ‘off’ check will fare better. thx.

think I don’t follow… sorry.

the first script would be

  play_sound_bite:
    alias: Play sound bite
    sequence:
      - service: script.sound_bite
      - delay:
          seconds: >
           {{state_attr(states('sensor.sound_bite_player'),'media_duration')|int}}
      - condition: template
        value_template: >
          {{is_state('input_boolean.loop_sound_bite','on')}}
      - service: script.play_sound_bite_loop

if id change that to

  play_sound_bite:
    alias: Play sound bite
    sequence:
      - condition: template
        value_template: >
          {{is_state('input_boolean.loop_sound_bite','on')}}
      - service: script.sound_bite
      - delay:
          seconds: >
           {{state_attr(states('sensor.sound_bite_player'),'media_duration')|int}}
      - service: script.play_sound_bite_loop

it wouldn’t ever play anything, if the boolean were ‘off’? (i now use the same setup to play files just once, so only flip the boolean to decide that.

1 Like

@pnbruckner

can report that they are close, but most certainly not identical. My final setup with the 2 identical scripts is definitely better, or more secure.
Had already experienced the timing issues when using 1 executable script and and looping, as is yours. That is happening with your script also. It somehow is more sensitive for HA internals than using 2 scripts both executing the play script. They don’t have to wait for one another.

The delay seems more robust than the wait_template.

tested in a complete comparison, only needing the boolean to flip for either loop script:

with the below code:

  sound_bite:
    alias: Sound bite
    sequence:
      - service: media_player.play_media
        data_template:
          entity_id: >
            {{states('sensor.sound_bite_player')}}
          media_content_id: >
            {{states('sensor.sound_bite')}}
          media_content_type: 'music'

  play_sound_bite:
    alias: Play sound bite
    sequence:
      - service: script.sound_bite
      - delay:
          seconds: >
           {{state_attr(states('sensor.sound_bite_player'),'media_duration')|int}}
      - condition: template
        value_template: >
          {{is_state('input_boolean.loop_sound_bite','on')}}
      - service_template: >
          script.play_sound_bite_{{ 'loop' if is_state('input_boolean.play_loop_or_alt','off') else 'loop_alt' }}

  play_sound_bite_loop:
    alias: Play sound bite loop
    sequence:
      - service: script.sound_bite
      - delay:
          seconds: >
           {{state_attr(states('sensor.sound_bite_player'),'media_duration')|int}}
      - condition: template
        value_template: >
          {{is_state('input_boolean.loop_sound_bite','on')}}
      - service: script.play_sound_bite

  play_sound_bite_loop_alt:
    alias: Play sound bite loop alt
    sequence:
      - wait_template: >
          {{ is_state('script.play_sound_bite', 'off') }}
      - service: script.play_sound_bite

too bad we can’t post videos, because its fun to see the scripts flip and take control

btw your suggestion Is better when changing sound bites in between, and the attribute ‘media_duration’ hasn’t been adjusted accordingly yet. That is 1 time only, and of course, only relevant in this testing scenario. But noteworthy. --> Cookbook

Fair enough. It wasn’t obvious to me that that’s how you were using the input_boolean.

I offer you another solution, based on a timer.

Just like your solution, it uses one input_boolean to start/stop looping.

input_boolean:
  loop_sound_bite:
    name: loop sound bite

Create one timer. Its duration will be assigned by an automation.

timer:
  looper:

Create three automations:

  1. Looping controller
  2. Looper timer started
  3. Looper timer finished

The first automation is triggered by input_boolean.loop_sound_bite and simply starts/stops the timer (i.e. starts/stops looping).

The key feature here is that the timer’s duration is determined by the media_duration attribute of your sensor.sound_bite_player. In other words, the timer runs for as long as the media requires to play.

- alias: 'Looping controller'
  trigger:
    platform: state
    entity_id: input_boolean.loop_sound_bite
  action:
    service_template: "{{'timer.start' if trigger.to_state.state == 'on' else 'timer.finish'}}"
    data_template:
      entity_id: timer.looper
      duration: >-
        {% set x = state_attr(states('sensor.sound_bite_player'),'media_duration')|int %}
        {{ x if trigger.to_state.state == 'on' else 0 }}

The second automation is triggered when the timer starts. It’s job is to simply start playing the media.

- alias: 'Looper timer started'
  trigger:
  - platform: event
    event_type: timer.started
    event_data:
      entity_id: timer.looper
  action:
    - service: media_player.play_media
      data_template:
        entity_id: "{{states('sensor.sound_bite_player')}}"
        media_content_id: "{{states('sensor.sound_bite')}}"
        media_content_type: 'music'

The third and final automation is triggered when the timer finishes. It’s job is to restart the timer but only if input_boolean.loop_sound_bite is on.

- alias: 'Looper timer finished'
  trigger:
  - platform: event
    event_type: timer.finished
    event_data:
      entity_id: timer.looper
  condition:
    condition: state
    entity_id: input_boolean.loop_sound_bite
    state: 'on'
  action:
  - service: timer.start
    entity_id: timer.looper

I couldn’t test this solution using a media_player (I don’t have one available). However, I simply replaced the media_player with a light and made it toggle on/off every 5 seconds. It works well.

In practice you may need to add a second or two to the timer’s duration to ensure it provides enough time for the media_player to finish playing the media.

Thanks, I will test that.

But, tbh, Ive already tried several configurations, and I can tell you a light behaves completely different from a media_player. ‘Simply’ replacing it probably won’t work.

As with the suggestion made by @pnbruckner above, that would be perfect used with a light entity. However the media_player entity causes, of suffers, which ever you prefer, timing issues, and that makes these strategies based on timing rather insecure.

Since you use the media_duration for the timer, (not ‘simply replaced’ at all!) this might work just fine, and I really really appreciate that!

Will report back, thanks!

if id make it like that, what would be the advantage of placing it first in line? Other than quitting the script immediately?

The technique should work well with a media_player because I used it when I developed a Text-to-Speech driver for Premise (7 years ago). One of the features from its description:

AudioQueue
All phrases are buffered in a queue (“AudioQueue”) and delivered to the Audio Card in sequence. You can create several spoken phrases, in rapid succession, and they will wait their turn to be played by the Audio Card.

If you submit 5 different text-phrases to the driver, one after another, it converts each one into a WAV file and enqueues each one (into a FIFO buffer). Now the queue contains 5 sound files to process where each file plays for a different duration. A timer is used to ‘pump’ each sound file through the queue.

  • The timer’s duration is set to the sound file’s duration.
  • When the timer starts, it plays the sound file.
  • When the timer stops, if there’s something left to play in the queue, the timer is restarted with its duration set to the next sound file’s duration.
  • This process continues until the queue is empty.

I used the term ‘Job’ to refer to each item in the queue to be processed. Here’s a sample of the driver’s PlayAudio method. It’s in VBScript so its fairly easy to read.

PlayAudio submits the sound file to this.AudioSpeechOutput and calls this.StartAudioTimer using the current sound file’s duration (this.AudioSpeechOutput.Duration.Seconds).

Actually, it pads the duration, by 2 seconds, to account for variability and to add a tiny amount of ‘dead air’ between each sound file it plays.

It’s been working well for over 7 years. :slight_smile:

I am considering to chang the template from:

{{state_attr(states('sensor.sound_bite_player'),'media_duration')|int}}

to

{{state_attr(states('sensor.sound_bite_player'),'media_duration')|round|int}}.

this would ensure that file that are over the .5 get rounded to the next int, and not cut off.

that tight also take away the need for an extra delay.

Yes, good idea. You’ll notice in my old code that I also adjusted the duration with round (and added 2 seconds).

' VBScript
this.StartAudioTimer(round(.Duration.Seconds + 2))

Not sure if this Premise driver technique is comparable to HA? Thing is I think it isn’t as much the architecture, but more the real life HA instance with its timing and processes, and not to mention processing power, that makes time related calculations and wait_templates rather insecure. Theory and practice …

But, it’s very interesting indeed, especially the link and description, which points out another thing I had to take care of this week (only just started using these google home players, TTS, and sound files in HA…)
I solved the interruption of radio playing by my TTS messages like below. If a tts message needs to be announced, it checks if radio is playing. If yes, it turns on the boolean, which is signal for the script to resume playing radio after the message has finished. Here i still use a delay of 20 seconds, but i will replace that soon now I know how. Please have a look if you find some spots to improve on?:

  intercom_message:
    alias: 'Intercom message'
    sequence:
      - condition: template
        value_template: >
          {{ is_state('input_boolean.announce_intercom', 'on') }}
      - service: script.radio_paused_for_message
      - service: media_player.volume_set
        data_template:
          entity_id: >
            {{states('sensor.intercom')}}
          volume_level: >
            {{  states('input_number.intercom_volume')|float }}
#            # {{ states('sensor.intercom_volume')|float  }}
      - service: tts.google_say
        data_template:
          language: >
            {{states('input_select.intercom_language')|lower}}
          entity_id: >
            {{states('sensor.intercom')}}
          message: >
            {{message}}
      - condition: template
        value_template: >
          {{is_state('input_boolean.radio_paused','on')}}
      - delay:
          seconds: 20
#      - wait_template: >
#          {{states(states('sensor.intercom')) != 'playing'}}
      - service: script.play_radio
      - service: input_boolean.turn_off
        entity_id: input_boolean.radio_paused

  radio_paused_for_message:
    alias: 'Radio paused for message'
    sequence:
      - condition: template
        value_template: >
          {{states('sensor.intercom') == states('sensor.media_player')}}
      - condition: template
        value_template: >
          {{states(states('sensor.media_player')) == 'playing'}}
      - service: input_boolean.turn_on
        entity_id: input_boolean.radio_paused
  • It’s not a matter of the software but the design pattern.
  • It’s an event-driven pattern, where the process moves along at the speed of the timer’s events.
  • It’s portable across languages and platforms.

strangest thing: ive creatd a dedicated Looper package, and all code is correct, checked it all in the dev-template.

Wont run though, no error, nothing. only this:

25
havent seen that before…(maybe shouldn’t have renamed the file during restart…)

btw @123, on start, the duration isn’t set yet, wont that prevent the timer from starting ? might take 0 as value when the media_file hasn’t been set yet?

The timer’s initial duration is set by the ‘Looping controller’ automation. When the input_boolean is set to ‘on’, it triggers ‘Looping controller’.

duration: >-
  {% set x = state_attr(states('sensor.sound_bite_player'),'media_duration')|int %}
  {{ x if trigger.to_state.state == 'on' else 0 }}

I guess if the result of state_attr is 0 then the timer will never start.

correct. and when no files has been played yet, the media_duration = 0. Ive tested that just now.

yes, at startup even None:

34

and that’s why I play the file first in my other scripts. so that the attribute gets set, and the templates can be used.

even so, if I play the file manually first to set the media_duration, the automation looper timer started wont start.
I can trigger it manually, and it plays the files correctly

I fully admit to having no knowledge of how your sensor.sound_bite_player works. I assumed that when it was assigned media to play, it determined the media’s duration and set the attribute. I didn’t know you had to play the media first to get its duration.

You’ll notice that in my driver’s code, there’s a similar need to begin playing the media first before its duration can be determined. However, it has flexibility similar to AppDaemon whereas here we are constrained by YAML and templates.

If we can’t determine the media’s duration prior to playing it, then my solution requires revision. I haven’t given it any deep thought yet but I feel it may not be worth the effort. The final result may lose its current simplicity and become a less attractive alternative to your existing solution.


EDIT
If you plan to play a limited selection of sound clips, a workaround would be to pre-calculate and store each clip’s duration in a dictionary. Then the automation would simply lookup the sound clip’s duration and set the timer accordingly. Not the most flexible of workarounds but it would be serviceable.

mmm, Im not giving up just yet. It seems to be an issue of the automatons not getting triggered at all.
after playing the file manually the first time, all should be set to flow correctly, but none of the automations do.

Ill recheck my code and the documentation to see if I have some spacing or indentation issues…

here’s my package:

update
testing with a preset duration

timer:
  looper:
    duration: '00:00:20'
##############################################################################################################
# Package for Looper timer
# 20190411 @mariusthvdb
# based on community https://community.home-assistant.io/t/how-to-loop-play-sound-files-solved/110533
# @123 suggested a timer looper
# see: https://community.home-assistant.io/t/how-to-loop-play-sound-files-solved/110533/27?u=mariusthvdb
# ##############################################################################################################


##############################################################################################################
# Inputs
##############################################################################################################

input_boolean:
  loop_sound_bite_timer:
    name: Loop sound bite (timer)
##############################################################################################################
# Timer
##############################################################################################################

timer:
  looper:

##############################################################################################################
# Automation
##############################################################################################################

automation:
# The first automation is triggered by input_boolean.loop_sound_bite_timer and simply starts/stops the timer (i.e. starts/stops looping).

  - alias: 'Looping controller'
    trigger:
      platform: state
      entity_id: input_boolean.loop_sound_bite_timer
    action:
      service_template: >
        {{'timer.start' if trigger.to_state.state == 'on' else 'timer.finish'}}
      data_template:
        entity_id: timer.looper
        duration: >
          {% set x = state_attr(states('sensor.sound_bite_player'),'media_duration')|round|int %}
          {{ x if trigger.to_state.state == 'on' else 0 }}

# The second automation is triggered when the timer starts. It’s job is to simply start playing the media.

  - alias: 'Looper timer started'
    trigger:
      platform: event
      event_type: timer.started
      event_data:
        entity_id: timer.looper
    action:
      service: media_player.play_media
      data_template:
        entity_id: >
          {{states('sensor.sound_bite_player')}}
        media_content_id: >
          {{states('sensor.sound_bite')}}
        media_content_type: 'music'

# The third and final automation is triggered when the timer finishes. It’s job is to restart the timer but 
# only if input_boolean.loop_sound_bite is on.

  - alias: 'Looper timer finished'
    trigger:
      platform: event
      event_type: timer.finished
      event_data:
        entity_id: timer.looper
    condition:
      condition: state
      entity_id: input_boolean.loop_sound_bite_timer
      state: 'on'
    action:
      service: timer.start
      entity_id: timer.looper