1# ---------------------------------------------------------------------------- 2# pyglet 3# Copyright (c) 2006-2008 Alex Holkner 4# Copyright (c) 2008-2021 pyglet contributors 5# All rights reserved. 6# 7# Redistribution and use in source and binary forms, with or without 8# modification, are permitted provided that the following conditions 9# are met: 10# 11# * Redistributions of source code must retain the above copyright 12# notice, this list of conditions and the following disclaimer. 13# * Redistributions in binary form must reproduce the above copyright 14# notice, this list of conditions and the following disclaimer in 15# the documentation and/or other materials provided with the 16# distribution. 17# * Neither the name of pyglet nor the names of its 18# contributors may be used to endorse or promote products 19# derived from this software without specific prior written 20# permission. 21# 22# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 25# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 26# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 27# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 28# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 29# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 31# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 32# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 33# POSSIBILITY OF SUCH DAMAGE. 34# ---------------------------------------------------------------------------- 35 36import ctypes 37import weakref 38from collections import namedtuple 39 40from . import lib_openal as al 41from . import lib_alc as alc 42 43from pyglet.util import debug_print 44from pyglet.media.exceptions import MediaException 45 46_debug = debug_print('debug_media') 47 48 49class OpenALException(MediaException): 50 def __init__(self, message=None, error_code=None, error_string=None): 51 self.message = message 52 self.error_code = error_code 53 self.error_string = error_string 54 55 def __str__(self): 56 if self.error_code is None: 57 return f'OpenAL Exception: {self.message}' 58 else: 59 return f'OpenAL Exception [{self.error_code}: {self.error_string}]: {self.message}' 60 61 62class OpenALObject: 63 """Base class for OpenAL objects.""" 64 @classmethod 65 def _check_error(cls, message=None): 66 """Check whether there is an OpenAL error and raise exception if present.""" 67 error_code = al.alGetError() 68 if error_code != 0: 69 error_string = al.alGetString(error_code) 70 # TODO: Fix return type in generated code? 71 error_string = ctypes.cast(error_string, ctypes.c_char_p) 72 raise OpenALException(message=message, 73 error_code=error_code, 74 error_string=str(error_string.value)) 75 76 @classmethod 77 def _raise_error(cls, message): 78 """Raise an exception. Try to check for OpenAL error code too.""" 79 cls._check_error(message) 80 raise OpenALException(message) 81 82 83class OpenALDevice(OpenALObject): 84 """OpenAL audio device.""" 85 def __init__(self, device_name=None): 86 self._al_device = alc.alcOpenDevice(device_name) 87 self.check_context_error('Failed to open device.') 88 if self._al_device is None: 89 raise OpenALException('No OpenAL devices.') 90 91 def __del__(self): 92 assert _debug("Delete interface.OpenALDevice") 93 self.delete() 94 95 def delete(self): 96 if self._al_device is not None: 97 if alc.alcCloseDevice(self._al_device) == alc.ALC_FALSE: 98 self._raise_context_error('Failed to close device.') 99 self._al_device = None 100 101 @property 102 def is_ready(self): 103 return self._al_device is not None 104 105 def create_context(self): 106 al_context = alc.alcCreateContext(self._al_device, None) 107 self.check_context_error('Failed to create context') 108 return OpenALContext(self, al_context) 109 110 def get_version(self): 111 major = alc.ALCint() 112 minor = alc.ALCint() 113 alc.alcGetIntegerv(self._al_device, alc.ALC_MAJOR_VERSION, 114 ctypes.sizeof(major), major) 115 self.check_context_error('Failed to get version.') 116 alc.alcGetIntegerv(self._al_device, alc.ALC_MINOR_VERSION, 117 ctypes.sizeof(minor), minor) 118 self.check_context_error('Failed to get version.') 119 return major.value, minor.value 120 121 def get_extensions(self): 122 extensions = alc.alcGetString(self._al_device, alc.ALC_EXTENSIONS) 123 self.check_context_error('Failed to get extensions.') 124 return ctypes.cast(extensions, ctypes.c_char_p).value.decode('ascii').split() 125 126 def check_context_error(self, message=None): 127 """Check whether there is an OpenAL error and raise exception if present.""" 128 error_code = alc.alcGetError(self._al_device) 129 if error_code != 0: 130 error_string = alc.alcGetString(self._al_device, error_code) 131 # TODO: Fix return type in generated code? 132 error_string = ctypes.cast(error_string, ctypes.c_char_p) 133 raise OpenALException(message=message, 134 error_code=error_code, 135 error_string=str(error_string.value)) 136 137 def _raise_context_error(self, message): 138 """Raise an exception. Try to check for OpenAL error code too.""" 139 self.check_context_error(message) 140 raise OpenALException(message) 141 142 143class OpenALContext(OpenALObject): 144 def __init__(self, device, al_context): 145 self.device = device 146 self._al_context = al_context 147 self.make_current() 148 149 def __del__(self): 150 assert _debug("Delete interface.OpenALContext") 151 self.delete() 152 153 def delete(self): 154 if self._al_context is not None: 155 # TODO: Check if this context is current 156 alc.alcMakeContextCurrent(None) 157 self.device.check_context_error('Failed to make context no longer current.') 158 alc.alcDestroyContext(self._al_context) 159 self.device.check_context_error('Failed to destroy context.') 160 self._al_context = None 161 162 def make_current(self): 163 alc.alcMakeContextCurrent(self._al_context) 164 self.device.check_context_error('Failed to make context current.') 165 166 def create_source(self): 167 self.make_current() 168 return OpenALSource(self) 169 170 171class OpenALSource(OpenALObject): 172 def __init__(self, context): 173 self.context = weakref.ref(context) 174 self.buffer_pool = OpenALBufferPool(self.context) 175 176 self._al_source = al.ALuint() 177 al.alGenSources(1, self._al_source) 178 self._check_error('Failed to create source.') 179 180 self._state = None 181 self._get_state() 182 183 self._owned_buffers = {} 184 185 def __del__(self): 186 assert _debug("Delete interface.OpenALSource") 187 self.delete() 188 189 def delete(self): 190 if self.context() and self._al_source is not None: 191 # Only delete source if the context still exists 192 al.alDeleteSources(1, self._al_source) 193 self._check_error('Failed to delete source.') 194 self.buffer_pool.clear() 195 self._al_source = None 196 197 @property 198 def is_initial(self): 199 self._get_state() 200 return self._state == al.AL_INITIAL 201 202 @property 203 def is_playing(self): 204 self._get_state() 205 return self._state == al.AL_PLAYING 206 207 @property 208 def is_paused(self): 209 self._get_state() 210 return self._state == al.AL_PAUSED 211 212 @property 213 def is_stopped(self): 214 self._get_state() 215 return self._state == al.AL_STOPPED 216 217 def _int_source_property(attribute): 218 return property(lambda self: self._get_int(attribute), 219 lambda self, value: self._set_int(attribute, value)) 220 221 def _float_source_property(attribute): 222 return property(lambda self: self._get_float(attribute), 223 lambda self, value: self._set_float(attribute, value)) 224 225 def _3floats_source_property(attribute): 226 return property(lambda self: self._get_3floats(attribute), 227 lambda self, value: self._set_3floats(attribute, value)) 228 229 position = _3floats_source_property(al.AL_POSITION) 230 velocity = _3floats_source_property(al.AL_VELOCITY) 231 gain = _float_source_property(al.AL_GAIN) 232 buffers_queued = _int_source_property(al.AL_BUFFERS_QUEUED) 233 buffers_processed = _int_source_property(al.AL_BUFFERS_PROCESSED) 234 min_gain = _float_source_property(al.AL_MIN_GAIN) 235 max_gain = _float_source_property(al.AL_MAX_GAIN) 236 reference_distance = _float_source_property(al.AL_REFERENCE_DISTANCE) 237 rolloff_factor = _float_source_property(al.AL_ROLLOFF_FACTOR) 238 pitch = _float_source_property(al.AL_PITCH) 239 max_distance = _float_source_property(al.AL_MAX_DISTANCE) 240 direction = _3floats_source_property(al.AL_DIRECTION) 241 cone_inner_angle = _float_source_property(al.AL_CONE_INNER_ANGLE) 242 cone_outer_angle = _float_source_property(al.AL_CONE_OUTER_ANGLE) 243 cone_outer_gain = _float_source_property(al.AL_CONE_OUTER_GAIN) 244 sec_offset = _float_source_property(al.AL_SEC_OFFSET) 245 sample_offset = _float_source_property(al.AL_SAMPLE_OFFSET) 246 byte_offset = _float_source_property(al.AL_BYTE_OFFSET) 247 248 del _int_source_property 249 del _float_source_property 250 del _3floats_source_property 251 252 def play(self): 253 al.alSourcePlay(self._al_source) 254 self._check_error('Failed to play source.') 255 256 def pause(self): 257 al.alSourcePause(self._al_source) 258 self._check_error('Failed to pause source.') 259 260 def stop(self): 261 al.alSourceStop(self._al_source) 262 self._check_error('Failed to stop source.') 263 264 def clear(self): 265 self._set_int(al.AL_BUFFER, al.AL_NONE) 266 while self._owned_buffers: 267 buf_name, buf = self._owned_buffers.popitem() 268 self.buffer_pool.unqueue_buffer(buf) 269 270 def get_buffer(self): 271 return self.buffer_pool.get_buffer() 272 273 def queue_buffer(self, buf): 274 assert buf.is_valid 275 al.alSourceQueueBuffers(self._al_source, 1, ctypes.byref(buf.al_buffer)) 276 self._check_error('Failed to queue buffer.') 277 self._add_buffer(buf) 278 279 def unqueue_buffers(self): 280 processed = self.buffers_processed 281 assert _debug("Processed buffer count: {}".format(processed)) 282 if processed > 0: 283 buffers = (al.ALuint * processed)() 284 al.alSourceUnqueueBuffers(self._al_source, len(buffers), buffers) 285 self._check_error('Failed to unqueue buffers from source.') 286 for buf in buffers: 287 self.buffer_pool.unqueue_buffer(self._pop_buffer(buf)) 288 return processed 289 290 def _get_state(self): 291 if self._al_source is not None: 292 self._state = self._get_int(al.AL_SOURCE_STATE) 293 294 def _get_int(self, key): 295 assert self._al_source is not None 296 al_int = al.ALint() 297 al.alGetSourcei(self._al_source, key, al_int) 298 self._check_error('Failed to get value') 299 return al_int.value 300 301 def _set_int(self, key, value): 302 assert self._al_source is not None 303 al.alSourcei(self._al_source, key, int(value)) 304 self._check_error('Failed to set value.') 305 306 def _get_float(self, key): 307 assert self._al_source is not None 308 al_float = al.ALfloat() 309 al.alGetSourcef(self._al_source, key, al_float) 310 self._check_error('Failed to get value') 311 return al_float.value 312 313 def _set_float(self, key, value): 314 assert self._al_source is not None 315 al.alSourcef(self._al_source, key, float(value)) 316 self._check_error('Failed to set value.') 317 318 def _get_3floats(self, key): 319 assert self._al_source is not None 320 x = al.ALfloat() 321 y = al.ALfloat() 322 z = al.ALfloat() 323 al.alGetSource3f(self._al_source, key, x, y, z) 324 self._check_error('Failed to get value') 325 return x.value, y.value, z.value 326 327 def _set_3floats(self, key, values): 328 assert self._al_source is not None 329 x, y, z = map(float, values) 330 al.alSource3f(self._al_source, key, x, y, z) 331 self._check_error('Failed to set value.') 332 333 def _add_buffer(self, buf): 334 self._owned_buffers[buf.name] = buf 335 336 def _pop_buffer(self, al_buffer): 337 buf = self._owned_buffers.pop(al_buffer, None) 338 assert buf is not None 339 return buf 340 341 342OpenALOrientation = namedtuple("OpenALOrientation", ['at', 'up']) 343 344 345class OpenALListener(OpenALObject): 346 @property 347 def position(self): 348 return self._get_3floats(al.AL_POSITION) 349 350 @position.setter 351 def position(self, values): 352 self._set_3floats(al.AL_POSITION, values) 353 354 @property 355 def velocity(self): 356 return self._get_3floats(al.AL_VELOCITY) 357 358 @velocity.setter 359 def velocity(self, values): 360 self._set_3floats(al.AL_VELOCITY, values) 361 362 @property 363 def gain(self): 364 return self._get_float(al.AL_GAIN) 365 366 @gain.setter 367 def gain(self, value): 368 self._set_float(al.AL_GAIN, value) 369 370 @property 371 def orientation(self): 372 values = self._get_float_vector(al.AL_ORIENTATION, 6) 373 return OpenALOrientation(values[0:3], values[3:6]) 374 375 @orientation.setter 376 def orientation(self, values): 377 if len(values) == 2: 378 actual_values = values[0] + values[1] 379 elif len(values) == 6: 380 actual_values = values 381 else: 382 actual_values = [] 383 if len(actual_values) != 6: 384 raise ValueError("Need 2 tuples of 3 or 1 tuple of 6.") 385 self._set_float_vector(al.AL_ORIENTATION, actual_values) 386 387 def _get_float(self, key): 388 al_float = al.ALfloat() 389 al.alGetListenerf(key, al_float) 390 self._check_error('Failed to get value') 391 return al_float.value 392 393 def _set_float(self, key, value): 394 al.alListenerf(key, float(value)) 395 self._check_error('Failed to set value.') 396 397 def _get_3floats(self, key): 398 x = al.ALfloat() 399 y = al.ALfloat() 400 z = al.ALfloat() 401 al.alGetListener3f(key, x, y, z) 402 self._check_error('Failed to get value') 403 return x.value, y.value, z.value 404 405 def _set_3floats(self, key, values): 406 x, y, z = map(float, values) 407 al.alListener3f(key, x, y, z) 408 self._check_error('Failed to set value.') 409 410 def _get_float_vector(self, key, count): 411 al_float_vector = (al.ALfloat * count)() 412 al.alGetListenerfv(key, al_float_vector) 413 self._check_error('Failed to get value') 414 return [x for x in al_float_vector] 415 416 def _set_float_vector(self, key, values): 417 al_float_vector = (al.ALfloat * len(values))(*values) 418 al.alListenerfv(key, al_float_vector) 419 self._check_error('Failed to set value.') 420 421 422class OpenALBuffer(OpenALObject): 423 _format_map = { 424 (1, 8): al.AL_FORMAT_MONO8, 425 (1, 16): al.AL_FORMAT_MONO16, 426 (2, 8): al.AL_FORMAT_STEREO8, 427 (2, 16): al.AL_FORMAT_STEREO16, 428 } 429 430 def __init__(self, al_buffer, context): 431 self._al_buffer = al_buffer 432 self.context = context 433 assert self.is_valid 434 435 def __del__(self): 436 assert _debug("Delete interface.OpenALBuffer") 437 self.delete() 438 439 @property 440 def is_valid(self): 441 self._check_error('Before validate buffer.') 442 if self._al_buffer is None: 443 return False 444 valid = bool(al.alIsBuffer(self._al_buffer)) 445 if not valid: 446 # Clear possible error due to invalid buffer 447 al.alGetError() 448 return valid 449 450 @property 451 def al_buffer(self): 452 assert self.is_valid 453 return self._al_buffer 454 455 @property 456 def name(self): 457 assert self.is_valid 458 return self._al_buffer.value 459 460 def delete(self): 461 if self._al_buffer is not None and self.context() and self.is_valid: 462 al.alDeleteBuffers(1, ctypes.byref(self._al_buffer)) 463 self._check_error('Error deleting buffer.') 464 self._al_buffer = None 465 466 def data(self, audio_data, audio_format, length=None): 467 assert self.is_valid 468 length = length or audio_data.length 469 470 try: 471 al_format = self._format_map[(audio_format.channels, audio_format.sample_size)] 472 except KeyError: 473 raise MediaException(f"OpenAL does not support '{audio_format.sample_size}bit' audio.") 474 475 al.alBufferData(self._al_buffer, 476 al_format, 477 audio_data.data, 478 length, 479 audio_format.sample_rate) 480 self._check_error('Failed to add data to buffer.') 481 482 483class OpenALBufferPool(OpenALObject): 484 """At least Mac OS X doesn't free buffers when a source is deleted; it just 485 detaches them from the source. So keep our own recycled queue. 486 """ 487 def __init__(self, context): 488 self.context = context 489 self._buffers = [] # list of free buffer names 490 491 def __del__(self): 492 assert _debug("Delete interface.OpenALBufferPool") 493 self.clear() 494 495 def __len__(self): 496 return len(self._buffers) 497 498 def clear(self): 499 while self._buffers: 500 self._buffers.pop().delete() 501 502 def get_buffer(self): 503 """Convenience for returning one buffer name""" 504 return self.get_buffers(1)[0] 505 506 def get_buffers(self, number): 507 """Returns an array containing `number` buffer names. The returned list must 508 not be modified in any way, and may get changed by subsequent calls to 509 get_buffers. 510 """ 511 buffers = [] 512 while number > 0: 513 if self._buffers: 514 b = self._buffers.pop() 515 else: 516 b = self._create_buffer() 517 if b.is_valid: 518 # Protect against implementations that DO free buffers 519 # when they delete a source - carry on. 520 buffers.append(b) 521 number -= 1 522 523 return buffers 524 525 def unqueue_buffer(self, buf): 526 """A buffer has finished playing, free it.""" 527 if buf.is_valid: 528 self._buffers.append(buf) 529 530 def _create_buffer(self): 531 """Create a new buffer.""" 532 al_buffer = al.ALuint() 533 al.alGenBuffers(1, al_buffer) 534 self._check_error('Error allocating buffer.') 535 return OpenALBuffer(al_buffer, self.context) 536