You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

294 lines
8.5 KiB

#!/usr/bin/env python3
# Oscilloscope Music Player for Raspberry Pi
# https://oscilloscopemusic.com
#
# Tested with a Pi Zero 2 W:
# https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/
#
# Connected to a HiFiBerry DAC+ Zero:
# https://www.hifiberry.com/shop/boards/hifiberry-dac-zero/
#
# Tested with Raspberry Pi OS (Legacy, 32bit) Lite:
# https://downloads.raspberrypi.com/raspios_oldstable_lite_armhf/images/raspios_oldstable_lite_armhf-2023-12-06/2023-12-05-raspios-bullseye-armhf-lite.img.xz
#
# Add "dtoverlay=hifiberry-dac" to "/boot/config.txt"
#
# Install ffmpeg for the ffplay dependency
#
# Powered by PiSugar 2:
# https://github.com/PiSugar/pisugar-power-manager-rs
# https://github.com/PiSugar/pisugar-server-py
#
# sudo sh -c 'echo "dtoverlay=hifiberry-dac" >> /boot/config.txt'
# sudo apt-get update
# sudo apt-get install python3 python3-pip python3-pil libjpeg-dev zlib1g-dev libfreetype6-dev liblcms2-dev libopenjp2-7 libtiff5 ffmpeg
# curl http://cdn.pisugar.com/release/pisugar-power-manager.sh | sudo bash
# pip install pisugar
# pip install luma.oled
# pip install psutil
# sudo usermod -a -G spi,gpio,i2c $USER
#
# IP address code taken from:
# https://github.com/rm-hull/luma.examples/blob/master/examples/sys_info_extended.py
#
# ----------------------------------------------------------------------------
# Copyright (c) 2024 Thomas Buck (thomas@xythobuz.de)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# See <http://www.gnu.org/licenses/>.
# ----------------------------------------------------------------------------
import sys
import os
import random
import subprocess
import signal
import glob
import socket
from collections import OrderedDict
import time
import pisugar
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
from luma.core.error import DeviceNotFoundError
import psutil
import RPi.GPIO as GPIO
from PIL import ImageFont
basevol = "70"
debouncems = 200
#fontfile = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
fontfile = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
fontsize = 11
LCD_REFRESH = 5.0
BTN_ARTIST = 16
BTN_NEXT = 26
currentplaying = None
lcd = None
font = None
bat = None
songlist = None
currentfile = None
lasttime = None
basedir = sys.argv[1]
if basedir.endswith("/"):
basedir = basedir.removesuffix("/")
def get_artist(fn):
parts = fn.replace(basedir + "/", "").split(os.sep)
artist = parts[0].replace("_", " ")
return artist
#originalsongs = os.listdir(basedir)
originalsongs = []
artists = []
for fn in glob.iglob(os.path.join(basedir, '**', '*.wav'), recursive=True):
originalsongs.append(fn)
artist = get_artist(fn)
if artist not in artists:
artists.append(artist)
random.shuffle(artists)
currentartist = artists[0]
def find_single_ipv4_address(addrs):
for addr in addrs:
if addr.family == socket.AddressFamily.AF_INET: # IPv4
return addr.address
def get_ipv4_address(interface_name=None):
if_addrs = psutil.net_if_addrs()
if isinstance(interface_name, str) and interface_name in if_addrs:
addrs = if_addrs.get(interface_name)
address = find_single_ipv4_address(addrs)
return address if isinstance(address, str) else ""
else:
if_stats = psutil.net_if_stats()
if_stats_filtered = {key: if_stats[key] for key, stat in if_stats.items() if "loopback" not in stat.flags}
if_names_sorted = [stat[0] for stat in sorted(if_stats_filtered.items(), key=lambda x: (x[1].isup, x[1].duplex), reverse=True)]
if_addrs_sorted = OrderedDict((key, if_addrs[key]) for key in if_names_sorted if key in if_addrs)
for _, addrs in if_addrs_sorted.items():
address = find_single_ipv4_address(addrs)
if isinstance(address, str):
return address
return ""
def status(filename):
try:
with canvas(lcd) as draw:
f = filename.replace(".wav", "")
f = f.replace(basedir + "/", "")
f = f.replace("/", "\n")
f = f.replace("_", " ")
f += "\n\n"
f += "Bat: {:.0f}% {:.2f}V {:.2f}A".format(bat.get_battery_level(), bat.get_battery_voltage(), bat.get_battery_current())
ip = get_ipv4_address()
if len(ip) > 0:
f += "\n"
f += "IP: %s" % (ip)
with open("/proc/asound/card0/pcm0p/sub0/hw_params", "r") as rf:
for line in rf:
if line.startswith("rate:"):
rate = int(line.split(" ")[1])
f += "\n"
f += "Rate: {:.0f}kHz".format(rate / 1000)
draw.multiline_text((0, 0), f, font=font, fill="white", spacing=-1)
except Exception as e:
raise e
def stop():
global currentplaying
if running():
try:
print("Stopping running player")
os.kill(currentplaying.pid, signal.SIGINT)
if not currentplaying.poll():
currentplaying = None
else:
print("Error stopping player")
except ProcessLookupError as e:
currentplaying = None
else:
currentplaying = None
def play(filename):
global currentplaying
global lcd
global basedir
global bat
global currentfile
stop()
print('Now playing "' + filename + '"')
currentfile = filename
status(currentfile)
currentplaying = subprocess.Popen(["ffplay", "-hide_banner", "-nostats", "-nodisp", "-autoexit", "-volume", basevol, filename])
def running():
global currentplaying
if currentplaying != None:
if currentplaying.poll() == None:
return True
return False
def playlist():
global songlist
global lasttime
if not running():
while True:
if (songlist == None) or (len(songlist) <= 0):
songlist = originalsongs.copy()
random.shuffle(songlist)
song = songlist.pop()
artist = get_artist(song)
if artist == currentartist:
play(song)
lasttime = time.time()
break
else:
if (time.time() - lasttime) >= LCD_REFRESH:
status(currentfile)
lasttime = time.time()
def switch_artist():
global artists
global currentartist
ca = currentartist
while currentartist == ca:
random.shuffle(artists)
currentartist = artists[0]
switch_track()
def switch_track():
stop()
def button(ch):
val = not GPIO.input(ch)
#name = "Unknown"
#if ch == BTN_ARTIST:
# name = "BTN_ARTIST"
#elif ch == BTN_NEXT:
# name = "BTN_NEXT"
#print(name + " is now " + str(val))
if val:
if ch == BTN_ARTIST:
switch_artist()
elif ch == BTN_NEXT:
switch_track()
def main():
global lcd
global font
global bat
global currentfile
if len(sys.argv) <= 1:
print("Usage:")
print("\t" + sys.argv[0] + " PATH")
sys.exit(1)
os.system("killall ffplay")
GPIO.setmode(GPIO.BCM)
for b in [ BTN_ARTIST, BTN_NEXT ]:
GPIO.setup(b, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.add_event_detect(b, GPIO.BOTH, callback=button, bouncetime=debouncems)
try:
bus = i2c(port=1, address=0x3C)
lcd = ssd1306(bus)
font = ImageFont.truetype(fontfile, fontsize)
except DeviceNotFoundError as E:
print("No LCD connected")
lcd = None
conn, event_conn = pisugar.connect_tcp()
bat = pisugar.PiSugarServer(conn, event_conn)
print(bat.get_model() + " " + bat.get_version())
print(str(bat.get_battery_level()) + "% " + str(bat.get_battery_voltage()) + "V " + str(bat.get_battery_current()) + "A")
print("Plug=" + str(bat.get_battery_power_plugged()) + " Charge=" + str(bat.get_battery_charging()))
try:
while True:
playlist()
time.sleep(0.05)
except KeyboardInterrupt:
print("Bye")
GPIO.cleanup()
sys.exit(0)
if __name__ == "__main__":
main()