123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- #!/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()
|