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 |
|---|---|
|
Time between envelope samples. Sets the range resolution, and is
republished as the |
|
Membrane diameter. The model derives the detector area
|
|
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. |
|
Transmit center frequency, republished verbatim as the
|
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 |
|---|---|---|
|
|
Echo amplitude at each envelope sample. |
|
|
Time offset relative to the sensor timestamp, one entry per signal way. |
|
|
|
|
|
Transmitter that emitted the signal way. |
|
|
Receiver group that observed it. |
|
|
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 |
|---|---|---|
|
|
The model’s own frame counter, never reset across a live reconfiguration. |
|
|
Signal ways in this frame. |
|
|
Samples of the echo envelope per signal way. |
|
|
Entries in each channel: |
|
|
Firing cycle this frame belongs to. |
|
|
Transmit center frequency, in Hz. |
|
|
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.