sync pyget
This commit is contained in:
parent
8d3e14fc39
commit
1e342ce0a8
@ -122,12 +122,24 @@ class EventLoop(event.EventDispatcher):
|
||||
def run(self, interval=1/60):
|
||||
"""Begin processing events, scheduled functions and window updates.
|
||||
|
||||
:Parameters:
|
||||
`interval` : float or None [default: 1/60]
|
||||
Windows redraw interval, in seconds (framerate).
|
||||
If `interval == 0`, windows will redraw at maximum rate.
|
||||
If `interval is None`, Pyglet will not call its redraw function.
|
||||
The user must schedule (or call on demand) a custom redraw
|
||||
function for each window, allowing a custom framerate per window.
|
||||
(see example in documentation)
|
||||
|
||||
This method returns when :py:attr:`has_exit` is set to True.
|
||||
|
||||
Developers are discouraged from overriding this method, as the
|
||||
implementation is platform-specific.
|
||||
"""
|
||||
if not interval:
|
||||
if interval is None:
|
||||
# User application will manage a custom _redraw_windows() method
|
||||
pass
|
||||
elif interval == 0:
|
||||
self.clock.schedule(self._redraw_windows)
|
||||
else:
|
||||
self.clock.schedule_interval(self._redraw_windows, interval)
|
||||
|
@ -135,6 +135,17 @@ class AudioData:
|
||||
self.duration -= num_bytes / audio_format.bytes_per_second
|
||||
self.timestamp += num_bytes / audio_format.bytes_per_second
|
||||
|
||||
def get_string_data(self):
|
||||
"""Return data as a bytestring.
|
||||
|
||||
Returns:
|
||||
bytes: Data as a (byte)string.
|
||||
"""
|
||||
if self.data is None:
|
||||
return b''
|
||||
|
||||
return memoryview(self.data).tobytes()[:self.length]
|
||||
|
||||
|
||||
class SourceInfo:
|
||||
"""Source metadata information.
|
||||
@ -398,7 +409,7 @@ class StaticSource(Source):
|
||||
audio_data = source.get_audio_data(buffer_size)
|
||||
if not audio_data:
|
||||
break
|
||||
data.write(audio_data.data)
|
||||
data.write(audio_data.get_string_data())
|
||||
self._data = data.getvalue()
|
||||
|
||||
self._duration = len(self._data) / self.audio_format.bytes_per_second
|
||||
|
@ -1,38 +1,40 @@
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from enum import Enum, auto
|
||||
from typing import Dict, Optional
|
||||
|
||||
from pyglet import event
|
||||
from pyglet.util import with_metaclass
|
||||
|
||||
|
||||
class DeviceState:
|
||||
ACTIVE = "active"
|
||||
DISABLED = "disabled"
|
||||
MISSING = "missing"
|
||||
UNPLUGGED = "unplugged"
|
||||
class DeviceState(Enum):
|
||||
ACTIVE = auto()
|
||||
DISABLED = auto()
|
||||
MISSING = auto()
|
||||
UNPLUGGED = auto()
|
||||
|
||||
|
||||
class DeviceFlow:
|
||||
OUTPUT = "output"
|
||||
INPUT = "input"
|
||||
INPUT_OUTPUT = "input/output"
|
||||
class DeviceFlow(Enum):
|
||||
OUTPUT = auto()
|
||||
INPUT = auto()
|
||||
INPUT_OUTPUT = auto()
|
||||
|
||||
|
||||
class AudioDevice:
|
||||
"""Base class for a platform independent audio device.
|
||||
_platform_state and _platform_flow is used to make device state numbers."""
|
||||
_platform_state = {} # Must be defined by the parent.
|
||||
_platform_flow = {} # Must be defined by the parent.
|
||||
platform_state: Dict[int, DeviceState] = {} # Must be defined by the parent.
|
||||
platform_flow: Dict[int, DeviceFlow] = {} # Must be defined by the parent.
|
||||
|
||||
def __init__(self, dev_id, name, description, flow, state):
|
||||
def __init__(self, dev_id: str, name: str, description: str, flow: int, state: int):
|
||||
self.id = dev_id
|
||||
self.flow = flow
|
||||
self.state = state
|
||||
self.flow = flow # platform value
|
||||
self.state = state # platform value
|
||||
self.name = name
|
||||
self.description = description
|
||||
|
||||
def __repr__(self):
|
||||
return "{}(name={}, state={}, flow={})".format(
|
||||
self.__class__.__name__, self.name, self._platform_state[self.state], self._platform_flow[self.flow])
|
||||
return "{}(name='{}', state={}, flow={})".format(
|
||||
self.__class__.__name__, self.name, self.platform_state[self.state].name, self.platform_flow[self.flow].name)
|
||||
|
||||
|
||||
class AbstractAudioDeviceManager(with_metaclass(ABCMeta, event.EventDispatcher, object)):
|
||||
@ -66,20 +68,23 @@ class AbstractAudioDeviceManager(with_metaclass(ABCMeta, event.EventDispatcher,
|
||||
"""Returns a list of all audio devices, no matter what state they are in."""
|
||||
pass
|
||||
|
||||
def on_device_state_changed(self, device, old_state, new_state):
|
||||
"""Event, occurs when the state of a device changes, provides old state and new state."""
|
||||
def on_device_state_changed(self, device: AudioDevice, old_state: DeviceState, new_state: DeviceState):
|
||||
"""Event, occurs when the state of a device changes, provides the old state and new state."""
|
||||
pass
|
||||
|
||||
def on_device_added(self, device):
|
||||
def on_device_added(self, device: AudioDevice):
|
||||
"""Event, occurs when a new device is added to the system."""
|
||||
pass
|
||||
|
||||
def on_device_removed(self, device):
|
||||
def on_device_removed(self, device: AudioDevice):
|
||||
"""Event, occurs when an existing device is removed from the system."""
|
||||
pass
|
||||
|
||||
def on_default_changed(self, device):
|
||||
"""Event, occurs when the default audio device changes."""
|
||||
def on_default_changed(self, device: Optional[AudioDevice], flow: DeviceFlow):
|
||||
"""Event, occurs when the default audio device changes.
|
||||
If there is no device that can be the default on the system, can be None.
|
||||
The flow determines whether an input or output device became it's respective default.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
from pyglet.libs.win32 import com
|
||||
from pyglet.libs.win32 import _ole32 as ole32
|
||||
from pyglet.libs.win32.constants import CLSCTX_INPROC_SERVER
|
||||
@ -5,7 +7,7 @@ from pyglet.libs.win32.types import *
|
||||
from pyglet.media.devices import base
|
||||
from pyglet.util import debug_print
|
||||
|
||||
_debug = debug_print('debug_media')
|
||||
_debug = debug_print('debug_input')
|
||||
|
||||
EDataFlow = UINT
|
||||
# Audio rendering stream. Audio data flows from the application to the audio endpoint device, which renders the stream.
|
||||
@ -49,7 +51,7 @@ class PROPERTYKEY(ctypes.Structure):
|
||||
return "PROPERTYKEY({}, pid={})".format(self.fmtid, self.pid)
|
||||
|
||||
|
||||
REFPROPERTYKEY = PROPERTYKEY
|
||||
REFPROPERTYKEY = POINTER(PROPERTYKEY)
|
||||
|
||||
PKEY_Device_FriendlyName = PROPERTYKEY(
|
||||
com.GUID(0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0), 14)
|
||||
@ -93,10 +95,20 @@ class IMMNotificationClient(com.IUnknown):
|
||||
]
|
||||
|
||||
|
||||
IID_IMMEndpoint = com.GUID(0x1BE09788, 0x6894, 0x4089, 0x85, 0x86, 0x9A, 0x2A, 0x6C, 0x26, 0x5A, 0xC5)
|
||||
|
||||
|
||||
class IMMEndpoint(com.pIUnknown):
|
||||
_methods_ = [
|
||||
('GetDataFlow',
|
||||
com.STDMETHOD(POINTER(EDataFlow))),
|
||||
]
|
||||
|
||||
|
||||
class AudioNotificationCB(com.COMObject):
|
||||
_interfaces_ = [IMMNotificationClient]
|
||||
|
||||
def __init__(self, audio_devices):
|
||||
def __init__(self, audio_devices: 'Win32AudioDeviceManager'):
|
||||
super().__init__()
|
||||
self.audio_devices = audio_devices
|
||||
self._lost = False
|
||||
@ -105,19 +117,24 @@ class AudioNotificationCB(com.COMObject):
|
||||
device = self.audio_devices.get_cached_device(pwstrDeviceId)
|
||||
|
||||
old_state = device.state
|
||||
|
||||
pyglet_old_state = Win32AudioDevice.platform_state[old_state]
|
||||
pyglet_new_state = Win32AudioDevice.platform_state[dwNewState]
|
||||
assert _debug(
|
||||
"Audio device {} changed state. From state: {} to state: {}".format(device.name, old_state, dwNewState))
|
||||
f"Audio device '{device.name}' changed state. From: {pyglet_old_state} to: {pyglet_new_state}")
|
||||
|
||||
device.state = dwNewState
|
||||
self.audio_devices.dispatch_event('on_device_state_changed', device, old_state, dwNewState)
|
||||
self.audio_devices.dispatch_event('on_device_state_changed', device, pyglet_old_state, pyglet_new_state)
|
||||
|
||||
def OnDeviceAdded(self, this, pwstrDeviceId):
|
||||
assert _debug("Audio device was added {}".format(pwstrDeviceId))
|
||||
self.audio_devices.dispatch_event('on_device_added', pwstrDeviceId)
|
||||
dev = self.audio_devices.add_device(pwstrDeviceId)
|
||||
assert _debug(f"Audio device was added {pwstrDeviceId}: {dev}")
|
||||
self.audio_devices.dispatch_event('on_device_added', dev)
|
||||
|
||||
def OnDeviceRemoved(self, this, pwstrDeviceId):
|
||||
assert _debug("Audio device was removed {}".format(pwstrDeviceId))
|
||||
self.audio_devices.dispatch_event('on_device_removed', pwstrDeviceId)
|
||||
dev = self.audio_devices.remove_device(pwstrDeviceId)
|
||||
assert _debug(f"Audio device was removed {pwstrDeviceId} : {dev}")
|
||||
self.audio_devices.dispatch_event('on_device_removed', dev)
|
||||
|
||||
def OnDefaultDeviceChanged(self, this, flow, role, pwstrDeviceId):
|
||||
# Only support eConsole role right now
|
||||
@ -127,7 +144,10 @@ class AudioNotificationCB(com.COMObject):
|
||||
else:
|
||||
device = self.audio_devices.get_cached_device(pwstrDeviceId)
|
||||
|
||||
self.audio_devices.dispatch_event('on_default_changed', device)
|
||||
pyglet_flow = Win32AudioDevice.platform_flow[flow]
|
||||
assert _debug(f"Default device was changed to: {device} ({pyglet_flow})")
|
||||
|
||||
self.audio_devices.dispatch_event('on_default_changed', device, pyglet_flow)
|
||||
|
||||
def OnPropertyValueChanged(self, this, pwstrDeviceId, key):
|
||||
pass
|
||||
@ -171,14 +191,14 @@ class IMMDeviceEnumerator(com.pIUnknown):
|
||||
|
||||
|
||||
class Win32AudioDevice(base.AudioDevice):
|
||||
_platform_state = {
|
||||
platform_state = {
|
||||
DEVICE_STATE_ACTIVE: base.DeviceState.ACTIVE,
|
||||
DEVICE_STATE_DISABLED: base.DeviceState.DISABLED,
|
||||
DEVICE_STATE_NOTPRESENT: base.DeviceState.MISSING,
|
||||
DEVICE_STATE_UNPLUGGED: base.DeviceState.UNPLUGGED
|
||||
}
|
||||
|
||||
_platform_flow = {
|
||||
platform_flow = {
|
||||
eRender: base.DeviceFlow.OUTPUT,
|
||||
eCapture: base.DeviceFlow.INPUT,
|
||||
eAll: base.DeviceFlow.INPUT_OUTPUT
|
||||
@ -192,14 +212,42 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
|
||||
byref(self._device_enum))
|
||||
|
||||
# Keep all devices cached, and the callback can keep them updated.
|
||||
self.devices = self._query_all_devices()
|
||||
self.devices: List[Win32AudioDevice] = self._query_all_devices()
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._callback = AudioNotificationCB(self)
|
||||
self._device_enum.RegisterEndpointNotificationCallback(self._callback)
|
||||
|
||||
def get_default_output(self):
|
||||
def add_device(self, pwstrDeviceId: str) -> Win32AudioDevice:
|
||||
dev = self.get_device(pwstrDeviceId)
|
||||
self.devices.append(dev)
|
||||
return dev
|
||||
|
||||
def remove_device(self, pwstrDeviceId: str) -> Win32AudioDevice:
|
||||
dev = self.audio_devices.get_cached_device(pwstrDeviceId)
|
||||
self.audio_devices.devices.remove(dev)
|
||||
return dev
|
||||
|
||||
def get_device(self, pwstrDeviceId: str) -> Win32AudioDevice:
|
||||
device = IMMDevice()
|
||||
|
||||
self._device_enum.GetDevice(pwstrDeviceId, byref(device))
|
||||
dev_id, name, desc, dev_state = self.get_device_info(device)
|
||||
|
||||
ep = IMMEndpoint()
|
||||
device.QueryInterface(IID_IMMEndpoint, byref(ep))
|
||||
|
||||
dataflow = EDataFlow()
|
||||
ep.GetDataFlow(byref(dataflow))
|
||||
flow = dataflow.value
|
||||
|
||||
windevice = Win32AudioDevice(dev_id, name, desc, flow, dev_state)
|
||||
ep.Release()
|
||||
device.Release()
|
||||
return windevice
|
||||
|
||||
def get_default_output(self) -> Optional[Win32AudioDevice]:
|
||||
"""Attempts to retrieve a default audio output for the system. Returns None if no available devices found."""
|
||||
try:
|
||||
device = IMMDevice()
|
||||
@ -207,14 +255,14 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
|
||||
dev_id, name, desc, dev_state = self.get_device_info(device)
|
||||
device.Release()
|
||||
|
||||
pid = self.get_cached_device(dev_id)
|
||||
pid.state = dev_state
|
||||
return pid
|
||||
except OSError:
|
||||
assert _debug("No default audio output was found.")
|
||||
cached_dev = self.get_cached_device(dev_id)
|
||||
cached_dev.state = dev_state
|
||||
return cached_dev
|
||||
except OSError as err:
|
||||
assert _debug("No default audio output was found.", err)
|
||||
return None
|
||||
|
||||
def get_default_input(self):
|
||||
def get_default_input(self) -> Optional[Win32AudioDevice]:
|
||||
"""Attempts to retrieve a default audio input for the system. Returns None if no available devices found."""
|
||||
try:
|
||||
device = IMMDevice()
|
||||
@ -222,37 +270,35 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
|
||||
dev_id, name, desc, dev_state = self.get_device_info(device)
|
||||
device.Release()
|
||||
|
||||
pid = self.get_cached_device(dev_id)
|
||||
pid.state = dev_state
|
||||
return pid
|
||||
except OSError:
|
||||
assert _debug("No default input output was found.")
|
||||
cached_dev = self.get_cached_device(dev_id)
|
||||
cached_dev.state = dev_state
|
||||
return cached_dev
|
||||
except OSError as err:
|
||||
assert _debug("No default input output was found.", err)
|
||||
return None
|
||||
|
||||
def get_cached_device(self, dev_id):
|
||||
def get_cached_device(self, dev_id) -> Win32AudioDevice:
|
||||
"""Gets the cached devices, so we can reduce calls to COM and tell current state vs new states."""
|
||||
for device in self.devices:
|
||||
if device.id == dev_id:
|
||||
return device
|
||||
|
||||
raise Exception("Attempted to get a device that does not exist.")
|
||||
raise Exception("Attempted to get a device that does not exist.", dev_id)
|
||||
|
||||
# return None
|
||||
|
||||
def get_output_devices(self, state=DEVICE_STATE_ACTIVE):
|
||||
def get_output_devices(self, state=DEVICE_STATE_ACTIVE) -> List[Win32AudioDevice]:
|
||||
return [device for device in self.devices if device.state == state and device.flow == eRender]
|
||||
|
||||
def get_input_devices(self, state=DEVICE_STATE_ACTIVE):
|
||||
def get_input_devices(self, state=DEVICE_STATE_ACTIVE) -> List[Win32AudioDevice]:
|
||||
return [device for device in self.devices if device.state == state and device.flow == eCapture]
|
||||
|
||||
def get_all_devices(self):
|
||||
def get_all_devices(self) -> List[Win32AudioDevice]:
|
||||
return self.devices
|
||||
|
||||
def _query_all_devices(self):
|
||||
def _query_all_devices(self) -> List[Win32AudioDevice]:
|
||||
return self.get_devices(flow=eRender, state=DEVICE_STATEMASK_ALL) + self.get_devices(flow=eCapture,
|
||||
state=DEVICE_STATEMASK_ALL)
|
||||
|
||||
def get_device_info(self, device):
|
||||
def get_device_info(self, device: IMMDevice) -> Tuple[str, str, str, int]:
|
||||
"""Return the ID, Name, and Description of the Audio Device."""
|
||||
store = IPropertyStore()
|
||||
device.OpenPropertyStore(STGM_READ, byref(store))
|
||||
@ -293,13 +339,13 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
|
||||
return devices
|
||||
|
||||
@staticmethod
|
||||
def get_pkey_value(store, pkey):
|
||||
def get_pkey_value(store: IPropertyStore, pkey: PROPERTYKEY):
|
||||
try:
|
||||
propvar = PROPVARIANT()
|
||||
store.GetValue(pkey, byref(propvar))
|
||||
value = propvar.pwszVal
|
||||
ole32.PropVariantClear(propvar)
|
||||
except Exception as err:
|
||||
ole32.PropVariantClear(byref(propvar))
|
||||
except Exception:
|
||||
value = "Unknown"
|
||||
|
||||
return value
|
||||
|
@ -165,7 +165,10 @@ class AbstractAudioPlayer(with_metaclass(ABCMeta)):
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
"""Source to play from."""
|
||||
"""Source to play from.
|
||||
May be swapped out for one of an equal audio format, but ensure that
|
||||
the player has been paused and cleared beforehand.
|
||||
"""
|
||||
return self._source
|
||||
|
||||
@source.setter
|
||||
|
@ -69,6 +69,10 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
|
||||
# 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().
|
||||
@ -101,13 +105,13 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
|
||||
|
||||
def play(self):
|
||||
assert _debug('DirectSound play')
|
||||
self.driver.worker.add(self)
|
||||
|
||||
if not self._playing:
|
||||
self._get_audiodata() # prebuffer if needed
|
||||
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):
|
||||
@ -128,6 +132,7 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
|
||||
self._play_cursor = self._write_cursor
|
||||
self._eos_cursor = None
|
||||
self._audiodata_buffer = None
|
||||
self._has_underrun = False
|
||||
del self._events[:]
|
||||
del self._timestamps[:]
|
||||
|
||||
@ -153,10 +158,6 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
|
||||
self.write(None, write_size)
|
||||
write_size = 0
|
||||
|
||||
def _has_underrun(self):
|
||||
return (self._eos_cursor is not None
|
||||
and self._play_cursor > self._eos_cursor)
|
||||
|
||||
def _dispatch_new_event(self, event_name):
|
||||
MediaEvent(event_name).sync_dispatch_to_player(self.player)
|
||||
|
||||
@ -243,9 +244,13 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
|
||||
del self._timestamps[0]
|
||||
|
||||
def _check_underrun(self):
|
||||
if self._playing and self._has_underrun():
|
||||
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.stop()
|
||||
self._has_underrun = True
|
||||
self._dispatch_new_event('on_eos')
|
||||
|
||||
def _get_write_size(self):
|
||||
|
@ -101,6 +101,10 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
|
||||
# Cursor position of end of queued AL buffer.
|
||||
self._write_cursor = 0
|
||||
|
||||
# Whether the source hit its end; protect against duplicate dispatch
|
||||
# of on_eos events.
|
||||
self._has_underrun = False
|
||||
|
||||
# List of currently queued buffer sizes (in bytes)
|
||||
self._buffer_sizes = []
|
||||
|
||||
@ -177,6 +181,7 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
|
||||
self._buffer_cursor = 0
|
||||
self._play_cursor = 0
|
||||
self._write_cursor = 0
|
||||
self._has_underrun = False
|
||||
del self._events[:]
|
||||
del self._buffer_sizes[:]
|
||||
del self._buffer_timestamps[:]
|
||||
@ -278,8 +283,8 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
|
||||
self._queue_events(self._audiodata_buffer)
|
||||
else:
|
||||
assert _debug('No audio data left')
|
||||
if self._has_underrun():
|
||||
assert _debug('Underrun')
|
||||
if self._has_just_underrun():
|
||||
assert _debug('Freshly underrun')
|
||||
MediaEvent('on_eos').sync_dispatch_to_player(self.player)
|
||||
|
||||
def _queue_audio_data(self, audio_data, length):
|
||||
@ -297,12 +302,17 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
|
||||
|
||||
def _queue_events(self, audio_data):
|
||||
for event in audio_data.events:
|
||||
cursor = self._write_cursor + event.timestamp * \
|
||||
self.source.audio_format.bytes_per_second
|
||||
cursor = self._write_cursor + event.timestamp * self.source.audio_format.bytes_per_second
|
||||
self._events.append((cursor, event))
|
||||
|
||||
def _has_underrun(self):
|
||||
return self.alsource.buffers_queued == 0
|
||||
def _has_just_underrun(self):
|
||||
if self._has_underrun:
|
||||
return False
|
||||
|
||||
if self.alsource.buffers_queued == 0:
|
||||
self._has_underrun = True
|
||||
|
||||
return self._has_underrun
|
||||
|
||||
def get_time(self):
|
||||
# Update first, might remove buffers
|
||||
|
@ -89,7 +89,6 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
|
||||
if not self._buffers:
|
||||
self._xa2_driver.return_voice(self._xa2_source_voice)
|
||||
|
||||
|
||||
def play(self):
|
||||
assert _debug('XAudio2 play')
|
||||
|
||||
@ -137,6 +136,8 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
|
||||
Unlike the other drivers this does not carve pieces of audio from the buffer and slowly
|
||||
consume it. This submits the buffer retrieved from the decoder in it's entirety.
|
||||
"""
|
||||
if not self._xa2_source_voice:
|
||||
return
|
||||
|
||||
buffers_queued = self._xa2_source_voice.buffers_queued
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import weakref
|
||||
from collections import namedtuple, defaultdict
|
||||
|
||||
from pyglet.media.devices.base import DeviceFlow
|
||||
|
||||
import pyglet
|
||||
from pyglet.libs.win32.types import *
|
||||
from pyglet.util import debug_print
|
||||
@ -69,7 +71,8 @@ class XAudio2Driver:
|
||||
|
||||
self._players.clear()
|
||||
|
||||
def on_default_changed(self, device):
|
||||
def on_default_changed(self, device, flow: DeviceFlow):
|
||||
if flow == DeviceFlow.OUTPUT:
|
||||
"""Callback derived from the Audio Devices to help us determine when the system no longer has output."""
|
||||
if device is None:
|
||||
assert _debug('Error: Default audio device was removed or went missing.')
|
||||
|
@ -10,143 +10,120 @@ from pyglet.util import debug_print
|
||||
_debug = debug_print('debug_media')
|
||||
|
||||
|
||||
class MediaThread:
|
||||
"""A thread that cleanly exits on interpreter shutdown, and provides
|
||||
a sleep method that can be interrupted and a termination method.
|
||||
|
||||
:Ivariables:
|
||||
`_condition` : threading.Condition
|
||||
Lock _condition on all instance variables.
|
||||
`_stopped` : bool
|
||||
True if `stop` has been called.
|
||||
|
||||
class PlayerWorkerThread(threading.Thread):
|
||||
"""Worker thread for refilling players. Exits on interpreter shutdown,
|
||||
provides a notify method to interrupt it as well as a termination method.
|
||||
"""
|
||||
|
||||
_threads = set()
|
||||
_threads_lock = threading.Lock()
|
||||
|
||||
# Time to wait if there are players, but they're all full:
|
||||
_nap_time = 0.05
|
||||
|
||||
def __init__(self):
|
||||
self._thread = threading.Thread(target=self._thread_run, daemon=True)
|
||||
self._condition = threading.Condition()
|
||||
super().__init__(daemon=True)
|
||||
|
||||
self._rest_event = threading.Event()
|
||||
# A lock that should be held as long as consistency of `self.players` is required.
|
||||
self._operation_lock = threading.Lock()
|
||||
self._stopped = False
|
||||
self.players = set()
|
||||
|
||||
def run(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _thread_run(self):
|
||||
if pyglet.options['debug_trace']:
|
||||
pyglet._install_trace()
|
||||
|
||||
with self._threads_lock:
|
||||
self._threads.add(self)
|
||||
self.run()
|
||||
with self._threads_lock:
|
||||
self._threads.remove(self)
|
||||
|
||||
def start(self):
|
||||
self._thread.start()
|
||||
sleep_time = None
|
||||
|
||||
while True:
|
||||
assert _debug(
|
||||
'PlayerWorkerThread: Going to sleep ' +
|
||||
('indefinitely; no active players' if sleep_time is None else f'for {sleep_time}')
|
||||
)
|
||||
self._rest_event.wait(sleep_time)
|
||||
self._rest_event.clear()
|
||||
|
||||
assert _debug(f'PlayerWorkerThread: woke up @{time.time()}')
|
||||
if self._stopped:
|
||||
break
|
||||
|
||||
with self._operation_lock:
|
||||
if self.players:
|
||||
sleep_time = self._nap_time
|
||||
for player in self.players:
|
||||
player.refill_buffer()
|
||||
else:
|
||||
# sleep until a player is added
|
||||
sleep_time = None
|
||||
|
||||
self._threads.remove(self)
|
||||
|
||||
def stop(self):
|
||||
"""Stop the thread and wait for it to terminate.
|
||||
|
||||
The `stop` instance variable is set to ``True`` and the condition is
|
||||
notified. It is the responsibility of the `run` method to check
|
||||
the value of `stop` after each sleep or wait and to return if set.
|
||||
The `stop` instance variable is set to ``True`` and the rest event
|
||||
is set. It is the responsibility of the `run` method to check
|
||||
the value of `_stopped` after each sleep or wait and to return if
|
||||
set.
|
||||
"""
|
||||
assert _debug('MediaThread.stop()')
|
||||
with self._condition:
|
||||
assert _debug('PlayerWorkerThread.stop()')
|
||||
self._stopped = True
|
||||
self._condition.notify()
|
||||
self._rest_event.set()
|
||||
try:
|
||||
self._thread.join()
|
||||
self.join()
|
||||
except RuntimeError:
|
||||
# Ignore on unclean shutdown
|
||||
pass
|
||||
|
||||
def sleep(self, timeout):
|
||||
"""Wait for some amount of time, or until notified.
|
||||
|
||||
:Parameters:
|
||||
`timeout` : float
|
||||
Time to wait, in seconds.
|
||||
|
||||
"""
|
||||
assert _debug(f'MediaThread.sleep({timeout!r})')
|
||||
with self._condition:
|
||||
if not self._stopped:
|
||||
self._condition.wait(timeout)
|
||||
|
||||
def notify(self):
|
||||
"""Interrupt the current sleep operation.
|
||||
|
||||
If the thread is currently sleeping, it will be woken immediately,
|
||||
instead of waiting the full duration of the timeout.
|
||||
If the thread is not sleeping, it will run again as soon as it is
|
||||
done with its operation.
|
||||
"""
|
||||
assert _debug('MediaThread.notify()')
|
||||
with self._condition:
|
||||
self._condition.notify()
|
||||
assert _debug('PlayerWorkerThread.notify()')
|
||||
self._rest_event.set()
|
||||
|
||||
def add(self, player):
|
||||
"""
|
||||
Add a player to the PlayerWorkerThread; which will call
|
||||
`refill_buffer` on it regularly. Notify the thread as well.
|
||||
|
||||
Do not call this method from within the thread, as it will deadlock.
|
||||
"""
|
||||
assert player is not None
|
||||
assert _debug('PlayerWorkerThread: player added')
|
||||
|
||||
with self._operation_lock:
|
||||
self.players.add(player)
|
||||
|
||||
self.notify()
|
||||
|
||||
def remove(self, player):
|
||||
"""
|
||||
Remove a player from the PlayerWorkerThread, or ignore if it does
|
||||
not exist.
|
||||
|
||||
Do not call this method from within the thread, as it may deadlock.
|
||||
"""
|
||||
assert _debug('PlayerWorkerThread: player removed')
|
||||
|
||||
if player in self.players:
|
||||
with self._operation_lock:
|
||||
self.players.remove(player)
|
||||
|
||||
# self.notify()
|
||||
|
||||
@classmethod
|
||||
def atexit(cls):
|
||||
with cls._threads_lock:
|
||||
threads = list(cls._threads)
|
||||
for thread in threads:
|
||||
for thread in list(cls._threads):
|
||||
thread.stop()
|
||||
# Can't be 100% sure that all threads are stopped here as it is technically possible that
|
||||
# a thread may just have removed itself from cls._threads as the last action in `run()`
|
||||
# and then was unscheduled; But it will definitely finish very soon after anyways
|
||||
|
||||
|
||||
atexit.register(MediaThread.atexit)
|
||||
|
||||
|
||||
class PlayerWorkerThread(MediaThread):
|
||||
"""Worker thread for refilling players."""
|
||||
|
||||
# Time to wait if there are players, but they're all full:
|
||||
_nap_time = 0.05
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.players = set()
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
# This is a big lock, but ensures a player is not deleted while
|
||||
# we're processing it -- this saves on extra checks in the
|
||||
# player's methods that would otherwise have to check that it's
|
||||
# still alive.
|
||||
with self._condition:
|
||||
assert _debug('PlayerWorkerThread: woke up @{}'.format(time.time()))
|
||||
if self._stopped:
|
||||
break
|
||||
sleep_time = -1
|
||||
|
||||
if self.players:
|
||||
filled = False
|
||||
for player in list(self.players):
|
||||
filled = player.refill_buffer()
|
||||
if not filled:
|
||||
sleep_time = self._nap_time
|
||||
else:
|
||||
assert _debug('PlayerWorkerThread: No active players')
|
||||
sleep_time = None # sleep until a player is added
|
||||
|
||||
if sleep_time != -1:
|
||||
self.sleep(sleep_time)
|
||||
else:
|
||||
# We MUST sleep, or we will starve pyglet's main loop. It
|
||||
# also looks like if we don't sleep enough, we'll starve out
|
||||
# various updates that stop us from properly removing players
|
||||
# that should be removed.
|
||||
self.sleep(self._nap_time)
|
||||
|
||||
def add(self, player):
|
||||
assert player is not None
|
||||
assert _debug('PlayerWorkerThread: player added')
|
||||
with self._condition:
|
||||
self.players.add(player)
|
||||
self._condition.notify()
|
||||
|
||||
def remove(self, player):
|
||||
assert _debug('PlayerWorkerThread: player removed')
|
||||
with self._condition:
|
||||
if player in self.players:
|
||||
self.players.remove(player)
|
||||
self._condition.notify()
|
||||
atexit.register(PlayerWorkerThread.atexit)
|
||||
|
@ -114,8 +114,9 @@ class CodecRegistry:
|
||||
will be return if no encoders for that extension are available.
|
||||
"""
|
||||
if filename:
|
||||
extension = os.path.splitext(filename)[1].lower()
|
||||
return self._encoder_extensions.get(extension, [])
|
||||
root, ext = os.path.splitext(filename)
|
||||
extension = ext if ext else root # If only ".ext" is provided
|
||||
return self._encoder_extensions.get(extension.lower(), [])
|
||||
return self._encoders
|
||||
|
||||
def get_decoders(self, filename=None):
|
||||
@ -124,8 +125,9 @@ class CodecRegistry:
|
||||
will be return if no encoders for that extension are available.
|
||||
"""
|
||||
if filename:
|
||||
extension = os.path.splitext(filename)[1].lower()
|
||||
return self._decoder_extensions.get(extension, [])
|
||||
root, ext = os.path.splitext(filename)
|
||||
extension = ext if ext else root # If only ".ext" is provided
|
||||
return self._decoder_extensions.get(extension.lower(), [])
|
||||
return self._decoders
|
||||
|
||||
def add_decoders(self, module):
|
||||
|
Loading…
Reference in New Issue
Block a user