A Rust port of Open Audio Renderer, the reference implementation of the OAR specification.
The library can be used to render various types of input audio into a loudspeaker or binaural output.
- Install bazelisk to run and manage bazel.
bazelisk build src:roarbazelisk test src:roar_test # unit tests
bazelisk test tests:all # integration tests
bazelisk test equivalence_tests:all # equivalence tests
bazelisk run -c opt equivalence_testing/benchmarks:benchmarks # benchmarksOutput is configured by Config (or oar_config_t for C) when the renderer
instance is created and contains the following:
target_layout: Output layout. Details below.samples_per_channel: Buffer chunk size in samples (per channel).sampling_rate: Sampling rate in Hz (e.g., 48000).
Output layouts are defined in oar_layout_t in c_types.rs for C++ or in the
Layout enum.
- Loudspeaker layouts: From
ck_oar_layout_monotock_oar_layout_sound_system_g_490. - Binaural:
ck_oar_layout_binaural
C++
oar_config_t config;
config.target_layout = ck_oar_layout_514;
config.samples_per_channel = 1024;
config.sampling_rate = 48000;Rust
let config = Config::new(
Layout::Layout51,
Samples::new(1024)?,
SampleRate::new(48000)?,
)?;-
Every input audio element must belong to a group so one group is required, but ROAR can be also be configured with two groups.
-
Audio groups are analogous to the concept of IAMF sub-mixes, so having two groups allows applying gains to the group of elements, indepent of the other group.
C++
// Add an audio group and get the ID (0 or 1)
int group_id = roar_add_audio_group(oar);
if (group_id < 0) {
// Handle error (e.g. ck_oar_error_nomem or ck_oar_error_busy if max groups reached)
}Rust
// Add group
let group_id = rdr.add_audio_group()?;Groups can be configured by updating metadata.
Group metadata contains one of the following:
- Gain (
oar_metadata_gain_t): Dynamic output gain applied to the mixed group output. Can be constant, per-sample, or animated (in dB). Has an ID to identify the gain parameter. - Head Rotation (
quaternion_t): Listener head orientation for binaural rendering (w, x, y, z).- Expected to be a unit quaternion (sum of squares is 1.0).
- Uses the ADM coordinate system (positive X = right, positive Y = front, positive Z = up).
- Although updated via the group API, it applies globally to all binaural sub-renderers.
Duration: In the C API, a duration for head rotation is ignored. For gain updates:
- C API: A duration of
0(or negative) is ignored (no-op). - Rust API: The duration is required (
Samples).
Note: In the C API, the same metadata struct is used for updating both groups and audio elements but groups can only accept gain and head rotation and audio elements can only accept gain, object position, and demix/downmix mode. Object positions and IAMF downmix modes are element metadata, not group metadata.
C++
// 1. Update group gain (constant gain of -3.0 dB for 1024 samples)
oar_metadata_t gain_meta = {};
gain_meta.type = ck_metadata_gain;
gain_meta.duration = 1024;
gain_meta.gain.id = 1; // Unique ID for this gain parameter
gain_meta.gain.param_type = ck_param_constant;
gain_meta.gain.constant_gain = -3.0f;
roar_update_metadata(oar, group_id, &gain_meta);
// 2. Update head rotation (quaternion for 1024 samples)
oar_metadata_t rot_meta = {};
rot_meta.type = ck_metadata_head_rotation;
rot_meta.head_rotation.w = 1.0f;
rot_meta.head_rotation.x = 0.0f;
rot_meta.head_rotation.y = 0.0f;
rot_meta.head_rotation.z = 0.0f;
roar_update_metadata(oar, group_id, &rot_meta);In the Rust API, there are separate APIs for updating the group gain and head rotation:
Rust
// 1. Update group gain (constant gain of -3.0 dB for 1024 samples)
let gain = Gain::new_constant(Decibels(-3.0))?;
rdr.update_group_gain(group_id, &gain, Samples(1024))?;
// 2. Update head rotation (w, x, y, z quaternion).
// Head rotation is updated globally via set_head_rotation
rdr.set_head_rotation(Quaternion::identity())?;Group loudness can also be modified:
C++
roar_enable_loudness_processor(oar, 1); // Enable. It is default off.
roar_set_loudness(oar, group_id, current_loudness_db, target_loudness_db);Rust
rdr.enable_loudness_processor(true)?;
rdr.set_loudness(group_id, current_loudness_db, target_loudness_db)?;Note: Audio Element IDs (provided by the caller) must be unique across all audio groups.
The Audio Element has the following configuration when created:
- Type
- Type-specific config
- Parameters
Type-specific config
| Input Type | Type-specific Config |
|---|---|
| Channel-based | layout: the layout of the input audio |
| Scene-based | order: oar_hoa_t, 0 to 4 |
| Object-based | num_objects: 1 or 2 |
Available Parameters
| Parameter | Values |
|---|---|
| Downmix Info (only channel-based elements) | |
| mode | Values from the IAMF spec |
| weight_index | Index into the mapping table |
| Rendering Config (only for binaural elements) | |
| headphones_rendering_mode | stereo, world-locked, or head-locked |
| binaural_filter_profile | ambient, direct, or reverberant |
Note: Using ck_world_locked_restricted (C API) or WorldLockedRestricted
(Rust API) means that element is rendered to stereo, not binaural.
C++
oar_audio_element_config_t elem_cfg = {};
// Example 1: Channel-based element (Stereo) with headphones rendering config
elem_cfg.type = ck_channel_based;
elem_cfg.cbc.layout = ck_oar_layout_stereo;
elem_cfg.parameters.flags = def_parameter_set_flag_iamf_element_rendering_config;
elem_cfg.parameters.element_rendering_config.headphones_rendering_mode = ck_world_locked; // or ck_head_locked
elem_cfg.parameters.element_rendering_config.binaural_filter_profile = ck_binaural_filter_profile_default;
uint32_t element_id = 42;
roar_add_audio_element(oar, group_id, element_id, &elem_cfg);
// Example 2: Channel-based element (5.1) with Downmix Info
oar_audio_element_config_t downmix_elem_cfg = {};
downmix_elem_cfg.type = ck_channel_based;
downmix_elem_cfg.cbc.layout = ck_oar_layout_51;
downmix_elem_cfg.parameters.flags = def_parameter_set_flag_iamf_downmix_info;
downmix_elem_cfg.parameters.downmix_info.mode = 0; // IAMF downmix mode 0
downmix_elem_cfg.parameters.downmix_info.weight_index = 0;
roar_add_audio_element(oar, group_id, element_id + 1, &downmix_elem_cfg);Rust
let rendering_cfg = ElementRenderingConfig {
headphones_rendering_mode: HeadphonesRenderingMode::WorldLocked, // or HeadLocked
binaural_filter_profile: BinauralFilterProfile::Ambient, // or Direct, Reverberant
};
let elem_cfg = AudioElementConfig::ChannelBased(ChannelBasedConfig {
layout: Layout::Stereo,
downmix_info: None,
rendering_config: Some(rendering_cfg),
});
rdr.add_element(group_id, element_id, &elem_cfg)?;- Audio elements can be removed by ID.
- Input audio data is provided by ID.
- Elements can be updated with dynamic metadata:
- Gain (
oar_metadata_gain_t): Dynamic element volume control (applies to all element types). - Object Positions (
oar_metadata_object_positions_t): Polar or Cartesian coordinates, both static and animated (applies to object-based elements only). See below for details. - Downmix Mode (
oar_metadata_iamf_downmix_mode_t): Dynamic downmix mode override (applies to channel-based with downmix only).
- Gain (
Duration: Downmix Mode supports optional duration. 0 (C) or None (Rust)
indicates the update should persist indefinitely. Gain and Object Positions
updates require duration. In the C API, a duration of 0 (or negative) means
the metadata update is ignored!
Note: In the C API, the same metadata struct is used for groups and audio elements but groups can only accept gain and head rotation and audio elements can only accept gain, object position, and demix/downmix mode.
Object position Object position can be static or animated, in either Polar or Cartesian systems:
- Static vs animated: Static positions keep the object fixed in place. Animated positions specify a trajectory (linear, step, or Bezier) over a duration.
- Coordinate systems and expected ranges:
- Polar coordinate system:
azimuth: Horizontal angle in degrees, range[-180.0, 180.0](0° = front, 90° = left, -90° = right, ±180° = behind).elevation: Vertical angle in degrees, range[-90.0, 90.0](0° = horizon, 90° = above, -90° = below).distance: Normalized distance from listener, range[0.0, 1.0].
- Cartesian coordinate system (ADM object coordinates):
x: Left (negative) / Right (positive) in[-1.0, 1.0].y: Back (negative) / Front (positive) in[-1.0, 1.0].z: Down (negative) / Up (positive) in[-1.0, 1.0].- Note: Cartesian magnitudes (vector distances) greater than 1.0 are clamped to 1.0 during polar conversion.
- Polar coordinate system:
- Evaluation of animated gains: The core OBR and OLR DSP renderers only
accept static polar coordinates. To render dynamic movement, the library's
outer routing layer (
AudioElementsRenderer) splits the audio block into smaller intervals of sizemetadata_unit_to_process, configured viaroar_set_metadata_unit_to_process(defaulting to the audio block sizesamples_per_channelconfigured at creation). For each render, the router calculates the snapshot position along the animation trajectory, flattens Cartesian coordinates to Polar, and updates the sub-renderer with the resulting static polar coordinates before rendering the sub-frame. - Animation smoothness: Choosing a smaller
metadata_unit_to_process(e.g., 64 samples) increases the "frame rate" of position updates, producing smoother spatial transitions at the cost of additional sub-renderer metadata updates.
C++
// 1. Update element gain (constant gain of -1.5 dB for 1024 samples)
oar_metadata_t gain_meta = {};
gain_meta.type = ck_metadata_gain;
gain_meta.duration = 1024;
gain_meta.gain.id = 1;
gain_meta.gain.param_type = ck_param_constant;
gain_meta.gain.constant_gain = -1.5f;
roar_update_audio_element_metadata(oar, element_id, &gain_meta);
// 2. Update object positions (if element is object-based)
oar_metadata_t pos_meta = {};
pos_meta.type = ck_metadata_object_positions;
pos_meta.duration = 1024;
pos_meta.object_positions.param_type = ck_param_constant;
pos_meta.object_positions.position_type = ck_polar;
pos_meta.object_positions.num_objects = 1;
pos_meta.object_positions.polar_positions[0].azimuth = 30.0f;
pos_meta.object_positions.polar_positions[0].elevation = 0.0f;
pos_meta.object_positions.polar_positions[0].distance = 1.0f;
roar_update_audio_element_metadata(oar, object_element_id, &pos_meta);
// 3. Update downmix mode (if element is channel-based and using downmix)
oar_metadata_t dmix_meta = {};
dmix_meta.type = ck_metadata_iamf_downmix_mode;
dmix_meta.duration = 1024;
dmix_meta.iamf_downmix_mode.mode = 1; // Switch to downmix mode 1
roar_update_audio_element_metadata(oar, channel_element_id, &dmix_meta);
// 4. Provide audio data block
oar_audio_block_t input_block;
input_block.channels = 2;
input_block.samples_per_channel = 1024;
input_block.data = planar_input_floats; // Pointer to (channels * samples) floats
roar_update_audio_element_data(oar, element_id, &input_block);
// 5. Remove the element
roar_remove_audio_element(oar, element_id);Rust
// 1. Update element gain (constant gain of -1.5 dB for 1024 samples)
let gain = Gain::new_constant(Decibels(-1.5))?;
let gain_id = 1;
rdr.update_element_gain(element_id, gain_id, &gain, Samples(1024))?;
// 2. Update object positions (if element is object-based)
let polar = PolarCoordinate::new_from_floats(30.0f32, 0.0f32, 1.0f32)?;
let positions = ObjectPosition::Polar(vec![polar]);
rdr.update_element_positions(object_element_id, &positions, Samples(1024))?;
// 3. Update downmix mode (if element is channel-based and using downmix)
rdr.update_element_downmix_mode(
channel_element_id, DownmixMode::Mode2NegOffset, Some(Samples(1024)))?;
// Note: In Rust, audio inputs are not registered beforehand via a data block.
// Instead, they are passed as a slice of references directly in the render call:
// rdr.render(&inputs, &mut output)?;
// 4. Remove the element
rdr.remove_element(element_id)?;These settings are supported in both C/C++ and Rust APIs.
C++
roar_enable_limiter(oar, 1); // EnableRust
rdr.enable_limiter(true)?;Enables head tracking for binaural rendering. Enable if you will provide head rotation information for the user's head to enable world-locked binaural audio.
C++
roar_enable_head_tracking(oar, 1); // EnableRust
rdr.enable_head_tracking(true)?;C/C++
// Example config for 5.1 output.
oar_config_t config;
config.target_layout = ck_oar_layout_51;
config.samples_per_channel = 1024;
config.sampling_rate = 48000;
// Create an instance of OAR.
oar_t* oar = roar_create(&config);
if (!oar) {
// Handle failure
}
// Add an audio group
int group_id = roar_add_audio_group(oar);
if (group_id < 0) {
// Handle failure
}
// Configure a 7.1.4 channel-based audio element (12 channels)
oar_audio_element_config_t element_config = {};
element_config.type = ck_channel_based;
element_config.cbc.layout = ck_oar_layout_714;
// Configure with downmix parameters to enable downmix renderer
element_config.parameters.flags = def_parameter_set_flag_iamf_downmix_info;
element_config.parameters.downmix_info.mode = 0;
element_config.parameters.downmix_info.weight_index = 0;
uint32_t element_id = 42;
int return_code = roar_add_audio_element(oar, group_id, element_id, &element_config);
if (return_code != 0) {
// Handle failure
}
// Fill the input data (12 channels * 1024 samples planar).
// The input data should not be destroyed before render is called.
oar_audio_block_t input_block;
input_block.channels = 12;
input_block.samples_per_channel = 1024;
input_block.data = input_buffer_ptr; // float*
return_code = roar_update_audio_element_data(oar, element_id, &input_block);
if (return_code != 0) {
// Handle failure
}
// Create output struct with space for 5.1 output (6 channels * 1024 samples planar)
oar_audio_block_t output_block;
output_block.channels = 6;
output_block.samples_per_channel = 1024;
output_block.data = output_buffer_ptr; // float*
// Render.
// This example only has one render call (and one set of inputs/outputs), but
// a real application would repeatedly call `roar_update_audio_element_data` and
// `roar_render`.
return_code = roar_render(oar, &output_block);
if (return_code != 0) {
// Handle failure
}
// Clean up
roar_destroy(oar);Rust
use roar::common::definitions::{
Config, Layout, AudioElementConfig, ChannelBasedConfig, OarError, Samples,
PlanarBufferRef, PlanarBufferMut, SampleRate
};
use roar::renderer::RoarRenderer;
fn render_audio() -> Result<(), OarError> {
let config = Config::new(
Layout::Layout51,
Samples::new(1024)?,
SampleRate::new(48000)?,
)?;
// Create renderer (Configuring status)
let mut rdr = RoarRenderer::create(&config)?;
// Add audio group
let group_id = rdr.add_audio_group()?;
// Configure and add audio element
let element_cfg = AudioElementConfig::ChannelBased(ChannelBasedConfig {
layout: Layout::Stereo,
downmix_info: None,
rendering_config: None,
});
let element_id = 42;
rdr.add_element(group_id, element_id, &element_cfg)?;
// Transition to rendering state (optional, render() will call it automatically)
rdr.init_rendering()?;
// Fill input data (planar format: slice of channel slices)
let input_ch0 = vec![0.0f32; 1024];
let input_ch1 = vec![0.0f32; 1024];
let input_slices = [&input_ch0[..], &input_ch1[..]];
let inputs = [
(element_id, PlanarBufferRef::Slices(&input_slices[..]))
];
// Prepare output buffer (planar format: 5.1 target has 6 channels)
let mut output_data = vec![0.0f32; 6 * 1024];
let mut output = PlanarBufferMut::Flat {
data: &mut output_data,
num_channels: 6,
samples_per_channel: 1024,
};
// Render
// This example only has one render call (and one set of inputs/outputs),
// but a real application would repeatedly call
// `roar_update_audio_element_data` and `roar_render`.
rdr.render(&inputs, &mut output)?;
Ok(())
}C++
// 1. Create OAR config for Stereo loudspeaker output
oar_config_t config;
config.target_layout = ck_oar_layout_stereo;
config.samples_per_channel = 1024;
config.sampling_rate = 48000;
oar_t* oar = roar_create(&config);
int group_id = roar_add_audio_group(oar);
// 2. Add scene-based (1OA) audio element (4 channels)
oar_audio_element_config_t elem_cfg = {};
elem_cfg.type = ck_scene_based;
elem_cfg.sbc.order = ck_oar_1oa;
uint32_t element_id = 200;
roar_add_audio_element(oar, group_id, element_id, &elem_cfg);
// 3. Provide 4-channel input data block (planar)
oar_audio_block_t input_block;
input_block.channels = 4;
input_block.samples_per_channel = 1024;
input_block.data = ambisonic_audio_ptr; // float*
roar_update_audio_element_data(oar, element_id, &input_block);
// 4. Render to Stereo output (2 channels)
oar_audio_block_t output_block;
output_block.channels = 2;
output_block.samples_per_channel = 1024;
output_block.data = output_buffer_ptr; // float*
roar_render(oar, &output_block);
roar_destroy(oar);Rust
// 1. Create config for Stereo loudspeaker output
let config = Config::new(
Layout::Stereo,
Samples::new(1024)?,
SampleRate::new(48000)?,
)?;
let mut rdr = RoarRenderer::create(&config)?;
let group_id = rdr.add_audio_group()?;
// 2. Add scene-based (1OA) audio element
let elem_cfg = AudioElementConfig::SceneBased(SceneBasedConfig {
order: HighOrderAmbisonics::Order1,
rendering_config: None,
});
let element_id = 200;
rdr.add_element(group_id, element_id, &elem_cfg)?;
// 3. Prepare inputs (4 channels * 1024 samples planar)
let inputs = [
(element_id, PlanarBufferRef::Slices(&input_channels[..]))
];
// Prepare output buffer (planar format: Stereo has 2 channels)
let mut output_data = vec![0.0f32; 2 * 1024];
let mut output = PlanarBufferMut::Flat {
data: &mut output_data,
num_channels: 2,
samples_per_channel: 1024,
};
// 4. Render
rdr.render(&inputs, &mut output)?;C++
// 1. Create OAR config for 5.1 loudspeaker output
oar_config_t config;
config.target_layout = ck_oar_layout_51;
config.samples_per_channel = 1024;
config.sampling_rate = 48000;
oar_t* oar = roar_create(&config);
int group_id = roar_add_audio_group(oar);
// 2. Add object-based audio element (1 object)
oar_audio_element_config_t elem_cfg = {};
elem_cfg.type = ck_object_based;
elem_cfg.obc.num_objects = 1;
uint32_t element_id = 100;
roar_add_audio_element(oar, group_id, element_id, &elem_cfg);
// 3. Render loop with dynamic position updates
float current_azimuth = -90.0f;
while (rendering) {
// Update object position (panning from left to right)
oar_metadata_t pos_meta = {};
pos_meta.type = ck_metadata_object_positions;
pos_meta.duration = 1024;
pos_meta.object_positions.param_type = ck_param_constant;
pos_meta.object_positions.position_type = ck_polar;
pos_meta.object_positions.num_objects = 1;
pos_meta.object_positions.polar_positions[0].azimuth = current_azimuth;
pos_meta.object_positions.polar_positions[0].elevation = 0.0f;
pos_meta.object_positions.polar_positions[0].distance = 1.0f;
roar_update_audio_element_metadata(oar, element_id, &pos_meta);
// Provide mono input block for the object
oar_audio_block_t input_block;
input_block.channels = 1;
input_block.samples_per_channel = 1024;
input_block.data = object_audio_ptr; // float*
roar_update_audio_element_data(oar, element_id, &input_block);
// Render to 5.1 output (6 channels)
oar_audio_block_t output_block;
output_block.channels = 6;
output_block.samples_per_channel = 1024;
output_block.data = output_buffer_ptr; // float*
roar_render(oar, &output_block);
current_azimuth += 1.0f;
if (current_azimuth > 90.0f) current_azimuth = -90.0f;
}
roar_destroy(oar);Rust
// 1. Create config for 5.1 loudspeaker output
let config = Config::new(
Layout::Layout51,
Samples::new(1024)?,
SampleRate::new(48000)?,
)?;
let mut rdr = RoarRenderer::create(&config)?;
let group_id = rdr.add_audio_group()?;
// 2. Add object-based audio element (1 object)
let elem_cfg = AudioElementConfig::ObjectBased(ObjectBasedConfig {
num_objects: 1,
rendering_config: None,
});
let element_id = 100;
rdr.add_element(group_id, element_id, &elem_cfg)?;
// 3. Render loop with dynamic position updates
let mut current_azimuth = -90.0f32;
while rendering {
// Update object position (panning from left to right)
let polar = PolarCoordinate::new_from_floats(current_azimuth, 0.0f32, 1.0f32)?;
let positions = ObjectPosition::Polar(vec![polar]);
rdr.update_element_positions(element_id, &positions, Some(Samples(1024)))?;
// Prepare inputs (1 channel * 1024 samples planar)
let inputs = [
(element_id, PlanarBufferRef::Slices(&object_audio[..]))
];
// Prepare output buffer (planar format: 6 channels for 5.1)
let mut output_data = vec![0.0f32; 6 * 1024];
let mut output = PlanarBufferMut::Flat {
data: &mut output_data,
num_channels: 6,
samples_per_channel: 1024,
};
// Render to 5.1
rdr.render(&inputs, &mut output)?;
current_azimuth += 1.0f32;
if current_azimuth > 90.0f32 {
current_azimuth = -90.0f32;
}
}C++
// 1. Create OAR config for Binaural output
oar_config_t config;
config.target_layout = ck_oar_layout_binaural;
config.samples_per_channel = 1024;
config.sampling_rate = 48000;
oar_t* oar = roar_create(&config);
int group_id = roar_add_audio_group(oar);
// 2. Add 5.1.4 channel-based audio element (10 channels) configured for world-locked rendering
oar_audio_element_config_t elem_cfg = {};
elem_cfg.type = ck_channel_based;
elem_cfg.cbc.layout = ck_oar_layout_514;
elem_cfg.parameters.flags = def_parameter_set_flag_iamf_element_rendering_config;
elem_cfg.parameters.element_rendering_config.headphones_rendering_mode = ck_world_locked;
elem_cfg.parameters.element_rendering_config.binaural_filter_profile = ck_binaural_filter_profile_default;
uint32_t element_id = 101;
roar_add_audio_element(oar, group_id, element_id, &elem_cfg);
// 3. Enable head tracking
roar_enable_head_tracking(oar, 1);
// 4. Render loop with head rotation updates
while (rendering) {
// Update head rotation from sensor data
oar_metadata_t rot_meta = {};
rot_meta.type = ck_metadata_head_rotation;
rot_meta.duration = 1024;
rot_meta.head_rotation.w = sensor_q.w;
rot_meta.head_rotation.x = sensor_q.x;
rot_meta.head_rotation.y = sensor_q.y;
rot_meta.head_rotation.z = sensor_q.z;
// Head rotation is updated via the group API
roar_update_metadata(oar, group_id, &rot_meta);
// Provide 5.1.4 input data block
oar_audio_block_t input_block;
input_block.channels = 10;
input_block.samples_per_channel = 1024;
input_block.data = input_audio_ptr; // float*
roar_update_audio_element_data(oar, element_id, &input_block);
// Render to Binaural (2 channels)
oar_audio_block_t output_block;
output_block.channels = 2;
output_block.samples_per_channel = 1024;
output_block.data = output_buffer_ptr; // float*
roar_render(oar, &output_block);
}
roar_destroy(oar);Rust
// 1. Create OAR config for Binaural output
let config = Config::new(
Layout::Binaural,
Samples::new(1024)?,
SampleRate::new(48000)?,
)?;
let mut rdr = RoarRenderer::create(&config)?;
let group_id = rdr.add_audio_group()?;
// 2. Add 5.1.4 channel-based audio element (10 channels) configured for world-locked rendering
let rendering_cfg = ElementRenderingConfig {
headphones_rendering_mode: HeadphonesRenderingMode::WorldLocked,
binaural_filter_profile: BinauralFilterProfile::Ambient,
};
let elem_cfg = AudioElementConfig::ChannelBased(ChannelBasedConfig {
layout: Layout::Layout514,
downmix_info: None,
rendering_config: Some(rendering_cfg),
});
let element_id = 101;
rdr.add_element(group_id, element_id, &elem_cfg)?;
// 3. Enable head tracking
rdr.enable_head_tracking(true)?;
// 4. Render loop with head rotation updates
while rendering {
// Update head rotation from sensor data
rdr.set_head_rotation(sensor_quaternion)?;
// Prepare inputs (10 channels * 1024 samples planar)
let inputs = [
(element_id, PlanarBufferRef::Slices(&input_channels[..]))
];
// Prepare output buffer (planar format: Binaural has 2 channels)
let mut output_data = vec![0.0f32; 2 * 1024];
let mut output = PlanarBufferMut::Flat {
data: &mut output_data,
num_channels: 2,
samples_per_channel: 1024,
};
// Render to Binaural (2 channels)
rdr.render(&inputs, &mut output)?;
}Via the C or Rust API, ROAR is structured into five main layers.
- C API Entrypoint (
ffi.rs): Exposes#[unsafe(no_mangle)] extern "C"functions matching the original OAR C specification. It catches Rust panics and translates errors to integer status codes. - Safe State & Type Translation (
c_types.rs/ffi_ext.rs): Conversions validate C inputs and instantiate safe Rust types. - Rust Orchestrator (
roar_renderer.rs): Uses a runtime state machine (Status) to manage transition from configuration to rendering, ensuring eager allocation of sub-renderers before rendering starts. - Routing & Slicing (
audio_elements_renderer.rs): Manages routing of active elements, applying sub-frame temporal block slicing and dynamic metadata interpolation. - DSP Backends: Concrete renderers (EAR, OBR, OLR, Downmix) performing
actual DSP calculations. All backends implement the
AudioRenderertrait.
graph TD
%% Clients
C_API["C Client (C++ / IAMF Decoders)"]
Rust_API["Rust Client (Native Rust Applications)"]
%% Layer 1: C Boundary
subgraph Layer 1: C API Entrypoint [ffi.rs]
FFI["roar_create() / roar_render()"]
PanicBound["catch_unwind (Guarantees no Rust panics cross C ABI)"]
end
%% Layer 2: Wrapper & Adaptation
subgraph Layer 2: Safe State & Types [c_types.rs / ffi_ext.rs]
Wrapper["OarWrapper"]
SafeTypes["to_safe() (Translates C Structs to Safe Rust Types)"]
end
%% Layer 3: Runtime State Machine
subgraph Layer 3: Rust Orchestrator [roar_renderer.rs]
Orchestrator["RoarRenderer (Runtime State Machine)"]
Status["Status (Configuring vs Rendering)"]
end
%% Layer 4: Routing & Slicing
subgraph Layer 4: Routing & Slicing [audio_elements_renderer.rs]
Router["AudioElementsRenderer (Temporal Slicing & Routing)"]
end
%% Layer 5: Renderers
subgraph Layer 5: DSP Backends [src/renderer/]
EAR["EAR<br>Inputs: Channel/Scene<br>Outputs: Loudspeakers"]
OBR["OBR<br>Inputs: Channel/Scene/Object<br>Outputs: Binaural (Headphones)"]
OLR["OLR<br>Inputs: Objects<br>Outputs: Loudspeakers"]
Downmix["Downmix Renderer<br>Inputs: Channel-based<br>Outputs: Downmixed Loudspeakers"]
end
%% Connections
C_API -->|Calls via C Decl| FFI
FFI --> PanicBound
PanicBound --> SafeTypes
PanicBound --> Wrapper
Wrapper --> Orchestrator
Rust_API -->|Direct Call| Orchestrator
Orchestrator --> Status
Orchestrator --> Router
Router --> EAR
Router --> OBR
Router --> OLR
Router --> Downmix
| Name | Directory | Used for input types | Used for output types |
|---|---|---|---|
| EAR | src/renderer/ear/ |
Channel-based, Scene-based | Loudspeaker (including world-locked restricted) |
| Open Binaural Renderer (OBR) | src/renderer/obr/ |
Channel-based, Scene-based, Object-based | Binaural |
| Open Loudspeaker Renderer (OLR) | src/renderer/olr/ |
Object-based | Loudspeaker |
| Downmix | src/renderer/downmix/ |
Channel-based | Loudspeaker |
The active internal renderer is selected during initialization based on the target output layout and the configuration of the audio elements:
- Binaural output (
ck_oar_layout_binaural)- OBR is always selected for all input types. For channel-based inputs, virtual speaker encoding is performed before decoding to binaural.
- Loudspeaker output
- Scene-based input: EAR is always used.
- Object-based input: OLR is always selected.
- Channel-based input: Selection depends on the downmix configuration:
- Downmix renderer is selected if:
- Downmix information is provided in the element parameters
(
def_parameter_set_flag_iamf_downmix_infois set). - The input layout and target layout form a supported downmix combination.
- Downmix information is provided in the element parameters
(
- EAR is selected as a fallback if the above conditions are not met.
- Downmix renderer is selected if:
ROAR uses the following Rust FFT libraries for frequency-domain processing in OBR:
rustfft: A high-performance, standards-compliant FFT library written in pure Rust.realfft: A wrapper aroundrustfftspecifically optimized for real-to-complex and complex-to-real FFTs, which cuts processing time and memory usage in half compared to complex-to-complex FFTs.
The FftManager in OBR pre-allocates all necessary plans and scratch buffers
during initialization to ensure zero heap allocation on the audio thread during
real-time rendering.
ROAR does not use explicit, platform-specific SIMD intrinsics (such as x86 AVX/SSE or ARM NEON). Instead, it relies on:
- Compiler Autovectorization: The vector math operations (e.g., in
simd_utils.rsfor OBR) are written as simple, contiguous slice loops with bounds checks elided where possible. This allows LLVM to automatically generate optimal SIMD instructions for the target architecture during release builds. - Library-Level SIMD: The underlying
rustfftlibrary contains its own architecture-specific SIMD implementations (supporting AVX, SSE, and NEON) which are automatically used when compiling for supported platforms.