commit
363f0f864c
2 changed files with 286 additions and 0 deletions
-
273osci-pi.py
-
13osci.service
@ -0,0 +1,273 @@ |
|||||
|
#!/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 |
||||
|
|
||||
|
basevol = "70" |
||||
|
|
||||
|
LCD_REFRESH = 5.0 |
||||
|
|
||||
|
BTN_ARTIST = 16 |
||||
|
BTN_NEXT = 26 |
||||
|
|
||||
|
currentplaying = None |
||||
|
lcd = 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" |
||||
|
f += "Batt: {:.0f}% {:.2f}V {:.2f}A".format(bat.get_battery_level(), bat.get_battery_voltage(), bat.get_battery_current()) |
||||
|
|
||||
|
f += "\n" |
||||
|
f += "IP: %s" % (get_ipv4_address()) |
||||
|
|
||||
|
draw.multiline_text((0, 0), f, fill="white") |
||||
|
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 bat |
||||
|
global currentfile |
||||
|
|
||||
|
if len(sys.argv) <= 1: |
||||
|
print("Usage:") |
||||
|
print("\t" + sys.argv[0] + " PATH") |
||||
|
sys.exit(1) |
||||
|
|
||||
|
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=100) |
||||
|
|
||||
|
try: |
||||
|
bus = i2c(port=1, address=0x3C) |
||||
|
lcd = ssd1306(bus) |
||||
|
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() |
@ -0,0 +1,13 @@ |
|||||
|
[Unit] |
||||
|
Description=Oscilloscope Music Player |
||||
|
After=multi-user.target |
||||
|
|
||||
|
[Service] |
||||
|
Type=idle |
||||
|
User=thomas |
||||
|
ExecStart=/home/thomas/osci-pi.py /home/thomas/music |
||||
|
Restart=always |
||||
|
RestartSec=5 |
||||
|
|
||||
|
[Install] |
||||
|
WantedBy=multi-user.target |
Write
Preview
Loading…
Cancel
Save
Reference in new issue