Skip to content

Commit d62182a

Browse files
committed
Major Refactor of Voice Recording
1 parent 88b5943 commit d62182a

File tree

15 files changed

+843
-109
lines changed

15 files changed

+843
-109
lines changed

discord/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from .widget import *
4444
from .object import *
4545
from .reaction import *
46-
from . import utils, opus, abc, ui
46+
from . import utils, opus, abc, ui, sinks
4747
from .enums import *
4848
from .embeds import *
4949
from .mentions import *
@@ -57,7 +57,6 @@
5757
from .sticker import *
5858
from .stage_instance import *
5959
from .interactions import *
60-
from .sink import *
6160
from .components import *
6261
from .threads import *
6362
from .bot import *

discord/errors.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -270,20 +270,6 @@ def __init__(self, shard_id: Optional[int]):
270270
)
271271
super().__init__(msg % shard_id)
272272

273-
class RecordingException(ClientException):
274-
"""Exception that's thrown when there is an error while trying to record
275-
audio from a voice channel.
276-
277-
.. versionadded:: 2.0
278-
"""
279-
pass
280-
281-
class SinkException(ClientException):
282-
"""Raised when a Sink error occurs.
283-
284-
.. versionadded:: 2.0
285-
"""
286-
287273
class InteractionResponded(ClientException):
288274
"""Exception that's raised when sending another interaction response using
289275
:class:`InteractionResponse` when one has already been done before.

discord/opus.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
import time
5353

5454
from .errors import DiscordException, InvalidArgument
55-
from .sink import RawData
55+
from .sinks import RawData
5656

5757
if TYPE_CHECKING:
5858
T = TypeVar("T")

discord/sinks/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""
2+
discord.sinks
3+
~~~~~~~~~~~~~
4+
5+
A place to store all officially given voice sinks.
6+
7+
:copyright: 2021-present Pycord Development
8+
:license: MIT, see LICENSE for more details.
9+
"""
10+
from .core import *
11+
from .errors import *
12+
from .m4a import *
13+
from .mka import *
14+
from .mkv import *
15+
from .mp3 import *
16+
from .mp4 import *
17+
from .ogg import *
18+
from .pcm import *
19+
from .wave import *

discord/sink.py renamed to discord/sinks/core.py

Lines changed: 30 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""
22
The MIT License (MIT)
33
4-
Copyright (c) 2015-2021 Rapptz & (c) 2021-present Pycord-Development
4+
Copyright (c) 2015-2021 Rapptz
5+
Copyright (c) 2021-present Pycord Development
56
67
Permission is hereby granted, free of charge, to any person obtaining a
78
copy of this software and associated documentation files (the "Software"),
@@ -21,17 +22,13 @@
2122
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
2223
DEALINGS IN THE SOFTWARE.
2324
"""
24-
import wave
25-
import logging
2625
import os
26+
import struct
27+
import sys
2728
import threading
2829
import time
29-
import subprocess
30-
import sys
31-
import struct
32-
from .errors import SinkException
3330

34-
_log = logging.getLogger(__name__)
31+
from .errors import SinkException
3532

3633
__all__ = (
3734
"Filters",
@@ -57,21 +54,22 @@
5754
class Filters:
5855
"""Filters for sink
5956
60-
.. versionadded:: 2.0
57+
.. versionadded:: 2.1
6158
6259
Parameters
6360
----------
64-
interface: :meth:`Filters.interface`
65-
61+
container
62+
Container of all Filters.
6663
"""
64+
6765
def __init__(self, **kwargs):
6866
self.filtered_users = kwargs.get("users", default_filters["users"])
6967
self.seconds = kwargs.get("time", default_filters["time"])
7068
self.max_size = kwargs.get("max_size", default_filters["max_size"])
7169
self.finished = False
7270

7371
@staticmethod
74-
def interface(func): # Contains all filters
72+
def container(func): # Contains all filters
7573
def _filter(self, data, user):
7674
if not self.filtered_users or user in self.filtered_users:
7775
return func(self, data, user)
@@ -92,9 +90,8 @@ def wait_and_stop(self):
9290

9391
class RawData:
9492
"""Handles raw data from Discord so that it can be decrypted and decoded to be used.
95-
96-
.. versionadded:: 2.0
9793
94+
.. versionadded:: 2.1
9895
"""
9996

10097
def __init__(self, data, client):
@@ -116,9 +113,7 @@ def __init__(self, data, client):
116113

117114
class AudioData:
118115
"""Handles data that's been completely decrypted and decoded and is ready to be saved to file.
119-
120-
.. versionadded:: 2.0
121-
116+
.. versionadded:: 2.1
122117
Raises
123118
------
124119
ClientException
@@ -158,40 +153,38 @@ def on_format(self, encoding):
158153
class Sink(Filters):
159154
"""A Sink "stores" all the audio data.
160155
161-
.. versionadded:: 2.0
156+
Can be subclassed for extra customizablilty,
157+
158+
.. warning::
159+
It is although recommended you use,
160+
the officially provided sink classes
161+
like :class:`~discord.sinks.WaveSink`
162+
163+
just replace the following like so: ::
164+
vc.start_recording(
165+
MySubClassedSink(),
166+
finished_callback,
167+
ctx.channel,
168+
)
169+
.. versionadded:: 2.1
162170
163171
Parameters
164172
----------
165-
encoding: :class:`string`
166-
The encoding to use. Valid types include wav, mp3, and pcm (even though it's not an actual encoding).
167173
output_path: :class:`string`
168174
A path to where the audio files should be output.
169-
175+
170176
Raises
171177
------
172178
ClientException
173179
An invalid encoding type was specified.
174180
Audio may only be formatted after recording is finished.
175181
"""
176182

