How to use the model

BayesianListener simulates a listener behavior in a sound localisation task. This class is a stateful object that holds three things: a template, a target, and noise parameters.

  • The template is the listener’s internal model of the acoustic world represented as auditory features extracted from their own HRTF and interpolated onto a quasi-uniform spherical grid. It is expensive to compute and rarely changes.

  • The target contains the auditory features of a single or multiple binaural stimuli to be localised. It can be swapped cheaply to change the sound source or test a non-individual HRTF.

  • The noise parameters (e.g. sigma_spectral, sigma_prior) control perceputal and behavioral uncertainties and can be updated between calls.

localise is the one-call shortcut. This design lets you swap just the target without recomputing everything.

If you want more control on the model internal workings, then infer always operates on whatever template and target are currently stored.

To give an idea of what this model can do, this guide shows four typical usage patterns. Each builds on the same BayesianListener API; the differences are in which features are computed and how target and template are combined before calling infer.

Workflow

When to use

Individual

Simulate responses for a listener’s localising with their own HRTF.

Non-individual HRTF

Simulate localisation with a non-individual HRTF.

Dynamic (loop over targets)

Track moving sound source over time.

Setting uncertainty parameters

Update noise parameters.

Individual

The simplest case: one listener, one HRTF. localise handles feature extraction, template interpolation, Bayesian inference, and motor noise in a single call.

    import pyfar as pf

    bl = BayesianListener(sofa_path)
    frontal = pf.Coordinates.from_spherical_elevation(0, 0, 1)
    estimates = bl.localise(directions=frontal, repetitions=1)
    lateral_deg = np.rad2deg(estimates.lateral.squeeze())
    polar_deg = np.rad2deg(estimates.polar.squeeze())
    print(f"\nFrontal estimate — lateral: {lateral_deg:.1f}°, polar: {polar_deg:.1f}°")

Results are cached on disk after the first call, so subsequent calls to compute_template return immediately.

Non-individual HRTF

Simulate how a listener performs when fitted with a non-individual HRTF. The individual listener’s template is retained; only the target (the stimulus representation) is replaced with the foreign HRTF.

    import pyfar as pf
    from bayesian_listener.metrics import angular_error

    frontal = pf.Coordinates.from_spherical_elevation(0, 0, 1)

    # Individual condition: P0001 template with P0001 target
    repetitions = 50

    bl_ind = BayesianListener(sofa_path)
    est_ind = bl_ind.localise(directions=frontal, repetitions=repetitions, seed=0)

    # Non-individual condition: P0001 template with P0002 target
    foreign = BayesianListener(sofa_path_non_individual)
    foreign.compute_target()
    bl_non = BayesianListener(sofa_path)
    est_non = bl_non.localise(target=foreign.target,
                               directions=frontal, repetitions=repetitions, seed=0)

    err_ind = np.rad2deg(angular_error(frontal.cartesian, est_ind.cartesian.squeeze())[0])
    err_non = np.rad2deg(angular_error(frontal.cartesian, est_non.cartesian.squeeze())[0])

    print(f"\nFrontal great-circle error — individual: {err_ind:.1f}°, "
          f"non-individual: {err_non:.1f}°, "
          f"difference: {err_non - err_ind:.1f}°")

localise passes target to infer, which compares the foreign HRTF features against the individual template grid, modelling the template-mismatch scenario.

Dynamic (loop over targets)

When comparing multiple stimulus conditions against the same template — e.g. different HRTFs or source sets — compute the template once and swap only the target inside the loop. Each target already contains features for all source directions in its SOFA file, so a single swap covers the full set of stimuli for that condition. This pattern is the basis of the FrAMBI framework [barumerli2025], which uses repeated inference over a sequence of conditions to model dynamic auditory tasks. [llado2024] applied this approach to predict how headphone HRTFs affect the time to localise a target in an auditory-guided visual search task.

