From 292a6e59f47759dc32d1b30ab3292885a24ded37 Mon Sep 17 00:00:00 2001 From: jp9000 Date: Thu, 28 Sep 2017 14:58:53 -0700 Subject: [PATCH] test: Add sync tests The async source sync test verifies that async sources are playing back in sync properly, and the "sync pair (video)" and "sync pair (audio)" sources test the back-end syncing down to the exact sample via inserting audio data directly in to the back-end with system timestamps. For these sources, when the video is white, a C sinewave should be playing, and when the video is black audio is mute. --- test/test-input/CMakeLists.txt | 3 + test/test-input/sync-async-source.c | 143 ++++++++++++++++++++++++++++ test/test-input/sync-pair-aud.c | 137 ++++++++++++++++++++++++++ test/test-input/sync-pair-vid.c | 132 +++++++++++++++++++++++++ test/test-input/test-input.c | 6 ++ 5 files changed, 421 insertions(+) create mode 100644 test/test-input/sync-async-source.c create mode 100644 test/test-input/sync-pair-aud.c create mode 100644 test/test-input/sync-pair-vid.c diff --git a/test/test-input/CMakeLists.txt b/test/test-input/CMakeLists.txt index 596e63730..abc2fb6a4 100644 --- a/test/test-input/CMakeLists.txt +++ b/test/test-input/CMakeLists.txt @@ -12,6 +12,9 @@ set(test-input_SOURCES test-filter.c test-input.c test-sinewave.c + sync-async-source.c + sync-pair-vid.c + sync-pair-aud.c test-random.c) add_library(test-input MODULE diff --git a/test/test-input/sync-async-source.c b/test/test-input/sync-async-source.c new file mode 100644 index 000000000..8aa6dd10b --- /dev/null +++ b/test/test-input/sync-async-source.c @@ -0,0 +1,143 @@ +#include +#include +#include +#include + +struct async_sync_test { + obs_source_t *source; + os_event_t *stop_signal; + pthread_t thread; + bool initialized; +}; + +/* middle C */ +static const double rate = 261.63/48000.0; + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif + +#define M_PI_X2 M_PI*2 + +static const char *ast_getname(void *unused) +{ + UNUSED_PARAMETER(unused); + return "Sync Test (Async Video/Audio Source)"; +} + +static void ast_destroy(void *data) +{ + struct async_sync_test *ast = data; + + if (ast->initialized) { + os_event_signal(ast->stop_signal); + pthread_join(ast->thread, NULL); + } + + os_event_destroy(ast->stop_signal); + bfree(ast); +} + +static inline void fill_texture(uint32_t *pixels, uint32_t color) +{ + size_t x, y; + + for (y = 0; y < 20; y++) { + for (x = 0; x < 20; x++) { + pixels[y*20 + x] = color; + } + } +} + +static void *video_thread(void *data) +{ + struct async_sync_test *ast = data; + + uint32_t sample_rate = audio_output_get_sample_rate(obs_get_audio()); + + uint32_t *pixels = bmalloc(20 * 20 * sizeof(uint32_t)); + float *samples = bmalloc(sample_rate * sizeof(float)); + uint64_t cur_time = os_gettime_ns(); + bool whitelist = false; + double cos_val = 0.0; + uint64_t start_time = cur_time; + + struct obs_source_frame frame = { + .data = {[0] = (uint8_t*)pixels}, + .linesize = {[0] = 20*4}, + .width = 20, + .height = 20, + .format = VIDEO_FORMAT_BGRX + }; + struct obs_source_audio audio = { + .speakers = SPEAKERS_MONO, + .data = {[0] = (uint8_t*)samples}, + .samples_per_sec = sample_rate, + .frames = sample_rate, + .format = AUDIO_FORMAT_FLOAT + }; + + while (os_event_try(ast->stop_signal) == EAGAIN) { + fill_texture(pixels, whitelist ? 0xFFFFFFFF : 0xFF000000); + + frame.timestamp = cur_time - start_time; + audio.timestamp = cur_time - start_time; + + if (whitelist) { + for (size_t i = 0; i < sample_rate; i++) { + cos_val += rate * M_PI_X2; + if (cos_val > M_PI_X2) + cos_val -= M_PI_X2; + + samples[i] = (float)(cos(cos_val) * 0.5); + } + } else { + for (size_t i = 0; i < sample_rate; i++) + samples[i] = 0.0f; + } + + obs_source_output_video(ast->source, &frame); + obs_source_output_audio(ast->source, &audio); + + os_sleepto_ns(cur_time += 1000000000); + + whitelist = !whitelist; + } + + bfree(pixels); + bfree(samples); + + return NULL; +} + +static void *ast_create(obs_data_t *settings, obs_source_t *source) +{ + struct async_sync_test *ast = bzalloc(sizeof(struct async_sync_test)); + ast->source = source; + + if (os_event_init(&ast->stop_signal, OS_EVENT_TYPE_MANUAL) != 0) { + ast_destroy(ast); + return NULL; + } + + if (pthread_create(&ast->thread, NULL, video_thread, ast) != 0) { + ast_destroy(ast); + return NULL; + } + + ast->initialized = true; + + UNUSED_PARAMETER(settings); + UNUSED_PARAMETER(source); + return ast; +} + +struct obs_source_info async_sync_test = { + .id = "async_sync_test", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_ASYNC_VIDEO | + OBS_SOURCE_AUDIO, + .get_name = ast_getname, + .create = ast_create, + .destroy = ast_destroy, +}; diff --git a/test/test-input/sync-pair-aud.c b/test/test-input/sync-pair-aud.c new file mode 100644 index 000000000..52a66c18a --- /dev/null +++ b/test/test-input/sync-pair-aud.c @@ -0,0 +1,137 @@ +#include +#include +#include +#include +#include + +struct sync_pair_aud { + bool initialized_thread; + pthread_t thread; + os_event_t *event; + obs_source_t *source; +}; + +/* middle C */ +static const double rate = 261.63/48000.0; + +#ifndef M_PI +#define M_PI 3.1415926535897932384626433832795 +#endif + +#define M_PI_X2 M_PI*2 + +extern uint64_t starting_time; + +static inline bool whitelist_time(uint64_t ts, uint64_t interval, + uint64_t fps_num, uint64_t fps_den) +{ + if (!starting_time) + return false; + + uint64_t count = (ts - starting_time) / interval; + uint64_t sec = count * fps_den / fps_num; + return sec % 2 == 1; +} + +static void *sync_pair_aud_thread(void *pdata) +{ + struct sync_pair_aud *spa = pdata; + uint32_t sample_rate = audio_output_get_sample_rate(obs_get_audio()); + uint32_t frames = sample_rate / 100; + uint64_t last_time = obs_get_video_frame_time(); + double cos_val = 0.0; + float *samples = malloc(frames * sizeof(float)); + + uint64_t interval = video_output_get_frame_time(obs_get_video()); + const struct video_output_info *voi = + video_output_get_info(obs_get_video()); + uint64_t fps_num = voi->fps_num; + uint64_t fps_den = voi->fps_den; + + while (os_event_try(spa->event) == EAGAIN) { + if (!os_sleepto_ns(last_time += 10000000)) + last_time = obs_get_video_frame_time(); + + for (uint64_t i = 0; i < frames; i++) { + uint64_t ts = last_time + + i * 1000000000ULL / sample_rate; + + if (whitelist_time(ts, interval, fps_num, fps_den)) { + cos_val += rate * M_PI_X2; + if (cos_val > M_PI_X2) + cos_val -= M_PI_X2; + + samples[i] = (float)(cos(cos_val) * 0.5); + } else { + samples[i] = 0.0f; + } + } + + struct obs_source_audio data; + data.data[0] = (uint8_t*)samples; + data.frames = frames; + data.speakers = SPEAKERS_MONO; + data.samples_per_sec = sample_rate; + data.timestamp = last_time; + data.format = AUDIO_FORMAT_FLOAT; + obs_source_output_audio(spa->source, &data); + } + + free(samples); + + return NULL; +} + +/* ------------------------------------------------------------------------- */ + +static const char *sync_pair_aud_getname(void *unused) +{ + UNUSED_PARAMETER(unused); + return "Sync Test Pair (Audio)"; +} + +static void sync_pair_aud_destroy(void *data) +{ + struct sync_pair_aud *spa = data; + + if (spa) { + if (spa->initialized_thread) { + void *ret; + os_event_signal(spa->event); + pthread_join(spa->thread, &ret); + } + + os_event_destroy(spa->event); + bfree(spa); + } +} + +static void *sync_pair_aud_create(obs_data_t *settings, + obs_source_t *source) +{ + struct sync_pair_aud *spa = bzalloc(sizeof(struct sync_pair_aud)); + spa->source = source; + + if (os_event_init(&spa->event, OS_EVENT_TYPE_MANUAL) != 0) + goto fail; + if (pthread_create(&spa->thread, NULL, sync_pair_aud_thread, spa) != 0) + goto fail; + + spa->initialized_thread = true; + + UNUSED_PARAMETER(settings); + return spa; + +fail: + sync_pair_aud_destroy(spa); + return NULL; +} + +struct obs_source_info sync_audio = { + .id = "sync_audio", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_AUDIO, + .get_name = sync_pair_aud_getname, + .create = sync_pair_aud_create, + .destroy = sync_pair_aud_destroy, +}; diff --git a/test/test-input/sync-pair-vid.c b/test/test-input/sync-pair-vid.c new file mode 100644 index 000000000..ebfc32ca3 --- /dev/null +++ b/test/test-input/sync-pair-vid.c @@ -0,0 +1,132 @@ +#include +#include +#include +#include +#include + +struct sync_pair_vid { + obs_source_t *source; + gs_texture_t *tex; + gs_texture_t *white; + gs_texture_t *black; +}; + +uint64_t starting_time = 0; +uint64_t last_frame = 0; + +static const char *sync_pair_vid_getname(void *unused) +{ + UNUSED_PARAMETER(unused); + return "Sync Test Pair (Video)"; +} + +static void sync_pair_vid_destroy(void *data) +{ + struct sync_pair_vid *spv = data; + + obs_enter_graphics(); + gs_texture_destroy(spv->tex); + gs_texture_destroy(spv->white); + gs_texture_destroy(spv->black); + obs_leave_graphics(); + + bfree(spv); +} + +static inline void fill_texture(uint32_t *pixels, uint32_t pixel) +{ + size_t x, y; + + for (y = 0; y < 32; y++) { + for (x = 0; x < 32; x++) { + pixels[y*32 + x] = pixel; + } + } +} + +static void *sync_pair_vid_create(obs_data_t *settings, obs_source_t *source) +{ + struct sync_pair_vid *spv = bzalloc(sizeof(struct sync_pair_vid)); + spv->source = source; + + obs_enter_graphics(); + spv->tex = gs_texture_create(32, 32, GS_RGBA, 1, NULL, GS_DYNAMIC); + spv->white = gs_texture_create(32, 32, GS_RGBA, 1, NULL, GS_DYNAMIC); + spv->black = gs_texture_create(32, 32, GS_RGBA, 1, NULL, GS_DYNAMIC); + + uint8_t *ptr; + uint32_t linesize; + if (gs_texture_map(spv->white, &ptr, &linesize)) { + fill_texture((uint32_t*)ptr, 0xFFFFFFFF); + gs_texture_unmap(spv->white); + } + if (gs_texture_map(spv->black, &ptr, &linesize)) { + fill_texture((uint32_t*)ptr, 0xFF000000); + gs_texture_unmap(spv->black); + } + + obs_leave_graphics(); + + return spv; +} + +static inline bool whitelist_time(uint64_t ts, uint64_t interval, + uint64_t fps_num, uint64_t fps_den) +{ + if (!starting_time) + return false; + + uint64_t count = (ts - starting_time) / interval; + uint64_t sec = count * fps_den / fps_num; + return sec % 2 == 1; +} + +static void sync_pair_vid_render(void *data, gs_effect_t *effect) +{ + struct sync_pair_vid *spv = data; + + uint64_t ts = obs_get_video_frame_time(); + if (!starting_time) + starting_time = ts; + + uint64_t interval = video_output_get_frame_time(obs_get_video()); + const struct video_output_info *voi = + video_output_get_info(obs_get_video()); + uint64_t fps_num = voi->fps_num; + uint64_t fps_den = voi->fps_den; + + bool whitelist = whitelist_time(ts, interval, fps_num, fps_den); + +#if 0 + if (last_frame != ts) { + uint8_t *ptr; + uint32_t linesize; + if (gs_texture_map(spv->tex, &ptr, &linesize)) { + fill_texture((uint32_t*)ptr, whitelist ? 0xFFFFFFFF : 0xFF000000); + gs_texture_unmap(spv->tex); + } + last_frame = ts; + } + + obs_source_draw(spv->tex, 0, 0, 0, 0, 0); +#else + obs_source_draw(whitelist ? spv->white : spv->black, 0, 0, 0, 0, 0); +#endif +} + +static uint32_t sync_pair_vid_size(void *data) +{ + return 32; +} + +struct obs_source_info sync_video = { + .id = "sync_video", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_VIDEO, + .get_name = sync_pair_vid_getname, + .create = sync_pair_vid_create, + .destroy = sync_pair_vid_destroy, + .video_render = sync_pair_vid_render, + .get_width = sync_pair_vid_size, + .get_height = sync_pair_vid_size, +}; diff --git a/test/test-input/test-input.c b/test/test-input/test-input.c index 187398f9a..43f8c2438 100644 --- a/test/test-input/test-input.c +++ b/test/test-input/test-input.c @@ -5,11 +5,17 @@ OBS_DECLARE_MODULE() extern struct obs_source_info test_random; extern struct obs_source_info test_sinewave; extern struct obs_source_info test_filter; +extern struct obs_source_info async_sync_test; +extern struct obs_source_info sync_video; +extern struct obs_source_info sync_audio; bool obs_module_load(void) { obs_register_source(&test_random); obs_register_source(&test_sinewave); obs_register_source(&test_filter); + obs_register_source(&async_sync_test); + obs_register_source(&sync_video); + obs_register_source(&sync_audio); return true; } -- GitLab