heroes-renaissance/pyglet/media/__init__.py

1405 lines
45 KiB
Python
Raw Permalink Normal View History

2008-11-23 20:07:47 +00:00
# ----------------------------------------------------------------------------
# pyglet
# Copyright (c) 2006-2008 Alex Holkner
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
# * Neither the name of pyglet nor the names of its
# contributors may be used to endorse or promote products
# derived from this software without specific prior written
# permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# ----------------------------------------------------------------------------
# $Id: __init__.py 2154 2008-08-05 22:53:37Z Alex.Holkner $
'''Audio and video playback.
pyglet can play WAV files, and if AVbin is installed, many other audio and
video formats.
Playback is handled by the `Player` class, which reads raw data from `Source`
objects and provides methods for pausing, seeking, adjusting the volume, and
so on. The `Player` class implements a the best available audio device
(currently, only OpenAL is supported)::
player = Player()
A `Source` is used to decode arbitrary audio and video files. It is
associated with a single player by "queuing" it::
source = load('background_music.mp3')
player.queue(source)
Use the `Player` to control playback.
If the source contains video, the `Source.video_format` attribute will be
non-None, and the `Player.texture` attribute will contain the current video
image synchronised to the audio.
Decoding sounds can be processor-intensive and may introduce latency,
particularly for short sounds that must be played quickly, such as bullets or
explosions. You can force such sounds to be decoded and retained in memory
rather than streamed from disk by wrapping the source in a `StaticSource`::
bullet_sound = StaticSource(load('bullet.wav'))
The other advantage of a `StaticSource` is that it can be queued on any number
of players, and so played many times simultaneously.
'''
__docformat__ = 'restructuredtext'
__version__ = '$Id: __init__.py 2154 2008-08-05 22:53:37Z Alex.Holkner $'
import ctypes
import sys
import time
import StringIO
import pyglet
from pyglet import clock
from pyglet import event
_debug_media = pyglet.options['debug_media']
class MediaException(Exception):
pass
class MediaFormatException(MediaException):
pass
class CannotSeekException(MediaException):
pass
class AudioFormat(object):
'''Audio details.
An instance of this class is provided by sources with audio tracks. You
should not modify the fields, as they are used internally to describe the
format of data provided by the source.
:Ivariables:
`channels` : int
The number of channels: 1 for mono or 2 for stereo (pyglet does
not yet support surround-sound sources).
`sample_size` : int
Bits per sample; only 8 or 16 are supported.
`sample_rate` : int
Samples per second (in Hertz).
'''
def __init__(self, channels, sample_size, sample_rate):
self.channels = channels
self.sample_size = sample_size
self.sample_rate = sample_rate
# Convenience
self.bytes_per_sample = (sample_size >> 3) * channels
self.bytes_per_second = self.bytes_per_sample * sample_rate
def __eq__(self, other):
return (self.channels == other.channels and
self.sample_size == other.sample_size and
self.sample_rate == other.sample_rate)
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return '%s(channels=%d, sample_size=%d, sample_rate=%d)' % (
self.__class__.__name__, self.channels, self.sample_size,
self.sample_rate)
class VideoFormat(object):
'''Video details.
An instance of this class is provided by sources with a video track. You
should not modify the fields.
Note that the sample aspect has no relation to the aspect ratio of the
video image. For example, a video image of 640x480 with sample aspect 2.0
should be displayed at 1280x480. It is the responsibility of the
application to perform this scaling.
:Ivariables:
`width` : int
Width of video image, in pixels.
`height` : int
Height of video image, in pixels.
`sample_aspect` : float
Aspect ratio (width over height) of a single video pixel.
'''
def __init__(self, width, height, sample_aspect=1.0):
self.width = width
self.height = height
self.sample_aspect = sample_aspect
class AudioData(object):
'''A single packet of audio data.
This class is used internally by pyglet.
:Ivariables:
`data` : str or ctypes array or pointer
Sample data.
`length` : int
Size of sample data, in bytes.
`timestamp` : float
Time of the first sample, in seconds.
`duration` : float
Total data duration, in seconds.
'''
def __init__(self, data, length, timestamp, duration):
self.data = data
self.length = length
self.timestamp = timestamp
self.duration = duration
def consume(self, bytes, audio_format):
'''Remove some data from beginning of packet.'''
if bytes == self.length:
self.data = None
self.length = 0
self.timestamp += self.duration
self.duration = 0.
return
elif bytes == 0:
return
if not isinstance(self.data, str):
# XXX Create a string buffer for the whole packet then
# chop it up. Could do some pointer arith here and
# save a bit of data pushing, but my guess is this is
# faster than fudging aruond with ctypes (and easier).
data = ctypes.create_string_buffer(self.length)
ctypes.memmove(data, self.data, self.length)
self.data = data
self.data = self.data[bytes:]
self.length -= bytes
self.duration -= bytes / float(audio_format.bytes_per_second)
self.timestamp += bytes / float(audio_format.bytes_per_second)
def get_string_data(self):
'''Return data as a string.'''
if type(self.data) is str:
return self.data
buf = ctypes.create_string_buffer(self.length)
ctypes.memmove(buf, self.data, self.length)
return buf.raw
class AudioPlayer(object):
'''Abstract low-level interface for playing audio.
AudioPlayer has no knowledge of sources or eos behaviour. Once
created, its audio format cannot be modified. The player will attempt
to recover automatically from a buffer underrun (but this is not
guaranteed).
Applications should not use this class directly, but instead use `Player`.
:Ivariables:
`audio_format` : `AudioFormat`
The player's audio format (read-only).
'''
UPDATE_PERIOD = 0.15
def __init__(self, audio_format):
'''Create a new audio player for the given audio format.
:Parameters:
`audio_format` : `AudioFormat`
Audio format parameters.
'''
self.audio_format = audio_format
def get_write_size(self):
'''Return the maximum number of bytes that can be written.
This is used as a hint for preparing data for `write`, not as a strict
contract.
:rtype: int
'''
raise NotImplementedError('abstract')
def write(self, audio_data):
'''Write audio_data to the stream.
This method calls `AudioData.consume` to remove data actually written.
:Parameters:
`audio_data` : `AudioData`
Data to write.
'''
raise NotImplementedError('abstract')
def write_eos(self):
'''Write an EOS marker to the stream at the current write point.'''
raise NotImplementedError('abstract')
def write_end(self):
'''Mark that there will be no more audio data past the current write
point.
This does not produce an EOS, but is required to prevent data
underrun artifacts.
'''
raise NotImplementedError('abstract')
def play(self):
'''Begin playback.'''
raise NotImplementedError('abstract')
def stop(self):
'''Stop playback.'''
raise NotImplementedError('abstract')
def clear(self):
'''Clear all buffered data and prepare for replacement data.
The player should be stopped before calling this method.
'''
raise NotImplementedError('abstract')
def pump(self):
'''Called once per loop iteration before checking for eos
triggers.'''
raise NotImplementedError('abstract')
def get_time(self):
'''Return best guess of current playback time. The time is relative
to the timestamps provided in the data supplied to `write`. The time
is meaningless unless proper care has been taken to clear EOS markers.
:rtype: float
:return: current play cursor time, in seconds.
'''
raise NotImplementedError('abstract')
def clear_eos(self):
'''Check if an EOS marker has been passed, and clear it.
This method should be called repeatedly to clear all pending EOS
markers.
:rtype: bool
:return: True if an EOS marker was cleared.
'''
raise NotImplementedError('abstract')
def set_volume(self, volume):
'''See `Player.volume`.'''
pass
def set_position(self, position):
'''See `Player.position`.'''
pass
def set_min_distance(self, min_distance):
'''See `Player.min_distance`.'''
pass
def set_max_distance(self, max_distance):
'''See `Player.max_distance`.'''
pass
def set_pitch(self, pitch):
'''See `Player.pitch`.'''
pass
def set_cone_orientation(self, cone_orientation):
'''See `Player.cone_orientation`.'''
pass
def set_cone_inner_angle(self, cone_inner_angle):
'''See `Player.cone_inner_angle`.'''
pass
def set_cone_outer_angle(self, cone_outer_angle):
'''See `Player.cone_outer_angle`.'''
pass
def set_cone_outer_gain(self, cone_outer_gain):
'''See `Player.cone_outer_gain`.'''
pass
class Source(object):
'''An audio and/or video source.
:Ivariables:
`audio_format` : `AudioFormat`
Format of the audio in this source, or None if the source is
silent.
`video_format` : `VideoFormat`
Format of the video in this source, or None if there is no
video.
'''
_duration = None
audio_format = None
video_format = None
def _get_duration(self):
return self._duration
duration = property(lambda self: self._get_duration(),
doc='''The length of the source, in seconds.
Not all source durations can be determined; in this case the value
is None.
Read-only.
:type: float
''')
def play(self):
'''Play the source.
This is a convenience method which creates a ManagedSoundPlayer for
this source and plays it immediately.
:rtype: `ManagedSoundPlayer`
'''
player = ManagedSoundPlayer()
player.queue(self)
player.play()
return player
def get_animation(self):
'''Import all video frames into memory as an `Animation`.
An empty animation will be returned if the source has no video.
Otherwise, the animation will contain all unplayed video frames (the
entire source, if it has not been queued on a player). After creating
the animation, the source will be at EOS.
This method is unsuitable for videos running longer than a
few seconds.
:since: pyglet 1.1
:rtype: `pyglet.image.Animation`
'''
from pyglet.image import Animation, AnimationFrame
if not self.video_format:
return Animation([])
else:
# Create a dummy player for the source to push its textures onto.
frames = []
last_ts = 0
next_ts = self.get_next_video_timestamp()
while next_ts is not None:
image = self.get_next_video_frame()
assert image is not None
delay = next_ts - last_ts
frames.append(AnimationFrame(image, delay))
last_ts = next_ts
next_ts = self.get_next_video_timestamp()
return Animation(frames)
def get_next_video_timestamp(self):
'''Get the timestamp of the next video frame.
:since: pyglet 1.1
:rtype: float
:return: The next timestamp, or ``None`` if there are no more video
frames.
'''
pass
def get_next_video_frame(self):
'''Get the next video frame.
Video frames may share memory: the previous frame may be invalidated
or corrupted when this method is called unless the application has
made a copy of it.
:since: pyglet 1.1
:rtype: `pyglet.image.AbstractImage`
:return: The next video frame image, or ``None`` if there are no more
video frames.
'''
pass
# Internal methods that Players call on the source:
def _play(self):
'''Begin decoding in real-time.'''
pass
def _pause(self):
'''Pause decoding, but remain prerolled.'''
pass
def _stop(self):
'''Stop forever and clean up.'''
pass
def _seek(self, timestamp):
'''Seek to given timestamp.'''
raise CannotSeekException()
def _get_queue_source(self):
'''Return the `Source` to be used as the queue source for a player.
Default implementation returns self.'''
return self
def _get_audio_data(self, bytes):
'''Get next packet of audio data.
:Parameters:
`bytes` : int
Maximum number of bytes of data to return.
:rtype: `AudioData`
:return: Next packet of audio data, or None if there is no (more)
data.
'''
return None
def _init_texture(self, player):
'''Create the player's texture.'''
pass
def _update_texture(self, player, timestamp):
'''Update the texture on player.'''
pass
def _release_texture(self, player):
'''Release the player's texture.'''
pass
class StreamingSource(Source):
'''A source that is decoded as it is being played, and can only be
queued once.
'''
_is_queued = False
is_queued = property(lambda self: self._is_queued,
doc='''Determine if this source has been queued
on a `Player` yet.
Read-only.
:type: bool
''')
def _get_queue_source(self):
'''Return the `Source` to be used as the queue source for a player.
Default implementation returns self.'''
if self._is_queued:
raise MediaException('This source is already queued on a player.')
self._is_queued = True
return self
class StaticSource(Source):
'''A source that has been completely decoded in memory. This source can
be queued onto multiple players any number of times.
'''
def __init__(self, source):
'''Construct a `StaticSource` for the data in `source`.
:Parameters:
`source` : `Source`
The source to read and decode audio and video data from.
'''
source = source._get_queue_source()
if source.video_format:
raise NotImplementedException(
'Static sources not supported for video yet.')
self.audio_format = source.audio_format
if not self.audio_format:
return
# TODO enable time-insensitive playback
source._play()
# Arbitrary: number of bytes to request at a time.
buffer_size = 1 << 20 # 1 MB
# Naive implementation. Driver-specific implementations may override
# to load static audio data into device (or at least driver) memory.
data = StringIO.StringIO()
while True:
audio_data = source._get_audio_data(buffer_size)
if not audio_data:
break
data.write(audio_data.get_string_data())
self._data = data.getvalue()
def _get_queue_source(self):
return StaticMemorySource(self._data, self.audio_format)
def _get_audio_data(self, bytes):
raise RuntimeError('StaticSource cannot be queued.')
class StaticMemorySource(StaticSource):
'''Helper class for default implementation of `StaticSource`. Do not use
directly.'''
def __init__(self, data, audio_format):
'''Construct a memory source over the given data buffer.
'''
self._file = StringIO.StringIO(data)
self._max_offset = len(data)
self.audio_format = audio_format
self._duration = len(data) / float(audio_format.bytes_per_second)
def _seek(self, timestamp):
offset = int(timestamp * self.audio_format.bytes_per_second)
# Align to sample
if self.audio_format.bytes_per_sample == 2:
offset &= 0xfffffffe
elif self.audio_format.bytes_per_sample == 4:
offset &= 0xfffffffc
self._file.seek(offset)
def _get_audio_data(self, bytes):
offset = self._file.tell()
timestamp = float(offset) / self.audio_format.bytes_per_second
# Align to sample size
if self.audio_format.bytes_per_sample == 2:
bytes &= 0xfffffffe
elif self.audio_format.bytes_per_sample == 4:
bytes &= 0xfffffffc
data = self._file.read(bytes)
if not len(data):
return None
duration = float(len(data)) / self.audio_format.bytes_per_second
return AudioData(data, len(data), timestamp, duration)
class Player(event.EventDispatcher):
'''A sound and/or video player.
Queue sources on this player to play them.
'''
#: The player will pause when it reaches the end of the stream.
EOS_PAUSE = 'pause'
#: The player will loop the current stream continuosly.
EOS_LOOP = 'loop'
#: The player will move on to the next queued stream when it reaches the
#: end of the current source. If there is no source queued, the player
#: will pause.
EOS_NEXT = 'next'
#: The player will stop entirely; valid only for ManagedSoundPlayer.
EOS_STOP = 'stop'
# Source and queuing attributes
_source_read_index = 0
_eos_action = EOS_NEXT
_playing = False
# If True and _playing is False, user is currently seeking while paused;
# should refrain from filling the audio buffer.
_pause_seek = False
# Override audio timestamp for seeking and silent video
_timestamp = None
# Used to track timestamp for silent sources
_last_system_time = 0.
# Audio attributes
_audio = None
_audio_finished = False
_next_audio_data = None
# Video attributes
_texture = None
# Spacialisation attributes, preserved between audio players
_volume = 1.0
_min_distance = 1.0
_max_distance = 100000000.
_position = (0, 0, 0)
_pitch = 1.0
_cone_orientation = (0, 0, 1)
_cone_inner_angle = 360.
_cone_outer_angle = 360.
_cone_outer_gain = 1.
def __init__(self):
self._sources = []
def _create_audio(self):
'''Create _audio for sources[0].
Reuses existing _audio if it exists and is compatible.
'''
if not self._sources:
return
source = self._sources[0]
if not source.audio_format:
self._audio = None
return
if self._audio:
self._audio_finished = False
if self._audio.audio_format == source.audio_format:
return
else:
self._audio = None
self._audio = audio_player_class(source.audio_format)
self._audio.set_volume(self._volume)
self._audio.set_min_distance(self._min_distance)
self._audio.set_max_distance(self._max_distance)
self._audio.set_position(self._position)
self._audio.set_pitch(self._pitch)
self._audio.set_cone_orientation(self._cone_orientation)
self._audio.set_cone_inner_angle(self._cone_inner_angle)
self._audio.set_cone_outer_angle(self._cone_outer_angle)
self._audio.set_cone_outer_gain(self._cone_outer_gain)
def _fill_audio(self):
'''Ensure _audio is full.'''
if not self._audio or self._audio_finished:
return
write_size = self._audio.get_write_size()
if not write_size:
return
for audio_data, audio_format in self._get_audio_data(write_size):
if audio_data == 'eos':
self._audio.write_eos()
continue
elif audio_data == 'end':
self._audio.write_end()
self._audio_finished = True
return
if audio_format != self._audio.audio_format:
self._next_audio_data = audio_format, audio_data
return
length = audio_data.length
self._audio.write(audio_data)
if audio_data.length:
self._next_audio_data = audio_format, audio_data
return
write_size -= length
if write_size <= 0:
return
def _get_audio_data(self, bytes):
'''Yields pairs of (audio_data, audio_format).'''
if self._next_audio_data:
audio_format, audio_data = self._next_audio_data
self._next_audio_data = None
bytes -= audio_data.length
yield audio_data, audio_format
try:
source = self._sources[self._source_read_index]
except IndexError:
source = None
while source and bytes > 4: # bytes > 4 compensates for alignment loss
audio_data = source._get_audio_data(bytes)
if audio_data:
bytes -= audio_data.length
yield audio_data, source.audio_format
else:
yield 'eos', source.audio_format
if self._eos_action == self.EOS_NEXT:
self._source_read_index += 1
try:
source = self._sources[self._source_read_index]
source._play()
except IndexError:
source = None
elif self._eos_action == self.EOS_LOOP:
source._seek(0)
elif self._eos_action == self.EOS_PAUSE:
source = None
elif self._eos_action == self.EOS_STOP:
source = None
else:
assert False, 'Invalid eos_action'
source = None
if not source:
yield 'end', None
def _update_schedule(self):
clock.unschedule(self.dispatch_events)
if self._playing and self._sources:
interval = 1000.
if self._sources[0].video_format:
interval = min(interval, 1/24.)
if self._audio:
interval = min(interval, self._audio.UPDATE_PERIOD)
clock.schedule_interval_soft(self.dispatch_events, interval)
def queue(self, source):
'''Queue the source on this player.
If the player has no source, the player will be paused immediately
on this source.
:Parameters:
`source` : Source
The source to queue.
'''
self._sources.append(source._get_queue_source())
if len(self._sources) == 1:
self._source_read_index = 0
self._begin_source()
def play(self):
'''Begin playing the current source.
This has no effect if the player is already playing.
'''
self._playing = True
self._pause_seek = False
if self._audio:
self._timestamp = None
self._audio.play()
else:
self._last_system_time = time.time()
self.dispatch_events()
self._update_schedule()
def pause(self):
'''Pause playback of the current source.
This has no effect if the player is already paused.
'''
self._playing = False
self._pause_seek = False
if self._audio:
self._audio.stop()
self._update_schedule()
def seek(self, timestamp):
'''Seek for playback to the indicated timestamp in seconds on the
current source. If the timestamp is outside the duration of the
source, it will be clamped to the end.
:Parameters:
`timestamp` : float
Timestamp to seek to.
'''
if not self._sources:
return
if not self._playing:
self._pause_seek = True
self._audio_finished = False
source = self._sources[0]
self._source_read_index = 0
self._next_audio_data = None
source._seek(timestamp)
self._timestamp = timestamp
if self._audio:
self._audio.stop()
self._audio.clear()
else:
self._last_system_time = time.time()
self.dispatch_events()
def next(self):
'''Move immediately to the next queued source.
There may be a gap in playback while the audio buffer is refilled.
'''
if not self._sources:
return
if self._audio:
self._audio.stop()
self._audio.clear()
else:
self._last_system_time = time.time()
self._timestamp = 0.
self._next_source()
def _next_source(self):
if not self._sources:
self._update_schedule()
return
self._source_read_index = max(0, self._source_read_index - 1)
source = self._sources.pop(0)
source._release_texture(self)
source._stop()
self._begin_source()
def _begin_source(self):
if not self._sources:
return
source = self._sources[0]
source._init_texture(self)
self._create_audio()
self._fill_audio()
if not self._audio:
self._timestamp = 0.
if self._playing:
self.play()
self._update_schedule()
def _on_eos(self):
'''Internal method when EOS is encountered. Returns False if
dispatch_events should be immediately aborted.'''
if self._eos_action == self.EOS_NEXT:
self._next_source()
elif self._eos_action == self.EOS_PAUSE:
self._playing = False
self._timestamp = self._sources[0].duration
elif self._eos_action == self.EOS_STOP:
self.stop()
self._sources = []
return False
self.dispatch_event('on_eos')
return True
def dispatch_events(self, dt=None):
'''Dispatch any pending events and perform regular heartbeat functions
to maintain playback.
:Parameters:
`dt` : None
Ignored (for compatibility with `pyglet.clock.schedule`)
:deprecated: Since pyglet 1.1, Player objects schedule themselves on
the default clock automatically. Applications should not call
this method.
'''
if not self._sources:
return
if not self._pause_seek:
self._fill_audio()
if self._audio:
underrun = self._audio.pump()
while self._audio.clear_eos():
if not self._on_eos():
return
if underrun:
self._audio.UPDATE_PERIOD *= 0.75
self._audio.__class__.UPDATE_PERIOD *= 0.75
self._update_schedule()
if _debug_media:
print '%r underrun: reducing update period to %.2f' % \
(self._audio, self._audio.UPDATE_PERIOD)
else:
if self._playing:
t = time.time()
self._timestamp += t - self._last_system_time
self._last_system_time = t
while self._timestamp > self._sources[0].duration:
if not self._on_eos():
return
if self._eos_action == self.EOS_LOOP:
self._timestamp -= self._sources[0].duration
if self._texture:
self._sources[0]._update_texture(self, self._get_time())
def _get_time(self):
if self._timestamp is not None:
return self._timestamp
elif self._audio:
return self._audio.get_time()
time = property(lambda self: self._get_time(),
doc='''Retrieve the current playback time of the current
source.
The playback time is a float expressed in seconds, with 0.0 being
the beginning of the sound. The playback time returned represents
the time encoded in the source, and may not reflect actual time
passed due to pitch shifting or pausing.
Read-only.
:type: float
''')
def _get_source(self):
if self._sources:
return self._sources[0]
source = property(lambda self: self._get_source(),
doc='''Return the current source.
Read-only.
:type: Source
''')
def _set_eos_action(self, action):
self._eos_action = action
eos_action = property(lambda self: self._eos_action,
_set_eos_action,
doc='''Set the behaviour of the player when it
reaches the end of the current source.
This must be one of the constants `EOS_NEXT`, `EOS_PAUSE` or
`EOS_LOOP`.
:type: str
''')
playing = property(lambda self: self._playing,
doc='''Determine if the player state is playing.
The `playing` property is irrespective of whether or not there is
actually a source to play. If `playing` is True and a source is
queued, it will begin playing immediately. If `playing` is False,
it is implied that the player is paused. There is no other possible
state.
Read-only.
:type: bool
''')
def _set_volume(self, volume):
self._volume = volume
if self._audio:
self._audio.set_volume(volume)
volume = property(lambda self: self._volume,
lambda self, volume: self._set_volume(volume),
doc='''The volume level of sound playback.
The nominal level is 1.0, and 0.0 is silence.
The volume level is affected by the distance from the listener (if
positioned).
:type: float
''')
def _set_position(self, position):
self._position = position
if self._audio:
self._audio.set_position(position)
position = property(lambda self: self._position,
lambda self, position: self._set_position(position),
doc='''The position of the sound in 3D space.
The position is given as a tuple of floats (x, y, z). The unit
defaults to meters, but can be modified with the listener
properties.
:type: 3-tuple of float
''')
def _set_min_distance(self, min_distance):
self._min_distance = min_distance
if self._audio:
self._audio.set_min_distance(min_distance)
min_distance = property(lambda self: self._min_distance,
lambda self, v: self._set_min_distance(v),
doc='''The distance beyond which the sound volume
drops by half, and within which no attenuation is applied.
The minimum distance controls how quickly a sound is attenuated
as it moves away from the listener. The gain is clamped at the
nominal value within the min distance. By default the value is
1.0.
The unit defaults to meters, but can be modified with the listener
properties.
:type: float
''')
def _set_max_distance(self, max_distance):
self._max_distance = max_distance
if self._audio:
self._audio.set_max_distance(max_distance)
max_distance = property(lambda self: self._max_distance,
lambda self, v: self._set_max_distance(v),
doc='''The distance at which no further attenuation
is applied.
When the distance from the listener to the player is greater than
this value, attenuation is calculated as if the distance
were value. By default the maximum distance is infinity.
The unit defaults to meters, but can be modified with the listener
properties.
:type: float
''')
def _set_pitch(self, pitch):
self._pitch = pitch
if self._audio:
self._audio.set_pitch(pitch)
pitch = property(lambda self: self._pitch,
lambda self, pitch: self._set_pitch(pitch),
doc='''The pitch shift to apply to the sound.
The nominal pitch is 1.0. A pitch of 2.0 will sound one octave
higher, and play twice as fast. A pitch of 0.5 will sound one octave
lower, and play twice as slow. A pitch of 0.0 is not permitted.
:type: float
''')
def _set_cone_orientation(self, cone_orientation):
self._cone_orientation = cone_orientation
if self._audio:
self._audio.set_cone_orientation(cone_orientation)
cone_orientation = property(lambda self: self._cone_orientation,
lambda self, c: self._set_cone_orientation(c),
doc='''The direction of the sound in 3D space.
The direction is specified as a tuple of floats (x, y, z), and has no
unit. The default direction is (0, 0, -1). Directional effects are
only noticeable if the other cone properties are changed from their
default values.
:type: 3-tuple of float
''')
def _set_cone_inner_angle(self, cone_inner_angle):
self._cone_inner_angle = cone_inner_angle
if self._audio:
self._audio.set_cone_inner_angle(cone_inner_angle)
cone_inner_angle = property(lambda self: self._cone_inner_angle,
lambda self, a: self._set_cone_inner_angle(a),
doc='''The interior angle of the inner cone.
The angle is given in degrees, and defaults to 360. When the listener
is positioned within the volume defined by the inner cone, the sound
is played at normal gain (see `volume`).
:type: float
''')
def _set_cone_outer_angle(self, cone_outer_angle):
self._cone_outer_angle = cone_outer_angle
if self._audio:
self._audio.set_cone_outer_angle(cone_outer_angle)
cone_outer_angle = property(lambda self: self._cone_outer_angle,
lambda self, a: self._set_cone_outer_angle(a),
doc='''The interior angle of the outer cone.
The angle is given in degrees, and defaults to 360. When the listener
is positioned within the volume defined by the outer cone, but outside
the volume defined by the inner cone, the gain applied is a smooth
interpolation between `volume` and `cone_outer_gain`.
:type: float
''')
def _set_cone_outer_gain(self, cone_outer_gain):
self._cone_outer_gain = cone_outer_gain
if self._audio:
self._audio.set_cone_outer_gain(cone_outer_gain)
cone_outer_gain = property(lambda self: self._cone_outer_gain,
lambda self, g: self._set_cone_outer_gain(g),
doc='''The gain applied outside the cone.
When the listener is positioned outside the volume defined by the
outer cone, this gain is applied instead of `volume`.
:type: float
''')
def get_texture(self):
'''Get the texture for the current video frame.
You should call this method every time you display a frame
of video, as multiple textures might be used. The return value will
be `None` if there is no video in the current source.
:since: pyglet 1.1
:rtype: `pyglet.image.Texture`
'''
return self._texture
texture = property(lambda self: self._texture,
doc='''The video texture.
You should rerequest this property every time you display a frame
of video, as multiple textures might be used. This property will
be `None` if there is no video in the current source.
:deprecated: Use `get_texture`.
:type: `pyglet.image.Texture`
''')
if getattr(sys, 'is_epydoc', False):
def on_eos():
'''The player has reached the end of the current source.
This event is dispatched regardless of the EOS action. You
can alter the EOS action in this event handler, however playback
may stutter as the media device will not have enough time to
decode and buffer the new data in advance.
:event:
'''
Player.register_event_type('on_eos')
class ManagedSoundPlayer(Player):
'''A player which takes care of updating its own audio buffers.
This player will continue playing the sound until the sound is
finished, even if the application discards the player early.
Only one source can be queued on the player; the player will be
discarded when the source finishes.
'''
#: The only possible end of stream action for a managed player.
EOS_STOP = 'stop'
_eos_action = EOS_STOP
eos_action = property(lambda self: EOS_STOP,
doc='''The fixed eos_action is `EOS_STOP`,
in which the player is discarded as soon as the source has
finished.
Read-only.
:type: str
''')
def __init__(self):
super(ManagedSoundPlayer, self).__init__()
managed_players.append(self)
def stop(self):
self._timestamp = 0.
clock.unschedule(self.dispatch_events)
managed_players.remove(self)
class Listener(object):
'''The listener properties for positional audio.
You can obtain the singleton instance of this class as
`pyglet.media.listener`.
'''
_volume = 1.0
_position = (0, 0, 0)
_forward_orientation = (0, 0, -1)
_up_orientation = (0, 1, 0)
def _set_volume(self, volume):
raise NotImplementedError('abstract')
volume = property(lambda self: self._volume,
lambda self, volume: self._set_volume(volume),
doc='''The master volume for sound playback.
All sound volumes are multiplied by this master volume before being
played. A value of 0 will silence playback (but still consume
resources). The nominal volume is 1.0.
:type: float
''')
def _set_position(self, position):
raise NotImplementedError('abstract')
position = property(lambda self: self._position,
lambda self, position: self._set_position(position),
doc='''The position of the listener in 3D space.
The position is given as a tuple of floats (x, y, z). The unit
defaults to meters, but can be modified with the listener
properties.
:type: 3-tuple of float
''')
def _set_forward_orientation(self, orientation):
raise NotImplementedError('abstract')
forward_orientation = property(lambda self: self._forward_orientation,
lambda self, o: self._set_forward_orientation(o),
doc='''A vector giving the direction the
listener is facing.
The orientation is given as a tuple of floats (x, y, z), and has
no unit. The forward orientation should be orthagonal to the
up orientation.
:type: 3-tuple of float
''')
def _set_up_orientation(self, orientation):
raise NotImplementedError('abstract')
up_orientation = property(lambda self: self._up_orientation,
lambda self, o: self._set_up_orientation(o),
doc='''A vector giving the "up" orientation
of the listener.
The orientation is given as a tuple of floats (x, y, z), and has
no unit. The up orientation should be orthagonal to the
forward orientation.
:type: 3-tuple of float
''')
if getattr(sys, 'is_epydoc', False):
#: The singleton listener.
#:
#: :type: `Listener`
listener = Listener()
#: Indication of the presence of AVbin. When `have_avbin` is ``True``
#: pyglet will be able to play back compressed media streams such as
#: MP3, OGG and various video formats. If ``False`` only uncompressed
#: Wave files can be loaded.
#:
#: :type: bool
have_avbin = False
else:
# Find best available sound driver according to user preference
import pyglet
driver = None
for driver_name in pyglet.options['audio']:
try:
driver_name = 'pyglet.media.drivers.' + driver_name
__import__(driver_name)
driver = sys.modules[driver_name]
driver.driver_init()
break
except (ImportError, AttributeError, MediaException):
pass
if not driver:
raise ImportError('No suitable audio driver could be loaded.')
audio_player_class = driver.driver_audio_player_class
listener = driver.driver_listener
# Find best available source loader
have_avbin = False
try:
from pyglet.media import avbin
_source_class = avbin.AVbinSource
have_avbin = True
except ImportError:
from pyglet.media import riff
_source_class = riff.WaveSource
# Pretend to import some common audio drivers so that py2exe/py2app
# are fooled into packagin them.
if False:
import pyglet.media.drivers.silent
import pyglet.media.drivers.openal
import pyglet.media.drivers.directsound
import pyglet.media.drivers.alsa
def load(filename, file=None, streaming=True):
'''Load a source from a file.
Currently the `file` argument is not supported; media files must exist
as real paths.
:Parameters:
`filename` : str
Filename of the media file to load.
`file` : file-like object
Not yet supported.
`streaming` : bool
If False, a `StaticSource` will be returned; otherwise (default) a
`StreamingSource` is created.
:rtype: `Source`
'''
source = _source_class(filename, file)
if not streaming:
source = StaticSource(source)
return source
managed_players = []
def dispatch_events():
'''Process managed audio events.
You must call this function regularly (typically once per run loop
iteration) in order to keep audio buffers of managed players full.
:deprecated: Since pyglet 1.1, Player objects schedule themselves on
the default clock automatically. Applications should not call this
method.
'''
for player in managed_players:
player.dispatch_events()