diff --git a/docs/changelog.rst b/docs/changelog.rst index 5c39e093..90d11aae 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,24 @@ :orphan: +2.10.1 +======= +- ext.sounds + - Additions + - Added :class:`twitchio.ext.sounds.AudioQueueManager` + - Added :meth:`AudioQueueManager.add_audio ` + - Added :meth:`AudioQueueManager.play_next ` + - Added :meth:`AudioQueueManager.skip_audio ` + - Added :meth:`AudioQueueManager.stop_audio ` + - Added :meth:`AudioQueueManager.pause_audio ` + - Added :meth:`AudioQueueManager.resume_audio ` + - Added :meth:`AudioQueueManager.clear_queue ` + - Added :meth:`AudioQueueManager.pause_queue ` + - Added :meth:`AudioQueueManager.resume_queue ` + - Added :meth:`AudioQueueManager.get_queue_contents ` + - Added :meth:`AudioQueueManager.queue_loop ` + + 2.10.0 ======= - TwitchIO @@ -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 diff --git a/docs/exts/sounds.rst b/docs/exts/sounds.rst index eb5b9a14..27be7590 100644 --- a/docs/exts/sounds.rst +++ b/docs/exts/sounds.rst @@ -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:** @@ -181,3 +185,6 @@ API Reference .. autoclass:: AudioPlayer :members: + +.. autoclass:: AudioQueueManager + :members: diff --git a/examples/music_queue.py b/examples/music_queue.py new file mode 100644 index 00000000..e035d1e8 --- /dev/null +++ b/examples/music_queue.py @@ -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() diff --git a/twitchio/ext/sounds/__init__.py b/twitchio/ext/sounds/__init__.py index 24ff1b87..d269c042 100644 --- a/twitchio/ext/sounds/__init__.py +++ b/twitchio/ext/sounds/__init__.py @@ -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__) @@ -59,9 +61,6 @@ ffmpeg_bin = "ffmpeg" -__all__ = ("Sound", "AudioPlayer") - - @dataclasses.dataclass class OutputDevice: """Class which represents an OutputDevice usable with :class:`AudioPlayer` . diff --git a/twitchio/ext/sounds/audioqueuemanager.py b/twitchio/ext/sounds/audioqueuemanager.py new file mode 100644 index 00000000..9c5b0906 --- /dev/null +++ b/twitchio/ext/sounds/audioqueuemanager.py @@ -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