Repository: dabneyhovse/Further3
Pull request #1: Add seperate SFX and normal queues
Author: login: Pandapip1
Created: 2025-04-15T03:06:46Z | Merged: 2025-04-23T07:32:18Z

This will mean that hampters don't play over each other, and also paves the way for more SFXs


Diff:
diff --git a/async_queue.py b/async_queue.py
index 3b1f2b3..95105ec 100644
--- a/async_queue.py
+++ b/async_queue.py
@@ -136,6 +136,10 @@ async def maybe_popleft(self) -> Maybe[T]:
             if not self._queue:
                 self._non_empty_event.clear()
             return out
+    
+    async def currently_empty(self) -> bool:
+        async with self.read_lock:
+            return not self._queue
 
     def __bool__(self) -> bool:
         return bool(self._queue) and bool(self._queue[0])
diff --git a/audio_queue.py b/audio_queue.py
index 3e2a328..9ea3049 100644
--- a/audio_queue.py
+++ b/audio_queue.py
@@ -18,15 +18,17 @@
 from quiet_hours import is_quiet_hours
 from resource_handler import ResourceHandler
 from settings import Settings
+from audio_sources.local_audio_source import LocalAudioSource
 
+import os
 
 @dataclass
 class AudioQueueElement:
     element_id: int
-    resource: ResourceHandler.Resource
+    resource: ResourceHandler.Resource | None
     audio_source: AudioSource
     processing: AudioProcessingSettings
-    message_setter: MessageEditStatusCallback
+    message_setter: MessageEditStatusCallback | None
     path: Future[PathLike | None]
     download_task: Future[Task]
     active: bool = False
@@ -35,9 +37,11 @@ class AudioQueueElement:
 
     @property
     def freed(self) -> bool:
-        return not self.resource.is_open
+        return self.resource is not None and not self.resource.is_open
 
     async def set_message(self, message: str, skippable: bool = True) -> None:
+        if self.message_setter is None:
+            return
         try:
             await self.message_setter(message, self.element_id if skippable else None, self.audio_source.url)
         except Exception as e:
@@ -50,6 +54,10 @@ async def download(self):
             path: Path = await self.audio_source.download(self.resource)
             # path: Path = await to_thread(self.audio_source.download, self.resource)
             if self.processing.requires_audio_processing:
+                # TODO: Support processing with no source
+                if self.resource is None:
+                    await self.skip("@WhoopsNoResourceFIXME")
+                    raise CancelledError()
                 await self.set_message("Processing")  # Can be removed if Telegram throttling is too bad
                 processed_path: Path = self.resource.path / "processed.mp3"
                 self.vlc_settings = await process_audio(path, processed_path, self.processing)
@@ -71,11 +79,12 @@ async def skip(self, username: str) -> bool:
         if not self.active:
             download_task = await self.download_task
             download_task.cancel()
-        self.resource.close()
+        if self.resource is not None:
+            self.resource.close()
         await self.set_message(f"Skipped by {username}", skippable=False)
 
     async def finish(self):
-        if not self.freed:
+        if not self.freed and self.resource is not None:
             self.resource.close()
         self.active = False
         if not self.skipped:
@@ -111,92 +120,50 @@ def __str__(self):
                     return "VLC Error"
 
     queue: AsyncQueue[AudioQueueElement]
+    sfx_queue: AsyncQueue[AudioQueueElement]
     instance: Instance
-    player: MediaPlayer
+    main_player: MediaPlayer
+    sfx_player: MediaPlayer
     current: AudioQueueElement | None = None
     _next_id: int = 0
 
     def __init__(self):
         self.queue = AsyncQueue()
+        self.sfx_queue = AsyncQueue()
         self.instance = Instance()
-        self.player = self.instance.media_player_new()
+        self.main_player = self.instance.media_player_new()
+        self.sfx_player = self.instance.media_player_new()
         get_event_loop().create_task(self.play_queue())
+        get_event_loop().create_task(self.play_sfx_queue())
 
     async def add(self, element: AudioQueueElement):
         await self.queue.append(element)
         download_task = get_event_loop().create_task(element.download())
         element.download_task.set_result(download_task)
 
