Make sounds not overlap with python

Hey, y’all.

I’m trying to make it so when I play a random sound from a list, the sounds don’t overlap.
We’ve all seen the iconic NPC who’s saying multiple lines at once when you skip through the dialogue. I want to avoid that.

I’m using this system to play sounds from a directory: (The code plays random sounds,but they overlap.)

import bge
import aud

from random import choice
from bge.logic import expandPath, globalDict
from pathlib import Path

def getSounds(path):
	""" Returns a list containing all the audio files paths 
	with specific extensions on the given path. """
	
	sounds = []
	
	# Path objects make easier to work with paths
	path = Path(path).resolve()
	
	# Iterates over all files in the given path
	for _file in path.iterdir():
		
		# Add to list only files with the given extensions
		if _file.suffix in (".mp3", ".wav", ".ogg"):
			sounds.append(_file.as_posix())
			
	return sounds

def playSound(path, obj):
	""" Play 3D sound from path in the position of given object.
	Remember that 3D sound only works with mono audio files. """
	
	# Get the audio device and set its properties according to current camera
	device = aud.device()
	device.distance_model = aud.AUD_DISTANCE_MODEL_LINEAR
	device.listener_location = obj.scene.active_camera.worldPosition
	device.listener_orientation = obj.scene.active_camera.worldOrientation.to_quaternion()
	
	# Create an audio factory and play it, with a handle as result
	factory = aud.Factory(path)
	handle = device.play(factory)
	
	# Makes the handle behave as a 3D sound
	handle.relative = False
	handle.location = obj.worldPosition
	
	# If sound source is farther from listener than the value below, volume is zero
	handle.distance_maximum = 10


def main():
	cont = bge.logic.getCurrentController()
	own = cont.owner
	spacebar = cont.sensors["Spacebar"]
	
	# Load path list of sounds into globalDict if not already loaded
	if not "Sounds" in globalDict.keys():
		globalDict["Sounds"] = getSounds(expandPath("//sounds/"))
		print("Loaded sound paths to globalDict")
	
	# Play a sound then spacebar is pressed
	if spacebar.positive:
		
		# Get a random sound path from loaded sound paths
		soundToPlay = choice(globalDict["Sounds"])
		
		# Play random sound in the position of current object
		playSound(soundToPlay, own)
		print("Played random sound:", soundToPlay)
	
main()

So, the documentation seems to suggest I can check the status of a handle and if the status is 0, then the sound would be done playing.

Problem is, I seem to be missing something, because I can’t figure out how to check a handle’s status. is there a if handle.status == 0: function, or what?

I appreciate your time.

You seem to be asking about two different things:

  1. Stopping the previous sound when the next starts, such as when skipping through dialog
  2. Knowing when one sound has finished, so you can queue the next sound

The first one is easy. Just store a reference to “handle” and call stop() on it before you start the next.

The second one is a pain in the arse… I personally solved it by putting a timer on an empty and setting it to the negative of the duration of the first sound. I used a property sensor that detected when the property reached 0 so I could start the next sound.

So… Would one have to input the duration the each sound manually? is there a way to return the duration? My directory holds a library of many sounds of different durations, see.

If you can automatically return the duration of the sound, would you please show me how? I can’t figure it out myself.

if using aud module you got the option to use handle.status to check if something is playing or the option to join(factory)to play songs in sequence, more info here:

https://upbge-docs.readthedocs.io/en/latest/api/aud.html

On the function playSound add these lines at the end and it should work as you expect (tested on my test file).

	# Stop sound if already playing in obj
	if "Handle" in obj and obj["Handle"].status == aud.AUD_STATUS_PLAYING:
		obj["Handle"].stop()
		
	# Store handle reference in obj
	obj["Handle"] = handle

You can calculate it.

This is my function for WAV:

		import wave, contextlib
		with contextlib.closing(wave.open(soundPath,'r')) as f:
			frames = f.getnframes()
			rate = f.getframerate()
			duration = frames / float(rate)

This is my function for OGG:

from tinytag import TinyTag
length = TinyTag.get(soundPath).duration

I’m a dumb dumb.

For some reason, it wont write the handle to the property handle in the object.
Same for @TheDave’s solution.

