Source code for engines.sound

# ../engines/sound.py

"""Provides access to the Sound and StreamSound interfaces."""

# =============================================================================
# >> IMPORTS
# =============================================================================
# Python Imports
#   Contextlib
from contextlib import closing
#   Enum
from enum import Enum
#   Os
import os
#   Wave
import wave

# Site Package Imports
#   Mutagen
from mutagen import mp3
from mutagen import oggvorbis

# Source.Python Imports
#   Core
from core import WeakAutoUnload
#   Engines
from engines import engines_logger
#   Entities
from entities.constants import INVALID_ENTITY_INDEX
#   Filesystem
from filesystem import SourceFile
#   Mathlib
from mathlib import NULL_VECTOR
#   Paths
from paths import GAME_PATH
#   Stringtables
from stringtables import INVALID_STRING_INDEX, string_tables
from stringtables.downloads import Downloadables


# =============================================================================
# >> FORWARD IMPORTS
# =============================================================================
# Source.Python Imports
#   Engines
from _engines._sound import Channel
from _engines._sound import VOL_NORM
from _engines._sound import ATTN_NONE
from _engines._sound import ATTN_NORM
from _engines._sound import ATTN_IDLE
from _engines._sound import ATTN_STATIC
from _engines._sound import ATTN_RICOCHET
from _engines._sound import ATTN_GUNFIRE
from _engines._sound import MAX_ATTENUATION
from _engines._sound import SoundFlags
from _engines._sound import Pitch
from _engines._sound import SOUND_FROM_LOCAL_PLAYER
from _engines._sound import SOUND_FROM_WORLD
from _engines._sound import engine_sound


# =============================================================================
# >> ALL DECLARATION
# =============================================================================
__all__ = ('Attenuation',
           'Channel',
           'Pitch',
           'SOUND_FROM_LOCAL_PLAYER',
           'SOUND_FROM_WORLD',
           'Sound',
           'SoundFlags',
           'StreamSound',
           'VOL_NORM',
           'engine_sound',
           )


# =============================================================================
# >> GLOBAL VARIABLES
# =============================================================================
# Get the sp.engines.sound logger
engines_sound_logger = engines_logger.sound


