MPD: lot of comments, fix blocking fifo read

This commit is contained in:
raspbeguy 2017-03-16 23:01:20 +01:00
parent 7a4f8aa4af
commit b46d438fb4
1 changed files with 95 additions and 14 deletions

View File

@ -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()