No Description
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.

osci-pi.py 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. #!/usr/bin/env python3
  2. # Oscilloscope Music Player for Raspberry Pi
  3. # https://oscilloscopemusic.com
  4. #
  5. # Tested with a Pi Zero 2 W:
  6. # https://www.raspberrypi.com/products/raspberry-pi-zero-2-w/
  7. #
  8. # Connected to a HiFiBerry DAC+ Zero:
  9. # https://www.hifiberry.com/shop/boards/hifiberry-dac-zero/
  10. #
  11. # Tested with Raspberry Pi OS (Legacy, 32bit) Lite:
  12. # 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
  13. #
  14. # Add "dtoverlay=hifiberry-dac" to "/boot/config.txt"
  15. #
  16. # Install ffmpeg for the ffplay dependency
  17. #
  18. # Powered by PiSugar 2:
  19. # https://github.com/PiSugar/pisugar-power-manager-rs
  20. # https://github.com/PiSugar/pisugar-server-py
  21. #
  22. # sudo sh -c 'echo "dtoverlay=hifiberry-dac" >> /boot/config.txt'
  23. # sudo apt-get update
  24. # sudo apt-get install python3 python3-pip python3-pil libjpeg-dev zlib1g-dev libfreetype6-dev liblcms2-dev libopenjp2-7 libtiff5 ffmpeg
  25. # curl http://cdn.pisugar.com/release/pisugar-power-manager.sh | sudo bash
  26. # pip install pisugar
  27. # pip install luma.oled
  28. # pip install psutil
  29. # sudo usermod -a -G spi,gpio,i2c $USER
  30. #
  31. # IP address code taken from:
  32. # https://github.com/rm-hull/luma.examples/blob/master/examples/sys_info_extended.py
  33. #
  34. # ----------------------------------------------------------------------------
  35. # Copyright (c) 2024 Thomas Buck (thomas@xythobuz.de)
  36. #
  37. # This program is free software: you can redistribute it and/or modify
  38. # it under the terms of the GNU General Public License as published by
  39. # the Free Software Foundation, either version 3 of the License, or
  40. # (at your option) any later version.
  41. #
  42. # This program is distributed in the hope that it will be useful,
  43. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  44. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  45. # GNU General Public License for more details.
  46. #
  47. # See <http://www.gnu.org/licenses/>.
  48. # ----------------------------------------------------------------------------
  49. import sys
  50. import os
  51. import random
  52. import subprocess
  53. import signal
  54. import glob
  55. import socket
  56. from collections import OrderedDict
  57. import time
  58. import pisugar
  59. from luma.core.interface.serial import i2c
  60. from luma.core.render import canvas
  61. from luma.oled.device import ssd1306
  62. from luma.core.error import DeviceNotFoundError
  63. import psutil
  64. import RPi.GPIO as GPIO
  65. from PIL import ImageFont
  66. basevol = "70"
  67. debouncems = 200
  68. #fontfile = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
  69. fontfile = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
  70. fontsize = 11
  71. LCD_REFRESH = 5.0
  72. BTN_ARTIST = 16
  73. BTN_NEXT = 26
  74. currentplaying = None
  75. lcd = None
  76. font = None
  77. bat = None
  78. songlist = None
  79. currentfile = None
  80. lasttime = None
  81. basedir = sys.argv[1]
  82. if basedir.endswith("/"):
  83. basedir = basedir.removesuffix("/")
  84. def get_artist(fn):
  85. parts = fn.replace(basedir + "/", "").split(os.sep)
  86. artist = parts[0].replace("_", " ")
  87. return artist
  88. #originalsongs = os.listdir(basedir)
  89. originalsongs = []
  90. artists = []
  91. for fn in glob.iglob(os.path.join(basedir, '**', '*.wav'), recursive=True):
  92. originalsongs.append(fn)
  93. artist = get_artist(fn)
  94. if artist not in artists:
  95. artists.append(artist)
  96. random.shuffle(artists)
  97. currentartist = artists[0]
  98. def find_single_ipv4_address(addrs):
  99. for addr in addrs:
  100. if addr.family == socket.AddressFamily.AF_INET: # IPv4
  101. return addr.address
  102. def get_ipv4_address(interface_name=None):
  103. if_addrs = psutil.net_if_addrs()
  104. if isinstance(interface_name, str) and interface_name in if_addrs:
  105. addrs = if_addrs.get(interface_name)
  106. address = find_single_ipv4_address(addrs)
  107. return address if isinstance(address, str) else ""
  108. else:
  109. if_stats = psutil.net_if_stats()
  110. if_stats_filtered = {key: if_stats[key] for key, stat in if_stats.items() if "loopback" not in stat.flags}
  111. if_names_sorted = [stat[0] for stat in sorted(if_stats_filtered.items(), key=lambda x: (x[1].isup, x[1].duplex), reverse=True)]
  112. if_addrs_sorted = OrderedDict((key, if_addrs[key]) for key in if_names_sorted if key in if_addrs)
  113. for _, addrs in if_addrs_sorted.items():
  114. address = find_single_ipv4_address(addrs)
  115. if isinstance(address, str):
  116. return address
  117. return ""
  118. def status(filename):
  119. try:
  120. with canvas(lcd) as draw:
  121. f = filename.replace(".wav", "")
  122. f = f.replace(basedir + "/", "")
  123. f = f.replace("/", "\n")
  124. f = f.replace("_", " ")
  125. f += "\n\n"
  126. f += "Bat: {:.0f}% {:.2f}V {:.2f}A".format(bat.get_battery_level(), bat.get_battery_voltage(), bat.get_battery_current())
  127. ip = get_ipv4_address()
  128. if len(ip) > 0:
  129. f += "\n"
  130. f += "IP: %s" % (ip)
  131. with open("/proc/asound/card0/pcm0p/sub0/hw_params", "r") as rf:
  132. for line in rf:
  133. if line.startswith("rate:"):
  134. rate = int(line.split(" ")[1])
  135. f += "\n"
  136. f += "Rate: {:.0f}kHz".format(rate / 1000)
  137. draw.multiline_text((0, 0), f, font=font, fill="white", spacing=-1)
  138. except Exception as e:
  139. raise e
  140. def stop():
  141. global currentplaying
  142. if running():
  143. try:
  144. print("Stopping running player")
  145. os.kill(currentplaying.pid, signal.SIGINT)
  146. if not currentplaying.poll():
  147. currentplaying = None
  148. else:
  149. print("Error stopping player")
  150. except ProcessLookupError as e:
  151. currentplaying = None
  152. else:
  153. currentplaying = None
  154. def play(filename):
  155. global currentplaying
  156. global lcd
  157. global basedir
  158. global bat
  159. global currentfile
  160. stop()
  161. print('Now playing "' + filename + '"')
  162. currentfile = filename
  163. status(currentfile)
  164. currentplaying = subprocess.Popen(["ffplay", "-hide_banner", "-nostats", "-nodisp", "-autoexit", "-volume", basevol, filename])
  165. def running():
  166. global currentplaying
  167. if currentplaying != None:
  168. if currentplaying.poll() == None:
  169. return True
  170. return False
  171. def playlist():
  172. global songlist
  173. global lasttime
  174. if not running():
  175. while True:
  176. if (songlist == None) or (len(songlist) <= 0):
  177. songlist = originalsongs.copy()
  178. random.shuffle(songlist)
  179. song = songlist.pop()
  180. artist = get_artist(song)
  181. if artist == currentartist:
  182. play(song)
  183. lasttime = time.time()
  184. break
  185. else:
  186. if (time.time() - lasttime) >= LCD_REFRESH:
  187. status(currentfile)
  188. lasttime = time.time()
  189. def switch_artist():
  190. global artists
  191. global currentartist
  192. ca = currentartist
  193. while currentartist == ca:
  194. random.shuffle(artists)
  195. currentartist = artists[0]
  196. switch_track()
  197. def switch_track():
  198. stop()
  199. def button(ch):
  200. val = not GPIO.input(ch)
  201. #name = "Unknown"
  202. #if ch == BTN_ARTIST:
  203. # name = "BTN_ARTIST"
  204. #elif ch == BTN_NEXT:
  205. # name = "BTN_NEXT"
  206. #print(name + " is now " + str(val))
  207. if val:
  208. if ch == BTN_ARTIST:
  209. switch_artist()
  210. elif ch == BTN_NEXT:
  211. switch_track()
  212. def main():
  213. global lcd
  214. global font
  215. global bat
  216. global currentfile
  217. if len(sys.argv) <= 1:
  218. print("Usage:")
  219. print("\t" + sys.argv[0] + " PATH")
  220. sys.exit(1)
  221. os.system("killall ffplay")
  222. GPIO.setmode(GPIO.BCM)
  223. for b in [ BTN_ARTIST, BTN_NEXT ]:
  224. GPIO.setup(b, GPIO.IN, pull_up_down=GPIO.PUD_UP)
  225. GPIO.add_event_detect(b, GPIO.BOTH, callback=button, bouncetime=debouncems)
  226. try:
  227. bus = i2c(port=1, address=0x3C)
  228. lcd = ssd1306(bus)
  229. font = ImageFont.truetype(fontfile, fontsize)
  230. except DeviceNotFoundError as E:
  231. print("No LCD connected")
  232. lcd = None
  233. conn, event_conn = pisugar.connect_tcp()
  234. bat = pisugar.PiSugarServer(conn, event_conn)
  235. print(bat.get_model() + " " + bat.get_version())
  236. print(str(bat.get_battery_level()) + "% " + str(bat.get_battery_voltage()) + "V " + str(bat.get_battery_current()) + "A")
  237. print("Plug=" + str(bat.get_battery_power_plugged()) + " Charge=" + str(bat.get_battery_charging()))
  238. try:
  239. while True:
  240. playlist()
  241. time.sleep(0.05)
  242. except KeyboardInterrupt:
  243. print("Bye")
  244. GPIO.cleanup()
  245. sys.exit(0)
  246. if __name__ == "__main__":
  247. main()