Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5a95f98
fix(voice): implement DAVE E2E decryption for voice reception
vito1317 Mar 17, 2026
c9e40d9
feat: Fixes and typesafe discord/voice
Paillat-dev Mar 19, 2026
58518f0
fix: TypeVar ParamSpec
Paillat-dev Mar 19, 2026
fee38b7
chore: Move BytesIO import to the top
Paillat-dev Mar 19, 2026
6caf18f
fix: Better exception catch
Paillat-dev Mar 19, 2026
203a691
fix: Stage channels don't have DAVE
Paillat-dev Mar 19, 2026
fd339e6
fix: Problem
Paillat-dev Mar 19, 2026
884cdc9
chore: Better logging and comments
Paillat-dev Mar 23, 2026
63b6d6a
chore: Minimize WaveSink changes
Paillat-dev Mar 23, 2026
9bb2930
Merge branch 'master' into fix/voice-rec-2
Paillat-dev Mar 23, 2026
e0130cb
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 23, 2026
ce9fbdc
Merge branch 'master' into fix/voice-rec-2
Paillat-dev Mar 24, 2026
923cd8a
Update discord/voice/client.py
Paillat-dev Mar 24, 2026
ec08647
fix(voice): fall back to OPUS_SILENCE when DAVE brute-force uid looku…
orarange Mar 24, 2026
28f404b
fix: Change `HAS_NACL` to `has_nacl` in voice client imports
Paillat-dev Mar 24, 2026
9bf8826
Merge branch 'master' into fix/voice-rec-2
Paillat-dev Mar 24, 2026
757c5ce
docs: CHANGELOG.md
Paillat-dev Mar 24, 2026
7889ca3
Merge branch 'master' into fix/voice-rec-2
Paillat-dev Mar 27, 2026
8dff186
fix: Actually mb that doesn't exist
Paillat-dev Apr 7, 2026
7b2cbea
fix: use client._ssrc_to_id for ssrc_user_map retrieval
Paillat-dev Apr 7, 2026
c48341e
fix: Use PLC instead of silence when buggy packets arrive
Paillat-dev Apr 7, 2026
bdba6a4
feat: Gracefully handle OpusError s
Paillat-dev Apr 7, 2026
1a4e767
fix: Set passthrough 120 bc that's what davey usage docs suggest
Paillat-dev Apr 7, 2026
dda3098
style(pre-commit): auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 7, 2026
374e7f2
fix: Undo VoiceClient Deprecations
Paillat-dev Apr 7, 2026
42cd1b4
Merge branch 'master' into fix/voice-rec-2
Paillat-dev Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ These changes are available on the `master` branch, but have not yet been releas

### Changed

