Files
collaplex-audio/dsp/状态.py
T

129 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""读 Collaplex 响度归一化的共享状态(诊断 / web 面板数据源)
共享区布局(见 loudness_norm.py 顶部注释):
每声道 32 字节, 两声道:
+0 f32 e_smooth 平滑能量(响度判据) —— 归一化的"输入电平"
+4 f32 e_block 本块能量(快响应)
+8 f32 peak 本块峰值
+12 f32 gain_db 本块实际施加的增益 dB -> 推子
+16 f64 t 时间戳(monotonic)
+24 u32 n 已处理块数(判活)
+64 f32 target_db 目标响度
+68 u32 rate 采样率
用法:
python3 read_state.py 打印一次
python3 read_state.py --watch 每 0.5 秒刷新(诊断动态平衡过程)
"""
import json
import math
import mmap
import os
import struct
import sys
import time
SHM_PATH = "/dev/shm/collaplex-loudness"
CH_STRIDE = 32
SHM_SIZE = 80
TARGET_OFF = 64
RATE_OFF = 68
STALE_S = 1.0
EPS = 1e-12
def db(x: float) -> float:
"""线性能量/幅度 -> dBFS(0 以下)。"""
return 10.0 * math.log10(x + EPS)
def read_slot(mm: mmap.mmap, i: int) -> dict[str, float]:
base = i * CH_STRIDE
e_smooth = struct.unpack_from("<f", mm, base)[0]
e_block = struct.unpack_from("<f", mm, base + 4)[0]
peak = struct.unpack_from("<f", mm, base + 8)[0]
gain = struct.unpack_from("<f", mm, base + 12)[0]
t = struct.unpack_from("<d", mm, base + 16)[0]
n = struct.unpack_from("<I", mm, base + 24)[0]
age = time.monotonic() - t
return {
"e_smooth": e_smooth,
"e_block": e_block,
"peak": peak,
"gain_db": gain,
"n": n,
"age": age,
"alive": age < STALE_S,
}
def snapshot() -> dict[str, object]:
if not os.path.exists(SHM_PATH):
return {"ok": False, "err": "共享区不存在(响度归一化未启用)"}
fd = os.open(SHM_PATH, os.O_RDONLY)
try:
mm = mmap.mmap(fd, SHM_SIZE, prot=mmap.PROT_READ)
try:
target = struct.unpack_from("<f", mm, TARGET_OFF)[0]
rate = struct.unpack_from("<I", mm, RATE_OFF)[0]
ch = [read_slot(mm, 0), read_slot(mm, 1)]
finally:
mm.close()
finally:
os.close(fd)
out: dict[str, object] = {
"ok": bool(ch[0]["alive"] or ch[1]["alive"]),
"target_db": round(target, 2),
"rate": rate,
}
for i, name in ((0, "L"), (1, "R")):
c = ch[i]
# 原始输入电平 = 快响应块能量; 处理后 = 原始 + 实际增益
raw_db = db(c["e_block"])
raw_pk = 20.0 * math.log10(c["peak"] + EPS) if c["peak"] > 0 else -120.0
out["raw_" + name] = round(raw_db, 2) if c["alive"] else None
out["raw_peak_" + name] = round(raw_pk, 2) if c["alive"] else None
out["post_" + name] = round(raw_db + c["gain_db"], 2) if c["alive"] else None
out["gain_db_" + name] = round(c["gain_db"], 2) if c["alive"] else None
# 平滑响度(归一化真正依据的那条)
out["loud_" + name] = round(db(c["e_smooth"]), 2) if c["alive"] else None
if ch[0]["alive"] and ch[1]["alive"]:
joint = ch[0]["e_smooth"] + ch[1]["e_smooth"]
out["loud_joint"] = round(db(joint), 2)
out["deviation_db"] = round(target - db(joint), 2) # >0 = 还要提, <0 = 还要压
return out
def main() -> None:
watch = "--watch" in sys.argv
try:
if not watch:
print(json.dumps(snapshot(), ensure_ascii=False, indent=2))
return
print("每 0.5 秒刷新 (Ctrl-C 退出)")
print("%8s %9s %9s %9s %9s %8s" % (
"时间", "原始L", "原始R", "处理后L", "处理后R", "增益dB"))
t0 = time.time()
while time.time() - t0 < 3600:
s = snapshot()
if not s.get("ok"):
print(" (无数据: %s)" % s.get("err", "声道未在跑"))
else:
fmt = lambda k: ("%9.2f" % s[k]) if s.get(k) is not None else "%9s" % "-"
_g = s.get("gain_db_L")
gv = float(_g) if isinstance(_g, (int, float)) else 0.0
print("%8.1f %s %s %s %s %8.2f" % (
time.time() - t0, fmt("raw_L"), fmt("raw_R"),
fmt("post_L"), fmt("post_R"), gv))
sys.stdout.flush()
time.sleep(0.5)
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()