A moving source can be modelled by feeding the posterior of each step as the prior for the next. The template stays fixed; only the single-direction target changes at each step. With one repetition and store_posterior=True, infer returns an array of shape (1, 1, n_templates) — squeezed to (n_templates,) it is a valid prior for the following call.

    import numpy as np

    bl = BayesianListener(sofa_path)
    bl.compute_template()
    bl.compute_target()

    # Select directions on the horizontal plane, azimuth 0–90°
    sph = bl.target.coords.spherical_elevation
    azi_deg = np.rad2deg(sph[:, 0])
    ele_deg = np.rad2deg(sph[:, 1])
    mask = (ele_deg == 0) & (azi_deg >= 0) & (azi_deg <= 90)

    # Build trajectory: one single-direction target per step, sorted by azimuth
    trajectory_indices = np.where(mask)[0][np.argsort(azi_deg[mask])]
    trajectory = [bl.target[i] for i in trajectory_indices]
    actual_azimuths = azi_deg[trajectory_indices]

    prior = "horizontal"  # initialise with the default elevation prior
    estimates = []
    for step_target in trajectory:
        bl.target = step_target
        posterior = bl.infer(repetitions=1, prior=prior, store_posterior=True)
        estimates.append(bl.estimate(posterior))
        # squeeze (1, 1, n_templates) → (n_templates,) and convert to linear domain
        prior = np.exp(posterior.squeeze())

    print("\nTrajectory (actual → estimated azimuth):")
    for actual, est in zip(actual_azimuths, estimates):
        est_azi = np.rad2deg(est.spherical_elevation.squeeze()[0])
        print(f"  {actual:5.1f}° → {est_azi:5.1f}°")

Setting uncertainty parameters

The default parameter values are group averages from Barumerli et al. (2023) and are a reasonable starting point, but individual listeners differ substantially in their perceptual uncertainties. For accurate per-listener simulation, set parameters explicitly before calling localise or infer.

Each parameter has a specific physical role in the static localisation task:

Parameter

Default

What it controls

sigma_spectral

10.4 dB

Reliability of monaural spectral cues (elevation, front/back). Higher values flatten the elevation response and increase front-back reversals.

sigma_prior

69.0 deg

Width of the elevation prior. Lower values pull responses towards the horizontal plane regardless of the stimulus.

kappa_motor

23.31

Concentration of the motor-noise distribution. Lower values produce more scattered pointing responses independent of sensory processing.

sigma_itd

0.569

ITD perceptual noise (lateral localisation). Fixed at the literature value in most analyses — poorly identifiable from spatial data alone.

sigma_ild

1.0 dB

ILD perceptual noise (lateral localisation). Fixed at the literature value in most analyses.

Note

sigma_itd and sigma_ild cannot be separated from kappa_motor along the lateral dimension and should be left at their defaults unless you have strong prior justification. See Noise parameters for the identifiability analysis.

All parameters can be set at construction time or updated in place:

    import pyfar as pf
    from bayesian_listener.metrics import localization_error

    frontal = pf.Coordinates.from_spherical_elevation(0, 0, 1)

    # Normal parameters with non-individual target
    bl = BayesianListener(sofa_path,
                          sigma_spectral=8.2,
                          sigma_prior=45.0,
                          kappa_motor=30.0)
    est_normal = bl.localise(directions=frontal, repetitions=50, seed=0)

    # Impaired hearing (higher spectral uncertainty)
    bl.sigma_spectral = 15.0
    est_impaired = bl.localise(directions=frontal, repetitions=50, seed=0)

    err_normal = localization_error(
        frontal, est_normal, metric='angular_error', degrees=True)
    err_impaired = localization_error(
        frontal, est_impaired, metric='angular_error', degrees=True)

    print(f"\nNon-individual frontal error — normal: {err_normal:.1f}°, "
          f"impaired: {err_impaired:.1f}°, "
          f"difference: {err_impaired - err_normal:.1f}°")

If you have measured behavioural responses from a listener, individual parameters can be estimated objectively via maximum likelihood optimisation rather than set by hand. See Fit the Model to Your Own HRTF for the two-stage fitting procedure.

See also

References

[barumerli2025]

R. Barumerli and P. Majdak, “FrAMBI: A Software Framework for Auditory Modeling Based on Bayesian Inference,” Neuroinformatics, vol. 23, no. 2, 2025. https://doi.org/10.1007/s12021-024-09702-5

[llado2024]

P. Lladó, R. Barumerli, R. Baumgartner, and P. Majdak, “Predicting the effect of headphones on the time to localize a target in an auditory-guided visual search task,” Frontiers in Virtual Reality, vol. 5, 2024. https://doi.org/10.3389/frvir.2024.1359987