some improvements to the crossfade stuff
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 6 Jun 2019 20:16:53 +0000 (22:16 +0200)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 6 Jun 2019 20:16:53 +0000 (22:16 +0200)
music_assistant/models.py
music_assistant/modules/http_streamer.py
music_assistant/modules/playerproviders/chromecast.py

index 06609e346f36a6ab20fe8f0b02430fe6c8e3231b..ef2a0a065c41f14db3e0386b6f7f7d3a92cfc847 100755 (executable)
@@ -504,6 +504,7 @@ class MusicPlayer():
         self.powered = False
         self.cur_item = None
         self.cur_item_time = 0
+        self.cur_queue_index = 0
         self.volume_level = 0
         self.shuffle_enabled = True
         self.repeat_enabled = False
index 67515e2ddef890646f8cd0aeb2240f140618e9c7..572526da47fe0617ca47859bb9813ddba5f261e6 100755 (executable)
@@ -156,122 +156,9 @@ class HTTPStreamer():
                 raise asyncio.CancelledError()
         return resp
 
-    async def __stream_queue__old_usingfile(self, player_id, buffer, cancelled):
-        ''' start streaming radio from provider '''
-        # stream audio with sox
-        queue_tracks = await self.mass.player.player_queue(player_id, 0, 1000)
-        sample_rate = self.mass.config['player_settings'][player_id]['max_sample_rate']
-        fade_length = self.mass.config['player_settings'][player_id]["crossfade_duration"]
-        pcm_args = 'raw -b 32 -c 2 -e signed-integer -r %s' % sample_rate
-        args = 'sox -t %s - -t flac -C 2 -' % pcm_args
-        sox_proc = await asyncio.create_subprocess_shell(args, 
-                stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE)
-
-        async def fill_buffer():
-            while not sox_proc.stdout.at_eof():
-                chunk = await sox_proc.stdout.read(256000)
-                if not chunk:
-                    break
-                await buffer.put(chunk)
-            await sox_proc.wait()
-            await buffer.put('') # indicate EOF
-            LOGGER.info("streaming of queue for player %s completed" % player_id)
-        asyncio.create_task(fill_buffer())
-
-        last_fadeout_data = None
-        for queue_track in queue_tracks:
-            
-            while buffer.qsize() > 5 and not cancelled.is_set():
-                await asyncio.sleep(1)
-            if cancelled.is_set():
-                break
-            
-            params = urllib.parse.parse_qs(queue_track.uri.split('?')[1])
-            track_id = params['track_id'][0]
-            provider = params['provider'][0]
-            LOGGER.info("Stream queue track: %s - %s" % (track_id, queue_track.name))
-            temp_file = await self.__get_raw_audio(track_id, provider, sample_rate)
-            
-            # get fade in part
-            args = 'sox -t sox %s -t %s - trim 0 %s fade t %s' % (temp_file.name, pcm_args,  fade_length, fade_length)
-            process = await asyncio.create_subprocess_shell(args,
-                    stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE)
-            fade_in_part, stderr = await process.communicate()
-            LOGGER.debug("Got %s bytes in memory for fadein_part after sox" % len(fade_in_part))
-            if last_fadeout_data:
-                # perform crossfade with previous fadeout samples
-                fadeinfile = MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0)
-                fadeinfile.write(fade_in_part)
-                fadeoutfile = MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0)
-                fadeoutfile.write(last_fadeout_data)
-                args = 'sox -m -v 1.0 -t %s %s -v 1.0 -t %s %s -t %s -' % (pcm_args, fadeoutfile.name, pcm_args, fadeinfile.name, pcm_args)
-                process = await asyncio.create_subprocess_shell(args,
-                        stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE)
-                crossfade_part, stderr = await process.communicate(fade_in_part)
-                LOGGER.debug("Got %s bytes in memory for crossfade_part after sox" % len(crossfade_part))
-                sox_proc.stdin.write(crossfade_part)
-                await sox_proc.stdin.drain()
-                fadeinfile.close()
-                fadeoutfile.close()
-            else:
-                # simply put the fadein part in the final file
-                sox_proc.stdin.write(fade_in_part)
-                await sox_proc.stdin.drain()
-
-            # get middle frames (main track without the fade-in and fade-out)
-            args = 'sox -t sox %s -t %s - trim %s -%s' % (temp_file.name, pcm_args, fade_length, fade_length)
-            process = await asyncio.create_subprocess_shell(args,
-                    stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE)
-            middle_part, stderr = await process.communicate()
-            LOGGER.debug("Got %s bytes in memory for middle_part after sox" % len(middle_part))
-            sox_proc.stdin.write(middle_part)
-            await sox_proc.stdin.drain()
-
-            # get fade out part (all remaining chunks of 1 second)
-            args = 'sox -t sox %s -t %s - reverse trim 0 %s fade t %s reverse ' % (temp_file.name, pcm_args, fade_length, fade_length)
-            process = await asyncio.create_subprocess_shell(args,
-                    stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE)
-            fade_out_part, stderr = await process.communicate()
-            LOGGER.debug("Got %s bytes in memory for fade_out_part after sox" % len(fade_out_part))
-            last_fadeout_data = fade_out_part
-            # close temp file
-            temp_file.close()
-        # end of queue reached, pass last fadeout bits to final output
-        if last_fadeout_data:
-            sox_proc.stdin.write(last_fadeout_data)
-            await sox_proc.stdin.drain()
-
-    async def __get_raw_audio__old_usingfile(self, track_id, provider, sample_rate=96000):
-        ''' get raw pcm data for a track upsampled to given sample_rate packed as wav '''
-        temp_audiofile = MemoryTempfile(fallback=True).NamedTemporaryFile(buffering=0)
-        cachefile = self.__get_track_cache_filename(track_id, provider)
-        if self.mass.config['base']['http_streamer']['volume_normalisation']:
-            gain_correct = await self.__get_track_gain_correct(track_id, provider)
-        else:
-            gain_correct = -6 # always need some headroom for upsampling and crossfades
-        if os.path.isfile(cachefile):
-            # we have a cache file for this track which we can use
-            # always convert to 64 bit floating point to do any processing/effects
-            args = 'sox -V3 -t flac "%s" -t sox %s vol %s dB rate -v %s' % (cachefile, temp_audiofile.name, gain_correct, sample_rate)
-            process = await asyncio.create_subprocess_shell(args)
-        else:
-            # stream from provider
-            # always convert to 64 bit floating point to do any processing/effects
-            input_content_type = await self.mass.music.providers[provider].get_stream_content_type(track_id)
-            assert(input_content_type)
-            args = 'sox -V3 -t %s - -t sox %s vol %s dB rate -v %s' % (input_content_type, temp_audiofile.name, gain_correct, sample_rate)
-            process = await asyncio.create_subprocess_shell(args,
-                    stdin=asyncio.subprocess.PIPE)
-            asyncio.get_event_loop().create_task(
-                     self.__fill_audio_buffer(process.stdin, track_id, provider, input_content_type))
-        await process.wait()
-        LOGGER.debug("__get_pcm_audio for track_id %s completed" % track_id)
-        return temp_audiofile
-    
     async def __stream_queue(self, player_id, buffer, cancelled):
         ''' start streaming all queue tracks '''
         # TODO: get correct queue index and implement reporting of position
