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