-    # TODO return this when there is a FileAudioSource
-    # async def play_without_queue(self, element: AudioQueueElement) -> None:
-    #     if element.skipped:
-    #         return
-    #     path: PathLike = await element.path
-    #     if path is None:
-    #         assert element.skipped
-    #         return
-    #
-    #     if is_quiet_hours():
-    #         return
-    #
-    #     instance: Instance = Instance()
-    #     player: MediaPlayer = instance.media_player_new()
-    #
-    #     player.audio_set_volume(self.player.audio_get_volume())
-    #
-    #     if Settings.sound_starter_path is not None:
-    #         starter_media: Media = instance.media_new_path(Settings.sound_starter_path)
-    #         player.set_media(starter_media)
-    #
-    #         player.set_rate(1)
-    #
-    #         player.play()
-    #         while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #                 not is_quiet_hours():
-    #             await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #         if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #             player.stop()
-    #
-    #     media: Media = instance.media_new_path(path)
-    #     player.set_media(media)
-    #
-    #     if element.processing.tempo_scale != 1:
-    #         player.set_rate(element.processing.tempo_scale)
-    #
-    #     player.play()
-    #     element.active = True
-    #
-    #     while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #             not is_quiet_hours():
-    #         await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #     if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #         player.stop()
-    #
-    #     await element.finish()
+    async def add_sfx(self, sfx: str):
+        path = Path(os.path.join(Settings.sfx_path, sfx))
+        if not os.path.exists(path):
+            print(f"Sound effect {sfx} not found in {Settings.sfx_path}", file=stderr)
+            return None
+        media: Media = self.instance.media_new_path(path)
+        el = AudioQueueElement(
+            element_id=self.get_id(),
+            resource=None,
+            audio_source=LocalAudioSource(path),
+            processing=AudioProcessingSettings(),
+            message_setter=None,
+            path=Future(),
+            download_task=Future()
+        )
+        await el.download()
+        await self.sfx_queue.append(el)
 
     async def hampter(self) -> None:
         if is_quiet_hours():
             return
 
-        instance: Instance = Instance()
-        player: MediaPlayer = instance.media_player_new()
-
-        player.audio_set_volume(self.player.audio_get_volume())
-
-        player.set_rate(1)
-
-        media: Media = instance.media_new_path(Settings.hampter_path)
-        player.set_media(media)
-
-        player.play()
-
-        while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not is_quiet_hours():
-            await sleep(Settings.async_sleep_refresh_rate)
-
-        if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-            player.stop()
+        await self.add_sfx("hampter.wav")
 
     async def play_queue(self) -> None:
         async with self.queue.async_iter() as async_iterator:
@@ -216,16 +183,16 @@ async def play_queue(self) -> None:
 
                 while not element.skipped:
                     media: Media = self.instance.media_new_path(path)
-                    self.player.set_media(media)
+                    self.main_player.set_media(media)
 
-                    self.player.set_rate(element.vlc_settings.tempo_scale)
+                    self.main_player.set_rate(element.vlc_settings.tempo_scale)
 
                     await element.set_message("Playing")
 
-                    self.player.play()
+                    self.main_player.play()
                     element.active = True
 
-                    while self.player.get_state() not in (VLCState.Ended, VLCState.Stopped) and \
+                    while self.main_player.get_state() not in (VLCState.Ended, VLCState.Stopped) and \
                             not element.skipped and not is_quiet_hours():
                         # TODO: Wait for the duration or skip or quiet hours (whichever first)
                         await sleep(Settings.async_sleep_refresh_rate)
@@ -233,8 +200,8 @@ async def play_queue(self) -> None:
                     if is_quiet_hours():
                         await self.skip_all("@GoToBedFroshDitchDayIsTomorrow (quiet hours)")
 
-                    if self.player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-                        self.player.stop()
+                    if self.main_player.get_state() not in (VLCState.Ended, VLCState.Stopped):
+                        self.main_player.stop()
 
                     if not element.processing.loop:
                         break
@@ -242,6 +209,35 @@ async def play_queue(self) -> None:
                 # TODO: release() media if needed
                 await element.finish()
                 self.current = None
