Elixir NIF bindings (Rustler / RustlerPrecompiled) for a Rust GCC-PHAT → SRP-PHAT acoustic source localizer over a known microphone-array geometry.
ExSrpPhat.localize/3 fuses a single time-aligned frame-set (one PCM frame per
emplacement) against the array's known WGS-84 ECEF geometry into zero or more
localized ExSrpPhat.Source structs. The NIF runs on a dirty CPU scheduler; PCM and
geometry cross the boundary as packed binaries (never term lists), and the grid search is
rayon-parallel.
I/O contract
@spec localize(frames, geometry, opts) :: {:ok, [ExSrpPhat.Source.t()]} | {:error, term()}
# frames: [%{emplacement_id: term(), samples: [float()]}] # one frame per emplacement, equal length
# geometry: %{sample_rate_hz: pos_integer(),
# emplacements: [%{emplacement_id: term(), ecef: {x, y, z}}]} # meters, WGS-84 ECEF
# opts: keyword — :grid_extent_m, :coarse_res_m, :fine_res_m, :min_peak_ratio, :max_sourcesFrames are matched to geometry by emplacement_id (order-independent); the two id sets
must be identical. Input is fully validated in Elixir before crossing the NIF — bad input
returns {:error, reason} (e.g. :too_few_emplacements, :emplacement_mismatch,
:unequal_frame_lengths, {:invalid_sample_rate, v}).
Each detected source:
%ExSrpPhat.Source{
ecef: {x, y, z}, # meters ECEF
velocity_mps: {vx, vy, vz} | nil, # nil in v0 (Doppler deferred)
radial_velocity: float() | nil, # nil in v0
position_covariance: [9 floats] | nil, # 3x3 ECEF row-major, from the TDOA system
confidence: 0.0..1.0, # normalized SRP peak sharpness
dominant_hz: float() # dominant frequency of the steered peak signal
}Source identity
This library is library-neutral and deliberately assigns no cross-solve source_ref.
Stable identity is the caller's concern — dominant_hz is exposed so an adapter can
reproduce a convention such as "band-#{round(dominant_hz)}".
Coordinate frame & units
Positions are WGS-84 ECEF meters. Two rules the math depends on:
- Slant range is straight-line Euclidean (chord) distance,
sqrt(dx²+dy²+dz²)— sound travels the chord, not a great-circle/surface arc. Emplacements at different altitudes are handled correctly. - Geometry is recentered at a reference emplacement before any squared term is formed
(raw ECEF coords are ~5e6 m;
|p_i|² − |p_0|²in raw coordinates loses all precision to catastrophic cancellation). The solve runs locally and shifts back.
Speed of sound is 343.0 m/s (~20 °C). Use ExSrpPhat.Geo to convert lat/lon/alt → ECEF
(WGS-84: a = 6_378_137.0 m, f = 1/298.257223563); the forward transform is exact
closed form, the inverse uses Bowring's method.
| Field suffix / name | Unit |
|---|---|
ecef, _m, position_covariance | meters (m², row-major, for covariance) |
velocity_mps, radial_velocity | meters/second |
sample_rate_hz, dominant_hz | hertz |
confidence | unitless, 0.0–1.0 |
Algorithm
- GCC-PHAT per emplacement pair: real FFT each channel (
realfft), conj-multiply, phase-transform whitening (divide by magnitude), inverse FFT → a sharp correlation vs lag, robust to the source spectrum. - Localization by closed-form TDOA multilateration. Sub-sample TDOAs are read from the
correlation peaks; combinations across reference pairs (one peak per source) are each
solved with the recentered linear system closed by the
|s| = r₀constraint, and the point-sampled SRP score keeps the genuine intersections — localizing overlapping sources without separating them. (A volumetric grid search is unusable here: the whitened peak is ~centimeters wide in position space, far sharper than any affordable grid; the brief's "recenter before squaring" gotcha is precisely the closed-form method.) - Confidence from the pooled SRP response surface (
rayon-parallel coarse grid): normalized peak sharpness relative to the field. - Covariance from the linearized range-difference system (
σ²·(JᵀJ)⁻¹). - dominant_hz from the steered-and-summed peak signal's strongest spectral bin.
Doppler / velocity estimation is deferred (returns nil) — a single time-aligned
frame-set does not constrain it.
Install
From hex (precompiled binaries, no Rust toolchain required):
def deps do
[{:ex_srp_phat, "~> 0.1.0"}]
endOr pin the git dependency directly:
{:ex_srp_phat, github: "cortfritz/ex_srp_phat", tag: "v0.1.0"}Precompiled binaries vs. source builds
Releases ship precompiled NIF binaries for common platforms; rustler_precompiled
downloads and checksum-verifies the right one at compile time, so consumers do not need
a Rust toolchain. To force a source build (needs a Rust toolchain):
EX_SRP_PHAT_BUILD=1 mix deps.compile ex_srp_phat --force
The :dev and :test environments of this library always build from source.
Development
# Rust
cd native/srp_phat && cargo test && cargo clippy -- -D warnings && cargo fmt --check
# Elixir
EX_SRP_PHAT_BUILD=1 mix compile --warnings-as-errors && mix format --check-formatted && mix test
v0.1 scope
Single-frame localization of one or more sources, confidence, position covariance, and
dominant frequency. Deferred: Doppler / velocity (velocity_mps, radial_velocity return
nil).
License
MIT — see LICENSE.