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