How to loop play sound files [Solved]

@Mariusthvdb

This is exactly what I suggested to you in my first reply.

For my alarm clock I took the 2-3 second samples I had and used Audacity to create a 5 minute loop of the audio. Now I know how long the files are so I can then set a 5 minute timer just in case I don’t wake up in the first 5 minutes and have to restart the play to keep the alarm sounding.

Here is my entire alarm clock package. Maybe there is something in there you can pick out and use.

First time I’ve done one of these gists, hope it works! (Not ready to make my entire config public yet)

https://gist.github.com/jazzyisj/ff5fda62ac857d93ecb66fef9149defb

1 Like

partial succes!
changed setup to:

  play_sound_bite:
    alias: Play sound bite
    sequence:
      - service: script.turn_off
        entity_id: script.sound_bite_loop
      - service: media_player.play_media
        data_template:
          entity_id: >
            {{states('sensor.sound_bite_player')}}
          media_content_id: >
            {{states('sensor.sound_bite')}}
          media_content_type: 'audio/mp4'
#      - condition: template
#        value_template: >
#          {{is_state('input_boolean.loop_sound_bite','on')}}
#      - wait_template: >
#          {{states(states('sensor.sound_bite_player')) != 'playing'}}
#      - delay:
#          seconds: 5
#      - service: script.play_sound_bite
      - service: script.turn_on
        entity_id: script.sound_bite_loop

  sound_bite_loop:
    alias: Sound bite loop
    sequence:
      - condition: template
        value_template: >
          {{is_state('input_boolean.loop_sound_bite','on')}}
      - delay:
          seconds: 5
      - wait_template: >
         {{states(states('sensor.sound_bite_player')) != 'playing'}}
      - service: script.turn_off
        entity_id: script.play_sound_bite
      - service: script.turn_on
        entity_id: script.play_sound_bite

and all my shorter files are looping nicely. At least the architecture should be fine now, and finetuning for the templates needs to be done.

1 Like

@Mariusthvdb
Side question…

What is the difference (if any) between using

      - service: script.turn_on
        entity_id: script.play_sound_bite

and

      - service: script.play_sound_bite

I have always used the second method but am now wondering if there is something I don’t know about.

(Obviously there is lots I don’t know about but you know what I mean :wink: )

there is none, other than when using variables. see https://www.home-assistant.io/components/script/#passing-variables-to-scripts

I’ve only changed it here, because of the fact I need to turn_off the script, which can only be done using script.turn_off, and I like the scripts being symmetrical.

1 Like

testing:

      - delay:
          seconds: >
           {{1+state_attr(states('sensor.sound_bite_player'),'media_duration')|int}}

which should use the file length in seconds, and add 1 just to be sure.

still hoping a media_player.loop_media service could be made, and filed a feature request for that…

bingo!
all files loop. there’s still some lag, the file won’t append directly, but for an alarm this wil do.

  play_sound_bite:
    alias: Play sound bite
    sequence:
      - service: script.turn_off
        entity_id: script.play_sound_bite_loop
      - 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'
      - delay:
          seconds: 1
      - delay:
          seconds: >
           {{state_attr(states('sensor.sound_bite_player'),'media_duration')|int}}
      - condition: template
        value_template: >
          {{is_state('input_boolean.loop_sound_bite','on')}}
#      - delay:
#          seconds: 2
#      - wait_template: >
#          {{states(states('sensor.sound_bite_player')) != 'playing'}}
#      - delay:
#          seconds: 1
#      - service: script.play_sound_bite
      - service: script.play_sound_bite_loop

  play_sound_bite_loop:
    alias: Play sound bite loop
    sequence:
      - service: script.turn_off
        entity_id: script.play_sound_bite
      - 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'
      - delay:
          seconds: 1
      - delay:
          seconds: >
           {{state_attr(states('sensor.sound_bite_player'),'media_duration')|int}}
      - condition: template
        value_template: >
          {{is_state('input_boolean.loop_sound_bite','on')}}
#      - wait_template: >
#          {{states(states('sensor.sound_bite_player')) != 'playing'}}
      - service: script.play_sound_bite

still checking out wether with this new setup, the initial service turn_off is necessary, as this timing ought to be precise and immediate.
the delay of 1 second might also be superfluous, but Ive set it for now to give the media_duration sensor time to check…

edit
delay: 1 second: it is superfluous…
service: turn_off: not necessary

which is kind of wonderful. leading to my final setup for now:

  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

  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

  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'

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.