The open source OpenXR runtime

a/util: Add audio resampler

Basic nearest neighbour audio resampling.

Part-of: <https://gitlab.freedesktop.org/monado/monado/-/merge_requests/2463>

+366
+2
src/xrt/auxiliary/util/CMakeLists.txt
··· 78 78 u_pretty_print.h 79 79 u_prober.c 80 80 u_prober.h 81 + u_resampler.c 82 + u_resampler.h 81 83 u_session.c 82 84 u_session.h 83 85 u_space_overseer.c
+167
src/xrt/auxiliary/util/u_resampler.c
··· 1 + // Copyright 2025, Collabora, Ltd. 2 + // SPDX-License-Identifier: BSL-1.0 3 + /*! 4 + * @file 5 + * @brief Simple audio resampler 6 + * @author Beyley Cardellio <ep1cm1n10n123@gmail.com> 7 + * @ingroup aux_util 8 + */ 9 + 10 + #include "util/u_misc.h" 11 + #include "math/m_api.h" 12 + 13 + #include "u_resampler.h" 14 + 15 + #include <assert.h> 16 + 17 + struct u_resampler * 18 + u_resampler_create(size_t num_samples, float sample_rate) 19 + { 20 + struct u_resampler *resampler = U_TYPED_CALLOC(struct u_resampler); 21 + 22 + resampler->samples = calloc(num_samples, sizeof(sample_t)); 23 + resampler->scratch = calloc(num_samples, sizeof(sample_t)); 24 + resampler->num_samples = num_samples; 25 + resampler->sample_rate = sample_rate; 26 + 27 + resampler->read_index = 0; 28 + resampler->write_index = 1; 29 + 30 + return resampler; 31 + } 32 + 33 + void 34 + u_resampler_destroy(struct u_resampler *resampler) 35 + { 36 + free(resampler->samples); 37 + free(resampler->scratch); 38 + free(resampler); 39 + } 40 + 41 + #define WRAP_ADD(resampler, a, b) ((a + b) % resampler->num_samples) 42 + #define BETWEEN(resampler, a, b) ((b > a) ? (b - a) : ((resampler->num_samples - a) + b)) 43 + 44 + size_t 45 + u_resampler_read(struct u_resampler *resampler, sample_t *samples, size_t num_samples) 46 + { 47 + size_t total_read = 0; 48 + 49 + // the amount of samples that are in the buffer 50 + size_t samples_in_buffer = BETWEEN(resampler, resampler->read_index, resampler->write_index) - 1; 51 + 52 + size_t until_end = resampler->num_samples - resampler->read_index; 53 + 54 + size_t read = MIN(samples_in_buffer, MIN(until_end, num_samples)); 55 + total_read += read; 56 + memcpy(samples, resampler->samples + resampler->read_index, read * sizeof(sample_t)); 57 + num_samples -= read; 58 + samples += read; 59 + resampler->read_index = WRAP_ADD(resampler, resampler->read_index, read); 60 + samples_in_buffer -= read; 61 + 62 + if (num_samples > 0 && samples_in_buffer > 0) { 63 + read = MIN(samples_in_buffer, num_samples); 64 + total_read += read; 65 + memcpy(samples, resampler->samples + resampler->read_index, read * sizeof(sample_t)); 66 + resampler->read_index = WRAP_ADD(resampler, resampler->read_index, read); 67 + } 68 + 69 + return total_read; 70 + } 71 + 72 + // push without resampling 73 + size_t 74 + u_resampler_write_raw(struct u_resampler *resampler, const sample_t *samples, size_t num_samples) 75 + { 76 + // the amount of bytes we can write until we start overwriting the play index 77 + size_t can_write = BETWEEN(resampler, resampler->write_index, resampler->read_index) - 1; 78 + 79 + if (can_write == 0) 80 + return 0; 81 + 82 + size_t written = 0; 83 + 84 + // the amount of bytes until the end of the buffer 85 + size_t until_end = resampler->num_samples - resampler->write_index; 86 + 87 + written += MIN(can_write, MIN(until_end, num_samples)); 88 + num_samples -= written; 89 + can_write -= written; 90 + 91 + // copy in the samples until the end of the buffer 92 + memcpy(resampler->samples + resampler->write_index, samples, written * sizeof(sample_t)); 93 + resampler->write_index = WRAP_ADD(resampler, resampler->write_index, written); 94 + samples += written; 95 + 96 + if (num_samples > 0 && can_write > 0) { 97 + assert(resampler->write_index == 0); 98 + 99 + // copy in the samples that go at the start of the buffer 100 + can_write = resampler->read_index; 101 + 102 + // bytes to write after the start 103 + size_t written_after_start = MIN(resampler->read_index, num_samples); 104 + 105 + written += written_after_start; 106 + num_samples -= written; 107 + 108 + // copy the data 109 + memcpy(resampler->samples + resampler->write_index, samples, written_after_start * sizeof(sample_t)); 110 + } 111 + 112 + return written; 113 + } 114 + 115 + // converts an index from one sample rate to another 116 + #define TO_RATE(idx, source_rate, target_rate) ((size_t)((float)(idx) * ((target_rate) / (source_rate)))) 117 + 118 + size_t 119 + u_resampler_write(struct u_resampler *resampler, const sample_t *source_samples, size_t num_samples, float sample_rate) 120 + { 121 + // no writing needed 122 + if (num_samples == 0) 123 + return 0; 124 + 125 + // short-circuit if the sample rate is already matching, no resampling needed 126 + if (sample_rate == resampler->sample_rate) { 127 + return u_resampler_write_raw(resampler, source_samples, num_samples); 128 + } 129 + 130 + const float target_sample_rate = resampler->sample_rate; 131 + 132 + // the amount of samples we *can* write 133 + size_t can_write = BETWEEN(resampler, resampler->write_index, resampler->read_index); 134 + 135 + sample_t *target_samples = resampler->scratch; 136 + 137 + size_t target_idx = 0; 138 + while (true) { 139 + size_t source_idx = TO_RATE(target_idx, target_sample_rate, sample_rate); 140 + 141 + // can't read any more samples 142 + if (source_idx >= num_samples) 143 + break; 144 + 145 + // can't write any more samples 146 + if (can_write == 0) 147 + break; 148 + 149 + target_samples[target_idx] = source_samples[source_idx]; 150 + 151 + target_idx++; 152 + can_write--; 153 + } 154 + 155 + // note: ignoring return value since we should never resample more than we can write 156 + u_resampler_write_raw(resampler, target_samples, target_idx); 157 + 158 + return TO_RATE(target_idx - 1, target_sample_rate, sample_rate); 159 + } 160 + 161 + void 162 + u_resampler_reset(struct u_resampler *resampler) 163 + { 164 + resampler->samples[0] = 0; 165 + resampler->read_index = 0; 166 + resampler->write_index = 1; 167 + }
+52
src/xrt/auxiliary/util/u_resampler.h
··· 1 + // Copyright 2025, Collabora, Ltd. 2 + // SPDX-License-Identifier: BSL-1.0 3 + /*! 4 + * @file 5 + * @brief Simple audio resampler 6 + * @author Beyley Cardellio <ep1cm1n10n123@gmail.com> 7 + * @ingroup aux_util 8 + */ 9 + 10 + #pragma once 11 + 12 + #include <stddef.h> 13 + #include <stdint.h> 14 + 15 + #ifdef __cplusplus 16 + extern "C" { 17 + #endif 18 + 19 + typedef float sample_t; 20 + 21 + struct u_resampler 22 + { 23 + float sample_rate; 24 + 25 + size_t num_samples; 26 + sample_t *samples; 27 + 28 + // static scratch buffer for resampling use 29 + sample_t *scratch; 30 + 31 + size_t read_index; 32 + size_t write_index; 33 + }; 34 + 35 + struct u_resampler * 36 + u_resampler_create(size_t num_samples, float sample_rate); 37 + 38 + void 39 + u_resampler_destroy(struct u_resampler *resampler); 40 + 41 + size_t 42 + u_resampler_read(struct u_resampler *resampler, sample_t *samples, size_t num_samples); 43 + 44 + size_t 45 + u_resampler_write(struct u_resampler *resampler, const sample_t *source_samples, size_t num_samples, float sample_rate); 46 + 47 + void 48 + u_resampler_reset(struct u_resampler *resampler); 49 + 50 + #ifdef __cplusplus 51 + } 52 + #endif
+1
src/xrt/state_trackers/gui/CMakeLists.txt
··· 19 19 gui_scene_record_euroc.c 20 20 gui_scene_main_menu.c 21 21 gui_scene_record.c 22 + gui_scene_resampler_test.c 22 23 gui_scene_remote.c 23 24 gui_scene_video.c 24 25 gui_scene_tracking_overrides.c
+3
src/xrt/state_trackers/gui/gui_common.h
··· 269 269 struct xrt_fs *xfs, 270 270 struct xrt_settings_tracking *s); 271 271 272 + void 273 + gui_scene_resampler_test(struct gui_program *p); 274 + 272 275 /*! 273 276 * @} 274 277 */
+5
src/xrt/state_trackers/gui/gui_scene_main_menu.c
··· 87 87 gui_scene_record_euroc(p); 88 88 } 89 89 90 + if (igButton("Resampler Test", button_dims)) { 91 + gui_scene_delete_me(p, scene); 92 + gui_scene_resampler_test(p); 93 + } 94 + 90 95 igSeparator(); 91 96 92 97 if (igButton("Exit", button_dims)) {
+136
src/xrt/state_trackers/gui/gui_scene_resampler_test.c
··· 1 + // Copyright 2025, Collabora, Ltd. 2 + // SPDX-License-Identifier: BSL-1.0 3 + /*! 4 + * @file 5 + * @brief Audio resampler test. 6 + * @author Beyley Cardellio <ep1cm1n10n123@gmail.com> 7 + * @ingroup gui 8 + */ 9 + 10 + #include <assert.h> 11 + #include <math.h> 12 + #include <stdlib.h> 13 + #include <stdio.h> 14 + #include <stdlib.h> 15 + 16 + #include "os/os_time.h" 17 + 18 + #include "math/m_api.h" 19 + #include "math/m_mathinclude.h" 20 + 21 + #include "util/u_debug.h" 22 + #include "util/u_logging.h" 23 + #include "util/u_misc.h" 24 + #include "util/u_time.h" 25 + #include "util/u_resampler.h" 26 + 27 + #include "xrt/xrt_defines.h" 28 + #include "xrt/xrt_tracking.h" 29 + 30 + #include "gui_common.h" 31 + #include "gui_imgui.h" 32 + 33 + #define CSV_EOL "\r\n" 34 + // #define CSV_PRECISION 10 35 + #define FMT_F32_GENERAL "%.4f" 36 + #define FMT_F32_TIME "%.6f" 37 + #define FMT_F32_VARIANCE "%.6f" 38 + 39 + static ImVec2 button_dims = {256, 0}; 40 + 41 + struct resampler_test 42 + { 43 + struct gui_scene base; 44 + struct u_resampler *resampler; 45 + 46 + int to_write; 47 + int last_written; 48 + int to_read; 49 + int last_read; 50 + float frequency; 51 + float sample_rate; 52 + }; 53 + 54 + #define BUFFER_SIZE 4000 55 + #define SAMPLE_RATE 3000 56 + 57 + static inline void 58 + scene_render(struct gui_scene *scene, struct gui_program *p) 59 + { 60 + struct resampler_test *test_scene = (struct resampler_test *)scene; 61 + 62 + // Draw the controls first, and decide whether to update. 63 + igBegin("Resampler", NULL, 0); 64 + 65 + igSeparatorText("Controls"); 66 + 67 + float temp[BUFFER_SIZE] = {0}; 68 + 69 + igDragInt("Samples To Push", &test_scene->to_write, 1, 1, BUFFER_SIZE, "%d samples", 0); 70 + igDragInt("Samples To Read", &test_scene->to_read, 1, 1, BUFFER_SIZE, "%d samples", 0); 71 + igDragFloat("Frequency", &test_scene->frequency, 0.1, 50, 8000, "%f hz", 0); 72 + igDragFloat("Sample Rate", &test_scene->sample_rate, 1, 50, 8000, "%f hz", 0); 73 + 74 + igText("Last Written %d", test_scene->last_written); 75 + igText("Last Read %d", test_scene->last_read); 76 + 77 + if (igButton("Push Samples", button_dims)) { 78 + for (int i = 0; i < test_scene->to_write; i++) { 79 + float t = i / test_scene->sample_rate; 80 + 81 + temp[i] = sinf((M_PI * 2) * test_scene->frequency * t); 82 + } 83 + 84 + test_scene->last_written = 85 + u_resampler_write(test_scene->resampler, temp, test_scene->to_write, test_scene->sample_rate); 86 + } 87 + if (igButton("Read Samples", button_dims)) { 88 + // discard 89 + test_scene->last_read = u_resampler_read(test_scene->resampler, temp, test_scene->to_read); 90 + } 91 + 92 + igSeparatorText("State"); 93 + 94 + igText("Read Index: %d", test_scene->resampler->read_index); 95 + igText("Write Index: %d", test_scene->resampler->write_index); 96 + 97 + ImPlot_BeginPlot("Ring Buffer", (ImVec2){800, 400}, 0); 98 + ImPlot_PlotLine_FloatPtrInt("Sample", test_scene->resampler->samples, test_scene->resampler->num_samples, 1, 0, 99 + 0, 0, sizeof(sample_t)); 100 + 101 + ImPlot_Annotation_Str(test_scene->resampler->read_index, 0, (ImVec4){0, 0, 1, 1}, (ImVec2){0}, true, 102 + "Read Index"); 103 + ImPlot_Annotation_Str(test_scene->resampler->write_index, 0, (ImVec4){1, 0, 0, 1}, (ImVec2){0}, true, 104 + "Write Index"); 105 + 106 + ImPlot_EndPlot(); 107 + 108 + igEnd(); 109 + } 110 + 111 + static void 112 + scene_destroy(struct gui_scene *scene, struct gui_program *p) 113 + { 114 + struct resampler_test *test_scene = (struct resampler_test *)scene; 115 + 116 + u_resampler_destroy(test_scene->resampler); 117 + 118 + free(test_scene); 119 + } 120 + 121 + void 122 + gui_scene_resampler_test(struct gui_program *p) 123 + { 124 + struct resampler_test *test_scene = U_TYPED_CALLOC(struct resampler_test); 125 + 126 + test_scene->base.render = scene_render; 127 + test_scene->base.destroy = scene_destroy; 128 + 129 + test_scene->resampler = u_resampler_create(BUFFER_SIZE, SAMPLE_RATE); 130 + test_scene->to_write = 1024; 131 + test_scene->to_read = 1024; 132 + test_scene->sample_rate = SAMPLE_RATE; 133 + test_scene->frequency = 300; 134 + 135 + gui_scene_push_front(p, &test_scene->base); 136 + }