1# ----------------------------------------------------------------------------
2# pyglet
3# Copyright (c) 2006-2008 Alex Holkner
4# Copyright (c) 2008-2020 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"""
36Pythonic interface to DirectSound.
37"""
38from collections import namedtuple
39import ctypes
40import weakref
41
42from pyglet.debug import debug_print
43from pyglet.window.win32 import _user32
44
45from . import lib_dsound as lib
46from .exceptions import DirectSoundNativeError
47
48_debug = debug_print('debug_media')
49
50
51def _check(hresult):
52    if hresult != lib.DS_OK:
53        raise DirectSoundNativeError(hresult)
54
55
56class DirectSoundDriver:
57    def __init__(self):
58        assert _debug('Constructing DirectSoundDriver')
59
60        self._native_dsound = lib.IDirectSound()
61        _check(
62            lib.DirectSoundCreate(None, ctypes.byref(self._native_dsound), None)
63        )
64
65        # A trick used by mplayer.. use desktop as window handle since it
66        # would be complex to use pyglet window handles (and what to do when
67        # application is audio only?).
68        hwnd = _user32.GetDesktopWindow()
69        _check(
70            self._native_dsound.SetCooperativeLevel(hwnd, lib.DSSCL_NORMAL)
71        )
72
73        self._buffer_factory = DirectSoundBufferFactory(self._native_dsound)
74        self.primary_buffer = self._buffer_factory.create_primary_buffer()
75
76    def __del__(self):
77        self.primary_buffer = None
78        self._native_dsound.Release()
79
80    def create_buffer(self, audio_format):
81        return self._buffer_factory.create_buffer(audio_format)
82
83    def create_listener(self):
84        return self.primary_buffer.create_listener()
85
86
87class DirectSoundBufferFactory:
88    default_buffer_size = 2.0
89
90    def __init__(self, native_dsound):
91        # We only keep a weakref to native_dsound which is owned by
92        # interface.DirectSoundDriver
93        self._native_dsound = weakref.proxy(native_dsound)
94
95    def create_buffer(self, audio_format):
96        buffer_size = int(audio_format.sample_rate * self.default_buffer_size)
97        wave_format = self._create_wave_format(audio_format)
98        buffer_desc = self._create_buffer_desc(wave_format, buffer_size)
99        return DirectSoundBuffer(
100                self._create_buffer(buffer_desc),
101                audio_format,
102                buffer_size)
103
104    def create_primary_buffer(self):
105        return DirectSoundBuffer(
106                self._create_buffer(self._create_primary_buffer_desc()),
107                None,
108                0)
109
110    def _create_buffer(self, buffer_desc):
111        buf = lib.IDirectSoundBuffer()
112        _check(
113            self._native_dsound.CreateSoundBuffer(buffer_desc, ctypes.byref(buf), None)
114        )
115        return buf
116
117    @staticmethod
118    def _create_wave_format(audio_format):
119        wfx = lib.WAVEFORMATEX()
120        wfx.wFormatTag = lib.WAVE_FORMAT_PCM
121        wfx.nChannels = audio_format.channels
122        wfx.nSamplesPerSec = audio_format.sample_rate
123        wfx.wBitsPerSample = audio_format.sample_size
124        wfx.nBlockAlign = wfx.wBitsPerSample * wfx.nChannels // 8
125        wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign
126        return wfx
127
128    @classmethod
129    def _create_buffer_desc(cls, wave_format, buffer_size):
130        dsbdesc = lib.DSBUFFERDESC()
131        dsbdesc.dwSize = ctypes.sizeof(dsbdesc)
132        dsbdesc.dwFlags = (lib.DSBCAPS_GLOBALFOCUS |
133                           lib.DSBCAPS_GETCURRENTPOSITION2 |
134                           lib.DSBCAPS_CTRLFREQUENCY |
135                           lib.DSBCAPS_CTRLVOLUME)
136        if wave_format.nChannels == 1:
137            dsbdesc.dwFlags |= lib.DSBCAPS_CTRL3D
138        dsbdesc.dwBufferBytes = buffer_size
139        dsbdesc.lpwfxFormat = ctypes.pointer(wave_format)
140
141        return dsbdesc
142
143    @classmethod
144    def _create_primary_buffer_desc(cls):
145        """Primary buffer with 3D and volume capabilities"""
146        buffer_desc = lib.DSBUFFERDESC()
147        buffer_desc.dwSize = ctypes.sizeof(buffer_desc)
148        buffer_desc.dwFlags = (lib.DSBCAPS_CTRL3D |
149                               lib.DSBCAPS_CTRLVOLUME |
150                               lib.DSBCAPS_PRIMARYBUFFER)
151
152        return buffer_desc
153
154class DirectSoundBuffer:
155    def __init__(self, native_buffer, audio_format, buffer_size):
156        self.audio_format = audio_format
157        self.buffer_size = buffer_size
158
159        self._native_buffer = native_buffer
160
161        if audio_format is not None and audio_format.channels == 1:
162            self._native_buffer3d = lib.IDirectSound3DBuffer()
163            self._native_buffer.QueryInterface(lib.IID_IDirectSound3DBuffer,
164                                               ctypes.byref(self._native_buffer3d))
165        else:
166            self._native_buffer3d = None
167
168    def __del__(self):
169        self.delete()
170
171    def delete(self):
172        if self._native_buffer is not None:
173            self._native_buffer.Stop()
174            self._native_buffer.Release()
175            self._native_buffer = None
176            if self._native_buffer3d is not None:
177                self._native_buffer3d.Release()
178                self._native_buffer3d = None
179
180    @property
181    def volume(self):
182        vol = lib.LONG()
183        _check(
184            self._native_buffer.GetVolume(ctypes.byref(vol))
185        )
186        return vol.value
187
188    @volume.setter
189    def volume(self, value):
190        _check(
191            self._native_buffer.SetVolume(value)
192        )
193
194    _CurrentPosition = namedtuple('_CurrentPosition', ['play_cursor', 'write_cursor'])
195
196    @property
197    def current_position(self):
198        """Tuple of current play position and current write position.
199        Only play position can be modified, so setter only accepts a single value."""
200        play_cursor = lib.DWORD()
201        write_cursor = lib.DWORD()
202        _check(
203            self._native_buffer.GetCurrentPosition(play_cursor,
204                                                   write_cursor)
205        )
206        return self._CurrentPosition(play_cursor.value, write_cursor.value)
207
208    @current_position.setter
209    def current_position(self, value):
210        _check(
211            self._native_buffer.SetCurrentPosition(value)
212        )
213
214    @property
215    def is3d(self):
216        return self._native_buffer3d is not None
217
218    @property
219    def is_playing(self):
220        return (self._get_status() & lib.DSBSTATUS_PLAYING) != 0
221
222    @property
223    def is_buffer_lost(self):
224        return (self._get_status() & lib.DSBSTATUS_BUFFERLOST) != 0
225
226    def _get_status(self):
227        status = lib.DWORD()
228        _check(
229            self._native_buffer.GetStatus(status)
230        )
231        return status.value
232
233    @property
234    def position(self):
235        if self.is3d:
236            position = lib.D3DVECTOR()
237            _check(
238                self._native_buffer3d.GetPosition(ctypes.byref(position))
239            )
240            return position.x, position.y, position.z
241        else:
242            return 0, 0, 0
243
244    @position.setter
245    def position(self, position):
246        if self.is3d:
247            x, y, z = position
248            _check(
249                self._native_buffer3d.SetPosition(x, y, z, lib.DS3D_IMMEDIATE)
250            )
251
252    @property
253    def min_distance(self):
254        """The minimum distance, which is the distance from the
255        listener at which sounds in this buffer begin to be attenuated."""
256        if self.is3d:
257            value = lib.D3DVALUE()
258            _check(
259                self._native_buffer3d.GetMinDistance(ctypes.byref(value))
260            )
261            return value.value
262        else:
263            return 0
264
265    @min_distance.setter
266    def min_distance(self, value):
267        if self.is3d:
268            _check(
269                self._native_buffer3d.SetMinDistance(value, lib.DS3D_IMMEDIATE)
270            )
271
272    @property
273    def max_distance(self):
274        """The maximum distance, which is the distance from the listener beyond which
275        sounds in this buffer are no longer attenuated."""
276        if self.is3d:
277            value = lib.D3DVALUE()
278            _check(
279                self._native_buffer3d.GetMaxDistance(ctypes.byref(value))
280            )
281            return value.value
282        else:
283            return 0
284
285    @max_distance.setter
286    def max_distance(self, value):
287        if self.is3d:
288            _check(
289                self._native_buffer3d.SetMaxDistance(value, lib.DS3D_IMMEDIATE)
290            )
291
292    @property
293    def frequency(self):
294        value = lib.DWORD()
295        _check(
296            self._native_buffer.GetFrequency(value)
297        )
298        return value.value
299
300    @frequency.setter
301    def frequency(self, value):
302        """The frequency, in samples per second, at which the buffer is playing."""
303        _check(
304            self._native_buffer.SetFrequency(value)
305        )
306
307    @property
308    def cone_orientation(self):
309        """The orientation of the sound projection cone."""
310        if self.is3d:
311            orientation = lib.D3DVECTOR()
312            _check(
313                self._native_buffer3d.GetConeOrientation(ctypes.byref(orientation))
314            )
315            return orientation.x, orientation.y, orientation.z
316        else:
317            return 0, 0, 0
318
319    @cone_orientation.setter
320    def cone_orientation(self, value):
321        if self.is3d:
322            x, y, z = value
323            _check(
324                self._native_buffer3d.SetConeOrientation(x, y, z, lib.DS3D_IMMEDIATE)
325            )
326
327    _ConeAngles = namedtuple('_ConeAngles', ['inside', 'outside'])
328    @property
329    def cone_angles(self):
330        """The inside and outside angles of the sound projection cone."""
331        if self.is3d:
332            inside = lib.DWORD()
333            outside = lib.DWORD()
334            _check(
335                self._native_buffer3d.GetConeAngles(ctypes.byref(inside), ctypes.byref(outside))
336            )
337            return self._ConeAngles(inside.value, outside.value)
338        else:
339            return self._ConeAngles(0, 0)
340
341    def set_cone_angles(self, inside, outside):
342        """The inside and outside angles of the sound projection cone."""
343        if self.is3d:
344            _check(
345                self._native_buffer3d.SetConeAngles(inside, outside, lib.DS3D_IMMEDIATE)
346            )
347
348    @property
349    def cone_outside_volume(self):
350        """The volume of the sound outside the outside angle of the sound projection cone."""
351        if self.is3d:
352            volume = lib.LONG()
353            _check(
354                self._native_buffer3d.GetConeOutsideVolume(ctypes.byref(volume))
355            )
356            return volume.value
357        else:
358            return 0
359
360    @cone_outside_volume.setter
361    def cone_outside_volume(self, value):
362        if self.is3d:
363            _check(
364                self._native_buffer3d.SetConeOutsideVolume(value, lib.DS3D_IMMEDIATE)
365            )
366
367    def create_listener(self):
368        native_listener = lib.IDirectSound3DListener()
369        self._native_buffer.QueryInterface(lib.IID_IDirectSound3DListener,
370                                           ctypes.byref(native_listener))
371        return DirectSoundListener(self, native_listener)
372
373    def play(self):
374        _check(
375            self._native_buffer.Play(0, 0, lib.DSBPLAY_LOOPING)
376        )
377
378    def stop(self):
379        _check(
380            self._native_buffer.Stop()
381        )
382
383    class _WritePointer:
384        def __init__(self):
385            self.audio_ptr_1 = ctypes.c_void_p()
386            self.audio_length_1 = lib.DWORD()
387            self.audio_ptr_2 = ctypes.c_void_p()
388            self.audio_length_2 = lib.DWORD()
389
390    def lock(self, write_cursor, write_size):
391        assert _debug('DirectSoundBuffer.lock({}, {})'.format(write_cursor, write_size))
392        pointer = self._WritePointer()
393        _check(
394            self._native_buffer.Lock(write_cursor,
395                                     write_size,
396                                     ctypes.byref(pointer.audio_ptr_1),
397                                     pointer.audio_length_1,
398                                     ctypes.byref(pointer.audio_ptr_2),
399                                     pointer.audio_length_2,
400                                     0)
401        )
402        return pointer
403
404    def unlock(self, pointer):
405        _check(
406            self._native_buffer.Unlock(pointer.audio_ptr_1,
407                                       pointer.audio_length_1,
408                                       pointer.audio_ptr_2,
409                                       pointer.audio_length_2)
410        )
411
412
413class DirectSoundListener:
414    def __init__(self, ds_buffer, native_listener):
415        # We only keep a weakref to ds_buffer as it is owned by
416        # interface.DirectSound or a DirectSoundAudioPlayer
417        self.ds_buffer = weakref.proxy(ds_buffer)
418        self._native_listener = native_listener
419
420    def __del__(self):
421        self.delete()
422
423    def delete(self):
424        if self._native_listener:
425            self._native_listener.Release()
426            self._native_listener = None
427
428    @property
429    def position(self):
430        vector = lib.D3DVECTOR()
431        _check(
432            self._native_listener.GetPosition(ctypes.byref(vector))
433        )
434        return vector.x, vector.y, vector.z
435
436    @position.setter
437    def position(self, value):
438        _check(
439            self._native_listener.SetPosition(*(list(value) + [lib.DS3D_IMMEDIATE]))
440        )
441
442    @property
443    def orientation(self):
444        front = lib.D3DVECTOR()
445        top = lib.D3DVECTOR()
446        _check(
447            self._native_listener.GetOrientation(ctypes.byref(front), ctypes.byref(top))
448        )
449        return front.x, front.y, front.z, top.x, top.y, top.z
450
451    @orientation.setter
452    def orientation(self, orientation):
453        _check(
454            self._native_listener.SetOrientation(*(list(orientation) + [lib.DS3D_IMMEDIATE]))
455        )
456
457
458