# =============================================================================
# >> ENUMERATORS
# =============================================================================
[docs]class Attenuation(float, Enum): """Attenuation values wrapper enumerator.""" NONE = ATTN_NONE NORMAL = ATTN_NORM IDLE = ATTN_IDLE STATIC = ATTN_STATIC RICOCHET = ATTN_RICOCHET GUNFIRE = ATTN_GUNFIRE MAXIMUM = MAX_ATTENUATION
# ============================================================================= # >> CLASSES # ============================================================================= class _BaseSound(WeakAutoUnload): """Base class for sound classes.""" # Set the base _downloads attribute to know whether # or not the sample was added to the downloadables _downloads = None def __init__( self, sample, index=SOUND_FROM_WORLD, volume=VOL_NORM, attenuation=Attenuation.NONE, channel=Channel.AUTO, flags=SoundFlags.NO_FLAGS, pitch=Pitch.NORMAL, origin=NULL_VECTOR, direction=NULL_VECTOR, origins=(), update_positions=True, sound_time=0.0, speaker_entity=INVALID_ENTITY_INDEX, download=False): """Store all the given attributes and set the module for unloading. :param str sample: Path to the sound file. :param int index: An entity index to play the sound from. Use :data:`SOUND_FROM_WORLD` if you want to play the sound from the world. :param float volume: Volume to play the sound with. :param Attenuation attenuation: Define how the sound attenuates. :param Channel channel: Channel to use when playing the sound. :param SoundFlags flags: :param Pitch pitch: Pitch of the sound. :param Vector origin: Location to play the sound from. :param Vector direction: Direction to play the sound. :param tuple origins: Locations to play the sound from. :param bool update_positions: :param float sound_time: :param int speaker_entity: :param bool download: If ``True`` the sound file will be added to the ``downloadables`` list. """ # Set sample as a private attribute, since it should never change # Added replacing \ with / in paths for comformity self._sample = sample.replace('\\', '/') self._duration = None # Set all the base attributes self.index = index self.volume = volume self.attenuation = attenuation self.channel = channel self.flags = flags self.pitch = pitch self.origin = origin self.direction = direction self.origins = origins self.update_positions = update_positions self.sound_time = sound_time self.speaker_entity = speaker_entity # Should the sample be downloaded by clients? if download: # Add the sample to Downloadables self._downloads = Downloadables() self._downloads.add(self.relative_path) def play(self, *recipients): """Play the sound. :param recipients: Players who will hear the sound. """ # Done here to fix a cyclic import... from filters.recipients import RecipientFilter # Get the recipients to play the sound to recipients = RecipientFilter(*recipients) # Is the sound precached? if not self.is_precached: # Precache the sound self.precache() # Play the sound self._play(recipients) def stop(self, index=None, channel=None): """Stop a sound from being played.""" # Was an index passed in? if index is None: # Use the instance's index index = self.index # Was a channel passed in? if channel is None: # Use the instance's index channel = self.channel # Stop the sound self._stop(index, channel) def _play(self, recipients): """Play the sound (internal).""" raise NotImplementedError def _stop(self, index, channel): """Stop a sound from being played (internal).""" raise NotImplementedError def precache(self): """Precache the sample.""" raise NotImplementedError @property def is_precached(self): """Return whether or not the sample is precached.""" raise NotImplementedError @property def sample(self): """Return the filename of the Sound instance. :rtype: str """ return self._sample @property def extension(self): """Return the type of sound. :rtype: str """ return self.full_path.ext[1:] @property def full_path(self): """Return the full path to the file. :rtype: path.Path """ return GAME_PATH / 'sound' / self.sample @property def relative_path(self): """Return the relative path to the file. :rtype: str """ return 'sound' + os.sep + self.sample @property def duration(self): """Return the duration of the sample. :rtype: float """ if self._duration is not None: return self._duration with closing(SourceFile.open(self.relative_path, 'rb')) as f: if self.extension == 'ogg': value = oggvorbis.Open(f).info.length elif self.extension == 'mp3': value = mp3.Open(f).info.length elif self.extension == 'wav': with closing(wave.open(f)) as open_file: value = open_file.getnframes() / open_file.getframerate() else: raise NotImplementedError( 'Sound extension "{extension}" is not supported.'.format( extension=self.extension, ) ) self._duration = value return value def _unload_instance(self): """Remove the sample from the downloads list.""" if self._downloads is not None: self._downloads._unload_instance()
[docs]class Sound(_BaseSound): """Class used to interact with precached sounds. .. note:: On some engines (e.g. CS:GO) server is unable to precache the sound, thus the sound won't be played. StreamSound is recommended in that case. However, sounds located in sound/music/ directory are always streamed on those engines, and this class will be able to play them. """ def _play(self, recipients): """Play the sound (internal).""" engine_sound.emit_sound( recipients, self.index, self.channel, self.sample, self.volume, self.attenuation, self.flags, self.pitch, self.origin, self.direction, self.origins, self.update_positions, self.sound_time, self.speaker_entity) def _stop(self, index, channel): """Stop a sound from being played (internal).""" engine_sound.stop_sound(index, channel, self.sample)
[docs] def precache(self): """Precache the sample.""" engine_sound.precache_sound(self.sample)
@property def is_precached(self): """Return whether or not the sample is precached. :rtype: bool """ # We can't use engine_sound.is_sound_precached here because it always # returns True. return self.sample in string_tables.soundprecache
[docs]class StreamSound(_BaseSound): """Class used to interact with streamed sounds. .. note:: This class is a recommended choice on some engines (e.g. CS:GO), however, it's unable to play *.wav-files. .. note:: On some engines (e.g. CS:GO) files that are located in sound/music/ directory are already streamed, so simple Sound class can be used instead. """ @property def _stream_sample(self): """Return the streamed sample path of the Sound instance.""" return "*/{}".format(self.sample) def _play(self, recipients): """Play the sound (internal).""" engine_sound.emit_sound( recipients, self.index, self.channel, self._stream_sample, self.volume, self.attenuation, self.flags, self.pitch, self.origin, self.direction, self.origins, self.update_positions, self.sound_time, self.speaker_entity) def _stop(self, index, channel): """Stop a sound from being played (internal).""" engine_sound.stop_sound(index, channel, self._stream_sample)
[docs] def precache(self): """Precache the sample.""" string_tables.soundprecache.add_string( self._stream_sample, self._stream_sample)
@property def is_precached(self): """Return whether or not the sample is precached. :rtype: bool """ # We can't use engine_sound.is_sound_precached here because it always # returns True. return self._stream_sample in string_tables.soundprecache