Spaces:
Running
Running
File size: 6,432 Bytes
2461738 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | """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()
|