Spaces:
Running
Running
| """Score DNS5 noise files for music-likeness vs broadband-noise-likeness. | |
| Music has structured pitch + rhythmic onset patterns that differentiate it | |
| from HVAC / fan / wind / room-tone noise. We don't need a perfect classifier | |
| — just a ranker that surfaces music-free candidates for a demo clip. | |
| Features (computed on the first 10 s): | |
| - spectral_flatness : 1.0 = white noise; ~0.3 = pitched. Higher = noise-like. | |
| - harmonicity : ratio of largest non-trivial autocorrelation peak to | |
| mean. High = pitched (music or speech). Lower = noise. | |
| - chroma_entropy : entropy of pitch-class distribution. High = energy | |
| spread across all 12 classes (= noise). Low = a few | |
| notes (= music). | |
| - onset_periodicity : autocorrelation peak of the onset-strength envelope at | |
| beat-rate lags. High = music with steady beat. | |
| - music_score : weighted combo (low = clean noise, high = music). | |
| Usage (inside Docker): | |
| LOCALVQE_CONTAINER=localvqe-audit ./train/scripts/docker-run.sh \\ | |
| python /workspace/localvqe/space/_audit_noise.py | |
| """ | |
| from pathlib import Path | |
| import numpy as np | |
| import soundfile as sf | |
| NOISE = Path("/workspace/localvqe/datasets_fullband/noise") | |
| SR = 16000 | |
| CLIP = 10 * SR | |
| # Top stationary-broadband candidates from the earlier ranking | |
| # (high spectral flatness + low spectral flux). These are the files we'd | |
| # pick from for the demo; just need to filter out the music-ones. | |
| CANDIDATES = [ | |
| "p1BmrTosB9w.flac", "ilVoxqe_z7E.flac", "iPGbDshbeuI.flac", | |
| "jyrMW_21Ocs.flac", "G3DdnBL0waA.flac", "G3fcLfe2HWs.flac", | |
| "EXM-wgH46OI.flac", "_dReWTQ4WqE.flac", "OuPtpakIs2k.flac", | |
| "BTSQlC4wVWg.flac", "r4XYK1WQKFo.flac", "_gjBxWy7v4Y.flac", | |
| "iqbiV-ttlA8.flac", | |
| # Throw in the bad pick for calibration | |
| "4Kud1014Cmg.flac", | |
| ] | |
| def load_clip(path): | |
| wav, sr = sf.read(str(path), dtype="float32") | |
| if wav.ndim > 1: | |
| wav = wav.mean(axis=1) | |
| if sr != SR: | |
| # crude resample — fine for feature extraction | |
| from scipy.signal import resample_poly | |
| from math import gcd | |
| g = gcd(sr, SR) | |
| wav = resample_poly(wav, SR // g, sr // g).astype(np.float32) | |
| n = min(CLIP, len(wav)) | |
| return wav[:n] | |
| def spectral_flatness(wav, n_fft=1024, hop=512): | |
| n_frames = (len(wav) - n_fft) // hop + 1 | |
| flat = [] | |
| for i in range(n_frames): | |
| frame = wav[i * hop:i * hop + n_fft] * np.hanning(n_fft) | |
| spec = np.abs(np.fft.rfft(frame)) | |
| spec = spec + 1e-12 | |
| gm = np.exp(np.mean(np.log(spec))) | |
| am = np.mean(spec) | |
| flat.append(gm / am) | |
| return float(np.mean(flat)) | |
| def harmonicity(wav, n_fft=2048, hop=1024, min_lag=40, max_lag=400): | |
| """Ratio of strongest non-trivial autocorrelation peak to mean.""" | |
| n_frames = (len(wav) - n_fft) // hop + 1 | |
| ratios = [] | |
| for i in range(n_frames): | |
| frame = wav[i * hop:i * hop + n_fft] | |
| if np.sqrt(np.mean(frame ** 2)) < 1e-4: | |
| continue | |
| # Auto-corr via FFT | |
| x = frame - frame.mean() | |
| ac = np.correlate(x, x, mode="full")[len(x) - 1:] | |
| ac = ac / (ac[0] + 1e-12) | |
| seg = ac[min_lag:max_lag] | |
| peak = float(seg.max()) if len(seg) > 0 else 0.0 | |
| ratios.append(peak) | |
| return float(np.mean(ratios)) if ratios else 0.0 | |
| def chroma_entropy(wav, n_fft=2048, hop=1024): | |
| """Entropy of the chroma vector averaged over time. High = noise (flat | |
| pitch-class energy); low = music (energy in a few classes).""" | |
| n_frames = (len(wav) - n_fft) // hop + 1 | |
| if n_frames <= 0: | |
| return float("nan") | |
| chroma_sum = np.zeros(12, dtype=np.float64) | |
| for i in range(n_frames): | |
| frame = wav[i * hop:i * hop + n_fft] * np.hanning(n_fft) | |
| spec = np.abs(np.fft.rfft(frame)) | |
| # Map FFT bins to MIDI pitch -> chroma | |
| bins = np.arange(1, len(spec)) | |
| freqs = bins * SR / n_fft | |
| midi = 69 + 12 * np.log2(freqs / 440.0 + 1e-12) | |
| valid = (midi >= 24) & (midi <= 108) | |
| midi = midi[valid].astype(int) | |
| spec = spec[1:][valid] | |
| for cls in range(12): | |
| chroma_sum[cls] += spec[midi % 12 == cls].sum() | |
| p = chroma_sum / (chroma_sum.sum() + 1e-12) | |
| p = p[p > 0] | |
| return float(-np.sum(p * np.log(p)) / np.log(12)) # normalised to [0, 1] | |
| def onset_periodicity(wav, n_fft=1024, hop=256): | |
| """Autocorrelation peak of the onset-strength envelope at beat-rate lags | |
| (60-180 BPM band, lags 22-66 frames at this hop=256 / sr=16000).""" | |
| # Frame energy | |
| n_frames = (len(wav) - n_fft) // hop + 1 | |
| E = np.zeros(n_frames, dtype=np.float32) | |
| for i in range(n_frames): | |
| E[i] = np.sqrt(np.mean(wav[i * hop:i * hop + n_fft] ** 2)) | |
| # Onset envelope: rectified frame-to-frame energy difference | |
| onset = np.maximum(np.diff(E), 0) | |
| if onset.std() < 1e-6: | |
| return 0.0 | |
| onset = (onset - onset.mean()) / (onset.std() + 1e-9) | |
| ac = np.correlate(onset, onset, mode="full")[len(onset) - 1:] | |
| ac = ac / (ac[0] + 1e-12) | |
| # Lags ~22-66 frames at hop=256, sr=16000 => 0.35-1.05s period => ~57-170 BPM | |
| band = ac[22:66] | |
| return float(band.max()) if len(band) > 0 else 0.0 | |
| def main(): | |
| rows = [] | |
| for name in CANDIDATES: | |
| p = NOISE / name | |
| if not p.exists(): | |
| continue | |
| try: | |
| wav = load_clip(p) | |
| if len(wav) < SR: | |
| continue | |
| flat = spectral_flatness(wav) | |
| harm = harmonicity(wav) | |
| ce = chroma_entropy(wav) | |
| op = onset_periodicity(wav) | |
| # Music score: high harmonicity + low chroma entropy + high onset | |
| # periodicity all push up. Low spectral flatness (more pitched) | |
| # also pushes up. Hand-tuned weights (no fitted classifier; this | |
| # is a ranker, not a calibrated probability). | |
| music_score = (1.0 - flat) + harm + (1.0 - ce) + op | |
| rows.append((name, flat, harm, ce, op, music_score)) | |
| except Exception as e: | |
| print(f" {name}: error {e}") | |
| rows.sort(key=lambda r: r[5]) # ascending music_score = noise-first | |
| print(f"{'file':<24} {'flat':>5} {'harm':>5} {'ce':>5} {'beat':>5} {'music_score':>11}") | |
| for r in rows: | |
| print(f"{r[0]:<24} {r[1]:>5.2f} {r[2]:>5.2f} {r[3]:>5.2f} {r[4]:>5.2f} {r[5]:>11.2f}") | |
| if __name__ == "__main__": | |
| main() | |