audio_analyzer / issue_detection.py
Mr7Explorer's picture
Update issue_detection.py
344a57b verified
import numpy as np
import scipy.signal as sps
def detect_audio_issues(spectral, time_stats):
"""Detect audio processing artifacts using forensic rules."""
issues = []
energy = spectral["energy_distribution"]
freqs = spectral["freqs"]
hf_env = spectral.get("hf_env", None)
lf_env = spectral.get("lf_env", None)
flatness = spectral.get("spectral_flatness", None)
notches = spectral.get("spectral_notches", [])
# ============================================================
# 1️⃣ HF LOSS LOGIC
# ============================================================
hf_8_12 = energy["8k_12khz"]
highest_freq = spectral["highest_freq_minus60db"]
if hf_8_12 < 0.01 and highest_freq < 9000:
issues.append((
"HF_LOSS", "HIGH",
f"Severe HF cutoff: {hf_8_12:.3f}% in 8–12k and rolloff at {highest_freq:.1f} Hz."
))
elif hf_8_12 < 0.02:
issues.append((
"HF_LOSS", "LOW",
f"Low HF energy ({hf_8_12:.3f}%). Normal for speech."
))
# ============================================================
# 2️⃣ LPF DETECTOR
# ============================================================
if hf_env is not None:
hf_region = (freqs >= 5000) & (freqs <= 12000)
hf_vals = hf_env[hf_region]
hf_freq = freqs[hf_region]
if len(hf_vals) > 10:
coef = np.polyfit(hf_freq, hf_vals, 1)
slope_per_hz = coef[0]
slope_db_oct = slope_per_hz * np.log2(2) * 12000
if highest_freq < 10000:
issues.append((
"LPF_DETECTED", "HIGH",
f"Low-pass filter near {highest_freq:.0f} Hz."
))
elif slope_db_oct < -6:
issues.append((
"HF_EQ_SHELF", "LOW",
f"HF rolloff detected (~{slope_db_oct:.1f} dB/oct)."
))
# ============================================================
# 3️⃣ HPF DETECTOR
# ============================================================
if lf_env is not None:
low_region = (freqs >= 20) & (freqs <= 300)
min_len = min(len(low_region), len(lf_env))
low_region = low_region[:min_len]
lf_env_trim = lf_env[:min_len]
freqs_trim = freqs[:min_len]
lf_vals = lf_env_trim[low_region]
lf_freq = freqs_trim[low_region]
if len(lf_vals) > 10:
coef_l = np.polyfit(lf_freq, lf_vals, 1)
slope_l = coef_l[0]
slope_db_oct_l = slope_l * np.log2(2) * 300
if energy["below_100hz"] < 0.5:
if slope_db_oct_l > 6:
issues.append((
"HPF_DETECTED", "HIGH",
f"High-pass filter detected (~{slope_db_oct_l:.1f} dB/oct)."
))
else:
issues.append((
"HPF_SUSPECTED", "LOW",
"Possible mild HPF (LF rolloff)."
))
# ============================================================
# 4️⃣ NOISE REDUCTION DETECTOR
# ============================================================
if flatness is not None:
hf_flat = flatness
if hf_flat > 0.40 and len(notches) >= 3:
issues.append((
"NOISE_REDUCTION_ARTIFACTS", "HIGH",
f"NR artifacts: HF flattening ({hf_flat:.2f}) + {len(notches)} notches."
))
elif hf_flat > 0.35:
issues.append((
"NR_SOFT", "LOW",
f"Mild noise reduction detected (HF flattening={hf_flat:.2f})."
))
# ============================================================
# 5️⃣ SPECTRAL NOTCHES
# ============================================================
if len(notches) > 0:
issues.append((
"SPECTRAL_NOTCHES", "MEDIUM",
f"{len(notches)} spectral notches detected."
))
# ============================================================
# 6️⃣ BRICK-WALL DETECTOR
# ============================================================
if spectral["brick_wall_detected"]:
issues.append((
"BRICK_WALL", "HIGH",
f"Brick-wall behavior at {spectral['brick_wall_freq']:.0f} Hz."
))
# ============================================================
# 7️⃣ COMPRESSION / DYNAMICS
# ============================================================
crest = time_stats["crest_factor_db"]
if crest < 3:
issues.append((
"OVER_COMPRESSION", "HIGH",
f"Very low crest factor ({crest:.1f} dB)."
))
elif crest < 6:
issues.append((
"COMPRESSION", "MEDIUM",
f"Moderate compression ({crest:.1f} dB)."
))
# ============================================================
# 8️⃣ CLIPPING
# ============================================================
if time_stats["peak"] >= 0.999:
issues.append((
"CLIPPING", "CRITICAL",
f"Peak amplitude {time_stats['peak']:.6f}. Possible clipping."
))
# ============================================================
# 9️⃣ DE-ESSER DETECTION
# ============================================================
if hf_env is not None:
band_3_6k = (freqs >= 3000) & (freqs <= 6000)
band_6_10k = (freqs >= 6000) & (freqs <= 10000)
presence_energy = np.mean(hf_env[band_3_6k])
sibilance_energy = np.mean(hf_env[band_6_10k])
if sibilance_energy < (presence_energy * 0.20):
issues.append((
"DE_ESSER_DETECTED", "MEDIUM",
"Sibilance band (6–10 kHz) strongly reduced vs presence band (3–6 kHz)."
))
# ============================================================
# 🔟 MULTIBAND COMPRESSION
# ============================================================
if hf_env is not None:
def band_crest(env, band):
vals = env[band]
if len(vals) == 0:
return None
return np.max(vals) - np.mean(vals)
lf_band = (freqs >= 80) & (freqs <= 300)
mf_band = (freqs >= 300) & (freqs <= 3000)
hf_band = (freqs >= 3000) & (freqs <= 8000)
cf_lf = band_crest(hf_env, lf_band)
cf_mf = band_crest(hf_env, mf_band)
cf_hf = band_crest(hf_env, hf_band)
if cf_lf is not None and cf_mf is not None and cf_hf is not None:
if cf_hf < (cf_lf * 0.4):
issues.append((
"MULTIBAND_COMPRESSION", "MEDIUM",
"HF crest factor significantly lower than LF."
))
if cf_mf < (cf_lf * 0.5):
issues.append((
"MULTIBAND_COMPRESSION", "LOW",
"Mid-band crest factor compressed vs LF."
))
# ============================================================
# 1️⃣1️⃣ EQ CURVE CLASSIFIER
# ============================================================
if hf_env is not None:
smooth = sps.medfilt(hf_env, kernel_size=9)
coef_eq = np.polyfit(freqs, smooth, 1)
tilt = coef_eq[0]
curvature = np.polyfit(freqs, smooth, 2)[0]
if tilt > 0.00002:
issues.append((
"EQ_HF_BOOST", "LOW",
"HF shelf boost detected (positive tilt)."
))
elif tilt < -0.00002:
issues.append((
"EQ_HF_CUT", "LOW",
"HF shelf cut detected (negative tilt)."
))
if curvature > 1e-12:
issues.append((
"EQ_PEAKING", "LOW",
"Spectral curvature suggests midrange peaking EQ."
))
if abs(tilt) > 0.00001 and abs(curvature) < 1e-12:
issues.append((
"EQ_TILT", "LOW",
"Tilt EQ detected (linear spectral tilt)."
))
return issues