Source code for pygame_menu.sound

"""
pygame-menu
https://github.com/ppizarror/pygame-menu

SOUND
Sound class.
"""

__all__ = [

    # Main class
    'Sound',

    # Sound types
    'SOUND_TYPE_CLICK_MOUSE',
    'SOUND_TYPE_CLICK_TOUCH',
    'SOUND_TYPE_CLOSE_MENU',
    'SOUND_TYPE_ERROR',
    'SOUND_TYPE_EVENT',
    'SOUND_TYPE_EVENT_ERROR',
    'SOUND_TYPE_KEY_ADDITION',
    'SOUND_TYPE_KEY_DELETION',
    'SOUND_TYPE_OPEN_MENU',
    'SOUND_TYPE_WIDGET_SELECTION',

    # Sound example paths
    'SOUND_EXAMPLE_CLICK_MOUSE',
    'SOUND_EXAMPLE_CLICK_TOUCH',
    'SOUND_EXAMPLE_CLOSE_MENU',
    'SOUND_EXAMPLE_ERROR',
    'SOUND_EXAMPLE_EVENT',
    'SOUND_EXAMPLE_EVENT_ERROR',
    'SOUND_EXAMPLE_KEY_ADD',
    'SOUND_EXAMPLE_KEY_DELETE',
    'SOUND_EXAMPLE_OPEN_MENU',
    'SOUND_EXAMPLE_WIDGET_SELECTION',
    'SOUND_EXAMPLES'

]

from pathlib import Path
import os.path as path
import time

from pygame import error as pygame_error
from pygame import mixer
from pygame import vernum as pygame_version

from pygame_menu._base import Base
from pygame_menu.utils import warn

from pygame_menu._types import NumberType, Dict, Any, Optional, Union, NumberInstance

try:  # pygame<2.0.0 compatibility
    from pygame import AUDIO_ALLOW_CHANNELS_CHANGE, AUDIO_ALLOW_FREQUENCY_CHANGE
except ImportError:
    AUDIO_ALLOW_CHANNELS_CHANGE, AUDIO_ALLOW_FREQUENCY_CHANGE = False, False

# Sound types
SOUND_TYPE_CLICK_MOUSE = '__pygame_menu_sound_click_mouse__'
SOUND_TYPE_CLICK_TOUCH = '__pygame_menu_sound_click_touch__'
SOUND_TYPE_CLOSE_MENU = '__pygame_menu_sound_close_menu__'
SOUND_TYPE_ERROR = '__pygame_menu_sound_error__'
SOUND_TYPE_EVENT = '__pygame_menu_sound_event__'
SOUND_TYPE_EVENT_ERROR = '__pygame_menu_sound_event_error__'
SOUND_TYPE_KEY_ADDITION = '__pygame_menu_sound_key_addition__'
SOUND_TYPE_KEY_DELETION = '__pygame_menu_sound_key_deletion__'
SOUND_TYPE_OPEN_MENU = '__pygame_menu_sound_open_menu__'
SOUND_TYPE_WIDGET_SELECTION = '__pygame_menu_sound_widget_selection__'

SOUND_TYPES = (
    SOUND_TYPE_CLICK_MOUSE,
    SOUND_TYPE_CLICK_TOUCH,
    SOUND_TYPE_CLOSE_MENU,
    SOUND_TYPE_ERROR,
    SOUND_TYPE_EVENT,
    SOUND_TYPE_EVENT_ERROR,
    SOUND_TYPE_KEY_ADDITION,
    SOUND_TYPE_KEY_DELETION,
    SOUND_TYPE_OPEN_MENU,
    SOUND_TYPE_WIDGET_SELECTION
)

# Sound example paths
__sounds_path__ = path.join(path.dirname(path.abspath(__file__)), 'resources', 'sounds', '{0}')

