diff --git a/discord/opus.py b/discord/opus.py index 966a8ccc6..ab3916138 100644 --- a/discord/opus.py +++ b/discord/opus.py @@ -39,10 +39,17 @@ from .errors import DiscordException 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 @@ -90,9 +97,10 @@ OK = 0 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 @@ -105,6 +113,12 @@ CTL_SET_GAIN = 4034 CTL_LAST_PACKET_DURATION = 4039 # fmt: on +application_ctl: ApplicationCtl = { + 'audio': 2049, + 'voip': 2048, + 'lowdelay': 2051, +} + band_ctl: BandCtl = { 'narrow': 1101, 'medium': 1102, @@ -319,16 +333,38 @@ class _OpusStruct: 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'): @@ -355,7 +391,7 @@ class Encoder(_OpusStruct): 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) diff --git a/discord/voice_client.py b/discord/voice_client.py index 66fa1d2af..8309218a1 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -58,7 +58,7 @@ if TYPE_CHECKING: 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 @@ -569,7 +569,18 @@ class VoiceClient(VoiceProtocol): 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 @@ -579,9 +590,15 @@ class VoiceClient(VoiceProtocol): 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` @@ -590,6 +607,27 @@ class VoiceClient(VoiceProtocol): 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 ------- @@ -599,6 +637,8 @@ class VoiceClient(VoiceProtocol): 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(): @@ -610,8 +650,15 @@ class VoiceClient(VoiceProtocol): 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()