-        queue_tracks = await self.mass.player.player_queue(player_id, 0, 1000)
         sample_rate = self.mass.config['player_settings'][player_id]['max_sample_rate']
         fade_length = self.mass.config['player_settings'][player_id]["crossfade_duration"]
         pcm_args = 'raw -b 32 -c 2 -e signed-integer -r %s' % sample_rate
@@ -285,28 +172,34 @@ class HTTPStreamer():
                 if not chunk:
                     break
                 await buffer.put(chunk)
-            await buffer.put('') # indicate EOF
+            await buffer.put(b'') # indicate EOF
         asyncio.create_task(fill_buffer())
 
         last_fadeout_data = None
-        for queue_track in queue_tracks:
-            
-            while buffer.qsize() > 5 and not cancelled.is_set():
-                await asyncio.sleep(1)
-            if cancelled.is_set():
+        while True:
+            # get current track in queue
+            queue_tracks = await self.mass.player.player_queue(player_id, 0, 10000)
+            player = await self.mass.player.player(player_id)
+            queue_index = player.cur_queue_index
+            try:
+                queue_track = queue_tracks[queue_index]
+            except IndexError:
+                LOGGER.info("queue index out of range or end reached")
                 break
-            
+
             params = urllib.parse.parse_qs(queue_track.uri.split('?')[1])
             track_id = params['track_id'][0]
             provider = params['provider'][0]
             LOGGER.info("Stream queue track: %s - %s" % (track_id, queue_track.name))
             audiodata = await self.__get_raw_audio(track_id, provider, sample_rate)
