diff --git a/discord/sinks/core.py b/discord/sinks/core.py index 4998c35f0f..af8c94068d 100644 --- a/discord/sinks/core.py +++ b/discord/sinks/core.py @@ -27,6 +27,7 @@ import sys import threading import time +import io from ..types import snowflake from .errors import SinkException @@ -125,9 +126,7 @@ class AudioData: """ def __init__(self, file): - self.file = open(file, "ab") - self.dir_path = os.path.split(file)[0] - + self.file = file self.finished = False def write(self, data): @@ -141,16 +140,12 @@ def write(self, data): def cleanup(self): if self.finished: raise SinkException("The AudioData is already finished writing.") - self.file.close() - self.file = os.path.join(self.dir_path, self.file.name) + self.file.seek(0) self.finished = True def on_format(self, encoding): if not self.finished: raise SinkException("The AudioData is still writing.") - name = os.path.split(self.file)[1] - name = name.split(".")[0] + f".{encoding}" - self.file = os.path.join(self.dir_path, name) class Sink(Filters): @@ -169,13 +164,8 @@ class Sink(Filters): finished_callback, ctx.channel, ) - - .. versionadded:: 2.1 - Parameters - ---------- - output_path: :class:`string` - A path to where the audio files should be output. + .. versionadded:: 2.1 Raises ------ @@ -184,12 +174,11 @@ class Sink(Filters): Audio may only be formatted after recording is finished. """ - def __init__(self, *, output_path="", filters=None): + def __init__(self, *, filters=None): if filters is None: filters = default_filters self.filters = filters Filters.__init__(self, **self.filters) - self.file_path = output_path self.vc = None self.audio_data = {} @@ -200,8 +189,7 @@ def init(self, vc): # called under listen @Filters.container def write(self, data, user): if user not in self.audio_data: - ssrc = self.vc.get_ssrc(user) - file = os.path.join(self.file_path, f"{ssrc}.pcm") + file = io.BytesIO() self.audio_data.update({user: AudioData(file)}) file = self.audio_data[user] @@ -215,8 +203,8 @@ def cleanup(self): def get_all_audio(self): """Gets all audio files.""" - return [os.path.realpath(x.file) for x in self.audio_data.values()] - + return [x.file for x in self.audio_data.values()] + def get_user_audio(self, user: snowflake.Snowflake): """Gets the audio file(s) of one specific user.""" - return os.path.realpath(self.audio_data.pop(user)) \ No newline at end of file + return os.path.realpath(self.audio_data.pop(user)) diff --git a/discord/sinks/errors.py b/discord/sinks/errors.py index eb42402d41..e3eaaebc58 100644 --- a/discord/sinks/errors.py +++ b/discord/sinks/errors.py @@ -75,14 +75,16 @@ class WaveSinkError(SinkException): .. versionadded:: 2.1 """ + class M4ASinkError(SinkException): """Exception thrown when a exception occurs with :class:`M4ASink` .. versionadded:: 2.1 """ + class MKASinkError(SinkException): """Exception thrown when a exception occurs with :class:`MKAsSink` .. versionadded:: 2.1 - """ \ No newline at end of file + """ diff --git a/discord/sinks/m4a.py b/discord/sinks/m4a.py index 5c6d457702..11d0cc144a 100644 --- a/discord/sinks/m4a.py +++ b/discord/sinks/m4a.py @@ -21,8 +21,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import io import os import subprocess +import time from .core import CREATE_NO_WINDOW, Filters, Sink, default_filters from .errors import M4ASinkError @@ -35,11 +37,6 @@ class M4ASink(Sink): .. versionadded:: 2.1 - Parameters - ---------- - output_path: :class:`string` - A path to where the audio files should be output. - Raises ------ ClientException @@ -47,14 +44,13 @@ class M4ASink(Sink): Audio may only be formatted after recording is finished. """ - def __init__(self, *, output_path="", filters=None): + def __init__(self, *, filters=None): if filters is None: filters = default_filters self.filters = filters Filters.__init__(self, **self.filters) self.encoding = "m4a" - self.file_path = output_path self.vc = None self.audio_data = {} @@ -63,7 +59,7 @@ def format_audio(self, audio): raise M4ASinkError( "Audio may only be formatted after recording is finished." ) - m4a_file = audio.file.split(".")[0] + ".m4a" + m4a_file = f"{time.time()}.tmp" args = [ "ffmpeg", "-f", @@ -73,16 +69,18 @@ def format_audio(self, audio): "-ac", "2", "-i", - audio.file, + "-", + "-f", + "ipod", m4a_file, ] - process = None if os.path.exists(m4a_file): os.remove( m4a_file ) # process will get stuck asking whether or not to overwrite, if file already exists. try: - process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW) + process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, + stdin=subprocess.PIPE) except FileNotFoundError: raise M4ASinkError("ffmpeg was not found.") from None except subprocess.SubprocessError as exc: @@ -90,7 +88,11 @@ def format_audio(self, audio): "Popen failed: {0.__class__.__name__}: {0}".format(exc) ) from exc - process.wait() + process.communicate(audio.file.read()) + + with open(m4a_file, "rb") as f: + audio.file = io.BytesIO(f.read()) + audio.file.seek(0) + os.remove(m4a_file) - os.remove(audio.file) - audio.on_format(self.encoding) \ No newline at end of file + audio.on_format(self.encoding) diff --git a/discord/sinks/mka.py b/discord/sinks/mka.py index b933ef02b7..6ae7958e59 100644 --- a/discord/sinks/mka.py +++ b/discord/sinks/mka.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import io import os import subprocess @@ -35,11 +36,6 @@ class MKASink(Sink): .. versionadded:: 2.1 - Parameters - ---------- - output_path: :class:`string` - A path to where the audio files should be output. - Raises ------ ClientException @@ -47,14 +43,13 @@ class MKASink(Sink): Audio may only be formatted after recording is finished. """ - def __init__(self, *, output_path="", filters=None): + def __init__(self, *, filters=None): if filters is None: filters = default_filters self.filters = filters Filters.__init__(self, **self.filters) self.encoding = "mka" - self.file_path = output_path self.vc = None self.audio_data = {} @@ -63,7 +58,6 @@ def format_audio(self, audio): raise MKASinkError( "Audio may only be formatted after recording is finished." ) - mka_file = audio.file.split(".")[0] + ".mka" args = [ "ffmpeg", "-f", @@ -73,16 +67,14 @@ def format_audio(self, audio): "-ac", "2", "-i", - audio.file, - mka_file, + "-", + "-f", + "matroska", + "pipe:1" ] - process = None - if os.path.exists(mka_file): - os.remove( - mka_file - ) # process will get stuck asking whether or not to overwrite, if file already exists. try: - process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW) + process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, + stdout=subprocess.PIPE, stdin=subprocess.PIPE) except FileNotFoundError: raise MKASinkError("ffmpeg was not found.") from None except subprocess.SubprocessError as exc: @@ -90,7 +82,8 @@ def format_audio(self, audio): "Popen failed: {0.__class__.__name__}: {0}".format(exc) ) from exc - process.wait() - - os.remove(audio.file) - audio.on_format(self.encoding) \ No newline at end of file + out = process.communicate(audio.file.read())[0] + out = io.BytesIO(out) + out.seek(0) + audio.file = out + audio.on_format(self.encoding) diff --git a/discord/sinks/mkv.py b/discord/sinks/mkv.py index a7398741d7..a1ff086631 100644 --- a/discord/sinks/mkv.py +++ b/discord/sinks/mkv.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import io import os import subprocess @@ -35,11 +36,6 @@ class MKVSink(Sink): .. versionadded:: 2.1 - Parameters - ---------- - output_path: :class:`string` - A path to where the audio files should be output. - Raises ------ ClientException @@ -47,14 +43,13 @@ class MKVSink(Sink): Audio may only be formatted after recording is finished. """ - def __init__(self, *, output_path="", filters=None): + def __init__(self, *, filters=None): if filters is None: filters = default_filters self.filters = filters Filters.__init__(self, **self.filters) self.encoding = "mkv" - self.file_path = output_path self.vc = None self.audio_data = {} @@ -63,7 +58,6 @@ def format_audio(self, audio): raise MKVSinkError( "Audio may only be formatted after recording is finished." ) - mkv_file = audio.file.split(".")[0] + ".mkv" args = [ "ffmpeg", "-f", @@ -73,16 +67,14 @@ def format_audio(self, audio): "-ac", "2", "-i", - audio.file, - mkv_file, + "-", + "-f", + "matroska", + "pipe:1" ] - process = None - if os.path.exists(mkv_file): - os.remove( - mkv_file - ) # process will get stuck asking whether or not to overwrite, if file already exists. try: - process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW) + process = subprocess.Popen(args, #creationflags=CREATE_NO_WINDOW, + stdout=subprocess.PIPE, stdin=subprocess.PIPE) except FileNotFoundError: raise MKVSinkError("ffmpeg was not found.") from None except subprocess.SubprocessError as exc: @@ -90,7 +82,8 @@ def format_audio(self, audio): "Popen failed: {0.__class__.__name__}: {0}".format(exc) ) from exc - process.wait() - - os.remove(audio.file) - audio.on_format(self.encoding) \ No newline at end of file + out = process.communicate(audio.file.read())[0] + out = io.BytesIO(out) + out.seek(0) + audio.file = out + audio.on_format(self.encoding) diff --git a/discord/sinks/mp3.py b/discord/sinks/mp3.py index 7700cce254..cee3b7b457 100644 --- a/discord/sinks/mp3.py +++ b/discord/sinks/mp3.py @@ -21,8 +21,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import io import os import subprocess +import threading +from typing import Optional, IO from .core import CREATE_NO_WINDOW, Filters, Sink, default_filters from .errors import MP3SinkError @@ -47,14 +50,13 @@ class MP3Sink(Sink): Audio may only be formatted after recording is finished. """ - def __init__(self, *, output_path="", filters=None): + def __init__(self, *, filters=None): if filters is None: filters = default_filters self.filters = filters Filters.__init__(self, **self.filters) self.encoding = "mp3" - self.file_path = output_path self.vc = None self.audio_data = {} @@ -63,7 +65,6 @@ def format_audio(self, audio): raise MP3SinkError( "Audio may only be formatted after recording is finished." ) - mp3_file = audio.file.split(".")[0] + ".mp3" args = [ "ffmpeg", "-f", @@ -73,16 +74,14 @@ def format_audio(self, audio): "-ac", "2", "-i", - audio.file, - mp3_file, + "-", + "-f", + "mp3", + "pipe:1" ] - process = None - if os.path.exists(mp3_file): - os.remove( - mp3_file - ) # process will get stuck asking whether or not to overwrite, if file already exists. try: - process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW) + process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, + stdout=subprocess.PIPE, stdin=subprocess.PIPE) except FileNotFoundError: raise MP3SinkError("ffmpeg was not found.") from None except subprocess.SubprocessError as exc: @@ -90,7 +89,8 @@ def format_audio(self, audio): "Popen failed: {0.__class__.__name__}: {0}".format(exc) ) from exc - process.wait() - - os.remove(audio.file) - audio.on_format(self.encoding) \ No newline at end of file + out = process.communicate(audio.file.read())[0] + out = io.BytesIO(out) + out.seek(0) + audio.file = out + audio.on_format(self.encoding) diff --git a/discord/sinks/mp4.py b/discord/sinks/mp4.py index 64783f21ae..7c40f6d24f 100644 --- a/discord/sinks/mp4.py +++ b/discord/sinks/mp4.py @@ -21,8 +21,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import io import os import subprocess +import time from .core import CREATE_NO_WINDOW, Filters, Sink, default_filters from .errors import MP4SinkError @@ -35,11 +37,6 @@ class MP4Sink(Sink): .. versionadded:: 2.1 - Parameters - ---------- - output_path: :class:`string` - A path to where the audio files should be output. - Raises ------ ClientException @@ -47,14 +44,13 @@ class MP4Sink(Sink): Audio may only be formatted after recording is finished. """ - def __init__(self, *, output_path="", filters=None): + def __init__(self, *, filters=None): if filters is None: filters = default_filters self.filters = filters Filters.__init__(self, **self.filters) self.encoding = "mp4" - self.file_path = output_path self.vc = None self.audio_data = {} @@ -63,7 +59,7 @@ def format_audio(self, audio): raise MP4SinkError( "Audio may only be formatted after recording is finished." ) - mp4_file = audio.file.split(".")[0] + ".mp4" + mp4_file = f"{time.time()}.tmp" args = [ "ffmpeg", "-f", @@ -73,16 +69,18 @@ def format_audio(self, audio): "-ac", "2", "-i", - audio.file, + "-", + "-f", + "mp4", mp4_file, ] - process = None if os.path.exists(mp4_file): os.remove( mp4_file ) # process will get stuck asking whether or not to overwrite, if file already exists. try: - process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW) + process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, + stdin=subprocess.PIPE) except FileNotFoundError: raise MP4SinkError("ffmpeg was not found.") from None except subprocess.SubprocessError as exc: @@ -90,7 +88,11 @@ def format_audio(self, audio): "Popen failed: {0.__class__.__name__}: {0}".format(exc) ) from exc - process.wait() + process.communicate(audio.file.read()) + + with open(mp4_file, "rb") as f: + audio.file = io.BytesIO(f.read()) + audio.file.seek(0) + os.remove(mp4_file) - os.remove(audio.file) - audio.on_format(self.encoding) \ No newline at end of file + audio.on_format(self.encoding) diff --git a/discord/sinks/ogg.py b/discord/sinks/ogg.py index fe410ce4a8..d70aa0546a 100644 --- a/discord/sinks/ogg.py +++ b/discord/sinks/ogg.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import io import os import subprocess @@ -35,11 +36,6 @@ class OGGSink(Sink): .. versionadded:: 2.1 - Parameters - ---------- - output_path: :class:`string` - A path to where the audio files should be output. - Raises ------ ClientException @@ -47,14 +43,13 @@ class OGGSink(Sink): Audio may only be formatted after recording is finished. """ - def __init__(self, *, output_path="", filters=None): + def __init__(self, *, filters=None): if filters is None: filters = default_filters self.filters = filters Filters.__init__(self, **self.filters) self.encoding = "ogg" - self.file_path = output_path self.vc = None self.audio_data = {} @@ -63,7 +58,6 @@ def format_audio(self, audio): raise OGGSinkError( "Audio may only be formatted after recording is finished." ) - ogg_file = audio.file.split(".")[0] + ".ogg" args = [ "ffmpeg", "-f", @@ -73,16 +67,14 @@ def format_audio(self, audio): "-ac", "2", "-i", - audio.file, - ogg_file, + "-", + "-f", + "ogg", + "pipe:1" ] - process = None - if os.path.exists(ogg_file): - os.remove( - ogg_file - ) # process will get stuck asking whether or not to overwrite, if file already exists. try: - process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW) + process = subprocess.Popen(args, creationflags=CREATE_NO_WINDOW, + stdin=subprocess.PIPE, stdout=subprocess.PIPE) except FileNotFoundError: raise OGGSinkError("ffmpeg was not found.") from None except subprocess.SubprocessError as exc: @@ -90,7 +82,8 @@ def format_audio(self, audio): "Popen failed: {0.__class__.__name__}: {0}".format(exc) ) from exc - process.wait() - - os.remove(audio.file) - audio.on_format(self.encoding) \ No newline at end of file + out = process.communicate(audio.file.read())[0] + out = io.BytesIO(out) + out.seek(0) + audio.file = out + audio.on_format(self.encoding) diff --git a/discord/sinks/pcm.py b/discord/sinks/pcm.py index c547f0d6ba..ccd89d8062 100644 --- a/discord/sinks/pcm.py +++ b/discord/sinks/pcm.py @@ -31,11 +31,6 @@ class PCMSink(Sink): .. versionadded:: 2.1 - Parameters - ---------- - output_path: :class:`string` - A path to where the audio files should be output. - Raises ------ ClientException @@ -43,16 +38,15 @@ class PCMSink(Sink): Audio may only be formatted after recording is finished. """ - def __init__(self, *, output_path="", filters=None): + def __init__(self, *, filters=None): if filters is None: filters = default_filters self.filters = filters Filters.__init__(self, **self.filters) self.encoding = "ogg" - self.file_path = output_path self.vc = None self.audio_data = {} def format_audio(self, audio): - return \ No newline at end of file + return diff --git a/discord/sinks/wave.py b/discord/sinks/wave.py index fe4b716442..d0774384e9 100644 --- a/discord/sinks/wave.py +++ b/discord/sinks/wave.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ +import io import os import wave @@ -35,13 +36,6 @@ class WaveSink(Sink): .. versionadded:: 2.1 - Parameters - ---------- - encoding: :class:`string` - The encoding to use. Valid types include wav, mp3, and pcm (even though it's not an actual encoding). - output_path: :class:`string` - A path to where the audio files should be output. - Raises ------ ClientException @@ -49,14 +43,13 @@ class WaveSink(Sink): Audio may only be formatted after recording is finished. """ - def __init__(self, *, output_path="", filters=None): + def __init__(self, *, filters=None): if filters is None: filters = default_filters self.filters = filters Filters.__init__(self, **self.filters) self.encoding = "wav" - self.file_path = output_path self.vc = None self.audio_data = {} @@ -65,16 +58,12 @@ def format_audio(self, audio): raise WaveSinkError( "Audio may only be formatted after recording is finished." ) - with open(audio.file, "rb") as pcm: - data = pcm.read() - pcm.close() + 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) - wav_file = audio.file.split(".")[0] + ".wav" - with wave.open(wav_file, "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) - f.writeframes(data) - f.close() - os.remove(audio.file) - audio.on_format(self.encoding) \ No newline at end of file + data.seek(0) + audio.on_format(self.encoding)