I’m going to upload a blend is anyone want’s to dissect it and tell me what I’m doing wrong. This code was actualy written by you, @joelgomes1994 so I’m gunna try and go with your solution first if that’s ok with y’all.

ex-play-random-sound.blend (122.0 KB)

I place the code one the function that plays the sound, but it won’t write the handle to the obj[“Handle”] property.

As I know everyone here knows, you can’t pack sound effects and your directory will be different so there’s that.

That Handle property should not be added in the Blender interface, it must be created at runtime through the Python script.
Screenshot from 2021-02-18 14-57-16

It’s a common mistake, but properties added through the Blender interface cannot be overwritten with a value different from its original type (string, integer, float, boolean). On the other side, properties added at runtime are Python objects, so they can have any type (in this case, an aud.Handle object).
Just delete the Handle property from your object in the interface and your code should work just fine.

Sorry, had to watch Perseverance land on Mars. :stuck_out_tongue:

I removed the handle property and added your new code after the play function (The bottom) as such:

import bge
import aud

from random import choice
from bge.logic import expandPath, globalDict
from pathlib import Path

def getSounds(path):
	""" Returns a list containing all the audio files paths 
	with specific extensions on the given path. """
	
	sounds = []
	
	# Path objects make easier to work with paths
	path = Path(path).resolve()
	
	# Iterates over all files in the given path
	for _file in path.iterdir():
		
		# Add to list only files with the given extensions
		if _file.suffix in (".mp3", ".wav", ".ogg"):
			sounds.append(_file.as_posix())
			
	return sounds

def playSound(path, obj):
	""" Play 3D sound from path in the position of given object.
	Remember that 3D sound only works with mono audio files. """
	
	# Get the audio device and set its properties according to current camera
	device = aud.device()
	device.distance_model = aud.AUD_DISTANCE_MODEL_LINEAR
	device.listener_location = obj.scene.active_camera.worldPosition
	device.listener_orientation = obj.scene.active_camera.worldOrientation.to_quaternion()
	
	# Create an audio factory and play it, with a handle as result
	factory = aud.Factory(path)
	handle = device.play(factory)
	
	# Makes the handle behave as a 3D sound
	handle.relative = False
	handle.location = obj.worldPosition
	
	# If sound source is farther from listener than the value below, volume is zero
	handle.distance_maximum = 25


def main():
	cont = bge.logic.getCurrentController()
	own = cont.owner
	spacebar = cont.sensors["Spacebar"]
	
	# Load path list of sounds into globalDict if not already loaded
	if not "Sounds" in globalDict.keys():
		globalDict["Sounds"] = getSounds(expandPath("//Sounds\Misc\Explosions"))
		print("Loaded sound paths to globalDict")
	
	# Play a sound then spacebar is pressed
	if spacebar.positive:
		
		# Get a random sound path from loaded sound paths
		soundToPlay = choice(globalDict["Sounds"])
		
		# Play random sound in the position of current object

		playSound(soundToPlay, own)
		print("Played random sound:", soundToPlay)
		
		
main()

I’ve obviously done something wrong again, as it doesn’t work. I’m getting an error that the name “handle” and “obj” aren’t defined.

Thanks for your patience. What exactly am I doing wrong?

Are you sure you pasted the script that the error is coming from? I don’t see handle referenced anywhere before you’ve defined it.

Note that the way you’ve written it, that variable is local to the function only. If you’re trying to access it from outside of the function, then you need to put it somewhere global, or on an object as a property.

It’s the only script.
Here, I’ll just re-upload the same blend I did before, but at the bottom of the script, under the paly sound function, I’ll put joelgomes1995’s above piece of code. (He said to put it under the play function.)

EDIT: i uploaded the wrong file. Here’s the correct one.ex-play-random-sound.blend (122.2 KB)

That’s not the same script as the one in the blend…

		if "Handle" in obj and obj["Handle"].status == aud.AUD_STATUS_PLAYING:
			obj["Handle"].stop()

You haven’t defined “obj” anywhere.

1 Like

You pasted the code snippet on the main function, you’re supposed to paste it in the playSound function. The blend updated:
ex-play-random-sound.blend (116.4 KB)

Thanks man, that was it.
I really appreciate all of you peep’s time.

1 Like