Files

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"