Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions include/oar.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/olr/object_audio_renderer/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 13 additions & 3 deletions src/renderer/olr/olr.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions tests/examples/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
)
7 changes: 7 additions & 0 deletions tests/examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
155 changes: 155 additions & 0 deletions tests/examples/test_object_position_gap.c
Original file line number Diff line number Diff line change
@@ -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 <stdint.h>
#include <stdio.h>
#include <string.h>

#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);
}
Loading
Loading