MPD: lot of comments, fix blocking fifo read
This commit is contained in:
parent
7a4f8aa4af
commit
b46d438fb4
91
mpd_dot3k.py
91
mpd_dot3k.py
|
@ -9,13 +9,27 @@ import threading
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import select
|
||||||
import audioop
|
import audioop
|
||||||
import dot3k.lcd as lcd
|
import dot3k.lcd as lcd
|
||||||
import dot3k.backlight as backlight
|
import dot3k.backlight as backlight
|
||||||
import dot3k.joystick as nav
|
import dot3k.joystick as nav
|
||||||
from mpd import MPDClient
|
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 = [
|
char_play = [
|
||||||
0b10000,
|
0b10000,
|
||||||
|
@ -50,13 +64,46 @@ char_stop = [
|
||||||
0b00000,
|
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()
|
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():
|
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)
|
fifo = os.open('/tmp/mpd.fifo', os.O_RDONLY)
|
||||||
while run_event.is_set():
|
while run_event.is_set():
|
||||||
|
# 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:
|
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)
|
rawStream = os.read(fifo, 4096)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
if err.errno == errno.EAGAIN or err.errno == errno.EWOULDBLOCK:
|
if err.errno == errno.EAGAIN or err.errno == errno.EWOULDBLOCK:
|
||||||
|
@ -64,19 +111,35 @@ def vumeter():
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
if rawStream:
|
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)
|
backlight.set_graph(audioop.rms(rawStream,2)/32768)
|
||||||
|
# After exiting the almost-infinite loop, we turn the LEDs off.
|
||||||
backlight.set_graph(0)
|
backlight.set_graph(0)
|
||||||
|
|
||||||
|
|
||||||
def stringrotate(text, width, step):
|
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:
|
if len(text) > width:
|
||||||
fulltext = text + " - "
|
fulltext = text + " - "
|
||||||
fulltext = fulltext[step:] + fulltext[:step]
|
fulltext = fulltext[step:] + fulltext[:step]
|
||||||
return fulltext[:width]
|
return fulltext[:width]
|
||||||
else:
|
else:
|
||||||
|
# No need to scroll the text if it can fit on the screen
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def display():
|
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 = ''
|
title = ''
|
||||||
artist = ''
|
artist = ''
|
||||||
album = ''
|
album = ''
|
||||||
|
@ -84,7 +147,10 @@ def display():
|
||||||
step_title = step_artist = step_album = 0
|
step_title = step_artist = step_album = 0
|
||||||
while run_event.is_set():
|
while run_event.is_set():
|
||||||
try:
|
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']
|
status = arg['status']['state']
|
||||||
|
|
||||||
if status == "play":
|
if status == "play":
|
||||||
|
@ -97,6 +163,7 @@ def display():
|
||||||
backlight.rgb(255, 0, 0)
|
backlight.rgb(255, 0, 0)
|
||||||
lcd.create_char(0, char_stop)
|
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']:
|
if status not in {'play','pause'} or song_file != arg['song']['file']:
|
||||||
song_file = arg['song']['file'] if 'file' in arg['song'].keys() else ''
|
song_file = arg['song']['file'] if 'file' in arg['song'].keys() else ''
|
||||||
title = arg['song']['title'] if 'title' 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)
|
lcd.write(album)
|
||||||
|
|
||||||
except queue.Empty:
|
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 len(title) > 15:
|
||||||
if step_title == 0 and delay_title > 0:
|
if step_title == 0 and delay_title > 0:
|
||||||
delay_title -= 1
|
delay_title -= 1
|
||||||
|
@ -142,6 +211,7 @@ def display():
|
||||||
if delay_album == 0:
|
if delay_album == 0:
|
||||||
delay_album = rotate_pause
|
delay_album = rotate_pause
|
||||||
|
|
||||||
|
# Redraw everything.
|
||||||
if len(title) > 15:
|
if len(title) > 15:
|
||||||
lcd.set_cursor_position(1, 0)
|
lcd.set_cursor_position(1, 0)
|
||||||
lcd.write(stringrotate(title, 15, step_title))
|
lcd.write(stringrotate(title, 15, step_title))
|
||||||
|
@ -153,7 +223,14 @@ def display():
|
||||||
lcd.write(stringrotate(album, 16, step_album))
|
lcd.write(stringrotate(album, 16, step_album))
|
||||||
lcd.clear()
|
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):
|
def button_center(pin):
|
||||||
client = MPDClient() # Please close your eyes
|
client = MPDClient() # Please close your eyes
|
||||||
client.connect("localhost", 6600) # Dirty dirty dirty...
|
client.connect("localhost", 6600) # Dirty dirty dirty...
|
||||||
|
@ -178,7 +255,11 @@ def button_right(pin):
|
||||||
client.close() # Okay, you can open your eyes
|
client.close() # Okay, you can open your eyes
|
||||||
|
|
||||||
def on_exit(sig, func=None):
|
def on_exit(sig, func=None):
|
||||||
|
'''
|
||||||
|
Basically, close everything before quitting.
|
||||||
|
'''
|
||||||
run_event.clear()
|
run_event.clear()
|
||||||
|
# Join the threads
|
||||||
t_display.join()
|
t_display.join()
|
||||||
t_vumeter.join()
|
t_vumeter.join()
|
||||||
backlight.rgb(0, 0, 0)
|
backlight.rgb(0, 0, 0)
|
||||||
|
@ -187,11 +268,11 @@ def on_exit(sig, func=None):
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
lcd.set_contrast(50)
|
lcd.set_contrast(50)
|
||||||
run_event = threading.Event()
|
|
||||||
run_event.set()
|
run_event.set()
|
||||||
client = MPDClient()
|
client = MPDClient()
|
||||||
client.connect("localhost", 6600)
|
client.connect("localhost", 6600)
|
||||||
|
|
||||||
|
# Launch the threads
|
||||||
t_display = threading.Thread(name='display',target=display)
|
t_display = threading.Thread(name='display',target=display)
|
||||||
t_vumeter = threading.Thread(name='vumeter',target=vumeter)
|
t_vumeter = threading.Thread(name='vumeter',target=vumeter)
|
||||||
t_display.start()
|
t_display.start()
|
||||||
|
|
Loading…
Reference in New Issue