Acoustic Sensors#

An ovrtx acoustic scene needs an OmniAcoustic sensor prim and a RenderProduct whose SignalWays RenderVar delivers the sensor’s output.

Unlike lidar and radar, the acoustic model does not report discrete returns. It transmits a pulse and reports the received echo envelope: an amplitude series sampled uniformly in time, one series per signal way. A signal way is one transmitter/receiver-group pairing from the sensor’s firing sequence. This is why the acoustic composite is SignalWays rather than PointCloud, and why it has no Counts channel and no per-point valid flag – the frame’s extent comes from its parameters instead.

Configure the Acoustic Prim#

    # The sensor fires along its local -Z. This transform maps that onto world
    # +X, where the wall stands, and puts the sensor 0.5 m up, level with the
    # wall's center. It is authored as a matrix rather than as translate/rotate
    # ops so it matches the layout the runtime "omni:xform" attribute expects.
    def OmniAcoustic "Acoustic" (
        prepend apiSchemas = ["OmniSensorGenericAcousticWpmAPI"]
    )
    {
        double2 omni:sensor:frameRate = (10, 1)

        # Ray tracing: a wide ultrasonic lobe pointing along sensor -Z.
        float omni:sensor:WpmAcoustic:azSpanDeg = 90
        float omni:sensor:WpmAcoustic:elSpanDeg = 60
        uint omni:sensor:WpmAcoustic:raysPerDeg = 3
        uint omni:sensor:WpmAcoustic:traceTreeDepth = 2

        matrix4d xformOp:transform = ( (0, -1, 0, 0), (0, 0, 1, 0), (-1, 0, 0, 0), (0, 0, 0.5, 1) )
        uniform token[] xformOpOrder = ["xformOp:transform"]
    }

Apply OmniSensorGenericAcousticWpmAPI for the generic WPM acoustic model. The sensor fires along its local -Z, so in a Z-up scene rotate the prim so that -Z points in the intended forward direction.

Note

Motion BVH is required, not preferred. The acoustic model runs the multi-tick trace path, and every envelope comes back empty without it. Set it on RendererConfig at renderer creation time – see Renderer Configuration. The model also publishes nothing on its first frames, so take a few warm-up steps before reading an envelope.

Important Output Attributes#

Attribute

Use

omni:sensor:WpmAcoustic:sampleDuration

Time between envelope samples. Sets the range resolution, and is republished as the sampleDurationUs parameter that a consumer needs to convert a sample index back to a range.

omni:sensor:WpmAcoustic:membraneDiameter

Membrane diameter. The model derives the detector area pi * d^2 / 4 from it, so the received amplitude rises with it.

omni:sensor:WpmAcoustic:enableInterference

Select the interference trace kernel. Read at configuration time and cannot be changed at runtime: the model logs a warning and keeps the old value, so exercising both paths needs two sensor prims.

omni:sensor:WpmAcoustic:centerFrequency

Transmit center frequency, republished verbatim as the centerFrequencyHz parameter.

The lobe and tracing controls – azSpanDeg, elSpanDeg, raysPerDeg, traceTreeDepth – shape what the sensor sees rather than how the output is laid out. They and the remaining model parameters are documented with their defaults in the OmniSensorGenericAcousticWpmAPI schema.

The firing sequence (firingSeq), receiver groups (rxGroup), and mount transforms (sensorMount) determine how many signal ways a frame carries. Their schema defaults describe a single transmitter, a single receiver group and one firing event, which yields one signal way per frame.

Configure SignalWays Output#

    # The acoustic model publishes one composite RenderVar, "SignalWays". It
    # carries every per-sample channel plus the frame header as parameters, so
    # the product requests a single var rather than one per channel.
    def "Render"
    {
        def "Products"
        {
            def RenderProduct "AcousticProduct"
            {
                rel camera = </World/Acoustic>
                rel orderedVars = [<../../Vars/SignalWays>]
            }
        }

        def "Vars"
        {
            def RenderVar "SignalWays"
            {
                uniform string sourceName = "SignalWays"
            }
        }
    }

