some fixes
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 3 Aug 2021 08:33:48 +0000 (10:33 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Tue, 3 Aug 2021 08:33:48 +0000 (10:33 +0200)
- auto select quality for queue stream
- improve detection of alert finish

music_assistant/constants.py
music_assistant/helpers/alert.mp3 [deleted file]
music_assistant/helpers/audio.py
music_assistant/managers/players.py
music_assistant/models/player_queue.py
music_assistant/resources/alert_announce.flac [new file with mode: 0644]
music_assistant/resources/alert_finish.flac [new file with mode: 0644]
music_assistant/web/api.py
music_assistant/web/stream.py

index 3926751d9bc49b67886693ce3f44a7083b421947..c213513bc1c28fe65a7b68c4c7c95dd2362122d0 100755 (executable)
@@ -1,6 +1,6 @@
 """All constants for Music Assistant."""
 
-__version__ = "0.2.2"
+__version__ = "0.2.3"
 REQUIRED_PYTHON_VER = "3.8"
 
 # configuration keys/attributes
@@ -55,7 +55,6 @@ EVENT_TRACK_ADDED = "track added"
 EVENT_PLAYLIST_ADDED = "playlist added"
 EVENT_RADIO_ADDED = "radio added"
 EVENT_TASK_UPDATED = "task updated"
-EVENT_ALERT_FINISHED = "alert finished"
 
 # player attributes
 ATTR_PLAYER_ID = "player_id"
diff --git a/music_assistant/helpers/alert.mp3 b/music_assistant/helpers/alert.mp3
deleted file mode 100644 (file)
index 133b3ac..0000000
Binary files a/music_assistant/helpers/alert.mp3 and /dev/null differ
index af5c98068e29a91faaa853a1aec0b5c54f1e3aa9..789dd82c9e7601ba275c04491bb5132c33059773 100644 (file)
@@ -122,16 +122,13 @@ async def get_stream_details(
     """
     if queue_item.provider == "url":
         # special case: a plain url was added to the queue
-        if queue_item.streamdetails is not None:
-            streamdetails = queue_item.streamdetails
-        else:
-            streamdetails = StreamDetails(
-                type=StreamType.URL,
-                provider="url",
-                item_id=queue_item.item_id,
-                path=queue_item.uri,
-                content_type=ContentType(queue_item.uri.split(".")[-1]),
-            )
+        streamdetails = StreamDetails(
+            type=StreamType.URL,
+            provider="url",
+            item_id=queue_item.item_id,
+            path=queue_item.uri if queue_item.uri else queue_item.item_id,
+            content_type=ContentType(queue_item.uri.split(".")[-1]),
+        )
     else:
         # always request the full db track as there might be other qualities available
         # except for radio
@@ -316,8 +313,10 @@ def get_sox_args(
     filter_args = []
     if streamdetails.gain_correct:
         filter_args += ["vol", str(streamdetails.gain_correct), "dB"]
-    if resample:
+    if resample and resample > 48000:
         filter_args += ["rate", "-v", str(resample)]
+    elif resample:
+        filter_args += ["rate", str(resample)]
     # TODO: still not sure about the order of the filter arguments in the chain
     # assumption is they need to be at the end of the chain
     return input_args + output_args + filter_args
index aac1829db0d0524e0e723150eae6f9b64f9ac885..5fe9e4c1f1903945bc467b47ba4823c8faa00b48 100755 (executable)
@@ -9,7 +9,6 @@ from music_assistant.constants import (
     CONF_CROSSFADE_DURATION,
     CONF_POWER_CONTROL,
     CONF_VOLUME_CONTROL,
-    EVENT_ALERT_FINISHED,
     EVENT_PLAYER_ADDED,
     EVENT_PLAYER_REMOVED,
 )
@@ -29,6 +28,11 @@ from music_assistant.models.provider import PlayerProvider, ProviderType
 POLL_INTERVAL = 30
 
 LOGGER = logging.getLogger("player_manager")
+RESOURCES_DIR = (
+    pathlib.Path(__file__).parent.resolve().parent.resolve().joinpath("resources")
+)
+ALERT_ANNOUNCE_FILE = str(RESOURCES_DIR.joinpath("alert_announce.flac"))
+ALERT_FINISH_FILE = str(RESOURCES_DIR.joinpath("alert_finish.flac"))
 
 
 class PlayerManager:
@@ -297,6 +301,13 @@ class PlayerManager:
                 QueueOption.NEXT -> Play item(s) after current playing item
                 QueueOption.ADD -> Append new items at end of the queue
         """
+        # turn on player
+        player = self.get_player(player_id)
+        if not player:
+            raise FileNotFoundError("Player not found %s" % player_id)
+        if not player.calculated_state.powered:
+            await self.cmd_power_on(player_id)
+        player_queue = self.get_active_player_queue(player_id)
         # a single item or list of items may be provided
         if not isinstance(items, list):
             items = [items]
@@ -332,22 +343,10 @@ class PlayerManager:
             for track in tracks:
                 if not track.available:
                     continue
-                queue_item = QueueItem.from_track(track)
-                # generate url for this queue item
-                queue_item.stream_url = "%s/queue/%s/%s" % (
-                    self.mass.web.stream_url,
-                    player_id,
-                    queue_item.queue_item_id,
-                )
+                queue_item = player_queue.create_queue_item(track)
                 queue_items.append(queue_item)
-        # turn on player
-        player = self.get_player(player_id)
-        if not player:
-            raise FileNotFoundError("Player not found %s" % player_id)
-        if not player.calculated_state.powered:
-            await self.cmd_power_on(player_id)
+
         # load items into the queue
-        player_queue = self.get_active_player_queue(player_id)
         if queue_opt == QueueOption.REPLACE:
             return await player_queue.load(queue_items)
         if queue_opt in [QueueOption.PLAY, QueueOption.NEXT] and len(queue_items) > 100:
@@ -377,14 +376,6 @@ class PlayerManager:
             if item:
                 return await self.play_media(player_id, item, queue_opt)
             raise FileNotFoundError("Invalid uri: %s" % uri)
-        # fallback to regular url
-        queue_item = QueueItem(item_id=uri, provider="url", name=uri, uri=uri)
-        # generate url for this queue item
-        queue_item.stream_url = "%s/queue/%s/%s" % (
-            self.mass.web.stream_url,
-            player_id,
-            queue_item.queue_item_id,
-        )
         # turn on player
         player = self.get_player(player_id)
         if not player:
@@ -393,6 +384,9 @@ class PlayerManager:
             await self.cmd_power_on(player_id)
         # load items into the queue
         player_queue = self.get_active_player_queue(player_id)
+        queue_item = player_queue.create_queue_item(
+            item_id=uri, provider="url", name=uri, uri=uri
+        )
         if queue_opt == QueueOption.REPLACE:
             return await player_queue.load([queue_item])
         if queue_opt == QueueOption.NEXT:
@@ -459,50 +453,42 @@ class PlayerManager:
         # load alert items in player queue
         queue_items = []
         if announce:
-            alert_announce = (
-                pathlib.Path(__file__)
-                .parent.resolve()
-                .parent.resolve()
-                .joinpath("helpers", "alert.mp3")
-            )
-            queue_item = QueueItem(
-                item_id="alert_announce",
-                provider="url",
-                name="alert",
-                duration=3,
-                uri=str(alert_announce),
+            queue_items.append(
+                player_queue.create_queue_item(
+                    item_id="alert_announce",
+                    provider="url",
+                    name="alert_announce",
+                    uri=ALERT_ANNOUNCE_FILE,
+                )
             )
-            queue_item.stream_url = "%s/queue/%s/%s" % (
-                self.mass.web.stream_url,
-                player_queue.queue_id,
-                queue_item.queue_item_id,
+        queue_items.append(
+            player_queue.create_queue_item(
+                item_id="alert", provider="url", name="alert", uri=url
             )
-            queue_items.append(queue_item)
-
-        queue_item = QueueItem(
-            item_id="alert_sound",
-            provider="url",
-            name="alert",
-            duration=10,
-            uri=url,
         )
-        queue_item.stream_url = "%s/queue/%s/%s" % (
-            self.mass.web.stream_url,
-            player_queue.queue_id,
-            queue_item.queue_item_id,
+        queue_items.append(
+            # add a special (silent) file so we can detect finishing of the alert
+            player_queue.create_queue_item(
+                item_id="alert_finish",
+                provider="url",
+                name="alert_finish",
+                uri=ALERT_FINISH_FILE,
+            )
         )
-        queue_items.append(queue_item)
-
         # load queue items
         await player_queue.load(queue_items)
 
         # add listener when playback of alert finishes
-        async def restore_queue_listener(event: str, event_data: str):
-            """Restore queue after the alert was played."""
-            if event_data != queue_item.queue_item_id:
-                return
-            # player stopped playing
-            remove_cb()
+        async def restore_queue():
+            count = 0
+            while count < 30:
+                if (
+                    player_queue.cur_item == queue_items[-1]
+                    and player_queue.cur_item_time > 2
+                ):
+                    break
+                count += 1
+                await asyncio.sleep(1)
             # restore queue
             if volume:
                 await self.cmd_volume_set(player_id, prev_volume)
@@ -520,9 +506,7 @@ class PlayerManager:
                 await self.cmd_power_off(player_id)
             player_queue.signal_update()
 
-        remove_cb = self.mass.eventbus.add_listener(
-            restore_queue_listener, EVENT_ALERT_FINISHED
-        )
+        create_task(restore_queue)
 
     @api_route("players/{player_id}/cmd/stop", method="PUT")
     async def cmd_stop(self, player_id: str) -> None:
index 6682fd980efdb53708ce316deac243d5ed27702e..8eaebd78e069ce5d701a1ef0c5d09fdce568066d 100755 (executable)
@@ -10,7 +10,6 @@ from typing import Any, Dict, List, Optional, Set, Tuple, Union
 
 from music_assistant.constants import (
     CONF_CROSSFADE_DURATION,
-    EVENT_ALERT_FINISHED,
     EVENT_QUEUE_ITEMS_UPDATED,
     EVENT_QUEUE_UPDATED,
 )
@@ -371,7 +370,7 @@ class PlayerQueue:
         """Load (overwrite) queue with new items."""
         for index, item in enumerate(queue_items):
             item.sort_index = index
-        if self._shuffle_enabled and len(queue_items) > 2:
+        if self._shuffle_enabled and len(queue_items) > 5:
             queue_items = self.__shuffle_items(queue_items)
         self._items = queue_items
         if self.use_queue_stream:
@@ -396,7 +395,7 @@ class PlayerQueue:
         insert_at_index = self.cur_index + offset
         for index, item in enumerate(queue_items):
             item.sort_index = insert_at_index + index
-        if self.shuffle_enabled and len(queue_items) > 10:
+        if self.shuffle_enabled and len(queue_items) > 5:
             queue_items = self.__shuffle_items(queue_items)
         if offset == 0:
             # replace current item with new
@@ -527,18 +526,7 @@ class PlayerQueue:
         prev_item_time = int(self._cur_item_time)
         self._cur_item_time = int(track_time)
         if self._last_playback_state != self.state:
-            # handle special usecase where an alert is played
-            if (
-                self._last_playback_state == PlayerState.PLAYING
-                and self.state == PlayerState.IDLE
-                and self.cur_item
-                and self.cur_item.name == "alert"
-            ):
-                self.mass.eventbus.signal(
-                    EVENT_ALERT_FINISHED,
-                    self.cur_item.queue_item_id,
-                )
-            # fire regular event with updated state
+            # fire event with updated state
             self.signal_update()
             self._last_playback_state = self.state
         elif abs(prev_item_time - self._cur_item_time) > 3:
@@ -641,6 +629,19 @@ class PlayerQueue:
             self._cur_index = cache_data.get("cur_index", 0)
             self._queue_stream_next_index = self._cur_index
 
+    def create_queue_item(self, *args, **kwargs):
+        """Create QueueItem including correct stream URL."""
+        if args and isinstance(args[0], (Track, Radio)):
+            new_item = QueueItem.from_track(args[0])
+        else:
+            new_item = QueueItem(*args, **kwargs)
+        new_item.stream_url = "%s/queue/%s/%s" % (
+            self.mass.web.stream_url,
+            self.queue_id,
+            new_item.queue_item_id,
+        )
+        return new_item
+
     # pylint: enable=unused-argument
 
     async def _save_state(self) -> None:
diff --git a/music_assistant/resources/alert_announce.flac b/music_assistant/resources/alert_announce.flac
new file mode 100644 (file)
index 0000000..d14bfc8
Binary files /dev/null and b/music_assistant/resources/alert_announce.flac differ
diff --git a/music_assistant/resources/alert_finish.flac b/music_assistant/resources/alert_finish.flac
new file mode 100644 (file)
index 0000000..352dd6f
Binary files /dev/null and b/music_assistant/resources/alert_finish.flac differ
index 51cdc7bacff92594e2dede5a3f23d495976c40db..504ce491fd55ad0e5c3932441babf0a034cd4e99 100644 (file)
@@ -39,6 +39,8 @@ async def get_media_item_image_url(
     mass: MusicAssistant, media_type: MediaType, provider: str, item_id: str
 ) -> str:
     """Return image URL for given media item."""
+    if provider == "url":
+        return None
     return await get_image_url(mass, item_id, provider, media_type)
 
 
index b160b89f915dfe9c47fcc568cc9308ab8d445f72..7dc5020dfdea4fa3a606385725b8e60d087ab4d8 100644 (file)
@@ -54,8 +54,20 @@ async def stream_queue(request: Request):
     await resp.prepare(request)
 
     player_conf = player_queue.player.config
-    pcm_format = "f64"
-    sample_rate = min(player_conf.get(CONF_MAX_SAMPLE_RATE, 96000), 96000)
+    # determine sample rate and pcm format for the queue stream, depending on player capabilities
+    player_max_sample_rate = player_conf.get(CONF_MAX_SAMPLE_RATE, 48000)
+    sample_rate = min(player_max_sample_rate, 96000)
+    if player_max_sample_rate > 96000:
+        # assume that highest possible quality is needed
+        # if player supports sample rates > 96000
+        # we use float64 PCM format internally which is heavy on CPU
+        pcm_format = "f64"
+    elif sample_rate > 48000:
+        # prefer internal PCM_S32LE format
+        pcm_format = "s32"
+    else:
+        # fallback to 24 bits
+        pcm_format = "s24"
 
     args = [
         "sox",
@@ -80,7 +92,7 @@ async def stream_queue(request: Request):
         # feed stdin with pcm samples
         async def fill_buffer():
             """Feed audio data into sox stdin for processing."""
-            async for audio_chunk in get_queue_stream(
+            async for audio_chunk in get_pcm_queue_stream(
                 mass, player_queue, sample_rate, pcm_format
             ):
                 await sox_proc.write(audio_chunk)
@@ -235,7 +247,7 @@ async def get_media_stream(
                 )
 
 
-async def get_queue_stream(
+async def get_pcm_queue_stream(
     mass: MusicAssistant,
     player_queue: PlayerQueue,
     sample_rate=96000,
@@ -247,16 +259,14 @@ async def get_queue_stream(
     queue_index = None
     # get crossfade details
     fade_length = player_queue.crossfade_duration
-    if pcm_format in ["s64", "f64"]:
+    if "64" in pcm_format:
         bit_depth = 64
-    elif pcm_format in ["s32", "f32"]:
+    elif "32" in pcm_format:
         bit_depth = 32
-    elif pcm_format == "s16":
-        bit_depth = 16
-    elif pcm_format == "s24":
+    elif "24" in pcm_format:
         bit_depth = 24
     else:
-        raise NotImplementedError("Unsupported PCM format: %s" % pcm_format)
+        bit_depth = 16
     pcm_args = [pcm_format, "-c", "2", "-r", str(sample_rate)]
     sample_size = int(sample_rate * (bit_depth / 8) * channels)  # 1 second
     buffer_size = sample_size * fade_length if fade_length else sample_size * 10