| 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) |
|
|