class WinampApplication(displayio.Group): """ WinampApplication Helper class that manages song playback and UI components. :param playlist_file: json file containing the playlist of songs :param skin_image: BMP image file for skin background :param skin_config_file: json file containing color values :param pyportal_titano: boolean value. True if using Titano, False otherwise. """ STATE_PLAYING = 0 STATE_PAUSED = 1 # pylint: disable=too-many-statements,too-many-branches def __init__( self, playlist_file="playlist.json", skin_image="/base_240x320.bmp", skin_config_file="base_config.json", pyportal_titano=False, ): self.SKIN_IMAGE = skin_image self.SKIN_CONFIG_FILE = skin_config_file self.PLAYLIST_FILE = playlist_file # read the skin config data into variable f = open(self.SKIN_CONFIG_FILE, "r") self.CONFIG_DATA = json.loads(f.read()) f.close() if self.PLAYLIST_FILE: try: # read the playlist data into variable f = open(self.PLAYLIST_FILE, "r") self.PLAYLIST = json.loads(f.read()) f.close() except OSError: # file not found self.auto_find_tracks() except ValueError: # json parse error self.auto_find_tracks() else: # playlist file argument was None self.auto_find_tracks() if self.PLAYLIST: try: if len(self.PLAYLIST["playlist"]["files"]) == 0: # valid playlist json data, but no tracks self.auto_find_tracks() except KeyError: self.auto_find_tracks() # initialize clock display self.clock_display = ClockDisplay( text_color=self.CONFIG_DATA["time_color"]) if not pyportal_titano: # standard PyPortal and pynt clock display location # and playlist display parameters self.clock_display.x = 44 self.clock_display.y = 22 _max_playlist_display_chars = 30 _rows = 3 else: # PyPortal Titano clock display location # and playlist display parameters self.clock_display.x = 65 self.clock_display.y = 37 _max_playlist_display_chars = 42 _rows = 4 # initialize playlist display self.playlist_display = PlaylistDisplay( text_color=self.CONFIG_DATA["text_color"], max_chars=_max_playlist_display_chars, rows=_rows, ) if not pyportal_titano: # standard PyPortal and pynt playlist display location self.playlist_display.x = 13 self.playlist_display.y = 234 else: # PyPortal Titano playlist display location self.playlist_display.x = 20 self.playlist_display.y = 354 # set playlist into playlist display self.playlist_display.from_files_list( self.PLAYLIST["playlist"]["files"]) self.playlist_display.current_track_number = 1 # get name of current song self.current_song_file_name = self.PLAYLIST["playlist"]["files"][ self.playlist_display.current_track_number - 1] if not pyportal_titano: # standard PyPortal and pynt max characters for track title _max_chars = 22 else: # PyPortal Titano max characters for track title _max_chars = 29 # initialize ScrollingLabel for track name self.current_song_lbl = scrolling_label.ScrollingLabel( terminalio.FONT, text=self.playlist_display.current_track_title, color=self.CONFIG_DATA["text_color"], max_characters=_max_chars, ) self.current_song_lbl.anchor_point = (0, 0) if not pyportal_titano: # standard PyPortal and pynt track title location self.current_song_lbl.anchored_position = (98, 19) else: # PyPortal Titano track title location self.current_song_lbl.anchored_position = (130, 33) # Setup the skin image file as the bitmap data source self.background_bitmap = displayio.OnDiskBitmap(self.SKIN_IMAGE) # Create a TileGrid to hold the bitmap self.background_tilegrid = displayio.TileGrid( self.background_bitmap, pixel_shader=self.background_bitmap.pixel_shader) # initialize parent displayio.Group super().__init__() # Add the TileGrid to the Group self.append(self.background_tilegrid) # add other UI componenets self.append(self.current_song_lbl) self.append(self.clock_display) self.append(self.playlist_display) # Start playing first track self.current_song_file = open(self.current_song_file_name, "rb") self.decoder = MP3Decoder(self.current_song_file) self.audio = AudioOut(board.SPEAKER) self.audio.play(self.decoder) self.CURRENT_STATE = self.STATE_PLAYING # behavior variables. self._start_time = time.monotonic() self._cur_time = time.monotonic() self._pause_time = None self._pause_elapsed = 0 self._prev_time = None self._seconds_elapsed = 0 self._last_increment_time = 0 def auto_find_tracks(self): """ Initialize the song_list by searching for all MP3's within two layers of directories on the SDCard. e.g. It will find all of: /sd/Amazing Song.mp3 /sd/[artist_name]/Amazing Song.mp3 /sd/[artist_name]/[album_name]/Amazing Song.mp3 but won't find: /sd/my_music/[artist_name]/[album_name]/Amazing Song.mp3 :return: None """ # list that holds all files in the root of SDCard _root_sd_all_files = os.listdir("/sd/") # list that will hold all directories in the root of the SDCard. _root_sd_dirs = [] # list that will hold all subdirectories inside of root level directories _second_level_dirs = [] # list that will hold all MP3 file songs that we find _song_list = [] # loop over all files found on SDCard for _file in _root_sd_all_files: try: # Check if the current file is a directory os.listdir("/sd/{}".format(_file)) # add it to a list to look at later _root_sd_dirs.append(_file) except OSError: # current file was not a directory, nothing to do. pass # if current file is an MP3 file if _file.endswith(".mp3"): # we found an MP3 file, add it to the list that will become our playlist _song_list.append("/sd/{}".format(_file)) # loop over root level directories for _dir in _root_sd_dirs: # loop over all files inside of root level directory for _file in os.listdir("/sd/{}".format(_dir)): # check if current file is a directory try: # if it is a directory, loop over all files inside of it for _inner_file in os.listdir("/sd/{}/{}".format( _dir, _file)): # check if inner file is an MP3 if _inner_file.endswith(".mp3"): # we found an MP3 file, add it to the list that will become our playlist _song_list.append("/sd/{}/{}/{}".format( _dir, _file, _inner_file)) except OSError: # current file is not a directory pass # if the current file is an MP3 file if _file.endswith(".mp3"): # we found an MP3 file, add it to the list that will become our playlist _song_list.append("/sd/{}/{}".format(_dir, _file)) # format the songs we found into the PLAYLIST data structure self.PLAYLIST = {"playlist": {"files": _song_list}} # print message to user letting them know we auto-generated the playlist print("Auto Generated Playlist from MP3's found on SDCard:") print(json.dumps(self.PLAYLIST)) def update(self): """ Must be called each iteration from the main loop. Responsible for updating all sub UI components and managing song playback :return: None """ self._cur_time = time.monotonic() if self.CURRENT_STATE == self.STATE_PLAYING: # if it's time to increase the time on the ClockDisplay if self._cur_time >= self._last_increment_time + 1: # increase ClockDisplay by 1 second self._seconds_elapsed += 1 self._last_increment_time = self._cur_time self.clock_display.seconds = int(self._seconds_elapsed) # update the track label (scrolling) self.current_song_lbl.update() if self.CURRENT_STATE == self.STATE_PLAYING: # if we are supposed to be playing but aren't # it means the track ended. if not self.audio.playing: # start the next track self.next_track() # store time for comparison later self._prev_time = self._cur_time def play_current_track(self): """ Update the track label and begin playing the song for current track in the playlist. :return: None """ # set the track title self.current_song_lbl.full_text = self.playlist_display.current_track_title # save start time in a variable self._start_time = self._cur_time # if previous song is still playing if self.audio.playing: # stop playing self.audio.stop() # close previous song file self.current_song_file.close() # open new song file self.current_song_file_name = self.PLAYLIST["playlist"]["files"][ self.playlist_display.current_track_number - 1] self.current_song_file = open(self.current_song_file_name, "rb") self.decoder.file = self.current_song_file # play new song file self.audio.play(self.decoder) # if user paused the playback if self.CURRENT_STATE == self.STATE_PAUSED: # pause so it's loaded, and ready to resume self.audio.pause() def next_track(self): """ Advance to the next track. :return: None """ # reset ClockDisplay to 0 self._seconds_elapsed = 0 self.clock_display.seconds = int(self._seconds_elapsed) # increment current track number self.playlist_display.current_track_number += 1 try: # start playing track self.play_current_track() except OSError as e: # file not found print("Error playing: {}".format(self.current_song_file_name)) print(e) self.next_track() return def previous_track(self): """ Go back to previous track. :return: None """ # reset ClockDisplay to 0 self._seconds_elapsed = 0 self.clock_display.seconds = int(self._seconds_elapsed) # decrement current track number self.playlist_display.current_track_number -= 1 try: # start playing track self.play_current_track() except OSError as e: # file not found print("Error playing: {}".format(self.current_song_file_name)) print(e) self.previous_track() return def pause(self): """ Stop playing song and wait until resume function. :return: None """ if self.audio.playing: self.audio.pause() self.CURRENT_STATE = self.STATE_PAUSED def resume(self): """ Resume playing song after having been paused. :return: None """ self._last_increment_time = self._cur_time if self.audio.paused: self.audio.resume() self.CURRENT_STATE = self.STATE_PLAYING
from audioio import AudioOut except ImportError: try: from audiopwmio import PWMAudioOut as AudioOut except ImportError: pass # not always supported by every board! button = digitalio.DigitalInOut(board.A1) button.switch_to_input(pull=digitalio.Pull.UP) wave_file = open("StreetChicken.wav", "rb") wave = WaveFile(wave_file) audio = AudioOut(board.A0) while True: audio.play(wave) # This allows you to do other things while the audio plays! t = time.monotonic() while time.monotonic() - t < 6: pass audio.pause() print("Waiting for button press to continue!") while button.value: pass audio.resume() while audio.playing: pass print("Done!")