+    
+    async def play_sfx_queue(self) -> None:
+        async with self.sfx_queue.async_iter() as async_iterator:
+            async for element in async_iterator:
+                path: Path | None = await element.path
+                if path is None:
+                    assert element.skipped
+                    continue
+
+                if is_quiet_hours():
+                    continue
+
+                media: Media = self.instance.media_new_path(path)
+                self.sfx_player.set_media(media)
+                self.sfx_player.set_rate(element.vlc_settings.tempo_scale)
+                await element.set_message("Playing")
+                self.sfx_player.play()
+                element.active = True
+
+                while self.sfx_player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not is_quiet_hours():
+                    await sleep(Settings.async_sleep_refresh_rate)
+
+                if is_quiet_hours():
+                    continue
+
+                if self.sfx_player.get_state() not in (VLCState.Ended, VLCState.Stopped):
+                    self.sfx_player.stop()
+
+                await element.finish()
 
     async def skip(self, username: str) -> bool:
         if self.current is None:
@@ -267,30 +263,34 @@ async def skip_specific(self, username: str, element_id: int) -> bool:
         return False
 
     async def pause(self) -> None:
-        self.player.set_pause(True)
+        self.main_player.set_pause(True)
 
     async def resume(self) -> None:
-        self.player.set_pause(False)
+        self.main_player.set_pause(False)
 
     async def set_digital_volume(self, volume: float) -> bool:
         absolute_volume: float = volume * Settings.hundred_percent_volume_value
         if 0 <= absolute_volume <= Settings.max_absolute_volume * 100:
-            return self.player.audio_set_volume(round(absolute_volume)) + 1
+            main_success = self.main_player.audio_set_volume(round(absolute_volume)) == 0
+            sfx_success = self.sfx_player.audio_set_volume(round(absolute_volume)) == 0
+            return main_success and sfx_success
         else:
             return False
 
     async def set_clamped_digital_volume(self, volume: float) -> bool:
         absolute_volume: float = volume * Settings.hundred_percent_volume_value
         absolute_volume = min(max(absolute_volume, 0), Settings.max_absolute_volume * 100)
-        return self.player.audio_set_volume(round(absolute_volume)) + 1
+        main_success = self.main_player.audio_set_volume(round(absolute_volume)) == 0
+        sfx_success = self.sfx_player.audio_set_volume(round(absolute_volume)) == 0
+        return main_success and sfx_success
 
     async def get_digital_volume(self) -> float:
-        scaled_volume = self.player.audio_get_volume()
+        scaled_volume = self.sfx_player.audio_get_volume()
         return scaled_volume / Settings.hundred_percent_volume_value
 
     @property
     def state(self) -> State:
-        match (self.player.get_state(), bool(self.queue), self.current is not None and not self.current.skipped):
+        match (self.main_player.get_state(), bool(self.queue), self.current is not None and not self.current.skipped):
             case (VLCState.Playing, _, True):
                 return AudioQueue.State.PLAYING
             case (VLCState.Paused, _, True):
@@ -307,11 +307,14 @@ def state(self) -> State:
                 return AudioQueue.State.LOADING
             case (player_state, queue_nonempty, current_set):
                 print(f"Unknown audio queue state error:\n"
-                      f"\tself.player.get_state(): {player_state}\n"
+                      f"\tself.main_player.get_state(): {player_state}\n"
+                      f"\tself.sfx_player.get_state(): {self.sfx_player.get_state()}\n"
                       f"\tbool(self.queue): {queue_nonempty}\n"
-                      f"\tself.current is not None: {current_set}"
-                      f"\tself.queue: {self.queue}"
-                      f"\tself.current: {self.current}"
+                      f"\tbool(self.sfx_queue): {bool(self.sfx_queue)}\n"
+                      f"\tself.current is not None: {current_set}\n"
+                      f"\tself.queue: {self.queue}\n"
+                      f"\tself.sfx_queue: {self.sfx_queue}\n"
+                      f"\tself.current: {self.current}\n"
                       f"\tself.current.skipped: {self.current.skipped}")
                 return AudioQueue.State.UNKNOWN_ERROR
 
