From b46d438fb45d53722c370a5496357abcda1ef3b6 Mon Sep 17 00:00:00 2001 From: raspbeguy Date: Thu, 16 Mar 2017 23:01:20 +0100 Subject: [PATCH] MPD: lot of comments, fix blocking fifo read --- mpd_dot3k.py | 109 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 95 insertions(+), 14 deletions(-) diff --git a/mpd_dot3k.py b/mpd_dot3k.py index c242f55..96c8dbc 100644 --- a/mpd_dot3k.py +++ b/mpd_dot3k.py @@ -9,13 +9,27 @@ import threading import signal import sys import os +import select import audioop import dot3k.lcd as lcd import dot3k.backlight as backlight import dot3k.joystick as nav from mpd import MPDClient -rotate_pause = 10 # delay in rotation steps +# When a line is too long to fit in a line of the LCD display, +# it scrolls with a cool effect to let you read the entire text. +# You may choose the speed of the scrolling and the pause between +# two rotations. +step_length = 0.2 # delay in seconds +rotate_pause = 10 # delay in steps count + + +# Symbols play, pause and stop are not defined in the dot3k library. +# We need to draw them manually. +# On the display, a character is a 8x5 pixels matrix. It is represented +# in python as a list of 8 integers, one per line. Each integer has +# 5 bits, one per raw. +# It is easier to draw if you represent your integers in binary notation. char_play = [ 0b10000, @@ -50,33 +64,82 @@ char_stop = [ 0b00000, ] - +# We use a queue to communicate the song info to the display thread. +# The main reason is that it has some amazing blocking properties that +# enable not to think too much about mutex/lock/whatever logic you might +# think about especially if you coded in C before. +# It's python magic... q = queue.Queue() +# This variable is the flag that will be turned down by the main thread +# to notify the other threads that they need to stop in order for the +# program to quit gracefully. +run_event = threading.Event() + def vumeter(): + ''' + This function will me used as the thread in charge of the LEDs + behind the screen. We'll use them to watch the MPD output power + in real time. Cool, huh? In order to do that, you need to configure + an extra output in MPD by adding this snippet in /etc/mpd.conf: + + audio_output { + type "fifo" + name "my_fifo" + path "/tmp/mpd.fifo" + format "44100:16:2" + } + ''' fifo = os.open('/tmp/mpd.fifo', os.O_RDONLY) while run_event.is_set(): - try: - rawStream = os.read(fifo, 4096) - except OSError as err: - if err.errno == errno.EAGAIN or err.errno == errno.EWOULDBLOCK: - rawStream = None - else: - raise - if rawStream: - backlight.set_graph(audioop.rms(rawStream,2)/32768) + # When MPD is in pause or stopped, it doesn't write any EOF in the fifo + # so os.read is blocked. In order to avoid that, we need to put a timeout, + # and as os.open doesn't support any timeout, we need to put a select + # before, as it has a timeout parameter. We only need the r return variable, + # which is the list of readable file descriptors given in parameters, + # returned as soon as one get ready or after the timeout. + # We do all this in order to have a clean quit, else we wouldn't give a shit. + r, w, e = select.select([ fifo ], [], [], 1) + if fifo in r: + try: + # We read a chunk of the sound that MPD put in the FIFO. + # Each chunk is a sequence of samples that describes the signal. + rawStream = os.read(fifo, 4096) + except OSError as err: + if err.errno == errno.EAGAIN or err.errno == errno.EWOULDBLOCK: + rawStream = None + else: + raise + if rawStream: + # The RMS function calculates the square root of the average of + # the squared samples: sqrt(sum(Si^2)/n) + # This is a good power indicator. + # Then, we just normalize it to reduce it between 0 and 1. + # Finally, we call the set_graph methos that will display the + # fraction by putting on the right number of LEDs. + backlight.set_graph(audioop.rms(rawStream,2)/32768) + # After exiting the almost-infinite loop, we turn the LEDs off. backlight.set_graph(0) def stringrotate(text, width, step): + ''' + This function's purpose is to scroll the given text to the given step, + knowing that the screen has the given width. + ''' if len(text) > width: fulltext = text + " - " fulltext = fulltext[step:] + fulltext[:step] return fulltext[:width] else: + # No need to scroll the text if it can fit on the screen return text def display(): + ''' + This function will be run as the thread that will manage the LCD screan + itself. It uses the global queue defined at the top of the script. + ''' title = '' artist = '' album = '' @@ -84,7 +147,10 @@ def display(): step_title = step_artist = step_album = 0 while run_event.is_set(): try: - arg = q.get(timeout=0.2) + # If a new MPD even comes before the timeout, grab it, else + # throw an exception that will be caught. + arg = q.get(timeout=step_length) + # From here, we assume a new MPD event has occured (song or status change). status = arg['status']['state'] if status == "play": @@ -97,6 +163,7 @@ def display(): backlight.rgb(255, 0, 0) lcd.create_char(0, char_stop) + # Tests if there is some new text to display, including nothing. if status not in {'play','pause'} or song_file != arg['song']['file']: song_file = arg['song']['file'] if 'file' in arg['song'].keys() else '' title = arg['song']['title'] if 'title' in arg['song'].keys() else '' @@ -120,6 +187,8 @@ def display(): lcd.write(album) except queue.Empty: + # Is this code is executed, it means that no new MPD event + # has occured, so an exception were thrown after the timeout. if len(title) > 15: if step_title == 0 and delay_title > 0: delay_title -= 1 @@ -142,6 +211,7 @@ def display(): if delay_album == 0: delay_album = rotate_pause + # Redraw everything. if len(title) > 15: lcd.set_cursor_position(1, 0) lcd.write(stringrotate(title, 15, step_title)) @@ -153,7 +223,14 @@ def display(): lcd.write(stringrotate(album, 16, step_album)) lcd.clear() -@nav.on(nav.BUTTON) + +# These functions are event handlers related to the joystick. +# Each time the button is used, the matching function will be +# executed. That said, because the python-mpd2 library isn't +# thread-safe, we need to open a client for each event. +# This is ugly as fuck. + +@ nav.on(nav.BUTTON) def button_center(pin): client = MPDClient() # Please close your eyes client.connect("localhost", 6600) # Dirty dirty dirty... @@ -178,7 +255,11 @@ def button_right(pin): client.close() # Okay, you can open your eyes def on_exit(sig, func=None): + ''' + Basically, close everything before quitting. + ''' run_event.clear() + # Join the threads t_display.join() t_vumeter.join() backlight.rgb(0, 0, 0) @@ -187,11 +268,11 @@ def on_exit(sig, func=None): sys.exit(0) lcd.set_contrast(50) -run_event = threading.Event() run_event.set() client = MPDClient() client.connect("localhost", 6600) +# Launch the threads t_display = threading.Thread(name='display',target=display) t_vumeter = threading.Thread(name='vumeter',target=vumeter) t_display.start()