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?
Stopping the previous sound when the next starts, such as when skipping through dialog
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:
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
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.
That Handle property should not be added in the Blender interface, it must be created at runtime through the Python script.
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.
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.)
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)