Skip to content

Commit

Permalink
Add encoder params to VoiceClient.play
Browse files Browse the repository at this point in the history
  • Loading branch information
imayhaveborkedit committed Aug 24, 2023
1 parent b276f3f commit 8b8ce55
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 17 deletions.
62 changes: 49 additions & 13 deletions discord/opus.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,17 @@

if TYPE_CHECKING:
T = TypeVar('T')
APPLICATION_CTL = Literal['audio', 'voip', 'lowdelay']
BAND_CTL = Literal['narrow', 'medium', 'wide', 'superwide', 'full']
SIGNAL_CTL = Literal['auto', 'voice', 'music']


class ApplicationCtl(TypedDict):
audio: int
voip: int
lowdelay: int


class BandCtl(TypedDict):
narrow: int
medium: int
Expand Down Expand Up @@ -90,9 +97,10 @@ class DecoderStruct(ctypes.Structure):
BAD_ARG = -1

# Encoder CTLs
APPLICATION_AUDIO = 2049
APPLICATION_VOIP = 2048
APPLICATION_LOWDELAY = 2051
APPLICATION_AUDIO = 'audio'
APPLICATION_VOIP = 'voip'
APPLICATION_LOWDELAY = 'lowdelay'
# These remain as strings for backwards compat

CTL_SET_BITRATE = 4002
CTL_SET_BANDWIDTH = 4008
Expand All @@ -105,6 +113,12 @@ class DecoderStruct(ctypes.Structure):
CTL_LAST_PACKET_DURATION = 4039
# fmt: on

application_ctl: ApplicationCtl = {
'audio': 2049,
'voip': 2048,
'lowdelay': 2051,
}

