360 lines
12 KiB
Python
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
|
|
|