177-
valid_encodings = [
178-
"wav",
179-
"mp3",
180-
"pcm",
181-
]
182-
183-
def __init__(self, *, encoding="wav", output_path="", filters=None):
183+
def __init__(self, *, output_path="", filters=None):
184184
if filters is None:
185185
filters = default_filters
186186
self.filters = filters
187187
Filters.__init__(self, **self.filters)
188-
189-
encoding = encoding.lower()
190-
191-
if encoding not in self.valid_encodings:
192-
raise SinkException("An invalid encoding type was specified.")
193-
194-
self.encoding = encoding
195188
self.file_path = output_path
196189
self.vc = None
197190
self.audio_data = {}
@@ -200,7 +193,7 @@ def init(self, vc): # called under listen
200193
self.vc = vc
201194
super().init()
202195

203-
@Filters.interface
196+
@Filters.container
204197
def write(self, data, user):
205198
if user not in self.audio_data:
206199
ssrc = self.vc.get_ssrc(user)
@@ -214,55 +207,4 @@ def cleanup(self):
214207
self.finished = True
215208
for file in self.audio_data.values():
216209
file.cleanup()
217-
self.format_audio(file)
218-
219-
def format_audio(self, audio):
220-
if self.vc.recording:
221-
raise SinkException(
222-
"Audio may only be formatted after recording is finished."
223-
)
224-
if self.encoding == "pcm":
225-
return
226-
if self.encoding == "mp3":
227-
mp3_file = audio.file.split(".")[0] + ".mp3"
228-
args = [
229-
"ffmpeg",
230-
"-f",
231-
"s16le",
232-
"-ar",
233-
"48000",
234-
"-ac",
235-
"2",
236-
"-i",
237-
audio.file,
238-
mp3_file,
239-
]
240-
process = None
241-
if os.path.exists(mp3_file):
242-
os.remove(
243-
mp3_file
244-
) # process will get stuck asking whether or not to overwrite, if file already exists.
245-
try:
246-
process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW)
247-
except FileNotFoundError:
248-
raise SinkException("ffmpeg was not found.") from None
249-
except subprocess.SubprocessError as exc:
250-
raise SinkException(
251-
"Popen failed: {0.__class__.__name__}: {0}".format(exc)
252-
) from exc
253-
process.wait()
254-
elif self.encoding == "wav":
255-
with open(audio.file, "rb") as pcm:
256-
data = pcm.read()
257-
pcm.close()
258-
259-
wav_file = audio.file.split(".")[0] + ".wav"
260-
with wave.open(wav_file, "wb") as f:
261-
f.setnchannels(self.vc.decoder.CHANNELS)
262-
f.setsampwidth(self.vc.decoder.SAMPLE_SIZE // self.vc.decoder.CHANNELS)
263-
f.setframerate(self.vc.decoder.SAMPLING_RATE)
264-
f.writeframes(data)
265-
f.close()
266-
267-
os.remove(audio.file)
268-
audio.on_format(self.encoding)
210+
self.format_audio(file)

discord/sinks/errors.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
"""
2+
The MIT License (MIT)
3+
4+
Copyright (c) 2021-present Pycord Development
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a
7+
copy of this software and associated documentation files (the "Software"),
8+
to deal in the Software without restriction, including without limitation
9+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
10+
and/or sell copies of the Software, and to permit persons to whom the
11+
Software is furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22+
DEALINGS IN THE SOFTWARE.
23+
"""
24+
from discord.errors import DiscordException
25+
26+
27+
class SinkException(DiscordException):
28+
"""Raised when a Sink error occurs.
29+
.. versionadded:: 2.1
30+
"""
31+
32+
33+
class RecordingException(SinkException):
34+
"""Exception that's thrown when there is an error while trying to record
35+
audio from a voice channel.
36+
.. versionadded:: 2.1
37+
"""
38+
39+
pass
40+
41+
42+
class MP3SinkError(SinkException):
43+
"""Exception thrown when a exception occurs with :class:`MP3Sink`
44+
.. versionadded:: 2.1
45+
"""
46+
47+
48+
class MP4SinkError(SinkException):
49+
"""Exception thrown when a exception occurs with :class:`MP4Sink`
50+
.. versionadded:: 2.1
51+
"""
52+
53+
54+
class OGGSinkError(SinkException):
55+
"""Exception thrown when a exception occurs with :class:`OGGSink`
56+
.. versionadded:: 2.1
57+
"""
58+
59+
60+
class MKVSinkError(SinkException):
61+
"""Exception thrown when a exception occurs with :class:`MKVSink`
62+
.. versionadded:: 2.1
63+
"""
64+
65+
66+
class WaveSinkError(SinkException):
67+
"""Exception thrown when a exception occurs with :class:`WaveSink`
68+
.. versionadded:: 2.1
69+
"""
70+
71+
class M4ASinkError(SinkException):
72+
"""Exception thrown when a exception occurs with :class:`M4ASink`
73+
.. versionadded:: 2.1
74+
"""

0 commit comments

Comments
 (0)