Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
61 lines
2.1 KiB
Python
61 lines
2.1 KiB
Python
"""Audio side of sdr-classify: ffmpeg decode, features, peak envelope and the voice/noise rule (numpy)."""
|
|
import subprocess
|
|
|
|
import numpy as np
|
|
|
|
SR = 8000
|
|
NPEAK = 120
|
|
|
|
|
|
def pcm(path):
|
|
r = subprocess.run(["ffmpeg", "-nostdin", "-v", "quiet", "-i", path, "-f", "s16le", "-ac", "1",
|
|
"-ar", str(SR), "-"], capture_output=True, timeout=120)
|
|
return np.frombuffer(r.stdout, dtype=np.int16).astype(np.float32) / 32768.0
|
|
|
|
|
|
def peaks(x):
|
|
if len(x) == 0:
|
|
return []
|
|
n = min(NPEAK, max(1, len(x) // 80))
|
|
seg = np.array_split(np.abs(x), n)
|
|
p = np.array([s.max() if len(s) else 0 for s in seg])
|
|
m = p.max()
|
|
return [int(v) for v in np.round(np.sqrt(p / m) * 99)] if m > 0 else [0] * n
|
|
|
|
|
|
def features(x):
|
|
dur = len(x) / SR
|
|
fl, hop = 256, 128
|
|
out = dict(dur=round(dur, 2), act=0.0, vf=0.0, pstd=0.0, hm=0.0)
|
|
if len(x):
|
|
# audio level (for diagnostics only: NFM noise is as loud as voice, so level is NOT a voice cue)
|
|
out["peak_db"] = round(float(20 * np.log10(np.max(np.abs(x)) + 1e-9)), 1)
|
|
out["rms_db"] = round(float(10 * np.log10(np.mean(x ** 2) + 1e-12)), 1)
|
|
if len(x) < fl * 4:
|
|
return out
|
|
nf = (len(x) - fl) // hop
|
|
fr = np.lib.stride_tricks.as_strided(x, (nf, fl), (x.strides[0] * hop, x.strides[0]))
|
|
e = np.mean(fr ** 2, axis=1)
|
|
act = e > 1e-6 # squelch-open (non-silent) frames
|
|
out["act"] = round(float(act.sum() * hop / SR), 2)
|
|
if act.sum() < 8:
|
|
return out
|
|
fa = fr[act] * np.hanning(fl)
|
|
S = np.abs(np.fft.rfft(fa, 512, axis=1)) ** 2 + 1e-12
|
|
ac = np.fft.irfft(S, 512, axis=1)[:, :160]
|
|
ac = ac / (ac[:, :1] + 1e-12)
|
|
lo, hi = SR // 400, SR // 80
|
|
h = ac[:, lo:hi].max(1)
|
|
lag = ac[:, lo:hi].argmax(1) + lo
|
|
ef = e[act]
|
|
voiced = (h > 0.45) & (ef > np.percentile(ef, 30))
|
|
out["hm"] = round(float(np.median(h)), 3)
|
|
out["vf"] = round(float(voiced.mean()), 3)
|
|
if voiced.sum() > 3:
|
|
out["pstd"] = round(float(np.std(np.log(SR / lag[voiced]))), 3)
|
|
return out
|
|
|
|
|
|
def classify(f):
|
|
return "voice" if (f["act"] >= 1.0 and f["vf"] >= 0.10 and f["pstd"] >= 0.10) else "noise"
|