diff --git a/audio_sources/local_audio_source.py b/audio_sources/local_audio_source.py
new file mode 100644
index 0000000..cde4868
--- /dev/null
+++ b/audio_sources/local_audio_source.py
@@ -0,0 +1,36 @@
+from __future__ import annotations
+
+from datetime import timedelta
+from pathlib import Path
+
+from audio_sources import AudioSource
+from duration import Duration
+from resource_handler import ResourceHandler
+
+
+class LocalAudioSource(AudioSource):
+    file_path: Path
+
+    def __init__(self, file_path: Path):
+        self.file_path = file_path
+
+    async def download(self, resource: ResourceHandler.Resource) -> Path:
+        return self.file_path
+
+    @property
+    def title(self) -> str:
+        return self.file_path.stem or "<Unknown local audio file>"
+
+    @property
+    def author_and_author_type(self) -> tuple[str, str]:
+        # No metadata available for local files by default
+        return "performer", "<Unknown>"
+
+    @property
+    def duration(self) -> Duration:
+        # TODO: Implement
+        return Duration.from_timedelta(timedelta(seconds=0))
+
+    @property
+    def url(self) -> None:
+        return
diff --git a/bot_configs/further_bot.py b/bot_configs/further_bot.py
index 5ebe2e6..fb478a5 100644
--- a/bot_configs/further_bot.py
+++ b/bot_configs/further_bot.py
@@ -70,7 +70,7 @@ def format_get_queue(queue: AudioQueue) -> TreeMessage:
                         (
                                 queue.current.duration -
                                 Duration.from_timedelta(
-                                    timedelta(milliseconds=queue.player.get_time())
+                                    timedelta(milliseconds=queue.main_player.get_time())
                                 ) / queue.current.processing.tempo_scale
                         )
                         if queue.state in [AudioQueue.State.PLAYING, AudioQueue.State.PAUSED] else
diff --git a/settings.py b/settings.py
index df628ff..0c4619f 100644
--- a/settings.py
+++ b/settings.py
@@ -33,7 +33,7 @@ class Settings:
     max_telegram_flood_control_retries: int = 4
 
     # Hampter
-    hampter_path: str | None = "audio_files/hampter.wav"
+    sfx_path: str | None = "audio_files/"
 
     # Amogus
     amogus_ban_id: int


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.


<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**audio_queue.py:124**
* [nitpick] Consider renaming 'sfx_playermult' to a more descriptive name such as 'sfx_volume_multiplier' to improve clarity.
```
sfx_playermult: float = 1.0
```
</details>


state: COMMENTED
author_association: NONE
submitted_at: 2025-04-15T03:08:03Z
user: login: Pandapip1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T03:10:04Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.




state: COMMENTED
author_association: NONE
submitted_at: 2025-04-15T03:11:28Z
user: login: Pandapip1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T03:12:03Z
user: login: Pandapip1
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T03:12:21Z
user: login: lambdairis
body: Assuming you've tested this, it looks good to me! Thank you!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T01:40:47Z

Review comments:
diff_hunk: @@ -111,92 +114,50 @@ def __str__(self):
                     return "VLC Error"
 
     queue: AsyncQueue[AudioQueueElement]
+    sfx_queue: AsyncQueue[AudioQueueElement]
     instance: Instance
     player: MediaPlayer
+    sfx_player: MediaPlayer
     current: AudioQueueElement | None = None
     _next_id: int = 0
 
+    sfx_playermult: float = 1.0
+
     def __init__(self):
         self.queue = AsyncQueue()
+        self.sfx_queue = AsyncQueue()
         self.instance = Instance()
         self.player = self.instance.media_player_new()
+        self.sfx_player = self.instance.media_player_new()
         get_event_loop().create_task(self.play_queue())
+        get_event_loop().create_task(self.play_sfx_queue())
 
     async def add(self, element: AudioQueueElement):
         await self.queue.append(element)
         download_task = get_event_loop().create_task(element.download())
         element.download_task.set_result(download_task)
 
