- 新增 web/ 纯音频 HTML5 平台:共享引擎(手势/WebAudio 三层/设置/震动)、 Node 零依赖服务器(静态 + 注册登录 + 进度存档 API) - 游戏:剑与声(RPG 新作)、声命线、暗室(重设计)、音序回响(重设计)、 节拍行者(跟随领奏重构)、盲航(同向闪避重构);声东击西删除 - 全局功能:男声/女声双套 TTS 资产、三档语速、BGM 音量三档、 震动反馈语言(10 种震型)、服务器进度同步 - 资产:899 ogg + 899 m4a(Qwen3-TTS 双性别语音、Stable Audio 3 音效、 Mureka BGM 4+3 首),终检零缺失 - 文档:docs/design/ 13 篇设计审查文档、docs/audio/ 资产管线、 web/server/README.md API 文档
46 lines
1.4 KiB
Python
46 lines
1.4 KiB
Python
#!/usr/bin/env python3
|
|
"""实测音频 BPM 并找出首个强拍起点(librosa)。
|
|
|
|
用法: .venv-tts/bin/python tools/bpm_check.py <audio> <target_bpm>
|
|
输出 JSON: {"measured_bpm": ..., "octave_ok": bool, "first_beat_sec": ..., "n_beats": ...}
|
|
octave_ok: 实测值与目标偏差 <=2%(含半速/倍速的节拍八度等价)。
|
|
"""
|
|
import json
|
|
import sys
|
|
|
|
import librosa
|
|
import numpy as np
|
|
|
|
path, target = sys.argv[1], float(sys.argv[2])
|
|
y, sr = librosa.load(path, sr=22050, mono=True)
|
|
onset = librosa.onset.onset_strength(y=y, sr=sr)
|
|
tempo, beats = librosa.beat.beat_track(y=y, sr=sr, onset_envelope=onset, start_bpm=target)
|
|
tempo = float(np.atleast_1d(tempo)[0])
|
|
|
|
def dev(a, b):
|
|
return abs(a - b) / b
|
|
|
|
ok = any(dev(tempo, target * k) <= 0.02 for k in (0.5, 1.0, 2.0))
|
|
|
|
beat_times = librosa.frames_to_time(beats, sr=sr)
|
|
first_beat = 0.0
|
|
if len(beat_times):
|
|
# 第一个 onset 强度足够高的拍(跳过 intro 弱起)
|
|
thresh = 0.4 * float(np.max(onset))
|
|
for bf, bt in zip(beats, beat_times):
|
|
if bf < len(onset) and onset[bf] >= thresh:
|
|
first_beat = float(bt)
|
|
break
|
|
else:
|
|
first_beat = float(beat_times[0])
|
|
|
|
print(json.dumps({
|
|
"file": path,
|
|
"measured_bpm": round(tempo, 2),
|
|
"target": target,
|
|
"deviation_pct": round(dev(tempo, target) * 100, 2),
|
|
"octave_ok": ok,
|
|
"first_beat_sec": round(first_beat, 3),
|
|
"n_beats": int(len(beats)),
|
|
}))
|