Skip to content

Audio

Plays an audio clip from path. Supports both global stereo (positional: false) and 3D positional audio with the configured distance model. Runtime fields (object, audio, isPlaying, isPaused) are recreated when the project loads and are never persisted to saved snapshots.

Managed by: AudioSystem

Editor inspector

Audio inspector

Properties

PropertyTypeDescription
pathstringSource URL or localasset:// path of the audio file.
optionstypeof DEFAULT_AUDIO_OPTIONSPlayback options.
objectTHREE.Object3D | nullLive three.js object hosting the audio (a parented Audio/PositionalAudio). null until built.
audioTHREE.Audio<GainNode> | THREE.PositionalAudio | nullLive three.js audio source. null until built. Recreated when the project loads.
isPlayingbooleanWhether the audio should be playing. Managed by AudioSystem in preview. Non-enumerable — not persisted to snapshots or save data. Set via play / stop.
isPausedbooleanWhether the audio is paused mid-playback (position is preserved). Non-enumerable — not persisted to snapshots or save data. Set via pause / play / stop.

API reference

Scripting examples

Component access patterns

Use this.getComponent(X) to access a component on the entity this script is attached to. Use this.world.getEntityByName to find another entity by name, then this.world.getComponent to read its component:

typescript
// Component on the entity this script is on
const audio = this.getComponent(AudioComponent);

// Component on another entity, found by its scene name
const other = this.world.getEntityByName("AmbientSpeaker");
if (other) {
  const audio = this.world.getComponent(other.entityId, AudioComponent);
}

Play / stop on key press

Toggle audio playback when the player presses Space. Volume is exposed as an inspector field.

typescript
import { Behaviour, AudioComponent, KeyboardKeys } from "@relu-interactives/spatial-ecs";
import type { FloatInput } from "@relu-interactives/spatial-ecs";

export default class AudioController extends Behaviour {
  data = {
    volume: 1 as FloatInput,
  };

  protected init() {
    const audio = this.getComponent(AudioComponent);
    if (audio) audio.options.volume = this.data.volume;
  }

  protected onUpdate() {
    const audio = this.getComponent(AudioComponent);
    if (!audio) return;

    // Keep volume in sync with the inspector slider
    audio.options.volume = this.data.volume;

    if (this.world.getKeyDown(KeyboardKeys.Space)) {
      if (audio.isPlaying) {
        audio.stop();
      } else {
        audio.play();
      }
    }
  }
}

Proximity-triggered audio

Start playing when the camera comes within a configurable range, and stop when it moves away.

typescript
import { Behaviour, AudioComponent, TransformComponent, CameraComponent } from "@relu-interactives/spatial-ecs";
import type { FloatInput } from "@relu-interactives/spatial-ecs";

export default class ProximityAudio extends Behaviour {
  data = {
    triggerDistance: 3 as FloatInput,
  };

  protected onUpdate() {
    const audio = this.getComponent(AudioComponent);
    if (!audio) return;

    // Find the default camera entity
    let cameraPos = { x: 0, y: 0, z: 0 };
    for (const [id, cam] of this.world.query([CameraComponent])) {
      if (cam.isDefault) {
        const ct = this.world.getComponent(id, TransformComponent);
        if (ct) cameraPos = ct.position;
        break;
      }
    }

    const dx = this.transform.position.x - cameraPos.x;
    const dy = this.transform.position.y - cameraPos.y;
    const dz = this.transform.position.z - cameraPos.z;
    const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);

    if (dist < this.data.triggerDistance && !audio.isPlaying) {
      audio.play();
    } else if (dist >= this.data.triggerDistance && audio.isPlaying) {
      audio.stop();
    }
  }
}

Playback methods

Use audio.play(), audio.pause(), and audio.stop() rather than calling methods on audio.audio directly. These helpers update isPlaying / isPaused so the AudioSystem applies the correct THREE.js call in preview.

Via ComponentInput in the inspector

You can also pick this component from the inspector using a ComponentInput field. Assign any entity in the inspector and the field resolves to the live AudioComponent instance at runtime.

typescript
import {
  Behaviour,
  AudioComponent,
  type ComponentInput,
} from "@relu-interactives/spatial-ecs";

export default class Example extends Behaviour {
  data = {
    targetAudio: {
      type: "component",
      value: null,
    } as unknown as ComponentInput,
  };

  protected onUpdate() {
    const audio = this.data.targetAudio.value as AudioComponent | null;
    if (!audio) return;
    // Use the live component instance.
  }
}

Via EntityInput in the inspector

Alternatively, pick an entity from the inspector using an EntityInput field and then read the component off that entity via world.getComponent:

typescript
import {
  Behaviour,
  AudioComponent,
  type EntityInput,
  type WorldEntityView,
} from "@relu-interactives/spatial-ecs";

export default class Example extends Behaviour {
  data = {
    targetEntity: { type: "entity", value: null } as unknown as EntityInput,
  };

  protected onUpdate() {
    const entity = this.data.targetEntity.value as WorldEntityView | null;
    if (!entity) return;

    const audio = this.world.getComponent(
      entity.entityId,
      AudioComponent,
    ) as AudioComponent | null;
    if (!audio) return;
    // Use the component on the picked entity.
  }
}