From d0abe1052d10f1cb6858ab075084207b9fd830e5 Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Mon, 27 Jul 2026 09:21:12 +0100 Subject: [PATCH 01/11] Add Bazel module build for bazel_dep-style consumption Ports the fork's MODULE.bazel, BUILD.bazel, extensions.bzl and third_party/pffft.BUILD onto the public repo layout (code at the repository root instead of liboar/). --- .gitignore | 7 ++ BUILD.bazel | 151 ++++++++++++++++++++++++++++++++++++++++ MODULE.bazel | 13 ++++ extensions.bzl | 13 ++++ third_party/BUILD.bazel | 4 ++ third_party/pffft.BUILD | 8 +++ 6 files changed, 196 insertions(+) create mode 100644 BUILD.bazel create mode 100644 MODULE.bazel create mode 100644 extensions.bzl create mode 100644 third_party/BUILD.bazel create mode 100644 third_party/pffft.BUILD diff --git a/.gitignore b/.gitignore index 567609b..ea87ba1 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,8 @@ build/ + +# macOS +.DS_Store + +# Bazel build artifacts (standalone builds in this repo) +/bazel-* +/MODULE.bazel.lock diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 0000000..88c9d92 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,151 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +package(default_visibility = ["//visibility:public"]) + +# ───────────────────────────────────────────────────────────────────────────── +# OAR Core — Open Loudspeaker Renderer (OLR) + framework +# Everything under src/ except the OBR bridge and oar.c itself +# (those are compiled with -D__binauralizer__ below). +# ───────────────────────────────────────────────────────────────────────────── + +cc_library( + name = "oar_core", + srcs = glob( + ["src/**/*.c"], + exclude = [ + "src/renderer/obr/**", + "src/oar.c", + ], + ), + hdrs = glob(["include/*.h"]), + copts = ["-std=c11"], + includes = [ + "include", + "src", + "src/common", + "src/renderer", + "src/renderer/ear", + "src/renderer/olr", + "src/renderer/olr/object_audio_renderer", + "src/utility", + ], + textual_hdrs = glob( + ["src/**/*.h"], + exclude = ["src/renderer/obr/**"], + ), +) + +# ───────────────────────────────────────────────────────────────────────────── +# OBR — Open Binaural Renderer (nested google/obr subtree) +# Sources live at src/renderer/obr/obr_capi/obr/obr/**. +# Include root is src/renderer/obr/obr_capi/obr/ so callers use +# "obr/..." paths. +# ───────────────────────────────────────────────────────────────────────────── + +OBR_PREFIX = "src/renderer/obr/obr_capi/obr" + +cc_library( + name = "obr_lib", + srcs = glob( + [OBR_PREFIX + "/obr/**/*.cc"], + exclude = [ + OBR_PREFIX + "/obr/**/test*/**", + OBR_PREFIX + "/obr/**/tests/**", + OBR_PREFIX + "/obr/cli/**", + ], + ), + hdrs = glob( + [OBR_PREFIX + "/obr/**/*.h"], + exclude = [ + OBR_PREFIX + "/obr/**/test*/**", + OBR_PREFIX + "/obr/**/tests/**", + OBR_PREFIX + "/obr/cli/**", + ], + ), + copts = ["-std=c++20"], + includes = [OBR_PREFIX], + deps = [ + "@abseil-cpp//absl/base:no_destructor", + "@abseil-cpp//absl/container:btree", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/log:absl_check", + "@abseil-cpp//absl/log:absl_log", + "@abseil-cpp//absl/log:die_if_null", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/synchronization", + "@eigen", + "@pffft", + ], +) + +# ───────────────────────────────────────────────────────────────────────────── +# OBR C API wrapper (obr_capi.cpp wraps obr::ObrImpl in a C interface). +# ───────────────────────────────────────────────────────────────────────────── + +cc_library( + name = "obr_capi", + srcs = ["src/renderer/obr/obr_capi/obr_capi.cpp"], + hdrs = ["src/renderer/obr/obr_capi/obr_capi.h"], + copts = ["-std=c++20"], + includes = ["src/renderer/obr/obr_capi"], + deps = [":obr_lib"], +) + +# ───────────────────────────────────────────────────────────────────────────── +# OBR bridge (C glue between OAR core and OBR). +# ───────────────────────────────────────────────────────────────────────────── + +cc_library( + name = "obr_bridge", + srcs = ["src/renderer/obr/obr.c"], + hdrs = ["src/renderer/obr/obr.h"], + copts = [ + "-std=c11", + "-D__binauralizer__", + ], + includes = [ + "include", + "src", + "src/common", + "src/renderer", + "src/renderer/obr", + "src/utility", + ], + textual_hdrs = glob( + ["src/**/*.h"], + exclude = ["src/renderer/obr/obr_capi/**"], + ), + deps = [ + ":oar_core", + ":obr_capi", + ], +) + +# ───────────────────────────────────────────────────────────────────────────── +# Full OAR — OLR + OBR combined. +# oar.c is recompiled here WITH __binauralizer__ so it registers the OBR +# renderer library at init time. +# ───────────────────────────────────────────────────────────────────────────── + +cc_library( + name = "oar", + srcs = ["src/oar.c"], + copts = [ + "-std=c11", + "-D__binauralizer__", + ], + includes = [ + "include", + "src", + "src/common", + "src/renderer", + "src/utility", + ], + textual_hdrs = glob(["src/**/*.h"]), + deps = [ + ":oar_core", + ":obr_bridge", + ], +) diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..d2a0400 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,13 @@ +module( + name = "oar", + version = "0.1.0", +) + +bazel_dep(name = "rules_cc", version = "0.2.17") +bazel_dep(name = "abseil-cpp", version = "20260107.1") +bazel_dep(name = "eigen", version = "3.4.0.bcr.3") + +# pffft — small FFT library needed by OBR. Not on BCR; pulled from upstream +# bitbucket with a local BUILD shim. +non_module_deps = use_extension("//:extensions.bzl", "non_module_deps") +use_repo(non_module_deps, "pffft") diff --git a/extensions.bzl b/extensions.bzl new file mode 100644 index 0000000..983e87a --- /dev/null +++ b/extensions.bzl @@ -0,0 +1,13 @@ +"""Module extension for non-bzlmod dependencies of OAR.""" + +load("@bazel_tools//tools/build_defs/repo:git.bzl", "new_git_repository") + +def _non_module_deps_impl(_ctx): + new_git_repository( + name = "pffft", + build_file = "@oar//third_party:pffft.BUILD", + commit = "d7a4c0206a29423478776d6b23a37bbb308f21d5", + remote = "https://bitbucket.org/jpommier/pffft.git", + ) + +non_module_deps = module_extension(implementation = _non_module_deps_impl) diff --git a/third_party/BUILD.bazel b/third_party/BUILD.bazel new file mode 100644 index 0000000..0c44d4d --- /dev/null +++ b/third_party/BUILD.bazel @@ -0,0 +1,4 @@ +# Marks this directory as a Bazel package so pffft.BUILD can be referenced +# by `@oar//third_party:pffft.BUILD` from the module extension. (Bazel 9 +# reserves the top-level `external` package name, so the shim lives here.) +exports_files(["pffft.BUILD"]) diff --git a/third_party/pffft.BUILD b/third_party/pffft.BUILD new file mode 100644 index 0000000..30ae693 --- /dev/null +++ b/third_party/pffft.BUILD @@ -0,0 +1,8 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") + +cc_library( + name = "pffft", + srcs = ["pffft.c"], + hdrs = ["pffft.h"], + visibility = ["//visibility:public"], +) From b8ddeb7557b343d66882c2059915d3990e72cc07 Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Mon, 27 Jul 2026 09:21:12 +0100 Subject: [PATCH 02/11] Delete nested Bazel package markers inside OBR subtree so glob traverses Bazel globs do not cross package or module boundaries; the vendored OBR subtree's BUILD and MODULE.bazel files would stop the top-level targets from picking up its sources. --- src/renderer/obr/obr_capi/obr/BUILD | 10 -- src/renderer/obr/obr_capi/obr/MODULE.bazel | 36 ----- src/renderer/obr/obr_capi/obr/WORKSPACE | 26 ---- .../obr/obr/ambisonic_binaural_decoder/BUILD | 127 ------------------ .../binaural_filters/BUILD | 64 --------- .../ambisonic_binaural_decoder/tests/BUILD | 95 ------------- .../obr_capi/obr/obr/ambisonic_encoder/BUILD | 35 ----- .../obr/obr/ambisonic_encoder/tests/BUILD | 41 ------ .../obr_capi/obr/obr/ambisonic_rotator/BUILD | 18 --- .../obr/obr/ambisonic_rotator/tests/BUILD | 43 ------ .../obr/obr_capi/obr/obr/audio_buffer/BUILD | 48 ------- .../obr_capi/obr/obr/audio_buffer/tests/BUILD | 42 ------ src/renderer/obr/obr_capi/obr/obr/cli/BUILD | 40 ------ .../obr/obr_capi/obr/obr/cli/proto/BUILD | 15 --- .../obr/obr_capi/obr/obr/cli/testdata/BUILD | 14 -- .../obr/obr_capi/obr/obr/cli/tests/BUILD | 20 --- .../obr/obr_capi/obr/obr/common/BUILD | 35 ----- .../obr/obr_capi/obr/obr/common/tests/BUILD | 30 ----- .../obr/obr_capi/obr/obr/peak_limiter/BUILD | 14 -- .../obr/obr_capi/obr/obr/renderer/BUILD | 108 --------------- .../obr/obr_capi/obr/obr/renderer/tests/BUILD | 63 --------- 21 files changed, 924 deletions(-) delete mode 100644 src/renderer/obr/obr_capi/obr/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/MODULE.bazel delete mode 100644 src/renderer/obr/obr_capi/obr/WORKSPACE delete mode 100644 src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/binaural_filters/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/tests/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/tests/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/tests/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/audio_buffer/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/audio_buffer/tests/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/cli/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/cli/proto/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/cli/testdata/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/cli/tests/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/common/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/common/tests/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/peak_limiter/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/renderer/BUILD delete mode 100644 src/renderer/obr/obr_capi/obr/obr/renderer/tests/BUILD diff --git a/src/renderer/obr/obr_capi/obr/BUILD b/src/renderer/obr/obr_capi/obr/BUILD deleted file mode 100644 index b312c05..0000000 --- a/src/renderer/obr/obr_capi/obr/BUILD +++ /dev/null @@ -1,10 +0,0 @@ -# [internal] licensing conditions. -# Open Binaural Renderer (obr). - -# [internal] load license.bzl - -# [internal] license - -licenses(["by_exception_only"]) - -exports_files(["LICENSE"]) diff --git a/src/renderer/obr/obr_capi/obr/MODULE.bazel b/src/renderer/obr/obr_capi/obr/MODULE.bazel deleted file mode 100644 index 1290999..0000000 --- a/src/renderer/obr/obr_capi/obr/MODULE.bazel +++ /dev/null @@ -1,36 +0,0 @@ -############################################################################### -# Bazel now uses Bzlmod by default to manage external dependencies. -# Please consider migrating your external dependencies from WORKSPACE to MODULE.bazel. -# -# For more details, please check https://github.com/bazelbuild/bazel/issues/18958 -############################################################################### - -module( - name = "obr", - version = "1.0.0", -) - -bazel_dep(name = "rules_cc", version = "0.1.2") -bazel_dep( - name = "abseil-cpp", - version = "20250512.1", -) -bazel_dep( - name = "googletest", - version = "1.17.0", - repo_name = "com_google_googletest", -) -bazel_dep( - name = "protobuf", - version = "32.0", - repo_name = "com_google_protobuf", -) -bazel_dep( - name = "google_benchmark", - version = "1.9.4", - repo_name = "com_google_benchmark", -) -bazel_dep( - name = "eigen", - version = "3.4.0.bcr.3", -) diff --git a/src/renderer/obr/obr_capi/obr/WORKSPACE b/src/renderer/obr/obr_capi/obr/WORKSPACE deleted file mode 100644 index a254478..0000000 --- a/src/renderer/obr/obr_capi/obr/WORKSPACE +++ /dev/null @@ -1,26 +0,0 @@ -################################################################################ -# This project now manages dependencies with both bazelmod and WORKSPACE, -# with the goal of moving all dependencies to bazelmod. -# -# For more details, please check https://github.com/bazelbuild/bazel/issues/18958. -################################################################################ - -load( - "@bazel_tools//tools/build_defs/repo:git.bzl", - "git_repository", -) - -# Google Audio-to-Tactile Lib -git_repository( - name = "com_google_audio_to_tactile", - commit = "d3f449fdfd8cfe4a845d0ae244fce2a0bca34a15", - remote = "https://github.com/google/audio-to-tactile.git", -) - -# PFFFT -git_repository( - name = "pffft", - build_file = "pffft.BUILD", - commit = "d7a4c0206a29423478776d6b23a37bbb308f21d5", - remote = "https://bitbucket.org/jpommier/pffft.git", -) diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/BUILD b/src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/BUILD deleted file mode 100644 index c31ee16..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/BUILD +++ /dev/null @@ -1,127 +0,0 @@ -load("@rules_cc//cc:cc_library.bzl", "cc_library") - -package(default_visibility = ["//obr:__subpackages__"]) - -cc_library( - name = "ambisonic_binaural_decoder", - srcs = [ - "ambisonic_binaural_decoder.cc", - ], - hdrs = ["ambisonic_binaural_decoder.h"], - deps = [ - ":fft_manager", - ":partitioned_fft_filter", - "//obr/audio_buffer", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - ], -) - -cc_library( - name = "dsp_utils", - srcs = ["dsp_utils.cc"], - hdrs = ["dsp_utils.h"], - deps = [ - "//obr/audio_buffer", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - ], -) - -cc_library( - name = "fft_manager", - srcs = ["fft_manager.cc"], - hdrs = ["fft_manager.h"], - deps = [ - "//obr/audio_buffer", - "//obr/audio_buffer:simd_utils", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - "@pffft", - ], -) - -cc_library( - name = "partitioned_fft_filter", - srcs = ["partitioned_fft_filter.cc"], - hdrs = ["partitioned_fft_filter.h"], - deps = [ - ":dsp_utils", - ":fft_manager", - "//obr/audio_buffer", - "//obr/audio_buffer:simd_utils", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - ], -) - -cc_library( - name = "planar_interleaved_conversion", - srcs = ["planar_interleaved_conversion.cc"], - hdrs = ["planar_interleaved_conversion.h"], - deps = [ - ":sample_type_conversion", - "//obr/audio_buffer", - "//obr/audio_buffer:simd_utils", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - ], -) - -cc_library( - name = "resampler", - srcs = ["resampler.cc"], - hdrs = ["resampler.h"], - deps = [ - ":dsp_utils", - "//obr/audio_buffer", - "//obr/audio_buffer:simd_utils", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - ], -) - -cc_library( - name = "sample_type_conversion", - srcs = ["sample_type_conversion.cc"], - hdrs = ["sample_type_conversion.h"], - deps = [ - "//obr/audio_buffer:simd_utils", - "@abseil-cpp//absl/log:absl_check", - ], -) - -cc_library( - name = "sh_hrir_creator", - srcs = ["sh_hrir_creator.cc"], - hdrs = ["sh_hrir_creator.h"], - deps = [ - ":planar_interleaved_conversion", - ":resampler", - ":wav", - "//obr/ambisonic_binaural_decoder/binaural_filters:binaural_filters_wrapper", - "//obr/audio_buffer", - "//obr/audio_buffer:simd_utils", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - "@abseil-cpp//absl/log:absl_log", - "@abseil-cpp//absl/log:die_if_null", - ], -) - -cc_library( - name = "wav", - srcs = ["wav.cc"], - hdrs = ["wav.h"], - deps = [":wav_reader"], -) - -cc_library( - name = "wav_reader", - srcs = ["wav_reader.cc"], - hdrs = ["wav_reader.h"], - deps = [ - "@abseil-cpp//absl/log:absl_check", - "@abseil-cpp//absl/log:die_if_null", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/binaural_filters/BUILD b/src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/binaural_filters/BUILD deleted file mode 100644 index a29d3bd..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/binaural_filters/BUILD +++ /dev/null @@ -1,64 +0,0 @@ -load("@rules_cc//cc:cc_library.bzl", "cc_library") - -package(default_visibility = ["//obr:__subpackages__"]) - -cc_library( - name = "binaural_filters_wrapper", - srcs = [ - "binaural_filters_1_oa_ambient_l.cc", - "binaural_filters_1_oa_ambient_r.cc", - "binaural_filters_1_oa_direct_l.cc", - "binaural_filters_1_oa_direct_r.cc", - "binaural_filters_1_oa_reverberant_l.cc", - "binaural_filters_1_oa_reverberant_r.cc", - "binaural_filters_2_oa_ambient_l.cc", - "binaural_filters_2_oa_ambient_r.cc", - "binaural_filters_2_oa_direct_l.cc", - "binaural_filters_2_oa_direct_r.cc", - "binaural_filters_2_oa_reverberant_l.cc", - "binaural_filters_2_oa_reverberant_r.cc", - "binaural_filters_3_oa_ambient_l.cc", - "binaural_filters_3_oa_ambient_r.cc", - "binaural_filters_3_oa_direct_l.cc", - "binaural_filters_3_oa_direct_r.cc", - "binaural_filters_3_oa_reverberant_l.cc", - "binaural_filters_3_oa_reverberant_r.cc", - "binaural_filters_4_oa_ambient_l.cc", - "binaural_filters_4_oa_ambient_r.cc", - "binaural_filters_4_oa_direct_l.cc", - "binaural_filters_4_oa_direct_r.cc", - "binaural_filters_4_oa_reverberant_l.cc", - "binaural_filters_4_oa_reverberant_r.cc", - "binaural_filters_wrapper.cc", - ], - hdrs = [ - "binaural_filters_1_oa_ambient_l.h", - "binaural_filters_1_oa_ambient_r.h", - "binaural_filters_1_oa_direct_l.h", - "binaural_filters_1_oa_direct_r.h", - "binaural_filters_1_oa_reverberant_l.h", - "binaural_filters_1_oa_reverberant_r.h", - "binaural_filters_2_oa_ambient_l.h", - "binaural_filters_2_oa_ambient_r.h", - "binaural_filters_2_oa_direct_l.h", - "binaural_filters_2_oa_direct_r.h", - "binaural_filters_2_oa_reverberant_l.h", - "binaural_filters_2_oa_reverberant_r.h", - "binaural_filters_3_oa_ambient_l.h", - "binaural_filters_3_oa_ambient_r.h", - "binaural_filters_3_oa_direct_l.h", - "binaural_filters_3_oa_direct_r.h", - "binaural_filters_3_oa_reverberant_l.h", - "binaural_filters_3_oa_reverberant_r.h", - "binaural_filters_4_oa_ambient_l.h", - "binaural_filters_4_oa_ambient_r.h", - "binaural_filters_4_oa_direct_l.h", - "binaural_filters_4_oa_direct_r.h", - "binaural_filters_4_oa_reverberant_l.h", - "binaural_filters_4_oa_reverberant_r.h", - "binaural_filters_wrapper.h", - ], - deps = [ - "@abseil-cpp//absl/types:span", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/tests/BUILD b/src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/tests/BUILD deleted file mode 100644 index 82177ea..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/tests/BUILD +++ /dev/null @@ -1,95 +0,0 @@ -load("@rules_cc//cc:cc_test.bzl", "cc_test") - -cc_test( - name = "ambisonic_binaural_decoder_test", - srcs = [ - "ambisonic_binaural_decoder_test.cc", - ], - deps = [ - "//obr/ambisonic_binaural_decoder", - "//obr/ambisonic_binaural_decoder:fft_manager", - "//obr/audio_buffer", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "dsp_utils_test", - srcs = ["dsp_utils_test.cc"], - deps = [ - "//obr/ambisonic_binaural_decoder:dsp_utils", - "//obr/audio_buffer", - "//obr/common", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "fft_manager_test", - srcs = ["fft_manager_test.cc"], - deps = [ - "//obr/ambisonic_binaural_decoder:fft_manager", - "//obr/audio_buffer", - "//obr/common", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "partitioned_fft_filter_test", - srcs = ["partitioned_fft_filter_test.cc"], - deps = [ - "//obr/ambisonic_binaural_decoder:fft_manager", - "//obr/ambisonic_binaural_decoder:partitioned_fft_filter", - "//obr/audio_buffer", - "//obr/common", - "//obr/common:test_util", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "planar_interleaved_conversion_test", - srcs = ["planar_interleaved_conversion_test.cc"], - deps = [ - "//obr/ambisonic_binaural_decoder:planar_interleaved_conversion", - "//obr/audio_buffer", - "@abseil-cpp//absl/log:absl_check", - "@abseil-cpp//absl/log:absl_log", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "resampler_test", - srcs = ["resampler_test.cc"], - deps = [ - "//obr/ambisonic_binaural_decoder:resampler", - "//obr/audio_buffer", - "//obr/common", - "//obr/common:test_util", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "sample_type_conversion_test", - srcs = ["sample_type_conversion_test.cc"], - deps = [ - "//obr/ambisonic_binaural_decoder:sample_type_conversion", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "sh_hrir_creator_test", - srcs = ["sh_hrir_creator_test.cc"], - deps = [ - "//obr/ambisonic_binaural_decoder:resampler", - "//obr/ambisonic_binaural_decoder:sh_hrir_creator", - "//obr/audio_buffer", - "@com_google_googletest//:gtest_main", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/BUILD b/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/BUILD deleted file mode 100644 index 181eb6d..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/BUILD +++ /dev/null @@ -1,35 +0,0 @@ -load("@rules_cc//cc:cc_library.bzl", "cc_library") - -package(default_visibility = ["//obr:__subpackages__"]) - -cc_library( - name = "ambisonic_encoder", - srcs = [ - "ambisonic_encoder.cc", - ], - hdrs = ["ambisonic_encoder.h"], - visibility = ["//visibility:public"], - deps = [ - ":associated_legendre_polynomials_generator", - "//obr/audio_buffer", - "//obr/common", - "@abseil-cpp//absl/container:flat_hash_map", - "@abseil-cpp//absl/log:absl_check", - "@eigen", - ], -) - -cc_library( - name = "associated_legendre_polynomials_generator", - srcs = [ - "associated_legendre_polynomials_generator.cc", - ], - hdrs = [ - "associated_legendre_polynomials_generator.h", - ], - visibility = ["//visibility:public"], - deps = [ - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/tests/BUILD b/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/tests/BUILD deleted file mode 100644 index 94194fa..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/tests/BUILD +++ /dev/null @@ -1,41 +0,0 @@ -load("@rules_cc//cc:cc_test.bzl", "cc_test") - -# keep-sorted start block=yes prefix_order=cc_test newline_separated=yes -# Benchmark with -# `bazel run -c opt :ambisonic_encoder_benchmark -- --benchmark_filter=.` -cc_test( - name = "ambisonic_encoder_benchmark", - srcs = ["ambisonic_encoder_benchmark.cc"], - deps = [ - "//obr/ambisonic_encoder", - "//obr/audio_buffer", - "//obr/common", - "@com_google_benchmark//:benchmark_main", - ], -) - -cc_test( - name = "ambisonic_encoder_test", - srcs = ["ambisonic_encoder_test.cc"], - deps = [ - "//obr/ambisonic_encoder", - "//obr/audio_buffer", - "//obr/common", - "@abseil-cpp//absl/container:flat_hash_map", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "associated_legendre_polynomials_generator_test", - srcs = [ - "associated_legendre_polynomials_generator_test.cc", - ], - deps = [ - "//obr/ambisonic_encoder:associated_legendre_polynomials_generator", - "//obr/common", - "@com_google_googletest//:gtest_main", - ], -) - -# keep-sorted end diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/BUILD b/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/BUILD deleted file mode 100644 index 697af94..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/BUILD +++ /dev/null @@ -1,18 +0,0 @@ -load("@rules_cc//cc:cc_library.bzl", "cc_library") - -package(default_visibility = ["//src:__subpackages__"]) - -cc_library( - name = "ambisonic_rotator", - srcs = [ - "ambisonic_rotator.cc", - ], - hdrs = ["ambisonic_rotator.h"], - visibility = ["//visibility:public"], - deps = [ - "//obr/audio_buffer", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - "@eigen", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/tests/BUILD b/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/tests/BUILD deleted file mode 100644 index b7a6e06..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/tests/BUILD +++ /dev/null @@ -1,43 +0,0 @@ -load("@rules_cc//cc:cc_library.bzl", "cc_library") -load("@rules_cc//cc:cc_test.bzl", "cc_test") - -package(default_visibility = ["//src:__subpackages__"]) - -cc_test( - name = "ambisonic_rotator_test", - srcs = [ - "ambisonic_rotator_test.cc", - ], - deps = [ - ":spherical_angle", - "//obr/ambisonic_binaural_decoder:planar_interleaved_conversion", - "//obr/ambisonic_encoder", - "//obr/ambisonic_rotator", - "//obr/audio_buffer", - "//obr/common", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "spherical_angle_test", - srcs = [ - "spherical_angle_test.cc", - ], - deps = [ - ":spherical_angle", - "//obr/common", - "@com_google_googletest//:gtest_main", - ], -) - -cc_library( - name = "spherical_angle", - srcs = [ - "spherical_angle.cc", - ], - hdrs = ["spherical_angle.h"], - deps = [ - "//obr/common", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/audio_buffer/BUILD b/src/renderer/obr/obr_capi/obr/obr/audio_buffer/BUILD deleted file mode 100644 index d1496e0..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/audio_buffer/BUILD +++ /dev/null @@ -1,48 +0,0 @@ -load("@rules_cc//cc:cc_library.bzl", "cc_library") - -package(default_visibility = ["//obr:__subpackages__"]) - -cc_library( - name = "audio_buffer", - srcs = [ - "audio_buffer.cc", - ], - hdrs = [ - "aligned_allocator.h", - "audio_buffer.h", - ], - visibility = ["//visibility:public"], - deps = [ - ":channel_view", - ":simd_utils", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - ], -) - -cc_library( - name = "channel_view", - srcs = ["channel_view.cc"], - hdrs = ["channel_view.h"], - deps = [ - ":simd_utils", - "@abseil-cpp//absl/log:absl_check", - ], -) - -cc_library( - name = "simd_macros", - hdrs = ["simd_macros.h"], - deps = ["//obr/common"], -) - -cc_library( - name = "simd_utils", - srcs = ["simd_utils.cc"], - hdrs = ["simd_utils.h"], - deps = [ - ":simd_macros", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/audio_buffer/tests/BUILD b/src/renderer/obr/obr_capi/obr/obr/audio_buffer/tests/BUILD deleted file mode 100644 index 82d5780..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/audio_buffer/tests/BUILD +++ /dev/null @@ -1,42 +0,0 @@ -load("@rules_cc//cc:cc_test.bzl", "cc_test") - -cc_test( - name = "audio_buffer_test", - srcs = ["audio_buffer_test.cc"], - deps = [ - "//obr/audio_buffer", - "//obr/common", - "//obr/common:test_util", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "channel_view_test", - srcs = ["channel_view_test.cc"], - deps = [ - "//obr/audio_buffer", - "//obr/audio_buffer:channel_view", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "simd_utils_test", - srcs = ["simd_utils_test.cc"], - deps = [ - "//obr/audio_buffer", - "//obr/audio_buffer:simd_utils", - "//obr/common", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "aligned_allocator_test", - srcs = ["aligned_allocator_test.cc"], - deps = [ - "//obr/audio_buffer", - "@com_google_googletest//:gtest_main", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/cli/BUILD b/src/renderer/obr/obr_capi/obr/obr/cli/BUILD deleted file mode 100644 index 46889b7..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/cli/BUILD +++ /dev/null @@ -1,40 +0,0 @@ -load("@rules_cc//cc:cc_binary.bzl", "cc_binary") -load("@rules_cc//cc:cc_library.bzl", "cc_library") - -package(default_visibility = ["//obr:__subpackages__"]) - -cc_library( - name = "obr_cli_lib", - srcs = ["obr_cli_lib.cc"], - hdrs = ["obr_cli_lib.h"], - deps = [ - "//obr/audio_buffer", - "//obr/cli/proto:oba_metadata_cc_proto", - "//obr/renderer:audio_element_config", - "//obr/renderer:audio_element_type", - "//obr/renderer:obr_impl", - "@abseil-cpp//absl/log:absl_check", - "@abseil-cpp//absl/log:absl_log", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/strings", - "@com_google_audio_to_tactile//:dsp", - "@com_google_protobuf//:protobuf", - ], -) - -cc_binary( - name = "obr_cli", - srcs = ["obr_cli.cc"], - deps = [ - ":obr_cli_lib", - "//obr/renderer:audio_element_config", - "//obr/renderer:audio_element_type", - "@abseil-cpp//absl/flags:flag", - "@abseil-cpp//absl/flags:parse", - "@abseil-cpp//absl/flags:usage", - "@abseil-cpp//absl/log:absl_log", - "@abseil-cpp//absl/log:flags", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/strings", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/cli/proto/BUILD b/src/renderer/obr/obr_capi/obr/obr/cli/proto/BUILD deleted file mode 100644 index 7801f07..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/cli/proto/BUILD +++ /dev/null @@ -1,15 +0,0 @@ -# [internal] load cc_proto_library.bzl -load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") - -package(default_visibility = ["//obr/cli:__subpackages__"]) - -# Proto for obr_cli OBA input metadata. -proto_library( - name = "oba_metadata_proto", - srcs = ["oba_metadata.proto"], -) - -cc_proto_library( - name = "oba_metadata_cc_proto", - deps = [":oba_metadata_proto"], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/cli/testdata/BUILD b/src/renderer/obr/obr_capi/obr/obr/cli/testdata/BUILD deleted file mode 100644 index fe2985d..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/cli/testdata/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -package(default_visibility = ["//obr:__subpackages__"]) - -# keep-sorted start block=yes prefix_order=filegroup newline_separated=yes -filegroup( - name = "input_wav_files", - srcs = glob(["*.wav"]), -) - -filegroup( - name = "textprotos", - srcs = glob(["*.textproto"]), -) - -# keep-sorted end diff --git a/src/renderer/obr/obr_capi/obr/obr/cli/tests/BUILD b/src/renderer/obr/obr_capi/obr/obr/cli/tests/BUILD deleted file mode 100644 index a0698e1..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/cli/tests/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -load("@rules_cc//cc:cc_test.bzl", "cc_test") - -package(default_visibility = ["//obr/cli/tests:__subpackages__"]) - -cc_test( - name = "obr_cli_lib_test", - srcs = ["obr_cli_lib_test.cc"], - data = [ - "//obr/cli/testdata:input_wav_files", - "//obr/cli/testdata:textprotos", - ], - shard_count = 4, - deps = [ - "//obr/cli:obr_cli_lib", - "//obr/renderer:audio_element_config", - "//obr/renderer:audio_element_type", - "@abseil-cpp//absl/strings", - "@com_google_googletest//:gtest_main", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/common/BUILD b/src/renderer/obr/obr_capi/obr/obr/common/BUILD deleted file mode 100644 index b20ef17..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/common/BUILD +++ /dev/null @@ -1,35 +0,0 @@ -load("@rules_cc//cc:cc_library.bzl", "cc_library") - -package(default_visibility = ["//obr:__subpackages__"]) - -cc_library( - name = "common", - hdrs = [ - "ambisonic_utils.h", - "constants.h", - "misc_math.h", - "status_macros.h", - ], - deps = [ - "@abseil-cpp//absl/log:absl_check", - "@eigen", - ], -) - -cc_library( - name = "test_util", - testonly = True, - srcs = [ - "test_util.cc", - ], - hdrs = [ - "test_util.h", - ], - deps = [ - ":common", - "//obr/ambisonic_encoder", - "//obr/audio_buffer", - "@abseil-cpp//absl/log:absl_check", - "@com_google_googletest//:gtest", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/common/tests/BUILD b/src/renderer/obr/obr_capi/obr/obr/common/tests/BUILD deleted file mode 100644 index 1bc40de..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/common/tests/BUILD +++ /dev/null @@ -1,30 +0,0 @@ -load("@rules_cc//cc:cc_test.bzl", "cc_test") - -cc_test( - name = "ambisonic_utils_test", - srcs = ["ambisonic_utils_test.cc"], - deps = [ - "//obr/common", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "misc_math_test", - srcs = ["misc_math_test.cc"], - deps = [ - "//obr/common", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "test_util_test", - srcs = ["test_util_test.cc"], - deps = [ - "//obr/audio_buffer", - "//obr/common", - "//obr/common:test_util", - "@com_google_googletest//:gtest_main", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/peak_limiter/BUILD b/src/renderer/obr/obr_capi/obr/obr/peak_limiter/BUILD deleted file mode 100644 index ae5dbd4..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/peak_limiter/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -load("@rules_cc//cc:cc_library.bzl", "cc_library") - -package(default_visibility = ["//obr:__subpackages__"]) - -cc_library( - name = "peak_limiter", - srcs = ["peak_limiter.cc"], - hdrs = ["peak_limiter.h"], - visibility = ["//visibility:public"], - deps = [ - "//obr/audio_buffer", - "@abseil-cpp//absl/log:absl_check", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/renderer/BUILD b/src/renderer/obr/obr_capi/obr/obr/renderer/BUILD deleted file mode 100644 index 9d46883..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/renderer/BUILD +++ /dev/null @@ -1,108 +0,0 @@ -load("@rules_cc//cc:cc_library.bzl", "cc_library") - -package(default_visibility = ["//obr:__subpackages__"]) - -cc_library( - name = "audio_element_config", - srcs = ["audio_element_config.cc"], - hdrs = ["audio_element_config.h"], - deps = [ - ":audio_element_type", - ":input_channel_config", - ":loudspeaker_layouts", - "//obr/common", - "@abseil-cpp//absl/container:btree", - "@abseil-cpp//absl/log:absl_check", - "@abseil-cpp//absl/log:absl_log", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/status:statusor", - "@abseil-cpp//absl/strings", - ], -) - -cc_library( - name = "audio_element_type", - hdrs = ["audio_element_type.h"], - visibility = ["//visibility:public"], - deps = [ - "@abseil-cpp//absl/base:no_destructor", - "@abseil-cpp//absl/container:btree", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/status:statusor", - "@abseil-cpp//absl/strings", - ], -) - -cc_library( - name = "audio_element_table_formatter", - srcs = ["audio_element_table_formatter.cc"], - hdrs = ["audio_element_table_formatter.h"], - deps = [ - ":audio_element_config", - ":audio_element_type", - "@abseil-cpp//absl/log:absl_log", - ], -) - -cc_library( - name = "processing_group", - srcs = ["processing_group.cc"], - hdrs = ["processing_group.h"], - deps = [ - ":audio_element_config", - ":audio_element_type", - "//obr/ambisonic_binaural_decoder", - "//obr/ambisonic_binaural_decoder:fft_manager", - "//obr/ambisonic_binaural_decoder:resampler", - "//obr/ambisonic_binaural_decoder:sh_hrir_creator", - "//obr/ambisonic_encoder", - "//obr/ambisonic_rotator", - "//obr/audio_buffer", - "//obr/common", - "@abseil-cpp//absl/log:absl_check", - "@abseil-cpp//absl/log:absl_log", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/strings", - ], -) - -cc_library( - name = "obr_impl", - srcs = [ - "obr_impl.cc", - ], - hdrs = ["obr_impl.h"], - visibility = ["//visibility:public"], - deps = [ - ":audio_element_config", - ":audio_element_table_formatter", - ":audio_element_type", - ":processing_group", - "//obr/ambisonic_binaural_decoder:fft_manager", - "//obr/ambisonic_binaural_decoder:resampler", - "//obr/audio_buffer", - "//obr/common", - "//obr/peak_limiter", - "@abseil-cpp//absl/log:absl_check", - "@abseil-cpp//absl/log:absl_log", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/synchronization", - ], -) - -cc_library( - name = "input_channel_config", - hdrs = ["input_channel_config.h"], - deps = [], -) - -cc_library( - name = "loudspeaker_layouts", - hdrs = ["loudspeaker_layouts.h"], - deps = [ - ":audio_element_type", - ":input_channel_config", - "@abseil-cpp//absl/container:flat_hash_map", - "@abseil-cpp//absl/log:absl_log", - ], -) diff --git a/src/renderer/obr/obr_capi/obr/obr/renderer/tests/BUILD b/src/renderer/obr/obr_capi/obr/obr/renderer/tests/BUILD deleted file mode 100644 index 390d20d..0000000 --- a/src/renderer/obr/obr_capi/obr/obr/renderer/tests/BUILD +++ /dev/null @@ -1,63 +0,0 @@ -load("@rules_cc//cc:cc_test.bzl", "cc_test") - -cc_test( - name = "audio_element_config_test", - srcs = ["audio_element_config_test.cc"], - deps = [ - "//obr/renderer:audio_element_config", - "//obr/renderer:audio_element_type", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "audio_element_type_test", - srcs = ["audio_element_type_test.cc"], - deps = [ - "//obr/renderer:audio_element_type", - "@abseil-cpp//absl/status:status_matchers", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "obr_impl_test", - srcs = ["obr_impl_test.cc"], - deps = [ - "//obr/audio_buffer", - "//obr/common:test_util", - "//obr/renderer:audio_element_config", - "//obr/renderer:audio_element_type", - "//obr/renderer:obr_impl", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/status:status_matchers", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "processing_group_test", - srcs = ["processing_group_test.cc"], - deps = [ - "//obr/ambisonic_binaural_decoder:fft_manager", - "//obr/ambisonic_binaural_decoder:resampler", - "//obr/audio_buffer", - "//obr/common", - "//obr/common:test_util", - "//obr/renderer:audio_element_config", - "//obr/renderer:audio_element_type", - "//obr/renderer:processing_group", - "@abseil-cpp//absl/status:status_matchers", - "@com_google_googletest//:gtest_main", - ], -) - -cc_test( - name = "loudspeaker_layouts_test", - srcs = ["loudspeaker_layouts_test.cc"], - deps = [ - "//obr/renderer:audio_element_type", - "//obr/renderer:loudspeaker_layouts", - "@com_google_googletest//:gtest_main", - ], -) From bcae9ae8b5dd9415778db4b9a55752d6cfd6b4f9 Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Wed, 15 Apr 2026 23:00:46 +0100 Subject: [PATCH 03/11] Expose oar_get_limiter_env() API for per-frame envelope tracking --- include/oar.h | 11 +++++++++++ src/limiter/oar_limiter.c | 37 ++++++++++++++++++++++++------------- src/limiter/oar_limiter.h | 8 +++++++- src/oar.c | 8 +++++++- 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/include/oar.h b/include/oar.h index 06e48fc..05b6d8f 100644 --- a/include/oar.h +++ b/include/oar.h @@ -270,6 +270,17 @@ int oar_set_loudness(oar_t *oar, uint32_t gid, float loudness, */ int oar_enable_limiter(oar_t *oar, int enable); +/** + * @brief Get the current limiter envelope value. + * + * Returns the limiter gain multiplier after the most recent oar_render() call. + * 1.0 means no gain reduction; values below 1.0 indicate active limiting. + * + * @param [in] oar : OAR object + * @return Limiter envelope (0..1], or 1.0 if limiter is NULL/disabled. + */ +double oar_get_limiter_env(const oar_t *oar); + /** * @brief Enable or disable head tracking for all audio elements. * diff --git a/src/limiter/oar_limiter.c b/src/limiter/oar_limiter.c index 1be074c..9270f49 100644 --- a/src/limiter/oar_limiter.c +++ b/src/limiter/oar_limiter.c @@ -20,12 +20,22 @@ #include "definitions.h" oar_limiter_t* oar_limiter_create(int sampling_rate, double release_ms, - double ceiling_db) { - if (sampling_rate <= 0 || release_ms <= 0) return 0; + double ceiling_db, uint32_t max_frames) { + if (sampling_rate <= 0 || release_ms <= 0 || max_frames == 0) return 0; oar_limiter_t* limiter = def_mallocz(oar_limiter_t, 1); if (!limiter) return 0; + limiter->max_samples = (float*)calloc(max_frames, sizeof(float)); + limiter->limiter_env = (float*)calloc(max_frames, sizeof(float)); + if (!limiter->max_samples || !limiter->limiter_env) { + free(limiter->max_samples); + free(limiter->limiter_env); + def_free(limiter); + return 0; + } + limiter->capacity = max_frames; + limiter->sampling_rate = sampling_rate; limiter->ceiling = pow(10.0, ceiling_db / 20.0); limiter->release_time_constant = @@ -35,11 +45,17 @@ oar_limiter_t* oar_limiter_create(int sampling_rate, double release_ms, return limiter; } -void oar_limiter_destroy(oar_limiter_t* limiter) { def_free(limiter); } +void oar_limiter_destroy(oar_limiter_t* limiter) { + if (limiter) { + free(limiter->max_samples); + free(limiter->limiter_env); + } + def_free(limiter); +} static double GetMaximumRequiredGain(const oar_limiter_t* limiter, double sample) { - if (!limiter) return 1.0; // Should not happen if called from Process + if (!limiter) return 1.0; return fabs(sample) > limiter->ceiling ? limiter->ceiling / fabs(sample) : 1.0; } @@ -51,15 +67,12 @@ int oar_limiter_process(oar_limiter_t* limiter, oar_audio_block_t* block) { uint32_t num_frames = block->samples_per_channel; if (num_channels == 0 || num_frames == 0) return ck_oar_error_inval; + if (num_frames > limiter->capacity) return ck_oar_error_inval; - float* max_samples = def_mallocz(float, num_frames); - float* limiter_env = def_mallocz(float, num_frames); + float* max_samples = limiter->max_samples; + float* limiter_env = limiter->limiter_env; - if (!max_samples || !limiter_env) { - def_free(max_samples); - def_free(limiter_env); - return ck_oar_error_nomem; - } + memset(max_samples, 0, sizeof(float) * num_frames); for (uint32_t c = 0; c < num_channels; ++c) { const float* channel_data = block->data + (c * num_frames); @@ -90,7 +103,5 @@ int oar_limiter_process(oar_limiter_t* limiter, oar_audio_block_t* block) { } } - free(max_samples); - free(limiter_env); return ck_oar_ok; } diff --git a/src/limiter/oar_limiter.h b/src/limiter/oar_limiter.h index aba18ef..3fd9330 100644 --- a/src/limiter/oar_limiter.h +++ b/src/limiter/oar_limiter.h @@ -22,6 +22,8 @@ #ifndef _OAR_LIMITER_H_ #define _OAR_LIMITER_H_ +#include + #include "oar_base.h" // For oar_audio_block_t #ifdef __cplusplus @@ -34,6 +36,9 @@ typedef struct OarLimiter { double ceiling; double release_time_constant; double env; + float* max_samples; /**< Pre-allocated per-frame peak buffer. */ + float* limiter_env; /**< Pre-allocated per-frame envelope buffer. */ + uint32_t capacity; /**< Max frames the buffers can hold. */ } oar_limiter_t; /*!\brief Constructor for oar_limiter_t. @@ -41,11 +46,12 @@ typedef struct OarLimiter { * \param sampling_rate Sampling rate of the audio data. * \param release_ms Release time in milliseconds. * \param ceiling_db Ceiling level in decibels. + * \param max_frames Maximum number of frames per process call. * \return A pointer to the newly created oar_limiter_t instance, or NULL on * failure. */ oar_limiter_t* oar_limiter_create(int sampling_rate, double release_ms, - double ceiling_db); + double ceiling_db, uint32_t max_frames); /*!\brief Destructor for oar_limiter_t. * diff --git a/src/oar.c b/src/oar.c index b8f0b19..7643a3c 100644 --- a/src/oar.c +++ b/src/oar.c @@ -151,7 +151,8 @@ oar_t *oar_create(const oar_config_t *config) { oar->limiter = oar_limiter_create(config->sampling_rate, def_limiter_release_ms, - def_limiter_threshold_dbfs); + def_limiter_threshold_dbfs, + config->samples_per_channel); if (!oar->limiter) { oar_destroy(oar); return 0; @@ -592,6 +593,11 @@ int oar_enable_limiter(oar_t *oar, int enable) { return ck_oar_ok; } +double oar_get_limiter_env(const oar_t *oar) { + if (!oar || !oar->limiter) return 1.0; + return oar->limiter->env; +} + int oar_enable_head_tracking(oar_t *oar, int enable) { int i, j; From c02576018c6017b41b90b1e0b7cb04a61285e38b Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Mon, 27 Jul 2026 09:21:49 +0100 Subject: [PATCH 04/11] Add CLAUDE.md documenting fork rationale and upstream relationship --- CLAUDE.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e2e3af7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,42 @@ +# CLAUDE.md — fork notes + +This repo is a fork of the AOM Open Audio Renderer. + +- `origin` → `eclipsa-audio/oar` (this fork, read/write) +- `upstream` → `AOMediaCodec/oar` (fetch-only; push URL set to `no_push`) + +The public `AOMediaCodec/oar` repo is a fresh cut (no shared history with the old `oar-private` staging repo) with the code moved from `liboar/` to the repository root. This fork's `main` was rebuilt on top of it in July 2026; the pre-rebuild history is archived on the `archive/oar-private-main` branch. + +## Why the fork exists + +Eclipsa's engine consumes OAR+OBR as a Bazel module. Upstream ships a CMake-only build, so this fork adds a `MODULE.bazel` plus the BUILD files needed for `bazel_dep`-style consumption. Fork-local changes are limited to what's needed for that integration. + +## Fork-specific changes (on top of upstream) + +- `MODULE.bazel`, `BUILD.bazel`, `extensions.bzl`, `third_party/pffft.BUILD` — Bazel module build. +- `oar_get_limiter_env()` — public API exposing per-frame limiter envelope (used by the engine for metering). +- Nested Bazel package markers inside the OBR subtree (`src/renderer/obr/obr_capi/obr/`) deleted so the top-level glob traverses. + +### Bug fixes pending upstreaming + +These are merged into our `main` and also pushed as branches on `AOMediaCodec/oar` (pending review/merge upstream). Once upstream merges them, a sync should reduce them to no-ops. + +- `fix/arm-neon-matrix-render-stub` — `src/renderer/ear/arch/arm/matrix_render_arm.c`: adds `#include "matrix_render.h"` before the `#if defined(def_oar_arch_arm)` guard. Without it, `def_oar_arch_arm` (derived from `__ARM_NEON` in that header) is undefined in this translation unit and `multiply_channels_by_matrix_neon()` is preprocessed away to an empty stub — every matrix render (multichannel downmix M2M and HOA-to-loudspeaker H2M) is silent on ARM; only native-layout passthrough works. x86 is unaffected (scalar C `#else` path); OBR's HRIR binaural path doesn't use this renderer. +- `fix/obr-resampler-shhrir-dsp` — `src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/`, with tests: + - `resampler.cc` — polyphase partition derived from `up_rate_` (downsampling gain/anti-aliasing errors) and `begin()`-relative state addressing for short input blocks. + - `sh_hrir_creator.cc` — resampled HRIRs scaled by `wav_rate/target_rate` so binaural loudness is invariant to the output rate (fixed the louder-at-96k bug). +- `fix/lfe-filter-sample-rate` — LFE low-pass filter used a hardcoded 48 kHz sample rate; with test `tests/examples/test_hoa_lfe_rendering.c`. + +## Syncing with upstream + +```sh +git fetch upstream +git log --oneline HEAD..upstream/main # what's new upstream +git log --oneline upstream/main..HEAD # fork-only commits +``` + +When rebasing onto upstream, the fork-specific commits above are the ones to preserve. Anything else on `main` should match upstream. + +## pffft + +pffft isn't on BCR. It's pulled via the module extension in `extensions.bzl` with a local BUILD shim at `third_party/pffft.BUILD`. See the comment in `MODULE.bazel`. From c34b05c6778c820929a74187f264c857ce0b6b7c Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Mon, 27 Jul 2026 12:55:27 +0100 Subject: [PATCH 05/11] Wrap object azimuths and use circular closest-speaker distance in OLR Azimuths outside (-180, 180] (e.g. +270 for -90) skipped the front-back fold and hit the closest-speaker fallback, whose linear angle difference picks the wrong speaker whenever the true separation crosses the +/-180 seam. Normalize azimuths once at metadata ingestion so every downstream consumer sees canonical angles, and make the fallback distance circular. --- .../gain_calculator/vbap_panner_2d.c | 6 +++++- src/renderer/olr/object_audio_renderer/numc.h | 2 ++ src/renderer/olr/olr.c | 18 ++++++++++++++++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/renderer/olr/object_audio_renderer/gain_calculator/vbap_panner_2d.c b/src/renderer/olr/object_audio_renderer/gain_calculator/vbap_panner_2d.c index c8edba4..9eade39 100644 --- a/src/renderer/olr/object_audio_renderer/gain_calculator/vbap_panner_2d.c +++ b/src/renderer/olr/object_audio_renderer/gain_calculator/vbap_panner_2d.c @@ -334,7 +334,11 @@ static int _vbap_panner_2d_calculate_gains(vbap_panner_2d_t *self, for (int j = 0; j < r; ++j) { speaker_position_t *sp = def_value_wrap_ptr(vector_at(self->target_speakers, j)); - az_diff = nc_abs(sp->azimuth - azimuth); + // Circular distance: a linear difference is wrong whenever the true + // angular separation crosses the +/-180 seam (e.g. -170 vs +135 is + // 55 degrees apart, not 305). + az_diff = nc_fmod(nc_abs(sp->azimuth - azimuth), 360.0); + if (az_diff > 180.0) az_diff = 360.0 - az_diff; if (az_diff < min_diff) { min_diff = az_diff; closest_spk_idx = j; diff --git a/src/renderer/olr/object_audio_renderer/numc.h b/src/renderer/olr/object_audio_renderer/numc.h index ba09914..fe1da86 100644 --- a/src/renderer/olr/object_audio_renderer/numc.h +++ b/src/renderer/olr/object_audio_renderer/numc.h @@ -24,6 +24,7 @@ #define nc_arctan2 atan2 #define nc_cos cos #define nc_sin sin +#define nc_fmod fmod #else #define nc_abs fabsf #define nc_tan tanf @@ -31,6 +32,7 @@ #define nc_arctan2 atan2f #define nc_cos cosf #define nc_sin sinf +#define nc_fmod fmodf #endif auto_float_t nc_radians(auto_float_t deg); diff --git a/src/renderer/olr/olr.c b/src/renderer/olr/olr.c index d38b667..5b646dd 100644 --- a/src/renderer/olr/olr.c +++ b/src/renderer/olr/olr.c @@ -12,6 +12,7 @@ #include "olr.h" +#include #include #include "clog.h" @@ -158,6 +159,19 @@ static int _set_attribute(renderer_library_context_t *ctx, return ck_oar_ok; } +// Wrap an azimuth into the canonical (-180, 180] range so downstream gain +// calculators never see aliased angles (e.g. +270 for -90): the front-back +// fold and the closest-speaker fallback only handle canonical values. +static float __wrap_azimuth(float azimuth) { + azimuth = fmodf(azimuth, 360.f); + if (azimuth > 180.f) { + azimuth -= 360.f; + } else if (azimuth <= -180.f) { + azimuth += 360.f; + } + return azimuth; +} + static int _metadata_update(renderer_library_context_t *ctx, uint32_t index, const oar_metadata_t *metadata) { olr_context_t *olr = ctx->renderer; @@ -186,8 +200,8 @@ static int _metadata_update(renderer_library_context_t *ctx, uint32_t index, metadata->duration > 0 ? metadata->duration : 0; olr->metadata_block_durations[i] += olr->metadata_blocks[i].duration; - olr->metadata_blocks[i].azimuth = - metadata->object_positions.polar_positions[i].azimuth; + olr->metadata_blocks[i].azimuth = __wrap_azimuth( + metadata->object_positions.polar_positions[i].azimuth); olr->metadata_blocks[i].elevation = metadata->object_positions.polar_positions[i].elevation; olr->metadata_blocks[i].distance = From ca6ec35c67205237be7c7f68fc70fdcb7a5a20b4 Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Mon, 27 Jul 2026 12:56:26 +0100 Subject: [PATCH 06/11] Add regression test for out-of-range object azimuths Renders aliased azimuth pairs (+270 vs -90 etc.) to stereo and requires bit-identical output, and checks lateral sources collapse onto opposite speakers. Before the fix a source at +270 landed on the left speaker. --- tests/examples/CMakeLists.txt | 6 + tests/examples/test_object_azimuth_wrapping.c | 232 ++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 tests/examples/test_object_azimuth_wrapping.c diff --git a/tests/examples/CMakeLists.txt b/tests/examples/CMakeLists.txt index 01e1881..6b02b4a 100644 --- a/tests/examples/CMakeLists.txt +++ b/tests/examples/CMakeLists.txt @@ -12,6 +12,9 @@ add_executable(test_scene_based_rendering test_scene_based_rendering.c) # Add the test for object-based rendering add_executable(test_object_based_rendering test_object_based_rendering.c) +# Add the regression test for out-of-range object azimuths +add_executable(test_object_azimuth_wrapping test_object_azimuth_wrapping.c) + # Link the tests against the main oar library. # The 'oar' target is defined in the parent CMakeLists.txt, # so it's accessible here. @@ -19,6 +22,7 @@ target_link_libraries(test_audio_element_types PRIVATE oar) target_link_libraries(test_channel_based_rendering PRIVATE oar) target_link_libraries(test_scene_based_rendering PRIVATE oar) target_link_libraries(test_object_based_rendering PRIVATE oar) +target_link_libraries(test_object_azimuth_wrapping PRIVATE oar) # Include directories are inherited from the parent project, # so oar.h and other headers should be found automatically. @@ -28,10 +32,12 @@ set_property(TARGET test_audio_element_types PROPERTY C_STANDARD 99) set_property(TARGET test_channel_based_rendering PROPERTY C_STANDARD 99) set_property(TARGET test_scene_based_rendering PROPERTY C_STANDARD 99) set_property(TARGET test_object_based_rendering PROPERTY C_STANDARD 99) +set_property(TARGET test_object_azimuth_wrapping PROPERTY C_STANDARD 99) if(NOT MSVC) target_link_libraries(test_channel_based_rendering PRIVATE m) target_link_libraries(test_scene_based_rendering PRIVATE m) target_link_libraries(test_object_based_rendering PRIVATE m) + target_link_libraries(test_object_azimuth_wrapping PRIVATE m) endif() # The HOA LFE test exercises the 120 Hz LFE low-pass, which is only compiled diff --git a/tests/examples/test_object_azimuth_wrapping.c b/tests/examples/test_object_azimuth_wrapping.c new file mode 100644 index 0000000..be5ca39 --- /dev/null +++ b/tests/examples/test_object_azimuth_wrapping.c @@ -0,0 +1,232 @@ +/* + * 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. + */ + +/* + * Regression test for out-of-range object azimuths in the OLR. + * + * An azimuth outside (-180, 180] is an alias of a canonical angle (+270 is + * -90). The renderer used to pass such angles through unfolded, and the + * closest-speaker fallback compared angles linearly, so on a stereo layout a + * source at +270 collapsed onto the LEFT speaker even though -90 is on the + * right. This test renders a sine at aliased azimuth pairs and requires + * bit-identical output, plus checks that lateral sources land on opposite + * output channels. + */ + +#include +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +#include "oar.h" + +#define SAMPLES_PER_CHANNEL 256 +#define SAMPLE_RATE 48000 + +// Renders one 440 Hz object at the given azimuth to a stereo output block. +// Returns 0 on success; the caller owns out->data. +static int render_object_at_azimuth(float azimuth, oar_audio_block_t* out) { + oar_config_t cfg; + cfg.target_layout = ck_oar_layout_stereo; + cfg.samples_per_channel = SAMPLES_PER_CHANNEL; + cfg.sampling_rate = SAMPLE_RATE; + + oar_t* oar = oar_create(&cfg); + if (!oar) { + fprintf(stderr, "oar_create failed\n"); + return -1; + } + + int group_id = oar_add_audio_group(oar); + if (group_id < 0) { + fprintf(stderr, "oar_add_audio_group failed: %d\n", group_id); + oar_destroy(oar); + return -1; + } + + oar_audio_element_config_t element_cfg; + memset(&element_cfg, 0, sizeof(element_cfg)); + element_cfg.type = ck_object_based; + element_cfg.obc.num_objects = 1; + + int ret = oar_add_audio_element(oar, group_id, 1, &element_cfg); + if (ret != 0) { + fprintf(stderr, "oar_add_audio_element failed: %d\n", ret); + oar_destroy(oar); + return -1; + } + + float input[SAMPLES_PER_CHANNEL]; + for (int i = 0; i < SAMPLES_PER_CHANNEL; ++i) { + input[i] = (float)sin(2.0 * M_PI * 440.0 * i / SAMPLE_RATE); + } + + oar_audio_block_t input_block; + input_block.channels = 1; + input_block.samples_per_channel = SAMPLES_PER_CHANNEL; + input_block.data = input; + ret = oar_update_audio_element_data(oar, 1, &input_block); + if (ret != 0) { + fprintf(stderr, "oar_update_audio_element_data failed: %d\n", ret); + oar_destroy(oar); + return -1; + } + + oar_metadata_t metadata; + memset(&metadata, 0, sizeof(metadata)); + metadata.type = ck_metadata_object_positions; + metadata.duration = SAMPLES_PER_CHANNEL; + metadata.object_positions.param_type = ck_param_constant; + metadata.object_positions.position_type = ck_polar; + metadata.object_positions.num_objects = 1; + metadata.object_positions.polar_positions[0].azimuth = azimuth; + metadata.object_positions.polar_positions[0].elevation = 0.f; + metadata.object_positions.polar_positions[0].distance = 1.f; + ret = oar_update_audio_element_metadata(oar, 1, &metadata); + if (ret != 0) { + fprintf(stderr, "oar_update_audio_element_metadata failed: %d\n", ret); + oar_destroy(oar); + return -1; + } + + out->channels = oar_get_number_of_output_channels(oar); + out->samples_per_channel = oar_get_samples_per_channel(oar); + out->data = (float*)calloc(out->channels * out->samples_per_channel, + sizeof(float)); + if (!out->data) { + oar_destroy(oar); + return -1; + } + + ret = oar_render(oar, out); + oar_destroy(oar); + if (ret != 0) { + fprintf(stderr, "oar_render failed: %d\n", ret); + free(out->data); + out->data = NULL; + return -1; + } + return 0; +} + +static double channel_energy(const oar_audio_block_t* block, uint32_t channel) { + double energy = 0.0; + const float* p = block->data + channel * block->samples_per_channel; + for (uint32_t i = 0; i < block->samples_per_channel; ++i) { + energy += (double)p[i] * p[i]; + } + return energy; +} + +static double max_abs_difference(const oar_audio_block_t* a, + const oar_audio_block_t* b) { + double max_diff = 0.0; + uint32_t n = a->channels * a->samples_per_channel; + for (uint32_t i = 0; i < n; ++i) { + double diff = fabs((double)a->data[i] - b->data[i]); + if (diff > max_diff) max_diff = diff; + } + return max_diff; +} + +// Renders `azimuth` and its alias `azimuth + turns*360` and requires the two +// outputs to be identical. Returns 0 on pass. +static int check_alias_pair(float azimuth, int turns) { + float alias = azimuth + 360.f * turns; + oar_audio_block_t ref = {0}, aliased = {0}; + int failed = 0; + + if (render_object_at_azimuth(azimuth, &ref) != 0 || + render_object_at_azimuth(alias, &aliased) != 0) { + free(ref.data); + free(aliased.data); + return 1; + } + + if (channel_energy(&ref, 0) + channel_energy(&ref, 1) < 1e-6) { + fprintf(stderr, "FAIL: azimuth %.1f rendered silence\n", azimuth); + failed = 1; + } + + double diff = max_abs_difference(&ref, &aliased); + if (diff > 1e-6) { + fprintf(stderr, + "FAIL: azimuth %.1f and alias %.1f differ (max abs diff %g)\n", + azimuth, alias, diff); + failed = 1; + } else { + printf("PASS: azimuth %.1f == alias %.1f (max abs diff %g)\n", azimuth, + alias, diff); + } + + free(ref.data); + free(aliased.data); + return failed; +} + +// Renders two laterally opposed azimuths and requires their dominant output +// channels to be opposite (each side collapses onto its closest speaker). +static int check_opposite_sides(float azimuth_a, float azimuth_b) { + oar_audio_block_t a = {0}, b = {0}; + int failed = 0; + + if (render_object_at_azimuth(azimuth_a, &a) != 0 || + render_object_at_azimuth(azimuth_b, &b) != 0) { + free(a.data); + free(b.data); + return 1; + } + + int dominant_a = channel_energy(&a, 0) > channel_energy(&a, 1) ? 0 : 1; + int dominant_b = channel_energy(&b, 0) > channel_energy(&b, 1) ? 0 : 1; + if (dominant_a == dominant_b) { + fprintf(stderr, + "FAIL: azimuths %.1f and %.1f both favor output channel %d\n", + azimuth_a, azimuth_b, dominant_a); + failed = 1; + } else { + printf("PASS: azimuth %.1f favors channel %d, azimuth %.1f favors %d\n", + azimuth_a, dominant_a, azimuth_b, dominant_b); + } + + free(a.data); + free(b.data); + return failed; +} + +int main(void) { + int failures = 0; + + printf("Object azimuth wrapping test\n"); + + // Aliased angles must render identically to their canonical forms. + failures += check_alias_pair(-90.f, 1); // +270 == -90 + failures += check_alias_pair(90.f, 1); // +450 == +90 + failures += check_alias_pair(-170.f, 1); // +190 == -170 + failures += check_alias_pair(30.f, -2); // -690 == +30 + + // Lateral sources must collapse onto opposite stereo speakers; before the + // fix, +270 landed on the same (left) speaker as +90. + failures += check_opposite_sides(90.f, -90.f); + failures += check_opposite_sides(90.f, 270.f); + + if (failures) { + fprintf(stderr, "\n%d azimuth wrapping check(s) FAILED\n", failures); + return 1; + } + printf("\nAll azimuth wrapping checks passed.\n"); + return 0; +} From d9f743dd5657f4e8d0b15514c5fdcd171aa2a9b4 Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Mon, 27 Jul 2026 12:58:53 +0100 Subject: [PATCH 07/11] Register liboar example tests with ctest enable_testing() at the top level puts the obr unit tests and the liboar examples in one ctest registry; the examples are registered under an "oar" label (mirroring the obr subtree's "obr" label) derived from the directory's target list so new examples cannot be silently left out. The CI workflow's hardcoded example list and separate obr ctest invocation collapse into a single label-filtered ctest run. --- .github/workflows/ci-cmake.yml | 14 ++++++-------- CMakeLists.txt | 4 ++++ tests/examples/CMakeLists.txt | 10 ++++++++++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-cmake.yml b/.github/workflows/ci-cmake.yml index c075919..7220261 100644 --- a/.github/workflows/ci-cmake.yml +++ b/.github/workflows/ci-cmake.yml @@ -36,14 +36,12 @@ jobs: - name: Build run: cmake --build build --parallel - - name: Run smoke tests + # PATH must include the build dir so the examples find oar.dll in the + # shared-library configuration on Windows. The label filter selects the + # oar examples and obr unit tests while skipping the tests that + # FetchContent'd dependencies (Eigen) register but never build. + - name: Run tests shell: bash run: | export PATH="$PWD/build:$PATH" - ./build/tests/examples/test_audio_element_types - ./build/tests/examples/test_channel_based_rendering - ./build/tests/examples/test_scene_based_rendering - ./build/tests/examples/test_object_based_rendering - - - name: Run obr unit tests - run: ctest --test-dir build/src/renderer/obr/obr_capi/obr -L obr --output-on-failure + ctest --test-dir build -L 'oar|obr' --output-on-failure diff --git a/CMakeLists.txt b/CMakeLists.txt index 57f0a05..c155e51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,6 +3,10 @@ cmake_minimum_required(VERSION 3.28) set(PROJECT_N oar) project(${PROJECT_N} VERSION 1.0.0) +# Register tests from every subtree (obr unit tests, liboar examples) so a +# single `ctest --test-dir build` covers them all. +enable_testing() + # Export all symbols when building oar as a DLL so an import library is # generated for the examples to link against. set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) diff --git a/tests/examples/CMakeLists.txt b/tests/examples/CMakeLists.txt index 6b02b4a..0d26327 100644 --- a/tests/examples/CMakeLists.txt +++ b/tests/examples/CMakeLists.txt @@ -50,3 +50,13 @@ if(OAR_ENABLE_HOA_LFE) endif() set_property(TARGET test_hoa_lfe_rendering PROPERTY C_STANDARD 99) endif() + +# Register every example in this directory with ctest under the "oar" label +# (mirroring the "obr" label in the obr subtree). Derived from the directory's +# target list rather than a hardcoded list so new examples cannot be silently +# left out of `ctest`. +get_directory_property(OAR_EXAMPLE_TESTS BUILDSYSTEM_TARGETS) +foreach(example_test IN LISTS OAR_EXAMPLE_TESTS) + add_test(NAME ${example_test} COMMAND ${example_test}) + set_tests_properties(${example_test} PROPERTIES LABELS oar) +endforeach() From 38784b971b1a951a28d3eda9770cdc7cb67d1213 Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Mon, 27 Jul 2026 13:00:20 +0100 Subject: [PATCH 08/11] Clear stack-local output-layout pointer before EAR _open returns _open stored the address of the stack-local pout in ear_renderer->out_sp_layout, which lives for the renderer's lifetime. The pointer is only dereferenced during _open itself (matrix lookups), so the bug is latent, but any future reader of out_sp_layout.sp_layout.predefined_sp after _open returns would hit a dangling stack pointer. Null it before returning, matching how the input-layout paths already reset pin/cin. --- src/renderer/ear/ear.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/renderer/ear/ear.c b/src/renderer/ear/ear.c index 17de3ac..6c19708 100644 --- a/src/renderer/ear/ear.c +++ b/src/renderer/ear/ear.c @@ -279,6 +279,10 @@ static int _open(renderer_library_context_t *ctx) { } #endif } + // pout is stack-local; the matrix lookups above are its only consumers, so + // clear the pointer (as the input-layout paths do) rather than let it + // dangle for the renderer's lifetime. + ear_renderer->out_sp_layout.sp_layout.predefined_sp = 0; return 0; } From 15fd892b1b9faca89c36269db4264cdabdc40bd2 Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Thu, 30 Jul 2026 19:01:11 +0100 Subject: [PATCH 09/11] Update CLAUDE.md pending-fixes list after upstream sync The ARM NEON matrix-render, OBR resampler/sh_hrir DSP, and LFE filter sample-rate fixes were merged upstream (AOMediaCodec/oar #14, #15 and the matrix_render include commit), so they drop off the pending list. The OLR azimuth wrapping, ctest registration, and EAR dangling-pointer fixes take their place. --- CLAUDE.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e2e3af7..272c77f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,13 +19,13 @@ Eclipsa's engine consumes OAR+OBR as a Bazel module. Upstream ships a CMake-only ### Bug fixes pending upstreaming -These are merged into our `main` and also pushed as branches on `AOMediaCodec/oar` (pending review/merge upstream). Once upstream merges them, a sync should reduce them to no-ops. +These are merged into our `main` and kept as `fix/*` branches on this fork (not yet submitted upstream). Once upstream merges them, a sync should reduce them to no-ops. -- `fix/arm-neon-matrix-render-stub` — `src/renderer/ear/arch/arm/matrix_render_arm.c`: adds `#include "matrix_render.h"` before the `#if defined(def_oar_arch_arm)` guard. Without it, `def_oar_arch_arm` (derived from `__ARM_NEON` in that header) is undefined in this translation unit and `multiply_channels_by_matrix_neon()` is preprocessed away to an empty stub — every matrix render (multichannel downmix M2M and HOA-to-loudspeaker H2M) is silent on ARM; only native-layout passthrough works. x86 is unaffected (scalar C `#else` path); OBR's HRIR binaural path doesn't use this renderer. -- `fix/obr-resampler-shhrir-dsp` — `src/renderer/obr/obr_capi/obr/obr/ambisonic_binaural_decoder/`, with tests: - - `resampler.cc` — polyphase partition derived from `up_rate_` (downsampling gain/anti-aliasing errors) and `begin()`-relative state addressing for short input blocks. - - `sh_hrir_creator.cc` — resampled HRIRs scaled by `wav_rate/target_rate` so binaural loudness is invariant to the output rate (fixed the louder-at-96k bug). -- `fix/lfe-filter-sample-rate` — LFE low-pass filter used a hardcoded 48 kHz sample rate; with test `tests/examples/test_hoa_lfe_rendering.c`. +- `fix/olr-azimuth-wrapping` — `src/renderer/olr/`: wraps out-of-range object azimuths and uses circular closest-speaker distance in OLR; with test `tests/examples/test_object_azimuth_wrapping.c`. +- `fix/register-example-tests-ctest` — registers the liboar example tests with ctest (`enable_testing()` at the root, `add_test()` entries, CI runs them via `ctest -L`). +- `fix/ear-dangling-layout-pointer` — `src/renderer/ear/ear.c`: clears a stack-local output-layout pointer before `_open` returns so it can't dangle. + +Previously listed here and since merged upstream (now plain upstream history): the ARM NEON matrix-render include fix, the OBR resampler/`sh_hrir_creator` DSP fixes, and the LFE filter sample-rate fix. ## Syncing with upstream From 236ab943a8436e717611a000a738a05256c2b7a8 Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Thu, 30 Jul 2026 20:35:20 +0100 Subject: [PATCH 10/11] Snap OBR object positions set before the first rendered block An object source is registered with the ambisonic encoder at its default position (azimuth 0, elevation 0, distance 1) as soon as the audio element is added. A position update arriving before the first render call therefore only moved the ramp target, and the first processed block audibly glided the object from front-center to its configured position. Loudspeaker rendering (OLR) applies pre-render metadata immediately, so binaural output disagreed with loudspeaker output over the first block. Track whether the encoder has processed any audio yet; until it has, SetSource() snaps the current parameters to the new target instead of scheduling a ramp. There is no previously audible position to interpolate from, so metadata applied before the first render now takes effect from the first frame, matching OLR. Updates arriving after audio has been rendered still ramp across one block to avoid clicks. Reported upstream as AOMediaCodec/oar#21. --- .../ambisonic_encoder/ambisonic_encoder.cc | 13 ++++ .../obr/ambisonic_encoder/ambisonic_encoder.h | 6 ++ .../tests/ambisonic_encoder_test.cc | 78 +++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/ambisonic_encoder.cc b/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/ambisonic_encoder.cc index 0dac898..853e50a 100644 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/ambisonic_encoder.cc +++ b/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/ambisonic_encoder.cc @@ -77,6 +77,15 @@ void AmbisonicEncoder::SetSource(size_t input_channel, float gain, source_properties.target.elevation = elevation; source_properties.target.distance = distance; + // Before any audio has been rendered there is no previously audible + // position to ramp from, so snap to the new parameters. This makes + // metadata applied before the first processed block take effect + // immediately (matching loudspeaker rendering) instead of gliding in + // from the initial position over the first block. + if (!has_processed_) { + source_properties.current = source_properties.target; + } + // If target gain indicates silence, mute the static encoding matrix column // as a quick early-out for any code that may still use it. if (gain < kNegative120dbInAmplitude) { @@ -304,6 +313,10 @@ void AmbisonicEncoder::ProcessPlanarAudioData(const AudioBuffer& input_buffer, // Update the stored encoding matrix to the last frame's matrix so code that // expects a static encoding matrix still has a reasonable value. encoding_matrix_ = last_encoding; + + // Audio has now been rendered; subsequent `SetSource()` updates ramp from + // the previously audible parameters. + has_processed_ = true; } void AmbisonicEncoder::GetShCoeffs(float azimuth, float elevation, diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/ambisonic_encoder.h b/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/ambisonic_encoder.h index b4c2f0d..077b1ab 100644 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/ambisonic_encoder.h +++ b/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/ambisonic_encoder.h @@ -129,6 +129,12 @@ class AmbisonicEncoder { // Map of structs containing the properties of each source. absl::flat_hash_map sources_; + // False until the first call to `ProcessPlanarAudioData()`. While false, + // `SetSource()` snaps `current` to `target` instead of scheduling a ramp: + // no audio has been rendered yet, so there is no previously audible + // position to interpolate from. + bool has_processed_ = false; + AssociatedLegendrePolynomialsGenerator alp_generator_; Eigen::MatrixXf encoding_matrix_; }; diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/tests/ambisonic_encoder_test.cc b/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/tests/ambisonic_encoder_test.cc index d08be4a..e3a6179 100644 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/tests/ambisonic_encoder_test.cc +++ b/src/renderer/obr/obr_capi/obr/obr/ambisonic_encoder/tests/ambisonic_encoder_test.cc @@ -99,5 +99,83 @@ TEST(AmbisonicEncoderTest, TestOneSampleBufferOneSource) { } } +// A source position set after the source was created but before any audio +// has been processed must take effect from the very first frame, with no +// ramp from the previously set (e.g. default) position. +TEST(AmbisonicEncoderTest, PreRenderPositionUpdateTakesEffectImmediately) { + const size_t buffer_size = 64; + const int number_of_input_channels = 1; + const int ambisonic_order = 1; + + const float kEpsilon = 1e-6; + + AmbisonicEncoder encoder(number_of_input_channels, ambisonic_order); + + // Source is first registered at the default front position, then moved to + // hard left before any processing (mirrors an element being added and its + // metadata updated before the first render call). + encoder.SetSource(0, 1.0f, 0.0f, 0.0f, 1.0f); + encoder.SetSource(0, 1.0f, 90.0f, 0.0f, 1.0f); + + AudioBuffer input_buffer(number_of_input_channels, buffer_size); + for (float& sample : input_buffer[0]) { + sample = 1.0f; + } + + AudioBuffer output_buffer(GetNumPeriphonicComponents(ambisonic_order), + buffer_size); + encoder.ProcessPlanarAudioData(input_buffer, &output_buffer); + + // First-order ACN/SN3D coefficients for azimuth 90, elevation 0: + // ACN0 = 1, ACN1 = 1, ACN2 = 0, ACN3 = 0. Every frame of the block, + // including the first, must carry these coefficients. + const std::vector expected_coefficients = {1.0f, 1.0f, 0.0f, 0.0f}; + for (auto ch = 0; ch < output_buffer.num_channels(); ch++) { + for (float sample : output_buffer[ch]) { + EXPECT_NEAR(sample, expected_coefficients[ch], kEpsilon); + } + } +} + +// Once audio has been processed, position updates must still ramp across the +// next block to avoid clicks: the block starts at the previously audible +// position and ends at the new one. +TEST(AmbisonicEncoderTest, PostRenderPositionUpdateRampsAcrossBlock) { + const size_t buffer_size = 64; + const int number_of_input_channels = 1; + const int ambisonic_order = 1; + + const float kEpsilon = 1e-6; + + AmbisonicEncoder encoder(number_of_input_channels, ambisonic_order); + encoder.SetSource(0, 1.0f, 0.0f, 0.0f, 1.0f); + + AudioBuffer input_buffer(number_of_input_channels, buffer_size); + for (float& sample : input_buffer[0]) { + sample = 1.0f; + } + + AudioBuffer output_buffer(GetNumPeriphonicComponents(ambisonic_order), + buffer_size); + + // First block renders the source at the front. + encoder.ProcessPlanarAudioData(input_buffer, &output_buffer); + + // Move the source to hard left and render another block. + encoder.SetSource(0, 1.0f, 90.0f, 0.0f, 1.0f); + encoder.ProcessPlanarAudioData(input_buffer, &output_buffer); + + // First-order ACN/SN3D coefficients (ACN0..ACN3): front is {1, 0, 0, 1}, + // hard left is {1, 1, 0, 0}. The block must start at the front and end + // hard left. + const std::vector front_coefficients = {1.0f, 0.0f, 0.0f, 1.0f}; + const std::vector left_coefficients = {1.0f, 1.0f, 0.0f, 0.0f}; + for (auto ch = 0; ch < output_buffer.num_channels(); ch++) { + EXPECT_NEAR(output_buffer[ch][0], front_coefficients[ch], kEpsilon); + EXPECT_NEAR(output_buffer[ch][buffer_size - 1], left_coefficients[ch], + kEpsilon); + } +} + } // namespace } // namespace obr From 22932f6ed2611ec24ba1ddceebae9a8c3db9f6e4 Mon Sep 17 00:00:00 2001 From: Tomasz Rudzki Date: Thu, 30 Jul 2026 21:10:58 +0100 Subject: [PATCH 11/11] Snap head rotation set before the first rendered block The ambisonic rotator's current rotation starts at identity and only the target rotation is supplied per Process() call, so a head pose set before the first render call made the whole scene slerp from identity to that pose across the first processed block. Apply the same rule as for object positions: until any audio has been rendered there is no previously audible rotation to interpolate from, so the first processed block applies the target rotation in full from the first frame. Rotation changes after audio has been rendered still slerp across the block in 32-frame intervals. Blocks rendered with head tracking disabled bypass the rotator but are audible at the identity rotation, so the processing group marks them via MarkAudioRendered(); enabling head tracking with a stored pose after such blocks still slerps instead of snapping. --- .../ambisonic_rotator/ambisonic_rotator.cc | 13 ++ .../obr/ambisonic_rotator/ambisonic_rotator.h | 15 ++ .../tests/ambisonic_rotator_test.cc | 160 ++++++++++++++++++ .../obr/obr/renderer/processing_group.cc | 5 + 4 files changed, 193 insertions(+) diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/ambisonic_rotator.cc b/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/ambisonic_rotator.cc index ea4332e..c8b8c07 100644 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/ambisonic_rotator.cc +++ b/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/ambisonic_rotator.cc @@ -203,6 +203,19 @@ bool AmbisonicRotator::Process(const WorldRotation& target_rotation, static const WorldRotation kIdentityRotation; + // Before any audio has been rendered there is no previously audible + // rotation to slerp from, so snap to the target: a head pose set ahead of + // the first processed block takes effect from the first frame instead of + // gliding in from the initial identity rotation. + if (!has_processed_) { + has_processed_ = true; + if (current_rotation_.AngularDifferenceRad(target_rotation) >= + kRotationQuantizationRad) { + current_rotation_ = target_rotation; + UpdateRotationMatrix(current_rotation_); + } + } + if (current_rotation_.AngularDifferenceRad(kIdentityRotation) < kRotationQuantizationRad && target_rotation.AngularDifferenceRad(kIdentityRotation) < diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/ambisonic_rotator.h b/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/ambisonic_rotator.h index a377766..d743d35 100644 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/ambisonic_rotator.h +++ b/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/ambisonic_rotator.h @@ -44,6 +44,15 @@ class AmbisonicRotator { bool Process(const WorldRotation& target_rotation, const AudioBuffer& input, AudioBuffer* output); + /*!\brief Records that a block of audio was rendered without this rotator + * (e.g. with head tracking disabled). + * + * The unrotated scene was audible, so a later rotation change slerps from + * `current_rotation_` instead of being snapped as on the first processed + * block. + */ + void MarkAudioRendered() { has_processed_ = true; } + private: /*!\brief Updates the rotation matrix with using supplied WorldRotation. * @@ -58,6 +67,12 @@ class AmbisonicRotator { // compute new rotation matrix. Initialized with an identity rotation. WorldRotation current_rotation_; + // False until the first call to `Process()`. While false, the target + // rotation is applied from the first frame instead of slerping from + // `current_rotation_`: no audio has been rendered yet, so there is no + // previously audible rotation to interpolate from. + bool has_processed_ = false; + // Spherical harmonics rotation sub-matrices for each order. std::vector rotation_matrices_; diff --git a/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/tests/ambisonic_rotator_test.cc b/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/tests/ambisonic_rotator_test.cc index 5cca0ed..aa508ff 100644 --- a/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/tests/ambisonic_rotator_test.cc +++ b/src/renderer/obr/obr_capi/obr/obr/ambisonic_rotator/tests/ambisonic_rotator_test.cc @@ -13,6 +13,7 @@ #include "obr/ambisonic_rotator/ambisonic_rotator.h" #include +#include #include #include #include @@ -208,6 +209,165 @@ INSTANTIATE_TEST_SUITE_P( TestParams({0.0f, 1.0f, 0.0f}, kYrotatedSourceAngle), TestParams({0.0f, 0.0f, 1.0f}, kZrotatedSourceAngle))); +// A rotation set before any audio has been processed must be applied in full +// from the very first frame, with no slerp from the initial identity rotation. +TEST_F(AmbisonicRotatorTest, FirstProcessedBlockAppliesTargetRotationInFull) { + const size_t kFramesPerBuffer = 2 * kSlerpFrameInterval; + const size_t kNumThirdOrderAmbisonicChannels = 16; + const std::vector kInputData(kFramesPerBuffer, 1.0f); + AudioBuffer input_buffer(1, kFramesPerBuffer); + FillAudioBuffer(kInputData, 1, &input_buffer); + + // Soundfield with a source at the initial angle, to be rotated. + AmbisonicEncoder source_mono_codec(1, kAmbisonicOrder); + source_mono_codec.SetSource( + 0, 1.0f, kInitialSourceAngle.azimuth() * kDegreesFromRadians, + kInitialSourceAngle.elevation() * kDegreesFromRadians, 1.0f); + AudioBuffer encoded_buffer(kNumThirdOrderAmbisonicChannels, kFramesPerBuffer); + source_mono_codec.ProcessPlanarAudioData(input_buffer, &encoded_buffer); + + // Reference soundfield with the source already at the rotated angle. + AmbisonicEncoder reference_mono_codec(1, kAmbisonicOrder); + reference_mono_codec.SetSource( + 0, 1.0f, kZrotatedSourceAngle.azimuth() * kDegreesFromRadians, + kZrotatedSourceAngle.elevation() * kDegreesFromRadians, 1.0f); + AudioBuffer reference_buffer(kNumThirdOrderAmbisonicChannels, + kFramesPerBuffer); + reference_mono_codec.ProcessPlanarAudioData(input_buffer, &reference_buffer); + + const WorldRotation rotation = WorldRotation(AngleAxisf( + kAngleDegrees * kRadiansFromDegrees, WorldPosition(0.0f, 0.0f, 1.0f))); + + hoa_rotator_ = std::make_unique(kAmbisonicOrder); + EXPECT_TRUE( + hoa_rotator_->Process(rotation, encoded_buffer, &encoded_buffer)); + + // Every frame of the first processed block, including the first slerp + // interval, must match the fully rotated reference. + for (size_t channel = 0; channel < encoded_buffer.num_channels(); + ++channel) { + for (size_t frame = 0; frame < kFramesPerBuffer; ++frame) { + EXPECT_NEAR(encoded_buffer[channel][frame], + reference_buffer[channel][frame], kEpsilonFloat); + } + } +} + +// If audio has been rendered without the rotator (head tracking disabled), +// the unrotated scene was audible, so a later rotation must still slerp from +// identity across the block instead of being applied in full. +TEST_F(AmbisonicRotatorTest, RotationAfterBypassedBlocksStillSlerps) { + const size_t kFramesPerBuffer = 2 * kSlerpFrameInterval; + const size_t kNumThirdOrderAmbisonicChannels = 16; + const std::vector kInputData(kFramesPerBuffer, 1.0f); + AudioBuffer input_buffer(1, kFramesPerBuffer); + FillAudioBuffer(kInputData, 1, &input_buffer); + + AmbisonicEncoder source_mono_codec(1, kAmbisonicOrder); + source_mono_codec.SetSource( + 0, 1.0f, kInitialSourceAngle.azimuth() * kDegreesFromRadians, + kInitialSourceAngle.elevation() * kDegreesFromRadians, 1.0f); + AudioBuffer encoded_buffer(kNumThirdOrderAmbisonicChannels, kFramesPerBuffer); + source_mono_codec.ProcessPlanarAudioData(input_buffer, &encoded_buffer); + + // Reference soundfield with the source already at the rotated angle. + AmbisonicEncoder reference_mono_codec(1, kAmbisonicOrder); + reference_mono_codec.SetSource( + 0, 1.0f, kZrotatedSourceAngle.azimuth() * kDegreesFromRadians, + kZrotatedSourceAngle.elevation() * kDegreesFromRadians, 1.0f); + AudioBuffer reference_buffer(kNumThirdOrderAmbisonicChannels, + kFramesPerBuffer); + reference_mono_codec.ProcessPlanarAudioData(input_buffer, &reference_buffer); + + const WorldRotation rotation = WorldRotation(AngleAxisf( + kAngleDegrees * kRadiansFromDegrees, WorldPosition(0.0f, 0.0f, 1.0f))); + + hoa_rotator_ = std::make_unique(kAmbisonicOrder); + + // Blocks were rendered with the rotator bypassed (head tracking disabled). + hoa_rotator_->MarkAudioRendered(); + + AudioBuffer rotated_buffer(kNumThirdOrderAmbisonicChannels, kFramesPerBuffer); + EXPECT_TRUE( + hoa_rotator_->Process(rotation, encoded_buffer, &rotated_buffer)); + + float max_first_interval_difference = 0.0f; + for (size_t channel = 0; channel < rotated_buffer.num_channels(); + ++channel) { + // The last slerp interval has undergone the full rotation. + for (size_t frame = kFramesPerBuffer - kSlerpFrameInterval; + frame < kFramesPerBuffer; ++frame) { + EXPECT_NEAR(rotated_buffer[channel][frame], + reference_buffer[channel][frame], kEpsilonFloat); + } + // Track how far the first slerp interval is from the full rotation. + for (size_t frame = 0; frame < kSlerpFrameInterval; ++frame) { + max_first_interval_difference = std::max( + max_first_interval_difference, + std::abs(rotated_buffer[channel][frame] - + reference_buffer[channel][frame])); + } + } + // The first slerp interval must still be part-way through the rotation. + EXPECT_GT(max_first_interval_difference, 0.01f); +} + +// Once audio has been processed, rotation changes must still be smoothed +// across the next block: the block ends at the new rotation but does not +// start there. +TEST_F(AmbisonicRotatorTest, RotationChangeAfterFirstBlockStillSlerps) { + const size_t kFramesPerBuffer = 2 * kSlerpFrameInterval; + const size_t kNumThirdOrderAmbisonicChannels = 16; + const std::vector kInputData(kFramesPerBuffer, 1.0f); + AudioBuffer input_buffer(1, kFramesPerBuffer); + FillAudioBuffer(kInputData, 1, &input_buffer); + + AmbisonicEncoder source_mono_codec(1, kAmbisonicOrder); + source_mono_codec.SetSource( + 0, 1.0f, kInitialSourceAngle.azimuth() * kDegreesFromRadians, + kInitialSourceAngle.elevation() * kDegreesFromRadians, 1.0f); + AudioBuffer encoded_buffer(kNumThirdOrderAmbisonicChannels, kFramesPerBuffer); + source_mono_codec.ProcessPlanarAudioData(input_buffer, &encoded_buffer); + + const WorldRotation rotation = WorldRotation(AngleAxisf( + kAngleDegrees * kRadiansFromDegrees, WorldPosition(0.0f, 0.0f, 1.0f))); + + hoa_rotator_ = std::make_unique(kAmbisonicOrder); + + // First block: rotation applied in full (snap). + AudioBuffer rotated_buffer(kNumThirdOrderAmbisonicChannels, kFramesPerBuffer); + EXPECT_TRUE( + hoa_rotator_->Process(rotation, encoded_buffer, &rotated_buffer)); + + // Second block: return to identity. The output must end as the unrotated + // input (slerp completed) but must not start there (still mid-slerp). + AudioBuffer transition_buffer(kNumThirdOrderAmbisonicChannels, + kFramesPerBuffer); + EXPECT_TRUE(hoa_rotator_->Process(WorldRotation(), encoded_buffer, + &transition_buffer)); + + float max_first_interval_difference = 0.0f; + for (size_t channel = 0; channel < transition_buffer.num_channels(); + ++channel) { + // The last slerp interval has undergone the full rotation back to + // identity, so it must match the unrotated input. + for (size_t frame = kFramesPerBuffer - kSlerpFrameInterval; + frame < kFramesPerBuffer; ++frame) { + EXPECT_NEAR(transition_buffer[channel][frame], + encoded_buffer[channel][frame], kEpsilonFloat); + } + // Track how far the first slerp interval is from the unrotated input. + for (size_t frame = 0; frame < kSlerpFrameInterval; ++frame) { + max_first_interval_difference = std::max( + max_first_interval_difference, + std::abs(transition_buffer[channel][frame] - + encoded_buffer[channel][frame])); + } + } + // The first slerp interval must still be part-way through the rotation. + EXPECT_GT(max_first_interval_difference, 0.01f); +} + } // namespace } // namespace obr diff --git a/src/renderer/obr/obr_capi/obr/obr/renderer/processing_group.cc b/src/renderer/obr/obr_capi/obr/obr/renderer/processing_group.cc index b432715..da3ea3a 100644 --- a/src/renderer/obr/obr_capi/obr/obr/renderer/processing_group.cc +++ b/src/renderer/obr/obr_capi/obr/obr/renderer/processing_group.cc @@ -182,6 +182,11 @@ void ProcessingGroup::Process( // Pass world-locked Ambisonic mix bed through Ambisonic Rotator. ambisonic_rotator_->Process(world_rotation, ambisonic_mix_bed_, &ambisonic_mix_bed_); + } else { + // The block is rendered unrotated; let the rotator know so a later + // rotation change slerps from the audible identity rotation instead of + // snapping. + ambisonic_rotator_->MarkAudioRendered(); } // Sum both beds: combine world-locked (possibly rotated) and head-locked.