Thomas Buck vor 3 Monaten
Commit
363f0f864c
2 geänderte Dateien mit 286 neuen und 0 gelöschten Zeilen
  1. 273
    0
      osci-pi.py
  2. 13
    0
      osci.service

+ 273
- 0
osci-pi.py Datei anzeigen

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

+ 13
- 0
osci.service Datei anzeigen

@@ -0,0 +1,13 @@
1
+[Unit]
2
+Description=Oscilloscope Music Player
3
+After=multi-user.target
4
+
5
+[Service]
6
+Type=idle
7
+User=thomas
8
+ExecStart=/home/thomas/osci-pi.py /home/thomas/music
9
+Restart=always
10
+RestartSec=5
11
+
12
+[Install]
13
+WantedBy=multi-user.target

Laden…
Abbrechen
Speichern