heroes-renaissance/pyglet/media/drivers/openal/__init__.py
2008-11-23 21:07:47 +01:00

360 lines
12 KiB
Python

# ----------------------------------------------------------------------------
# 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 2270 2008-09-21 08:01:58Z Alex.Holkner $
import ctypes
import sys
import time
from pyglet.media import AudioPlayer, Listener, MediaException
from pyglet.media.drivers.openal import lib_openal as al
from pyglet.media.drivers.openal import lib_alc as alc
class OpenALException(MediaException):
pass
def _split_nul_strings(s):
# NUL-separated list of strings, double-NUL-terminated.
nul = False
i = 0
while True:
if s[i] == '\0':
if nul:
break
else:
nul = True
else:
nul = False
i += 1
s = s[:i - 1]
return s.split('\0')
def get_version():
major = alc.ALCint()
minor = alc.ALCint()
alc.alcGetIntegerv(_device, alc.ALC_MAJOR_VERSION,
ctypes.sizeof(major), major)
alc.alcGetIntegerv(_device, alc.ALC_MINOR_VERSION,
ctypes.sizeof(minor), minor)
return major.value, minor.value
def have_version(major, minor):
return (major, minor) <= get_version()
def get_extensions():
extensions = alc.alcGetString(_device, alc.ALC_EXTENSIONS)
# Check for null pointer
if not ctypes.cast(extensions, ctypes.c_void_p).value:
return []
if sys.platform == 'darwin':
return ctypes.cast(extensions, ctypes.c_char_p).value.split(' ')
else:
return _split_nul_strings(extensions)
def have_extension(extension):
return extension in get_extensions()
format_map = {
(1, 8): al.AL_FORMAT_MONO8,
(1, 16): al.AL_FORMAT_MONO16,
(2, 8): al.AL_FORMAT_STEREO8,
(2, 16): al.AL_FORMAT_STEREO16,
}
class OpenALAudioPlayer(AudioPlayer):
#: Seconds ahead to buffer audio. Keep small for low latency, but large
#: enough to avoid underruns. (0.05 is the minimum for my 2.2 GHz Linux)
_update_buffer_time = 0.2
#: Minimum size of an OpenAL buffer worth bothering with
_min_buffer_size = 512
#: Maximum size of an OpenAL buffer, in bytes. TODO: use OpenAL maximum
_max_buffer_size = 65536
UPDATE_PERIOD = 0.05
def __init__(self, audio_format):
super(OpenALAudioPlayer, self).__init__(audio_format)
try:
self._al_format = format_map[(audio_format.channels,
audio_format.sample_size)]
except KeyError:
raise OpenALException('Unsupported audio format.')
self._al_source = al.ALuint()
al.alGenSources(1, self._al_source)
# Seconds of audio currently queued not processed (estimate)
self._buffered_time = 0.0
# Seconds of audio into current (head) buffer
self._current_buffer_time = 0.0
# List of (timestamp, duration) corresponding to currently queued AL
# buffers
self._timestamps = []
# OpenAL 1.0 timestamp interpolation
self._timestamp_system_time = 0.0
# Desired play state (True even if stopped due to underrun)
self._playing = False
# Timestamp when paused
self._pause_timestamp = 0.0
self._eos_count = 0
def __del__(self):
try:
al.alDeleteSources(1, self._al_source)
except:
pass
def get_write_size(self):
t = self._buffered_time - self._current_buffer_time
size = int(max(0, self._update_buffer_time - t) * \
self.audio_format.bytes_per_second)
if size < self._min_buffer_size:
size = 0
return size
def write(self, audio_data):
buffer = al.ALuint()
al.alGenBuffers(1, buffer)
al.alBufferData(buffer,
self._al_format,
audio_data.data,
audio_data.length,
self.audio_format.sample_rate)
al.alSourceQueueBuffers(self._al_source, 1, ctypes.byref(buffer))
self._buffered_time += audio_data.duration
self._timestamps.append((audio_data.timestamp, audio_data.duration))
audio_data.consume(audio_data.length, self.audio_format)
def write_eos(self):
if self._timestamps:
self._timestamps.append((None, None))
def write_end(self):
pass
def play(self):
if self._playing:
return
self._playing = True
self._al_play()
if not _have_1_1:
self._timestamp_system_time = time.time()
def _al_play(self):
if not self._timestamps:
return
state = al.ALint()
al.alGetSourcei(self._al_source, al.AL_SOURCE_STATE, state)
if state.value != al.AL_PLAYING:
al.alSourcePlay(self._al_source)
def stop(self):
if not self._playing:
return
self._pause_timestamp = self.get_time()
al.alSourcePause(self._al_source)
self._playing = False
def clear(self):
al.alSourceStop(self._al_source)
self._playing = False
processed = al.ALint()
al.alGetSourcei(self._al_source, al.AL_BUFFERS_PROCESSED, processed)
if processed.value:
buffers = (al.ALuint * processed.value)()
al.alSourceUnqueueBuffers(self._al_source, len(buffers), buffers)
al.alDeleteBuffers(len(buffers), buffers)
self._pause_timestamp = 0.0
self._buffered_time = 0.0
self._current_buffer_time = 0.0
self._timestamps = []
def pump(self):
# Release spent buffers
processed = al.ALint()
al.alGetSourcei(self._al_source, al.AL_BUFFERS_PROCESSED, processed)
processed = processed.value
if processed:
buffers = (al.ALuint * processed)()
al.alSourceUnqueueBuffers(self._al_source, len(buffers), buffers)
al.alDeleteBuffers(len(buffers), buffers)
# Pop timestamps and check for eos markers
try:
while processed:
if not _have_1_1:
self._timestamp_system_time = time.time()
_, duration = self._timestamps.pop(0)
self._buffered_time -= duration
while self._timestamps[0][0] is None:
self._eos_count += 1
self._timestamps.pop(0)
processed -= 1
except IndexError:
pass
if _have_1_1:
samples = al.ALint()
al.alGetSourcei(self._al_source, al.AL_SAMPLE_OFFSET, samples)
self._current_buffer_time = samples.value / \
float(self.audio_format.sample_rate)
else:
# Interpolate system time past buffer timestamp
self._current_buffer_time = time.time() - \
self._timestamp_system_time
# Check for underrun
if self._playing:
state = al.ALint()
al.alGetSourcei(self._al_source, al.AL_SOURCE_STATE, state)
if state.value != al.AL_PLAYING:
al.alSourcePlay(self._al_source)
return True # underrun notification
def get_time(self):
state = al.ALint()
al.alGetSourcei(self._al_source, al.AL_SOURCE_STATE, state)
if not self._playing:
return self._pause_timestamp
if not self._timestamps:
return self._pause_timestamp
ts, _ = self._timestamps[0]
return ts + self._current_buffer_time
def clear_eos(self):
while self._eos_count > 0:
self._eos_count -= 1
return True
return False
def set_volume(self, volume):
al.alSourcef(self._al_source, al.AL_GAIN, max(0, volume))
def set_position(self, position):
x, y, z = position
al.alSource3f(self._al_source, al.AL_POSITION, x, y, z)
def set_min_distance(self, min_distance):
al.alSourcef(self._al_source, al.AL_REFERENCE_DISTANCE, min_distance)
def set_max_distance(self, max_distance):
al.alSourcef(self._al_source, al.AL_MAX_DISTANCE, max_distance)
def set_pitch(self, pitch):
al.alSourcef(self._al_source, al.AL_PITCH, max(0, pitch))
def set_cone_orientation(self, cone_orientation):
x, y, z = cone_orientation
al.alSource3f(self._al_source, al.AL_DIRECTION, x, y, z)
def set_cone_inner_angle(self, cone_inner_angle):
al.alSourcef(self._al_source, al.AL_CONE_INNER_ANGLE, cone_inner_angle)
def set_cone_outer_angle(self, cone_outer_angle):
al.alSourcef(self._al_source, al.AL_CONE_OUTER_ANGLE, cone_outer_angle)
def set_cone_outer_gain(self, cone_outer_gain):
al.alSourcef(self._al_source, al.AL_CONE_OUTER_GAIN, cone_outer_gain)
class OpenALListener(Listener):
def _set_volume(self, volume):
al.alListenerf(al.AL_GAIN, volume)
self._volume = volume
def _set_position(self, position):
x, y, z = position
al.alListener3f(al.AL_POSITION, x, y, z)
self._position = position
def _set_forward_orientation(self, orientation):
val = (al.ALfloat * 6)(*(orientation + self._up_orientation))
al.alListenerfv(al.AL_ORIENTATION, val)
self._forward_orientation = orientation
def _set_up_orientation(self, orientation):
val = (al.ALfloat * 6)(*(self._forward_orientation + orientation))
al.alListenerfv(al.AL_ORIENTATION, val)
self._up_orientation = orientation
_device = None
_have_1_1 = False
def driver_init(device_name = None):
global _device
global _have_1_1
# TODO devices must be enumerated on Windows, otherwise 1.0 context is
# returned.
_device = alc.alcOpenDevice(device_name)
if not _device:
raise OpenALException('No OpenAL device.')
alcontext = alc.alcCreateContext(_device, None)
alc.alcMakeContextCurrent(alcontext)
if have_version(1, 1):
# Good version info to cache
_have_1_1 = True
# See issue #163.
import sys
if sys.platform in ('win32', 'cygwin'):
from pyglet import clock
clock.Clock._force_sleep = True
driver_listener = OpenALListener()
driver_audio_player_class = OpenALAudioPlayer