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