-    # TODO return this when there is a FileAudioSource
-    # async def play_without_queue(self, element: AudioQueueElement) -> None:
-    #     if element.skipped:
-    #         return
-    #     path: PathLike = await element.path
-    #     if path is None:
-    #         assert element.skipped
-    #         return
-    #
-    #     if is_quiet_hours():
-    #         return
-    #
-    #     instance: Instance = Instance()
-    #     player: MediaPlayer = instance.media_player_new()
-    #
-    #     player.audio_set_volume(self.player.audio_get_volume())
-    #
-    #     if Settings.sound_starter_path is not None:
-    #         starter_media: Media = instance.media_new_path(Settings.sound_starter_path)
-    #         player.set_media(starter_media)
-    #
-    #         player.set_rate(1)
-    #
-    #         player.play()
-    #         while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #                 not is_quiet_hours():
-    #             await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #         if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #             player.stop()
-    #
-    #     media: Media = instance.media_new_path(path)
-    #     player.set_media(media)
-    #
-    #     if element.processing.tempo_scale != 1:
-    #         player.set_rate(element.processing.tempo_scale)
-    #
-    #     player.play()
-    #     element.active = True
-    #
-    #     while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #             not is_quiet_hours():
-    #         await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #     if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #         player.stop()
-    #
-    #     await element.finish()
+    async def add_sfx(self, sfx: str):
+        path = os.path.join(Settings.sfx_path, sfx)
+        if not os.path.exists(path):
+            print(f"Sound effect {sfx} not found in {Settings.sfx_path}", file=stderr)
path: audio_queue.py
user: login: Copilot
body: The 'stderr' is used without being imported, which can cause a runtime error. Please add 'from sys import stderr' at the top of the file to resolve this issue.
created_at: 2025-04-15T03:08:03Z
author_association: NONE
diff_hunk: @@ -111,92 +114,50 @@ def __str__(self):
                     return "VLC Error"
 
     queue: AsyncQueue[AudioQueueElement]
+    sfx_queue: AsyncQueue[AudioQueueElement]
     instance: Instance
     player: MediaPlayer
+    sfx_player: MediaPlayer
     current: AudioQueueElement | None = None
     _next_id: int = 0
 
+    sfx_playermult: float = 1.0
+
     def __init__(self):
         self.queue = AsyncQueue()
+        self.sfx_queue = AsyncQueue()
         self.instance = Instance()
         self.player = self.instance.media_player_new()
+        self.sfx_player = self.instance.media_player_new()
         get_event_loop().create_task(self.play_queue())
+        get_event_loop().create_task(self.play_sfx_queue())
 
     async def add(self, element: AudioQueueElement):
         await self.queue.append(element)
         download_task = get_event_loop().create_task(element.download())
         element.download_task.set_result(download_task)
 
-    # TODO return this when there is a FileAudioSource
-    # async def play_without_queue(self, element: AudioQueueElement) -> None:
-    #     if element.skipped:
-    #         return
-    #     path: PathLike = await element.path
-    #     if path is None:
-    #         assert element.skipped
-    #         return
-    #
-    #     if is_quiet_hours():
-    #         return
-    #
-    #     instance: Instance = Instance()
-    #     player: MediaPlayer = instance.media_player_new()
-    #
-    #     player.audio_set_volume(self.player.audio_get_volume())
-    #
-    #     if Settings.sound_starter_path is not None:
-    #         starter_media: Media = instance.media_new_path(Settings.sound_starter_path)
-    #         player.set_media(starter_media)
-    #
-    #         player.set_rate(1)
-    #
-    #         player.play()
-    #         while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #                 not is_quiet_hours():
-    #             await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #         if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #             player.stop()
-    #
-    #     media: Media = instance.media_new_path(path)
-    #     player.set_media(media)
-    #
-    #     if element.processing.tempo_scale != 1:
-    #         player.set_rate(element.processing.tempo_scale)
-    #
-    #     player.play()
-    #     element.active = True
-    #
-    #     while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #             not is_quiet_hours():
-    #         await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #     if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #         player.stop()
-    #
-    #     await element.finish()
+    async def add_sfx(self, sfx: str):
+        path = os.path.join(Settings.sfx_path, sfx)
+        if not os.path.exists(path):
+            print(f"Sound effect {sfx} not found in {Settings.sfx_path}", file=stderr)
path: audio_queue.py
user: login: Pandapip1
body: Okay, this is useless. It is imported lol.
created_at: 2025-04-15T03:10:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,92 +114,50 @@ def __str__(self):
                     return "VLC Error"
 
     queue: AsyncQueue[AudioQueueElement]
