from dataclasses import dataclass import librosa import numpy as np from .config import SR @dataclass class AcousticFeatures: duration_s: float rms_mean: float rms_std: float pitch_mean: float pitch_std: float zcr_mean: float spectral_flatness_mean: float clipping_ratio: float snr_estimate_db: float silence_ratio: float longest_silence_s: float def load_audio(path: str) -> np.ndarray: y, _ = librosa.load(path, sr=SR, mono=True) return y def extract(y: np.ndarray) -> AcousticFeatures: duration_s = len(y) / SR rms = librosa.feature.rms(y=y)[0] rms_mean, rms_std = float(np.mean(rms)), float(np.std(rms)) f0, voiced_flag, _ = librosa.pyin( y, fmin=librosa.note_to_hz("C2"), fmax=librosa.note_to_hz("C7"), sr=SR ) voiced_f0 = f0[voiced_flag] if voiced_flag is not None else np.array([]) pitch_mean = float(np.nanmean(voiced_f0)) if voiced_f0.size else 0.0 pitch_std = float(np.nanstd(voiced_f0)) if voiced_f0.size else 0.0 zcr_mean = float(np.mean(librosa.feature.zero_crossing_rate(y)[0])) flatness_mean = float(np.mean(librosa.feature.spectral_flatness(y=y)[0])) clipping_ratio = float(np.mean(np.abs(y) > 0.99)) frame_rms = rms sorted_rms = np.sort(frame_rms) noise_floor = np.mean(sorted_rms[: max(1, len(sorted_rms) // 5)]) + 1e-8 signal_level = np.mean(sorted_rms[-max(1, len(sorted_rms) // 10) :]) + 1e-8 snr_estimate_db = float(20 * np.log10(signal_level / noise_floor)) hop_length = 512 silence_mask = frame_rms < (0.1 * (np.max(frame_rms) + 1e-8)) silence_ratio = float(np.mean(silence_mask)) longest_silence_s = _longest_run_seconds(silence_mask, hop_length) return AcousticFeatures( duration_s=duration_s, rms_mean=rms_mean, rms_std=rms_std, pitch_mean=pitch_mean, pitch_std=pitch_std, zcr_mean=zcr_mean, spectral_flatness_mean=flatness_mean, clipping_ratio=clipping_ratio, snr_estimate_db=snr_estimate_db, silence_ratio=silence_ratio, longest_silence_s=longest_silence_s, ) def _longest_run_seconds(mask: np.ndarray, hop_length: int) -> float: longest = current = 0 for v in mask: current = current + 1 if v else 0 longest = max(longest, current) return float(longest * hop_length / SR)