Skip to content
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

Audio queue manager, solution for #292 #462

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
:orphan:


2.10.1
=======
- ext.sounds
- Additions
- Added :class:`twitchio.ext.sounds.AudioQueueManager`
- Added :meth:`AudioQueueManager.add_audio <twitchio.ext.sounds.AudioQueueManager.add_audio>`
- Added :meth:`AudioQueueManager.play_next <twitchio.ext.sounds.AudioQueueManager.play_next>`
- Added :meth:`AudioQueueManager.skip_audio <twitchio.ext.sounds.AudioQueueManager.skip_audio>`
- Added :meth:`AudioQueueManager.stop_audio <twitchio.ext.sounds.AudioQueueManager.stop_audio>`
- Added :meth:`AudioQueueManager.pause_audio <twitchio.ext.sounds.AudioQueueManager.pause_audio>`
- Added :meth:`AudioQueueManager.resume_audio <twitchio.ext.sounds.AudioQueueManager.resume_audio>`
- Added :meth:`AudioQueueManager.clear_queue <twitchio.ext.sounds.AudioQueueManager.clear_queue>`
- Added :meth:`AudioQueueManager.pause_queue <twitchio.ext.sounds.AudioQueueManager.pause_queue>`
- Added :meth:`AudioQueueManager.resume_queue <twitchio.ext.sounds.AudioQueueManager.resume_queue>`
- Added :meth:`AudioQueueManager.get_queue_contents <twitchio.ext.sounds.AudioQueueManager.get_queue_contents>`
- Added :meth:`AudioQueueManager.queue_loop <twitchio.ext.sounds.AudioQueueManager.queue_loop>`


2.10.0
=======
- TwitchIO
Expand Down Expand Up @@ -169,6 +187,7 @@
- Bumped ciso8601 from >=2.2,<2.3 to >=2.2,<3
- Bumped cchardet from >=2.1,<2.2 to >=2.1,<3


2.6.0
======
- TwitchIO
Expand Down
9 changes: 8 additions & 1 deletion docs/exts/sounds.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,15 @@ This bot will search YouTube for a relevant video and playback its audio.

**Sound with a Local File:**

This Sound will target a local file on your machine. Just pass the location to source.
This Sound will target a local file on your machine. Pass the location to source. You
may manually set the sample rate and number of channels if needed, however it should
be automatically detected.

.. code-block:: python3

sound = sounds.Sound(source='my_audio.mp3')
sound.channels = 1 # play mono channel
sound.rate = 24_000 # set sample


**Multiple Players:**
Expand Down Expand Up @@ -181,3 +185,6 @@ API Reference

.. autoclass:: AudioPlayer
:members:

.. autoclass:: AudioQueueManager
:members:
51 changes: 51 additions & 0 deletions examples/music_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import asyncio
from twitchio.ext import commands, sounds


class Bot(commands.Bot):

def __init__(self):
super().__init__(token="TOKEN", prefix="!", initial_channels=["CHANNEL"])
self.audio_manager = sounds.AudioQueueManager()
self.song_dict = {
"song_one": "C:\\PATH\\TO\\FILE.mp3",
"song_two": "C:\\PATH\\TO\\FILE.mp3",
"song_three": "C:\\PATH\\TO\\FILE.mp3",
}

async def event_ready(self):
loop = asyncio.get_event_loop()
self.task = loop.create_task(self.audio_manager.queue_loop())

@commands.command(name="sr")
async def addsound(self, ctx: commands.Context, sound: str):
sound_path = self.song_dict[sound]
await self.audio_manager.add_audio(sound_path)
await ctx.send(f"Added sound to queue: {sound_path}")

@commands.command(name="skip")
async def skip(self, ctx: commands.Context):
await ctx.send(f"Skipped the current sound. {self.audio_manager.current_sound}")
self.audio_manager.skip_audio()

@commands.command(name="pause")
async def pause(self, ctx: commands.Context):
self.audio_manager.pause_audio()

@commands.command(name="resume")
async def resume(self, ctx: commands.Context):
self.audio_manager.resume_audio()

@commands.command(name="queue")
async def queue(self, ctx: commands.Context):
queue_contents = self.audio_manager.get_queue_contents()
await ctx.send(f"Queue contents: {queue_contents}")

async def close(self):
self.task.cancel()
await super().close()


if __name__ == "__main__":
bot = Bot()
bot.run()
7 changes: 3 additions & 4 deletions twitchio/ext/sounds/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@
from yt_dlp import YoutubeDL
from tinytag import TinyTag

