# ---------------------------------------------------------------------------- # 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