Completed player

This commit is contained in:
Media
2025-06-30 20:24:59 -05:00
parent 765bf73383
commit 20677f0749
10 changed files with 316 additions and 106 deletions
+26 -2
View File
@@ -1,4 +1,28 @@
[
"CCAAefnX5vo",
"vzGpVce0Puk"
"1MwfBYzvB2A",
"5BdVtbwQnHs",
"5Pju9ITqLaY",
"Hk-9QNAD6V4",
"Ljl4aFILsOw",
"LqzHTDma900",
"M5n_-8IYDBk",
"OOgnJKo13Hs",
"Vyw3I1Kqets",
"ZaIxE5leHug",
"acktYyOGG6w",
"dALKtBPDd_E",
"eMxGu_Zk3OI",
"frGAB2aVHJA",
"iqyxMTAqr5Q",
"kQfdo-ghfMY",
"kjEFOaGnYcU",
"lH4jPkFcj3k",
"nPv3Evsc4jo",
"nxJeXwN0jiM",
"oLixTqKVPfo",
"q8ofnoWFpx4",
"qCcZRavCOyc",
"uIdUC83QOpg",
"yixWRocskp0",
"ykS7VDAiFAc"
]
+3
View File
@@ -2,8 +2,10 @@
"0jft6MfWN6k",
"3fZDS_5Jv5E",
"5NZFgqoWMq0",
"5aHJSSnjlhk",
"BF5pNOZF934",
"RwpTUmzz_oY",
"TlZCx3NTRMA",
"UIzIfEMpFV0",
"WYcpL6otp_Y",
"YwsTqkfOpcg",
@@ -11,6 +13,7 @@
"ds2H1hbm6xM",
"f2oxGYpuLkw",
"hVcUqZ3KryA",
"klPEGFmSyUo",
"qOD9M95_fS0",
"vh70Rbmcg8g",
"yNQp9toTAGo",
+8 -3
View File
@@ -1,5 +1,6 @@
zones:
sanctuary:
active: true
jack_input: "sanctuary"
ip: "10.40.0.100"
port: 5004
@@ -7,6 +8,7 @@ zones:
right: "sanctuary:in_r"
prayer:
active: true
jack_input: "prayer"
ip: "10.40.0.101"
port: 5004
@@ -14,6 +16,7 @@ zones:
right: "prayer:in_r"
office:
active: false
jack_input: "office"
ip: "10.40.0.102"
port: 5004
@@ -21,6 +24,7 @@ zones:
right: "office:in_r"
fellowship:
active: false
jack_input: "fellowship"
ip: "10.40.0.103"
port: 5004
@@ -28,6 +32,7 @@ zones:
right: "fellowship:in_r"
Macbook:
active: true
jack_input: "Mac"
ip: "192.168.2.1"
port: 5004
@@ -35,17 +40,17 @@ zones:
right: "Mac:in_r"
sources:
praise:
Praise:
left: "Praise:left"
right: "Praise:right"
youtube_url: "https://youtube.com/playlist?list=PLJjON0cPmpcvJaax7cNGjYJeSGCPKHxuF&si=cEfJhukvRhrul2oW"
id_cache: "/home/media/Scripts/ZonePlayer/cache/praise_ids.json"
worship:
Worship:
left: "Worship:left"
right: "Worship:right"
youtube_url: "https://youtube.com/playlist?list=PLJjON0cPmpctXc5x8--6lZDKjrprVvTkd&si=0El4d6t2lgtVYS69"
id_cache: "/home/media/Scripts/ZonePlayer/cache/worship_ids.json"
instrumental:
Instrumental:
left: "Instrumental:left"
right: "Instrumental:right"
youtube_url: "https://youtube.com/playlist?list=PLJjON0cPmpcuJwDtp6OczGJADZB4HYrAm&si=vtu-vQnY3wIgDzYB"
+50 -37
View File
@@ -1,13 +1,14 @@
import jack # type: ignore
import jack
import time
import numpy as np # type: ignore
from pydub import AudioSegment # type: ignore
import numpy as np
from pydub import AudioSegment
import random
import sys
import json
import os
import paho.mqtt.client as mqtt # type: ignore
from paho.mqtt.client import Client # type: ignore
import yaml
import paho.mqtt.client as mqtt
import threading
import re
# === load and shuffle .m3u playlist ===
@@ -22,7 +23,7 @@ def decode_mp3(path):
song = AudioSegment.from_file(path)
song = song.set_frame_rate(48000).set_channels(2).set_sample_width(4)
samples = np.array(song.get_array_of_samples()).astype(np.float32) / (2**31)
return samples.reshape((-1,2)) #stereo:L R
return samples.reshape((-1, 2)) # stereo: L R
def update_now_playing(playlist_name, song_title):
path = "/home/media/Music/now_playing.json"
@@ -38,8 +39,7 @@ def update_now_playing(playlist_name, song_title):
# MQTT title broadcast
mqtt_topic = f"music/now_playing/{playlist_name.lower()}"
mqtt_payload = os.path.basename(song_title)
mqtt_payload = os.path.splitext(mqtt_payload)[0]
mqtt_payload = os.path.splitext(os.path.basename(song_title))[0]
mqtt_payload = re.sub(r'\s*\[[^\]]+\]$', '', mqtt_payload)
try:
@@ -53,49 +53,40 @@ def update_now_playing(playlist_name, song_title):
except Exception as e:
print(f"[MQTT] failed to publish: {e}")
# === Get playlist name from CLI ===
if len(sys.argv) < 2:
print ("Usage: python3 jack_mp3_player.py Praise|Worship|Instrumental")
sys.exit(1)
# === YAML config ===
def load_config():
with open("config.yaml") as f:
return yaml.safe_load(f)
playlist_name = sys.argv[1]
playlist_file = f"/home/media/Music/{playlist_name}.m3u"
# === JACK + player logic ===
def run_playlist(playlist_name, playlist_file):
client = jack.Client(playlist_name)
out_l = client.outports.register("left")
out_r = client.outports.register("right")
# === jack setup ===
client = jack.Client(playlist_name)
out_l = client.outports.register("left")
out_r = client.outports.register("right")
playlist = load_playlist(playlist_file)
current_index = 0
samples = decode_mp3(playlist[current_index])
buffer_pos = [0]
playlist = load_playlist(playlist_file)
current_index = 0
samples = decode_mp3(playlist[current_index])
buffer_pos = [0]
@client.set_process_callback
def process(frames):
@client.set_process_callback
def process(frames):
bufsize = frames
end = buffer_pos[0] + bufsize
#zero fills if out of data
if buffer_pos[0] >= len(samples):
out_l.get_buffer()[:] = np.zeros(bufsize, dtype=np.float32)
out_r.get_buffer()[:] = np.zeros(bufsize, dtype=np.float32)
return
chunk = samples[buffer_pos[0]:end]
#pad if last chunk
if len(chunk) < bufsize:
pad = np.zeros((bufsize - len(chunk), 2), dtype=np.float32)
chunk = np.vstack((chunk, pad))
out_l.get_buffer()[:] = chunk[:, 0].copy()
out_r.get_buffer()[:] = chunk[:, 1].copy()
buffer_pos[0] += bufsize
# === Activate jack ===
with client:
print(f"Playing playlist: {playlist_name}")
print(f"Playing song: {playlist[current_index]}")
with client:
print(f"[{playlist_name}] Playing: {playlist[current_index]}")
update_now_playing(playlist_name, playlist[current_index])
while True:
if buffer_pos[0] >= len(samples):
@@ -103,10 +94,32 @@ with client:
if current_index >= len(playlist):
playlist = load_playlist(playlist_file)
current_index = 0
print(f"Playing: {playlist[current_index]}")
print(f"[{playlist_name}] Playing: {playlist[current_index]}")
update_now_playing(playlist_name, playlist[current_index])
samples = decode_mp3(playlist[current_index])
buffer_pos[0] = 0
time.sleep(0.1)
# === Main logic ===
if __name__ == "__main__":
config = load_config()
sources = config.get("sources", {})
if len(sys.argv) > 1:
playlist_name = sys.argv[1]
if playlist_name in sources and sources[playlist_name].get("active", True):
playlist_file = f"/home/media/Music/{playlist_name}.m3u"
run_playlist(playlist_name, playlist_file)
else:
print(f"[!] Source '{playlist_name}' not found or not active.")
else:
threads = []
for playlist_name, source_data in sources.items():
if source_data.get("active", True):
playlist_file = f"/home/media/Music/{playlist_name}.m3u"
t = threading.Thread(target=run_playlist, args=(playlist_name, playlist_file), daemon=True)
t.start()
threads.append(t)
for t in threads:
t.join()
+33 -2
View File
@@ -11,7 +11,7 @@ with open("config.yaml", "r") as f:
zones = config.get("zones", {})
sources = config.get("sources", {})
DEFAULT_SOURCE = "worship"
DEFAULT_SOURCE = "Worship"
print("Zones loaded:", zones.keys())
print("Sources loaded:", sources.keys())
@@ -73,7 +73,38 @@ def on_connect(client, userdata, flags, rc):
client.subscribe(topic)
print(f"subscribed to {topic}")
time.sleep(1)
def get_required_jack_ports():
required = []
# From sources
for source_name, source_data in sources.items():
if source_data.get("active", True):
required.append(source_data["left"])
required.append(source_data["right"])
# From zones
for zone_name, zone_data in zones.items():
if zone_data.get("active", True):
required.append(zone_data["left"])
required.append(zone_data["right"])
return required
def wait_for_required_jack_ports(required_ports, interval=5):
print("[router] Waiting for all required JACK ports to appear...")
while True:
available = set(p.name for p in client_jack.get_ports())
missing = [p for p in required_ports if p not in available]
if not missing:
print("[router] All required JACK ports found. Proceeding.")
return
print(f"[router] Still waiting for: {missing}")
time.sleep(interval)
required_ports = get_required_jack_ports()
wait_for_required_jack_ports(required_ports)
active_zones = wait_for_available_zone_ports()
apply_default_connections(active_zones)
+5
View File
@@ -16,3 +16,8 @@ Saved updated ID cache: /home/media/Scripts/ZonePlayer/cache/instrumental_ids.js
✅ Updated playlist: /home/media/Music/Praise.m3u with 16 songs
✅ Updated playlist: /home/media/Music/Instrumental.m3u with 2 songs
✅ Playlist sync complete.
Traceback (most recent call last):
File "/home/media/Scripts/ZonePlayer/yt_sync.py", line 12, in <module>
with open(CONFIG_FILE, "r") as f:
^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: 'config.yaml'
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env python3
import jack
import time
import sys
import subprocess
expected = {
"Praise:left", "Praise:right",
"Worship:left", "Worship:right",
"Instrumental:left", "Instrumental:right",
"sanctuary:in_l", "sanctuary:in_r",
"prayer:in_l", "prayer:in_r",
"Mac:in_l", "Mac:in_r"
}
client = jack.Client("waiter")
timeout = 15
deadline = time.time() + timeout
while time.time() < deadline:
current = {port.name for port in client.get_ports()}
if expected.issubset(current):
print("All JACK ports found.")
subprocess.run(["systemctl", "--system", "restart", "router.service"])
sys.exit(0)
time.sleep(0.5)
print("Timeout waiting for JACK ports.")
sys.exit(1)
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
REQUIRED_PORTS=(
"sanctuary_out:left"
"sanctuary_out:right"
"prayer_out:left"
"prayer_out:right"
"Macbook_out:left"
"Macbook_out:right"
"Praise:left"
"Praise:right"
"Worship:left"
"Worship:right"
"Instrumental:left"
"Instrumental:right"
)
DEADLINE=$((SECONDS + 10))
while [ $SECONDS -lt $DEADLINE ]; do
ALL_FOUND=true
for PORT in "${REQUIRED_PORTS[@]}"; do
if ! /usr/bin/jack_lsp | grep -q "$PORT"; then
ALL_FOUND=false
break
fi
done
if $ALL_FOUND; then
echo "All required JACK ports found."
exit 0
fi
sleep 0.5
done
echo "Timeout waiting for JACK ports"
exit 1
+43 -28
View File
@@ -4,49 +4,64 @@ import jack # type: ignore
import socket
import numpy as np # type: ignore
import time
import threading
def load_config():
with open("config.yaml", "r") as f:
return yaml.safe_load(f)
if len(sys.argv) < 2:
print("Usage: python3 zone.py <zone_name>")
sys.exit(1)
def run_zone(zone_name: str, zone_config: dict):
jack_input = zone_config["jack_input"]
ip = zone_config["ip"]
port = zone_config["port"]
zone_name = sys.argv[1]
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
with open("config.yaml", "r") as f:
config = yaml.safe_load(f)
client = jack.Client(jack_input)
client.inports.register("in_l")
client.inports.register("in_r")
zone = config["zones"].get(zone_name)
if not zone:
print(f"Zone '{zone_name} not found in config")
sys.exit(1)
jack_input = zone["jack_input"]
ip = zone["ip"]
port = zone["port"]
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client = jack.Client(jack_input)
client.inports.register("in_l")
client.inports.register("in_r")
def send_pcm_frames(frames: np.ndarray):
def send_pcm_frames(frames: np.ndarray):
int16 = (frames * 32767.0).astype(np.int16)
interleaved = np.column_stack(int16).ravel()
sock.sendto(interleaved.tobytes(), (ip, port))
@client.set_process_callback
def process(frames):
@client.set_process_callback
def process(frames):
l = client.inports[0].get_array()
r = client.inports[1].get_array()
data = np.vstack((l, r))
send_pcm_frames(data)
with client:
print(f"Streaming {zone_name} to {ip}:{port}")
with client:
print(f"🎧 Streaming {zone_name} to {ip}:{port}")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Stopped")
print(f"🛑 {zone_name} stopped")
if __name__ == "__main__":
config = load_config()
zones = config.get("zones", {})
if len(sys.argv) > 1:
zone_name = sys.argv[1]
zone = zones.get(zone_name)
if not zone:
print(f"[!] Zone '{zone_name}' not found in config")
sys.exit(1)
if not zone.get("active", True):
print(f"[!] Zone '{zone_name}' is inactive in config")
sys.exit(1)
run_zone(zone_name, zone)
else:
threads = []
for zone_name, zone_config in zones.items():
if zone_config.get("active", True):
t = threading.Thread(target=run_zone, args=(zone_name, zone_config), daemon=True)
t.start()
threads.append(t)
for t in threads:
t.join()
+47
View File
@@ -0,0 +1,47 @@
import subprocess
import time
import os
venv_python = "/home/media/Scripts/ZonePlayer/venv/bin/python3"
script_dir = "/home/media/Scripts/ZonePlayer"
zones = ["sanctuary", "prayer", "office", "fellowship", "Macbook"]
players = ["praise", "worship", "instrumental"]
logfile = "/tmp/zoneplayer_launcher.log"
def log(msg):
with open(logfile, "a") as f:
f.write(msg + "\n")
print(msg)
def start_detached(script, *args):
try:
subprocess.Popen(
[venv_python, os.path.join(script_dir, script), *args],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
log(f"Started: {script} {' '.join(args)}")
except Exception as e:
log(f"ERROR: Failed to start {script} {' '.join(args)} - {e}")
log("=== ZonePlayer Launcher Starting ===")
# Launch zones
for zone in zones:
start_detached("zone.py", zone)
# Launch players
for player in players:
start_detached("jack_mp3_player.py", player)
log("Sleeping 10 seconds to allow JACK ports to appear")
time.sleep(10)
# Start router
start_detached("router.py")
log("Router started")
log("=== ZonePlayer Launcher Done ===")