Difficult-Rocket/libs/pyglet/media/drivers/directsound/adaptation.py
shenjack d84b490b99
with more logger
Add | more formatter and some more

Fix | type mis match

sync pyglet

Enhance | logger with Template

add lib-not-dr as requirement

sync pyglet

sync pyglet

Add | add lto=yes to nuitka_build

just incase

sync pyglet

sync lib_not_dr

Remove | external requirement lib-not-dr

some logger

sync lib-not-dr

sync pyglet

sync lib-not-dr

sync lib-not-dr

sync pyglet

sync pyglet

Fix | console thread been block

Update DR rs and DR sdk

sync lib not dr

sync lib-not-dr

sync lib-not-dr

sync pyglet and lib-not-dr

sync pyglet 0.1.8

sync lib not dr

logger almost done?

almost!

sync pyglet (clicpboard support!)

sync lib not dr

sync lib not dr

color code and sync pyglet

do not show memory and progress building localy

sync pyglet

synclibs
2023-11-20 20:12:59 +08:00

409 lines
14 KiB
Python

import math
import ctypes
from . import interface
from pyglet.util import debug_print
from pyglet.media.mediathreads import PlayerWorkerThread
from pyglet.media.drivers.base import AbstractAudioDriver, AbstractAudioPlayer, MediaEvent
from pyglet.media.drivers.listener import AbstractListener
_debug = debug_print('debug_media')
def _convert_coordinates(coordinates):
x, y, z = coordinates
return x, y, -z
def _gain2db(gain):
"""
Convert linear gain in range [0.0, 1.0] to 100ths of dB.
Power gain = P1/P2
dB = 2 log(P1/P2)
dB * 100 = 1000 * log(power gain)
"""
if gain <= 0:
return -10000
return max(-10000, min(int(1000 * math.log2(min(gain, 1))), 0))
def _db2gain(db):
"""Convert 100ths of dB to linear gain."""
return math.pow(10.0, float(db)/1000.0)
class DirectSoundAudioPlayer(AbstractAudioPlayer):
# Need to cache these because pyglet API allows update separately, but
# DSound requires both to be set at once.
_cone_inner_angle = 360
_cone_outer_angle = 360
min_buffer_size = 9600
def __init__(self, driver, ds_driver, source, player):
super(DirectSoundAudioPlayer, self).__init__(source, player)
# We keep here a strong reference because the AudioDriver is anyway
# a singleton object which will only be deleted when the application
# shuts down. The AudioDriver does not keep a ref to the AudioPlayer.
self.driver = driver
self._ds_driver = ds_driver
# Desired play state (may be actually paused due to underrun -- not
# implemented yet).
self._playing = False
# Up to one audio data may be buffered if too much data was received
# from the source that could not be written immediately into the
# buffer. See _refill().
self._audiodata_buffer = None
# Theoretical write and play cursors for an infinite buffer. play
# cursor is always <= write cursor (when equal, underrun is
# happening).
self._write_cursor = 0
self._play_cursor = 0
# Cursor position of end of data. Silence is written after
# eos for one buffer size.
self._eos_cursor = None
# Whether the source has hit its end; protect against duplicate
# dispatch of on_eos events.
self._has_underrun = False
# Indexes into DSound circular buffer. Complications ensue wrt each
# other to avoid writing over the play cursor. See _get_write_size and
# write().
self._play_cursor_ring = 0
self._write_cursor_ring = 0
# List of (play_cursor, MediaEvent), in sort order
self._events = []
# List of (cursor, timestamp), in sort order (cursor gives expiry
# place of the timestamp)
self._timestamps = []
audio_format = source.audio_format
# DSound buffer
self._ds_buffer = self._ds_driver.create_buffer(audio_format)
self._buffer_size = self._ds_buffer.buffer_size
self._ds_buffer.current_position = 0
self._refill(self._buffer_size)
def __del__(self):
# We decrease the IDirectSound refcount
self.driver._ds_driver._native_dsound.Release()
def delete(self):
self.driver.worker.remove(self)
def play(self):
assert _debug('DirectSound play')
if not self._playing:
self._playing = True
self._get_audiodata() # prebuffer if needed
self._ds_buffer.play()
self.driver.worker.add(self)
assert _debug('return DirectSound play')
def stop(self):
assert _debug('DirectSound stop')
self.driver.worker.remove(self)
if self._playing:
self._playing = False
self._ds_buffer.stop()
assert _debug('return DirectSound stop')
def clear(self):
assert _debug('DirectSound clear')
super(DirectSoundAudioPlayer, self).clear()
self._ds_buffer.current_position = 0
self._play_cursor_ring = self._write_cursor_ring = 0
self._play_cursor = self._write_cursor
self._eos_cursor = None
self._audiodata_buffer = None
self._has_underrun = False
del self._events[:]
del self._timestamps[:]
def refill_buffer(self):
write_size = self._get_write_size()
if write_size > self.min_buffer_size:
self._refill(write_size)
return True
return False
def _refill(self, write_size):
while write_size > 0:
assert _debug(f'_refill, write_size = {write_size}')
audio_data = self._get_audiodata()
if audio_data is not None:
assert _debug(f'write {audio_data.length}')
length = min(write_size, audio_data.length)
self.write(audio_data, length)
write_size -= length
else:
assert _debug('write silence')
self.write(None, write_size)
write_size = 0
def _dispatch_new_event(self, event_name):
MediaEvent(event_name).sync_dispatch_to_player(self.player)
def _get_audiodata(self):
if self._audiodata_buffer is None or self._audiodata_buffer.length == 0:
self._get_new_audiodata()
return self._audiodata_buffer
def _get_new_audiodata(self):
assert _debug('Getting new audio data buffer.')
# Pass a reference of ourself to allow the audio decoding to get time
# information for synchronization.
compensation_time = self.get_audio_time_diff()
self._audiodata_buffer = self.source.get_audio_data(self._buffer_size, compensation_time)
if self._audiodata_buffer is not None:
assert _debug('New audio data available: {} bytes'.format(self._audiodata_buffer.length))
if self._eos_cursor is not None:
self._move_write_cursor_after_eos()
self._add_audiodata_events(self._audiodata_buffer)
self._add_audiodata_timestamp(self._audiodata_buffer)
self._eos_cursor = None
elif self._eos_cursor is None:
assert _debug('No more audio data.')
self._eos_cursor = self._write_cursor
def _move_write_cursor_after_eos(self):
# Set the write cursor back to eos_cursor or play_cursor to prevent gaps
if self._play_cursor < self._eos_cursor:
cursor_diff = self._write_cursor - self._eos_cursor
assert _debug(f'Moving cursor back {cursor_diff}')
self._write_cursor = self._eos_cursor
self._write_cursor_ring -= cursor_diff
self._write_cursor_ring %= self._buffer_size
else:
cursor_diff = self._play_cursor - self._eos_cursor
assert _debug(f'Moving cursor back {cursor_diff}')
self._write_cursor = self._play_cursor
self._write_cursor_ring -= cursor_diff
self._write_cursor_ring %= self._buffer_size
def _add_audiodata_events(self, audio_data):
for event in audio_data.events:
event_cursor = self._write_cursor + event.timestamp * \
self.source.audio_format.bytes_per_second
assert _debug(f'Adding event {event} at {event_cursor}')
self._events.append((event_cursor, event))
def _add_audiodata_timestamp(self, audio_data):
ts_cursor = self._write_cursor + audio_data.length
self._timestamps.append(
(ts_cursor, audio_data.timestamp + audio_data.duration))
def update_play_cursor(self):
play_cursor_ring = self._ds_buffer.current_position.play_cursor
if play_cursor_ring < self._play_cursor_ring:
# Wrapped around
self._play_cursor += self._buffer_size - self._play_cursor_ring
self._play_cursor_ring = 0
self._play_cursor += play_cursor_ring - self._play_cursor_ring
self._play_cursor_ring = play_cursor_ring
self._dispatch_pending_events()
self._cleanup_timestamps()
self._check_underrun()
def _dispatch_pending_events(self):
pending_events = []
while self._events and self._events[0][0] <= self._play_cursor:
_, event = self._events.pop(0)
pending_events.append(event)
assert _debug('Dispatching pending events: {}'.format(pending_events))
assert _debug('Remaining events: {}'.format(self._events))
for event in pending_events:
event.sync_dispatch_to_player(self.player)
def _cleanup_timestamps(self):
while self._timestamps and self._timestamps[0][0] < self._play_cursor:
del self._timestamps[0]
def _check_underrun(self):
if (
not self._has_underrun and
self._playing and
(self._eos_cursor is not None and self._play_cursor > self._eos_cursor)
):
assert _debug('underrun, stopping')
self._has_underrun = True
self._dispatch_new_event('on_eos')
def _get_write_size(self):
self.update_play_cursor()
play_cursor = self._play_cursor
write_cursor = self._write_cursor
return self._buffer_size - max(write_cursor - play_cursor, 0)
def write(self, audio_data, length):
# Pass audio_data=None to write silence
if length == 0:
return 0
write_ptr = self._ds_buffer.lock(self._write_cursor_ring, length)
assert 0 < length <= self._buffer_size
assert length == write_ptr.audio_length_1.value + write_ptr.audio_length_2.value
if audio_data:
ctypes.memmove(write_ptr.audio_ptr_1, audio_data.data, write_ptr.audio_length_1.value)
audio_data.consume(write_ptr.audio_length_1.value, self.source.audio_format)
if write_ptr.audio_length_2.value > 0:
ctypes.memmove(write_ptr.audio_ptr_2, audio_data.data, write_ptr.audio_length_2.value)
audio_data.consume(write_ptr.audio_length_2.value, self.source.audio_format)
else:
if self.source.audio_format.sample_size == 8:
c = 0x80
else:
c = 0
ctypes.memset(write_ptr.audio_ptr_1, c, write_ptr.audio_length_1.value)
if write_ptr.audio_length_2.value > 0:
ctypes.memset(write_ptr.audio_ptr_2, c, write_ptr.audio_length_2.value)
self._ds_buffer.unlock(write_ptr)
self._write_cursor += length
self._write_cursor_ring += length
self._write_cursor_ring %= self._buffer_size
def get_time(self):
self.update_play_cursor()
if self._timestamps:
cursor, ts = self._timestamps[0]
result = ts + (self._play_cursor - cursor) / float(self.source.audio_format.bytes_per_second)
else:
result = None
return result
def set_volume(self, volume):
self._ds_buffer.volume = _gain2db(volume)
def set_position(self, position):
if self._ds_buffer.is3d:
self._ds_buffer.position = _convert_coordinates(position)
def set_min_distance(self, min_distance):
if self._ds_buffer.is3d:
self._ds_buffer.min_distance = min_distance
def set_max_distance(self, max_distance):
if self._ds_buffer.is3d:
self._ds_buffer.max_distance = max_distance
def set_pitch(self, pitch):
frequency = int(pitch * self.source.audio_format.sample_rate)
self._ds_buffer.frequency = frequency
def set_cone_orientation(self, cone_orientation):
if self._ds_buffer.is3d:
self._ds_buffer.cone_orientation = _convert_coordinates(cone_orientation)
def set_cone_inner_angle(self, cone_inner_angle):
if self._ds_buffer.is3d:
self._cone_inner_angle = int(cone_inner_angle)
self._set_cone_angles()
def set_cone_outer_angle(self, cone_outer_angle):
if self._ds_buffer.is3d:
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._ds_buffer.set_cone_angles(inner, outer)
def set_cone_outer_gain(self, cone_outer_gain):
if self._ds_buffer.is3d:
volume = _gain2db(cone_outer_gain)
self._ds_buffer.cone_outside_volume = volume
def prefill_audio(self):
write_size = self._get_write_size()
self._refill(write_size)
class DirectSoundDriver(AbstractAudioDriver):
def __init__(self):
self._ds_driver = interface.DirectSoundDriver()
self._ds_listener = self._ds_driver.create_listener()
assert self._ds_driver is not None
assert self._ds_listener is not None
self.worker = PlayerWorkerThread()
self.worker.start()
def __del__(self):
self.delete()
def create_audio_player(self, source, player):
assert self._ds_driver is not None
# We increase IDirectSound refcount for each AudioPlayer instantiated
# This makes sure the AudioPlayer still has a valid _native_dsound to
# clean-up itself during tear-down.
self._ds_driver._native_dsound.AddRef()
return DirectSoundAudioPlayer(self, self._ds_driver, source, player)
def get_listener(self):
assert self._ds_driver is not None
assert self._ds_listener is not None
return DirectSoundListener(self._ds_listener, self._ds_driver.primary_buffer)
def delete(self):
if hasattr(self, 'worker'):
self.worker.stop()
# Make sure the _ds_listener is deleted before the _ds_driver
self._ds_listener = None
class DirectSoundListener(AbstractListener):
def __init__(self, ds_listener, ds_buffer):
self._ds_listener = ds_listener
self._ds_buffer = ds_buffer
def _set_volume(self, volume):
self._volume = volume
self._ds_buffer.volume = _gain2db(volume)
def _set_position(self, position):
self._position = position
self._ds_listener.position = _convert_coordinates(position)
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):
self._ds_listener.orientation = (_convert_coordinates(self._forward_orientation)
+ _convert_coordinates(self._up_orientation))