# ---------------------------------------------------------------------------- # 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. # ---------------------------------------------------------------------------- '''Windows DirectSound audio implementation. ''' __docformat__ = 'restructuredtext' __version__ = '$Id: $' import ctypes import math import time from pyglet.media import AudioPlayer, Listener, MediaException from pyglet.media.drivers.directsound import lib_dsound as lib from pyglet.window.win32 import _user32 class DirectSoundException(MediaException): pass def _db(gain): '''Convert linear gain in range [0.0, 1.0] to 100ths of dB.''' if gain <= 0: return -10000 return max(-10000, min(int(1000 * math.log(min(gain, 1))), 0)) class DirectSoundAudioPlayer(AudioPlayer): _buffer_size = 44800 * 1 _update_buffer_size = _buffer_size // 4 _buffer_size_secs = None _cone_inner_angle = 360 _cone_outer_angle = 360 UPDATE_PERIOD = 0.05 def __init__(self, audio_format): super(DirectSoundAudioPlayer, self).__init__(audio_format) self._playing = False self._timestamp = 0. self._buffer = None self._buffer_playing = False self._data_size = 0 # amount of buffer filled by this player self._play_cursor = 0 self._buffer_time = 0. # ts of buffer at buffer_time_pos self._buffer_time_pos = 0 self._write_cursor = 0 self._timestamps = [] self._eos_count = 0 self._dirty_size = 0 wfx = lib.WAVEFORMATEX() wfx.wFormatTag = lib.WAVE_FORMAT_PCM wfx.nChannels = audio_format.channels wfx.nSamplesPerSec = audio_format.sample_rate wfx.wBitsPerSample = audio_format.sample_size wfx.nBlockAlign = wfx.wBitsPerSample * wfx.nChannels // 8 wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign dsbdesc = lib.DSBUFFERDESC() dsbdesc.dwSize = ctypes.sizeof(dsbdesc) dsbdesc.dwFlags = (lib.DSBCAPS_GLOBALFOCUS | lib.DSBCAPS_GETCURRENTPOSITION2 | lib.DSBCAPS_CTRLFREQUENCY | lib.DSBCAPS_CTRLVOLUME) if audio_format.channels == 1: dsbdesc.dwFlags |= lib.DSBCAPS_CTRL3D dsbdesc.dwBufferBytes = self._buffer_size dsbdesc.lpwfxFormat = ctypes.pointer(wfx) self._buffer = lib.IDirectSoundBuffer() dsound.CreateSoundBuffer(dsbdesc, ctypes.byref(self._buffer), None) if audio_format.channels == 1: self._buffer3d = lib.IDirectSound3DBuffer() self._buffer.QueryInterface(lib.IID_IDirectSound3DBuffer, ctypes.byref(self._buffer3d)) else: self._buffer3d = None self._buffer_size_secs = \ self._buffer_size / float(audio_format.bytes_per_second) self._buffer.SetCurrentPosition(0) def __del__(self): try: self._buffer.Stop() self._buffer.Release() if self._buffer3d: self._buffer3d.Release() except: pass def get_write_size(self): if self._data_size < self._buffer_size: return self._buffer_size - self._data_size play_cursor = self._play_cursor if self._write_cursor == play_cursor and self._buffer_playing: # Polling too fast, no play cursor movement return 0 elif self._write_cursor == play_cursor and not self._playing: # Paused and up-to-date return 0 elif self._write_cursor < play_cursor: # Play cursor ahead of write cursor write_size = play_cursor - self._write_cursor else: # Play cursor behind write cursor, wraps around write_size = self._buffer_size - self._write_cursor + play_cursor if write_size < self._update_buffer_size and not self._dirty_size: return 0 return write_size def write(self, audio_data, length=None): # Pass audio_data=None, length>0 to write silence if length is None: write_size = self.get_write_size() length = min(audio_data.length, write_size) if length == 0: return 0 if self._data_size < self._buffer_size: self._data_size = min(self._data_size + length, self._buffer_size) p1 = ctypes.c_void_p() l1 = lib.DWORD() p2 = ctypes.c_void_p() l2 = lib.DWORD() self._buffer.Lock(self._write_cursor, length, ctypes.byref(p1), l1, ctypes.byref(p2), l2, 0) assert length == l1.value + l2.value if audio_data: if self._write_cursor >= self._play_cursor: wc = self._write_cursor else: wc = self._write_cursor + self._buffer_size self._timestamps.append((wc, audio_data.timestamp)) ctypes.memmove(p1, audio_data.data, l1.value) audio_data.consume(l1.value, self.audio_format) if l2.value: ctypes.memmove(p2, audio_data.data, l2.value) audio_data.consume(l2.value, self.audio_format) else: ctypes.memset(p1, 0, l1.value) if l2.value: ctypes.memset(p2, 0, l2.value) pass self._buffer.Unlock(p1, l1, p2, l2) self._write_cursor += length self._write_cursor %= self._buffer_size def write_eos(self): if self._write_cursor > self._play_cursor: wc = self._write_cursor else: wc = self._write_cursor + self._buffer_size self._timestamps.append((wc, 'eos')) def write_end(self): if not self._dirty_size: self._dirty_size = self._buffer_size def pump(self): # Update play cursor, check for wraparound and EOS markers play_cursor = lib.DWORD() self._buffer.GetCurrentPosition(play_cursor, None) if play_cursor.value < self._play_cursor: # Wrapped around self._buffer_time_pos -= self._buffer_size self._timestamps = \ [(a - self._buffer_size, t) for a, t in self._timestamps] self._play_cursor = play_cursor.value try: while self._timestamps[0][0] < self._play_cursor: pos, timestamp = self._timestamps.pop(0) if timestamp == 'eos': self._eos_count += 1 else: self._buffer_time = timestamp self._buffer_time_pos = pos except IndexError: pass self._timestamp = self._buffer_time + \ (self._play_cursor - self._buffer_time_pos) \ / float(self.audio_format.bytes_per_second) # Write silence if self._dirty_size: write_size = self.get_write_size() length = min(write_size, self._dirty_size) self.write(None, length) self._dirty_size -= length if self._dirty_size < 0: self._dirty_size = 0 if self._playing and not self._buffer_playing: self._buffer.Play(0, 0, lib.DSBPLAY_LOOPING) self._buffer_playing = True def get_time(self): return self._timestamp def play(self): if self._playing: return self._playing = True self._buffer.Play(0, 0, lib.DSBPLAY_LOOPING) self._buffer_playing = True def stop(self): if not self._playing: return self._playing = False self._buffer.Stop() self._buffer_playing = False def clear(self): self._eos_count = 0 self._timestamps = [] self._write_cursor = 0 self._buffer.SetCurrentPosition(0) self._buffer_time = 0. self._buffer_time_pos = 0 self._data_size = 0 def clear_eos(self): if self._eos_count > 0: self._eos_count -= 1 return True return False def _get_source(self): if self._sources: return self._sources[0] return None def set_volume(self, volume): volume = _db(volume) self._buffer.SetVolume(volume) def set_position(self, position): if self._buffer3d: x, y, z = position self._buffer3d.SetPosition(x, y, -z, lib.DS3D_IMMEDIATE) def set_min_distance(self, min_distance): if self._buffer3d: self._buffer3d.SetMinDistance(min_distance, lib.DS3D_IMMEDIATE) def set_max_distance(self, max_distance): if self._buffer3d: self._buffer3d.SetMaxDistance(max_distance, lib.DS3D_IMMEDIATE) def set_pitch(self, pitch): frequency = int(pitch * self.audio_format.sample_rate) self._buffer.SetFrequency(frequency) def set_cone_orientation(self, cone_orientation): if self._buffer3d: x, y, z = cone_orientation self._buffer3d.SetConeOrientation(x, y, -z, lib.DS3D_IMMEDIATE) def set_cone_inner_angle(self, cone_inner_angle): if self._buffer3d: self._cone_inner_angle = int(cone_inner_angle) self._set_cone_angles() def set_cone_outer_angle(self, cone_outer_angle): if self._buffer3d: self._cone_outer_angle = int(cone_outer_angle) self._set_cone_angles() def _set_cone_angles(self): inner = min(self._cone_inner_angle, self._cone_outer_angle) outer = max(self._cone_inner_angle, self._cone_outer_angle) self._buffer3d.SetConeAngles(inner, outer, lib.DS3D_IMMEDIATE) def set_cone_outer_gain(self, cone_outer_gain): if self._buffer3d: volume = _db(cone_outer_gain) self._buffer3d.SetConeOutsideVolume(volume, lib.DS3D_IMMEDIATE) class DirectSoundListener(Listener): def _init(self): # Called after driver_init() self._buffer = lib.IDirectSoundBuffer() dsbd = lib.DSBUFFERDESC() dsbd.dwSize = ctypes.sizeof(dsbd) dsbd.dwFlags = (lib.DSBCAPS_CTRL3D | lib.DSBCAPS_CTRLVOLUME | lib.DSBCAPS_PRIMARYBUFFER) dsound.CreateSoundBuffer(dsbd, ctypes.byref(self._buffer), None) self._listener = lib.IDirectSound3DListener() self._buffer.QueryInterface(lib.IID_IDirectSound3DListener, ctypes.byref(self._listener)) def __del__(self): try: self._buffer.Release() self._listener.Release() except: pass def _set_volume(self, volume): self._volume = volume self._buffer.SetVolume(_db(volume)) def _set_position(self, position): self._position = position x, y, z = position self._listener.SetPosition(x, y, -z, lib.DS3D_IMMEDIATE) def _set_forward_orientation(self, orientation): self._forward_orientation = orientation self._set_orientation() def _set_up_orientation(self, orientation): self._up_orientation = orientation self._set_orientation() def _set_orientation(self): x, y, z = self._forward_orientation ux, uy, uz = self._up_orientation self._listener.SetOrientation(x, y, -z, ux, uy, -uz, lib.DS3D_IMMEDIATE) dsound = None def driver_init(): global dsound dsound = lib.IDirectSound() lib.DirectSoundCreate(None, ctypes.byref(dsound), None) # A trick used by mplayer.. use desktop as window handle since it would # be complex to use pyglet window handles (and what to do when application # is audio only?). hwnd = _user32.GetDesktopWindow() dsound.SetCooperativeLevel(hwnd, lib.DSSCL_NORMAL) driver_listener._init() # Force a context switch, as some Windows audio drivers don't get time # to process short sounds if Python hogs all the CPU. See issue #163. from pyglet import clock clock.Clock._force_sleep = True driver_listener = DirectSoundListener() driver_audio_player_class = DirectSoundAudioPlayer