Skip to content

Commit 74d6590

Browse files
committed
yay voice recv
1 parent 8304dba commit 74d6590

File tree

23 files changed

+2374
-1417
lines changed

23 files changed

+2374
-1417
lines changed

discord/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@
7272
from .template import *
7373
from .threads import *
7474
from .user import *
75-
from .voice import *
7675
from .webhook import *
7776
from .welcome_screen import *
7877
from .widget import *
78+
from ._voice_aliases import *
79+
7980

8081
logging.getLogger(__name__).addHandler(logging.NullHandler())

discord/_voice_aliases.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2015-2021 Rapptz
5+
Copyright (c) 2021-present Pycord Development
6+
7+
Permission is hereby granted, free of charge, to any person obtaining a
8+
copy of this software and associated documentation files (the "Software"),
9+
to deal in the Software without restriction, including without limitation
10+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
11+
and/or sell copies of the Software, and to permit persons to whom the
12+
Software is furnished to do so, subject to the following conditions:
13+
14+
The above copyright notice and this permission notice shall be included in
15+
all copies or substantial portions of the Software.
16+
17+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23+
DEALINGS IN THE SOFTWARE.
24+
"""
25+
from __future__ import annotations
26+
27+
from typing import TYPE_CHECKING
28+
29+
from .utils import warn_deprecated
30+
31+
"""
32+
since discord.voice raises an error when importing it without having the
33+
required package (ie davey) installed, we can't import it in __init__ because
34+
that would break the whole library, that is why this file is here.
35+
36+
the error would still be raised, but at least here we have more freedom on how we are typing it
37+
"""
38+
39+
__all__ = ("VoiceProtocol", "VoiceClient")
40+
41+
42+
if TYPE_CHECKING:
43+
from typing_extensions import deprecated
44+
45+
from discord.voice import VoiceProtocolC, VoiceClientC
46+
47+
@deprecated(
48+
"discord.VoiceClient is deprecated in favour "
49+
"of discord.voice.VoiceClient since 2.7 and "
50+
"will be removed in 3.0",
51+
)
52+
def VoiceClient(client, channel) -> VoiceClientC:
53+
...
54+
55+
@deprecated(
56+
"discord.VoiceProtocol is deprecated in favour "
57+
"of discord.voice.VoiceProtocol since 2.7 and "
58+
"will be removed in 3.0",
59+
)
60+
def VoiceProtocol(client, channel) -> VoiceProtocolC:
61+
...
62+
else:
63+
@warn_deprecated("discord.VoiceClient", "discord.voice.VoiceClient", "2.7", "3.0")
64+
def VoiceClient(client, channel):
65+
from discord.voice import VoiceClient
66+
return VoiceClient(client, channel)
67+
68+
@warn_deprecated("discord.VoiceProtocol", "discord.voice.VoiceProtocol", "2.7", "3.0")
69+
def VoiceProtocol(client, channel):
70+
from discord.voice import VoiceProtocol
71+
return VoiceProtocol(client, channel)

discord/opus.py

Lines changed: 167 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,23 @@
3636
import sys
3737
from typing import TYPE_CHECKING, Any, Callable, Literal, TypedDict, TypeVar
3838

39+
from discord.voice.packets.rtp import FakePacket
40+
from discord.voice.utils.wrapped import gap_wrapped, add_wrapped
41+
from discord.voice.utils.buffer import JitterBuffer
42+
43+
import davey
44+
3945
from .errors import DiscordException
4046
from .sinks import RawData
4147

4248
if TYPE_CHECKING:
49+
from discord.user import User
50+
from discord.member import Member
4351
from discord.voice.client import VoiceClient
52+
from discord.voice.receive.router import PacketRouter
53+
from discord.voice.packets.core import Packet
54+
from discord.voice.packets import VoiceData
55+
from discord.sinks.core import Sink
4456

4557
T = TypeVar("T")
4658
APPLICATION_CTL = Literal["audio", "voip", "lowdelay"]
@@ -102,6 +114,12 @@ class DecoderStruct(ctypes.Structure):
102114
# Error codes
103115
OK = 0
104116
BAD_ARG = -1
117+
BUFF_TOO_SMALL = -2
118+
INTERNAL_ERROR = -3
119+
INVALID_PACKET = -4
120+
UNIMPLEMENTED = -5
121+
INVALID_STATE = -6
122+
ALLOC_FAIL = -7
105123

106124
# Encoder CTLs
107125
application_ctl: ApplicationCtl = {
@@ -449,20 +467,23 @@ def set_fec(self, enabled: bool = True) -> None:
449467
def set_expected_packet_loss_percent(self, percentage: float) -> None:
450468
_lib.opus_encoder_ctl(self._state, CTL_SET_PLP, min(100, max(0, int(percentage * 100)))) # type: ignore
451469

452-
def encode(self, pcm: bytes, frame_size: int) -> bytes:
470+
def encode(self, pcm: bytes, frame_size: int | None = None) -> bytes:
453471
max_data_bytes = len(pcm)
454472
# bytes can be used to reference pointer
455473
pcm_ptr = ctypes.cast(pcm, c_int16_ptr) # type: ignore
456474
data = (ctypes.c_char * max_data_bytes)()
457475

476+
if frame_size is None:
477+
frame_size = self.FRAME_SIZE
478+
458479
ret = _lib.opus_encode(self._state, pcm_ptr, frame_size, data, max_data_bytes)
459480

460481
# array can be initialized with bytes but mypy doesn't know
461482
return array.array("b", data[:ret]).tobytes() # type: ignore
462483

463484

464485
class Decoder(_OpusStruct):
465-
def __init__(self):
486+
def __init__(self) -> None:
466487
_OpusStruct.get_opus_version()
467488

468489
self._state = self._create_state()
@@ -521,18 +542,18 @@ def _get_last_packet_duration(self):
521542
_lib.opus_decoder_ctl(self._state, CTL_LAST_PACKET_DURATION, ctypes.byref(ret))
522543
return ret.value
523544

524-
def decode(self, data, *, fec=False):
545+
def decode(self, data: bytes | None, *, fec: bool = True):
525546
if data is None and fec:
526547
raise OpusError(
527548
message="Invalid arguments: FEC cannot be used with null data"
528549
)
529550

551+
channel_count = self.CHANNELS
552+
530553
if data is None:
531554
frame_size = self._get_last_packet_duration() or self.SAMPLES_PER_FRAME
532-
channel_count = self.CHANNELS
533555
else:
534556
frames = self.packet_get_nb_frames(data)
535-
channel_count = self.CHANNELS
536557
samples_per_frame = self.packet_get_samples_per_frame(data)
537558
frame_size = frames * samples_per_frame
538559

@@ -542,9 +563,150 @@ def decode(self, data, *, fec=False):
542563
# )()
543564
pcm = (ctypes.c_int16 * (frame_size * channel_count))()
544565
pcm_ptr = ctypes.cast(pcm, c_int16_ptr)
566+
pcm_ptr = ctypes.cast(
567+
pcm,
568+
c_int16_ptr,
569+
)
545570

546571
ret = _lib.opus_decode(
547572
self._state, data, len(data) if data else 0, pcm_ptr, frame_size, fec
548573
)
549574

550575
return array.array("h", pcm[: ret * channel_count]).tobytes()
576+
577+
578+
class PacketDecoder:
579+
def __init__(self, router: PacketRouter, ssrc: int) -> None:
580+
self.router: PacketRouter = router
581+
self.ssrc: int = ssrc
582+
583+
self._decoder: Decoder | None = None if self.sink.is_opus() else Decoder()
584+
self._buffer: JitterBuffer = JitterBuffer()
585+
self._cached_id: int | None = None
586+
587+
self._last_seq: int = -1
588+
self._last_ts: int = -1
589+
590+
@property
591+
def sink(self) -> Sink:
592+
return self.router.sink
593+
594+
def _get_user(self, user_id: int) -> User | Member | None:
595+
vc: VoiceClient = self.sink.client # type: ignore
596+
return vc.guild.get_member(user_id) or vc.client.get_user(user_id)
597+
598+
def _get_cached_member(self) -> User | Member | None:
599+
return self._get_user(self._cached_id) if self._cached_id else None
600+
601+
def _flag_ready_state(self) -> None:
602+
if self._buffer.peek():
603+
self.router.waiter.register(self)
604+
else:
605+
self.router.waiter.unregister(self)
606+
607+
def push_packet(self, packet: Packet) -> None:
608+
self._buffer.push(packet)
609+
self._flag_ready_state()
610+
611+
def pop_data(self, *, timeout: float = 0) -> VoiceData | None:
612+
packet = self._get_next_packet(timeout)
613+
self._flag_ready_state()
614+
615+
if packet is None:
616+
return None
617+
return self._process_packet(packet)
618+
619+
def set_user_id(self, user_id: int) -> None:
620+
self._cached_id = user_id
621+
622+
def reset(self) -> None:
623+
self._buffer.reset()
624+
self._decoder = None if self.sink.is_opus() else Decoder()
625+
self._last_seq = self._last_ts = -1
626+
self._flag_ready_state()
627+
628+
def destroy(self) -> None:
629+
self._buffer.reset()
630+
self._decoder = None
631+
self._flag_ready_state()
632+
633+
def _get_next_packet(self, timeout: float) -> Packet | None:
634+
packet = self._buffer.pop(timeout=timeout)
635+
636+
if packet is None:
637+
if self._buffer:
638+
packets = self._buffer.flush()
639+
if any(packets[1:]):
640+
_log.warning(
641+
"%s packets were lost being flushed in decoder-%s",
642+
len(packets) - 1,
643+
self.ssrc,
644+
)
645+
return packets[0]
646+
return
647+
elif not packet:
648+
packet = self._make_fakepacket()
649+
return packet
650+
651+
def _make_fakepacket(self) -> FakePacket:
652+
seq = add_wrapped(self._last_seq, 1)
653+
ts = add_wrapped(self._last_ts, Decoder.SAMPLES_PER_FRAME, wrap=2**32)
654+
return FakePacket(self.ssrc, seq, ts)
655+
656+
def _process_packet(self, packet: Packet) -> VoiceData:
657+
from discord.object import Object
658+
659+
pcm = None
660+
661+
if not self.sink.is_opus():
662+
packet, pcm = self._decode_packet(packet)
663+
664+
member = self._get_cached_member()
665+
666+
if member is None:
667+
self._cached_id = self.sink.client._connection._get_id_from_ssrc(self.ssrc)
668+
member = self._get_cached_member()
669+
670+
# yet still none, use Object
671+
if member is None and self._cached_id:
672+
member = Object(id=self._cached_id)
673+
674+
data = VoiceData(packet, member, pcm=pcm)
675+
self._last_seq = packet.sequence
676+
self._last_ts = packet.timestamp
677+
return data
678+
679+
def _decode_packet(self, packet: Packet) -> tuple[Packet, bytes]:
680+
assert self._decoder is not None
681+
assert self.sink.client
682+
683+
user_id: int | None = self._cached_id
684+
dave: davey.DaveSession | None = self.sink.client._connection.dave_session
685+
in_dave = dave is not None
686+
687+
# personally, the best variable
688+
other_code = True
689+
690+
if packet:
691+
other_code = False
692+
pcm = self._decoder.decode(packet.decrypted_data, fec=False)
693+
694+
if other_code:
695+
next_packet = self._buffer.peek_next()
696+
697+
if next_packet is not None:
698+
nextdata: bytes = next_packet.decrypted_data # type: ignore
699+
700+
_log.debug(
701+
"Generating fec packet: fake=%s, fec=%s",
702+
packet.sequence,
703+
next_packet.sequence,
704+
)
705+
pcm = self._decoder.decode(nextdata, fec=True)
706+
else:
707+
pcm = self._decoder.decode(None, fec=False)
708+
709+
if user_id is not None and in_dave and dave.can_passthrough(user_id):
710+
pcm = dave.decrypt(user_id, davey.MediaType.audio, pcm)
711+
712+
return packet, pcm

0 commit comments

Comments
 (0)