Playing Random Sounds without repeating until all have been played once

Okay, I’m wondering how to accomplish something.

I have a motion sensor in my guest bathroom that’s going to be triggering random pre-recorded audio that matches the theme of an upcoming party.

Here are some of the things that I’d like to have happen:

  • Files will play at random upon motion detection.
  • No single file will be repeated until all files in the folder have been played.
  • Once they’ve all been played, it will reset and start over with the list at random.
  • The motion sensor will trigger the sound, but while the sound is playing, the motion sensor will be ignored.
  • When an audio file ends, a 20 second timer will start. New sounds won’t be played until that timer expires to prevent guests from triggering twice.

I know how to play random sounds. What I don’t know how to do is cycle through all of the sounds until they’ve all been played without repeating. I also don’t know if there’s a state change or event that signals that the audio file has completed.

Any ideas?

I’m interested in this principle too for a completely different reason.

I can tell you that you can check your media player state. I use a wait_template with my Sonos. I wait for playing and then wait for paused so that I know when a media clip or announcement is finished.

(I believe some media players use stopped not paused).

Is that (partly) what you are after?

Partly, yes. I’ll have to monitor the event bus to see what happens when the audio stops.

Use Bitwise type binary to store the played sounds in a global variable. Next time it selects a sound to play check it against that variable to see if its been played. If it has, select again and if not, play and add that number to the stored value. If no sounds have been played that stored will be 0 if there are 8 sounds and all have been played then it will be 256. If sound 7 and sound 1 have been played the number will be 65

Not a great person at explaining what I mean and willing to explain more if needed

This isn’t really tested but should give a good starting point. If node-red has access to the directory where you’re storing the files you could replace the change node, set flow.musicList, with a node that scans the directory and returns the file list in an array. The timeout on the wait-until node might be useful, so the whole flow doesn’t get stuck in the disable position.

[{"id":"1ee17272.5e600e","type":"trigger-state","z":"ffbd7f06.4a014","name":"Bathroom Motion","entityid":"sensor.motion","entityidfiltertype":"exact","debugenabled":false,"constraints":[],"constraintsmustmatch":"all","outputs":2,"customoutputs":[],"outputinitially":false,"state_type":"str","x":250,"y":832,"wires":[["7116a743.9a35d8","aeb6c4fd.2386f8"],[]]},{"id":"7116a743.9a35d8","type":"function","z":"ffbd7f06.4a014","name":"","func":"// Get Array from flow variable or set to empty array\nconst array = flow.get(\"musicList\") || [];\n\n// If empty array send to second output\nif(array.length === 0) return [null, msg];\n\n// get a random index of array\nconst index = Math.floor(Math.random() * array.length);\n\n// remove the the random index from array \n// and save it to msg.payload\nmsg.payload = array.splice(index, 1)[0];\n\n// save new array to flow var minus the previous\n// selected item\nflow.set(\"musicList\", array);\n\nreturn [msg, null];","outputs":2,"noerr":0,"x":434,"y":832,"wires":[["6e8c847e.9d8b0c"],["88c785ce.d806a8"]]},{"id":"88c785ce.d806a8","type":"change","z":"ffbd7f06.4a014","name":"","rules":[{"t":"set","p":"musicList","pt":"flow","to":"[\"filename1.mp3\",\"filename2.mp3\",\"filename3.mp3\",\"filename4.mp3\",\"filename5.mp3\"]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":602,"y":880,"wires":[["7116a743.9a35d8"]]},{"id":"6e8c847e.9d8b0c","type":"api-call-service","z":"ffbd7f06.4a014","name":"Play media","service_domain":"media_player","service":"play_media","data":"{\"entity_id\":\"media_player.bathroom\",\"media_content_id\":\"{{payload}}\",\"media_content_type\":\"music\"}","mergecontext":"","output_location":"","output_location_type":"none","x":582,"y":832,"wires":[["1e0deb83.41c724"]]},{"id":"1e0deb83.41c724","type":"ha-wait-until","z":"ffbd7f06.4a014","name":"","outputs":2,"entityId":"media_player.bathroom","property":"state","comparator":"is_not","value":"playing","valueType":"str","timeout":"5","timeoutUnits":"minutes","entityLocation":"","entityLocationType":"none","checkCurrentState":false,"x":752,"y":832,"wires":[["e398c78d.ce3998"],["b1228530.56cc88"]]},{"id":"aeb6c4fd.2386f8","type":"change","z":"ffbd7f06.4a014","name":"disable","rules":[{"t":"set","p":"payload","pt":"msg","to":"disable","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":444,"y":784,"wires":[["1ee17272.5e600e"]]},{"id":"b1228530.56cc88","type":"change","z":"ffbd7f06.4a014","name":"enable","rules":[{"t":"set","p":"payload","pt":"msg","to":"enable","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":1058,"y":832,"wires":[["1ee17272.5e600e"]]},{"id":"e398c78d.ce3998","type":"delay","z":"ffbd7f06.4a014","name":"","pauseType":"delay","timeout":"20","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":908,"y":800,"wires":[["b1228530.56cc88"]]}]

Not sure how to get a file list, but I could get the file count from a sensor so I could pick off each number until there were no more available.

I might need a bit of help understanding the flow.

  • Motion is detected. Payload passes to “disable” and to the function, trigger:state node is disabled while the rest of the flow runs.

  • Media is played.

  • Function is processed.

  • Wait is set to either proceed to a 20 second pause when the media file ends, or bypass the 20 second delay if 5 minutes passes.

  • Enable is set.

  • Flow waits for new trigger.

So, you’re right, the only part I don’t know how to do is create the musicList.

Welp, I figured out how to use readdir to read the /config/www/sounds/party folder and I now have an array of all of the files in that folder. Getting there.

take a look at node-red-contrib-dir2files should be able to drop it in place of the change node and it should work

I need to brush up on regex. It creates the same result as readdir. Not sure how to filter out the ._file.mp3 hidden files.

^(?!\.).+\.mp3 will match files with the extension .mp3 but not if the filename starts with a .

Okay, so I made a few changes to this.

First, I used the fs-ops-dir node because the other nodes were including the file paths in the array data and I just needed the file name. So that works.

Secondly, I set the flow.musicList change node to set flow.musicList to msg.payload since the payload is now an array of files from a folder found by the Directory node.

Thirdly, after the Play Media node, I needed to put an additional Wait node because the state of the media player was being reported to the original wait node faster than the state was being updated in HASS.io by the Play Media node. I just put another Wait node with a 2 second delay in so the correct state could be established before the next wait. Without this, the 20 second delay was being triggered every time which caused audio files longer than 20 seconds to be at risk of being interrupted by the enabled motion detection.