#!/usr/bin/env python3 """ Media player main module """ import glob import json import os import time import sys import pygame import pygame.freetype import emoji import gpiozero PLAY = pygame.USEREVENT+2 PAUSE = pygame.USEREVENT+3 PREV = pygame.USEREVENT+4 NEXT = pygame.USEREVENT+5 STOP = pygame.USEREVENT+6 VOLUP = pygame.USEREVENT+7 VOLDOWN = pygame.USEREVENT+8 MUTE = pygame.USEREVENT+9 SHUTDOWN = pygame.USEREVENT+10 def queue_event(event_code): """ Return a function that queues the specified event """ return lambda self: pygame.event.post(pygame.event.Event(pygame.USEREVENT, event_code=event_code)) class Player(): """ I represent the media player and hold all the state """ screen = None state = {} splash_images = {} def __init__(self, config_file='/home/pi/player/code/player.json'): pygame.init() pygame.mouse.set_visible(False) pygame.display.init() print(pygame.display.get_driver()) print(pygame.display.Info()) print(pygame.display.list_modes()) self.config_file = config_file self.load_config(config_file) os.chdir(self.state['config']['basedir']) self.screen = pygame.display.set_mode(self.state['config']['resolution'], pygame.HWSURFACE | pygame.DOUBLEBUF) self.splash_images = self.state['config']['splash_images'] self.state['background'] = pygame.image.load(self.splash_images['bootup']).convert() self.screen.blit(self.state['background'], self.screen.get_rect()) pygame.display.flip() self.switch_mode('album_select') self.on_rescan_albums() self.init_playback_settings() self.restart_playback_after_shutdown() self.init_gpios() def load_config(self, conf): """ Initialise the state from the config file """ with open(conf) as file: self.state['config'] = json.load(file) print(self.state) def store_config(self, filename): """ Write the current config to file """ with open(filename, mode = 'w') as file: file.write(json.dumps(self.state['config'], sort_keys=True, indent=4)) print('Storing', self.state['config']) def init_playback_settings(self): self.state['volume'] = self.state['config']['volume'] self.state['muted'] = False self.state['player'] = 'stopped' def restart_playback_after_shutdown(self): if self.state['config']['last'] != {} : self.state['current_album'] = self.state['config']['last']['album'] self.load_queue(self.state['current_album']) self.state['current_song'] = self.state['config']['last']['song'] self.state['starting_offset'] = self.state['config']['last']['pos'] if self.state['config']['last']['state'] == 'playing' or self.state['config']['last']['state'] == 'paused': self.display_album(self.state['current_album']) self.switch_mode('play_mode') self.on_play(offset=self.state['starting_offset']) def scan_for_files(self, path, types): return glob.glob(path + '/' + types, recursive=True) def scan_covers(self, media_path): return self.scan_for_files(media_path, '**/cover.png') def scan_album(self, dir): files = self.scan_for_files(dir, '*.ogg') if not files: files = self.scan_for_files(dir, '*.mp3') if not files: print('no files found') return sorted(files) def display_album(self, idx): self.state['current_album'] = idx self.state['background'] = self.state['albums'][idx]['surface'] def load_queue(self, idx): """ Load all music files from current album """ print(self.state['albums'][idx]['path']) self.state['queue'] = self.scan_album(self.state['albums'][idx]['path']) self.state['current_song'] = 0 print('Loaded songs from album', self.state['queue']) def on_play(self, offset=0): """ Handle play event """ if self.state['player'] == 'paused': return self.on_pause() pygame.mixer.music.load(self.state['queue'][self.state['current_song']]) self.state['player'] = 'playing' self.state['starting_offset'] = offset pygame.mixer.music.set_endevent(pygame.USEREVENT) pygame.mixer.music.play(start=offset) pygame.mixer.music.set_volume(self.state['volume']) print(self.state['player'], self.state['volume']) def on_pause(self): """ pause """ if self.state['player'] == 'playing': self.state['player'] = 'paused' pygame.mixer.music.pause() elif self.state['player'] == 'paused': pygame.mixer.music.unpause() self.state['player'] = 'playing' print(self.state['player']) def on_prev(self): """ select previous track """ print('previous') if self.state['current_song'] > 0: self.state['current_song'] -= 1 self.on_play() def on_next(self): """ select next track """ print('next') if self.state['current_song'] < len(self.state['queue']) - 1: self.state['current_song'] += 1 self.on_play() return True return False def on_stop(self): """ stop playback """ print('stop') self.state['player'] = 'stopped' self.state['muted'] = False pygame.mixer.music.pause() self.switch_mode('album_select') def on_volume_up(self): """ increase volume """ print('volup') if self.state['muted']: print('but muted') return if self.state['volume'] <= self.state['config']['volmax']: self.state['volume'] += self.state['config']['volchange'] pygame.mixer.music.set_volume(self.state['volume']) def on_volume_down(self): """ decrease volume """ print('voldown') if self.state['muted']: print('but muted') return if self.state['volume'] - self.state['config']['volchange'] > 0: self.state['volume'] -= self.state['config']['volchange'] else: self.state['volume'] = 0 pygame.mixer.music.set_volume(self.state['volume']) def on_mute(self): """ toggle silence! """ print('mute') if self.state['muted']: pygame.mixer.music.set_volume(self.state['volume']) self.state['muted'] = False else: self.state['muted'] = True pygame.mixer.music.set_volume(0) def on_shutdown(self): """ prepare and initialise power off """ print('shutdown') if self.state['player'] == 'playing' or self.state['player'] == 'paused': self.state['config']['last']['album'] = self.state['current_album'] self.state['config']['last']['song'] = self.state['current_song'] # storing the offset does not work with current SDL, so we restart from the last track for now self.state['config']['last']['pos'] = 0 # self.state['config']['last']['pos'] = self.state['starting_offset'] + pygame.mixer.music.get_pos() self.state['config']['last']['state'] = self.state['player'] else: self.state['config']['last'] = {} self.store_config(self.config_file) os.sync() print('power off') self.gpios['power_switch'].off() time.sleep(10) sys.exit(0) def on_select_album(self): """ queue all the sound files in the album dir and play the first """ self.switch_mode('play_mode') self.load_queue(self.state['current_album']) self.init_playback_settings() self.on_play() def on_prev_album(self): """ Display previous cover """ print('prev_album') self.state['current_album'] = (self.state['current_album'] - 1 ) % len( self.state['albums']) self.display_album(self.state['current_album']) def on_next_album(self): """ Display next cover """ print('next_album') self.state['current_album'] = (self.state['current_album'] + 1 ) % len( self.state['albums']) self.display_album(self.state['current_album']) def on_rescan_albums(self): """ rescan the media dir for album covers """ self.state['albums'] = {} albums = self.scan_covers(self.state['config']['music_dir']) for i, album in enumerate(sorted(albums)): print('loading', album) self.state['albums'][i] = {} self.state['albums'][i]['surface'] = pygame.image.load(album).convert() self.state['albums'][i]['path'] = os.path.dirname(album) self.display_album(0) print('Found albums', albums) play_mode = {PLAY : on_play, PAUSE :on_pause, PREV : on_prev, NEXT : on_next, STOP : on_stop, VOLUP :on_volume_up, VOLDOWN : on_volume_down, MUTE : on_mute, SHUTDOWN : on_shutdown, pygame.QUIT : on_shutdown} album_mode = { PLAY : on_select_album, PREV : on_prev_album, NEXT : on_next_album, STOP : on_rescan_albums, SHUTDOWN : on_shutdown, pygame.QUIT : on_shutdown } key_handlers = { pygame.K_SPACE : queue_event(PLAY), pygame.K_RETURN : queue_event(PAUSE), pygame.K_LEFTBRACKET : queue_event(PREV), pygame.K_RIGHTBRACKET : queue_event(NEXT), pygame.K_s : queue_event(STOP), pygame.K_COMMA : queue_event(VOLUP), pygame.K_PERIOD : queue_event(VOLDOWN), pygame.K_m : queue_event(MUTE), pygame.K_ESCAPE : queue_event(SHUTDOWN), pygame.K_q : queue_event(SHUTDOWN), } gpio_rotary = { 'clk' : 5, 'dat' : 6, } gpio_buttons = { 'rotary_sw' : (12, MUTE), 'blue_left' : (23, PREV), 'red' : (22, STOP), 'yellow' : (27, PAUSE), 'green' : (17, PLAY), 'blue_right' : (4, NEXT), 'green_pwr' : (24, SHUTDOWN), } def switch_mode(self, type): """ The player has different modes and I will handle the transitions by recreating the event handlers """ if type == 'album_select': self.event_handler = self.album_mode elif type == 'play_mode': self.event_handler = self.play_mode else: print('Unknown type', type) return print('switched mode to', type) self.event_handler['KEYS'] = self.key_handlers def init_gpios(self): """ setup the GPIOs and their callbacks """ self.gpios = {} self.gpios['buttons'] = {} self.gpios['rotary'] = {} for name in self.gpio_buttons: (pin, id) = self.gpio_buttons[name] print(pin, id, name) self.gpios['buttons'][name] = gpiozero.Button(pin, hold_time=0.01) self.gpios['buttons'][name].when_held = queue_event(id) for id in self.gpio_rotary: pin = self.gpio_rotary[id] print(id, pin) self.gpios['rotary'][id] = gpiozero.Button(pin) self.state['clklast'] = self.gpios['rotary']['clk'].value self.gpios['power_switch'] = gpiozero.DigitalOutputDevice(25, initial_value=True, active_high=False) def poll_rotary_encoder(self): clk = self.gpios['rotary']['clk'].is_pressed dat = self.gpios['rotary']['dat'].is_pressed if clk != self.state['clklast']: if dat != clk: queue_event(VOLUP)(self) else: queue_event(VOLDOWN)(self) self.state['clklast'] = clk def display_state(self): """ I will draw screen updates """ screen_rect = self.screen.get_rect() fgcolor = pygame.Color(self.state['config']['colors']['foreground']) bgcolor = pygame.Color(self.state['config']['colors']['background']) if self.state['player'] != 'stopped': pos = self.state['starting_offset'] + pygame.mixer.music.get_pos() track = self.state['current_song'] + 1 # it is 0 based... if self.state['player'] == 'playing': icon = "▶" else: icon = u'\u23F8' seconds = int(pos/1000)%60 minutes = int((pos/1000)/60) % 60 hours = int((pos/1000)/60/60) text = "{} {:02} {:02}:{:02}:{:02}".format(icon, track, hours, minutes, seconds) else: text = os.path.dirname(self.state['albums'][self.state['current_album']]['path']) font = pygame.font.Font(pygame.font.match_font(self.state['config']['font']), self.state['config']['fontsize']) draw = font.render(text, True, fgcolor) text_rect = pygame.Rect((screen_rect[2] - font.size(text)[0])/2 , screen_rect[3] - font.size(text)[1], screen_rect[2], screen_rect[3]) status_rect = pygame.Rect(0,screen_rect[3] - font.size(text)[1], screen_rect[2], font.size(text)[1]) self.screen.fill(pygame.Color(0, 0, 0, 255)) cover = pygame.transform.scale(self.state['background'], (screen_rect[2], screen_rect[3])) self.screen.blit(cover, screen_rect) if self.state['player'] != 'stopped': pygame.draw.rect(self.screen, bgcolor, status_rect) self.screen.blit(draw, text_rect) if self.state['muted']: mute_font = pygame.freetype.Font(pygame.font.match_font(self.state['config']['font']), self.state['config']['fontsize']*2) mute_symbol = mute_font.render(emoji.emojize(':mute:', use_aliases=True), bgcolor=bgcolor, fgcolor=fgcolor) mute_symbol[1].move_ip(screen_rect[2]/2-mute_symbol[1][2]/2, screen_rect[3]/2-mute_symbol[1][3]) self.screen.blit(mute_symbol[0], mute_symbol[1]) pygame.display.flip() def run(self): """ Run the main loop """ self.state['running'] = True while self.state['running']: self.display_state() pygame.time.Clock().tick() pygame.display.flip() self.poll_rotary_encoder() ev = pygame.event.poll() if ev.type == pygame.USEREVENT: if 'event_code' not in ev.__dict__: if not self.on_next(): self.on_stop() elif ev.event_code in self.event_handler: self.event_handler[ev.event_code](self) else: print('Unknown event', ev) if ev.type == pygame.KEYDOWN: if ev.key in self.event_handler['KEYS']: self.event_handler['KEYS'][ev.key](self) def main(): """ Main function """ player = Player() player.run() if __name__ == '__main__': main()