codehearts/pickles-fetch-quest

View on GitHub
engine/audio/audio_director.py

Summary

Maintainability
A
0 mins
Test Coverage
from .audio_source import AudioSource
from engine import disk
import pyglet.media


class AudioDirector(object):
    """Director for loading audio and controlling playback.

    Attributes:
        attenuation_distance (int): The default attenuation distance for newly
            loaded audio. Existing audio will retain its attenuation distance,
            see :fn:`set_attenuation_distance` for setting distance on existing
            sources.
        master_volume (float): The master volume for audio playback.
            0 for silence, 1 for nominal volume. A value of 1 disables
            audio attenuation and ignore the position of audio sources.
            To avoid this, set volume to 0.99 or lower.
        position (tuple of int): The location of the audio listener in
            two-dimensional space. Listeners close to this position will be
            louder than those further away.
    """

    def __init__(self, master_volume=1, position=(0, 0)):
        """Creates a director for grouping and controlling audio playback.

        Kwargs:
            master_volume (float, optional): Master volume for audio playback.
                0 for silence, 1 for nominal volume. A value of 1 will disable
                audio attenuation and ignore the position of audio sources.
                To avoid this, set volume to 0.99 or lower. Defaults to 1.
            position (tuple of int, optional): The location of the audio
                listener in two-dimensional space. Listeners close to this
                position will be louder than those farther. Defaults to (0, 0).
        """
        super(AudioDirector, self).__init__()

        self.attenuation_distance = 1
        self.master_volume = master_volume
        self.position = position

        # Cache of loaded resources from disk
        self._disk_cache = {}

        # Groupings for audio sources
        self._groups = {
            'all': set()
        }

    def load(self, filepath, streaming=True):
        """Loads and audio file from disk.

        The loaded audio will be added to the 'all' group for this director.

        A cached object will be returned if the file has already been loaded.
        Streaming should be used for large audio sources, such as music.
        Only one instance of a streaming audio source can be played at a time.

        Args:
            filepath (str): Path to audio, relative to the resource directory.

        Kwargs:
            streaming (bool, optional): Streams the audio from disk rather
                than loading the entire file into memory. Defaults to True.

        Returns:
            An :obj:`audio.AudioSource` object for the resource on disk.
        """
        # Load the file from disk and cache it if necessary
        if filepath not in self._disk_cache:
            disk_file = disk.DiskLoader.load_audio(filepath, streaming)
            new_source = AudioSource(disk_file, streaming)

            # Cache the new source
            self._disk_cache[filepath] = new_source

            # Apply the default attenuation distance
            new_source.attenuation_distance = self.attenuation_distance

            # Add this audio source to the default group
            self.add(new_source)

        return self._disk_cache[filepath]

    def add(self, audio_source, group='all'):
        """Adds an audio source to a group.

        Grouping audio allows you to control the playback of the entire group
        rather than an individual source instance. By default, the audio source
        is added to the 'all' group.

        Args:
            audio_source (:obj:`audio.AudioSource`): The audio source to add.

        Kwargs:
            group (str, optional): The group to add the audio to.
                Defaults to 'all'.
        """
        self._groups.setdefault(group, set()).add(audio_source)

    def _filter_sources(self, group='all', states=None):
        """Returns all sources in the group matching the given states.

        Kwargs:
            group (str, optional): Name of group to filter. Defaults to 'all'.
            states (list of int, optional): List of :cls:`AudioSource` states
                to filter on. If the list is not empty and a source's state is
                not in the list, it will be excluded from the return value.

        Returns:
            An iterator containing sources in the group matching the states.
        """
        # If the group does not exist, return an empty iterator
        if group not in self._groups:
            return iter(())

        # If there are no states to filter on, return all sources in the group
        if not states:
            return iter(self._groups[group])

        # Return sources in the group matching the states to filter on
        return filter(lambda src: src.state in states, self._groups[group])

    def play(self, group='all'):
        """Plays all audio sources in a group.

        Kwargs:
            group (str, optional): Name of group to play. Defaults to 'all'.
        """
        for audio_source in self._filter_sources(group=group):
            audio_source.play()

    def pause(self, group='all'):
        """Pauses all playing audio sources in a group.

        Audio sources which are not currently playing will be left alone.

        Kwargs:
            group (str, optional): Name of group to pause. Defaults to 'all'.
        """
        states = [AudioSource.PLAY]
        for audio_source in self._filter_sources(group=group, states=states):
            audio_source.pause()

    def stop(self, group='all'):
        """Stops all audio sources in a group.

        Kwargs:
            group (str, optional): Name of group to stop. Defaults to 'all'.
        """
        states = [AudioSource.PLAY, AudioSource.PAUSE]
        for audio_source in self._filter_sources(group=group, states=states):
            audio_source.stop()

    def resume(self, group='all'):
        """Resumes playback of all paused audio sources in a group.

        Audio sources which are not currently paused will be left alone.

        Kwargs:
            group (str, optional): Name of group to resume. Defaults to 'all'.
        """
        states = [AudioSource.PAUSE]
        for audio_source in self._filter_sources(group=group, states=states):
            audio_source.play()

    def set_volume(self, level, group='all'):
        """Sets the volume of all audio sources in a group.

        Args:
            volume (float): 0 for silence, 1 for nominal volume.

        Kwargs:
            group (str, optional): Group to set volume of. Defaults to 'all'.
        """
        for audio_source in self._filter_sources(group=group):
            audio_source.volume = level

    def set_attenuation_distance(self, distance, group='all'):
        """Sets the distance from the listener before player volumes attenuate.

        Args:
            distance (int): The distance from the listener before the source
                volume attenuates. Within this distance, the volume remains
                nominal. Outside this distance, the volume approaches zero.

        Kwargs:
            group (str, optional): Group to set distance of. Defaults to 'all'.
        """
        for audio_source in self._filter_sources(group=group):
            audio_source.attenuation_distance = distance

    @property
    def position(self):
        """The position of the listener in 2d space as a tuple-like type."""
        return self._position

    @position.setter
    def position(self, position):
        """Sets the listener location in 2d space with a tuple-like object."""
        self._position = position

        # Pyglet uses 3d coordinates, convert 2d to a 3d tuple
        listener = pyglet.media.get_audio_driver().get_listener()
        listener.position = (position[0], position[1], 0)

    @property
    def master_volume(self):
        """Returns the master audio volume as a float between 0 and 1."""
        listener = pyglet.media.get_audio_driver().get_listener()
        return listener.volume

    @master_volume.setter
    def master_volume(self, level):
        """Sets the master audio playback volume.

        0 for silence, 1 for nominal volume. Setting this to 1 disables audio
        attenuation, ignoring the position of listeners. Set to 0.99 to
        allow for audio positioning.
        """
        listener = pyglet.media.get_audio_driver().get_listener()
        listener.volume = level