Musikspieler mit Display und Bedienelementen
Verfasst: Mittwoch 19. Juni 2019, 22:29
Hallo,
vor Jahren habt ihr mir in diesem Forum schon einmal sehr geholfen („Events auslösen und darauf reagieren“). So habe ich es immerhin zu zwei funktionierenden Geräten geschafft, das letzte namens orpheus sieht so aus:

Nun habe ich nach Jahren endlich beschlossen weiterzumachen.
Es gibt folgende Bedienelemente (von links nach rechts)
Auf dem System laufen neben mpd zur Musikwiedergabe auch mpdlcd und lcdproc/LCDd zum Ansteuern des Displays, wobei ich das eigentlich gern direkt in meinem Programm machen würde. Momentan gebe ich in meinem Programm lediglich als Client von lcdproc/LCDd Rückmeldungen über die Tastendrücke aus.
In erster Linie ist mein Programm ein mpd-Client, der entsprechend der Tastendrücke und Drehimpulsgeber mpd bedient und es sieht aktuell so aus
(Dass die Taster "switch_mute" und "switch_restart" momentan das Netzwerk (de)aktivieren liegt an einem kleinen Problem, das ich mit dem WLAN habe.)
Mir geht es vor allem um
vor Jahren habt ihr mir in diesem Forum schon einmal sehr geholfen („Events auslösen und darauf reagieren“). So habe ich es immerhin zu zwei funktionierenden Geräten geschafft, das letzte namens orpheus sieht so aus:
Nun habe ich nach Jahren endlich beschlossen weiterzumachen.
Es gibt folgende Bedienelemente (von links nach rechts)
- Taster zum Teil mit LEDs
- Ein-/Aus ("switch_power") mit ringförmiger LED (lässt sich ebenfalls steuern kommt bis jetzt aber im Programm nicht vor)
- Play-/Pause ("switch_play") mit LED darüber ("led_play")
- Shuffle-Taster ("switch_random") mit LED darüber ("led_random")
- Internetradio ("switch_radio") mit LED darüber ("led_x")
- Playlist löschen ("switch_clear")
- Display
- Drehimpulsgeber
- voriges/nächstes Stück ("encoder_control") mit eingebautem Taster ("switch_restart")
- Lautstärke ("encoder_volume") mit eingebautem Taster ("switch_mute")
Auf dem System laufen neben mpd zur Musikwiedergabe auch mpdlcd und lcdproc/LCDd zum Ansteuern des Displays, wobei ich das eigentlich gern direkt in meinem Programm machen würde. Momentan gebe ich in meinem Programm lediglich als Client von lcdproc/LCDd Rückmeldungen über die Tastendrücke aus.
In erster Linie ist mein Programm ein mpd-Client, der entsprechend der Tastendrücke und Drehimpulsgeber mpd bedient und es sieht aktuell so aus
Code: Alles auswählen
#!/usr/bin/env python
import math
import threading
import time
import random
import mpd
import Queue
import subprocess
import socket
import wiringpi2
#1 und 2 fuer Pull-up und -down sind beim Cubietruck vertauscht
PULLUP=1
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
def lcd_open(host, port):
s.connect(("localhost", 13666))
lcdproc_version = lcd_cmd('hello')
lcd_cmd('client_set name orpheus')
def lcd_close():
lcd_cmd('screen_set ' + 'commando' + ' backlight off')
s.close()
def lcd_cmd(lcdproc_cmd):
s.send(lcdproc_cmd + '\n')
def lcd_text(msg_text):
lcd_cmd('screen_add ' + 'commando')
lcd_cmd('screen_set ' + 'commando' + ' -priority alert -heartbeat off')
lcd_cmd('screen_set ' + 'commando' + ' -timeout 16')
lcd_cmd('widget_add ' + 'commando' + ' anzeige' + ' string')
lcd_cmd('widget_set ' + 'commando' + ' anzeige' + ' 1 1 ' + '"' + msg_text + '"')
wiringpi2.wiringPiSetup()
class LED:
def __init__(self, pin):
self.pin = pin
self.state = 0
wiringpi2.pinMode(self.pin, 1)
wiringpi2.digitalWrite(self.pin, 0)
def getState(self):
return self.state
def setState(self, state):
wiringpi2.digitalWrite(self.pin, state)
self.state = state
class RotaryEncoder(threading.Thread):
def __init__(self, a_pin, b_pin, pin_pud, callback):
threading.Thread.__init__(self)
self.daemon = True
self.a_pin = a_pin
self.b_pin = b_pin
self.pin_pud = pin_pud
self.steps_per_indent = 4
self.callback=callback
wiringpi2.pinMode(self.a_pin, 0)
wiringpi2.pinMode(self.b_pin, 0)
wiringpi2.pullUpDnControl(self.a_pin, self.pin_pud)
wiringpi2.pullUpDnControl(self.b_pin, self.pin_pud)
def run(self):
last_direction = 0
delta_sum = 0
a_state = wiringpi2.digitalRead(self.a_pin)
b_state = wiringpi2.digitalRead(self.b_pin)
last_rotation_sequence = (a_state ^ b_state) | b_state << 1
while True:
a_state = wiringpi2.digitalRead(self.a_pin)
b_state = wiringpi2.digitalRead(self.b_pin)
rotation_sequence = (a_state ^ b_state) | b_state << 1
if rotation_sequence != last_rotation_sequence:
delta = (rotation_sequence - last_rotation_sequence) % 4
if delta == 3:
delta = -1
elif delta == 2:
delta = int(math.copysign(delta, last_direction))
last_direction = delta
last_rotation_sequence = rotation_sequence
delta_sum += delta
delta = delta_sum // self.steps_per_indent
if delta != 0:
self.callback(delta)
delta_sum %= self.steps_per_indent
time.sleep(0.004)
class Switch(threading.Thread):
def __init__(self, pin, pin_pud, debounce, callback):
threading.Thread.__init__(self)
self.daemon = True
self.pin = pin
self.pin_pud = pin_pud
self.debounce = debounce
self.callback = callback
wiringpi2.pinMode(self.pin, 0)
wiringpi2.pullUpDnControl(self.pin, self.pin_pud)
def run(self):
last_state = wiringpi2.digitalRead(self.pin)
while True:
state = wiringpi2.digitalRead(self.pin)
if state != last_state:
self.callback(state - last_state)
time.sleep(self.debounce)
last_state = state
time.sleep(0.08)
localmpd = mpd.MPDClient(use_unicode=True)
localmpd.connect("localhost",6600)
def rotated_volume(event):
if event == -1:
commands.put("quieter")
elif event == 1:
commands.put("louder")
return
def rotated_control(event):
if event == -1:
commands.put("previous")
elif event == 1:
commands.put("next")
return
def pressed_power(event):
if event == 1:
commands.put("shutdown")
def pressed_play(event):
if event == 1:
commands.put("play")
def pressed_random(event):
if event == 1:
commands.put("random")
def pressed_radio(event):
if event == 1:
commands.put("radio")
def pressed_clear(event):
if event == 1:
commands.put("clear")
def pressed_restart(event):
if event == 1:
commands.put("restart")
def pressed_mute(event):
if event == 1:
commands.put("mute")
commands=Queue.Queue()
encoder_volume = RotaryEncoder(10, 8, PULLUP, rotated_volume)
encoder_control = RotaryEncoder(11, 9, PULLUP, rotated_control)
switch_power = Switch(20, PULLUP, 0.1, pressed_power)
switch_play = Switch(17, PULLUP, 0.1, pressed_play)
switch_random = Switch(19, PULLUP, 0.1, pressed_random)
switch_radio = Switch(16, PULLUP, 0.1, pressed_radio)
switch_clear = Switch(18, PULLUP, 0.1, pressed_clear)
switch_restart = Switch(15, PULLUP, 0.1, pressed_restart)
switch_mute = Switch(12, PULLUP, 0.1, pressed_mute)
encoder_volume.start()
encoder_control.start()
switch_power.start()
switch_play.start()
switch_random.start()
switch_radio.start()
switch_clear.start()
switch_restart.start()
switch_mute.start()
led_power = LED(28)
led_play = LED(22)
led_random = LED(21)
led_x = LED(26)
lcd_open("localhost", 13666)
led_power.setState(0)
while True:
try:
command=commands.get(1, 1)
except:
command=99
if command == "play":
if localmpd.status()['state'] == 'play':
localmpd.pause()
lcd_text('pause')
else:
if localmpd.status()['playlistlength'] == '0':
albums = localmpd.list("album")
albumindex=0
lcd_text(albums[albumindex].encode('ascii', 'ignore'))
while True:
navigate=commands.get(1)
if navigate == "next":
albumindex = (albumindex + 1) % len(albums)
elif navigate == "previous":
albumindex = (albumindex - 1) % len(albums)
elif navigate == "louder":
albumindex = (albumindex + 10) % len(albums)
elif navigate == "quieter":
albumindex = (albumindex - 10) % len(albums)
lcd_text(albums[albumindex].encode('ascii', 'ignore'))
if navigate == "random":
localmpd.findadd("album", albums[albumindex])
break
if navigate == "play":
localmpd.clear()
localmpd.findadd("album", albums[albumindex])
break
else:
lcd_text('play')
localmpd.play()
elif command == "next":
localmpd.next()
try:
lcd_text('%3.0f: ' % float(localmpd.status()['song']) + localmpd.currentsong()['name'])
except:
lcd_text('next ' + localmpd.status()['song'] + '/' + localmpd.status()['playlistlength'])
elif command == "previous":
localmpd.previous()
try:
lcd_text('%3.0f: ' % float(localmpd.status()['song']) + localmpd.currentsong()['name'])
except:
lcd_text('previous ' + localmpd.status()['song'] + '/' + localmpd.status()['playlistlength'])
elif command == "random":
random = (1 - int(localmpd.status()['random']))
localmpd.random(random)
if int(localmpd.status()['random']) == 1:
lcd_text('randomize')
else:
lcd_text('random off')
elif command == "quieter":
volume = int(localmpd.status()['volume']) - 1
if volume < 0:
volume = 0
localmpd.setvol(sorted([0, volume, 100 ])[1])
lcd_text('volume %1.2f' % (float(volume)/100))
elif command == "louder":
volume = int(localmpd.status()['volume']) + 1
if volume > 100:
volume = 100
localmpd.setvol(sorted([0, volume, 100 ])[1])
lcd_text('volume %1.2f' % (float(volume)/100))
elif command == "clear":
localmpd.clear()
lcd_text('clear playlist')
elif command == "radio":
localmpd.clear()
localmpd.load('radio')
localmpd.play()
lcd_text('internet radio')
elif command == "restart":
lcd_text('stopping network')
subprocess.call(["systemctl", "stop", "ssh.service"])
subprocess.call(["systemctl", "stop", "systemd-timesyncd.service"])
subprocess.call(["systemctl", "stop", "systemd-resolved.service"])
subprocess.call(["systemctl", "stop", "systemd-networkd.service"])
subprocess.call(["systemctl", "stop", "wpa_supplicant@wlan0.service"])
lcd_text('done')
#lcd_text('restart ' + localmpd.status()['song'] + '/' + localmpd.status()['playlistlength'])
elif command == "mute":
lcd_text('stopping network')
subprocess.call(["systemctl", "start", "wpa_supplicant@wlan0.service"])
subprocess.call(["systemctl", "start", "systemd-resolved.service"])
subprocess.call(["systemctl", "start", "systemd-networkd.service"])
subprocess.call(["systemctl", "start", "systemd-timesyncd.service"])
subprocess.call(["systemctl", "start", "ssh.service"])
lcd_text('done')
#subprocess.call(["amixer", "-D", "hw:U24XL", "set", "PCM", "1+", "toggle"])
#lcd_text('toggle mute')
elif command == "shutdown":
lcd_text('shutting down')
subprocess.call(["systemctl", "poweroff"])
if localmpd.status()['state'] == 'play':
led_play.setState(1)
else:
led_play.setState(0)
led_random.setState(int(localmpd.status()['random']))
command = 99
Mir geht es vor allem um
- allgemeine Hinweise
(ich weiß, dass ich vieles ungünstig/patschert mache oder mich bei Variablennamen blöd anstelle, aber oft fehlt mir auch die Idee wie es besser ginge) - ich versuche gerade eine Möglichkeit zu schaffen am Gerät selbst ein Album (oder auch mehrere Musikstücke nach einem anderen Kriterium) zur Wiedergabe auszuwählen. Momentan steht das alles in der Hauptschleife hinter dem else bei command == "play":
Falls die Wiedergabeliste leer ist kann man mit den Drehimpulsgebern die Alben einzeln oder 10er-Schritten durchgehen und dann mit "switch_random" oder "switch_play" abspielen. Allerdings würde ich da gerne auch andere Tags verwenden und habe nicht die geringste Idee wie ich das anstelle, weil python-mpd offensichtlich nicht direkt das bietet was ich dazu bräuchte. Ich würde beispielsweise gerne die Musik nach
Dirigent → Komponist → Album
oder
Interpret → Album
also recht flexibel auswählen können, aber ich scheitere schon daran zu einer nach Interpret gefilterten Albenliste zu kommen... - Threads/Prozesse
Hier bin ich komplett ahnungslos.
Zum Beispiel habe ich den Eindruck, dass mir das Multithreading nicht viel bringt und die mpd-Befehle die Abfrage der Drehimpulsgeber stören/lahmlegen/unterbrechen (eigentlich nicht sehr störend, aber ich habe das Gefühl, dass das etwas besser gehen müsste).
Schließlich möchte ich wie erwähnt auch das Display direkt aus meinem Programm (ohne lcdproc/LCDd und ohne mpdlcd) ansprechen und dann steh ich komplett an - immerhin will ich gleichzeitig- das Display ansteuern, dass entweder das aktuelle Album mit Titel angezeigt wird oder im "Musikauswahlmodus" eben nacheinander die Interpreten, Komponisten, Alben,...
- die LEDs so ansteuern, dass sie den Wiedergabestatus und den Zufallswiedergabemodus richtig anzeigen. Eine LED sollte oder könnte dann später auch anzeigen ob man sich im "Musikauswahlmodus" befindet.
- die mpd-Befehle abarbeiten so dass sie möglichst nicht den Bedienfluss aufhalten. Ich fände es zum Beispiel beim Regeln der Lautstärke nett, wenn das Display ohne Verzögerung die "Ziel-Lautsärke" anzeigt, man also nicht darauf warten muss, dass die vorigen mpd-Befehle abgearbeitet wurden, aber dazu müsste ich wohl parallel zu mpd im Programm einen eigenen Lautstärkewert speichern, den ich dann irgendwie (in beide Richtungen) mit dem Lautstärkewert von mpd abgleichen muss.
- Nachdem sich mpd auch über das Netzwerk bedienen lässt, wäre es nett, wenn man einige Befehle, die über das Netzwerk gegeben wurden, auch am Display sehen kann, zum Beispiel Lautstärkeänderungen.