394 lines
14 KiB
Python
394 lines
14 KiB
Python
# ----------------------------------------------------------------------------
|
|
# 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
|