Source code for sge.snd

# Copyright (C) 2012-2014, 2016 Julie Marchant <onpon4@riseup.net>
# 
# This file is part of the Pygame SGE.
# 
# The Pygame SGE is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# The Pygame SGE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
# 
# You should have received a copy of the GNU Lesser General Public License
# along with the Pygame SGE.  If not, see <http://www.gnu.org/licenses/>.

"""
This module provides classes related to the sound system.
"""

from __future__ import division
from __future__ import absolute_import
from __future__ import print_function
from __future__ import unicode_literals

import os
import random
import warnings

import pygame
import six

import sge
from sge import r
from sge.r import _get_channel, _release_channel


__all__ = ["Sound", "Music", "stop_all"]


[docs]class Sound(object): """ This class stores and plays sound effects. Note that this is inefficient for large music files; for those, use :class:`sge.snd.Music` instead. What sound formats are supported depends on the implementation of the SGE, but sound formats that are generally a good choice are Ogg Vorbis and uncompressed WAV. See the implementation-specific information for a full list of supported formats. .. attribute:: volume The volume of the sound as a value from ``0`` to ``1`` (``0`` for no sound, ``1`` for maximum volume). .. attribute:: max_play The maximum number of instances of this sound playing permitted. If a sound is played while this number of the instances of the same sound are already playing, one of the already playing sounds will be stopped before playing the new instance. Set to :const:`None` for no limit. .. attribute:: parent Indicates another sound which is treated as being the same sound as this one for the purpose of determining whether or not, and how many times, the sound is playing. Set to :const:`None` for no parent. If the sound has a parent, :attr:`max_play` will have no effect and instead the parent sound's :attr:`max_play` will apply to both the parent sound and this sound. .. warning:: It is acceptable for a sound to both be a parent and have a parent. However, there MUST be a parent at the top which has no parent. The behavior of circular parenting, such as making two sounds parents of each other, is undefined. .. attribute:: fname The file name of the sound given when it was created. (Read-only) .. attribute:: length The length of the sound in milliseconds. (Read-only) .. attribute:: playing The number of instances of this sound playing. (Read-only) .. attribute:: rd Reserved dictionary for internal use by the SGE. (Read-only) """ @property def max_play(self): return len(self.rd["channels"]) if self.rd["channels"] else None @max_play.setter def max_play(self, value): self.__max_play = value if value is None: value = 0 if self.__sound is not None and self.parent is None: value = max(0, value) while len(self.rd["channels"]) < value: self.rd["channels"].append(_get_channel()) while len(self.rd["channels"]) > value: _release_channel(self.rd["channels"].pop(-1)) @property def parent(self): return self.__parent @parent.setter def parent(self, value): if value != self.__parent: self.__parent = value if value is None: self.rd["channels"] = [] self.max_play = self.__max_play else: while self.rd["channels"]: _release_channel(self.rd["channels"].pop(-1)) self.rd["channels"] = value.rd["channels"] @property def length(self): if self.__sound is not None: return self.__sound.get_length() * 1000 else: return 0 @property def playing(self): n = 0 for channel in self.rd["channels"] + self.__temp_channels: if channel.get_busy(): n += 1 return n
[docs] def __init__(self, fname, volume=1, max_play=1, parent=None): """ Arguments: - ``fname`` -- The path to the sound file. If set to :const:`None`, this object will not actually play any sound. If this is neither a valid sound file nor :const:`None`, :exc:`OSError` is raised. All other arguments set the respective initial attributes of the sound. See the documentation for :class:`sge.snd.Sound` for more information. """ self.rd = {} errlist = [] if fname is not None and pygame.mixer.get_init(): try: self.__sound = pygame.mixer.Sound(fname) except pygame.error as e: raise OSError(e) else: self.__sound = None self.rd["channels"] = [] self.__temp_channels = [] self.fname = fname self.volume = volume self.__parent = None if parent is None: self.max_play = max_play else: self.__max_play = max_play self.parent = parent
[docs] def play(self, loops=1, volume=1, balance=0, maxtime=None, fade_time=None, force=True): """ Play the sound. Arguments: - ``loops`` -- The number of times to play the sound; set to :const:`None` or ``0`` to loop indefinitely. - ``volume`` -- The volume to play the sound at as a factor of :attr:`self.volume` (``0`` for no sound, ``1`` for :attr:`self.volume`). - ``balance`` -- The balance of the sound effect on stereo speakers as a float from ``-1`` to ``1``, where ``0`` is centered (full volume in both speakers), ``1`` is entirely in the right speaker, and ``-1`` is entirely in the left speaker. - ``maxtime`` -- The maximum amount of time to play the sound in milliseconds; set to :const:`None` for no limit. - ``fade_time`` -- The time in milliseconds over which to fade the sound in; set to :const:`None` or ``0`` to immediately play the sound at full volume. - ``force`` -- Whether or not the sound should be played even if it is already playing the maximum number of times. If set to :const:`True` and the sound is already playing the maximum number of times, one of the instances of the sound already playing will be stopped. """ if self.__sound is not None: if not loops: loops = 0 if maxtime is None: maxtime = 0 if fade_time is None: fade_time = 0 # Adjust for the way Pygame does repeats loops -= 1 # Calculate volume for each speaker left_volume = min(1, self.volume * volume) right_volume = left_volume if balance < 0: right_volume *= 1 - abs(balance) elif balance > 0: left_volume *= 1 - abs(balance) if self.max_play: for channel in self.rd["channels"]: if not channel.get_busy(): channel.play(self.__sound, loops, maxtime, fade_time) channel.set_volume(left_volume, right_volume) break else: if force: channel = random.choice(self.rd["channels"]) channel.play(self.__sound, loops, maxtime, fade_time) channel.set_volume(left_volume, right_volume) else: channel = _get_channel() channel.play(self.__sound, loops, maxtime, fade_time) channel.set_volume(left_volume, right_volume) self.__temp_channels.append(channel) # Clean up old temporary channels while (self.__temp_channels and not self.__temp_channels[0].get_busy()): _release_channel(self.__temp_channels.pop(0))
[docs] def stop(self, fade_time=None): """ Stop the sound. Arguments: - ``fade_time`` -- The time in milliseconds over which to fade the sound out before stopping; set to :const:`None` or ``0`` to immediately stop the sound. """ if self.__sound is not None: self.__sound.stop()
[docs] def pause(self): """Pause playback of the sound.""" for channel in self.rd["channels"]: channel.pause()
[docs] def unpause(self): """Resume playback of the sound if paused.""" for channel in self.rd["channels"]: channel.unpause()
[docs]class Music(object): """ This class stores and plays music. Music is very similar to sound effects, but only one music file can be played at a time, and it is more efficient for larger files than :class:`sge.snd.Sound`. What music formats are supported depends on the implementation of the SGE, but Ogg Vorbis is generally a good choice. See the implementation-specific information for a full list of supported formats. .. note:: You should avoid the temptation to use MP3 files; MP3 is a patent-encumbered format, so many systems do not support it and royalties to the patent holders may be required for commercial use. There are many programs which can convert your MP3 files to the free Ogg Vorbis format. .. attribute:: volume The volume of the music as a value from ``0`` to ``1`` (``0`` for no sound, ``1`` for maximum volume). .. attribute:: fname The file name of the music given when it was created. (Read-only) .. attribute:: length The length of the music in milliseconds. (Read-only) .. attribute:: playing Whether or not the music is playing. (Read-only) .. attribute:: position The current position (time) playback of the music is at in milliseconds. (Read-only) .. attribute:: rd Reserved dictionary for internal use by the SGE. (Read-only) """ @property def volume(self): return self.__volume @volume.setter def volume(self, value): self.__volume = min(value, 1) if self.playing: pygame.mixer.music.set_volume(value) @property def length(self): if self.__length is None: if self.fname is not None: snd = pygame.mixer.Sound(self.fname) self.__length = snd.get_length() * 1000 else: self.__length = 0 return self.__length @property def playing(self): return r.music is self and pygame.mixer.music.get_busy() @property def position(self): if self.playing: return self.__start + pygame.mixer.music.get_pos() else: return 0
[docs] def __init__(self, fname, volume=1): """ Arguments: - ``fname`` -- The path to the sound file. If set to :const:`None`, this object will not actually play any music. If this is neither a valid sound file nor :const:`None`, :exc:`OSError` is raised. All other arguments set the respective initial attributes of the music. See the documentation for :class:`sge.snd.Music` for more information. """ self.rd = {} if fname is None or os.path.isfile(fname): self.fname = fname else: if six.PY2: raise OSError('File "{}" not found.'.format(fname)) else: raise FileNotFoundError('File "{}" not found.'.format(fname)) self.volume = volume self.rd["timeout"] = None self.rd["fade_time"] = None self.__start = 0 self.__length = None
[docs] def play(self, start=0, loops=1, maxtime=None, fade_time=None): """ Play the music. Arguments: - ``start`` -- The number of milliseconds from the beginning to start playing at. See the documentation for :meth:`sge.snd.Sound.play` for more information. """ if self.fname is not None: if not self.playing: try: pygame.mixer.music.load(self.fname) except pygame.error as e: warnings.warn(str(e)) return if not loops: loops = -1 r.music = self self.rd["timeout"] = maxtime self.rd["fade_time"] = fade_time if fade_time is not None and fade_time > 0: pygame.mixer.music.set_volume(0) else: pygame.mixer.music.set_volume(self.volume) if self.fname.lower().endswith(".mod"): # MOD music is handled differently in Pygame: it uses # the pattern order number rather than the time to # indicate the start time. self._start = 0 pygame.mixer.music.play(loops, start) else: self.__start = start try: pygame.mixer.music.play(loops, start / 1000) except NotImplementedError: pygame.mixer.music.play(loops)
[docs] def queue(self, start=0, loops=1, maxtime=None, fade_time=None): """ Queue the music for playback. This will cause the music to be added to a list of music to play in order, after the previous music has finished playing. See the documentation for :meth:`sge.snd.Music.play` for more information. """ r.music_queue.append((self, start, loops, maxtime, fade_time))
@staticmethod
[docs] def stop(fade_time=None): """ Stop the currently playing music. See the documentation for :meth:`sge.snd.Sound.stop` for more information. """ if fade_time: pygame.mixer.music.fadeout(fade_time) else: pygame.mixer.music.stop()
@staticmethod
[docs] def pause(): """Pause playback of the currently playing music.""" pygame.mixer.music.pause()
@staticmethod
[docs] def unpause(): """Resume playback of the currently playing music if paused.""" pygame.mixer.music.unpause()
@staticmethod
[docs] def clear_queue(): """Clear the music queue.""" r.music_queue = []
[docs]def stop_all(): """Stop playback of all sounds.""" pygame.mixer.stop()