File size: 2,143 Bytes
d2f59ca
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""8 kHz log-mel frontend for LS-EEND (``logmel23_cummn``).

Mirrors ``FS-EEND/LS-EEND/datasets/feature.py`` exactly: 23 mel bins, cumulative
mean normalization, +-7 frame splicing, then 10x subsampling. One output frame
therefore covers 0.1 s of audio.
"""
from __future__ import annotations

import numpy as np

SAMPLE_RATE = 8000
N_MELS = 23
CONTEXT = 7
SUBSAMPLING = 10
FRAME_SHIFT = 80
WIN_LENGTH = 200
N_FFT = 1024
FEATURE_DIM = (2 * CONTEXT + 1) * N_MELS
FRAME_SEC = FRAME_SHIFT * SUBSAMPLING / SAMPLE_RATE  # 0.1 s


def load_audio(wav_path):
    """Read a wav as mono float32 at 8 kHz. Returns (audio, duration_seconds)."""
    import soundfile as sf

    audio, sr = sf.read(str(wav_path), dtype='float32', always_2d=False)
    if audio.ndim > 1:
        audio = audio[:, 0]
    duration = len(audio) / sr
    if sr != SAMPLE_RATE:
        import librosa
        audio = librosa.resample(audio, orig_sr=sr, target_sr=SAMPLE_RATE)
    return audio, duration


def extract_features(audio):
    """audio (float32, 8 kHz) -> features (T, 345) float32."""
    import librosa

    spec = librosa.stft(audio, n_fft=N_FFT, win_length=WIN_LENGTH, hop_length=FRAME_SHIFT).T
    if len(audio) % FRAME_SHIFT == 0:
        spec = spec[:-1]
    mag = np.abs(spec)
    mel_fb = librosa.filters.mel(sr=SAMPLE_RATE, n_fft=2 * (mag.shape[1] - 1), n_mels=N_MELS)
    logmel = np.log10(np.maximum(np.dot(mag ** 2, mel_fb.T), 1e-10))

    # Cumulative mean normalization: frame t is normalized by the mean of frames 0..t.
    cum = np.cumsum(logmel, axis=0)
    idx = np.arange(1, logmel.shape[0] + 1, dtype=np.float32)
    logmel = logmel - cum / idx[:, None]

    padded = np.pad(logmel, ((CONTEXT, CONTEXT), (0, 0)), mode='constant')
    n = logmel.shape[0]
    spliced = np.lib.stride_tricks.as_strided(
        padded, (n, FEATURE_DIM), (padded.itemsize * N_MELS, padded.itemsize)
    ).copy()
    return spliced[::SUBSAMPLING].astype(np.float32)


def wav_to_features(wav_path):
    """Convenience wrapper: wav path -> (features (T,345), duration_seconds)."""
    audio, duration = load_audio(wav_path)
    return extract_features(audio), duration