- Added support for Discord DAVE (Audio & Video E2EE) for voice-receive related features
and refactored the voice-reception system.
([#3159](https://github.com/Pycord-Development/pycord/pull/3159))

### Fixed

- Fixed internal use of deprecated role type methods.
Expand Down
15 changes: 9 additions & 6 deletions discord/opus.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,15 @@ def _decode_packet(self, packet: Packet) -> tuple[Packet, bytes]:

if packet:
other_code = False
pcm = self._decoder.decode(packet.decrypted_data, fec=False)
try:
pcm = self._decoder.decode(packet.decrypted_data, fec=False)
except OpusError:
_log.warning(
"Opus decode failed for packet seq=%s, using PLC",
packet.sequence,
exc_info=True,
)
pcm = self._decoder.decode(None, fec=False)

if other_code:
next_packet = self._buffer.peek_next()
Expand All @@ -725,9 +733,4 @@ def _decode_packet(self, packet: Packet) -> tuple[Packet, bytes]:
else:
pcm = self._decoder.decode(None, fec=False)

if HAS_DAVEY:
if user_id is not None and in_dave and dave.can_passthrough(user_id):
_log.debug("User ID %s can passthrough, decrypting with DAVE", user_id)
pcm = dave.decrypt(user_id, davey.MediaType.audio, pcm)

return packet, pcm
31 changes: 28 additions & 3 deletions discord/sinks/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@
from .errors import SinkException

if TYPE_CHECKING:
from ..member import Member
from ..user import User
from ..voice import VoiceClient
from ..voice.packets import VoiceData

__all__ = (
"Filters",
Expand Down Expand Up @@ -210,6 +213,8 @@ class Sink(Filters):
Audio may only be formatted after recording is finished.
"""

__sink_listeners__: list[tuple[str, str]] = []

def __init__(self, *, filters=None):
if filters is None:
filters = default_filters
Expand All @@ -222,18 +227,38 @@ def __init__(self, *, filters=None):
def client(self) -> VoiceClient | None:
return self.vc

@property
def recording(self) -> bool:
"""Whether the voice client is currently recording."""
return self.vc is not None and self.vc.is_recording()

def is_opus(self) -> bool:
"""Whether this sink accepts raw opus packets instead of decoded PCM."""
return False

def walk_children(self):
"""Yields child sinks. Base implementation yields nothing."""
return
yield # make it a generator

def init(self, vc: VoiceClient): # called under listen
self.vc = vc
super().init()

@Filters.container
def write(self, data, user):
def write(self, data: VoiceData | bytes, user: User | Member | None) -> None:
from ..voice.packets import VoiceData

if isinstance(data, VoiceData):
pcm_data = data.pcm
else:
pcm_data = data

if user not in self.audio_data:
file = io.BytesIO()
self.audio_data.update({user: AudioData(file)})

file = self.audio_data[user]
file.write(data)
self.audio_data[user].write(pcm_data)

def cleanup(self):
self.finished = True
Expand Down
2 changes: 1 addition & 1 deletion discord/sinks/m4a.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def format_audio(self, audio):
M4ASinkError
Formatting the audio failed.
"""
if self.vc.recording:
if self.recording:
raise M4ASinkError(
"Audio may only be formatted after recording is finished."
)
Expand Down
2 changes: 1 addition & 1 deletion discord/sinks/mka.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def format_audio(self, audio):
MKASinkError
Formatting the audio failed.
"""
if self.vc.recording:
if self.recording:
raise MKASinkError(
"Audio may only be formatted after recording is finished."
)
Expand Down
2 changes: 1 addition & 1 deletion discord/sinks/mkv.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def format_audio(self, audio):
MKVSinkError
Formatting the audio failed.
"""
if self.vc.recording:
if self.recording:
raise MKVSinkError(
"Audio may only be formatted after recording is finished."
)
Expand Down
2 changes: 1 addition & 1 deletion discord/sinks/mp3.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def format_audio(self, audio):
MP3SinkError
Formatting the audio failed.
"""
if self.vc.recording:
if self.recording:
raise MP3SinkError(
"Audio may only be formatted after recording is finished."
)
Expand Down
2 changes: 1 addition & 1 deletion discord/sinks/mp4.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def format_audio(self, audio):
MP4SinkError
Formatting the audio failed.
"""
if self.vc.recording:
if self.recording:
raise MP4SinkError(
"Audio may only be formatted after recording is finished."
)
Expand Down
2 changes: 1 addition & 1 deletion discord/sinks/ogg.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def format_audio(self, audio):
OGGSinkError
Formatting the audio failed.
"""
if self.vc.recording:
if self.recording:
raise OGGSinkError(
"Audio may only be formatted after recording is finished."
)
Expand Down
18 changes: 12 additions & 6 deletions discord/sinks/wave.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
"""

import wave
from io import BytesIO

from ..opus import Decoder as OpusDecoder
from .core import Filters, Sink, default_filters
from .errors import WaveSinkError

Expand Down Expand Up @@ -54,16 +56,20 @@ def format_audio(self, audio):
WaveSinkError
Formatting the audio failed.
"""
if self.vc.recording:
if self.recording:
raise WaveSinkError(
"Audio may only be formatted after recording is finished."
)
data = audio.file

with wave.open(data, "wb") as f:
f.setnchannels(self.vc.decoder.CHANNELS)
f.setsampwidth(self.vc.decoder.SAMPLE_SIZE // self.vc.decoder.CHANNELS)
f.setframerate(self.vc.decoder.SAMPLING_RATE)
audio.file.seek(0)
pcm_data = audio.file.read()

data = BytesIO()
with wave.open(data, "wb") as f:
f.setnchannels(OpusDecoder.CHANNELS)
f.setsampwidth(OpusDecoder.SAMPLE_SIZE // OpusDecoder.CHANNELS)
f.setframerate(OpusDecoder.SAMPLING_RATE)
f.writeframes(pcm_data)
data.seek(0)
audio.file = data
audio.on_format(self.encoding)
9 changes: 8 additions & 1 deletion discord/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
Iterator,
Literal,
Mapping,
ParamSpec,
Protocol,
Sequence,
TypeVar,
Expand Down Expand Up @@ -176,6 +177,8 @@ class _RequestLike(Protocol):

T = TypeVar("T")
T_co = TypeVar("T_co", covariant=True)
_MC_P = ParamSpec("_MC_P")
_MC_T = TypeVar("_MC_T")
_Iter = Union[Iterator[T], AsyncIterator[T]]


Expand Down Expand Up @@ -880,7 +883,11 @@ def _parse_ratelimit_header(request: Any, *, use_clock: bool = False) -> float:
return (reset - now).total_seconds()


async def maybe_coroutine(f, *args, **kwargs):
async def maybe_coroutine(
f: Callable[_MC_P, _MC_T | Awaitable[_MC_T]],
*args: _MC_P.args,
**kwargs: _MC_P.kwargs,
) -> _MC_T:
value = f(*args, **kwargs)
if _isawaitable(value):
return await value
Expand Down
4 changes: 2 additions & 2 deletions discord/voice/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
from ..errors import MissingVoiceDependenciesError
from ..utils import get_missing_voice_dependencies

if missing := get_missing_voice_dependencies():
raise MissingVoiceDependenciesError(missing=missing)
if _missing := get_missing_voice_dependencies():
raise MissingVoiceDependenciesError(missing=_missing)

from ._types import *
from .client import *
Expand Down
Loading
Loading