/src/mozilla-central/media/libcubeb/gtest/test_resampler.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /* |
2 | | * Copyright © 2016 Mozilla Foundation |
3 | | * |
4 | | * This program is made available under an ISC-style license. See the |
5 | | * accompanying file LICENSE for details. |
6 | | */ |
7 | | #ifndef NOMINMAX |
8 | | #define NOMINMAX |
9 | | #endif // NOMINMAX |
10 | | #include "gtest/gtest.h" |
11 | | #include "common.h" |
12 | | #include "cubeb_resampler_internal.h" |
13 | | #include <stdio.h> |
14 | | #include <algorithm> |
15 | | #include <iostream> |
16 | | |
17 | | /* Windows cmath USE_MATH_DEFINE thing... */ |
18 | | const float PI = 3.14159265359f; |
19 | | |
20 | | /* Testing all sample rates is very long, so if THOROUGH_TESTING is not defined, |
21 | | * only part of the test suite is ran. */ |
22 | | #ifdef THOROUGH_TESTING |
23 | | /* Some standard sample rates we're testing with. */ |
24 | | const uint32_t sample_rates[] = { |
25 | | 8000, |
26 | | 16000, |
27 | | 32000, |
28 | | 44100, |
29 | | 48000, |
30 | | 88200, |
31 | | 96000, |
32 | | 192000 |
33 | | }; |
34 | | /* The maximum number of channels we're resampling. */ |
35 | | const uint32_t max_channels = 2; |
36 | | /* The minimum an maximum number of milliseconds we're resampling for. This is |
37 | | * used to simulate the fact that the audio stream is resampled in chunks, |
38 | | * because audio is delivered using callbacks. */ |
39 | | const uint32_t min_chunks = 10; /* ms */ |
40 | | const uint32_t max_chunks = 30; /* ms */ |
41 | | const uint32_t chunk_increment = 1; |
42 | | |
43 | | #else |
44 | | |
45 | | const uint32_t sample_rates[] = { |
46 | | 8000, |
47 | | 44100, |
48 | | 48000, |
49 | | }; |
50 | | const uint32_t max_channels = 2; |
51 | | const uint32_t min_chunks = 10; /* ms */ |
52 | | const uint32_t max_chunks = 30; /* ms */ |
53 | | const uint32_t chunk_increment = 10; |
54 | | #endif |
55 | | |
56 | | #define DUMP_ARRAYS |
57 | | #ifdef DUMP_ARRAYS |
58 | | /** |
59 | | * Files produced by dump(...) can be converted to .wave files using: |
60 | | * |
61 | | * sox -c <channel_count> -r <rate> -e float -b 32 file.raw file.wav |
62 | | * |
63 | | * for floating-point audio, or: |
64 | | * |
65 | | * sox -c <channel_count> -r <rate> -e unsigned -b 16 file.raw file.wav |
66 | | * |
67 | | * for 16bit integer audio. |
68 | | */ |
69 | | |
70 | | /* Use the correct implementation of fopen, depending on the platform. */ |
71 | | void fopen_portable(FILE ** f, const char * name, const char * mode) |
72 | 0 | { |
73 | | #ifdef WIN32 |
74 | | fopen_s(f, name, mode); |
75 | | #else |
76 | | *f = fopen(name, mode); |
77 | 0 | #endif |
78 | 0 | } |
79 | | |
80 | | template<typename T> |
81 | | void dump(const char * name, T * frames, size_t count) |
82 | 0 | { |
83 | 0 | FILE * file; |
84 | 0 | fopen_portable(&file, name, "wb"); |
85 | 0 |
|
86 | 0 | if (!file) { |
87 | 0 | fprintf(stderr, "error opening %s\n", name); |
88 | 0 | return; |
89 | 0 | } |
90 | 0 | |
91 | 0 | if (count != fwrite(frames, sizeof(T), count, file)) { |
92 | 0 | fprintf(stderr, "error writing to %s\n", name); |
93 | 0 | } |
94 | 0 | fclose(file); |
95 | 0 | } |
96 | | #else |
97 | | template<typename T> |
98 | | void dump(const char * name, T * frames, size_t count) |
99 | | { } |
100 | | #endif |
101 | | |
102 | | // The more the ratio is far from 1, the more we accept a big error. |
103 | | float epsilon_tweak_ratio(float ratio) |
104 | 0 | { |
105 | 0 | return ratio >= 1 ? ratio : 1 / ratio; |
106 | 0 | } |
107 | | |
108 | | // Epsilon values for comparing resampled data to expected data. |
109 | | // The bigger the resampling ratio is, the more lax we are about errors. |
110 | | template<typename T> |
111 | | T epsilon(float ratio); |
112 | | |
113 | | template<> |
114 | 0 | float epsilon(float ratio) { |
115 | 0 | return 0.08f * epsilon_tweak_ratio(ratio); |
116 | 0 | } |
117 | | |
118 | | template<> |
119 | 0 | int16_t epsilon(float ratio) { |
120 | 0 | return static_cast<int16_t>(10 * epsilon_tweak_ratio(ratio)); |
121 | 0 | } |
122 | | |
123 | | void test_delay_lines(uint32_t delay_frames, uint32_t channels, uint32_t chunk_ms) |
124 | 0 | { |
125 | 0 | const size_t length_s = 2; |
126 | 0 | const size_t rate = 44100; |
127 | 0 | const size_t length_frames = rate * length_s; |
128 | 0 | delay_line<float> delay(delay_frames, channels, rate); |
129 | 0 | auto_array<float> input; |
130 | 0 | auto_array<float> output; |
131 | 0 | uint32_t chunk_length = channels * chunk_ms * rate / 1000; |
132 | 0 | uint32_t output_offset = 0; |
133 | 0 | uint32_t channel = 0; |
134 | 0 |
|
135 | 0 | /** Generate diracs every 100 frames, and check they are delayed. */ |
136 | 0 | input.push_silence(length_frames * channels); |
137 | 0 | for (uint32_t i = 0; i < input.length() - 1; i+=100) { |
138 | 0 | input.data()[i + channel] = 0.5; |
139 | 0 | channel = (channel + 1) % channels; |
140 | 0 | } |
141 | 0 | dump("input.raw", input.data(), input.length()); |
142 | 0 | while(input.length()) { |
143 | 0 | uint32_t to_pop = std::min<uint32_t>(input.length(), chunk_length * channels); |
144 | 0 | float * in = delay.input_buffer(to_pop / channels); |
145 | 0 | input.pop(in, to_pop); |
146 | 0 | delay.written(to_pop / channels); |
147 | 0 | output.push_silence(to_pop); |
148 | 0 | delay.output(output.data() + output_offset, to_pop / channels); |
149 | 0 | output_offset += to_pop; |
150 | 0 | } |
151 | 0 |
|
152 | 0 | // Check the diracs have been shifted by `delay_frames` frames. |
153 | 0 | for (uint32_t i = 0; i < output.length() - delay_frames * channels + 1; i+=100) { |
154 | 0 | ASSERT_EQ(output.data()[i + channel + delay_frames * channels], 0.5); |
155 | 0 | channel = (channel + 1) % channels; |
156 | 0 | } |
157 | 0 |
|
158 | 0 | dump("output.raw", output.data(), output.length()); |
159 | 0 | } |
160 | | /** |
161 | | * This takes sine waves with a certain `channels` count, `source_rate`, and |
162 | | * resample them, by chunk of `chunk_duration` milliseconds, to `target_rate`. |
163 | | * Then a sample-wise comparison is performed against a sine wave generated at |
164 | | * the correct rate. |
165 | | */ |
166 | | template<typename T> |
167 | | void test_resampler_one_way(uint32_t channels, uint32_t source_rate, uint32_t target_rate, float chunk_duration) |
168 | 0 | { |
169 | 0 | size_t chunk_duration_in_source_frames = static_cast<uint32_t>(ceil(chunk_duration * source_rate / 1000.)); |
170 | 0 | float resampling_ratio = static_cast<float>(source_rate) / target_rate; |
171 | 0 | cubeb_resampler_speex_one_way<T> resampler(channels, source_rate, target_rate, 3); |
172 | 0 | auto_array<T> source(channels * source_rate * 10); |
173 | 0 | auto_array<T> destination(channels * target_rate * 10); |
174 | 0 | auto_array<T> expected(channels * target_rate * 10); |
175 | 0 | uint32_t phase_index = 0; |
176 | 0 | uint32_t offset = 0; |
177 | 0 | const uint32_t buf_len = 2; /* seconds */ |
178 | 0 |
|
179 | 0 | // generate a sine wave in each channel, at the source sample rate |
180 | 0 | source.push_silence(channels * source_rate * buf_len); |
181 | 0 | while(offset != source.length()) { |
182 | 0 | float p = phase_index++ / static_cast<float>(source_rate); |
183 | 0 | for (uint32_t j = 0; j < channels; j++) { |
184 | 0 | source.data()[offset++] = 0.5 * sin(440. * 2 * PI * p); |
185 | 0 | } |
186 | 0 | } |
187 | 0 |
|
188 | 0 | dump("input.raw", source.data(), source.length()); |
189 | 0 |
|
190 | 0 | expected.push_silence(channels * target_rate * buf_len); |
191 | 0 | // generate a sine wave in each channel, at the target sample rate. |
192 | 0 | // Insert silent samples at the beginning to account for the resampler latency. |
193 | 0 | offset = resampler.latency() * channels; |
194 | 0 | for (uint32_t i = 0; i < offset; i++) { |
195 | 0 | expected.data()[i] = 0.0f; |
196 | 0 | } |
197 | 0 | phase_index = 0; |
198 | 0 | while (offset != expected.length()) { |
199 | 0 | float p = phase_index++ / static_cast<float>(target_rate); |
200 | 0 | for (uint32_t j = 0; j < channels; j++) { |
201 | 0 | expected.data()[offset++] = 0.5 * sin(440. * 2 * PI * p); |
202 | 0 | } |
203 | 0 | } |
204 | 0 |
|
205 | 0 | dump("expected.raw", expected.data(), expected.length()); |
206 | 0 |
|
207 | 0 | // resample by chunk |
208 | 0 | uint32_t write_offset = 0; |
209 | 0 | destination.push_silence(channels * target_rate * buf_len); |
210 | 0 | while (write_offset < destination.length()) |
211 | 0 | { |
212 | 0 | size_t output_frames = static_cast<uint32_t>(floor(chunk_duration_in_source_frames / resampling_ratio)); |
213 | 0 | uint32_t input_frames = resampler.input_needed_for_output(output_frames); |
214 | 0 | resampler.input(source.data(), input_frames); |
215 | 0 | source.pop(nullptr, input_frames * channels); |
216 | 0 | resampler.output(destination.data() + write_offset, |
217 | 0 | std::min(output_frames, (destination.length() - write_offset) / channels)); |
218 | 0 | write_offset += output_frames * channels; |
219 | 0 | } |
220 | 0 |
|
221 | 0 | dump("output.raw", destination.data(), expected.length()); |
222 | 0 |
|
223 | 0 | // compare, taking the latency into account |
224 | 0 | bool fuzzy_equal = true; |
225 | 0 | for (uint32_t i = resampler.latency() + 1; i < expected.length(); i++) { |
226 | 0 | float diff = fabs(expected.data()[i] - destination.data()[i]); |
227 | 0 | if (diff > epsilon<T>(resampling_ratio)) { |
228 | 0 | fprintf(stderr, "divergence at %d: %f %f (delta %f)\n", i, expected.data()[i], destination.data()[i], diff); |
229 | 0 | fuzzy_equal = false; |
230 | 0 | } |
231 | 0 | } |
232 | 0 | ASSERT_TRUE(fuzzy_equal); |
233 | 0 | } |
234 | | |
235 | | template<typename T> |
236 | | cubeb_sample_format cubeb_format(); |
237 | | |
238 | | template<> |
239 | | cubeb_sample_format cubeb_format<float>() |
240 | 0 | { |
241 | 0 | return CUBEB_SAMPLE_FLOAT32NE; |
242 | 0 | } |
243 | | |
244 | | template<> |
245 | | cubeb_sample_format cubeb_format<short>() |
246 | 0 | { |
247 | 0 | return CUBEB_SAMPLE_S16NE; |
248 | 0 | } |
249 | | |
250 | | struct osc_state { |
251 | | osc_state() |
252 | | : input_phase_index(0) |
253 | | , output_phase_index(0) |
254 | | , output_offset(0) |
255 | | , input_channels(0) |
256 | | , output_channels(0) |
257 | 0 | {} |
258 | | uint32_t input_phase_index; |
259 | | uint32_t max_output_phase_index; |
260 | | uint32_t output_phase_index; |
261 | | uint32_t output_offset; |
262 | | uint32_t input_channels; |
263 | | uint32_t output_channels; |
264 | | uint32_t output_rate; |
265 | | uint32_t target_rate; |
266 | | auto_array<float> input; |
267 | | auto_array<float> output; |
268 | | }; |
269 | | |
270 | | uint32_t fill_with_sine(float * buf, uint32_t rate, uint32_t channels, |
271 | | uint32_t frames, uint32_t initial_phase) |
272 | 0 | { |
273 | 0 | uint32_t offset = 0; |
274 | 0 | for (uint32_t i = 0; i < frames; i++) { |
275 | 0 | float p = initial_phase++ / static_cast<float>(rate); |
276 | 0 | for (uint32_t j = 0; j < channels; j++) { |
277 | 0 | buf[offset++] = 0.5 * sin(440. * 2 * PI * p); |
278 | 0 | } |
279 | 0 | } |
280 | 0 | return initial_phase; |
281 | 0 | } |
282 | | |
283 | | long data_cb_resampler(cubeb_stream * /*stm*/, void * user_ptr, |
284 | | const void * input_buffer, void * output_buffer, long frame_count) |
285 | 0 | { |
286 | 0 | osc_state * state = reinterpret_cast<osc_state*>(user_ptr); |
287 | 0 | const float * in = reinterpret_cast<const float*>(input_buffer); |
288 | 0 | float * out = reinterpret_cast<float*>(output_buffer); |
289 | 0 |
|
290 | 0 | state->input.push(in, frame_count * state->input_channels); |
291 | 0 |
|
292 | 0 | /* Check how much output frames we need to write */ |
293 | 0 | uint32_t remaining = state->max_output_phase_index - state->output_phase_index; |
294 | 0 | uint32_t to_write = std::min<uint32_t>(remaining, frame_count); |
295 | 0 | state->output_phase_index = fill_with_sine(out, |
296 | 0 | state->target_rate, |
297 | 0 | state->output_channels, |
298 | 0 | to_write, |
299 | 0 | state->output_phase_index); |
300 | 0 |
|
301 | 0 | return to_write; |
302 | 0 | } |
303 | | |
304 | | template<typename T> |
305 | | bool array_fuzzy_equal(const auto_array<T>& lhs, const auto_array<T>& rhs, T epsi) |
306 | | { |
307 | | uint32_t len = std::min(lhs.length(), rhs.length()); |
308 | | |
309 | | for (uint32_t i = 0; i < len; i++) { |
310 | | if (fabs(lhs.at(i) - rhs.at(i)) > epsi) { |
311 | | std::cout << "not fuzzy equal at index: " << i |
312 | | << " lhs: " << lhs.at(i) << " rhs: " << rhs.at(i) |
313 | | << " delta: " << fabs(lhs.at(i) - rhs.at(i)) |
314 | | << " epsilon: "<< epsi << std::endl; |
315 | | return false; |
316 | | } |
317 | | } |
318 | | return true; |
319 | | } |
320 | | |
321 | | template<typename T> |
322 | | void test_resampler_duplex(uint32_t input_channels, uint32_t output_channels, |
323 | | uint32_t input_rate, uint32_t output_rate, |
324 | | uint32_t target_rate, float chunk_duration) |
325 | 0 | { |
326 | 0 | cubeb_stream_params input_params; |
327 | 0 | cubeb_stream_params output_params; |
328 | 0 | osc_state state; |
329 | 0 |
|
330 | 0 | input_params.format = output_params.format = cubeb_format<T>(); |
331 | 0 | state.input_channels = input_params.channels = input_channels; |
332 | 0 | state.output_channels = output_params.channels = output_channels; |
333 | 0 | input_params.rate = input_rate; |
334 | 0 | state.output_rate = output_params.rate = output_rate; |
335 | 0 | state.target_rate = target_rate; |
336 | 0 | input_params.prefs = output_params.prefs = CUBEB_STREAM_PREF_NONE; |
337 | 0 | long got; |
338 | 0 |
|
339 | 0 | cubeb_resampler * resampler = |
340 | 0 | cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, &output_params, target_rate, |
341 | 0 | data_cb_resampler, (void*)&state, CUBEB_RESAMPLER_QUALITY_VOIP); |
342 | 0 |
|
343 | 0 | long latency = cubeb_resampler_latency(resampler); |
344 | 0 |
|
345 | 0 | const uint32_t duration_s = 2; |
346 | 0 | int32_t duration_frames = duration_s * target_rate; |
347 | 0 | uint32_t input_array_frame_count = ceil(chunk_duration * input_rate / 1000) + ceilf(static_cast<float>(input_rate) / target_rate) * 2; |
348 | 0 | uint32_t output_array_frame_count = chunk_duration * output_rate / 1000; |
349 | 0 | auto_array<float> input_buffer(input_channels * input_array_frame_count); |
350 | 0 | auto_array<float> output_buffer(output_channels * output_array_frame_count); |
351 | 0 | auto_array<float> expected_resampled_input(input_channels * duration_frames); |
352 | 0 | auto_array<float> expected_resampled_output(output_channels * output_rate * duration_s); |
353 | 0 |
|
354 | 0 | state.max_output_phase_index = duration_s * target_rate; |
355 | 0 |
|
356 | 0 | expected_resampled_input.push_silence(input_channels * duration_frames); |
357 | 0 | expected_resampled_output.push_silence(output_channels * output_rate * duration_s); |
358 | 0 |
|
359 | 0 | /* expected output is a 440Hz sine wave at 16kHz */ |
360 | 0 | fill_with_sine(expected_resampled_input.data() + latency, |
361 | 0 | target_rate, input_channels, duration_frames - latency, 0); |
362 | 0 | /* expected output is a 440Hz sine wave at 32kHz */ |
363 | 0 | fill_with_sine(expected_resampled_output.data() + latency, |
364 | 0 | output_rate, output_channels, output_rate * duration_s - latency, 0); |
365 | 0 |
|
366 | 0 | while (state.output_phase_index != state.max_output_phase_index) { |
367 | 0 | uint32_t leftover_samples = input_buffer.length() * input_channels; |
368 | 0 | input_buffer.reserve(input_array_frame_count); |
369 | 0 | state.input_phase_index = fill_with_sine(input_buffer.data() + leftover_samples, |
370 | 0 | input_rate, |
371 | 0 | input_channels, |
372 | 0 | input_array_frame_count - leftover_samples, |
373 | 0 | state.input_phase_index); |
374 | 0 | long input_consumed = input_array_frame_count; |
375 | 0 | input_buffer.set_length(input_array_frame_count); |
376 | 0 |
|
377 | 0 | got = cubeb_resampler_fill(resampler, |
378 | 0 | input_buffer.data(), &input_consumed, |
379 | 0 | output_buffer.data(), output_array_frame_count); |
380 | 0 |
|
381 | 0 | /* handle leftover input */ |
382 | 0 | if (input_array_frame_count != static_cast<uint32_t>(input_consumed)) { |
383 | 0 | input_buffer.pop(nullptr, input_consumed * input_channels); |
384 | 0 | } else { |
385 | 0 | input_buffer.clear(); |
386 | 0 | } |
387 | 0 |
|
388 | 0 | state.output.push(output_buffer.data(), got * state.output_channels); |
389 | 0 | } |
390 | 0 |
|
391 | 0 | dump("input_expected.raw", expected_resampled_input.data(), expected_resampled_input.length()); |
392 | 0 | dump("output_expected.raw", expected_resampled_output.data(), expected_resampled_output.length()); |
393 | 0 | dump("input.raw", state.input.data(), state.input.length()); |
394 | 0 | dump("output.raw", state.output.data(), state.output.length()); |
395 | 0 |
|
396 | 0 | // This is disabled because the latency estimation in the resampler code is |
397 | 0 | // slightly off so we can generate expected vectors. |
398 | 0 | // See https://github.com/kinetiknz/cubeb/issues/93 |
399 | 0 | // ASSERT_TRUE(array_fuzzy_equal(state.input, expected_resampled_input, epsilon<T>(input_rate/target_rate))); |
400 | 0 | // ASSERT_TRUE(array_fuzzy_equal(state.output, expected_resampled_output, epsilon<T>(output_rate/target_rate))); |
401 | 0 |
|
402 | 0 | cubeb_resampler_destroy(resampler); |
403 | 0 | } |
404 | | |
405 | 0 | #define array_size(x) (sizeof(x) / sizeof(x[0])) |
406 | | |
407 | | TEST(cubeb, resampler_one_way) |
408 | 0 | { |
409 | 0 | /* Test one way resamplers */ |
410 | 0 | for (uint32_t channels = 1; channels <= max_channels; channels++) { |
411 | 0 | for (uint32_t source_rate = 0; source_rate < array_size(sample_rates); source_rate++) { |
412 | 0 | for (uint32_t dest_rate = 0; dest_rate < array_size(sample_rates); dest_rate++) { |
413 | 0 | for (uint32_t chunk_duration = min_chunks; chunk_duration < max_chunks; chunk_duration+=chunk_increment) { |
414 | 0 | fprintf(stderr, "one_way: channels: %d, source_rate: %d, dest_rate: %d, chunk_duration: %d\n", |
415 | 0 | channels, sample_rates[source_rate], sample_rates[dest_rate], chunk_duration); |
416 | 0 | test_resampler_one_way<float>(channels, sample_rates[source_rate], |
417 | 0 | sample_rates[dest_rate], chunk_duration); |
418 | 0 | } |
419 | 0 | } |
420 | 0 | } |
421 | 0 | } |
422 | 0 | } |
423 | | |
424 | | TEST(cubeb, DISABLED_resampler_duplex) |
425 | 0 | { |
426 | 0 | for (uint32_t input_channels = 1; input_channels <= max_channels; input_channels++) { |
427 | 0 | for (uint32_t output_channels = 1; output_channels <= max_channels; output_channels++) { |
428 | 0 | for (uint32_t source_rate_input = 0; source_rate_input < array_size(sample_rates); source_rate_input++) { |
429 | 0 | for (uint32_t source_rate_output = 0; source_rate_output < array_size(sample_rates); source_rate_output++) { |
430 | 0 | for (uint32_t dest_rate = 0; dest_rate < array_size(sample_rates); dest_rate++) { |
431 | 0 | for (uint32_t chunk_duration = min_chunks; chunk_duration < max_chunks; chunk_duration+=chunk_increment) { |
432 | 0 | fprintf(stderr, "input channels:%d output_channels:%d input_rate:%d " |
433 | 0 | "output_rate:%d target_rate:%d chunk_ms:%d\n", |
434 | 0 | input_channels, output_channels, |
435 | 0 | sample_rates[source_rate_input], |
436 | 0 | sample_rates[source_rate_output], |
437 | 0 | sample_rates[dest_rate], |
438 | 0 | chunk_duration); |
439 | 0 | test_resampler_duplex<float>(input_channels, output_channels, |
440 | 0 | sample_rates[source_rate_input], |
441 | 0 | sample_rates[source_rate_output], |
442 | 0 | sample_rates[dest_rate], |
443 | 0 | chunk_duration); |
444 | 0 | } |
445 | 0 | } |
446 | 0 | } |
447 | 0 | } |
448 | 0 | } |
449 | 0 | } |
450 | 0 | } |
451 | | |
452 | | TEST(cubeb, resampler_delay_line) |
453 | 0 | { |
454 | 0 | for (uint32_t channel = 1; channel <= 2; channel++) { |
455 | 0 | for (uint32_t delay_frames = 4; delay_frames <= 40; delay_frames+=chunk_increment) { |
456 | 0 | for (uint32_t chunk_size = 10; chunk_size <= 30; chunk_size++) { |
457 | 0 | fprintf(stderr, "channel: %d, delay_frames: %d, chunk_size: %d\n", |
458 | 0 | channel, delay_frames, chunk_size); |
459 | 0 | test_delay_lines(delay_frames, channel, chunk_size); |
460 | 0 | } |
461 | 0 | } |
462 | 0 | } |
463 | 0 | } |
464 | | |
465 | | long test_output_only_noop_data_cb(cubeb_stream * /*stm*/, void * /*user_ptr*/, |
466 | | const void * input_buffer, |
467 | | void * output_buffer, long frame_count) |
468 | 0 | { |
469 | 0 | EXPECT_TRUE(output_buffer); |
470 | 0 | EXPECT_TRUE(!input_buffer); |
471 | 0 | return frame_count; |
472 | 0 | } |
473 | | |
474 | | TEST(cubeb, resampler_output_only_noop) |
475 | 0 | { |
476 | 0 | cubeb_stream_params output_params; |
477 | 0 | int target_rate; |
478 | 0 |
|
479 | 0 | output_params.rate = 44100; |
480 | 0 | output_params.channels = 1; |
481 | 0 | output_params.format = CUBEB_SAMPLE_FLOAT32NE; |
482 | 0 | target_rate = output_params.rate; |
483 | 0 |
|
484 | 0 | cubeb_resampler * resampler = |
485 | 0 | cubeb_resampler_create((cubeb_stream*)nullptr, nullptr, &output_params, target_rate, |
486 | 0 | test_output_only_noop_data_cb, nullptr, |
487 | 0 | CUBEB_RESAMPLER_QUALITY_VOIP); |
488 | 0 |
|
489 | 0 | const long out_frames = 128; |
490 | 0 | float out_buffer[out_frames]; |
491 | 0 | long got; |
492 | 0 |
|
493 | 0 | got = cubeb_resampler_fill(resampler, nullptr, nullptr, |
494 | 0 | out_buffer, out_frames); |
495 | 0 |
|
496 | 0 | ASSERT_EQ(got, out_frames); |
497 | 0 |
|
498 | 0 | cubeb_resampler_destroy(resampler); |
499 | 0 | } |
500 | | |
501 | | long test_drain_data_cb(cubeb_stream * /*stm*/, void * user_ptr, |
502 | | const void * input_buffer, |
503 | | void * output_buffer, long frame_count) |
504 | 0 | { |
505 | 0 | EXPECT_TRUE(output_buffer); |
506 | 0 | EXPECT_TRUE(!input_buffer); |
507 | 0 | auto cb_count = static_cast<int *>(user_ptr); |
508 | 0 | (*cb_count)++; |
509 | 0 | return frame_count - 1; |
510 | 0 | } |
511 | | |
512 | | TEST(cubeb, resampler_drain) |
513 | 0 | { |
514 | 0 | cubeb_stream_params output_params; |
515 | 0 | int target_rate; |
516 | 0 |
|
517 | 0 | output_params.rate = 44100; |
518 | 0 | output_params.channels = 1; |
519 | 0 | output_params.format = CUBEB_SAMPLE_FLOAT32NE; |
520 | 0 | target_rate = 48000; |
521 | 0 | int cb_count = 0; |
522 | 0 |
|
523 | 0 | cubeb_resampler * resampler = |
524 | 0 | cubeb_resampler_create((cubeb_stream*)nullptr, nullptr, &output_params, target_rate, |
525 | 0 | test_drain_data_cb, &cb_count, |
526 | 0 | CUBEB_RESAMPLER_QUALITY_VOIP); |
527 | 0 |
|
528 | 0 | const long out_frames = 128; |
529 | 0 | float out_buffer[out_frames]; |
530 | 0 | long got; |
531 | 0 |
|
532 | 0 | do { |
533 | 0 | got = cubeb_resampler_fill(resampler, nullptr, nullptr, |
534 | 0 | out_buffer, out_frames); |
535 | 0 | } while (got == out_frames); |
536 | 0 |
|
537 | 0 | /* The callback should be called once but not again after returning < |
538 | 0 | * frame_count. */ |
539 | 0 | ASSERT_EQ(cb_count, 1); |
540 | 0 |
|
541 | 0 | cubeb_resampler_destroy(resampler); |
542 | 0 | } |
543 | | |
544 | | // gtest does not support using ASSERT_EQ and friend in a function that returns |
545 | | // a value. |
546 | | void check_output(const void * input_buffer, void * output_buffer, long frame_count) |
547 | 0 | { |
548 | 0 | ASSERT_EQ(input_buffer, nullptr); |
549 | 0 | ASSERT_EQ(frame_count, 256); |
550 | 0 | ASSERT_TRUE(!!output_buffer); |
551 | 0 | } |
552 | | |
553 | | long cb_passthrough_resampler_output(cubeb_stream * /*stm*/, void * /*user_ptr*/, |
554 | | const void * input_buffer, |
555 | | void * output_buffer, long frame_count) |
556 | 0 | { |
557 | 0 | check_output(input_buffer, output_buffer, frame_count); |
558 | 0 | return frame_count; |
559 | 0 | } |
560 | | |
561 | | TEST(cubeb, resampler_passthrough_output_only) |
562 | 0 | { |
563 | 0 | // Test that the passthrough resampler works when there is only an output stream. |
564 | 0 | cubeb_stream_params output_params; |
565 | 0 |
|
566 | 0 | const size_t output_channels = 2; |
567 | 0 | output_params.channels = output_channels; |
568 | 0 | output_params.rate = 44100; |
569 | 0 | output_params.format = CUBEB_SAMPLE_FLOAT32NE; |
570 | 0 | int target_rate = output_params.rate; |
571 | 0 |
|
572 | 0 | cubeb_resampler * resampler = |
573 | 0 | cubeb_resampler_create((cubeb_stream*)nullptr, nullptr, &output_params, |
574 | 0 | target_rate, cb_passthrough_resampler_output, nullptr, |
575 | 0 | CUBEB_RESAMPLER_QUALITY_VOIP); |
576 | 0 |
|
577 | 0 | float output_buffer[output_channels * 256]; |
578 | 0 |
|
579 | 0 | long got; |
580 | 0 | for (uint32_t i = 0; i < 30; i++) { |
581 | 0 | got = cubeb_resampler_fill(resampler, nullptr, nullptr, output_buffer, 256); |
582 | 0 | ASSERT_EQ(got, 256); |
583 | 0 | } |
584 | 0 |
|
585 | 0 | cubeb_resampler_destroy(resampler); |
586 | 0 | } |
587 | | |
588 | | // gtest does not support using ASSERT_EQ and friend in a function that returns |
589 | | // a value. |
590 | | void check_input(const void * input_buffer, void * output_buffer, long frame_count) |
591 | 0 | { |
592 | 0 | ASSERT_EQ(output_buffer, nullptr); |
593 | 0 | ASSERT_EQ(frame_count, 256); |
594 | 0 | ASSERT_TRUE(!!input_buffer); |
595 | 0 | } |
596 | | |
597 | | long cb_passthrough_resampler_input(cubeb_stream * /*stm*/, void * /*user_ptr*/, |
598 | | const void * input_buffer, |
599 | | void * output_buffer, long frame_count) |
600 | 0 | { |
601 | 0 | check_input(input_buffer, output_buffer, frame_count); |
602 | 0 | return frame_count; |
603 | 0 | } |
604 | | |
605 | | TEST(cubeb, resampler_passthrough_input_only) |
606 | 0 | { |
607 | 0 | // Test that the passthrough resampler works when there is only an output stream. |
608 | 0 | cubeb_stream_params input_params; |
609 | 0 |
|
610 | 0 | const size_t input_channels = 2; |
611 | 0 | input_params.channels = input_channels; |
612 | 0 | input_params.rate = 44100; |
613 | 0 | input_params.format = CUBEB_SAMPLE_FLOAT32NE; |
614 | 0 | int target_rate = input_params.rate; |
615 | 0 |
|
616 | 0 | cubeb_resampler * resampler = |
617 | 0 | cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, nullptr, |
618 | 0 | target_rate, cb_passthrough_resampler_input, nullptr, |
619 | 0 | CUBEB_RESAMPLER_QUALITY_VOIP); |
620 | 0 |
|
621 | 0 | float input_buffer[input_channels * 256]; |
622 | 0 |
|
623 | 0 | long got; |
624 | 0 | for (uint32_t i = 0; i < 30; i++) { |
625 | 0 | long int frames = 256; |
626 | 0 | got = cubeb_resampler_fill(resampler, input_buffer, &frames, nullptr, 0); |
627 | 0 | ASSERT_EQ(got, 256); |
628 | 0 | } |
629 | 0 |
|
630 | 0 | cubeb_resampler_destroy(resampler); |
631 | 0 | } |
632 | | |
633 | | template<typename T> |
634 | | long seq(T* array, int stride, long start, long count) |
635 | 0 | { |
636 | 0 | uint32_t output_idx = 0; |
637 | 0 | for(int i = 0; i < count; i++) { |
638 | 0 | for (int j = 0; j < stride; j++) { |
639 | 0 | array[output_idx + j] = static_cast<T>(start + i); |
640 | 0 | } |
641 | 0 | output_idx += stride; |
642 | 0 | } |
643 | 0 | return start + count; |
644 | 0 | } |
645 | | |
646 | | template<typename T> |
647 | | void is_seq(T * array, int stride, long count, long expected_start) |
648 | 0 | { |
649 | 0 | uint32_t output_index = 0; |
650 | 0 | for (long i = 0; i < count; i++) { |
651 | 0 | for (int j = 0; j < stride; j++) { |
652 | 0 | ASSERT_EQ(array[output_index + j], expected_start + i); |
653 | 0 | } |
654 | 0 | output_index += stride; |
655 | 0 | } |
656 | 0 | } |
657 | | |
658 | | template<typename T> |
659 | | void is_not_seq(T * array, int stride, long count, long expected_start) |
660 | 0 | { |
661 | 0 | uint32_t output_index = 0; |
662 | 0 | for (long i = 0; i < count; i++) { |
663 | 0 | for (int j = 0; j < stride; j++) { |
664 | 0 | ASSERT_NE(array[output_index + j], expected_start + i); |
665 | 0 | } |
666 | 0 | output_index += stride; |
667 | 0 | } |
668 | 0 | } |
669 | | |
670 | | struct closure { |
671 | | int input_channel_count; |
672 | | }; |
673 | | |
674 | | // gtest does not support using ASSERT_EQ and friend in a function that returns |
675 | | // a value. |
676 | | template<typename T> |
677 | | void check_duplex(const T * input_buffer, |
678 | | T * output_buffer, long frame_count, |
679 | | int input_channel_count) |
680 | 0 | { |
681 | 0 | ASSERT_EQ(frame_count, 256); |
682 | 0 | // Silence scan-build warning. |
683 | 0 | ASSERT_TRUE(!!output_buffer); assert(output_buffer); |
684 | 0 | ASSERT_TRUE(!!input_buffer); assert(input_buffer); |
685 | 0 |
|
686 | 0 | int output_index = 0; |
687 | 0 | int input_index = 0; |
688 | 0 | for (int i = 0; i < frame_count; i++) { |
689 | 0 | // output is two channels, input one or two channels. |
690 | 0 | if (input_channel_count == 1) { |
691 | 0 | output_buffer[output_index] = output_buffer[output_index + 1] = input_buffer[i]; |
692 | 0 | } else if (input_channel_count == 2) { |
693 | 0 | output_buffer[output_index] = input_buffer[input_index]; |
694 | 0 | output_buffer[output_index + 1] = input_buffer[input_index + 1]; |
695 | 0 | } |
696 | 0 | output_index += 2; |
697 | 0 | input_index += input_channel_count; |
698 | 0 | } |
699 | 0 | } |
700 | | |
701 | | long cb_passthrough_resampler_duplex(cubeb_stream * /*stm*/, void * user_ptr, |
702 | | const void * input_buffer, |
703 | | void * output_buffer, long frame_count) |
704 | 0 | { |
705 | 0 | closure * c = reinterpret_cast<closure*>(user_ptr); |
706 | 0 | check_duplex<float>(static_cast<const float*>(input_buffer), |
707 | 0 | static_cast<float*>(output_buffer), |
708 | 0 | frame_count, c->input_channel_count); |
709 | 0 | return frame_count; |
710 | 0 | } |
711 | | |
712 | | |
713 | | TEST(cubeb, resampler_passthrough_duplex_callback_reordering) |
714 | 0 | { |
715 | 0 | // Test that when pre-buffering on resampler creation, we can survive an input |
716 | 0 | // callback being delayed. |
717 | 0 |
|
718 | 0 | cubeb_stream_params input_params; |
719 | 0 | cubeb_stream_params output_params; |
720 | 0 |
|
721 | 0 | const int input_channels = 1; |
722 | 0 | const int output_channels = 2; |
723 | 0 |
|
724 | 0 | input_params.channels = input_channels; |
725 | 0 | input_params.rate = 44100; |
726 | 0 | input_params.format = CUBEB_SAMPLE_FLOAT32NE; |
727 | 0 |
|
728 | 0 | output_params.channels = output_channels; |
729 | 0 | output_params.rate = input_params.rate; |
730 | 0 | output_params.format = CUBEB_SAMPLE_FLOAT32NE; |
731 | 0 |
|
732 | 0 | int target_rate = input_params.rate; |
733 | 0 |
|
734 | 0 | closure c; |
735 | 0 | c.input_channel_count = input_channels; |
736 | 0 |
|
737 | 0 | cubeb_resampler * resampler = |
738 | 0 | cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, &output_params, |
739 | 0 | target_rate, cb_passthrough_resampler_duplex, &c, |
740 | 0 | CUBEB_RESAMPLER_QUALITY_VOIP); |
741 | 0 |
|
742 | 0 | const long BUF_BASE_SIZE = 256; |
743 | 0 | float input_buffer_prebuffer[input_channels * BUF_BASE_SIZE * 2]; |
744 | 0 | float input_buffer_glitch[input_channels * BUF_BASE_SIZE * 2]; |
745 | 0 | float input_buffer_normal[input_channels * BUF_BASE_SIZE]; |
746 | 0 | float output_buffer[output_channels * BUF_BASE_SIZE]; |
747 | 0 |
|
748 | 0 | long seq_idx = 0; |
749 | 0 | long output_seq_idx = 0; |
750 | 0 |
|
751 | 0 | long prebuffer_frames = ARRAY_LENGTH(input_buffer_prebuffer) / input_params.channels; |
752 | 0 | seq_idx = seq(input_buffer_prebuffer, input_channels, seq_idx, |
753 | 0 | prebuffer_frames); |
754 | 0 |
|
755 | 0 | long got = cubeb_resampler_fill(resampler, input_buffer_prebuffer, &prebuffer_frames, |
756 | 0 | output_buffer, BUF_BASE_SIZE); |
757 | 0 |
|
758 | 0 | output_seq_idx += BUF_BASE_SIZE; |
759 | 0 |
|
760 | 0 | // prebuffer_frames will hold the frames used by the resampler. |
761 | 0 | ASSERT_EQ(prebuffer_frames, BUF_BASE_SIZE); |
762 | 0 | ASSERT_EQ(got, BUF_BASE_SIZE); |
763 | 0 |
|
764 | 0 | for (uint32_t i = 0; i < 300; i++) { |
765 | 0 | long int frames = BUF_BASE_SIZE; |
766 | 0 | // Simulate that sometimes, we don't have the input callback on time |
767 | 0 | if (i != 0 && (i % 100) == 0) { |
768 | 0 | long zero = 0; |
769 | 0 | got = cubeb_resampler_fill(resampler, input_buffer_normal /* unused here */, |
770 | 0 | &zero, output_buffer, BUF_BASE_SIZE); |
771 | 0 | is_seq(output_buffer, 2, BUF_BASE_SIZE, output_seq_idx); |
772 | 0 | output_seq_idx += BUF_BASE_SIZE; |
773 | 0 | } else if (i != 0 && (i % 100) == 1) { |
774 | 0 | // if this is the case, the on the next iteration, we'll have twice the |
775 | 0 | // amount of input frames |
776 | 0 | seq_idx = seq(input_buffer_glitch, input_channels, seq_idx, BUF_BASE_SIZE * 2); |
777 | 0 | frames = 2 * BUF_BASE_SIZE; |
778 | 0 | got = cubeb_resampler_fill(resampler, input_buffer_glitch, &frames, output_buffer, BUF_BASE_SIZE); |
779 | 0 | is_seq(output_buffer, 2, BUF_BASE_SIZE, output_seq_idx); |
780 | 0 | output_seq_idx += BUF_BASE_SIZE; |
781 | 0 | } else { |
782 | 0 | // normal case |
783 | 0 | seq_idx = seq(input_buffer_normal, input_channels, seq_idx, BUF_BASE_SIZE); |
784 | 0 | long normal_input_frame_count = 256; |
785 | 0 | got = cubeb_resampler_fill(resampler, input_buffer_normal, &normal_input_frame_count, output_buffer, BUF_BASE_SIZE); |
786 | 0 | is_seq(output_buffer, 2, BUF_BASE_SIZE, output_seq_idx); |
787 | 0 | output_seq_idx += BUF_BASE_SIZE; |
788 | 0 | } |
789 | 0 | ASSERT_EQ(got, BUF_BASE_SIZE); |
790 | 0 | } |
791 | 0 |
|
792 | 0 | cubeb_resampler_destroy(resampler); |
793 | 0 | } |
794 | | |
795 | | // Artificially simulate output thread underruns, |
796 | | // by building up artificial delay in the input. |
797 | | // Check that the frame drop logic kicks in. |
798 | | TEST(cubeb, resampler_drift_drop_data) |
799 | 0 | { |
800 | 0 | for (uint32_t input_channels = 1; input_channels < 3; input_channels++) { |
801 | 0 | cubeb_stream_params input_params; |
802 | 0 | cubeb_stream_params output_params; |
803 | 0 |
|
804 | 0 | const int output_channels = 2; |
805 | 0 | const int sample_rate = 44100; |
806 | 0 |
|
807 | 0 | input_params.channels = input_channels; |
808 | 0 | input_params.rate = sample_rate; |
809 | 0 | input_params.format = CUBEB_SAMPLE_FLOAT32NE; |
810 | 0 |
|
811 | 0 | output_params.channels = output_channels; |
812 | 0 | output_params.rate = sample_rate; |
813 | 0 | output_params.format = CUBEB_SAMPLE_FLOAT32NE; |
814 | 0 |
|
815 | 0 | int target_rate = input_params.rate; |
816 | 0 |
|
817 | 0 | closure c; |
818 | 0 | c.input_channel_count = input_channels; |
819 | 0 |
|
820 | 0 | cubeb_resampler * resampler = |
821 | 0 | cubeb_resampler_create((cubeb_stream*)nullptr, &input_params, &output_params, |
822 | 0 | target_rate, cb_passthrough_resampler_duplex, &c, |
823 | 0 | CUBEB_RESAMPLER_QUALITY_VOIP); |
824 | 0 |
|
825 | 0 | const long BUF_BASE_SIZE = 256; |
826 | 0 |
|
827 | 0 | // The factor by which the deadline is missed. This is intentionally |
828 | 0 | // kind of large to trigger the frame drop quickly. In real life, multiple |
829 | 0 | // smaller under-runs would accumulate. |
830 | 0 | const long UNDERRUN_FACTOR = 10; |
831 | 0 | // Number buffer used for pre-buffering, that some backends do. |
832 | 0 | const long PREBUFFER_FACTOR = 2; |
833 | 0 |
|
834 | 0 | std::vector<float> input_buffer_prebuffer(input_channels * BUF_BASE_SIZE * PREBUFFER_FACTOR); |
835 | 0 | std::vector<float> input_buffer_glitch(input_channels * BUF_BASE_SIZE * UNDERRUN_FACTOR); |
836 | 0 | std::vector<float> input_buffer_normal(input_channels * BUF_BASE_SIZE); |
837 | 0 | std::vector<float> output_buffer(output_channels * BUF_BASE_SIZE); |
838 | 0 |
|
839 | 0 | long seq_idx = 0; |
840 | 0 | long output_seq_idx = 0; |
841 | 0 |
|
842 | 0 | long prebuffer_frames = input_buffer_prebuffer.size() / input_params.channels; |
843 | 0 | seq_idx = seq(input_buffer_prebuffer.data(), input_channels, seq_idx, |
844 | 0 | prebuffer_frames); |
845 | 0 |
|
846 | 0 | long got = cubeb_resampler_fill(resampler, input_buffer_prebuffer.data(), &prebuffer_frames, |
847 | 0 | output_buffer.data(), BUF_BASE_SIZE); |
848 | 0 |
|
849 | 0 | output_seq_idx += BUF_BASE_SIZE; |
850 | 0 |
|
851 | 0 | // prebuffer_frames will hold the frames used by the resampler. |
852 | 0 | ASSERT_EQ(prebuffer_frames, BUF_BASE_SIZE); |
853 | 0 | ASSERT_EQ(got, BUF_BASE_SIZE); |
854 | 0 |
|
855 | 0 | for (uint32_t i = 0; i < 300; i++) { |
856 | 0 | long int frames = BUF_BASE_SIZE; |
857 | 0 | if (i != 0 && (i % 100) == 1) { |
858 | 0 | // Once in a while, the output thread misses its deadline. |
859 | 0 | // The input thread still produces data, so it ends up accumulating. Simulate this by providing a |
860 | 0 | // much bigger input buffer. Check that the sequence is now unaligned, meaning we've dropped data |
861 | 0 | // to keep everything in sync. |
862 | 0 | seq_idx = seq(input_buffer_glitch.data(), input_channels, seq_idx, BUF_BASE_SIZE * UNDERRUN_FACTOR); |
863 | 0 | frames = BUF_BASE_SIZE * UNDERRUN_FACTOR; |
864 | 0 | got = cubeb_resampler_fill(resampler, input_buffer_glitch.data(), &frames, output_buffer.data(), BUF_BASE_SIZE); |
865 | 0 | is_seq(output_buffer.data(), 2, BUF_BASE_SIZE, output_seq_idx); |
866 | 0 | output_seq_idx += BUF_BASE_SIZE; |
867 | 0 | } |
868 | 0 | else if (i != 0 && (i % 100) == 2) { |
869 | 0 | // On the next iteration, the sequence should be broken |
870 | 0 | seq_idx = seq(input_buffer_normal.data(), input_channels, seq_idx, BUF_BASE_SIZE); |
871 | 0 | long normal_input_frame_count = 256; |
872 | 0 | got = cubeb_resampler_fill(resampler, input_buffer_normal.data(), &normal_input_frame_count, output_buffer.data(), BUF_BASE_SIZE); |
873 | 0 | is_not_seq(output_buffer.data(), output_channels, BUF_BASE_SIZE, output_seq_idx); |
874 | 0 | // Reclock so that we can use is_seq again. |
875 | 0 | output_seq_idx = output_buffer[BUF_BASE_SIZE * output_channels - 1] + 1; |
876 | 0 | } |
877 | 0 | else { |
878 | 0 | // normal case |
879 | 0 | seq_idx = seq(input_buffer_normal.data(), input_channels, seq_idx, BUF_BASE_SIZE); |
880 | 0 | long normal_input_frame_count = 256; |
881 | 0 | got = cubeb_resampler_fill(resampler, input_buffer_normal.data(), &normal_input_frame_count, output_buffer.data(), BUF_BASE_SIZE); |
882 | 0 | is_seq(output_buffer.data(), output_channels, BUF_BASE_SIZE, output_seq_idx); |
883 | 0 | output_seq_idx += BUF_BASE_SIZE; |
884 | 0 | } |
885 | 0 | ASSERT_EQ(got, BUF_BASE_SIZE); |
886 | 0 | } |
887 | 0 |
|
888 | 0 | cubeb_resampler_destroy(resampler); |
889 | 0 | } |
890 | 0 | } |
891 | | |