From 755601469f069aeb7d4519d13220594b83252f02 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 6 Jun 2019 22:16:53 +0200 Subject: [PATCH] some improvements to the crossfade stuff --- music_assistant/models.py | 1 + music_assistant/modules/http_streamer.py | 177 ++++-------------- .../modules/playerproviders/chromecast.py | 12 +- 3 files changed, 46 insertions(+), 144 deletions(-) diff --git a/music_assistant/models.py b/music_assistant/models.py index 06609e34..ef2a0a06 100755 --- a/music_assistant/models.py +++ b/music_assistant/models.py @@ -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 diff --git a/music_assistant/modules/http_streamer.py b/music_assistant/modules/http_streamer.py index 67515e2d..572526da 100755 --- a/music_assistant/modules/http_streamer.py +++ b/music_assistant/modules/http_streamer.py @@ -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)) diff --git a/music_assistant/modules/playerproviders/chromecast.py b/music_assistant/modules/playerproviders/chromecast.py index d383a2e2..373cb8f9 100644 --- a/music_assistant/modules/playerproviders/chromecast.py +++ b/music_assistant/modules/playerproviders/chromecast.py @@ -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): -- 2.34.1