SOUND_EXAMPLE_CLICK_MOUSE = __sounds_path__.format('click_mouse.ogg')
SOUND_EXAMPLE_CLICK_TOUCH = SOUND_EXAMPLE_CLICK_MOUSE
SOUND_EXAMPLE_CLOSE_MENU = __sounds_path__.format('close_menu.ogg')
SOUND_EXAMPLE_ERROR = __sounds_path__.format('error.ogg')
SOUND_EXAMPLE_EVENT = __sounds_path__.format('event.ogg')
SOUND_EXAMPLE_EVENT_ERROR = __sounds_path__.format('event_error.ogg')
SOUND_EXAMPLE_KEY_ADD = __sounds_path__.format('key_add.ogg')
SOUND_EXAMPLE_KEY_DELETE = __sounds_path__.format('key_delete.ogg')
SOUND_EXAMPLE_OPEN_MENU = __sounds_path__.format('open_menu.ogg')
SOUND_EXAMPLE_WIDGET_SELECTION = __sounds_path__.format('widget_selection.ogg')

SOUND_EXAMPLES = (
    SOUND_EXAMPLE_CLICK_MOUSE,
    SOUND_EXAMPLE_CLICK_TOUCH,
    SOUND_EXAMPLE_CLOSE_MENU,
    SOUND_EXAMPLE_ERROR,
    SOUND_EXAMPLE_EVENT,
    SOUND_EXAMPLE_EVENT_ERROR,
    SOUND_EXAMPLE_KEY_ADD,
    SOUND_EXAMPLE_KEY_DELETE,
    SOUND_EXAMPLE_OPEN_MENU,
    SOUND_EXAMPLE_WIDGET_SELECTION
)

# Stores global reference that marks sounds as initialized
SOUND_INITIALIZED = [False, True]


