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