neverlib 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- neverlib/__init__.py +2 -2
- neverlib/audio_aug/__init__.py +1 -1
- neverlib/audio_aug/audio_aug.py +4 -5
- neverlib/dataAnalyze/README.md +234 -0
- neverlib/dataAnalyze/__init__.py +87 -0
- neverlib/dataAnalyze/dataset_analyzer.py +590 -0
- neverlib/dataAnalyze/quality_metrics.py +364 -0
- neverlib/dataAnalyze/rms_distrubution.py +62 -0
- neverlib/dataAnalyze/spectral_analysis.py +218 -0
- neverlib/dataAnalyze/statistics.py +406 -0
- neverlib/dataAnalyze/temporal_features.py +126 -0
- neverlib/dataAnalyze/visualization.py +468 -0
- neverlib/filter/AudoEQ/README.md +165 -0
- neverlib/filter/AudoEQ/auto_eq_de.py +361 -0
- neverlib/filter/AudoEQ/auto_eq_ga_advanced.py +577 -0
- neverlib/filter/AudoEQ/auto_eq_ga_basic.py +380 -0
- neverlib/filter/AudoEQ/auto_eq_spectral_direct.py +75 -0
- neverlib/filter/README.md +101 -0
- neverlib/filter/__init__.py +7 -0
- neverlib/filter/biquad.py +45 -0
- neverlib/filter/common.py +5 -6
- neverlib/filter/core.py +339 -0
- neverlib/metrics/dnsmos.py +160 -0
- neverlib/metrics/snr.py +177 -0
- neverlib/metrics/spec.py +45 -0
- neverlib/metrics/test_pesq.py +35 -0
- neverlib/metrics/time.py +68 -0
- neverlib/tests/test_vad.py +21 -0
- neverlib/utils/audio_split.py +5 -3
- neverlib/utils/message.py +4 -4
- neverlib/utils/utils.py +32 -15
- neverlib/vad/PreProcess.py +1 -1
- neverlib/vad/README.md +10 -10
- neverlib/vad/VAD_Energy.py +1 -1
- neverlib/vad/VAD_Silero.py +1 -1
- neverlib/vad/VAD_WebRTC.py +1 -1
- neverlib/vad/VAD_funasr.py +1 -1
- neverlib/vad/VAD_statistics.py +3 -3
- neverlib/vad/VAD_vadlib.py +2 -2
- neverlib/vad/VAD_whisper.py +1 -1
- neverlib/vad/__init__.py +1 -1
- neverlib/vad/class_get_speech.py +4 -4
- neverlib/vad/class_vad.py +1 -1
- neverlib/vad/utils.py +47 -5
- {neverlib-0.2.1.dist-info → neverlib-0.2.3.dist-info}/METADATA +120 -120
- neverlib-0.2.3.dist-info/RECORD +53 -0
- {neverlib-0.2.1.dist-info → neverlib-0.2.3.dist-info}/WHEEL +1 -1
- neverlib/Documents/vad/VAD_Energy.ipynb +0 -159
- neverlib/Documents/vad/VAD_Silero.ipynb +0 -305
- neverlib/Documents/vad/VAD_WebRTC.ipynb +0 -183
- neverlib/Documents/vad/VAD_funasr.ipynb +0 -179
- neverlib/Documents/vad/VAD_ppasr.ipynb +0 -175
- neverlib/Documents/vad/VAD_statistics.ipynb +0 -522
- neverlib/Documents/vad/VAD_vadlib.ipynb +0 -184
- neverlib/Documents/vad/VAD_whisper.ipynb +0 -430
- neverlib/utils/waveform_analyzer.py +0 -51
- neverlib/wav_data/000_short.wav +0 -0
- neverlib-0.2.1.dist-info/RECORD +0 -40
- {neverlib-0.2.1.dist-info → neverlib-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {neverlib-0.2.1.dist-info → neverlib-0.2.3.dist-info}/top_level.txt +0 -0
neverlib/filter/core.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Author: 凌逆战 | Never
|
|
3
|
+
Date: 2025-08-04 16:25:00
|
|
4
|
+
Description: EQ滤波器
|
|
5
|
+
type(fc, gain, Q)
|
|
6
|
+
|
|
7
|
+
enum IIR_BIQUARD_TYPE {
|
|
8
|
+
IIR_BIQUARD_PASS = 0, // pass through
|
|
9
|
+
IIR_BIQUARD_RAW, // raw filter
|
|
10
|
+
IIR_BIQUARD_LPF, // 低通滤波器 low pass filter
|
|
11
|
+
IIR_BIQUARD_HPF, // 高通滤波器 high pass filter
|
|
12
|
+
IIR_BIQUARD_BPF0, // 带通滤波器 band pass filter, constant skirt gain, peak gain = Q
|
|
13
|
+
IIR_BIQUARD_BPF1, // 带通滤波器 band pass filter, const 0 dB peak gain
|
|
14
|
+
IIR_BIQUARD_NOTCH, // 陷波滤波器 notch filter
|
|
15
|
+
IIR_BIQUARD_APF, // 全通滤波器 allpass filter
|
|
16
|
+
IIR_BIQUARD_PEAKINGEQ, // 峰值滤波器 peakingEQ
|
|
17
|
+
IIR_BIQUARD_LOWSHELF, // 低切滤波器 low shelf filter
|
|
18
|
+
IIR_BIQUARD_HIGHSHELF, // 高切滤波器 high shelf filter
|
|
19
|
+
IIR_BIQUARD_QTY // number of biquard types
|
|
20
|
+
};
|
|
21
|
+
'''
|
|
22
|
+
import random
|
|
23
|
+
import numpy as np
|
|
24
|
+
from scipy import signal
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EQFilter():
|
|
28
|
+
def __init__(self, fs=16000):
|
|
29
|
+
self.fs = fs
|
|
30
|
+
|
|
31
|
+
def LowpassFilter(self, fc, Q=1 / np.sqrt(2.0)):
|
|
32
|
+
""" 低通滤波器(Low Pass Filter)
|
|
33
|
+
LPF: H(s) = 1 / (s^2 + s/Q + 1)
|
|
34
|
+
|
|
35
|
+
b0 = (1 - cos(w0))/2;
|
|
36
|
+
b1 = 1 - cos(w0);
|
|
37
|
+
b2 = (1 - cos(w0))/2;
|
|
38
|
+
a0 = 1 + alpha;
|
|
39
|
+
a1 = -2*cos(w0);
|
|
40
|
+
a2 = 1 - alpha;
|
|
41
|
+
"""
|
|
42
|
+
# 中间变量
|
|
43
|
+
w0 = 2.0 * np.pi * fc / self.fs # 角频率
|
|
44
|
+
cos_w0 = np.cos(w0) # cos(w0)
|
|
45
|
+
sin_w0 = np.sin(w0) # sin(w0)
|
|
46
|
+
alpha = sin_w0 / (2.0 * Q) # alpha
|
|
47
|
+
# ---------------------------------------------
|
|
48
|
+
b0 = (1.0 - cos_w0) / 2.0
|
|
49
|
+
b1 = 1.0 - cos_w0
|
|
50
|
+
b2 = (1.0 - cos_w0) / 2.0
|
|
51
|
+
a0 = 1.0 + alpha
|
|
52
|
+
a1 = -2.0 * cos_w0
|
|
53
|
+
a2 = 1.0 - alpha
|
|
54
|
+
numerator_B = np.array([b0, b1, b2], dtype=np.float32)
|
|
55
|
+
denominator_A = np.array([a0, a1, a2], dtype=np.float32)
|
|
56
|
+
return numerator_B / a0, denominator_A / a0
|
|
57
|
+
|
|
58
|
+
def HighpassFilter(self, fc, Q=1 / np.sqrt(2)):
|
|
59
|
+
""" 高通滤波器(High Pass Filter)
|
|
60
|
+
HPF: $H(s)=\frac{s^2}{s^2 + s/Q + 1}$
|
|
61
|
+
b0 = (1 + cos(w0))/2
|
|
62
|
+
b1 = -(1 + cos(w0))
|
|
63
|
+
b2 = (1 + cos(w0))/2
|
|
64
|
+
a0 = 1 + alpha
|
|
65
|
+
a1 = -2*cos(w0)
|
|
66
|
+
a2 = 1 - alpha
|
|
67
|
+
"""
|
|
68
|
+
# 中间变量
|
|
69
|
+
w0 = 2.0 * np.pi * fc / self.fs # 角频率
|
|
70
|
+
cos_w0 = np.cos(w0) # cos(w0)
|
|
71
|
+
sin_w0 = np.sin(w0) # sin(w0)
|
|
72
|
+
alpha = sin_w0 / (2.0 * Q) # alpha
|
|
73
|
+
# ---------------------------------------------
|
|
74
|
+
b0 = (1.0 + cos_w0) / 2.0
|
|
75
|
+
b1 = -(1.0 + cos_w0)
|
|
76
|
+
b2 = (1.0 + cos_w0) / 2.0
|
|
77
|
+
a0 = 1.0 + alpha
|
|
78
|
+
a1 = -2.0 * cos_w0
|
|
79
|
+
a2 = 1.0 - alpha
|
|
80
|
+
numerator_B = np.array([b0, b1, b2], dtype=np.float32)
|
|
81
|
+
denominator_A = np.array([a0, a1, a2], dtype=np.float32)
|
|
82
|
+
return numerator_B / a0, denominator_A / a0
|
|
83
|
+
|
|
84
|
+
def BandpassFilter_Q(self, fc, Q):
|
|
85
|
+
"""带通 Band Pass Filter (增益 = Q)
|
|
86
|
+
BPF: H(s) = s / (s^2 + s/Q + 1) (constant skirt gain, peak gain = Q)
|
|
87
|
+
b0 = sin(w0)/2 = Q*alpha
|
|
88
|
+
b1 = 0
|
|
89
|
+
b2 = -sin(w0)/2 = -Q*alpha
|
|
90
|
+
a0 = 1 + alpha
|
|
91
|
+
a1 = -2*cos(w0)
|
|
92
|
+
a2 = 1 - alpha
|
|
93
|
+
"""
|
|
94
|
+
# 中间变量
|
|
95
|
+
w0 = 2.0 * np.pi * fc / self.fs # 角频率
|
|
96
|
+
cos_w0 = np.cos(w0) # cos(w0)
|
|
97
|
+
sin_w0 = np.sin(w0) # sin(w0)
|
|
98
|
+
alpha = sin_w0 / (2.0 * Q) # alpha
|
|
99
|
+
# ---------------------------------------------
|
|
100
|
+
b0 = sin_w0 / 2.0 # Q*alpha
|
|
101
|
+
b1 = 0.0
|
|
102
|
+
b2 = -sin_w0 / 2.0 # -Q*alpha
|
|
103
|
+
a0 = 1.0 + alpha
|
|
104
|
+
a1 = -2.0 * cos_w0
|
|
105
|
+
a2 = 1.0 - alpha
|
|
106
|
+
numerator_B = np.array([b0, b1, b2], dtype=np.float32)
|
|
107
|
+
denominator_A = np.array([a0, a1, a2], dtype=np.float32)
|
|
108
|
+
return numerator_B / a0, denominator_A / a0
|
|
109
|
+
|
|
110
|
+
def BandpassFilter_0dB(self, fc, Q=1 / np.sqrt(2)):
|
|
111
|
+
"""带通 Band Pass Filter(0 db增益)
|
|
112
|
+
BPF: H(s) = (s/Q) / (s^2 + s/Q + 1) (constant 0 dB peak gain)
|
|
113
|
+
b0 = alpha
|
|
114
|
+
b1 = 0
|
|
115
|
+
b2 = -alpha
|
|
116
|
+
a0 = 1 + alpha
|
|
117
|
+
a1 = -2*cos(w0)
|
|
118
|
+
a2 = 1 - alpha
|
|
119
|
+
"""
|
|
120
|
+
# 中间变量
|
|
121
|
+
w0 = 2.0 * np.pi * fc / self.fs # 角频率
|
|
122
|
+
cos_w0 = np.cos(w0) # cos(w0)
|
|
123
|
+
sin_w0 = np.sin(w0) # sin(w0)
|
|
124
|
+
alpha = sin_w0 / (2.0 * Q) # alpha
|
|
125
|
+
# ---------------------------------------------
|
|
126
|
+
b0 = alpha
|
|
127
|
+
b1 = 0.0
|
|
128
|
+
b2 = -alpha
|
|
129
|
+
a0 = 1.0 + alpha
|
|
130
|
+
a1 = -2.0 * cos_w0
|
|
131
|
+
a2 = 1.0 - alpha
|
|
132
|
+
numerator_B = np.array([b0, b1, b2], dtype=np.float32)
|
|
133
|
+
denominator_A = np.array([a0, a1, a2], dtype=np.float32)
|
|
134
|
+
return numerator_B / a0, denominator_A / a0
|
|
135
|
+
|
|
136
|
+
def NotchFilter(self, fc, Q=1 / np.sqrt(2)):
|
|
137
|
+
"""Notch滤波器
|
|
138
|
+
notch: H(s) = (s^2 + 1) / (s^2 + s/Q + 1)$
|
|
139
|
+
b0 = 1
|
|
140
|
+
b1 = -2*cos(w0)
|
|
141
|
+
b2 = 1
|
|
142
|
+
a0 = 1 + alpha
|
|
143
|
+
a1 = -2*cos(w0)
|
|
144
|
+
a2 = 1 - alpha
|
|
145
|
+
"""
|
|
146
|
+
# 中间变量
|
|
147
|
+
w0 = 2.0 * np.pi * fc / self.fs # 角频率
|
|
148
|
+
cos_w0 = np.cos(w0) # cos(w0)
|
|
149
|
+
sin_w0 = np.sin(w0) # sin(w0)
|
|
150
|
+
alpha = sin_w0 / (2.0 * Q) # alpha
|
|
151
|
+
# ---------------------------------------------
|
|
152
|
+
b0 = 1.0
|
|
153
|
+
b1 = -2.0 * cos_w0
|
|
154
|
+
b2 = 1.0
|
|
155
|
+
a0 = 1.0 + alpha
|
|
156
|
+
a1 = -2.0 * cos_w0
|
|
157
|
+
a2 = 1.0 - alpha
|
|
158
|
+
numerator_B = np.array([b0, b1, b2], dtype=np.float32)
|
|
159
|
+
denominator_A = np.array([a0, a1, a2], dtype=np.float32)
|
|
160
|
+
return numerator_B / a0, denominator_A / a0
|
|
161
|
+
|
|
162
|
+
def AllpassFilter(self, fc, Q=1 / np.sqrt(2)):
|
|
163
|
+
"""全通 All Pass Filter
|
|
164
|
+
APF: H(s) = (s^2 - s/Q + 1) / (s^2 + s/Q + 1)$
|
|
165
|
+
b0 = 1 - alpha
|
|
166
|
+
b1 = -2*cos(w0)
|
|
167
|
+
b2 = 1 + alpha
|
|
168
|
+
a0 = 1 + alpha
|
|
169
|
+
a1 = -2*cos(w0)
|
|
170
|
+
a2 = 1 - alpha
|
|
171
|
+
"""
|
|
172
|
+
# 中间变量
|
|
173
|
+
w0 = 2.0 * np.pi * fc / self.fs # 角频率
|
|
174
|
+
cos_w0 = np.cos(w0) # cos(w0)
|
|
175
|
+
sin_w0 = np.sin(w0) # sin(w0)
|
|
176
|
+
alpha = sin_w0 / (2.0 * Q) # alpha
|
|
177
|
+
# ---------------------------------------------
|
|
178
|
+
b0 = 1.0 - alpha
|
|
179
|
+
b1 = -2.0 * cos_w0
|
|
180
|
+
b2 = 1.0 + alpha
|
|
181
|
+
a0 = 1.0 + alpha
|
|
182
|
+
a1 = -2.0 * cos_w0
|
|
183
|
+
a2 = 1.0 - alpha
|
|
184
|
+
numerator_B = np.array([b0, b1, b2], dtype=np.float32)
|
|
185
|
+
denominator_A = np.array([a0, a1, a2], dtype=np.float32)
|
|
186
|
+
return numerator_B / a0, denominator_A / a0
|
|
187
|
+
|
|
188
|
+
def PeakingFilter(self, fc, dBgain, Q=1 / np.sqrt(2)):
|
|
189
|
+
"""峰值滤波器
|
|
190
|
+
peakingEQ: H(s) = (s^2 + s*(A/Q) + 1) / (s^2 + s/(A*Q) + 1)
|
|
191
|
+
b0 = 1 + alpha*A
|
|
192
|
+
b1 = -2*cos(w0)
|
|
193
|
+
b2 = 1 - alpha*A
|
|
194
|
+
a0 = 1 + alpha/A
|
|
195
|
+
a1 = -2*cos(w0)
|
|
196
|
+
a2 = 1 - alpha/A
|
|
197
|
+
"""
|
|
198
|
+
# 中间变量
|
|
199
|
+
w0 = 2.0 * np.pi * fc / self.fs # 角频率
|
|
200
|
+
# cos_w0 = np.cos(w0) # cos(w0)
|
|
201
|
+
sin_w0 = np.sin(w0) # sin(w0)
|
|
202
|
+
alpha = sin_w0 / (2.0 * Q) # alpha
|
|
203
|
+
# gain、A 仅用于峰值和shelf滤波器
|
|
204
|
+
dBgain = round(float(dBgain), 3)
|
|
205
|
+
A = 10 ** (dBgain / 40)
|
|
206
|
+
# ---------------------------------------------
|
|
207
|
+
b0 = 1 + alpha * A
|
|
208
|
+
b1 = -2 * np.cos(w0)
|
|
209
|
+
b2 = 1 - alpha * A
|
|
210
|
+
a0 = 1 + alpha / A
|
|
211
|
+
a1 = -2 * np.cos(w0)
|
|
212
|
+
a2 = 1 - alpha / A
|
|
213
|
+
numerator_B = np.array([b0, b1, b2], dtype=np.float32)
|
|
214
|
+
denominator_A = np.array([a0, a1, a2], dtype=np.float32)
|
|
215
|
+
return numerator_B / a0, denominator_A / a0
|
|
216
|
+
|
|
217
|
+
def LowshelfFilter(self, fc, dBgain, Q=1 / np.sqrt(2)):
|
|
218
|
+
"""低切滤波器
|
|
219
|
+
lowShelf: H(s) = A * (s^2 + (sqrt(A)/Q)*s + A)/(A*s^2 + (sqrt(A)/Q)*s + 1)
|
|
220
|
+
b0 = A*( (A+1) - (A-1)*cos(w0) + 2*sqrt(A)*alpha )
|
|
221
|
+
b1 = 2*A*( (A-1) - (A+1)*cos(w0) )
|
|
222
|
+
b2 = A*( (A+1) - (A-1)*cos(w0) - 2*sqrt(A)*alpha )
|
|
223
|
+
a0 = (A+1) + (A-1)*cos(w0) + 2*sqrt(A)*alpha
|
|
224
|
+
a1 = -2*( (A-1) + (A+1)*cos(w0) )
|
|
225
|
+
a2 = (A+1) + (A-1)*cos(w0) - 2*sqrt(A)*alpha
|
|
226
|
+
"""
|
|
227
|
+
# 中间变量
|
|
228
|
+
w0 = 2.0 * np.pi * fc / self.fs # 角频率
|
|
229
|
+
cos_w0 = np.cos(w0) # cos(w0)
|
|
230
|
+
sin_w0 = np.sin(w0) # sin(w0)
|
|
231
|
+
alpha = sin_w0 / (2.0 * Q) # alpha
|
|
232
|
+
# gain、A 仅用于峰值和shelf滤波器
|
|
233
|
+
dBgain = round(float(dBgain), 3)
|
|
234
|
+
A = 10.0 ** (dBgain / 40.0)
|
|
235
|
+
# ---------------------------------------------
|
|
236
|
+
b0 = A * ((A + 1) - (A - 1) * cos_w0 + 2 * np.sqrt(A) * alpha)
|
|
237
|
+
b1 = 2 * A * ((A - 1) - (A + 1) * cos_w0)
|
|
238
|
+
b2 = A * ((A + 1) - (A - 1) * cos_w0 - 2 * np.sqrt(A) * alpha)
|
|
239
|
+
a0 = (A + 1) + (A - 1) * cos_w0 + 2 * np.sqrt(A) * alpha
|
|
240
|
+
a1 = -2 * ((A - 1) + (A + 1) * cos_w0)
|
|
241
|
+
a2 = (A + 1) + (A - 1) * cos_w0 - 2 * np.sqrt(A) * alpha
|
|
242
|
+
numerator_B = np.array([b0, b1, b2], dtype=np.float32)
|
|
243
|
+
denominator_A = np.array([a0, a1, a2], dtype=np.float32)
|
|
244
|
+
return numerator_B / a0, denominator_A / a0
|
|
245
|
+
|
|
246
|
+
def HighshelfFilter(self, fc, dBgain, Q=1 / np.sqrt(2)):
|
|
247
|
+
"""高切滤波器
|
|
248
|
+
highShelf: H(s) = A * (A*s^2 + (sqrt(A)/Q)*s + 1)/(s^2 + (sqrt(A)/Q)*s + A)
|
|
249
|
+
b0 = A*( (A+1) + (A-1)*cos(w0) + 2*sqrt(A)*alpha )
|
|
250
|
+
b1 = -2*A*( (A-1) + (A+1)*cos(w0) )
|
|
251
|
+
b2 = A*( (A+1) + (A-1)*cos(w0) - 2*sqrt(A)*alpha )
|
|
252
|
+
a0 = (A+1) - (A-1)*cos(w0) + 2*sqrt(A)*alpha
|
|
253
|
+
a1 = 2*( (A-1) - (A+1)*cos(w0) )
|
|
254
|
+
a2 = (A+1) - (A-1)*cos(w0) - 2*sqrt(A)*alpha
|
|
255
|
+
"""
|
|
256
|
+
# 中间变量
|
|
257
|
+
w0 = 2.0 * np.pi * fc / self.fs # 角频率
|
|
258
|
+
cos_w0 = np.cos(w0) # cos(w0)
|
|
259
|
+
sin_w0 = np.sin(w0) # sin(w0)
|
|
260
|
+
alpha = sin_w0 / (2.0 * Q) # alpha
|
|
261
|
+
# gain、A 仅用于峰值和shelf滤波器
|
|
262
|
+
dBgain = round(float(dBgain), 3)
|
|
263
|
+
A = 10.0 ** (dBgain / 40.0)
|
|
264
|
+
# ---------------------------------------------
|
|
265
|
+
b0 = A * ((A + 1) + (A - 1) * cos_w0 + 2 * np.sqrt(A) * alpha)
|
|
266
|
+
b1 = -2 * A * ((A - 1) + (A + 1) * cos_w0)
|
|
267
|
+
b2 = A * ((A + 1) + (A - 1) * cos_w0 - 2 * np.sqrt(A) * alpha)
|
|
268
|
+
a0 = (A + 1) - (A - 1) * cos_w0 + 2 * np.sqrt(A) * alpha
|
|
269
|
+
a1 = 2 * ((A - 1) - (A + 1) * cos_w0)
|
|
270
|
+
a2 = (A + 1) - (A - 1) * cos_w0 - 2 * np.sqrt(A) * alpha
|
|
271
|
+
numerator_B = np.array([b0, b1, b2], dtype=np.float32)
|
|
272
|
+
denominator_A = np.array([a0, a1, a2], dtype=np.float32)
|
|
273
|
+
return numerator_B / a0, denominator_A / a0
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def eq_process(sig, b_list, a_list, ratio):
|
|
277
|
+
""" psap eq增强
|
|
278
|
+
:param sig: input signal with size [T] or [T, C]
|
|
279
|
+
:param b_list: 滤波器系数列表
|
|
280
|
+
:param a_list: 滤波器系数列表
|
|
281
|
+
:param ratio: 0-1, processing ratio
|
|
282
|
+
:return: [T] when ipt.shape is [T]; [T, C] when ipt.shape is [T, C]
|
|
283
|
+
"""
|
|
284
|
+
if len(sig.shape) == 1:
|
|
285
|
+
sig.shape = -1, 1
|
|
286
|
+
assert len(sig.shape) == 2, f"input signal's shape must be 2, but {sig.shape} now!"
|
|
287
|
+
sig_eq = sig.copy()
|
|
288
|
+
if random.random() < ratio:
|
|
289
|
+
# 逐个应用滤波器
|
|
290
|
+
for b, a in zip(b_list, a_list):
|
|
291
|
+
sig_eq = signal.lfilter(b, a, sig_eq, axis=0)
|
|
292
|
+
return sig_eq
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def eq_process_test():
|
|
296
|
+
import soundfile as sf
|
|
297
|
+
|
|
298
|
+
EQ_Coef = [
|
|
299
|
+
([1.5023072, -1.0912886, 0.1981803], [1., -1.3417218, 0.4500543]), # f0 = 80, gain = -20, q = 1
|
|
300
|
+
([1.2288871, -1.173879, 0.18292512], [1., -1.173879, 0.4118123]), # f0 = 100, gain = -10, q = 1
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
wav, wav_sr = sf.read("./white.wav", dtype="float32", always_2d=True)
|
|
304
|
+
|
|
305
|
+
wav = eq_process(wav, EQ_Coef, ratio=1)
|
|
306
|
+
|
|
307
|
+
sf.write("./white_eq.wav", wav, wav_sr)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def EQ_test():
|
|
311
|
+
"""
|
|
312
|
+
lowshelf滤波器 freq=1.5kHz, gain=15dB, Q=0.5
|
|
313
|
+
b: [ 1.5023072 -1.0912886 0.1981803]
|
|
314
|
+
a: [ 1. -1.3417218 0.4500543]
|
|
315
|
+
|
|
316
|
+
peak freq=1.5kHz, gain=5dB, Q=0.5
|
|
317
|
+
b: [ 1.2288871 -1.173879 0.18292512]
|
|
318
|
+
a: [ 1. -1.173879 0.4118123]
|
|
319
|
+
"""
|
|
320
|
+
import numpy as np
|
|
321
|
+
import matplotlib.pyplot as plt
|
|
322
|
+
from scipy import signal
|
|
323
|
+
|
|
324
|
+
fs = 16000
|
|
325
|
+
|
|
326
|
+
# b, a = LowshelfFilter(1500, 15, Q=0.5)
|
|
327
|
+
b, a = PeakingFilter(1500, 5, Q=0.5)
|
|
328
|
+
print(b)
|
|
329
|
+
print(a)
|
|
330
|
+
|
|
331
|
+
w, h = signal.freqz(b, a) # 根据系数计算滤波器的频率响应, w是角频率, h是频率响应
|
|
332
|
+
plt.plot(0.5 * fs * w / np.pi, 20 * np.log10(h)) # 0.5*fs*w/np.pi 为频率
|
|
333
|
+
plt.title('Butterworth filter frequency response')
|
|
334
|
+
plt.xlabel('Frequency [Hz]')
|
|
335
|
+
plt.ylabel('Amplitude [dB]')
|
|
336
|
+
plt.grid(which='both', axis='both') # 显示网格
|
|
337
|
+
# 画红色的垂直线, 标记截止频率
|
|
338
|
+
plt.xscale('log') # x轴对数化
|
|
339
|
+
plt.show()
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Author: 凌逆战 | Never
|
|
3
|
+
Date: 2025-08-06 10:00:00
|
|
4
|
+
Description:
|
|
5
|
+
要计算个性化 MOS 分数(干扰说话者受到惩罚),请提供“-p”参数,例如:python dnsmos.py -t ./SampleClips -o sample.csv -p
|
|
6
|
+
要计算常规 MOS 分数,请省略“-p”参数。例如:python dnsmos.py -t ./SampleClips -o sample.csv
|
|
7
|
+
'''
|
|
8
|
+
import argparse
|
|
9
|
+
import concurrent.futures
|
|
10
|
+
import glob
|
|
11
|
+
import os
|
|
12
|
+
import librosa
|
|
13
|
+
import numpy as np
|
|
14
|
+
import onnxruntime as ort
|
|
15
|
+
import pandas as pd
|
|
16
|
+
import soundfile as sf
|
|
17
|
+
from tqdm import tqdm
|
|
18
|
+
|
|
19
|
+
SAMPLING_RATE = 16000
|
|
20
|
+
INPUT_LENGTH = 9.01
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ComputeScore:
|
|
24
|
+
def __init__(self, primary_model_path, p808_model_path) -> None:
|
|
25
|
+
self.onnx_sess = ort.InferenceSession(primary_model_path)
|
|
26
|
+
self.p808_onnx_sess = ort.InferenceSession(p808_model_path)
|
|
27
|
+
|
|
28
|
+
def audio_melspec(self, audio, n_mels=120, frame_size=320, hop_length=160, sr=16000, to_db=True):
|
|
29
|
+
mel_spec = librosa.feature.melspectrogram(y=audio, sr=sr, n_fft=frame_size+1, hop_length=hop_length, n_mels=n_mels)
|
|
30
|
+
if to_db:
|
|
31
|
+
mel_spec = (librosa.power_to_db(mel_spec, ref=np.max)+40)/40
|
|
32
|
+
return mel_spec.T
|
|
33
|
+
|
|
34
|
+
def get_polyfit_val(self, sig, bak, ovr, is_personalized_MOS):
|
|
35
|
+
if is_personalized_MOS:
|
|
36
|
+
p_ovr = np.poly1d([-0.00533021, 0.005101, 1.18058466, -0.11236046])
|
|
37
|
+
p_sig = np.poly1d([-0.01019296, 0.02751166, 1.19576786, -0.24348726])
|
|
38
|
+
p_bak = np.poly1d([-0.04976499, 0.44276479, -0.1644611, 0.96883132])
|
|
39
|
+
else:
|
|
40
|
+
p_ovr = np.poly1d([-0.06766283, 1.11546468, 0.04602535])
|
|
41
|
+
p_sig = np.poly1d([-0.08397278, 1.22083953, 0.0052439])
|
|
42
|
+
p_bak = np.poly1d([-0.13166888, 1.60915514, -0.39604546])
|
|
43
|
+
|
|
44
|
+
sig_poly = p_sig(sig)
|
|
45
|
+
bak_poly = p_bak(bak)
|
|
46
|
+
ovr_poly = p_ovr(ovr)
|
|
47
|
+
|
|
48
|
+
return sig_poly, bak_poly, ovr_poly
|
|
49
|
+
|
|
50
|
+
def __call__(self, fpath, sampling_rate, is_personalized_MOS):
|
|
51
|
+
aud, input_fs = sf.read(fpath)
|
|
52
|
+
fs = sampling_rate
|
|
53
|
+
if input_fs != fs:
|
|
54
|
+
audio = librosa.resample(aud, input_fs, fs)
|
|
55
|
+
else:
|
|
56
|
+
audio = aud
|
|
57
|
+
actual_audio_len = len(audio)
|
|
58
|
+
len_samples = int(INPUT_LENGTH*fs)
|
|
59
|
+
while len(audio) < len_samples:
|
|
60
|
+
audio = np.append(audio, audio)
|
|
61
|
+
|
|
62
|
+
num_hops = int(np.floor(len(audio)/fs) - INPUT_LENGTH)+1
|
|
63
|
+
hop_len_samples = fs
|
|
64
|
+
predicted_mos_sig_seg_raw = []
|
|
65
|
+
predicted_mos_bak_seg_raw = []
|
|
66
|
+
predicted_mos_ovr_seg_raw = []
|
|
67
|
+
predicted_mos_sig_seg = []
|
|
68
|
+
predicted_mos_bak_seg = []
|
|
69
|
+
predicted_mos_ovr_seg = []
|
|
70
|
+
predicted_p808_mos = []
|
|
71
|
+
|
|
72
|
+
for idx in range(num_hops):
|
|
73
|
+
audio_seg = audio[int(idx*hop_len_samples): int((idx+INPUT_LENGTH)*hop_len_samples)]
|
|
74
|
+
if len(audio_seg) < len_samples:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
input_features = np.array(audio_seg).astype('float32')[np.newaxis, :]
|
|
78
|
+
p808_input_features = np.array(self.audio_melspec(audio=audio_seg[:-160])).astype('float32')[np.newaxis, :, :]
|
|
79
|
+
oi = {'input_1': input_features}
|
|
80
|
+
p808_oi = {'input_1': p808_input_features}
|
|
81
|
+
p808_mos = self.p808_onnx_sess.run(None, p808_oi)[0][0][0]
|
|
82
|
+
mos_sig_raw, mos_bak_raw, mos_ovr_raw = self.onnx_sess.run(None, oi)[0][0]
|
|
83
|
+
mos_sig, mos_bak, mos_ovr = self.get_polyfit_val(mos_sig_raw, mos_bak_raw, mos_ovr_raw, is_personalized_MOS)
|
|
84
|
+
predicted_mos_sig_seg_raw.append(mos_sig_raw)
|
|
85
|
+
predicted_mos_bak_seg_raw.append(mos_bak_raw)
|
|
86
|
+
predicted_mos_ovr_seg_raw.append(mos_ovr_raw)
|
|
87
|
+
predicted_mos_sig_seg.append(mos_sig)
|
|
88
|
+
predicted_mos_bak_seg.append(mos_bak)
|
|
89
|
+
predicted_mos_ovr_seg.append(mos_ovr)
|
|
90
|
+
predicted_p808_mos.append(p808_mos)
|
|
91
|
+
|
|
92
|
+
clip_dict = {'filename': fpath, 'len_in_sec': actual_audio_len/fs, 'sr': fs}
|
|
93
|
+
clip_dict['num_hops'] = num_hops
|
|
94
|
+
clip_dict['OVRL_raw'] = np.mean(predicted_mos_ovr_seg_raw)
|
|
95
|
+
clip_dict['SIG_raw'] = np.mean(predicted_mos_sig_seg_raw)
|
|
96
|
+
clip_dict['BAK_raw'] = np.mean(predicted_mos_bak_seg_raw)
|
|
97
|
+
clip_dict['OVRL'] = np.mean(predicted_mos_ovr_seg)
|
|
98
|
+
clip_dict['SIG'] = np.mean(predicted_mos_sig_seg)
|
|
99
|
+
clip_dict['BAK'] = np.mean(predicted_mos_bak_seg)
|
|
100
|
+
clip_dict['P808_MOS'] = np.mean(predicted_p808_mos)
|
|
101
|
+
return clip_dict
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main(args):
|
|
105
|
+
models = glob.glob(os.path.join(args.testset_dir, "*"))
|
|
106
|
+
audio_clips_list = []
|
|
107
|
+
p808_model_path = os.path.join('DNSMOS', 'model_v8.onnx')
|
|
108
|
+
|
|
109
|
+
if args.personalized_MOS:
|
|
110
|
+
primary_model_path = os.path.join('pDNSMOS', 'sig_bak_ovr.onnx')
|
|
111
|
+
else:
|
|
112
|
+
primary_model_path = os.path.join('DNSMOS', 'sig_bak_ovr.onnx')
|
|
113
|
+
|
|
114
|
+
compute_score = ComputeScore(primary_model_path, p808_model_path)
|
|
115
|
+
|
|
116
|
+
rows = []
|
|
117
|
+
clips = []
|
|
118
|
+
clips = glob.glob(os.path.join(args.testset_dir, "*.wav"))
|
|
119
|
+
is_personalized_eval = args.personalized_MOS
|
|
120
|
+
desired_fs = SAMPLING_RATE
|
|
121
|
+
for m in tqdm(models):
|
|
122
|
+
max_recursion_depth = 10
|
|
123
|
+
audio_path = os.path.join(args.testset_dir, m)
|
|
124
|
+
audio_clips_list = glob.glob(os.path.join(audio_path, "*.wav"))
|
|
125
|
+
while len(audio_clips_list) == 0 and max_recursion_depth > 0:
|
|
126
|
+
audio_path = os.path.join(audio_path, "**")
|
|
127
|
+
audio_clips_list = glob.glob(os.path.join(audio_path, "*.wav"))
|
|
128
|
+
max_recursion_depth -= 1
|
|
129
|
+
clips.extend(audio_clips_list)
|
|
130
|
+
|
|
131
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
132
|
+
future_to_url = {executor.submit(compute_score, clip, desired_fs, is_personalized_eval): clip for clip in clips}
|
|
133
|
+
for future in tqdm(concurrent.futures.as_completed(future_to_url)):
|
|
134
|
+
clip = future_to_url[future]
|
|
135
|
+
try:
|
|
136
|
+
data = future.result()
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
print('%r generated an exception: %s' % (clip, exc))
|
|
139
|
+
else:
|
|
140
|
+
rows.append(data)
|
|
141
|
+
|
|
142
|
+
df = pd.DataFrame(rows)
|
|
143
|
+
if args.csv_path:
|
|
144
|
+
csv_path = args.csv_path
|
|
145
|
+
df.to_csv(csv_path)
|
|
146
|
+
else:
|
|
147
|
+
print(df.describe())
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
if __name__ == "__main__":
|
|
151
|
+
parser = argparse.ArgumentParser()
|
|
152
|
+
parser.add_argument('-t', "--testset_dir", default='.',
|
|
153
|
+
help='Path to the dir containing audio clips in .wav to be evaluated')
|
|
154
|
+
parser.add_argument('-o', "--csv_path", default=None, help='Dir to the csv that saves the results')
|
|
155
|
+
parser.add_argument('-p', "--personalized_MOS", action='store_true',
|
|
156
|
+
help='Flag to indicate if personalized MOS score is needed or regular')
|
|
157
|
+
|
|
158
|
+
args = parser.parse_args()
|
|
159
|
+
|
|
160
|
+
main(args)
|
neverlib/metrics/snr.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
sys.path.append("../")
|
|
3
|
+
import librosa
|
|
4
|
+
import numpy as np
|
|
5
|
+
from vad.utils import vad2nad
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_snr(speech, noise):
|
|
9
|
+
"""计算信噪比
|
|
10
|
+
Args:
|
|
11
|
+
speech: 语音音频
|
|
12
|
+
noise: 噪声音频
|
|
13
|
+
Returns:
|
|
14
|
+
snr: 信噪比
|
|
15
|
+
"""
|
|
16
|
+
assert speech.ndim == noise.ndim, "speech和noise的维度不一样"
|
|
17
|
+
|
|
18
|
+
power_speech = np.mean(speech**2)
|
|
19
|
+
power_noise = max(np.mean(noise**2), 1e-10)
|
|
20
|
+
|
|
21
|
+
snr = 10 * np.log10(power_speech / power_noise)
|
|
22
|
+
return snr
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_snr_from_noisy(noisy, speech_vad=None):
|
|
26
|
+
"""根据带噪音频计算信噪比
|
|
27
|
+
Args:
|
|
28
|
+
noisy: 带噪音频
|
|
29
|
+
speech_vad: [{start:xxx, end:xxx}, ...]
|
|
30
|
+
Returns:
|
|
31
|
+
snr: 信噪比
|
|
32
|
+
"""
|
|
33
|
+
assert speech_vad is not None, "speech_vad不能为空"
|
|
34
|
+
|
|
35
|
+
# 提取语音段
|
|
36
|
+
speech_segments = []
|
|
37
|
+
for segment in speech_vad:
|
|
38
|
+
start = segment['start']
|
|
39
|
+
end = segment['end']
|
|
40
|
+
speech_segments.append(noisy[start:end])
|
|
41
|
+
speech = np.concatenate(speech_segments, axis=0)
|
|
42
|
+
|
|
43
|
+
# 提取非语音段
|
|
44
|
+
noise_segments = []
|
|
45
|
+
noise_point_list = vad2nad(speech_vad, len(noisy))
|
|
46
|
+
for noise_point in noise_point_list:
|
|
47
|
+
noise_segments.append(noisy[noise_point['start']:noise_point['end']])
|
|
48
|
+
noise = np.concatenate(noise_segments, axis=0)
|
|
49
|
+
|
|
50
|
+
return get_snr(speech, noise)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def seg_snr(clean, noisy, frame_length: int, hop_length: int):
|
|
54
|
+
"""
|
|
55
|
+
分帧计算信噪比
|
|
56
|
+
Args:
|
|
57
|
+
clean: 干净音频, numpy array
|
|
58
|
+
noisy: 带噪音频, numpy array
|
|
59
|
+
frame_length: 帧长
|
|
60
|
+
hop_length: 帧移
|
|
61
|
+
Returns:
|
|
62
|
+
snr_mean: 平均信噪比, float
|
|
63
|
+
Raises:
|
|
64
|
+
ValueError: 当输入参数不合法时抛出
|
|
65
|
+
"""
|
|
66
|
+
assert clean.shape == noisy.shape, "clean和noisy的维度不一样"
|
|
67
|
+
|
|
68
|
+
# 分帧
|
|
69
|
+
clean_frames = librosa.util.frame(clean, frame_length=frame_length, hop_length=hop_length) # (frame_length, n_frames)
|
|
70
|
+
noisy_frames = librosa.util.frame(noisy, frame_length=frame_length, hop_length=hop_length) # (frame_length, n_frames)
|
|
71
|
+
|
|
72
|
+
# 计算每帧的信噪比
|
|
73
|
+
snr_frames = []
|
|
74
|
+
for i in range(clean_frames.shape[1]):
|
|
75
|
+
clean_frame = clean_frames[:, i]
|
|
76
|
+
noisy_frame = noisy_frames[:, i]
|
|
77
|
+
# 跳过静音帧
|
|
78
|
+
if np.all(np.abs(clean_frame) < 1e-6) or np.all(np.abs(noisy_frame) < 1e-6):
|
|
79
|
+
continue
|
|
80
|
+
snr_frames.append(get_snr(clean_frame, noisy_frame))
|
|
81
|
+
|
|
82
|
+
# 如果所有帧都是静音
|
|
83
|
+
if not snr_frames:
|
|
84
|
+
return float('-inf')
|
|
85
|
+
|
|
86
|
+
return np.mean(snr_frames)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def psnr(clean, noisy, max_val=None):
|
|
90
|
+
"""
|
|
91
|
+
计算峰值信噪比
|
|
92
|
+
Args:
|
|
93
|
+
clean: 干净音频, numpy array
|
|
94
|
+
noisy: 带噪音频, numpy array
|
|
95
|
+
max_val: 信号最大值, 如果为None则使用clean信号的实际最大值
|
|
96
|
+
Returns:
|
|
97
|
+
psnr: 峰值信噪比, 单位dB
|
|
98
|
+
"""
|
|
99
|
+
assert clean.shape == noisy.shape, "clean和noisy的维度不一样"
|
|
100
|
+
|
|
101
|
+
# 如果没有指定最大值, 使用clean信号的实际最大值
|
|
102
|
+
if max_val is None:
|
|
103
|
+
max_val = np.abs(clean).max()
|
|
104
|
+
|
|
105
|
+
# 计算均方误差 (MSE)
|
|
106
|
+
mse = np.mean((clean - noisy) ** 2)
|
|
107
|
+
|
|
108
|
+
# 避免除以0
|
|
109
|
+
if mse == 0:
|
|
110
|
+
return float('inf')
|
|
111
|
+
|
|
112
|
+
# 计算PSNR
|
|
113
|
+
psnr = 10 * np.log10(max_val**2 / mse)
|
|
114
|
+
return psnr
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def si_sdr(reference, estimate, epsilon=1e-8):
|
|
118
|
+
"""
|
|
119
|
+
计算尺度不变信噪比 (Scale-Invariant Signal-to-Distortion Ratio, SI-SDR)。
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
reference (np.ndarray): 原始的、干净的参考信号 (一维数组)。
|
|
123
|
+
estimate (np.ndarray): 模型估计或处理后的信号 (一维数组)。
|
|
124
|
+
epsilon (float): 一个非常小的数值, 用于防止分母为零, 保证数值稳定性。
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
float: SI-SDR 值, 单位为分贝 (dB)。
|
|
128
|
+
"""
|
|
129
|
+
assert reference.shape == estimate.shape, "reference和estimate的维度不一样"
|
|
130
|
+
|
|
131
|
+
# 2. 零均值化 (可选但推荐)
|
|
132
|
+
# 移除直流分量, 使计算更关注信号的动态变化
|
|
133
|
+
reference = reference - np.mean(reference)
|
|
134
|
+
estimate = estimate - np.mean(estimate)
|
|
135
|
+
|
|
136
|
+
# 3. 计算目标信号分量 (s_target)
|
|
137
|
+
# s_target 是 estimate 在 reference 上的投影
|
|
138
|
+
# 公式: s_target = (<ŝ, s> / ||s||²) * s
|
|
139
|
+
dot_product = np.dot(estimate, reference) # <ŝ, s> (点积)
|
|
140
|
+
norm_s_squared = np.dot(reference, reference) # ||s||² (s的能量)
|
|
141
|
+
|
|
142
|
+
# 检查参考信号能量, 避免除以零
|
|
143
|
+
if norm_s_squared < epsilon:
|
|
144
|
+
# 如果参考信号几乎是静音, SI-SDR没有意义
|
|
145
|
+
return -np.inf # 返回负无穷或np.nan
|
|
146
|
+
|
|
147
|
+
alpha = dot_product / (norm_s_squared + epsilon) # 最佳缩放因子 α
|
|
148
|
+
s_target = alpha * reference
|
|
149
|
+
|
|
150
|
+
# 4. 计算误差/失真分量 (e_noise)
|
|
151
|
+
e_noise = estimate - s_target
|
|
152
|
+
|
|
153
|
+
# 5. 计算 SI-SDR
|
|
154
|
+
# SI-SDR = 10 * log10 ( ||s_target||² / ||e_noise||² )
|
|
155
|
+
power_s_target = np.sum(s_target**2) # ||s_target||²
|
|
156
|
+
power_e_noise = np.sum(e_noise**2) # ||e_noise||²
|
|
157
|
+
|
|
158
|
+
# 同样加上 epsilon 防止除以零
|
|
159
|
+
if power_e_noise < epsilon:
|
|
160
|
+
# 如果噪声能量极小, 说明匹配得非常好
|
|
161
|
+
return np.inf # 返回正无穷
|
|
162
|
+
|
|
163
|
+
si_sdr_val = 10 * np.log10(power_s_target / (power_e_noise + epsilon))
|
|
164
|
+
|
|
165
|
+
return si_sdr_val
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
# 生成测试信号
|
|
170
|
+
speech = np.random.randn(1000)
|
|
171
|
+
noise = np.random.randn(1000) * 0.1 # 较小的噪声
|
|
172
|
+
noisy = speech + noise
|
|
173
|
+
|
|
174
|
+
# 测试各种信噪比计算方法
|
|
175
|
+
print(f"SNR: {get_snr(speech, noise):.2f} dB")
|
|
176
|
+
print(f"Segmental SNR: {SegSNR(speech, noisy, 100, 50):.2f} dB")
|
|
177
|
+
print(f"PSNR: {PSNR(speech, noisy):.2f} dB")
|