[docs]class Sound(Base): """ Sound engine class. :param allowedchanges: Convert the samples at runtime, only in pygame>=2.0.0 :param buffer: Buffer size :param channels: Number of channels :param devicename: Device name :param force_init: Force mixer init with new parameters :param frequency: Frequency of sounds :param size: Size of sample :param sound_id: Sound ID :param uniquechannel: Force the channel to be unique, this is set at the object creation moment """ _channel: Optional['mixer.Channel'] _last_play: str _last_time: float _mixer_configs: Dict[str, Union[bool, int, str]] _sound: Dict[str, Dict[str, Any]] _uniquechannel: bool def __init__( self, allowedchanges: int = AUDIO_ALLOW_CHANNELS_CHANGE | AUDIO_ALLOW_FREQUENCY_CHANGE, buffer: int = 4096, channels: int = 2, devicename: str = '', force_init: bool = False, frequency: int = 22050, size: int = -16, sound_id: str = '', uniquechannel: bool = True ) -> None: super(Sound, self).__init__(object_id=sound_id) assert isinstance(allowedchanges, int) assert isinstance(buffer, int) assert isinstance(channels, int) assert isinstance(devicename, str) assert isinstance(force_init, bool) assert isinstance(frequency, int) assert isinstance(size, int) assert isinstance(uniquechannel, bool) assert buffer > 0, 'buffer size must be greater than zero' assert channels > 0, 'channels must be greater than zero' assert frequency > 0, 'frequency must be greater than zero' # Check if mixer is init mixer_missing = 'MissingModule' in str(type(mixer)) if mixer_missing: warn('pygame mixer module could not be found, NotImplementedError' 'has been raised. Sound support is disabled') SOUND_INITIALIZED[1] = False # Initialize sounds if not initialized if not mixer_missing and \ ((mixer.get_init() is None and not SOUND_INITIALIZED[0]) or force_init): # Set sound as initialized globally SOUND_INITIALIZED[0] = True # Check pygame version version_major, _, version_minor = pygame_version # noinspection PyBroadException try: # pygame < 1.9.5 mixer_kwargs = { 'frequency': frequency, 'size': size, 'channels': channels, 'buffer': buffer } # pygame >= 1.9.5 if (version_major == 1 and version_minor > 4) or version_major > 1: mixer_kwargs['devicename'] = devicename # pygame >= 2.0.0 if version_major > 1: mixer_kwargs['allowedchanges'] = allowedchanges # Call to mixer mixer.init(**mixer_kwargs) except Exception as e: warn('sound error: ' + str(e)) except pygame_error as e: warn('sound engine could not be initialized, pygame error: ' + str(e)) # Store mixer configs self._mixer_configs = { 'allowedchanges': allowedchanges, 'buffer': buffer, 'channels': channels, 'devicename': devicename, 'frequency': frequency, 'size': size } # Channel where a sound is played self._channel = None self._uniquechannel = uniquechannel # Sound dict self._sound = {} for sound in SOUND_TYPES: self._sound[sound] = {} # Last played song self._last_play = '' self._last_time = 0
[docs] def copy(self) -> 'Sound': """ Return a copy of the object. :return: Sound copied """ new_sound = Sound(uniquechannel=self._uniquechannel) new_sound._channel = self._channel for key in self._mixer_configs: new_sound._mixer_configs[key] = self._mixer_configs[key] for sound_type in self._sound.keys(): s = self._sound[sound_type] if len(s) != 0: new_sound.set_sound( sound_type=sound_type, sound_file=s['path'], volume=s['volume'], loops=s['loops'], maxtime=s['maxtime'], fade_ms=s['fade_ms'] ) return new_sound
def __copy__(self) -> 'Sound': """ Copy method. :return: Return new sound """ return self.copy() def __deepcopy__(self, memodict: Dict) -> 'Sound': """ Deep-copy method. :param memodict: Memo dict :return: Return new sound """ return self.copy()
[docs] def get_channel(self) -> 'mixer.Channel': """ Return the channel of the sound engine. :return: Sound engine channel """ # noinspection PyArgumentList channel = mixer.find_channel() # force only available on pygame v2 if self._uniquechannel: # If the channel is unique if self._channel is None: # If the channel has not been set self._channel = channel else: self._channel = channel # Store the available channel return self._channel
[docs] def set_sound( self, sound_type: str, sound_file: Optional[Union[str, 'Path']], volume: float = 0.5, loops: int = 0, maxtime: NumberType = 0, fade_ms: NumberType = 0 ) -> bool: """ Link a sound file to a sound type. :param sound_type: Sound type :param sound_file: Sound file. If ``None`` disable the given sound type :param volume: Volume of the sound, from ``0.0`` to ``1.0`` :param loops: Loops of the sound :param maxtime: Max playing time of the sound :param fade_ms: Fading ms :return: The status of the sound load, ``True`` if the sound was loaded """ assert isinstance(sound_type, str) assert isinstance(sound_file, (str, type(None), Path)) assert isinstance(volume, NumberInstance) assert isinstance(loops, int) assert isinstance(maxtime, NumberInstance) assert isinstance(fade_ms, NumberInstance) assert loops >= 0, 'loops count must be equal or greater than zero' assert maxtime >= 0, 'maxtime must be equal or greater than zero' assert fade_ms >= 0, 'fade_ms must be equal or greater than zero' assert 1 >= volume >= 0, 'volume must be between 0 and 1' # Check sound type is correct if sound_type not in SOUND_TYPES: raise ValueError('sound type not valid, check the manual') # If file is none disable the sound if sound_file is None or not SOUND_INITIALIZED[1]: self._sound[sound_type] = {} return False # Check the file exists sound_file = str(sound_file) if not path.isfile(sound_file): raise IOError(f'sound file "{sound_file}" does not exist') # Load the sound try: # noinspection PyTypeChecker sound_data = mixer.Sound(file=sound_file) except pygame_error: warn(f'the sound file "{sound_file}" could not be loaded, it has been disabled') self._sound[sound_type] = {} return False # Configure the sound sound_data.set_volume(float(volume)) # Store the sound self._sound[sound_type] = { 'fade_ms': fade_ms, 'file': sound_data, 'length': sound_data.get_length(), 'loops': loops, 'maxtime': maxtime, 'path': sound_file, 'type': sound_type, 'volume': volume } return True
[docs] def load_example_sounds(self, volume: float = 0.5) -> 'Sound': """ Load the example sounds provided by the package. :param volume: Volume of the sound, from ``0`` to ``1`` :return: Self reference """ assert isinstance(volume, NumberInstance) and 0 <= volume <= 1 for sound in range(len(SOUND_TYPES)): self.set_sound(SOUND_TYPES[sound], SOUND_EXAMPLES[sound], volume=float(volume)) return self
def _play_sound(self, sound: Optional[Dict[str, Any]]) -> bool: """ Play a sound. :param sound: Sound to be played :return: ``True`` if the sound was played """ if not sound: return False # Find an available channel channel = self.get_channel() # This will set the channel if it's None if channel is None: # The sound can't be played because all channels are busy return False # Play the sound sound_time = time.time() # If the previous sound is the same and has not ended (max 10% overlap) if sound['type'] != self._last_play or \ sound_time - self._last_time >= 0.1 * sound['length'] or self._uniquechannel: try: if self._uniquechannel: # Stop the current channel if it's unique channel.stop() channel.play(sound['file'], loops=sound['loops'], maxtime=sound['maxtime'], fade_ms=sound['fade_ms'] ) except pygame_error: # Ignore errors pass # Store last execution self._last_play = sound['type'] self._last_time = sound_time return True
[docs] def play_click_mouse(self) -> 'Sound': """ Play click mouse sound. :return: Self reference """ self._play_sound(self._sound[SOUND_TYPE_CLICK_MOUSE]) return self
[docs] def play_click_touch(self) -> 'Sound': """ Play click touch sound. :return: Self reference """ self._play_sound(self._sound[SOUND_TYPE_CLICK_TOUCH]) return self
[docs] def play_error(self) -> 'Sound': """ Play error sound. :return: Self reference """ self._play_sound(self._sound[SOUND_TYPE_ERROR]) return self
[docs] def play_event(self) -> 'Sound': """ Play event sound. :return: Self reference """ self._play_sound(self._sound[SOUND_TYPE_EVENT]) return self
[docs] def play_event_error(self) -> 'Sound': """ Play event error sound. :return: Self reference """ self._play_sound(self._sound[SOUND_TYPE_EVENT_ERROR]) return self
[docs] def play_key_add(self) -> 'Sound': """ Play key addition sound. :return: Self reference """ self._play_sound(self._sound[SOUND_TYPE_KEY_ADDITION]) return self
[docs] def play_key_del(self) -> 'Sound': """ Play key deletion sound. :return: Self reference """ self._play_sound(self._sound[SOUND_TYPE_KEY_DELETION]) return self
[docs] def play_open_menu(self) -> 'Sound': """ Play open Menu sound. :return: Self reference """ self._play_sound(self._sound[SOUND_TYPE_OPEN_MENU]) return self
[docs] def play_close_menu(self) -> 'Sound': """ Play close Menu sound. :return: Self reference """ self._play_sound(self._sound[SOUND_TYPE_CLOSE_MENU]) return self
[docs] def play_widget_selection(self) -> 'Sound': """ Play widget selection sound. :return: Self reference """ self._play_sound(self._sound[SOUND_TYPE_WIDGET_SELECTION]) return self
[docs] def stop(self) -> 'Sound': """ Stop the channel. :return: Self reference """ channel = self.get_channel() if channel is None: # The sound can't be played because all channels are busy return self try: channel.stop() except pygame_error: pass return self
[docs] def pause(self) -> 'Sound': """ Pause the channel. :return: Self reference """ channel = self.get_channel() if channel is None: # The sound can't be played because all channels are busy return self try: channel.pause() except pygame_error: pass return self
[docs] def unpause(self) -> 'Sound': """ Unpause channel. :return: Self reference """ channel = self.get_channel() if channel is None: # The sound can't be played because all channels are busy return self try: channel.unpause() except pygame_error: pass return self
[docs] def get_channel_info(self) -> Dict[str, Any]: """ Return the channel information. :return: Information dict e.g.: ``{'busy': 0, 'endevent': 0, 'queue': None, 'sound': None, 'volume': 1.0}`` """ channel = self.get_channel() data = {} if channel is None: # The sound can't be played because all channels are busy return data data['busy'] = channel.get_busy() data['endevent'] = channel.get_endevent() data['queue'] = channel.get_queue() data['sound'] = channel.get_sound() data['volume'] = channel.get_volume() return data