Initial commit
This commit is contained in:
87
ackit/verify/loopback.py
Normal file
87
ackit/verify/loopback.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""WASAPI loopback capture of the default speaker + detection of a reference sound in it."""
|
||||
import threading
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
RATE = 48000
|
||||
|
||||
|
||||
class LoopbackRecorder(threading.Thread):
|
||||
def __init__(self, seconds, rate=RATE):
|
||||
super().__init__(daemon=True)
|
||||
self.seconds = seconds
|
||||
self.rate = rate
|
||||
self.chunks = []
|
||||
self.stop_flag = False
|
||||
self.error = None
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
import soundcard as sc
|
||||
sp = sc.default_speaker()
|
||||
mic = sc.get_microphone(id=str(sp.name), include_loopback=True)
|
||||
block = self.rate // 10
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore") # "data discontinuity" warnings
|
||||
with mic.recorder(samplerate=self.rate, blocksize=block) as rec:
|
||||
n = int(self.seconds * 10)
|
||||
for _ in range(n):
|
||||
if self.stop_flag:
|
||||
break
|
||||
self.chunks.append(rec.record(numframes=block))
|
||||
except Exception as e: # noqa
|
||||
self.error = repr(e)
|
||||
|
||||
def audio(self):
|
||||
if not self.chunks:
|
||||
return np.zeros(0, dtype=np.float32)
|
||||
a = np.concatenate(self.chunks, axis=0)
|
||||
return a.mean(axis=1).astype(np.float32) if a.ndim == 2 else a.astype(np.float32)
|
||||
|
||||
|
||||
def resample(x, src_rate, dst_rate):
|
||||
if src_rate == dst_rate:
|
||||
return x.astype(np.float32)
|
||||
n = int(round(len(x) * dst_rate / src_rate))
|
||||
t = np.linspace(0, len(x) - 1, n)
|
||||
return np.interp(t, np.arange(len(x)), x).astype(np.float32)
|
||||
|
||||
|
||||
def detect(capture, ref, rate=RATE):
|
||||
"""Max normalized cross-correlation of ref against every same-length window of capture.
|
||||
Returns (score 0..1, time_offset_seconds, ref_rms_in_window)."""
|
||||
if len(capture) < len(ref) or len(ref) == 0:
|
||||
return 0.0, None, 0.0
|
||||
ref = ref - ref.mean()
|
||||
rn = np.linalg.norm(ref)
|
||||
if rn == 0:
|
||||
return 0.0, None, 0.0
|
||||
n = 1 << int(np.ceil(np.log2(len(capture) + len(ref))))
|
||||
xc = np.fft.irfft(np.fft.rfft(capture, n) * np.conj(np.fft.rfft(ref, n)), n)[:len(capture) - len(ref) + 1]
|
||||
c2 = np.concatenate([[0.0], np.cumsum(capture.astype(np.float64) ** 2)])
|
||||
win = np.sqrt(np.maximum(c2[len(ref):] - c2[:-len(ref)], 1e-12))
|
||||
ncc = xc / (rn * win)
|
||||
# ignore near-silent windows
|
||||
ncc[win < 1e-3 * np.sqrt(len(ref))] = 0
|
||||
i = int(np.argmax(np.abs(ncc)))
|
||||
return float(abs(ncc[i])), i / rate, float(win[i] / np.sqrt(len(ref)))
|
||||
|
||||
|
||||
def detect_chunks(capture, ref, rate=RATE, chunk_s=1.0, step_s=0.5):
|
||||
"""Best NCC over 1 s chunks of the reference (robust to partial playback and resampler drift).
|
||||
Returns (best score, chunk start in ref [s], position in capture [s])."""
|
||||
c = int(chunk_s * rate)
|
||||
s = int(step_s * rate)
|
||||
best = (0.0, None, None)
|
||||
if len(ref) <= c:
|
||||
sc, t, _ = detect(capture, ref, rate)
|
||||
return sc, 0.0, t
|
||||
for i in range(0, len(ref) - c + 1, s):
|
||||
seg = ref[i:i + c]
|
||||
if np.sqrt(np.mean(seg ** 2)) < 1e-3:
|
||||
continue
|
||||
sc, t, _ = detect(capture, seg, rate)
|
||||
if sc > best[0]:
|
||||
best = (sc, i / rate, t)
|
||||
return best
|
||||
Reference in New Issue
Block a user