Impulse response & auralization
Clap-based T60 (decay_time) is fine for incidental
impulses, but a deliberate measurement deserves a deliberate excitation. If the only
recording of a room is a camera's, see the substitution warning in the
clap-based guide: T60 survives it, the early field does not.
The impulse module implements Farina's exponential sine sweep (ESS)
method: tens of decibels more dynamic range than any clap, and the
loudspeaker's harmonic distortion is pushed ahead of the linear
response, where it can simply be trimmed off. The resulting impulse
response (IR) is both a measurement (T60, clarity, STI) and an instrument:
convolve any dry sound with it and it plays in that room.
1. Generate a sweep
ambiscape sweep --duration 10 --f0 40 --f1 18000 -o sweep.wav
(--fs sets the sample rate, default 48000; --amplitude the peak,
default 0.5.) The command writes three files:
sweep.wav— the exponential sweep, equal time per octave, with raised-cosine fades (100 ms in, 20 ms out) so the loudspeaker is not stepped, at peak −6 dBFS (--amplitude 0.5). The headroom is deliberate: playback chains have bass boosts and resonances, and a sweep that clips anywhere in the chain injects distortion the method can no longer separate out.sweep_inverse.wav— the matched inverse filter: the time-reversed sweep with a −6 dB/octave amplitude envelope, scaled so that sweep ⊛ inverse is a unit-peak impulse.sweep.json— the generation parameters. Keep it with the recording; the inverse can be regenerated bit-identically from it, so the inverse WAV never needs to travel.
Longer sweeps buy signal-to-noise (3 dB per doubling); 10 s is a good default for rooms, 20–30 s for very reverberant or noisy spaces. Play the sweep at a healthy level, record with any rig (mono, stereo, binaural, or B-format — every channel is deconvolved), and leave a tail of silence at least as long as the reverberation.
2. Deconvolve to an impulse response
ambiscape impulse recorded.wav --inverse sweep_inverse.wav
ambiscape impulse recorded.wav --params sweep.json # equivalent
ambiscape impulse recorded.wav # finds sweep.json next to the recording
The recording is convolved with the inverse filter; everything earlier
than 5 ms before the direct-sound peak (--pre-ms) is trimmed —
deconvolution pre-ringing and the harmonic-distortion images both live
there — and --dur caps the kept IR tail in seconds (default: to the
end of the recording). The IR is written as float32 ir.wav, rescaled to peak −6 dBFS
(the applied gain is logged in impulse.json; every reported metric is
level-invariant). Alongside it, impulse.json holds:
- octave-band decay (125 Hz – 8 kHz): T60, and where the dynamic range
supports the ISO 3382 fixed ranges, T20 (−5…−25 dB) and T30 (−5…−35 dB);
EDT; clarity C50/C80; definition D50; and the usable dynamic range
dr_dbbehind each fit. These reuse the toolbox's truncated-Schroeder machinery (analysis.decay_metrics) — the same noise-floor and re-attack safeguards as the clap path, here fed a proper excitation. On a trimmed IR, one with no recording before its peak,dr_dbis measured from the quietest part of the decay rather than from the silenceir_metricsprepends to let the estimator run (fixed in 0.28.1; before that it read the padding and reported around 190 dB). -
STI (IEC 60268-16, indirect method): modulation transfer functions from Schroeder's integral of the squared IR at the 14 standard modulation frequencies, male-speech weights.
STI assumptions
The indirect method assumes the measurement chain is noise-free and the room linear and time-invariant. No ambient-noise or masking correction is applied, so the value is an upper bound describing reverberant smearing only — an occupied or noisy room will have a lower effective STI. Treat it as a room descriptor, not an occupancy-condition prediction.
-
IACC (early) for two-channel IRs: the maximum of the modulus of the interaural cross-correlation over 0–80 ms after the direct sound, lags ±1 ms, broadband. Taking the modulus is ISO 3382-1's definition, not a deviation from it. Meaningful for binaural (ear-signal) IRs; for an ordinary spaced stereo pair it is a channel-similarity figure, not a perceptual one. Multichannel decay/STI metrics are computed on channel 0 (the omni/W channel of a B-format IR).
-
IACC_E3 (
iacc_e3, since 0.28.0): the same quantity per octave band, plus the mean of the 500, 1000 and 2000 Hz bands.Use this one against the literature
Published concert-hall values are IACC_E3, not broadband. The two are different quantities — low-frequency content moves the broadband figure — so comparing
iacc_earlyagainst hall values compares different things.The result also carries
iacc_signed: the signed correlation at the same lag as the reported IACC. The modulus discards exactly one case, ears receiving anti-phase sound, which otherwise read as strongly correlated. The CLI mentions it only aboveIACC_SIGN_FLOOR(0.5), because the sign of a near-zero peak is noise — decorrelated ears would otherwise be flagged anti-phase on every band. A diagnostic, not an ISO quantity.
3. Auralize
ambiscape auralize dry.wav --ir ir.wav -o wet.wav
convolves dry (ideally anechoic) material with the measured IR using
uniformly partitioned FFT convolution — constant memory however long the
IR, exact linear convolution (impulse.partitioned_convolve is tested
against scipy.signal.oaconvolve to machine precision).
- Sample rates: a rate-mismatched IR is resampled (polyphase) to the dry material's rate; the dry audio is never resampled.
- Channels: equal counts convolve pairwise; mono dry material fans out through each IR channel (one source, N-channel room); a mono IR applies to each dry channel; any other mismatch mono-sums the dry input first.
- Normalization: raw convolution gain is arbitrary (it scales with
the IR's level), so by default the wet result is rescaled so its peak
equals the dry input's peak — clip-safe iff the input was, and A/B
comparisons sit at comparable levels.
--no-normalizekeeps the raw convolution; the applied make-up gain is printed either way.
Python API
from ambiscape import impulse
sweep, inverse, meta = impulse.exp_sweep(duration=10)
h = impulse.deconvolve(recording, inverse) # (n, ch)
ir, direct = impulse.extract_ir(h, fs)
impulse.ir_metrics(ir, fs) # {"500": {"T60": ...}, ...}
impulse.sti(ir, fs) # {"sti": ..., "mti": {...}}
impulse.iacc_early(ir, fs) # broadband, stereo/binaural
impulse.iacc_e3(ir, fs) # {"iacc_e3", "iacc", ...}
wet, gain_db = impulse.auralize(dry, fs, ir, fs_ir)
or impulse.measure(recording, params="sweep.json") for the whole
deconvolve-trim-save-analyse pass the CLI runs.