-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathtest_load_audio_bytes.py
More file actions
235 lines (178 loc) · 9.05 KB
/
Copy pathtest_load_audio_bytes.py
File metadata and controls
235 lines (178 loc) · 9.05 KB
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import base64
import importlib.util
import io
from pathlib import Path
import struct
import sys
import tempfile
import types
import unittest
from unittest import mock
import wave
import numpy as np
ROOT = Path(__file__).resolve().parents[1]
def _load_utils_module():
module_names = ["funasr", "funasr.download", "funasr.download.file"]
previous = {name: sys.modules.get(name) for name in module_names}
funasr_package = types.ModuleType("funasr")
funasr_package.__path__ = []
download_package = types.ModuleType("funasr.download")
download_package.__path__ = []
download_file = types.ModuleType("funasr.download.file")
download_file.download_from_url = lambda url: url
sys.modules["funasr"] = funasr_package
sys.modules["funasr.download"] = download_package
sys.modules["funasr.download.file"] = download_file
try:
spec = importlib.util.spec_from_file_location(
"load_utils_audio_bytes_test", ROOT / "funasr/utils/load_utils.py"
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
finally:
for name, original in previous.items():
if original is None:
sys.modules.pop(name, None)
else:
sys.modules[name] = original
LOAD_UTILS = _load_utils_module()
# 80 ms, 16 kHz mono, generated without ID3 or Xing metadata by ffmpeg 6.1.1.
NO_ID3_MP3 = base64.b64decode(
"//NIxAAa0LJcBVlIABsLTlpy05ZMsmWnLloB0Uy2BhEGUUDiEXzRhOvE88TrdOJY1lDSQMoYBCIAFBGIO4CYJgHAGBsEw2TtwUQIECBBBwnB8H8oGJTlA/wcOYgB/WDhzIA/wI7n+jhgHz+BDnfg+BAQBDB//B8H1QJghAAQSMFwa3j/mFYgGAQU1hUCfLMm//NIxBcg+gJkDZ2gADIa5vcxhhUcxwIo5huQh2CQ4GXugbI6BkioGWkgiPkQAyWIBoMXQsSBs0DYYRz/hlkMijHCghC3/kNFyi5SaHOHO//KJFSKmReJox//yKkVMi8XjEul1L//8mi8Yl0upF42EoS//Pf///XYuxb//41DPjII3hgHzBwACWYI5Npk2mcG//NIxBYiUm5MAZ6oABXAdm1MCaSE6GJWZaBm8zAWVAg8DDsXAAA46gGGABlEABoH+F1A6xxid//IwiAuAskT//HGWCcIIdJ///J84VCcOm5P///nDQuHTcvqNC5///+s3N1Ghos3N1Ghz//WD4gCIPiAIg////gwEQuQYFyiD4Eqijt1s/+gX9S2f8CFjqrn//NIxA8hCpaUAZqYAPlgf8AdQXUCtxZpIEW4sIuMgA7jJMuo+OAmyIkTLikklo/KxOF8qk+YIqSUtH8rE4ZlUvoGy6SqK/5cTPF9R8uLPUlUV0lf8vqPmizyaj6Cz1FdJVFdJX/6c+hPJz5wBpK2LsX/hgBmwwCYHDAJgcMKtVaq1VVMQU1FMy4xMDBVVVVV//NIxA0AAANIAcAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"
)[:720]
def _sine_pcm(sample_rate, duration=0.1):
sample_count = round(sample_rate * duration)
times = np.arange(sample_count, dtype=np.float64) / sample_rate
return np.round(np.sin(2 * np.pi * 440 * times) * 12000).astype(np.int16)
def _wav_bytes(samples, sample_rate):
output = io.BytesIO()
with wave.open(output, "wb") as wav_file:
wav_file.setnchannels(1)
wav_file.setsampwidth(2)
wav_file.setframerate(sample_rate)
wav_file.writeframes(samples.tobytes())
return output.getvalue()
def _rifx_bytes(samples, sample_rate):
big_endian_samples = samples.astype(">i2").tobytes()
return (
b"RIFX"
+ struct.pack(">I", 36 + len(big_endian_samples))
+ b"WAVEfmt "
+ struct.pack(">IHHIIHH", 16, 1, 1, sample_rate, sample_rate * 2, 2, 16)
+ b"data"
+ struct.pack(">I", len(big_endian_samples))
+ big_endian_samples
)
def _free_format_mp3_bytes():
data = bytearray(NO_ID3_MP3)
for offset in (0, 144, 288, 432, 576):
data[offset + 2] &= 0x0F
return bytes(data)
class TestLoadAudioBytes(unittest.TestCase):
def test_decodes_wav_container_without_treating_header_as_pcm(self):
samples = _sine_pcm(16000)
actual = LOAD_UTILS.load_bytes(_wav_bytes(samples, 16000))
expected = samples.astype(np.float32) / 32768.0
self.assertEqual(actual.dtype, np.float32)
np.testing.assert_allclose(actual, expected, atol=1e-6, rtol=0)
def test_resamples_wav_container_to_16khz(self):
samples = _sine_pcm(8000)
actual = LOAD_UTILS.load_bytes(_wav_bytes(samples, 8000))
self.assertEqual(actual.dtype, np.float32)
self.assertEqual(len(actual), 1600)
self.assertTrue(np.isfinite(actual).all())
self.assertGreater(float(np.max(np.abs(actual))), 0.1)
def test_decodes_big_endian_rifx_wav(self):
samples = np.array([-32768, -1000, 0, 1000, 32767], dtype=np.int16)
actual = LOAD_UTILS.load_bytes(_rifx_bytes(samples, 16000))
expected = samples.astype(np.float32) / 32768.0
np.testing.assert_array_equal(actual, expected)
def test_recognizes_large_wave_container_variants(self):
decoded = np.array([0.25, -0.25], dtype=np.float32)
for marker in (b"RF64", b"BW64"):
container = marker + b"\xff\xff\xff\xffWAVEplaceholder"
with self.subTest(marker=marker):
with mock.patch.object(
LOAD_UTILS, "load_audio_text_image_video", return_value=decoded
):
actual = LOAD_UTILS.load_bytes(container)
np.testing.assert_array_equal(actual, decoded)
def test_preserves_raw_int16_pcm_bytes(self):
samples = np.array([-32768, -12345, 0, 12345, 32767], dtype=np.int16)
actual = LOAD_UTILS.load_bytes(samples.tobytes())
expected = samples.astype(np.float32) / 32768.0
np.testing.assert_array_equal(actual, expected)
def test_preserves_raw_pcm_with_mp3_sync_like_first_sample(self):
raw_pcm = b"\xff\xfb\x00\x00\x39\x30\xc7\xcf"
samples = np.frombuffer(raw_pcm, dtype=np.int16)
actual = LOAD_UTILS.load_bytes(raw_pcm)
expected = samples.astype(np.float32) / 32768.0
np.testing.assert_array_equal(actual, expected)
def test_preserves_raw_pcm_with_inconsistent_free_format_sync_headers(self):
raw_pcm = bytearray(np.arange(160, dtype=np.int16).tobytes())
for offset in (0, 100, 210):
raw_pcm[offset : offset + 4] = b"\xff\xfb\x00\x00"
raw_pcm = bytes(raw_pcm)
samples = np.frombuffer(raw_pcm, dtype=np.int16)
actual = LOAD_UTILS.load_bytes(raw_pcm)
expected = samples.astype(np.float32) / 32768.0
np.testing.assert_array_equal(actual, expected)
def test_preserves_raw_pcm_with_non_wave_riff_prefix(self):
raw_pcm = b"RIFF\x00\x00\x00\x00NOPE\x00\x00\x00\x00"
samples = np.frombuffer(raw_pcm, dtype=np.int16)
actual = LOAD_UTILS.load_bytes(raw_pcm)
expected = samples.astype(np.float32) / 32768.0
np.testing.assert_array_equal(actual, expected)
def test_no_id3_mp3_never_falls_back_to_raw_pcm(self):
with mock.patch.object(
LOAD_UTILS,
"load_audio_text_image_video",
side_effect=RuntimeError("decoder unavailable"),
):
with self.assertRaisesRegex(RuntimeError, "complete supported audio file"):
LOAD_UTILS.load_bytes(NO_ID3_MP3)
def test_free_format_mp3_never_falls_back_to_raw_pcm(self):
with mock.patch.object(
LOAD_UTILS,
"load_audio_text_image_video",
side_effect=RuntimeError("decoder unavailable"),
):
with self.assertRaisesRegex(RuntimeError, "complete supported audio file"):
LOAD_UTILS.load_bytes(_free_format_mp3_bytes())
def test_decodes_no_id3_mp3_consistently_with_file_path(self):
with tempfile.NamedTemporaryFile(suffix=".mp3") as mp3_file:
mp3_file.write(NO_ID3_MP3)
mp3_file.flush()
expected = LOAD_UTILS.load_audio_text_image_video(mp3_file.name, fs=16000)
actual = LOAD_UTILS.load_bytes(NO_ID3_MP3)
if hasattr(expected, "detach"):
expected = expected.detach().cpu().numpy()
np.testing.assert_allclose(actual, expected, atol=1e-6, rtol=0)
def test_rewinds_file_like_audio_between_decoders(self):
wav_data = _wav_bytes(_sine_pcm(16000), 16000)
def consume_then_fail(stream):
stream.read()
raise RuntimeError("first decoder failed")
with mock.patch.object(
LOAD_UTILS.torchaudio, "load", side_effect=consume_then_fail
):
actual = LOAD_UTILS.load_audio_text_image_video(
io.BytesIO(wav_data), fs=16000
)
expected = _sine_pcm(16000).astype(np.float32) / 32768.0
np.testing.assert_allclose(actual, expected, atol=1e-6, rtol=0)
@unittest.skipUnless(LOAD_UTILS.is_ffmpeg_installed(), "ffmpeg is required")
def test_ffmpeg_decodes_file_like_audio(self):
samples = _sine_pcm(16000)
actual = LOAD_UTILS._load_audio_ffmpeg(
io.BytesIO(_wav_bytes(samples, 16000)), sr=16000
)
expected = samples.astype(np.float32) / 32768.0
np.testing.assert_allclose(actual, expected, atol=1e-6, rtol=0)
def test_corrupt_container_error_is_actionable(self):
corrupt_wav = b"RIFF\x10\x00\x00\x00WAVEbroken"
with self.assertRaisesRegex(RuntimeError, "complete supported audio file"):
LOAD_UTILS.load_bytes(corrupt_wav)
if __name__ == "__main__":
unittest.main()