Skip to content
Closed
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
14 changes: 6 additions & 8 deletions .github/workflows/ci-cmake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
build/

# macOS
.DS_Store

# Bazel build artifacts (standalone builds in this repo)
/bazel-*
/MODULE.bazel.lock
151 changes: 151 additions & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
@@ -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",
],
)
42 changes: 42 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -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 kept as `fix/*` branches on this fork (not yet submitted upstream). Once upstream merges them, a sync should reduce them to no-ops.

- `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

```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`.
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
@@ -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")
13 changes: 13 additions & 0 deletions extensions.bzl
Original file line number Diff line number Diff line change
@@ -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)
11 changes: 11 additions & 0 deletions include/oar.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
37 changes: 24 additions & 13 deletions src/limiter/oar_limiter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand All @@ -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;
}
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
8 changes: 7 additions & 1 deletion src/limiter/oar_limiter.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#ifndef _OAR_LIMITER_H_
#define _OAR_LIMITER_H_

#include <stdint.h>

#include "oar_base.h" // For oar_audio_block_t

#ifdef __cplusplus
Expand All @@ -34,18 +36,22 @@ 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.
*
* \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.
*
Expand Down
Loading
Loading