"""Useful blocks for sounds"""
import math
import random
import serge.sound
import serge.actor
[docs]class NoListener(Exception): """Positional sound enabled but there is no listener set"""
[docs]class SoundTexture(serge.actor.Actor):
"""An actor that manages a number of sounds to create a texture
The actor can control sounds that are produced either ambiently (everywhere)
or at specific locations. For the sounds at specific locations the sounds
will get louder as the listener gets closer to them.
"""
def __init__(self, tag, name, damping=None):
"""Initialise the SoundTexture"""
super(SoundTexture, self).__init__(tag, name)
#
self.sounds = []
self.random_sounds = []
self.listener = None
self._master_volume = 1.0
self._listener_required = False
self.damping = damping
self._playing = False
[docs] def setListener(self, listener):
"""Set the listener for the sounds
The listener is an actor and the sounds play at a volume determined
by the location of the listener relative to each sound.
:param listener: an actor
"""
self.listener = listener
[docs] def getListener(self):
"""Return the listener"""
return self.listener
[docs] def addAmbientSound(self, sound):
"""Add an ambient sound to the texture
An ambient sound plays at the same volume no matter where the listener
is. Ambient sounds still get paused with the other sounds.
:param sound: the serge sound object that should be played
"""
self.sounds.append(AmbientSound(sound))
[docs] def addRandomSound(self, sound, probability):
"""Add a random sound to the texture
A random sound plays with a likelihood of probability/second.
:param sound: the serge sound object that should be played
:param probability: the probability that the sound will play in a given second
"""
self.random_sounds.append(ProbabalisticSound(sound, probability))
[docs] def addPositionalSound(self, sound):
"""Add a positional sound to the texture
A position sound plays at one or more locations in space and its volume is dependent
on the location of the listener.
:param sound: the sound object that should be played. It should inherit from AmbientSound
"""
self.sounds.append(sound)
self._listener_required = True
[docs] def getSounds(self):
"""Return all the sounds that we are controlling"""
return self.sounds
[docs] def set_volume(self, volume):
"""Set the master volume
This affects the volume of all sounds. The target volume for a sound is multiplied
by this master.
:param volume: master volume setting (0=silent, 1=full volume)
"""
self._master_volume = volume
for sound in self.getSounds():
sound.set_volume(volume)
[docs] def get_volume(self):
"""Return the master value setting"""
return self._master_volume
[docs] def play(self, loops=0):
"""Play the sounds
:param loops: number of times to loop the sounds (0=never, -1=for ever)
"""
for sound in self.getSounds():
sound.play(loops)
self._playing = True
[docs] def pause(self):
"""Pause the sounds"""
for sound in self.getSounds():
sound.pause()
self._playing = False
[docs] def stop(self):
"""Stop the sounds"""
for sound in self.getSounds():
sound.stop()
self._playing = False
[docs] def updateActor(self, interval, world):
"""Update the actor"""
super(SoundTexture, self).updateActor(interval, world)
#
# Update the volume of all sounds
if self.listener:
for sound in self.sounds:
#
# Update the sound volume
current_volume = sound.get_volume()
target_volume = sound.get_scaled_volume((self.listener.x, self.listener.y))
#
# Damp changes in the sound volume
if self.damping is None:
new_volume = target_volume*self._master_volume
else:
new_volume = max(0.0, min(1.0, interval/1000.0*self.damping)*
(target_volume - current_volume) + current_volume)*self._master_volume
sound.set_volume(new_volume)
elif self._listener_required:
raise NoListener('A listener has not been set for this texture (%s)' % self.getNiceName())
#
# Play any random sounds
if self._playing:
for sound in self.random_sounds:
sound.try_to_play(interval)
[docs]class ProbabalisticSound(serge.sound.SoundItem):
"""A sound that plays with a certain probability"""
def __init__(self, sound, probability):
"""Initialise the ProbabalisticSound"""
super(ProbabalisticSound, self).__init__(sound=sound)
self.probability = probability
[docs] def try_to_play(self, interval):
"""Try to play the sound"""
if random.random() < self.probability * interval/1000.0:
self.play()
[docs]class AmbientSound(serge.sound.SoundItem):
"""A sound located everywhere in space"""
def __init__(self, sound):
"""Initialise the sound"""
super(AmbientSound, self).__init__(sound=sound)
[docs] def get_scaled_volume(self, listener_position):
"""Return the sound volume according to the listener position"""
return 1.0
[docs]class LocationalSound(AmbientSound):
"""A sound that is located somewhere in space"""
def __init__(self, sound, location, dropoff):
"""Initialise the sound"""
super(LocationalSound, self).__init__(sound)
#
self.location = location
self.dropoff = dropoff
[docs] def get_scaled_volume(self, listener_position):
"""Update the sound volume according to the listener position"""
dist = math.sqrt((listener_position[0]-self.location[0])**2 +(listener_position[1]-self.location[1])**2)
return max(0.0, 1.0-dist/self.dropoff)
[docs]class LocationalSounds(AmbientSound):
"""A series of sounds that are located at a number of places in space but generate only a single sound"""
def __init__(self, sound, locations, dropoff):
"""Initialise the sound"""
super(LocationalSounds, self).__init__(sound)
#
self.locations = locations
self.dropoff = dropoff
[docs] def get_scaled_volume(self, listener_position):
"""Update the sound volume according to the listener position"""
total = 0.0
for location in self.locations:
dist = math.sqrt((listener_position[0]-location[0])**2 +(listener_position[1]-location[1])**2)
total += max(0.0, 1.0-dist/self.dropoff)
return min(1.0, total)
[docs]class RectangularRegionSound(AmbientSound):
"""A sound that is located in a rectangular region
Inside the region the volume is full and outside the region the volume is
zero.
"""
def __init__(self, sound, region):
"""Initialise the sound"""
super(RectangularRegionSound, self).__init__(sound)
#
self.location = serge.geometry.Rectangle(*region)
[docs] def get_scaled_volume(self, listener_position):
"""Update the sound volume according to the listener position"""
p = serge.geometry.Point(*listener_position)
return 1.0 if p.isInside(self.location) else 0.0