+            fade_bytes = int(sample_rate * 4 * 2 * fade_length)
+            LOGGER.debug("total bytes in audio_data: %s - fade_bytes: %s" % (len(audiodata),fade_bytes))
             
             # get fade in part
-            args = 'sox -t sox - -t %s - trim 0 %s fade t %s' % (pcm_args,  fade_length, fade_length)
+            args = 'sox --ignore-length -t %s - -t %s - fade t %s' % (pcm_args, pcm_args, fade_length)
             process = await asyncio.create_subprocess_shell(args,
                     stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE)
-            fade_in_part, stderr = await process.communicate(audiodata[0:15360000])
+            fade_in_part, stderr = await process.communicate(audiodata[:fade_bytes])
             LOGGER.debug("Got %s bytes in memory for fadein_part after sox" % len(fade_in_part))
             if last_fadeout_data:
                 # perform crossfade with previous fadeout samples
@@ -332,24 +225,27 @@ class HTTPStreamer():
                 await sox_proc.stdin.drain()
                 del fade_in_part
 
-            # get middle frames (main track without the fade-in and fade-out)
-            args = 'sox -t sox - -t %s - trim %s -%s' % (pcm_args, fade_length, fade_length)
-            process = await asyncio.create_subprocess_shell(args,
-                    stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE)
-            middle_part, stderr = await process.communicate(audiodata)
-            LOGGER.debug("Got %s bytes in memory for middle_part after sox" % len(middle_part))
-            sox_proc.stdin.write(middle_part)
+            # feed the middle part into the main sox
+            sox_proc.stdin.write(audiodata[fade_bytes:-fade_bytes])
             await sox_proc.stdin.drain()
-            del middle_part
 
             # get fade out part
-            args = 'sox -t sox - -t %s - reverse trim 0 %s fade t %s reverse ' % (pcm_args, fade_length, fade_length)
+            args = 'sox --ignore-length -t %s - -t %s - reverse fade t %s reverse' % (pcm_args, pcm_args, fade_length)
             process = await asyncio.create_subprocess_shell(args,
                     stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE)
-            last_fadeout_data, stderr = await process.communicate(audiodata)
+            last_fadeout_data, stderr = await process.communicate(audiodata[-fade_bytes:])
             LOGGER.debug("Got %s bytes in memory for fade_out_part after sox" % len(last_fadeout_data))
             # cleanup audio data
             del audiodata
+
+            # wait for the queue to consume the data
+            while buffer.qsize() > 5 and not cancelled.is_set():
+                await asyncio.sleep(1)
+            if cancelled.is_set():
+                break
+            # assume end of track and increase queue_index
+            player.cur_queue_index += 1
+            await self.mass.player.trigger_update(player_id)
         
         # end of queue reached, pass last fadeout bits to final output
         if last_fadeout_data:
@@ -363,26 +259,27 @@ class HTTPStreamer():
         ''' get raw pcm data for a track upsampled to given sample_rate packed as wav '''
         audiodata = b''
         cachefile = self.__get_track_cache_filename(track_id, provider)
+        pcm_args = 'raw -b 32 -c 2 -e signed-integer'
         if self.mass.config['base']['http_streamer']['volume_normalisation']:
             gain_correct = await self.__get_track_gain_correct(track_id, provider)
         else:
             gain_correct = -6 # always need some headroom for upsampling and crossfades
         if os.path.isfile(cachefile):
             # we have a cache file for this track which we can use
-            args = 'sox -t flac "%s" -t sox - vol %s dB rate -v %s' % (cachefile, gain_correct, sample_rate)
+            args = 'sox -t flac "%s" -t %s - vol %s dB rate -v %s' % (cachefile, pcm_args, gain_correct, sample_rate)
             process = await asyncio.create_subprocess_shell(args, stdout=asyncio.subprocess.PIPE)
         else:
             # stream from provider
             input_content_type = await self.mass.music.providers[provider].get_stream_content_type(track_id)
             assert(input_content_type)
-            args = 'sox -t %s - -t sox - vol %s dB rate -v %s' % (input_content_type, gain_correct, sample_rate)
+            args = 'sox -t %s - -t %s - vol %s dB rate -v %s' % (input_content_type, pcm_args, gain_correct, sample_rate)
             process = await asyncio.create_subprocess_shell(args,
                     stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE)
             asyncio.get_event_loop().create_task(
                      self.__fill_audio_buffer(process.stdin, track_id, provider, input_content_type))
         #await process.wait()
         audiodata, stderr = await process.communicate()
