From 8b8ce5537863960af05971626c81ae6a1afce0a4 Mon Sep 17 00:00:00 2001
From: Imayhaveborkedit <imayhaveborkedit@users.noreply.github.com>
Date: Wed, 23 Aug 2023 21:04:15 -0400
Subject: [PATCH] Add encoder params to VoiceClient.play

---
 discord/opus.py         | 62 ++++++++++++++++++++++++++++++++---------
 discord/voice_client.py | 55 +++++++++++++++++++++++++++++++++---
 2 files changed, 100 insertions(+), 17 deletions(-)

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()