from .audioqueuemanager import AudioQueueManager

__all__ = ("Sound", "AudioPlayer")

__all__ = ("Sound", "AudioPlayer", "AudioQueueManager")


logger = logging.getLogger(__name__)
Expand All @@ -59,9 +61,6 @@
ffmpeg_bin = "ffmpeg"


__all__ = ("Sound", "AudioPlayer")


@dataclasses.dataclass
class OutputDevice:
"""Class which represents an OutputDevice usable with :class:`AudioPlayer` .
Expand Down
155 changes: 155 additions & 0 deletions twitchio/ext/sounds/audioqueuemanager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import asyncio
from twitchio.ext import sounds
from typing import Optional, List


class AudioQueueManager:
"""
Manages a queue of audio files to be played sequentially with optional repeat and pause functionalities.

Attributes
----------
queue: asyncio.Queue[:class:`str`]
A queue to hold paths of audio files to be played.
is_playing: :class:`bool`
Indicates whether an audio file is currently being played.
repeat_queue: :class:`bool`
If True, adds the current playing audio file back to the queue after playing.
queue_paused: :class:`bool`
If True, pauses the processing of the queue.
player: :class:`sounds.AudioPlayer`
An instance of AudioPlayer to play audio files.
current_sound: :class:`str`
Path of the currently playing audio file.
"""

def __init__(self, repeat_queue: Optional[bool] = True) -> None:
"""
Initializes an instance of AudioQueueManager with an empty queue and default settings.

Parameters
----------
repeat_queue: Optional[:class:`bool`]
If True, adds the current playing audio file back to the queue after playing, by default True
"""
self.queue: asyncio.Queue[str] = asyncio.Queue()
self.is_playing: bool = False
self.repeat_queue: bool = repeat_queue
self.queue_paused: bool = False
self.player: sounds.AudioPlayer = sounds.AudioPlayer(callback=self.player_done)
self.current_sound: str = ""

async def player_done(self) -> None:
"""
|coro|

Callback method called when the player finishes playing an audio file.
Resets the is_playing flag and marks the current task as done in the queue.
"""
self.is_playing = False
self.queue.task_done()

async def add_audio(self, sound_path: str) -> None:
"""
|coro|

Adds a new audio file to the queue.

Parameters
----------
sound_path: :class:`str`
Path of the audio file to add to the queue.
"""
await self.queue.put(sound_path)

async def play_next(self) -> None:
"""
|coro|

Plays the next audio file in the queue if the queue is not empty and not paused.
Sets the is_playing flag, retrieves the next audio file from the queue, and plays it.
If repeat_queue is True, adds the current audio file back to the queue after playing.
"""
if not self.queue.empty() and not self.queue_paused:
self.is_playing = True
sound_path = await self.queue.get()
self.current_sound = sound_path
sound = sounds.Sound(source=sound_path)
self.player.play(sound)
if self.repeat_queue:
await self.queue.put(self.current_sound)

def skip_audio(self) -> None:
"""
Stops the currently playing audio file if there is one.
"""
if self.is_playing:
self.player.stop()
self.is_playing = False

def stop_audio(self) -> None:
"""
Stops the currently playing audio file.
Resets the playing flag but leaves the queue intact.
"""
if self.is_playing:
self.player.stop()
self.is_playing = False

def pause_audio(self) -> None:
"""
Pauses the currently playing audio file.
"""
self.player.pause()

def resume_audio(self) -> None:
"""
Resumes the currently paused audio file.
"""
self.player.resume()

async def clear_queue(self) -> None:
"""
|coro|

Clears all audio files from the queue.
"""
while not self.queue.empty():
await self.queue.get()
self.queue.task_done()

def pause_queue(self) -> None:
"""
Pauses the processing of the queue.
"""
self.queue_paused = True

def resume_queue(self) -> None:
"""
Resumes the processing of the queue.
"""
self.queue_paused = False

def get_queue_contents(self) -> List[str]:
"""
Retrieves the current contents of the queue as a list.

Returns
-------
List[:class:`str`]
"""
return list(self.queue._queue)

async def queue_loop(self) -> None:
"""
|coro|

Continuously checks the queue and plays the next audio file if not currently playing and not paused.
"""
try:
while True:
await asyncio.sleep(0.2)
if not self.is_playing and not self.queue.empty() and not self.queue_paused:
await self.play_next()
finally:
return