77 lines
2.7 KiB
Python
77 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""爆音资产排查(R6):扫描 6 游戏全部 .ogg。
|
||
|
||
检测项:
|
||
1. 峰值 > -1dBFS(有损编码过冲/归一不足)
|
||
2. 首尾 5ms 内电平突变(无淡入淡出 → 爆豆声)。loop 类(bgm/ 与 sfx/amb_*)按
|
||
首尾交叉淡化工艺制造,边界连续是特性,只做 DC/峰值检查,不报爆豆、不修。
|
||
3. 直流偏置(|DC| > 0.001)
|
||
输出 JSON 行到 stdout 供汇总;--fix 模式见 fix_clicks.py 逻辑(本文件只扫)。
|
||
"""
|
||
import os
|
||
import sys
|
||
|
||
import numpy as np
|
||
import soundfile as sf
|
||
|
||
ROOT = "/home/xsl/SoundGame/web"
|
||
GAMES = ["storyline", "escape", "simon", "rhythm", "space", "rpg"]
|
||
EDGE_MS = 5
|
||
DC_THRESH = 0.001
|
||
PEAK_THRESH = 10 ** (-1.0 / 20) # -1dBFS
|
||
|
||
|
||
def is_loop(relpath):
|
||
parts = relpath.split(os.sep)
|
||
return "/bgm/" in "/" + relpath or os.path.basename(relpath).startswith("amb_")
|
||
|
||
|
||
def scan(path):
|
||
y, sr = sf.read(path, always_2d=True)
|
||
mono = y.mean(axis=1)
|
||
peak = float(np.max(np.abs(mono)))
|
||
dc = float(np.mean(mono))
|
||
n5 = max(1, int(sr * EDGE_MS / 1000))
|
||
n1 = max(1, int(sr * 1 / 1000))
|
||
n50 = max(1, int(sr * 50 / 1000))
|
||
head = float(np.max(np.abs(mono[:n5])))
|
||
tail = float(np.max(np.abs(mono[-n5:])))
|
||
# 爆豆判定(突变而非渐变):首/末 1ms 内的最大幅值 与 相邻 50ms 窗口峰值之比。
|
||
# 正常淡入(8ms cos²)首 1ms 幅值 < 局部峰值 15%;硬边/步进则趋近 1。
|
||
loc_h = float(np.max(np.abs(mono[:n50])))
|
||
loc_t = float(np.max(np.abs(mono[-n50:])))
|
||
FLOOR = 0.01 # 局部窗口低于 -40dB 视为静音区,不判爆豆(编解码底噪残余)
|
||
step_head = loc_h > FLOOR and float(np.max(np.abs(mono[:n1]))) > 0.15 * loc_h
|
||
step_tail = loc_t > FLOOR and float(np.max(np.abs(mono[-n1:]))) > 0.15 * loc_t
|
||
return {
|
||
"path": os.path.relpath(path, ROOT),
|
||
"loop": is_loop(os.path.relpath(path, ROOT)),
|
||
"peak_db": round(20 * np.log10(max(peak, 1e-9)), 2),
|
||
"dc": round(dc, 5),
|
||
"head5ms_db": round(20 * np.log10(max(head, 1e-9)), 2),
|
||
"tail5ms_db": round(20 * np.log10(max(tail, 1e-9)), 2),
|
||
"hot_peak": peak > PEAK_THRESH,
|
||
"dc_bad": abs(dc) > DC_THRESH,
|
||
"click_head": step_head,
|
||
"click_tail": step_tail,
|
||
}
|
||
|
||
|
||
def main():
|
||
results = []
|
||
for g in GAMES:
|
||
base = os.path.join(ROOT, g, "audio")
|
||
for dirpath, _d, files in os.walk(base):
|
||
for f in sorted(files):
|
||
if f.endswith(".ogg"):
|
||
results.append(scan(os.path.join(dirpath, f)))
|
||
import json
|
||
for r in results:
|
||
print(json.dumps(r, ensure_ascii=False))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|