1.0.0: Make a Scene!#

The Scene object#

The Scene object is the primary object we’ll be dealing with when working with AudibleLight. Our Scene object is comparable to scaper.core.Scaper or spatialscaper.core.Scaper, but with numerous adjustments that make data generation more straightforward and scalable.

A basic Scene object can be initialised as follows:

[1]:
from audiblelight import utils
from audiblelight.core import Scene
[3]:
scene = Scene(
    duration=60,
    sample_rate=44100,
    backend="rlr",
    backend_kwargs=dict(
        mesh=utils.get_project_root() / "tests/test_resources/meshes/Oyens.glb"
    )
)
CreateContext: Context created

Scene.__init__ takes many optional arguments, which are described in more detail within the documentation.

Using distributions#

When a Scene is initialised, various distributions can be passed to allow for randomly sampling parameters such as event start times and durations.

These durations must satisfy the following conditions:

  • Must be callable without arguments

  • OR define an rvs method that is callable without arguments

  • Must return a floating point value when called

This means that (for example), we can use scipy distributions, custom functions, etc.

[ ]:
import numpy as np
import scipy.stats as stats


def truncated_gaussian():
    return np.clip(np.random.normal(5., 1.), 4, 6)


# All of these are valid distributions
scene = Scene(
    duration=60,
    sample_rate=44100,
    backend="rlr",
    backend_kwargs=dict(
        mesh=utils.get_project_root() / "tests/test_resources/meshes/Oyens.glb"
    ),
    scene_start_dist=truncated_gaussian,
    event_start_dist=lambda: np.random.uniform(0.0, 10.0),
    event_velocity_dist=stats.uniform(10, 10)
)

When an Event is added with Scene.add_event, the following logic is used to decide whether the distributions passed to Scene.__init__ should be sampled from:

  • If overrides are passed directly to add_event, these will always be used

  • If overrides are not passed but a valid distribution has been, this will be sampled

  • If neither overrides nor a distribution has been passed, the value will be sampled from a sensible default distribution.

Passing audio directories#

We can pass fg_path to Scene.__init__. This allows us to define a directory (or list of directories) containing foreground audio. When we add an event with Scene.add_event without also specifying filepath=..., we’ll pull in from this directory.

[6]:
scene = Scene(
    duration=60,
    sample_rate=44100,
    backend="rlr",
    backend_kwargs=dict(
        mesh=utils.get_project_root() / "tests/test_resources/meshes/Oyens.glb"
    ),
    fg_path=utils.get_project_root() / "tests/test_resources/soundevents/music"
)
scene.add_event(event_type="static", alias="will_be_music")
music_event = scene.get_event("will_be_music")
print(music_event.filepath)
2025-10-30 14:48:32.400 | WARNING  | audiblelight.worldstate:load_mesh_navigation_waypoints:1878 - Cannot find waypoints for mesh Oyens inside default location (/home/huw-cheston/Documents/python_projects/AudibleLight/resources/waypoints/gibson). No navigation waypoints will be loaded.
CreateContext: Context created
Warning: initializing context twice. Will destroy old context and create a new one.
2025-10-30 14:48:34.420 | INFO     | audiblelight.core:add_event:961 - Event added successfully: Static 'Event' with alias 'will_be_music', audio file '/home/huw-cheston/Documents/python_projects/AudibleLight/tests/test_resources/soundevents/music/001666.mp3' (unloaded, 0 augmentations), 1 emitter(s).
CreateContext: Context created
/home/huw-cheston/Documents/python_projects/AudibleLight/tests/test_resources/soundevents/music/001666.mp3

In this example, we pass in a directory containing music objects. When we add an Event to the Scene, we’ll draw from this dictionary.

Controlling duplicate audio files#

By default, we allow a single unique audio file to appear numerous times in a Scene. In practice, this is usually not a problem as we would expect fg_dir to contain many audio files, and therefore duplicates (especially overlapping duplicates) are in reality very rare.

If this behaviour is undesirable, the argument allow_duplicate_audios=False can be passed when initialising a Scene:

[10]:
no_dupes_allowed = Scene(
    duration=60,
    sample_rate=44100,
    backend="rlr",
    backend_kwargs=dict(
        mesh=utils.get_project_root() / "tests/test_resources/meshes/Oyens.glb"
    ),
    fg_path=utils.get_project_root() / "tests/test_resources/soundevents/music",
    allow_duplicate_audios=False
)

# Add in some music files
for _ in range(2):
    no_dupes_allowed.add_event(event_type="static")

# Print the filepaths
events = no_dupes_allowed.get_events()
for ev in events:
    print(ev.filename)