+    sfx_queue: AsyncQueue[AudioQueueElement]
     instance: Instance
     player: MediaPlayer
+    sfx_player: MediaPlayer
     current: AudioQueueElement | None = None
     _next_id: int = 0
 
+    sfx_playermult: float = 1.0
+
     def __init__(self):
         self.queue = AsyncQueue()
+        self.sfx_queue = AsyncQueue()
         self.instance = Instance()
         self.player = self.instance.media_player_new()
+        self.sfx_player = self.instance.media_player_new()
         get_event_loop().create_task(self.play_queue())
+        get_event_loop().create_task(self.play_sfx_queue())
 
     async def add(self, element: AudioQueueElement):
         await self.queue.append(element)
         download_task = get_event_loop().create_task(element.download())
         element.download_task.set_result(download_task)
 
-    # TODO return this when there is a FileAudioSource
-    # async def play_without_queue(self, element: AudioQueueElement) -> None:
-    #     if element.skipped:
-    #         return
-    #     path: PathLike = await element.path
-    #     if path is None:
-    #         assert element.skipped
-    #         return
-    #
-    #     if is_quiet_hours():
-    #         return
-    #
-    #     instance: Instance = Instance()
-    #     player: MediaPlayer = instance.media_player_new()
-    #
-    #     player.audio_set_volume(self.player.audio_get_volume())
-    #
-    #     if Settings.sound_starter_path is not None:
-    #         starter_media: Media = instance.media_new_path(Settings.sound_starter_path)
-    #         player.set_media(starter_media)
-    #
-    #         player.set_rate(1)
-    #
-    #         player.play()
-    #         while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #                 not is_quiet_hours():
-    #             await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #         if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #             player.stop()
-    #
-    #     media: Media = instance.media_new_path(path)
-    #     player.set_media(media)
-    #
-    #     if element.processing.tempo_scale != 1:
-    #         player.set_rate(element.processing.tempo_scale)
-    #
-    #     player.play()
-    #     element.active = True
-    #
-    #     while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #             not is_quiet_hours():
-    #         await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #     if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #         player.stop()
-    #
-    #     await element.finish()
+    async def add_sfx(self, sfx: str):
+        path = os.path.join(Settings.sfx_path, sfx)
+        if not os.path.exists(path):
+            print(f"Sound effect {sfx} not found in {Settings.sfx_path}", file=stderr)
path: audio_queue.py
user: login: Copilot
body: The 'stderr' identifier is used but not imported; consider adding 'from sys import stderr' at the top of the file.
created_at: 2025-04-15T03:11:27Z
author_association: NONE
diff_hunk: @@ -242,6 +203,73 @@ async def play_queue(self) -> None:
                 # TODO: release() media if needed
                 await element.finish()
                 self.current = None
+    
+    async def play_sfx_queue(self) -> None:
+        async with self.sfx_queue.async_iter() as async_iterator:
+            async for element in async_iterator:
+                path: Path | None = await element.path
+                if path is None:
+                    assert element.skipped
+                    continue
+
+                if is_quiet_hours():
+                    continue
+
+                epsilon = 1e-3
+                target = Settings.sfx_volmult
+                ticks_needed = Settings.rampup_seconds // Settings.async_sleep_refresh_rate + 1
+                ramp = target ** (1 / ticks_needed)
+
+                while abs(self.sfx_playermult - target) > epsilon:
+                    self.sfx_playermult *= ramp
+                    self.player.audio_set_volume(
+                        self.sfx_player.audio_get_volume() * Settings.sfx_volmult
+                    )
+                    await sleep(Settings.async_sleep_refresh_rate)
path: audio_queue.py
user: login: Copilot
body: The 'sleep' function is used but not imported; consider adding 'from asyncio import sleep' (or the appropriate sleep import) to the file.
created_at: 2025-04-15T03:11:28Z
author_association: NONE
diff_hunk: @@ -111,92 +114,50 @@ def __str__(self):
                     return "VLC Error"
 
     queue: AsyncQueue[AudioQueueElement]
+    sfx_queue: AsyncQueue[AudioQueueElement]
     instance: Instance
     player: MediaPlayer
+    sfx_player: MediaPlayer
     current: AudioQueueElement | None = None
     _next_id: int = 0
 