SignalWays is a composite render variable: it carries every channel plus the frame header in one output, so the RenderProduct requests a single var rather than one per channel.

SignalWays Channels#

Channel

Type

Meaning

Amplitudes

float32

Echo amplitude at each envelope sample.

TimeOffsetNs

int32

Time offset relative to the sensor timestamp, one entry per signal way.

Flags

uint8

ElementFlags::VALID (0x40) at the signal way’s own index, set when that way produced a return; zero elsewhere. Indexed per signal way, not per element, in both trace modes.

TransmitterIds

float32

Transmitter that emitted the signal way.

ReceiverIds

float32

Receiver group that observed it.

ChannelIds

float32

Transmit/receive channel pairing.

SignalWays Parameters#

The frame header arrives as composite params rather than as channels, which is the role Counts plays for a point cloud.

Parameter

Type

Meaning

frameId

uint64

The model’s own frame counter, never reset across a live reconfiguration.

numSignalWays

uint32

Signal ways in this frame.

numSamplesPerSignalWay

uint32

Samples of the echo envelope per signal way.

numElements

uint32

Entries in each channel: numSignalWays * numSamplesPerSignalWay.

cycleIndex

uint32

Firing cycle this frame belongs to.

centerFrequencyHz

float32

Transmit center frequency, in Hz.

sampleDurationUs

float32

Time between envelope samples, in microseconds.

Interpreting Acoustic Output#

Every channel is a 1D array of numElements entries. Amplitudes fills all of them, one per envelope sample, so it reshapes to one row per signal way. The others describe a signal way as a whole and carry one entry per signal way, in the first numSignalWays positions; the remainder are zero.

def read_acoustic_signal_ways(frame) -> dict:
    """Map the SignalWays composite tensor to CPU and return one frame of echoes."""
    with frame.render_vars[SIGNAL_WAYS_RENDER_VAR].map(device=ovrtx.Device.CPU) as signal_ways:

        def param(name: str) -> float:
            return float(np.from_dlpack(signal_ways.params[name])[0])

        num_signal_ways = int(param("numSignalWays"))
        num_samples = int(param("numSamplesPerSignalWay"))

        if num_signal_ways == 0 or num_samples == 0:
            return {"num_signal_ways": 0, "num_samples": 0}

        num_elements = num_signal_ways * num_samples

        def channel(name: str) -> np.ndarray:
            return np.from_dlpack(signal_ways[name])[:num_elements]

        return {
            "num_signal_ways": num_signal_ways,
            "num_samples": num_samples,
            "sample_duration_s": param("sampleDurationUs") * 1.0e-6,
            "center_frequency_hz": param("centerFrequencyHz"),
            "cycle_index": int(param("cycleIndex")),
            "envelopes": channel("Amplitudes").reshape(num_signal_ways, num_samples).copy(),
            # The identifier channels carry one entry per signal way rather than one per
            # element, so only the first num_signal_ways entries are meaningful.
            "transmitter_ids": channel("TransmitterIds")[:num_signal_ways].astype(int),
            "receiver_ids": channel("ReceiverIds")[:num_signal_ways].astype(int),
            "channel_ids": channel("ChannelIds")[:num_signal_ways].astype(int),
        }


An envelope is sampled uniformly at sampleDurationUs, so a sample’s index is its round-trip echo time in units of that duration, and range is half the round trip at the speed of sound. Read the sample duration from the parameter rather than hardcoding it: it is an authored sensor setting.

def sample_ranges_m(frame_data: dict) -> np.ndarray:
    """Return the round-trip range in meters of each envelope sample."""
    sample_index = np.arange(frame_data["num_samples"], dtype=np.float64)
    return sample_index * frame_data["sample_duration_s"] * SPEED_OF_SOUND_M_PER_S / 2.0


At the default sampleDuration of 102.4 us, one sample spans about 1.8 cm of range, which bounds how precisely a peak can be located.

For the runnable end-to-end version of the above, see Python: Acoustic Sensor.