From 9ae4ca8e89044ec6522ca87d602a1b9b83542db6 Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Tue, 13 Oct 2026 13:26:40 +0200 Subject: [PATCH 1/2] Keep OLR's position timeline in step with renders without positions OLR counts the samples it has rendered (the render offset) and where each object's next position block starts. A render without a position advanced only the render offset. Every later position block then started before the next sample to render, _refil_processing_queue() dropped it as a metadata underrun and logged an error, and the object stayed silent until the instance was rebuilt. Through oar.h this happens whenever an object element is rendered for a block without a position queued, for example when a host drains the renderer's tail while paused with silent input and no metadata. _metadata_update() now starts a position block at the next sample to render when that is later than the end of the object's previous block. A host that pushes a position for every rendered block, or pushes ahead, gets the same block starts as before. In the same path: - After a processing block that ends inside a render call, the next block was checked against (uint64_t)-1 instead of the finished block's end, so every block starting inside a call counted as an underrun and was dropped. It is checked against the finished block's last sample. - The render offset was an int and overflowed after 2^31 samples, about 12.4 hours at 48 kHz; it is a uint64_t, like the metadata counters. A processing block also computed its overlap with a render call in int; it uses 64-bit sample positions and narrows only the offsets within the call. - The underrun log passed uint64_t values to %u and %lu; it casts them to unsigned long long for %llu. test_object_position_gap.c renders an object element through oar.h for 4 blocks with positions and 1 to 3 blocks without, fed program input or silence, and requires the next block with a position to be audible and equal to the first block of a new oar_t with the same position and input. test_olr_render_offsets.c calls the object audio renderer directly: 128-sample position blocks rendered in 256-sample calls must give the output of 128-sample calls, and blocks rendered at sample positions around 2^31 and 2^32 must give the output of the same blocks at position 0. It includes object_based_audio_renderer.h, so that header's Bazel target is visible to //tests/examples. Both tests fail on main. ctest -L 'oar|obr' (CMake, Release and ASan): 33 pass. --- .../olr/object_audio_renderer/BUILD.bazel | 5 + .../block_processing_channel.c | 12 +- .../block_processor/processing_block.c | 25 ++- src/renderer/olr/olr.c | 16 +- tests/examples/BUILD.bazel | 18 ++ tests/examples/CMakeLists.txt | 7 + tests/examples/test_object_position_gap.c | 155 ++++++++++++++ tests/examples/test_olr_render_offsets.c | 193 ++++++++++++++++++ 8 files changed, 414 insertions(+), 17 deletions(-) create mode 100644 tests/examples/test_object_position_gap.c create mode 100644 tests/examples/test_olr_render_offsets.c diff --git a/src/renderer/olr/object_audio_renderer/BUILD.bazel b/src/renderer/olr/object_audio_renderer/BUILD.bazel index 9146dd8..95527e5 100644 --- a/src/renderer/olr/object_audio_renderer/BUILD.bazel +++ b/src/renderer/olr/object_audio_renderer/BUILD.bazel @@ -8,6 +8,11 @@ cc_library( "object_based_audio_renderer.h", ], strip_include_prefix = "//src/renderer/olr/object_audio_renderer", + # test_olr_render_offsets calls the object audio renderer directly. + visibility = [ + "//src:__subpackages__", + "//tests/examples:__pkg__", + ], deps = [ "//src:oar_config", ], diff --git a/src/renderer/olr/object_audio_renderer/block_processor/block_processing_channel.c b/src/renderer/olr/object_audio_renderer/block_processor/block_processing_channel.c index f676938..34bd3d3 100644 --- a/src/renderer/olr/object_audio_renderer/block_processor/block_processing_channel.c +++ b/src/renderer/olr/object_audio_renderer/block_processor/block_processing_channel.c @@ -107,9 +107,12 @@ int block_processing_channel_process(block_processing_channel_t* self, num_samples, channels, output); if (processing_block->last_sample < end_sample) { + /* The block ends inside this call; the next one must not start + * before it ends. */ + const uint64_t next_start = processing_block->last_sample; deque_pop_front(self->processing_queue, &v); processing_block->destroy(processing_block); - _refil_processing_queue(self, sample_rate, -1); + _refil_processing_queue(self, sample_rate, next_start); } else if (processing_block->last_sample == end_sample) { deque_pop_front(self->processing_queue, &v); processing_block->destroy(processing_block); @@ -151,9 +154,10 @@ static int _refil_processing_queue(block_processing_channel_t* self, if (!processing_block) continue; if (processing_block->first_sample < start) { error( - "metadata underrun: metadata arrived %u after the samples that it " - "would apply to %lu", - processing_block->first_sample, start); + "metadata underrun: metadata starting at sample %llu arrived after " + "sample %llu", + (unsigned long long)processing_block->first_sample, + (unsigned long long)start); processing_block->destroy(processing_block); return -22; } diff --git a/src/renderer/olr/object_audio_renderer/block_processor/processing_block.c b/src/renderer/olr/object_audio_renderer/block_processor/processing_block.c index 617773c..9d1aa4b 100644 --- a/src/renderer/olr/object_audio_renderer/block_processor/processing_block.c +++ b/src/renderer/olr/object_audio_renderer/block_processor/processing_block.c @@ -48,22 +48,27 @@ static int _processing_block_init(processing_block_t *self, return 0; } -static int _processing_block_overlap(processing_block_t *self, int start_sample, - int num_samples, slice_t *states, - slice_t *samples) { - int end_sample = start_sample + num_samples; - int overlay_start_sample = def_max(start_sample, self->first_sample); - int overlay_end_sample = def_min(end_sample, self->last_sample); +/* The overlap of a render call of num_samples samples at start_sample with + * the block, as sample offsets into the call (samples) and into the block + * (states). Sample positions are 64-bit; only the offsets, which are below + * num_samples, are narrowed to int. */ +static int _processing_block_overlap(processing_block_t *self, + uint64_t start_sample, int num_samples, + slice_t *states, slice_t *samples) { + const uint64_t end_sample = start_sample + (uint64_t)num_samples; + const uint64_t overlay_start_sample = + def_max(start_sample, self->first_sample); + const uint64_t overlay_end_sample = def_min(end_sample, self->last_sample); if (overlay_start_sample < overlay_end_sample) { if (samples) { - samples->s = overlay_start_sample - start_sample; - samples->e = overlay_end_sample - start_sample; + samples->s = (int)(overlay_start_sample - start_sample); + samples->e = (int)(overlay_end_sample - start_sample); } if (states) { - states->s = overlay_start_sample - self->first_sample; - states->e = overlay_end_sample - self->first_sample; + states->s = (int)(overlay_start_sample - self->first_sample); + states->e = (int)(overlay_end_sample - self->first_sample); } } return 0; diff --git a/src/renderer/olr/olr.c b/src/renderer/olr/olr.c index 05233ad..3c43132 100644 --- a/src/renderer/olr/olr.c +++ b/src/renderer/olr/olr.c @@ -27,8 +27,10 @@ typedef struct OlrContext { uint32_t num_objects; olr_layout_t layout; metadata_block_t *metadata_blocks; + /* Where each object's next metadata block starts, in samples. */ uint64_t *metadata_block_durations; - int offset; + /* Samples rendered so far. */ + uint64_t offset; } olr_context_t; static int _close(renderer_library_context_t *ctx); @@ -182,10 +184,18 @@ static int _metadata_update(renderer_library_context_t *ctx, uint32_t index, } for (uint32_t i = 0; i < n; i++) { - olr->metadata_blocks[i].start = olr->metadata_block_durations[i]; + /* A block starts where the object's previous block ended. When + * renders without a position have moved past that point, it starts + * at the next sample to render instead: those samples were rendered + * without the object, and a block starting before them would be + * dropped as an underrun, together with every later block. */ + uint64_t start = olr->metadata_block_durations[i]; + if (start < olr->offset) start = olr->offset; + olr->metadata_blocks[i].start = start; olr->metadata_blocks[i].duration = metadata->duration > 0 ? metadata->duration : 0; - olr->metadata_block_durations[i] += olr->metadata_blocks[i].duration; + olr->metadata_block_durations[i] = + start + olr->metadata_blocks[i].duration; olr->metadata_blocks[i].azimuth = metadata->object_positions.polar_positions[i].azimuth; diff --git a/tests/examples/BUILD.bazel b/tests/examples/BUILD.bazel index af3bb91..ced9ff9 100644 --- a/tests/examples/BUILD.bazel +++ b/tests/examples/BUILD.bazel @@ -117,3 +117,21 @@ cc_test( ], ) +cc_test( + name = "test_object_position_gap", + srcs = ["test_object_position_gap.c"], + deps = [ + ":oar_test_common", + "//:oar", + ], +) + +cc_test( + name = "test_olr_render_offsets", + srcs = ["test_olr_render_offsets.c"], + deps = [ + ":oar_test_common", + "//:oar", + "//src/renderer/olr/object_audio_renderer:object_based_audio_renderer_header", + ], +) diff --git a/tests/examples/CMakeLists.txt b/tests/examples/CMakeLists.txt index fe0952a..f675042 100644 --- a/tests/examples/CMakeLists.txt +++ b/tests/examples/CMakeLists.txt @@ -34,6 +34,13 @@ oar_add_test(test_metadata_unit_processing) oar_add_test(test_remove_audio_element) oar_add_test(test_peak_limiter_oar_api) oar_add_test(test_downmix_mode_metadata) +oar_add_test(test_object_position_gap) +oar_add_test(test_olr_render_offsets) +# Calls the object audio renderer directly, through its internal header. +target_include_directories(test_olr_render_offsets PRIVATE + ${PROJECT_SOURCE_DIR}/src/renderer/olr/object_audio_renderer + ${PROJECT_SOURCE_DIR}/src + ${PROJECT_SOURCE_DIR}/src/common) # The HOA LFE test exercises the 120 Hz LFE low-pass, which is only compiled # into the library when OAR_ENABLE_HOA_LFE is ON. diff --git a/tests/examples/test_object_position_gap.c b/tests/examples/test_object_position_gap.c new file mode 100644 index 0000000..4e765f6 --- /dev/null +++ b/tests/examples/test_object_position_gap.c @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2026, Alliance for Open Media. All rights reserved + * + * This source code is subject to the terms of the BSD 3-Clause Clear License + * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause Clear + * License was not distributed with this source code in the LICENSE file, you + * can obtain it at www.aomedia.org/license/software-license/bsd-3-c-c. If the + * Alliance for Open Media Patent License 1.0 was not distributed with this + * source code in the PATENTS file, you can obtain it at + * www.aomedia.org/license/patent. + */ + +/** + * @file test_object_position_gap.c + * @brief Test an object element after renders without position metadata. + * + * OLR counts the samples it has rendered and where each object's next + * position block starts. A render without a position advances only the + * first; a position block starting before the next sample to render was + * dropped as an underrun, and so was every later one, so the object stayed + * silent. A host renders without positions when it drains a renderer's tail + * while paused, with silent input and no metadata. + * + * One one-object element renders to stereo with the limiter disabled. + * + * Test matrix: + * TC1: After 4 blocks with positions and 1 to 3 blocks without, the next + * block with a position is not silent and equals the first block of a + * new oar_t given the same position and input. + * TC2: The same with positions pushed for the 4 blocks only, and the + * renders without positions fed silent input. + */ + +#include +#include +#include + +#include "oar.h" +#include "test_framework.h" +#include "test_helpers.h" + +#define BLOCK 256 +#define ELEMENT_ID 1 + +static const polar_t g_position = {30.0f, 0.0f, 1.0f}; + +/* Deterministic noise in [-0.5, 0.5) for block `block`; silent for + * amplitude 0. */ +static void fill_input(float *data, int block, float amplitude) { + uint32_t state = 2654435761u * ((uint32_t)block + 1u); + for (int i = 0; i < BLOCK; i++) { + state = state * 1664525u + 1013904223u; + data[i] = amplitude * ((float)(state >> 8) / 16777216.0f - 0.5f); + } +} + +static oar_t *create_oar(void) { + oar_config_t cfg = + create_config(ck_oar_layout_stereo, BLOCK, TEST_SAMPLING_RATE); + oar_t *oar = oar_create(&cfg); + if (!oar) return NULL; + oar_audio_element_config_t element = create_object_element_config(1); + const int gid = oar_add_audio_group(oar); + if (oar_enable_limiter(oar, 0) != ck_oar_ok || gid < 0 || + oar_add_audio_element(oar, (uint32_t)gid, ELEMENT_ID, &element) != + ck_oar_ok) { + oar_destroy(oar); + return NULL; + } + return oar; +} + +/* Renders one block of `block`'s input, with a position when `position` is + * set. */ +static int render(oar_t *oar, int block, float amplitude, int position, + float *out) { + float in[BLOCK]; + fill_input(in, block, amplitude); + oar_audio_block_t input = {in, 1, BLOCK}; + if (oar_update_audio_element_data(oar, ELEMENT_ID, &input) != ck_oar_ok) + return -1; + if (position && set_object_position(oar, ELEMENT_ID, &g_position, BLOCK)) + return -1; + oar_audio_block_t output = {out, 2, BLOCK}; + memset(out, 0, sizeof(float) * 2 * BLOCK); + return oar_render(oar, &output) == ck_oar_ok ? 0 : -1; +} + +/* Renders 4 blocks with positions, `gap` blocks without (fed `gap_amplitude` + * input), then block 100 with a position into out. */ +static int render_after_gap(int gap, float gap_amplitude, float *out) { + float scratch[2 * BLOCK]; + oar_t *oar = create_oar(); + if (!oar) return -1; + int ret = 0; + for (int b = 0; ret == 0 && b < 4; b++) + ret = render(oar, b, 1.0f, 1, scratch); + for (int b = 0; ret == 0 && b < gap; b++) + ret = render(oar, 10 + b, gap_amplitude, 0, scratch); + if (ret == 0) ret = render(oar, 100, 1.0f, 1, out); + oar_destroy(oar); + return ret; +} + +static int expect_like_new_instance(float gap_amplitude) { + float expected[2 * BLOCK], got[2 * BLOCK]; + oar_t *oar = create_oar(); + if (!oar) return -1; + int ret = render(oar, 100, 1.0f, 1, expected); + oar_destroy(oar); + if (ret != 0 || !is_output_non_silent(expected, 2 * BLOCK)) return -1; + + for (int gap = 1; gap <= 3; gap++) { + if (render_after_gap(gap, gap_amplitude, got) != 0) return -1; + if (!is_output_non_silent(got, 2 * BLOCK)) { + fprintf(stderr, "silent after a gap of %d blocks\n", gap); + return -1; + } + for (int i = 0; i < 2 * BLOCK; i++) { + if (got[i] != expected[i]) { + fprintf(stderr, "gap %d, sample %d: got %.9g, expected %.9g\n", gap, i, + got[i], expected[i]); + return -1; + } + } + } + return 0; +} + +/* TC1: renders without positions, fed program input. */ +static int test_gap_with_input(void) { + TEST_START("TC1: renders without positions, with input"); + TEST_ASSERT(expect_like_new_instance(1.0f) == 0, + "block after the gap differs from a new instance"); + return TEST_PASS; +} + +/* TC2: renders without positions, fed silence, as a paused tail drain. */ +static int test_gap_with_silence(void) { + TEST_START("TC2: renders without positions, with silent input"); + TEST_ASSERT(expect_like_new_instance(0.0f) == 0, + "block after the gap differs from a new instance"); + return TEST_PASS; +} + +static test_entry_t g_tests[] = { + TEST_ENTRY("TC1", "renders without positions, with input", + test_gap_with_input), + TEST_ENTRY("TC2", "renders without positions, with silent input", + test_gap_with_silence), +}; + +int main(int argc, char *argv[]) { + return run_all_tests(g_tests, NUM_TESTS(g_tests), argc, argv); +} diff --git a/tests/examples/test_olr_render_offsets.c b/tests/examples/test_olr_render_offsets.c new file mode 100644 index 0000000..670532d --- /dev/null +++ b/tests/examples/test_olr_render_offsets.c @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2026, Alliance for Open Media. All rights reserved + * + * This source code is subject to the terms of the BSD 3-Clause Clear License + * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause Clear + * License was not distributed with this source code in the LICENSE file, you + * can obtain it at www.aomedia.org/license/software-license/bsd-3-c-c. If the + * Alliance for Open Media Patent License 1.0 was not distributed with this + * source code in the PATENTS file, you can obtain it at + * www.aomedia.org/license/patent. + */ + +/** + * @file test_olr_render_offsets.c + * @brief Test OLR's position blocks against render calls of other lengths + * and at large sample positions. + * + * oar_render() hands OLR one position block per render call, of the call's + * length and at the call's position, so these tests call the object audio + * renderer directly to reach the other cases. + * + * One track renders to stereo. + * + * Test matrix: + * TC1: Position blocks of 128 samples rendered in calls of 256 samples + * give the output of calls of 128 samples. After a block that ended + * inside a call, the next block was checked against (uint64_t)-1 and + * dropped as an underrun. + * TC2: Position blocks rendered at sample positions around 2^31 and 2^32 + * give the output of the same blocks at position 0, for blocks of the + * call's length and of twice the call's length. A processing block + * computed its overlap with a render call in int, which overflowed + * there. + */ + +#include +#include +#include + +#include "object_based_audio_renderer.h" +#include "test_framework.h" + +#define BLOCK 256 +#define BLOCKS 20 +#define STEREO 2 + +/* Deterministic noise in [-0.5, 0.5) for block `block`. */ +static void fill_input(float *data, uint32_t count, int block) { + uint32_t state = 2654435761u * ((uint32_t)block + 1u); + for (uint32_t i = 0; i < count; i++) { + state = state * 1664525u + 1013904223u; + data[i] = (float)(state >> 8) / 16777216.0f - 0.5f; + } +} + +/* Position block `index` of `duration` samples starting at `start`. */ +static metadata_block_t position(int index, uint64_t start, uint64_t duration) { + metadata_block_t m; + memset(&m, 0, sizeof(m)); + m.track_id = 0; + m.start = start; + m.duration = duration; + m.azimuth = -60.0f + 6.0f * (float)index; + m.elevation = 0.0f; + m.distance = 1.0f; + return m; +} + +static int compare(const float *got, const float *expected, int count) { + for (int i = 0; i < count; i++) { + if (got[i] != expected[i]) { + fprintf(stderr, "sample %d: got %.9g, expected %.9g\n", i, got[i], + expected[i]); + return -1; + } + } + return 0; +} + +static int is_non_silent(const float *data, int count) { + for (int i = 0; i < count; i++) + if (data[i] != 0.0f) return 1; + return 0; +} + +/* Renders the `count` samples of `in` starting at sample `base`, in calls of + * `call` samples, into out (planar, `count` samples per channel). */ +static int render_calls(object_audio_renderer_t *olr, const float *in, + int count, int call, uint64_t base, float *out) { + for (int c = 0; c < count; c += call) { + float o[STEREO * 2 * BLOCK]; + memset(o, 0, sizeof(o)); + if (object_audio_renderer_render(olr, (float *)in + c, call, 1, + base + (uint64_t)c, o) != 0) + return -1; + for (int ch = 0; ch < STEREO; ch++) + memcpy(out + ch * count + c, o + ch * call, sizeof(float) * call); + } + return 0; +} + +static float g_expected[BLOCKS * STEREO * BLOCK]; +static float g_got[BLOCKS * STEREO * BLOCK]; + +/* Renders BLOCKS blocks of input with 128-sample position blocks, in calls + * of `call` samples, into out. */ +static int render_short_blocks(int call, float *out) { + const int half = BLOCK / 2; + float in[BLOCK]; + object_audio_renderer_t *olr = object_audio_renderer_create( + ck_olr_layout_200, ck_gain_calculator_custom, 48000); + if (!olr) return -1; + int ret = 0; + for (int b = 0; ret == 0 && b < 2 * BLOCKS; b++) { + metadata_block_t m = position(b, (uint64_t)b * half, half); + ret = object_audio_renderer_add_metadatas(olr, &m, 1); + } + for (int b = 0; ret == 0 && b < BLOCKS; b++) { + fill_input(in, BLOCK, b); + ret = render_calls(olr, in, BLOCK, call, (uint64_t)b * BLOCK, + out + b * STEREO * BLOCK); + } + object_audio_renderer_destroy(olr); + return ret; +} + +/* TC1: position blocks shorter than the render call. */ +static int test_blocks_inside_a_call(void) { + TEST_START("TC1: position blocks shorter than a render call"); + TEST_ASSERT(render_short_blocks(BLOCK / 2, g_expected) == 0, + "reference failed"); + TEST_ASSERT(is_non_silent(g_expected, BLOCKS * STEREO * BLOCK), + "reference is silent"); + TEST_ASSERT(render_short_blocks(BLOCK, g_got) == 0, "render failed"); + TEST_ASSERT(compare(g_got, g_expected, BLOCKS * STEREO * BLOCK) == 0, + "calls of two blocks render differently"); + return TEST_PASS; +} + +/* Renders two position blocks of `block` samples starting at `base`, in + * calls of `call` samples, into out (2 * block samples per channel). block + * is at most 2 * BLOCK and call at most block. */ +static int render_at(uint64_t base, int block, int call, float *out) { + float in[4 * BLOCK]; + object_audio_renderer_t *olr = object_audio_renderer_create( + ck_olr_layout_200, ck_gain_calculator_custom, 48000); + if (!olr) return -1; + int ret = 0; + for (int b = 0; ret == 0 && b < 2; b++) { + metadata_block_t m = + position(b, base + (uint64_t)(b * block), (uint64_t)block); + ret = object_audio_renderer_add_metadatas(olr, &m, 1); + } + fill_input(in, 2 * block, 3); + if (ret == 0) ret = render_calls(olr, in, 2 * block, call, base, out); + object_audio_renderer_destroy(olr); + return ret; +} + +/* TC2: sample positions beyond the range of int. */ +static int test_large_sample_positions(void) { + TEST_START("TC2: sample positions around 2^31 and 2^32"); + const uint64_t bases[] = {(1ull << 31) - 256, 1ull << 31, (1ull << 32) - 256, + 1ull << 32}; + const int layouts[][2] = {{BLOCK, BLOCK}, {2 * BLOCK, BLOCK}}; + for (size_t l = 0; l < sizeof(layouts) / sizeof(layouts[0]); l++) { + const int block = layouts[l][0], call = layouts[l][1]; + TEST_ASSERT(render_at(0, block, call, g_expected) == 0, "reference failed"); + TEST_ASSERT(is_non_silent(g_expected, STEREO * 2 * block), + "reference is silent"); + for (size_t b = 0; b < sizeof(bases) / sizeof(bases[0]); b++) { + TEST_ASSERT(render_at(bases[b], block, call, g_got) == 0, + "render failed"); + if (compare(g_got, g_expected, STEREO * 2 * block) != 0) { + fprintf(stderr, "base %llu, block %d, call %d\n", + (unsigned long long)bases[b], block, call); + TEST_ASSERT(0, "output differs from the same blocks at position 0"); + } + } + } + return TEST_PASS; +} + +static test_entry_t g_tests[] = { + TEST_ENTRY("TC1", "position blocks shorter than a render call", + test_blocks_inside_a_call), + TEST_ENTRY("TC2", "sample positions around 2^31 and 2^32", + test_large_sample_positions), +}; + +int main(int argc, char *argv[]) { + return run_all_tests(g_tests, NUM_TESTS(g_tests), argc, argv); +} From c952acd0ce9d8dbdb16f78e0b033dd33181cc282 Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Mon, 19 Oct 2026 18:15:32 +0200 Subject: [PATCH 2/2] Document where metadata pushed after a drained queue applies Once rendering has used up an element's queued gain or position metadata, metadata_item_elapse() leaves the item's start and duration at 0, so metadata pushed afterwards applies from the next rendered sample. Duration-only metadata cannot describe a gap. A note on oar_update_audio_element_metadata() now states this, and that a host holds a gain or a position over a span by pushing metadata that covers it. Discussed in #51. --- include/oar.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/oar.h b/include/oar.h index 0bba72c..2c5fdb1 100644 --- a/include/oar.h +++ b/include/oar.h @@ -213,6 +213,13 @@ int oar_remove_audio_element(oar_t *oar, uint32_t id); * ck_metadata_iamf_downmix_mode) are hints that take effect only * when the current rendering path has a consumer for them. * Returning ck_oar_ok means "accepted", not "took effect". + * @note Gain and object position metadata are queued per element, and + * each applies for its duration in samples, in the order pushed. + * Once rendering has used up the queued metadata of a type, + * metadata of that type pushed afterwards applies from the next + * rendered sample. Duration-only metadata cannot describe a gap: + * to hold a gain or a position over a span, push metadata that + * covers it. */ int oar_update_audio_element_metadata(oar_t *oar, uint32_t id, const oar_metadata_t *metadata);