+    sfx_playermult: float = 1.0
+
     def __init__(self):
         self.queue = AsyncQueue()
+        self.sfx_queue = AsyncQueue()
         self.instance = Instance()
         self.player = self.instance.media_player_new()
+        self.sfx_player = self.instance.media_player_new()
         get_event_loop().create_task(self.play_queue())
+        get_event_loop().create_task(self.play_sfx_queue())
 
     async def add(self, element: AudioQueueElement):
         await self.queue.append(element)
         download_task = get_event_loop().create_task(element.download())
         element.download_task.set_result(download_task)
 
-    # TODO return this when there is a FileAudioSource
-    # async def play_without_queue(self, element: AudioQueueElement) -> None:
-    #     if element.skipped:
-    #         return
-    #     path: PathLike = await element.path
-    #     if path is None:
-    #         assert element.skipped
-    #         return
-    #
-    #     if is_quiet_hours():
-    #         return
-    #
-    #     instance: Instance = Instance()
-    #     player: MediaPlayer = instance.media_player_new()
-    #
-    #     player.audio_set_volume(self.player.audio_get_volume())
-    #
-    #     if Settings.sound_starter_path is not None:
-    #         starter_media: Media = instance.media_new_path(Settings.sound_starter_path)
-    #         player.set_media(starter_media)
-    #
-    #         player.set_rate(1)
-    #
-    #         player.play()
-    #         while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #                 not is_quiet_hours():
-    #             await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #         if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #             player.stop()
-    #
-    #     media: Media = instance.media_new_path(path)
-    #     player.set_media(media)
-    #
-    #     if element.processing.tempo_scale != 1:
-    #         player.set_rate(element.processing.tempo_scale)
-    #
-    #     player.play()
-    #     element.active = True
-    #
-    #     while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #             not is_quiet_hours():
-    #         await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #     if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #         player.stop()
-    #
-    #     await element.finish()
+    async def add_sfx(self, sfx: str):
+        path = os.path.join(Settings.sfx_path, sfx)
+        if not os.path.exists(path):
+            print(f"Sound effect {sfx} not found in {Settings.sfx_path}", file=stderr)
+            return None
+        media: Media = self.instance.media_new_path(path)
+        await self.sfx_queue.append(AudioQueueElement(
+            element_id=self.get_id(),
+            resource=ResourceHandler.Resource(path),
+            audio_source=LocalAudioSource(Path(path)),
+            processing=AudioProcessingSettings(),
+            message_setter=MessageEditStatusCallback(lambda x, y, z: None),
+            path=Future(),
+            download_task=Future(),
path: audio_queue.py
user: login: Copilot
body: The 'Future' class is used but not imported; consider importing it from asyncio (e.g., 'from asyncio import Future').
created_at: 2025-04-15T03:11:28Z
author_association: NONE
diff_hunk: @@ -242,6 +203,73 @@ async def play_queue(self) -> None:
                 # TODO: release() media if needed
                 await element.finish()
                 self.current = None
+    
+    async def play_sfx_queue(self) -> None:
+        async with self.sfx_queue.async_iter() as async_iterator:
+            async for element in async_iterator:
+                path: Path | None = await element.path
+                if path is None:
+                    assert element.skipped
+                    continue
+
+                if is_quiet_hours():
+                    continue
+
+                epsilon = 1e-3
+                target = Settings.sfx_volmult
+                ticks_needed = Settings.rampup_seconds // Settings.async_sleep_refresh_rate + 1
+                ramp = target ** (1 / ticks_needed)
+
+                while abs(self.sfx_playermult - target) > epsilon:
+                    self.sfx_playermult *= ramp
+                    self.player.audio_set_volume(
+                        self.sfx_player.audio_get_volume() * Settings.sfx_volmult
+                    )
+                    await sleep(Settings.async_sleep_refresh_rate)
path: audio_queue.py
user: login: Pandapip1
body: Nope, it is!
created_at: 2025-04-15T03:12:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -111,92 +114,50 @@ def __str__(self):
                     return "VLC Error"
 
     queue: AsyncQueue[AudioQueueElement]
+    sfx_queue: AsyncQueue[AudioQueueElement]
     instance: Instance
     player: MediaPlayer
+    sfx_player: MediaPlayer
     current: AudioQueueElement | None = None
     _next_id: int = 0
 
+    sfx_playermult: float = 1.0
+
     def __init__(self):
         self.queue = AsyncQueue()
+        self.sfx_queue = AsyncQueue()
         self.instance = Instance()
         self.player = self.instance.media_player_new()
+        self.sfx_player = self.instance.media_player_new()
         get_event_loop().create_task(self.play_queue())
+        get_event_loop().create_task(self.play_sfx_queue())
 
     async def add(self, element: AudioQueueElement):
         await self.queue.append(element)
         download_task = get_event_loop().create_task(element.download())
         element.download_task.set_result(download_task)
 
-    # TODO return this when there is a FileAudioSource
-    # async def play_without_queue(self, element: AudioQueueElement) -> None:
-    #     if element.skipped:
-    #         return
-    #     path: PathLike = await element.path
-    #     if path is None:
-    #         assert element.skipped
-    #         return
-    #
-    #     if is_quiet_hours():
-    #         return
-    #
-    #     instance: Instance = Instance()
-    #     player: MediaPlayer = instance.media_player_new()
-    #
-    #     player.audio_set_volume(self.player.audio_get_volume())
-    #
-    #     if Settings.sound_starter_path is not None:
-    #         starter_media: Media = instance.media_new_path(Settings.sound_starter_path)
-    #         player.set_media(starter_media)
-    #
-    #         player.set_rate(1)
-    #
-    #         player.play()
-    #         while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #                 not is_quiet_hours():
-    #             await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #         if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #             player.stop()
-    #
-    #     media: Media = instance.media_new_path(path)
-    #     player.set_media(media)
-    #
-    #     if element.processing.tempo_scale != 1:
-    #         player.set_rate(element.processing.tempo_scale)
-    #
-    #     player.play()
-    #     element.active = True
-    #
-    #     while player.get_state() not in (VLCState.Ended, VLCState.Stopped) and not element.skipped and \
-    #             not is_quiet_hours():
-    #         await sleep(Settings.async_sleep_refresh_rate)
-    #
-    #     if player.get_state() not in (VLCState.Ended, VLCState.Stopped):
-    #         player.stop()
-    #
-    #     await element.finish()
+    async def add_sfx(self, sfx: str):
+        path = os.path.join(Settings.sfx_path, sfx)
+        if not os.path.exists(path):
+            print(f"Sound effect {sfx} not found in {Settings.sfx_path}", file=stderr)
+            return None
+        media: Media = self.instance.media_new_path(path)
+        await self.sfx_queue.append(AudioQueueElement(
+            element_id=self.get_id(),
+            resource=ResourceHandler.Resource(path),
+            audio_source=LocalAudioSource(Path(path)),
+            processing=AudioProcessingSettings(),
+            message_setter=MessageEditStatusCallback(lambda x, y, z: None),
+            path=Future(),
+            download_task=Future(),
path: audio_queue.py
user: login: Pandapip1
body: Nope, it is imported!
created_at: 2025-04-15T03:12:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: Pandapip1
created_at: 2025-04-15T03:07:29Z
body: I haven't tried getting a review from Copilot before, let's see what happens
author_association: CONTRIBUTOR
user: login: Pandapip1
created_at: 2025-04-15T03:12:41Z
body: Okay. The bot's definitely 100% useless lol
author_association: CONTRIBUTOR
user: login: Pandapip1
created_at: 2025-04-21T22:05:33Z
body: I removed the volume change logic as it was having issues I couldn't figure out. This part works perfectly though and has been tested. @lambdairis please review.
author_association: CONTRIBUTOR
user: login: lambdairis
created_at: 2025-04-23T01:41:48Z
body: I'm not sure why you want to allow queue elements to be None though? What is that for?
author_association: CONTRIBUTOR
user: login: Pandapip1
created_at: 2025-04-23T07:30:51Z
body: > I'm not sure why you want to allow queue elements to be None though? What is that for?

Where is that? I thought I only added support for None `resource` and `message_setter` fields, as the `LocalAudioSource` doesn't use them. I'll merge this on the assumption that this was a misunderstanding, as this has been pretty thoroughly tested.
author_association: CONTRIBUTOR