band_ctl: BandCtl = {
'narrow': 1101,
'medium': 1102,
Expand Down Expand Up @@ -319,16 +333,38 @@ def get_opus_version() -> str:


class Encoder(_OpusStruct):
def __init__(self, application: int = APPLICATION_AUDIO):
_OpusStruct.get_opus_version()

self.application: int = application
def __init__(
self,
*,
application: APPLICATION_CTL = 'audio',
bitrate: int = 128,
fec: bool = True,
expected_packet_loss: float = 0.15,
bandwidth: BAND_CTL = 'full',
signal_type: SIGNAL_CTL = 'auto',
):
if application not in application_ctl:
raise ValueError(f'{application} is not a valid application setting. Try one of: {"".join(application_ctl)}')

if not 16 <= bitrate <= 512:
raise ValueError(f'bitrate must be between 16 and 512, not {bitrate}')

if not 0 < expected_packet_loss <= 1.0:
raise ValueError(
f'expected_packet_loss must be a positive number less than or equal to 1, not {expected_packet_loss}'
)

_OpusStruct.get_opus_version() # lazy loads the opus library

self.application: int = application_ctl[application]
self._state: EncoderStruct = self._create_state()
self.set_bitrate(128)
self.set_fec(True)
self.set_expected_packet_loss_percent(0.15)
self.set_bandwidth('full')
self.set_signal_type('auto')

self.set_bitrate(bitrate)
self.set_fec(fec)
if fec:
self.set_expected_packet_loss_percent(expected_packet_loss)
self.set_bandwidth(bandwidth)
self.set_signal_type(signal_type)

def __del__(self) -> None:
if hasattr(self, '_state'):
Expand All @@ -355,7 +391,7 @@ def set_bandwidth(self, req: BAND_CTL) -> None:

def set_signal_type(self, req: SIGNAL_CTL) -> None:
if req not in signal_ctl:
raise KeyError(f'{req!r} is not a valid bandwidth setting. Try one of: {",".join(signal_ctl)}')
raise KeyError(f'{req!r} is not a valid signal type setting. Try one of: {",".join(signal_ctl)}')

k = signal_ctl[req]
_lib.opus_encoder_ctl(self._state, CTL_SET_SIGNAL, k)
Expand Down
55 changes: 51 additions & 4 deletions discord/voice_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
from .guild import Guild
from .state import ConnectionState
from .user import ClientUser
from .opus import Encoder
from .opus import Encoder, APPLICATION_CTL, BAND_CTL, SIGNAL_CTL
from .channel import StageChannel, VoiceChannel
from . import abc

Expand Down Expand Up @@ -569,7 +569,18 @@ def _encrypt_xsalsa20_poly1305_lite(self, header: bytes, data) -> bytes:

return header + box.encrypt(bytes(data), bytes(nonce)).ciphertext + nonce[:4]

def play(self, source: AudioSource, *, after: Optional[Callable[[Optional[Exception]], Any]] = None) -> None:
def play(
self,
source: AudioSource,
*,
after: Optional[Callable[[Optional[Exception]], Any]] = None,
application: APPLICATION_CTL = 'audio',
bitrate: int = 128,
fec: bool = True,
expected_packet_loss: float = 0.15,
bandwidth: BAND_CTL = 'full',
signal_type: SIGNAL_CTL = 'auto',
) -> None:
"""Plays an :class:`AudioSource`.
The finalizer, ``after`` is called after the source has been exhausted
Expand All @@ -579,9 +590,15 @@ def play(self, source: AudioSource, *, after: Optional[Callable[[Optional[Except
caught and the audio player is then stopped. If no after callback is
passed, any caught exception will be logged using the library logger.
Extra parameters may be passed to the internal opus encoder if a PCM based
source is used. Otherwise, they are ignored.
.. versionchanged:: 2.0
Instead of writing to ``sys.stderr``, the library's logger is used.
.. versionchanged:: 2.4
Added encoder parameters as keyword arguments.
Parameters
-----------
source: :class:`AudioSource`
Expand All @@ -590,6 +607,27 @@ def play(self, source: AudioSource, *, after: Optional[Callable[[Optional[Except
The finalizer that is called after the stream is exhausted.
This function must have a single parameter, ``error``, that
denotes an optional exception that was raised during playing.
application: :class:`str`
Configures the encoder's intended application. Can be one of:
``'audio'``, ``'voip'``, ``'lowdelay'``.
Defaults to ``'audio'``.
bitrate: :class:`int`
Configures the bitrate in the encoder. Can be between ``16`` and ``512``.
Defaults to ``128``.
fec: :class:`bool`
Configures the encoder's use of inband forward error correction.
Defaults to ``True``.
expected_packet_loss: :class:`float`
Configures the encoder's expected packet loss percentage. Requires FEC.
Defaults to ``0.15``.
bandwidth: :class:`str`
Configures the encoder's bandpass. Can be one of:
``'narrow'``, ``'medium'``, ``'wide'``, ``'superwide'``, ``'full'``.
Defaults to ``'full'``.
signal_type: :class:`str`
Configures the type of signal being encoded. Can be one of:
``'auto'``, ``'voice'``, ``'music'``.
Defaults to ``'auto'``.
Raises
-------
Expand All @@ -599,6 +637,8 @@ def play(self, source: AudioSource, *, after: Optional[Callable[[Optional[Except
Source is not a :class:`AudioSource` or after is not a callable.
OpusNotLoaded
Source is not opus encoded and opus is not loaded.
ValueError
An improper value was passed as an encoder parameter.
"""

if not self.is_connected():
Expand All @@ -610,8 +650,15 @@ def play(self, source: AudioSource, *, after: Optional[Callable[[Optional[Except
if not isinstance(source, AudioSource):
raise TypeError(f'source must be an AudioSource not {source.__class__.__name__}')

if not self.encoder and not source.is_opus():
self.encoder = opus.Encoder()
if not source.is_opus():
self.encoder = opus.Encoder(
application=application,
bitrate=bitrate,
fec=fec,
expected_packet_loss=expected_packet_loss,
bandwidth=bandwidth,
signal_type=signal_type,
)

self._player = AudioPlayer(source, self, after=after)
self._player.start()
Expand Down

0 comments on commit 8b8ce55

Please sign in to comment.