File size: 2,383 Bytes
c3eb7ea
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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)