CreateContext: Context created
Warning: initializing context twice. Will destroy old context and create a new one.
2025-10-07 15:27:57.924 | INFO     | audiblelight.core:add_event:830 - Event added successfully: Static 'Event' with alias 'event000', audio file '/home/huw-cheston/Documents/python_projects/AudibleLight/tests/test_resources/soundevents/music/007527.mp3' (unloaded, 0 augmentations), 1 emitter(s).
CreateContext: Context created
Warning: initializing context twice. Will destroy old context and create a new one.
2025-10-07 15:27:58.251 | INFO     | audiblelight.core:add_event:830 - Event added successfully: Static 'Event' with alias 'event001', audio file '/home/huw-cheston/Documents/python_projects/AudibleLight/tests/test_resources/soundevents/music/001666.mp3' (unloaded, 0 augmentations), 1 emitter(s).
CreateContext: Context created
007527.mp3
001666.mp3

Serialising Scene objects#

Scene objects can be serialised to a Python dictionary or JSON object using the to_dict method.

This makes it easy to inspect the object and its parameters:

[8]:
out_dict = scene.to_dict()
out_dict
[8]:
{'audiblelight_version': '0.1.0',
 'rlr_audio_propagation_version': '0.0.1',
 'creation_time': '2025-10-30_14:48:49',
 'duration': 60.0,
 'backend': 'RLR',
 'sample_rate': 44100,
 'ref_db': -65,
 'max_overlap': 2,
 'fg_path': ['/home/huw-cheston/Documents/python_projects/AudibleLight/tests/test_resources/soundevents/music'],
 'bg_path': [],
 'ambience': {},
 'events': {'will_be_music': {'alias': 'will_be_music',
   'filename': '001666.mp3',
   'filepath': '/home/huw-cheston/Documents/python_projects/AudibleLight/tests/test_resources/soundevents/music/001666.mp3',
   'class_id': 8,
   'class_label': 'music',
   'is_moving': False,
   'scene_start': 3.6278842385833543,
   'scene_end': 33.60446020230218,
   'event_start': 0.0,
   'event_end': 29.976575963718822,
   'duration': 29.976575963718822,
   'snr': np.float64(21.866501465167776),
   'sample_rate': 44100.0,
   'spatial_resolution': None,
   'spatial_velocity': None,
   'shape': 'static',
   'num_emitters': 1,
   'emitters': [[3.9456078535606425, -0.2135563826678748, 0.5030086457338951]],
   'emitters_relative': {},
   'augmentations': []}},
 'state': {'backend': 'RLR',
  'sample_rate': 44100,
  'emitters': {'will_be_music': [[3.9456078535606425,
     -0.2135563826678748,
     0.5030086457338951]]},
  'microphones': {},
  'mesh': {'fname': 'Oyens',
   'ftype': '.glb',
   'fpath': '/home/huw-cheston/Documents/python_projects/AudibleLight/tests/test_resources/meshes/Oyens.glb',
   'units': 'meters',
   'from_gltf_primitive': False,
   'name': 'defaultobject',
   'node': 'defaultobject',
   'bounds': [[-3.0433080196380615, -10.448445320129395, -1.1850370168685913],
    [5.973234176635742, 2.101027011871338, 2.4577369689941406]],
   'centroid': [1.527919030159762, -4.550817438070386, 1.162934397641578]},
  'rlr_config': {'diffraction': 1,
   'direct': 1,
   'direct_ray_count': 500,
   'direct_sh_order': 3,
   'frequency_bands': 4,
   'global_volume': 1.0,
   'hrtf_back': [0.0, 0.0, 1.0],
   'hrtf_right': [1.0, 0.0, 0.0],
   'hrtf_up': [0.0, 1.0, 0.0],
   'indirect': 1,
   'indirect_ray_count': 5000,
   'indirect_ray_depth': 200,
   'indirect_sh_order': 1,
   'max_diffraction_order': 10,
   'max_ir_length': 4.0,
   'mesh_simplification': 0,
   'sample_rate': 44100.0,
   'size': 146,
   'source_ray_count': 200,
   'source_ray_depth': 10,
   'temporal_coherence': 0,
   'thread_count': 1,
   'transmission': 1,
   'unit_scale': 1.0},
  'empty_space_around_mic': 0.1,
  'empty_space_around_emitter': 0.2,
  'empty_space_around_surface': 0.2,
  'empty_space_around_capsule': 0.05,
  'repair_threshold': None,
  'material': 'Default'}}

Serialising to a dictionary of JSON also makes it easy to load the object back up again. To do this, we can instantiate the class using the from_dict class method:

[9]:
recreated = Scene.from_dict(out_dict)
# Alternatively, `Scene.from_json(...)` to load from a JSON object on the disk
2025-10-30 14:48:51.377 | WARNING  | audiblelight.core:from_dict:1653 - Currently, distributions cannot be loaded with `Scene.from_dict`. You will need to manually redefine these using, for instance, setattr(scene, 'event_start_dist', ...), repeating this for every distribution.
2025-10-30 14:48:51.466 | WARNING  | audiblelight.worldstate:load_mesh_navigation_waypoints:1878 - Cannot find waypoints for mesh Oyens inside default location (/home/huw-cheston/Documents/python_projects/AudibleLight/resources/waypoints/gibson). No navigation waypoints will be loaded.
CreateContext: Context created
Warning: initializing context twice. Will destroy old context and create a new one.
CreateContext: Context created

