diff --git a/src/auracast/multicast.py b/src/auracast/multicast.py index 71fa5fe..d9045a9 100644 --- a/src/auracast/multicast.py +++ b/src/auracast/multicast.py @@ -638,6 +638,12 @@ class Streamer(): except Exception: pass + def get_audio_levels(self) -> list[float]: + """Return current RMS audio levels (0.0-1.0) for each BIG.""" + if not self.bigs: + return [] + return [big.get('_audio_level_rms', 0.0) for big in self.bigs.values()] + async def stream(self): bigs = self.bigs @@ -867,6 +873,11 @@ class Streamer(): np.clip(pcm_arr, -32768, 32767, out=pcm_arr) pcm_frame = pcm_arr.astype(np.int16).tobytes() + # Compute RMS audio level (normalized 0.0-1.0) for level monitoring + pcm_samples = np.frombuffer(pcm_frame, dtype=np.int16).astype(np.float32) + rms = np.sqrt(np.mean(pcm_samples ** 2)) / 32768.0 if len(pcm_samples) > 0 else 0.0 + big['_audio_level_rms'] = float(rms) + # Measure LC3 encoding time t1 = time.perf_counter() num_bis = big.get('num_bis', 1) diff --git a/src/auracast/multicast_control.py b/src/auracast/multicast_control.py index 226210b..c34f3a5 100644 --- a/src/auracast/multicast_control.py +++ b/src/auracast/multicast_control.py @@ -37,6 +37,12 @@ class Multicaster: 'is_initialized': self.is_auracast_init, 'is_streaming': streaming, } + + def get_audio_levels(self) -> list[float]: + """Return current RMS audio levels (0.0-1.0) for each BIG.""" + if self.streamer is not None and self.streamer.is_streaming: + return self.streamer.get_audio_levels() + return [] async def init_broadcast(self): self.device_acm = multicast.create_device(self.global_conf) diff --git a/src/auracast/server/multicast_frontend.py b/src/auracast/server/multicast_frontend.py index 819671c..7b4971d 100644 --- a/src/auracast/server/multicast_frontend.py +++ b/src/auracast/server/multicast_frontend.py @@ -1,6 +1,7 @@ # frontend/app.py import os import time +import math import logging as log from PIL import Image @@ -227,6 +228,56 @@ if audio_mode == "Analog": help="Digital gain boost applied before encoding (0-20 dB). Use this when the line-level signal is too quiet even at max ADC gain. Higher values may cause clipping on loud signals." ) +# Audio level monitor (checkbox, not persisted across reloads) +show_level_monitor = st.checkbox("Audio level monitor", value=False, disabled=not is_streaming, + help="Show real-time audio level meters for active radios. Only works while streaming.") + +if show_level_monitor and is_streaming: + @st.fragment(run_every=0.2) + def _audio_level_fragment(): + cols = st.columns(2) + # Radio 1 + with cols[0]: + try: + r = requests.get(f"{BACKEND_URL}/audio_level", timeout=0.2) + levels = r.json().get("levels", []) if r.ok else [] + except Exception: + levels = [] + if levels: + rms = max(levels) + db = max(-60.0, 20.0 * (math.log10(rms) if rms > 0 else -3.0)) + pct = int(max(0, min(100, (db + 60) * 100 / 60))) + st.markdown( + f"**Radio 1**" + f'