Audio
class
AudioBuffer
¶
Source code in naff/api/voice/audio.py
class AudioBuffer:
def __init__(self) -> None:
self._buffer = bytearray()
self._lock = threading.Lock()
self.initialised = threading.Event()
def __len__(self) -> int:
return len(self._buffer)
def extend(self, data: bytes) -> None:
"""
Extend the buffer with additional data.
Args:
data: The data to add
"""
with self._lock:
self._buffer.extend(data)
def read(self, total_bytes: int) -> bytearray:
"""
Read `total_bytes` bytes of audio from the buffer.
Args:
total_bytes: Amount of bytes to read.
Returns:
Desired amount of bytes
"""
with self._lock:
view = memoryview(self._buffer)
self._buffer = bytearray(view[total_bytes:])
data = bytearray(view[:total_bytes])
if 0 < len(data) < total_bytes:
# pad incomplete frames with 0's
data.extend(b"\0" * (total_bytes - len(data)))
return data
method
extend(self, data)
¶
Extend the buffer with additional data.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
data |
bytes |
The data to add |
required |
Source code in naff/api/voice/audio.py
def extend(self, data: bytes) -> None:
"""
Extend the buffer with additional data.
Args:
data: The data to add
"""
with self._lock:
self._buffer.extend(data)
method
read(self, total_bytes)
¶
Read total_bytes
bytes of audio from the buffer.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
total_bytes |
int |
Amount of bytes to read. |
required |
Returns:
Type | Description |
---|---|
bytearray |
Desired amount of bytes |
Source code in naff/api/voice/audio.py
def read(self, total_bytes: int) -> bytearray:
"""
Read `total_bytes` bytes of audio from the buffer.
Args:
total_bytes: Amount of bytes to read.
Returns:
Desired amount of bytes
"""
with self._lock:
view = memoryview(self._buffer)
self._buffer = bytearray(view[total_bytes:])
data = bytearray(view[:total_bytes])
if 0 < len(data) < total_bytes:
# pad incomplete frames with 0's
data.extend(b"\0" * (total_bytes - len(data)))
return data
class
BaseAudio (ABC)
¶
Base structure of the audio.
Source code in naff/api/voice/audio.py
class BaseAudio(ABC):
"""Base structure of the audio."""
locked_stream: bool
"""Prevents the audio task from closing automatically when no data is received."""
needs_encode: bool
"""Does this audio data need encoding with opus?"""
bitrate: Optional[int]
"""Optionally specify a specific bitrate to encode this audio data with"""
def __del__(self) -> None:
self.cleanup()
def cleanup(self) -> None:
"""A method to optionally cleanup after this object is no longer required."""
...
@property
def audio_complete(self) -> bool:
"""A property to tell the player if more audio is expected."""
return False
@abstractmethod
def read(self, frame_size: int) -> bytes:
"""
Reads frame_size ms of audio from source.
returns:
bytes of audio
"""
...
method
cleanup(self)
¶
A method to optionally cleanup after this object is no longer required.
Source code in naff/api/voice/audio.py
def cleanup(self) -> None:
"""A method to optionally cleanup after this object is no longer required."""
...
property
readonly
audio_complete: bool
¶
A property to tell the player if more audio is expected.
method
read(self, frame_size)
¶
Reads frame_size ms of audio from source.
Returns:
Type | Description |
---|---|
bytes |
bytes of audio |
Source code in naff/api/voice/audio.py
@abstractmethod
def read(self, frame_size: int) -> bytes:
"""
Reads frame_size ms of audio from source.
returns:
bytes of audio
"""
...
class
Audio (BaseAudio)
¶
Audio for playing from file or URL.
Source code in naff/api/voice/audio.py
class Audio(BaseAudio):
"""Audio for playing from file or URL."""
source: str
"""The source ffmpeg should use to play the audio"""
process: subprocess.Popen
"""The ffmpeg process to use"""
buffer: AudioBuffer
"""The audio objects buffer to prevent stuttering"""
buffer_seconds: int
"""How many seconds of audio should be buffered"""
read_ahead_task: threading.Thread
"""A thread that reads ahead to create the buffer"""
ffmpeg_args: str | list[str]
"""Args to pass to ffmpeg"""
ffmpeg_before_args: str | list[str]
"""Args to pass to ffmpeg before the source"""
def __init__(self, src: Union[str, Path]) -> None:
self.source = src
self.needs_encode = True
self.locked_stream = False
self.process: Optional[subprocess.Popen] = None
self.buffer = AudioBuffer()
self.buffer_seconds = 3
self.read_ahead_task = threading.Thread(target=self._read_ahead, daemon=True)
self.ffmpeg_before_args = ""
self.ffmpeg_args = ""
def __repr__(self) -> str:
return f"<{type(self).__name__}: {self.source}>"
@property
def _max_buffer_size(self) -> int:
# 1ms of audio * (buffer seconds * 1000)
return 192 * (self.buffer_seconds * 1000)
@property
def audio_complete(self) -> bool:
"""Uses the state of the subprocess to determine if more audio is coming"""
if self.process:
if self.process.poll() is None:
return False
return True
def _create_process(self, *, block: bool = True) -> None:
before = (
self.ffmpeg_before_args if isinstance(self.ffmpeg_before_args, list) else self.ffmpeg_before_args.split()
)
after = self.ffmpeg_args if isinstance(self.ffmpeg_args, list) else self.ffmpeg_args.split()
cmd = [
"ffmpeg",
"-i",
self.source,
"-f",
"s16le",
"-ar",
"48000",
"-ac",
"2",
"-loglevel",
"warning",
"pipe:1",
"-vn",
]
cmd[1:1] = before
cmd.extend(after)
self.process = subprocess.Popen( # noqa: S603
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL
)
self.read_ahead_task.start()
if block:
# block until some data is in the buffer
self.buffer.initialised.wait()
def _read_ahead(self) -> None:
while self.process:
if self.process.poll() is not None:
# ffmpeg has exited, stop reading ahead
if not self.buffer.initialised.is_set():
# assume this is a small file and initialise the buffer
self.buffer.initialised.set()
return
if not len(self.buffer) >= self._max_buffer_size:
self.buffer.extend(self.process.stdout.read(3840))
else:
if not self.buffer.initialised.is_set():
self.buffer.initialised.set()
time.sleep(0.1)
def pre_buffer(self, duration: None | float = None) -> None:
"""
Start pre-buffering the audio.
Args:
duration: The duration of audio to pre-buffer.
"""
if duration:
self.buffer_seconds = duration
if self.process and self.process.poll() is None:
raise RuntimeError("Cannot pre-buffer an already running process")
# sanity value enforcement to prevent audio weirdness
self.buffer = AudioBuffer()
self.buffer.initialised.clear()
self._create_process(block=False)
def read(self, frame_size: int) -> bytes:
"""
Reads frame_size bytes of audio from the buffer.
returns:
bytes of audio
"""
if not self.process:
self._create_process()
if not self.buffer.initialised.is_set():
# we cannot start playing until the buffer is initialised
self.buffer.initialised.wait()
data = self.buffer.read(frame_size)
if len(data) != frame_size:
data = b""
return bytes(data)
def cleanup(self) -> None:
"""Cleans up after this audio object."""
if self.process and self.process.poll() is None:
self.process.kill()
self.process.wait()
property
readonly
audio_complete: bool
¶
Uses the state of the subprocess to determine if more audio is coming
method
pre_buffer(self, duration)
¶
Start pre-buffering the audio.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
duration |
None | float |
The duration of audio to pre-buffer. |
None |
Source code in naff/api/voice/audio.py
def pre_buffer(self, duration: None | float = None) -> None:
"""
Start pre-buffering the audio.
Args:
duration: The duration of audio to pre-buffer.
"""
if duration:
self.buffer_seconds = duration
if self.process and self.process.poll() is None:
raise RuntimeError("Cannot pre-buffer an already running process")
# sanity value enforcement to prevent audio weirdness
self.buffer = AudioBuffer()
self.buffer.initialised.clear()
self._create_process(block=False)
method
read(self, frame_size)
¶
Reads frame_size bytes of audio from the buffer.
Returns:
Type | Description |
---|---|
bytes |
bytes of audio |
Source code in naff/api/voice/audio.py
def read(self, frame_size: int) -> bytes:
"""
Reads frame_size bytes of audio from the buffer.
returns:
bytes of audio
"""
if not self.process:
self._create_process()
if not self.buffer.initialised.is_set():
# we cannot start playing until the buffer is initialised
self.buffer.initialised.wait()
data = self.buffer.read(frame_size)
if len(data) != frame_size:
data = b""
return bytes(data)
method
cleanup(self)
¶
Cleans up after this audio object.
Source code in naff/api/voice/audio.py
def cleanup(self) -> None:
"""Cleans up after this audio object."""
if self.process and self.process.poll() is None:
self.process.kill()
self.process.wait()
class
AudioVolume (Audio)
¶
An audio object with volume control
Source code in naff/api/voice/audio.py
class AudioVolume(Audio):
"""An audio object with volume control"""
_volume: float
"""The internal volume level of the audio"""
def __init__(self, src: Union[str, Path]) -> None:
super().__init__(src)
self._volume = 0.5
@property
def volume(self) -> float:
"""The volume of the audio"""
return self._volume
@volume.setter
def volume(self, value: float) -> None:
"""Sets the volume of the audio. Volume cannot be negative."""
self._volume = max(value, 0.0)
def read(self, frame_size: int) -> bytes:
"""
Reads frame_size ms of audio from source.
returns:
bytes of audio
"""
data = super().read(frame_size)
return audioop.mul(data, 2, self._volume)
inherited
property
readonly
audio_complete: bool
¶
Uses the state of the subprocess to determine if more audio is coming
inherited
method
pre_buffer(self, duration)
¶
Start pre-buffering the audio.
Parameters:
Name | Type | Description | Default |
---|---|---|---|
duration |
None | float |
The duration of audio to pre-buffer. |
None |
Source code in naff/api/voice/audio.py
def pre_buffer(self, duration: None | float = None) -> None:
"""
Start pre-buffering the audio.
Args:
duration: The duration of audio to pre-buffer.
"""
if duration:
self.buffer_seconds = duration
if self.process and self.process.poll() is None:
raise RuntimeError("Cannot pre-buffer an already running process")
# sanity value enforcement to prevent audio weirdness
self.buffer = AudioBuffer()
self.buffer.initialised.clear()
self._create_process(block=False)
inherited
method
cleanup(self)
¶
Cleans up after this audio object.
Source code in naff/api/voice/audio.py
def cleanup(self) -> None:
"""Cleans up after this audio object."""
if self.process and self.process.poll() is None:
self.process.kill()
self.process.wait()
property
writable
volume: float
¶
The volume of the audio
method
read(self, frame_size)
¶
Reads frame_size ms of audio from source.
Returns:
Type | Description |
---|---|
bytes |
bytes of audio |
Source code in naff/api/voice/audio.py
def read(self, frame_size: int) -> bytes:
"""
Reads frame_size ms of audio from source.
returns:
bytes of audio
"""
data = super().read(frame_size)
return audioop.mul(data, 2, self._volume)