-        LOGGER.debug("__get_pcm_audio for track_id %s completed" % track_id)
+        LOGGER.debug("__get_raw_audio for track_id %s completed" % (track_id))
         return audiodata
     
     async def __get_audio_stream(self, audioqueue, track_id, provider, player_id=None, cancelled=None):
@@ -475,12 +372,8 @@ class HTTPStreamer():
                 process = await asyncio.create_subprocess_shell(cmd)
                 await process.wait()
             if self.mass.config['base']['http_streamer']['enable_cache'] and not os.path.isfile(cachefile):
-                # use sox to store cache file (optionally strip silence from start and end)
-                if self.mass.config['base']['http_streamer']['trim_silence']:
-                    cmd = 'sox -t %s %s -t flac -C5 %s silence 1 0.1 1%% reverse silence 1 0.1 1%% reverse' %(content_type, tmpfile, cachefile)
-                else:
-                    # cachefile is always stored as flac 
-                    cmd = 'sox -t %s %s -t flac -C5 %s' %(content_type, tmpfile, cachefile)
+                # use sox to store cache file (strip silence from start and end for better transitions)
+                cmd = 'sox -t %s %s -t flac -C5 %s silence 1 0.1 1%% reverse silence 1 0.1 1%% reverse' %(content_type, tmpfile, cachefile)
                 process = await asyncio.create_subprocess_shell(cmd)
                 await process.wait()
         # always clean up temp file
@@ -524,7 +417,7 @@ class HTTPStreamer():
         await buf.drain()
         buf.write_eof()
         fd.close()
-        LOGGER.debug("fill_audio_buffer complete for track %s" % track_id)
+        LOGGER.info("fill_audio_buffer complete for track %s" % track_id)
         # successfull completion, process temp file for analysis
         self.mass.event_loop.create_task(
                 self.__analyze_audio(tmpfile, track_id, provider, content_type))
index d383a2e222d7f5a09b43f53829484dc88a2dee34..373cb8f9fc1add88e50937be826bd0cd88129131 100644 (file)
@@ -85,8 +85,10 @@ class ChromecastProvider(PlayerProvider):
         elif cmd == 'stop':
             self._chromecasts[player_id].media_controller.stop()
         elif cmd == 'next':
+            self.mass.player._players[player_id].cur_queue_index +=1
             self._chromecasts[player_id].media_controller.queue_next()
         elif cmd == 'previous':
+            self.mass.player._players[player_id].cur_queue_index -=1
             self._chromecasts[player_id].media_controller.queue_prev()
         elif cmd == 'power' and cmd_args == 'off':
             self._players[player_id].powered = False
@@ -146,6 +148,9 @@ class ChromecastProvider(PlayerProvider):
 
     async def __get_cur_queue_index(self, player_id):
         ''' retrieve index of current item in the player queue '''
+        enable_crossfade = self.mass.config['player_settings'][player_id]["crossfade_duration"] > 0
+        if enable_crossfade and player_id in self.mass.player._players:
+            return self.mass.player._players[player_id].cur_queue_index
         cur_index = 0
         for index, track in enumerate(self._player_queue[player_id]):
             if track.uri == self._chromecasts[player_id].media_controller.status.content_id:
@@ -158,6 +163,7 @@ class ChromecastProvider(PlayerProvider):
         castplayer = self._chromecasts[player_id]
         player = self._players[player_id]
         queue_items = await self.__create_queue_items(new_tracks[:50], player_id=player_id)
+        self.mass.player._players[player_id].cur_queue_index = 0
         queuedata = { 
                 "type": 'QUEUE_LOAD',
                 "repeatMode":  "REPEAT_ALL" if player.repeat_enabled else "REPEAT_OFF",
@@ -313,8 +319,10 @@ class ChromecastProvider(PlayerProvider):
             provider = params['provider'][0]
             track = await self.mass.music.providers[provider].track(track_id)
         elif uri.startswith('http') and '/stream_queue' in uri:
-            track = Track()
-            track.name = "Crossfade Queue streaming"
+            params = urllib.parse.parse_qs(uri.split('?')[1])
+            player_id = params['player_id'][0]
+            queue_index = await self.__get_cur_queue_index(player_id)
+            track = self._player_queue[player_id][queue_index]
         return track
 
     async def __handle_group_members_update(self, mz, added_player=None, removed_player=None):