commit ab9824a71f03e2e1059ef866f43c67bbe72f0129 Author: media Date: Sun Jun 14 14:16:10 2026 -0500 Initial Commit diff --git a/lufs_mqtt.py b/lufs_mqtt.py new file mode 100644 index 0000000..c79a41b --- /dev/null +++ b/lufs_mqtt.py @@ -0,0 +1,136 @@ +import time +import numpy as np +import sounddevice as sd +import pyloudnorm as pyln +import paho.mqtt.client as mqtt + +# MQTT +MQTT_HOST = "10.40.0.2" +MQTT_PORT = 1883 + +TOPIC_M = "stream/audio/lufs/momentary" +TOPIC_S = "stream/audio/lufs/shortterm" +TOPIC_I = "stream/audio/lufs/integrated" +DEVICE = "default" + +# Audio +RATE = 48000 +CHANNELS = 2 + +def find_scarlett(): + for i, d in enumerate(sd.query_devices()): + name = d["name"].lower() + if "scarlett" in name and d["max_input_channels"] >= 2: + return i + raise RuntimeError("Scarlett stereo input not found") + +# Windows +M_WINDOW = 0.4 +S_WINDOW = 3.0 +UPDATE = 0.25 + +meter = pyln.Meter(RATE) + +integrated_buffer = np.array([], dtype=np.float32) +shortterm_buffer = np.array([], dtype=np.float32) + +RESET_TOPIC = "stream/audio/lufs/reset" + +reset_requested = False + + +def on_message(client, userdata, msg): + global reset_requested + + if msg.topic == RESET_TOPIC: + print("Resetting integrated LUFS") + reset_requested = True + +client = mqtt.Client() + +client.on_message = on_message + +client.connect(MQTT_HOST, MQTT_PORT) + +client.subscribe(RESET_TOPIC) + +client.loop_start() + +print("LUFS → MQTT running") + +while True: + + try: + samples = sd.rec( + int(UPDATE * RATE), + samplerate=RATE, + channels=CHANNELS, + device=DEVICE, + dtype="float32", + ) + sd.wait() + + except Exception as e: + print(f"Audio route changed / device unavailable: {e}") + time.sleep(2) + continue + + mono = samples.flatten() + + if reset_requested: + integrated_buffer = np.array([], dtype=np.float32) + reset_requested = False + + integrated_buffer = np.concatenate( + [integrated_buffer, mono] + )[-RATE * 60 * 120:] + + shortterm_buffer = np.concatenate( + [shortterm_buffer, mono] + )[-int(S_WINDOW * RATE):] + + try: + + m_samples = shortterm_buffer[ + -int(M_WINDOW * RATE): + ] + + momentary = meter.integrated_loudness( + m_samples + ) + + shortterm = meter.integrated_loudness( + shortterm_buffer + ) + + integrated = meter.integrated_loudness( + integrated_buffer + ) + + client.publish( + TOPIC_M, + f"{momentary:.1f}", + retain=True + ) + + client.publish( + TOPIC_S, + f"{shortterm:.1f}", + retain=True + ) + + client.publish( + TOPIC_I, + f"{integrated:.1f}", + retain=True + ) + + print( + f"\rM {momentary:.1f} " + f"S {shortterm:.1f} " + f"I {integrated:.1f}", + end="" + ) + + except Exception: + pass \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..2ee353f --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +# LUFS MQTT Viewer + +A simple script to broadcast a sound sources LUFS over MQTT \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0de94fd --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +cffi==2.0.0 +numpy==2.4.6 +paho-mqtt==2.1.0 +pycparser==3.0 +pyloudnorm==0.2.0 +scipy==1.17.1 +sounddevice==0.5.5