Browse Source

Add PCMVolumeTransformer to augment volume of a PCM stream.

This also introduces the idea of replacing the VoiceClient.source on
the fly. Note that this internally pauses and resumes the audio
stream.
pull/546/head
Rapptz 8 years ago
parent
commit
f5cfc96aaf
  1. 58
      discord/player.py
  2. 15
      discord/voice_client.py
  3. 3
      docs/api.rst

58
discord/player.py

@ -26,13 +26,14 @@ DEALINGS IN THE SOFTWARE.
import threading import threading
import subprocess import subprocess
import audioop
import shlex import shlex
import time import time
from .errors import ClientException from .errors import ClientException
from .opus import Encoder as OpusEncoder from .opus import Encoder as OpusEncoder
__all__ = [ 'AudioSource', 'PCMAudio', 'FFmpegPCMAudio' ] __all__ = [ 'AudioSource', 'PCMAudio', 'FFmpegPCMAudio', 'PCMVolumeTransformer' ]
class AudioSource: class AudioSource:
"""Represents an audio stream. """Represents an audio stream.
@ -169,6 +170,51 @@ class FFmpegPCMAudio(AudioSource):
if proc.poll() is None: if proc.poll() is None:
proc.communicate() proc.communicate()
class PCMVolumeTransformer(AudioSource):
"""Transforms a previous :class:`AudioSource` to have volume controls.
This does not work on audio sources that have :meth:`AudioSource.is_opus`
set to ``True``.
Parameters
------------
original: :class:`AudioSource`
The original AudioSource to transform.
Raises
-------
TypeError
Not an audio source.
ClientException
The audio source is opus encoded.
"""
def __init__(self, original):
if not isinstance(original, AudioSource):
raise TypeError('expected AudioSource not {0.__class__.__name__}.'.format(original))
if original.is_opus():
raise ClientException('AudioSource must not be Opus encoded.')
self.original = original
self._volume = 1.0
@property
def volume(self):
"""Retrieves or sets the volume as a floating point percentage (e.g. 1.0 for 100%)."""
return self._volume
@volume.setter
def volume(self, value):
self._volume = max(value, 0.0)
def cleanup(self):
self.original.cleanup()
def read(self):
ret = self.original.read()
return audioop.mul(ret, 2, min(self._volume, 2.0))
class AudioPlayer(threading.Thread): class AudioPlayer(threading.Thread):
DELAY = OpusEncoder.FRAME_LENGTH / 1000.0 DELAY = OpusEncoder.FRAME_LENGTH / 1000.0
@ -184,6 +230,7 @@ class AudioPlayer(threading.Thread):
self._resumed.set() # we are not paused self._resumed.set() # we are not paused
self._current_error = None self._current_error = None
self._connected = client._connected self._connected = client._connected
self._lock = threading.Lock()
if after is not None and not callable(after): if after is not None and not callable(after):
raise TypeError('Expected a callable for the "after" parameter.') raise TypeError('Expected a callable for the "after" parameter.')
@ -191,7 +238,6 @@ class AudioPlayer(threading.Thread):
def _do_run(self): def _do_run(self):
self.loops = 0 self.loops = 0
self._start = time.time() self._start = time.time()
is_opus = self.source.is_opus()
# getattr lookup speed ups # getattr lookup speed ups
play_audio = self.client.send_audio_packet play_audio = self.client.send_audio_packet
@ -217,7 +263,7 @@ class AudioPlayer(threading.Thread):
self.stop() self.stop()
break break
play_audio(data, encode=not is_opus) play_audio(data, encode=not self.source.is_opus())
next_time = self._start + self.DELAY * self.loops next_time = self._start + self.DELAY * self.loops
delay = max(0, self.DELAY + (next_time - time.time())) delay = max(0, self.DELAY + (next_time - time.time()))
time.sleep(delay) time.sleep(delay)
@ -255,3 +301,9 @@ class AudioPlayer(threading.Thread):
def is_paused(self): def is_paused(self):
return not self._end.is_set() and not self._resumed.is_set() return not self._end.is_set() and not self._resumed.is_set()
def _set_source(self, source):
with self._lock:
self.pause()
self.source = source
self.resume()

15
discord/voice_client.py

@ -359,9 +359,22 @@ class VoiceClient:
@property @property
def source(self): def source(self):
"""Optional[:class:`AudioSource`]: The audio source being played, if playing.""" """Optional[:class:`AudioSource`]: The audio source being played, if playing.
This property can also be used to change the audio source currently being played.
"""
return self._player.source if self._player else None return self._player.source if self._player else None
@source.setter
def source(self, value):
if not isinstance(value, AudioSource):
raise TypeError('expected AudioSource not {0.__class__.__name__}.'.format(value))
if self._player is None:
raise ValueError('Not playing anything.')
self._player._set_source(value)
def send_audio_packet(self, data, *, encode=True): def send_audio_packet(self, data, *, encode=True):
"""Sends an audio packet composed of the data. """Sends an audio packet composed of the data.

3
docs/api.rst

@ -55,6 +55,9 @@ Voice
.. autoclass:: FFmpegPCMAudio .. autoclass:: FFmpegPCMAudio
:members: :members:
.. autoclass:: PCMVolumeTransformer
:members:
Opus Library Opus Library
~~~~~~~~~~~~~ ~~~~~~~~~~~~~

Loading…
Cancel
Save