master
c 619 lines 18.1 KB
Raw
1 /* SPDX-License-Identifier: GPL-2.0-or-later */
2
3 #include "qemu/osdep.h"
4 #include "qemu/config-file.h"
5 #include "qemu/cutils.h"
6 #include "qemu/help_option.h"
7 #include "qemu/module.h"
8 #include "qemu/main-loop.h"
9 #include "qemu/audio.h"
10 #include "qemu/log.h"
11 #include "qapi/error.h"
12 #include "trace/control.h"
13 #include "glib.h"
14
15 #include "audio/audio_int.h"
16
17 #include <math.h>
18 #ifdef CONFIG_SDL
19 /*
20 * SDL insists on wrapping the main() function with its own implementation on
21 * some platforms; it does so via a macro that renames our main function, so
22 * <SDL.h> must be #included here even with no SDL code called from this file.
23 */
24 #include <SDL.h>
25 #endif
26
27 #define SAMPLE_RATE 44100
28 #define CHANNELS 2
29 #define DURATION_SECS 2
30 #define FREQUENCY 440.0
31 #define BUFFER_FRAMES 1024
32 #define TIMEOUT_SECS (DURATION_SECS + 1)
33
34 /* Command-line options */
35 static gchar *opt_audiodev;
36 static gchar *opt_trace;
37
38 static GOptionEntry test_options[] = {
39 { "audiodev", 'a', 0, G_OPTION_ARG_STRING, &opt_audiodev,
40 "Audio device spec (e.g., none or pa,out.buffer-length=50000)", "DEV" },
41 { "trace", 'T', 0, G_OPTION_ARG_STRING, &opt_trace,
42 "Trace options (e.g., 'pw_*')", "TRACE" },
43 { NULL }
44 };
45
46 #define TEST_AUDIODEV_ID "test"
47
48 typedef struct TestSineState {
49 AudioBackend *be;
50 SWVoiceOut *voice;
51 int64_t total_frames;
52 int64_t frames_written;
53 } TestSineState;
54
55 /* Default audio settings for tests */
56 static const struct audsettings default_test_settings = {
57 .freq = SAMPLE_RATE,
58 .nchannels = CHANNELS,
59 .fmt = AUDIO_FORMAT_S16,
60 .big_endian = false,
61 };
62
63 static void dummy_audio_callback(void *opaque, int avail)
64 {
65 }
66
67 static AudioBackend *get_test_audio_backend(void)
68 {
69 AudioBackend *be;
70 Error *err = NULL;
71
72 if (opt_audiodev) {
73 be = audio_be_by_name(TEST_AUDIODEV_ID, &err);
74 } else {
75 be = audio_get_default_audio_be(&err);
76 }
77
78 if (err) {
79 g_error("%s", error_get_pretty(err));
80 error_free(err);
81 exit(1);
82 }
83 g_assert_nonnull(be);
84 return be;
85 }
86
87 /*
88 * Helper functions for opening test voices with default settings.
89 * These reduce boilerplate in test functions.
90 */
91 static SWVoiceOut *open_test_voice_out(AudioBackend *be, const char *name,
92 void *opaque, audio_callback_fn cb)
93 {
94 struct audsettings as = default_test_settings;
95 SWVoiceOut *voice;
96
97 voice = audio_be_open_out(be, NULL, name, opaque, cb, &as);
98 g_assert_nonnull(voice);
99 return voice;
100 }
101
102 static SWVoiceIn *open_test_voice_in(AudioBackend *be, const char *name,
103 void *opaque, audio_callback_fn cb)
104 {
105 struct audsettings as = default_test_settings;
106
107 return audio_be_open_in(be, NULL, name, opaque, cb, &as);
108 }
109
110 /*
111 * Generate 440Hz sine wave samples into buffer.
112 */
113 static void generate_sine_samples(int16_t *buffer, int frames,
114 int64_t start_frame)
115 {
116 for (int i = 0; i < frames; i++) {
117 double t = (double)(start_frame + i) / SAMPLE_RATE;
118 double sample = sin(2.0 * M_PI * FREQUENCY * t);
119 int16_t s = (int16_t)(sample * 32767.0);
120
121 buffer[i * 2] = s; /* left channel */
122 buffer[i * 2 + 1] = s; /* right channel */
123 }
124 }
125
126 static void test_sine_callback(void *opaque, int avail)
127 {
128 TestSineState *s = opaque;
129 int16_t buffer[BUFFER_FRAMES * CHANNELS];
130 int frames_remaining;
131 int frames_to_write;
132 size_t bytes_written;
133
134 frames_remaining = s->total_frames - s->frames_written;
135 if (frames_remaining <= 0) {
136 return;
137 }
138
139 frames_to_write = avail / (sizeof(int16_t) * CHANNELS);
140 frames_to_write = MIN(frames_to_write, BUFFER_FRAMES);
141 frames_to_write = MIN(frames_to_write, frames_remaining);
142
143 generate_sine_samples(buffer, frames_to_write, s->frames_written);
144
145 bytes_written = audio_be_write(s->be, s->voice, buffer,
146 frames_to_write * sizeof(int16_t) * CHANNELS);
147 s->frames_written += bytes_written / (sizeof(int16_t) * CHANNELS);
148 }
149
150
151 static void test_audio_out_sine_wave(void)
152 {
153 TestSineState state = {0};
154 int64_t start_time;
155 int64_t elapsed_ms;
156
157 state.be = get_test_audio_backend();
158 state.total_frames = SAMPLE_RATE * DURATION_SECS;
159 state.frames_written = 0;
160
161 g_test_message("Opening audio output...");
162 state.voice = open_test_voice_out(state.be, "test-sine",
163 &state, test_sine_callback);
164
165 g_test_message("Playing 440Hz sine wave for %d seconds...", DURATION_SECS);
166 audio_be_set_active_out(state.be, state.voice, true);
167
168 /*
169 * Run the audio subsystem until all frames are written or timeout.
170 */
171 start_time = g_get_monotonic_time();
172 while (state.frames_written < state.total_frames) {
173 audio_run(AUDIO_MIXENG_BACKEND(state.be), "test");
174 main_loop_wait(true);
175
176 elapsed_ms = (g_get_monotonic_time() - start_time) / 1000;
177 if (elapsed_ms > TIMEOUT_SECS * 1000) {
178 g_test_message("Timeout waiting for audio to complete");
179 break;
180 }
181
182 g_usleep(G_USEC_PER_SEC / 100); /* 10ms */
183 }
184
185 g_test_message("Wrote %" PRId64 " frames (%.2f seconds)",
186 state.frames_written,
187 (double)state.frames_written / SAMPLE_RATE);
188
189 g_assert_cmpint(state.frames_written, ==, state.total_frames);
190
191 audio_be_set_active_out(state.be, state.voice, false);
192 audio_be_close_out(state.be, state.voice);
193 }
194
195 static void test_audio_prio_list(void)
196 {
197 g_autofree gchar *backends = NULL;
198 GString *str = g_string_new(NULL);
199 bool has_none = false;
200
201 for (int i = 0; audio_prio_list[i]; i++) {
202 if (i > 0) {
203 g_string_append_c(str, ' ');
204 }
205 g_string_append(str, audio_prio_list[i]);
206
207 if (g_strcmp0(audio_prio_list[i], "none") == 0) {
208 has_none = true;
209 }
210 }
211
212 backends = g_string_free(str, FALSE);
213 g_test_message("Available backends: %s", backends);
214
215 /* The 'none' backend should always be available */
216 g_assert_true(has_none);
217 }
218
219 static void test_audio_out_active_state(void)
220 {
221 AudioBackend *be;
222 SWVoiceOut *voice;
223
224 be = get_test_audio_backend();
225 voice = open_test_voice_out(be, "test-active", NULL, dummy_audio_callback);
226
227 g_assert_false(audio_be_is_active_out(be, voice));
228
229 audio_be_set_active_out(be, voice, true);
230 g_assert_true(audio_be_is_active_out(be, voice));
231
232 audio_be_set_active_out(be, voice, false);
233 g_assert_false(audio_be_is_active_out(be, voice));
234
235 audio_be_close_out(be, voice);
236 }
237
238 static void test_audio_out_buffer_size(void)
239 {
240 AudioBackend *be;
241 SWVoiceOut *voice;
242 int buffer_size;
243
244 be = get_test_audio_backend();
245 voice = open_test_voice_out(be, "test-buffer", NULL, dummy_audio_callback);
246
247 buffer_size = audio_be_get_buffer_size_out(be, voice);
248 g_test_message("Buffer size: %d bytes", buffer_size);
249 g_assert_cmpint(buffer_size, >, 0);
250
251 audio_be_close_out(be, voice);
252
253 g_assert_cmpint(audio_be_get_buffer_size_out(be, NULL), ==, 0);
254 }
255
256 static void test_audio_out_volume(void)
257 {
258 AudioBackend *be;
259 SWVoiceOut *voice;
260 Volume vol;
261
262 be = get_test_audio_backend();
263 voice = open_test_voice_out(be, "test-volume", NULL, dummy_audio_callback);
264
265 vol = (Volume){ .mute = false, .channels = 2, .vol = {255, 255} };
266 audio_be_set_volume_out(be, voice, &vol);
267
268 vol = (Volume){ .mute = true, .channels = 2, .vol = {255, 255} };
269 audio_be_set_volume_out(be, voice, &vol);
270
271 vol = (Volume){ .mute = false, .channels = 2, .vol = {128, 128} };
272 audio_be_set_volume_out(be, voice, &vol);
273
274 audio_be_close_out(be, voice);
275 }
276
277 static void test_audio_in_active_state(void)
278 {
279 AudioBackend *be;
280 SWVoiceIn *voice;
281
282 be = get_test_audio_backend();
283 voice = open_test_voice_in(be, "test-in-active", NULL, dummy_audio_callback);
284 if (!voice) {
285 g_test_skip("The backend may not support input");
286 return;
287 }
288
289 g_assert_false(audio_be_is_active_in(be, voice));
290
291 audio_be_set_active_in(be, voice, true);
292 g_assert_true(audio_be_is_active_in(be, voice));
293
294 audio_be_set_active_in(be, voice, false);
295 g_assert_false(audio_be_is_active_in(be, voice));
296
297 audio_be_close_in(be, voice);
298 }
299
300 static void test_audio_in_volume(void)
301 {
302 AudioBackend *be;
303 SWVoiceIn *voice;
304 Volume vol;
305
306 be = get_test_audio_backend();
307 voice = open_test_voice_in(be, "test-in-volume", NULL, dummy_audio_callback);
308 if (!voice) {
309 g_test_skip("The backend may not support input");
310 return;
311 }
312
313 vol = (Volume){ .mute = false, .channels = 2, .vol = {255, 255} };
314 audio_be_set_volume_in(be, voice, &vol);
315
316 vol = (Volume){ .mute = true, .channels = 2, .vol = {255, 255} };
317 audio_be_set_volume_in(be, voice, &vol);
318
319 audio_be_close_in(be, voice);
320 }
321
322
323 /* Capture test state */
324 #define CAPTURE_BUFFER_FRAMES (SAMPLE_RATE / 10) /* 100ms of audio */
325 #define CAPTURE_BUFFER_SIZE (CAPTURE_BUFFER_FRAMES * CHANNELS * sizeof(int16_t))
326
327 typedef struct TestCaptureState {
328 bool notify_called;
329 bool capture_called;
330 bool destroy_called;
331 audcnotification_e last_notify;
332 int16_t *captured_samples;
333 size_t captured_bytes;
334 size_t capture_buffer_size;
335 } TestCaptureState;
336
337 static void test_capture_notify(void *opaque, audcnotification_e cmd)
338 {
339 TestCaptureState *s = opaque;
340 s->notify_called = true;
341 s->last_notify = cmd;
342 }
343
344 static void test_capture_capture(void *opaque, const void *buf, int size)
345 {
346 TestCaptureState *s = opaque;
347 size_t bytes_to_copy;
348
349 s->capture_called = true;
350
351 if (!s->captured_samples || s->captured_bytes >= s->capture_buffer_size) {
352 return;
353 }
354
355 bytes_to_copy = MIN(size, s->capture_buffer_size - s->captured_bytes);
356 memcpy((uint8_t *)s->captured_samples + s->captured_bytes, buf, bytes_to_copy);
357 s->captured_bytes += bytes_to_copy;
358 }
359
360 static void test_capture_destroy(void *opaque)
361 {
362 TestCaptureState *s = opaque;
363 s->destroy_called = true;
364 }
365
366 /*
367 * Compare captured audio with expected sine wave.
368 * Returns the number of matching samples (within tolerance).
369 */
370 static int compare_sine_samples(const int16_t *captured, int frames,
371 int64_t start_frame, int tolerance)
372 {
373 int matching = 0;
374
375 for (int i = 0; i < frames; i++) {
376 double t = (double)(start_frame + i) / SAMPLE_RATE;
377 double sample = sin(2.0 * M_PI * FREQUENCY * t);
378 int16_t expected = (int16_t)(sample * 32767.0);
379
380 /* Check left channel */
381 if (abs(captured[i * 2] - expected) <= tolerance) {
382 matching++;
383 }
384 /* Check right channel */
385 if (abs(captured[i * 2 + 1] - expected) <= tolerance) {
386 matching++;
387 }
388 }
389
390 return matching;
391 }
392
393 static void test_audio_capture(void)
394 {
395 AudioBackend *be;
396 CaptureVoiceOut *cap;
397 SWVoiceOut *voice;
398 TestCaptureState state = {0};
399 TestSineState sine_state = {0};
400 struct audsettings as = default_test_settings;
401 struct audio_capture_ops ops = {
402 .notify = test_capture_notify,
403 .capture = test_capture_capture,
404 .destroy = test_capture_destroy,
405 };
406 int64_t start_time;
407 int64_t elapsed_ms;
408 int captured_frames;
409 int matching_samples;
410 int total_samples;
411 double match_ratio;
412
413 be = get_test_audio_backend();
414
415 state.captured_samples = g_malloc0(CAPTURE_BUFFER_SIZE);
416 state.captured_bytes = 0;
417 state.capture_buffer_size = CAPTURE_BUFFER_SIZE;
418
419 cap = audio_be_add_capture(be, &as, &ops, &state);
420 g_assert_nonnull(cap);
421
422 sine_state.be = be;
423 sine_state.total_frames = CAPTURE_BUFFER_FRAMES;
424 sine_state.frames_written = 0;
425
426 voice = open_test_voice_out(be, "test-capture-sine",
427 &sine_state, test_sine_callback);
428 sine_state.voice = voice;
429
430 audio_be_set_active_out(be, voice, true);
431
432 start_time = g_get_monotonic_time();
433 while (sine_state.frames_written < sine_state.total_frames ||
434 state.captured_bytes < CAPTURE_BUFFER_SIZE) {
435 audio_run(AUDIO_MIXENG_BACKEND(be), "test-capture");
436 main_loop_wait(true);
437
438 elapsed_ms = (g_get_monotonic_time() - start_time) / 1000;
439 if (elapsed_ms > 1000) { /* 1 second timeout */
440 break;
441 }
442
443 g_usleep(G_USEC_PER_SEC / 1000); /* 1ms */
444 }
445
446 g_test_message("Wrote %" PRId64 " frames, captured %zu bytes",
447 sine_state.frames_written, state.captured_bytes);
448
449 g_assert_true(state.capture_called);
450 g_assert_cmpuint(state.captured_bytes, >, 0);
451
452 /* Compare captured data with expected sine wave */
453 captured_frames = state.captured_bytes / (CHANNELS * sizeof(int16_t));
454 if (captured_frames > 0) {
455 /*
456 * Allow some tolerance due to mixing/conversion.
457 * The tolerance accounts for potential rounding differences.
458 */
459 matching_samples = compare_sine_samples(state.captured_samples,
460 captured_frames, 0, 100);
461 total_samples = captured_frames * CHANNELS;
462 match_ratio = (double)matching_samples / total_samples;
463
464 g_test_message("Captured %d frames, %d/%d samples match (%.1f%%)",
465 captured_frames, matching_samples, total_samples,
466 match_ratio * 100.0);
467
468 /*
469 * Expect at least 90% of samples to match within tolerance.
470 * Some variation is expected due to mixing engine processing.
471 */
472 g_assert_cmpfloat(match_ratio, >=, 0.9);
473 }
474
475 audio_be_set_active_out(be, voice, false);
476 audio_be_close_out(be, voice);
477
478 audio_be_del_capture(be, cap, &state);
479 g_assert_true(state.destroy_called);
480
481 g_free(state.captured_samples);
482 }
483
484 static void test_audio_null_handling(void)
485 {
486 AudioBackend *be = get_test_audio_backend();
487 uint8_t buffer[64];
488
489 /* audio_be_is_active_out/in(NULL) should return false */
490 g_assert_false(audio_be_is_active_out(be, NULL));
491 g_assert_false(audio_be_is_active_in(be, NULL));
492
493 /* audio_be_get_buffer_size_out(NULL) should return 0 */
494 g_assert_cmpint(audio_be_get_buffer_size_out(be, NULL), ==, 0);
495
496 /* audio_be_write/read(NULL, ...) should return 0 */
497 g_assert_cmpuint(audio_be_write(be, NULL, buffer, sizeof(buffer)), ==, 0);
498 g_assert_cmpuint(audio_be_read(be, NULL, buffer, sizeof(buffer)), ==, 0);
499
500 /* These should not crash */
501 audio_be_set_active_out(be, NULL, true);
502 audio_be_set_active_out(be, NULL, false);
503 audio_be_set_active_in(be, NULL, true);
504 audio_be_set_active_in(be, NULL, false);
505 }
506
507 static void test_audio_multiple_voices(void)
508 {
509 AudioBackend *be;
510 SWVoiceOut *out1, *out2;
511 SWVoiceIn *in1;
512
513 be = get_test_audio_backend();
514 out1 = open_test_voice_out(be, "test-multi-out1", NULL, dummy_audio_callback);
515 out2 = open_test_voice_out(be, "test-multi-out2", NULL, dummy_audio_callback);
516 in1 = open_test_voice_in(be, "test-multi-in1", NULL, dummy_audio_callback);
517
518 audio_be_set_active_out(be, out1, true);
519 audio_be_set_active_out(be, out2, true);
520 audio_be_set_active_in(be, in1, true);
521
522 g_assert_true(audio_be_is_active_out(be, out1));
523 g_assert_true(audio_be_is_active_out(be, out2));
524 if (in1) {
525 g_assert_true(audio_be_is_active_in(be, in1));
526 }
527
528 audio_be_set_active_out(be, out1, false);
529 audio_be_set_active_out(be, out2, false);
530 audio_be_set_active_in(be, in1, false);
531
532 audio_be_close_in(be, in1);
533 audio_be_close_out(be, out2);
534 audio_be_close_out(be, out1);
535 }
536
537 static const struct audsettings invalid_test_settings = {
538 .nchannels = 0,
539 .freq = SAMPLE_RATE,
540 .fmt = AUDIO_FORMAT_S16,
541 .big_endian = false,
542 };
543
544 static void test_audio_invalid_settings(void)
545 {
546 AudioBackend *be = get_test_audio_backend();
547 void *voice;
548
549 voice = audio_be_open_out(be, NULL, "invalid", NULL,
550 dummy_audio_callback, &invalid_test_settings);
551 g_assert_null(voice);
552 voice = audio_be_open_in(be, NULL, "invalid", NULL,
553 dummy_audio_callback, &invalid_test_settings);
554 g_assert_null(voice);
555 }
556
557 int main(int argc, char **argv)
558 {
559 GOptionContext *context;
560 g_autoptr(GError) error = NULL;
561 g_autofree gchar *dir = NULL;
562 int ret;
563
564 context = g_option_context_new("- QEMU audio test");
565 g_option_context_add_main_entries(context, test_options, NULL);
566
567 if (!g_option_context_parse(context, &argc, &argv, &error)) {
568 g_printerr("Option parsing failed: %s\n", error->message);
569 return 1;
570 }
571 g_option_context_free(context);
572
573 g_test_init(&argc, &argv, NULL);
574
575 module_call_init(MODULE_INIT_TRACE);
576 qemu_add_opts(&qemu_trace_opts);
577 if (opt_trace) {
578 trace_opt_parse(opt_trace);
579 qemu_set_log(LOG_TRACE, &error_fatal);
580 }
581 trace_init_backends();
582 trace_init_file();
583
584 dir = g_test_build_filename(G_TEST_BUILT, "..", "..", NULL);
585 g_setenv("QEMU_MODULE_DIR", dir, true);
586 qemu_init_exec_dir(argv[0]);
587 module_call_init(MODULE_INIT_QOM);
588 module_init_info(qemu_modinfo);
589
590 qemu_init_main_loop(&error_abort);
591 if (opt_audiodev) {
592 g_autofree gchar *spec = is_help_option(opt_audiodev) ?
593 opt_audiodev : g_strdup_printf("%s,id=%s", opt_audiodev, TEST_AUDIODEV_ID);
594 audio_parse_option(spec);
595 }
596 audio_create_default_audiodevs();
597 audio_init_audiodevs();
598
599 g_test_add_func("/audio/prio-list", test_audio_prio_list);
600
601 g_test_add_func("/audio/out/active-state", test_audio_out_active_state);
602 g_test_add_func("/audio/out/sine-wave", test_audio_out_sine_wave);
603 g_test_add_func("/audio/out/buffer-size", test_audio_out_buffer_size);
604 g_test_add_func("/audio/out/volume", test_audio_out_volume);
605 g_test_add_func("/audio/out/capture", test_audio_capture);
606
607 g_test_add_func("/audio/in/active-state", test_audio_in_active_state);
608 g_test_add_func("/audio/in/volume", test_audio_in_volume);
609
610 g_test_add_func("/audio/null-handling", test_audio_null_handling);
611 g_test_add_func("/audio/multiple-voices", test_audio_multiple_voices);
612 g_test_add_func("/audio/invalid-settings", test_audio_invalid_settings);
613
614 ret = g_test_run();
615
616 audio_cleanup();
617
618 return ret;
619 }