Completed player
This commit is contained in:
Vendored
+26
-2
@@ -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"
|
||||
]
|
||||
Vendored
+3
@@ -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
@@ -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"
|
||||
|
||||
+73
-60
@@ -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,60 +53,73 @@ 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):
|
||||
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]}")
|
||||
update_now_playing(playlist_name, playlist[current_index])
|
||||
while True:
|
||||
@client.set_process_callback
|
||||
def process(frames):
|
||||
bufsize = frames
|
||||
end = buffer_pos[0] + bufsize
|
||||
if buffer_pos[0] >= len(samples):
|
||||
current_index += 1
|
||||
if current_index >= len(playlist):
|
||||
playlist = load_playlist(playlist_file)
|
||||
current_index = 0
|
||||
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]
|
||||
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
|
||||
|
||||
print(f"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)
|
||||
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):
|
||||
current_index += 1
|
||||
if current_index >= len(playlist):
|
||||
playlist = load_playlist(playlist_file)
|
||||
current_index = 0
|
||||
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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
Executable
+29
@@ -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)
|
||||
Executable
+38
@@ -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
|
||||
@@ -1,52 +1,67 @@
|
||||
import yaml # type: ignore
|
||||
import yaml # type: ignore
|
||||
import sys
|
||||
import jack # type: ignore
|
||||
import jack # type: ignore
|
||||
import socket
|
||||
import numpy as np # type: ignore
|
||||
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)
|
||||
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))
|
||||
|
||||
if not zone:
|
||||
print(f"Zone '{zone_name} not found in config")
|
||||
sys.exit(1)
|
||||
@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)
|
||||
|
||||
jack_input = zone["jack_input"]
|
||||
ip = zone["ip"]
|
||||
port = zone["port"]
|
||||
with client:
|
||||
print(f"🎧 Streaming {zone_name} to {ip}:{port}")
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print(f"🛑 {zone_name} stopped")
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
if __name__ == "__main__":
|
||||
config = load_config()
|
||||
zones = config.get("zones", {})
|
||||
|
||||
client = jack.Client(jack_input)
|
||||
client.inports.register("in_l")
|
||||
client.inports.register("in_r")
|
||||
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)
|
||||
|
||||
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):
|
||||
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}")
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
except KeyboardInterrupt:
|
||||
print("Stopped")
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
@@ -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 ===")
|
||||
Reference in New Issue
Block a user