File size: 3,875 Bytes
49525ce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
"""
ml/data/augment_min.py - Minimal, real-only audio augmentation
===============================================================
Augmentation is applied ONLY to REAL recordings (gold corpora / user
clips). It never invents a disorder. Purpose: make the model robust to
real-world recording conditions (noise, device bandwidth, loudness,
tempo) — the point is that a speaker is not penalised for *recording
quality*, only for speech content.

Bounded magnitude + small probability so label meaning is never flipped
(a lisp stays a lisp; a stutter stays a stutter).

Transforms:
    add_noise    : low SNR floor (room/mic hiss)
    random_gain  : global loudness scaling
    time_stretch : slight tempo change, pitch-preserving (WSOLA)
    highpass     : remove low rumble / mic thump
"""
from __future__ import annotations
import numpy as np
import soundfile as sf
from pathlib import Path
from typing import Optional

import librosa
from scipy import signal as sp_signal


def add_noise(audio: np.ndarray, snr_db: float = 22.0, rng: Optional[np.random.Generator] = None) -> np.ndarray:
    rng = rng or np.random.default_rng()
    power = np.mean(audio ** 2) + 1e-12
    npow = power / (10 ** (snr_db / 10.0))
    noise = rng.normal(0.0, np.sqrt(npow), audio.shape).astype(np.float32)
    return audio + noise


def random_gain(audio: np.ndarray, factor: float = 1.0) -> np.ndarray:
    return audio * float(factor)


def random_speed(audio: np.ndarray, sr: int, factor: float = 1.0) -> np.ndarray:
    """Tempo change preserving pitch; bound factor ~0.97-1.04."""
    return librosa.effects.time_stretch(audio.astype(np.float64), rate=1.0 / factor).astype(np.float32)


def highpass(audio: np.ndarray, sr: int, cutoff: float = 70.0) -> np.ndarray:
    sos = sp_signal.butter(1, cutoff, "hp", fs=sr, output="sos")
    return sp_signal.sosfilt(sos, audio).astype(np.float32)


def augment(
    audio: np.ndarray,
    sr: int,
    *,
    prob_gain: float = 0.3,
    prob_noise: float = 0.4,
    prob_speed: float = 0.3,
    prob_hp: float = 0.2,
    rng: Optional[np.random.Generator] = None,
) -> np.ndarray:
    """Bounded random subset of real-world transforms. Preserves content."""
    rng = rng or np.random.default_rng()
    x = audio.astype(np.float32).copy()
    if rng.random() < prob_gain:
        x = random_gain(x, rng.uniform(0.7, 1.3))
    if rng.random() < prob_noise:
        x = add_noise(x, rng.uniform(18, 28), rng)
    if rng.random() < prob_speed:
        x = random_speed(x, sr, rng.uniform(0.97, 1.04))
    if rng.random() < prob_hp:
        x = highpass(x, sr)
    return np.clip(x, -0.99, 0.99).astype(np.float32)


def augment_wav(src: Path, dst: Path, sr: int = 16000, seed: Optional[int] = None) -> None:
    """Augment a wav file; deterministic if seed given."""
    y, _ = sf.read(str(src), dtype="float32")
    rng = np.random.default_rng(seed)
    out = augment(y, sr, rng=rng)
    sf.write(str(dst), out, sr, subtype="PCM_16")


if __name__ == "__main__":
    # self-check: run each transform, confirm length/validity preserved
    sr = 16000
    t = np.linspace(0, 1, sr, endpoint=False)
    tone = (0.5 * np.sin(2 * np.pi * 220 * t)).astype(np.float32)
    checks = {
        "noise":        lambda: add_noise(tone, 20, np.random.default_rng(1)),
        "gain":         lambda: random_gain(tone, 1.2),
        "speed_stretch":lambda: random_speed(tone, sr, 1.03),
        "highpass":     lambda: highpass(tone, sr),
        "augment_full": lambda: augment(tone, sr),
    }
    ok = True
    for name, fn in checks.items():
        try:
            out = fn()
            assert len(out) > 0 and np.isfinite(out.all()), f"{name} produced non-finite/empty"
            print(f"{name:15} OK len={len(out)}")
        except Exception as e:
            ok = False
            print(f"{name:15} FAIL {e}")
    raise SystemExit(0 if ok else 1)