We can use the built in Python __eq__ method to check that our original and recreated Scene are identical:

[10]:
assert scene == recreated

Nearly every object in AudibleLight defines the to_dict and from_dict method, making it easy to load and unload objects using built-in Python datatypes.

Note that loading a higher level object (e.g., Scene) will automatically load in any lower level objects (e.g., Event, Augmentation) too. So, there’s no need to call Event.from_dict when all you want is Scene.from_dict: this will be handled automatically!

[12]:
event_as_dict = recreated.get_event(0).to_dict()
event_as_dict
[12]:
{'alias': 'will_be_music',
 'filename': '001666.mp3',
 'filepath': '/home/huw-cheston/Documents/python_projects/AudibleLight/tests/test_resources/soundevents/music/001666.mp3',
 'class_id': 8,
 'class_label': 'music',
 'is_moving': False,
 'scene_start': 3.6278842385833543,
 'scene_end': 33.60446020230218,
 'event_start': 0.0,
 'event_end': 29.976575963718822,
 'duration': 29.976575963718822,
 'snr': np.float64(21.866501465167776),
 'sample_rate': 44100.0,
 'spatial_resolution': None,
 'spatial_velocity': None,
 'shape': 'static',
 'num_emitters': 1,
 'emitters': [[3.9456078535606425, -0.2135563826678748, 0.5030086457338951]],
 'emitters_relative': {},
 'augmentations': []}

And we can, of course, check that the Event objects are equivalent…

[13]:
scene.get_event(0) == recreated.get_event(0)
[13]:
True

A note on backends#

Scene supports multiple backend types (which inherit from audiblelight.state.WorldState):

  • Ray-traced RIRs, using rlr-audio-propagation (backend="rlr")

  • Measured RIRs, reading from .sofa files in a manner similar to spatialscaper (backend="sofa")

  • Parametric (shoebox) RIRs, defined in a similar manner to pyroomacoustics

The underlying API is the same regardless of backend, however, making it easy to create complex datasets that work with different types of room impulse responses.

The examples given above all use the “rlr” backend, but the same principles apply to other backends too.

Let’s try creating a Scene with the “sofa” backend. We’ll need to pass in a SOFA file, just as we had to pass a mesh file into our RLR backend.

[5]:
sofa_scene = Scene(
    duration=60,
    sample_rate=44100,
    backend="sofa",
    fg_path=utils.get_project_root() / "tests/test_resources/soundevents/music",
    allow_duplicate_audios=True,
    backend_kwargs=dict(
        sofa=utils.get_project_root() / "tests/test_resources/daga_foa.sofa"
    ),
)

# Add in some music files
for _ in range(2):
    sofa_scene.add_event(event_type="static")

# Print the first event
sofa_events = sofa_scene.get_event(0)
sofa_events.to_dict()
2025-10-30 14:35:23.309 | INFO     | audiblelight.core:add_event:961 - Event added successfully: Static 'Event' with alias 'event000', audio file '/home/huw-cheston/Documents/python_projects/AudibleLight/tests/test_resources/soundevents/music/000010.mp3' (unloaded, 0 augmentations), 1 emitter(s).
2025-10-30 14:35:23.338 | INFO     | audiblelight.core:add_event:961 - Event added successfully: Static 'Event' with alias 'event001', audio file '/home/huw-cheston/Documents/python_projects/AudibleLight/tests/test_resources/soundevents/music/007527.mp3' (unloaded, 0 augmentations), 1 emitter(s).
[5]:
{'alias': 'event000',
 'filename': '000010.mp3',
 'filepath': '/home/huw-cheston/Documents/python_projects/AudibleLight/tests/test_resources/soundevents/music/000010.mp3',
 'class_id': 8,
 'class_label': 'music',
 'is_moving': False,
 'scene_start': 1.4424073409576228,
 'scene_end': 31.418983304676445,
 'event_start': 0.0,
 'event_end': 29.976575963718822,
 'duration': 29.976575963718822,
 'snr': np.float64(27.744524358272837),
 'sample_rate': 44100.0,
 'spatial_resolution': None,
 'spatial_velocity': None,
 'shape': 'static',
 'num_emitters': 1,
 'emitters': [[2.4995571849510796,
   -0.0004428150489204461,
   -0.0004428150489204461]],
 'emitters_relative': {'mic000': [[-0.010150371151877405,
    -0.010150370992594011,
    2.4995572633990406]]},
 'augmentations': []}