-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Add support to send voice messages #10230
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
4480fea
31344ca
aa5c26f
a7270b2
4a40683
9a4617e
f9ca81a
eb62338
27bca43
d1747b9
2a96d13
8332ca3
5231d51
2e6bfd3
60030d8
1d2ab9c
50cb4f6
3dd7f8f
e5cca7d
8f1d548
9936b0d
8bc906e
dd2fd33
bb4de89
0f3bc42
8bea5c3
394b16e
0f1ded6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,10 @@ | |
|
||
import os | ||
import io | ||
import base64 | ||
from .oggparse import OggStream | ||
from .opus import Decoder | ||
import struct | ||
|
||
from .utils import MISSING | ||
|
||
|
@@ -75,9 +79,37 @@ class File: | |
The file description to display, currently only supported for images. | ||
|
||
.. versionadded:: 2.0 | ||
|
||
voice: :class:`bool` | ||
Whether the file is a voice message. If left unspecified, the :attr:`~File.duration` is used | ||
to determine if the file is a voice message. | ||
|
||
.. note:: | ||
|
||
Voice files must be an audio only format. | ||
|
||
A *non-exhaustive* list of supported formats are: `ogg`, `mp3`, `wav`, `aac`, and `flac`. | ||
|
||
.. versionadded:: 2.6 | ||
|
||
duration: Optional[:class:`float`] | ||
The duration of the voice message in seconds | ||
|
||
.. versionadded:: 2.6 | ||
""" | ||
|
||
__slots__ = ('fp', '_filename', 'spoiler', 'description', '_original_pos', '_owner', '_closer') | ||
__slots__ = ( | ||
'fp', | ||
'_filename', | ||
'spoiler', | ||
'description', | ||
'_original_pos', | ||
'_owner', | ||
'_closer', | ||
'duration', | ||
'_waveform', | ||
'voice', | ||
) | ||
|
||
def __init__( | ||
self, | ||
|
@@ -86,6 +118,9 @@ def __init__( | |
*, | ||
spoiler: bool = MISSING, | ||
description: Optional[str] = None, | ||
voice: bool = MISSING, | ||
duration: Optional[float] = None, | ||
waveform: Optional[list[int]] = None, | ||
): | ||
if isinstance(fp, io.IOBase): | ||
if not (fp.seekable() and fp.readable()): | ||
|
@@ -117,6 +152,22 @@ def __init__( | |
|
||
self.spoiler: bool = spoiler | ||
self.description: Optional[str] = description | ||
self.duration = duration | ||
if waveform is not None: | ||
if len(waveform) > 256: | ||
raise ValueError("Waveforms have a maximum of 256 values") | ||
elif max(waveform) > 255: | ||
raise ValueError("Maximum value of ints is 255 for waveforms") | ||
elif min(waveform) < 0: | ||
raise ValueError("Minimum value of ints is 0 for waveforms") | ||
self._waveform = waveform | ||
|
||
if voice is MISSING: | ||
voice = duration is not None | ||
self.voice = voice | ||
|
||
if duration is None and voice: | ||
raise TypeError('Voice messages must have a duration') | ||
|
||
@property | ||
def filename(self) -> str: | ||
|
@@ -126,6 +177,24 @@ def filename(self) -> str: | |
""" | ||
return 'SPOILER_' + self._filename if self.spoiler else self._filename | ||
|
||
@property | ||
def waveform(self) -> list[int]: | ||
"""List[:class:`int`]: The waveform data for the voice message. | ||
|
||
.. note:: | ||
If a waveform was not given, it will be generated | ||
|
||
Only supports generating the waveform for Opus format files, other files will be given a random waveform | ||
|
||
.. versionadded:: 2.6""" | ||
if self._waveform is None: | ||
try: | ||
self._waveform = self.generate_waveform() | ||
except Exception: | ||
self._waveform = list(os.urandom(256)) | ||
self.reset() | ||
return self._waveform | ||
|
||
@filename.setter | ||
def filename(self, value: str) -> None: | ||
self._filename, self.spoiler = _strip_spoiler(value) | ||
|
@@ -156,4 +225,62 @@ def to_dict(self, index: int) -> Dict[str, Any]: | |
if self.description is not None: | ||
payload['description'] = self.description | ||
|
||
if self.voice: | ||
payload['duration_secs'] = self.duration | ||
payload['waveform'] = base64.b64encode(bytes(self.waveform)).decode('utf-8') | ||
|
||
return payload | ||
|
||
def generate_waveform(self) -> list[int]: | ||
if not self.voice: | ||
raise ValueError("Cannot produce waveform for non voice file") | ||
self.reset() | ||
ogg = OggStream(self.fp) # type: ignore | ||
decoder = Decoder() | ||
waveform: list[int] = [] | ||
prefixes = [b'OpusHead', b'OpusTags'] | ||
for packet in ogg.iter_packets(): | ||
if packet[:8] in prefixes: | ||
continue | ||
blord0 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if b'vorbis' in packet: | ||
raise ValueError("File format is 'vorbis'. Format of 'opus' is required for waveform generation") | ||
|
||
# these are PCM bytes in 16-bit signed little-endian form | ||
decoded = decoder.decode(packet, fec=False) | ||
|
||
# 16 bits -> 2 bytes per sample | ||
num_samples = len(decoded) // 2 | ||
|
||
# https://docs.python.org/3/library/struct.html#byte-order-size-and-alignment | ||
format = '<' + 'h' * num_samples | ||
samples: tuple[int] = struct.unpack(format, decoded) | ||
|
||
waveform.extend(samples) | ||
|
||
# Make sure all values are positive | ||
for i in range(len(waveform)): | ||
if waveform[i] < 0: | ||
waveform[i] = -waveform[i] | ||
|
||
point_count: int = self.duration * 10 # type: ignore | ||
point_count = min(point_count, 255) | ||
points_per_sample: int = len(waveform) // point_count | ||
sample_waveform: list[int] = [] | ||
|
||
total, count = 0, 0 | ||
# Average out the amplitudes for each point within a sample | ||
for i in range(len(waveform)): | ||
total += waveform[i] | ||
count += 1 | ||
if i % points_per_sample == 0: | ||
sample_waveform.append(total // count) | ||
total, count = 0, 0 | ||
|
||
# Maximum value of a waveform is 0xff (255) | ||
highest = max(sample_waveform) | ||
mult = 255 / highest | ||
for i in range(len(sample_waveform)): | ||
sample_waveform[i] = int(sample_waveform[i] * mult) | ||
Comment on lines
+266
to
+284
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already rely on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. audioop is depricated in python 3.11 and removed since 3.13 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We already include There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The part of code you have commented this on doesn't decode or process the Opus data. |
||
|
||
return sample_waveform |
Uh oh!
There was an error while loading. Please reload this page.