File size: 4,092 Bytes
575e16b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
105
"""Deterministic 16 kHz waveform -> log-mel features; standard-library/scipy implementation."""
import numpy as np
from scipy.io import wavfile
from scipy.signal import resample_poly, stft
from scipy.fft import dct

def to_float(y):
    """Scale PCM samples to float32 in [-1,1]; must run before mixing to mono, which drops the integer dtype."""
    y=np.asarray(y)
    if y.dtype==np.uint8: return (y.astype(np.float32)-128.)/128.
    if np.issubdtype(y.dtype,np.integer):
        return y.astype(np.float32)/max(abs(np.iinfo(y.dtype).min),np.iinfo(y.dtype).max)
    return y.astype(np.float32)

def read_audio(path):
    """(sample_rate, mono float32 waveform) from WAV via scipy, or FLAC/OGG/MP3/... via the optional soundfile package."""
    path=str(path)
    if path.lower().endswith(('.wav','.wave')):
        try:
            sr,y=wavfile.read(path); y=to_float(y)
        except ValueError:
            # Compressed or unusual WAV encodings scipy cannot parse; libsndfile often can.
            sr,y=_read_soundfile(path)
    else: sr,y=_read_soundfile(path)
    if y.ndim==2: y=y.mean(axis=1)
    return int(sr),y.astype(np.float32)

def _read_soundfile(path):
    try: import soundfile as sf
    except ImportError:
        raise RuntimeError(f"Reading {path} needs the soundfile package (pip install soundfile), "
                           "or convert it first: ffmpeg -i input.mp3 -ac 1 output.wav") from None
    y,sr=sf.read(path,dtype='float32',always_2d=False)
    return sr,y

SR=16000
DURATION=2.0
N_MELS=64
N_FFT=512
HOP=160

def _hz_to_mel(hz): return 2595.0*np.log10(1.0+np.asarray(hz)/700.0)
def _mel_to_hz(mel): return 700.0*(10.0**(np.asarray(mel)/2595.0)-1.0)
def mel_filterbank(sr=SR, n_fft=N_FFT, n_mels=N_MELS):
    points=_mel_to_hz(np.linspace(_hz_to_mel(0),_hz_to_mel(sr/2),n_mels+2))
    bins=np.floor((n_fft+1)*points/sr).astype(int)
    fb=np.zeros((n_mels,n_fft//2+1),dtype=np.float32)
    for m in range(1,n_mels+1):
        left,center,right=bins[m-1:m+2]
        if center>left:
            fb[m-1,left:center]=np.arange(left,center)-left
            fb[m-1,left:center]/=(center-left)
        if right>center:
            fb[m-1,center:right]=(right-np.arange(center,right))/(right-center)
    return fb

_FB=mel_filterbank()
def resample(y, sr):
    y=np.asarray(y,dtype=np.float32).reshape(-1)
    if sr != SR:
        import math
        g=math.gcd(int(sr),SR)
        y=resample_poly(y,SR//g,int(sr)//g).astype(np.float32)
    return y

def crop_offsets(n_samples, k):
    """k evenly spaced 2 s crop starts covering a 16 kHz waveform; k=1 gives the center crop."""
    n=int(SR*DURATION)
    if n_samples<=n or k<=1: return [None]
    return [int(round(o)) for o in np.linspace(0,n_samples-n,k)]

def prepare_waveform(y, sr, offset=None):
    y=resample(y,sr)
    n=int(SR*DURATION)
    if len(y)>n:
        # Center crop by default: deterministic inference and evaluation.
        start=(len(y)-n)//2 if offset is None else min(max(0,int(offset)),len(y)-n)
        y=y[start:start+n]
    elif len(y)<n:
        y=np.pad(y,(0,n-len(y)))
    peak=float(np.max(np.abs(y))) if len(y) else 0.
    if peak>1e-8: y=y/peak
    return y

def logmel(y, sr=SR, offset=None):
    y=prepare_waveform(y,sr,offset)
    _,_,z=stft(y,fs=SR,nperseg=N_FFT,noverlap=N_FFT-HOP,
               nfft=N_FFT,boundary='zeros',padded=True)
    power=(np.abs(z)**2).astype(np.float32)
    mel=_FB @ power
    feat=np.log(np.maximum(mel,1e-8))
    # Per-record normalization stabilizes recorder gain; retain a consistent transformation.
    feat=(feat-feat.mean())/(feat.std()+1e-5)
    return feat.astype(np.float32)[None,:,:]

def audio_quality(y):
    y=np.asarray(y,dtype=np.float32)
    if y.size==0: return False, ['empty_audio']
    rms=float(np.sqrt(np.mean(y*y)))
    peak=float(np.max(np.abs(y)))
    reasons=[]
    if rms<0.002: reasons.append('low_level_or_silence')
    if peak>0.999: reasons.append('possible_clipping')
    return len(reasons)==0, reasons