> Cinder 0.1.0 documentation: cinder.audio API
> Canonical page: https://cinder.ash10.dev/docs/api/audio/ · Source: `docs/api/audio.md` in the engine repository (engine commit 47c9352, imported 2026-11-09)
> Index of every page for agents: https://cinder.ash10.dev/llms.txt

# `cinder.audio`


## `cinder.audio`

Source: `cinder/audio/__init__.mojo`

Audio over miniaudio: buses, decoded and streamed sounds, one-shots, 3D spatialisation, a listener, and the ECS bridge that follows entities and the camera.


## `cinder.audio.engine`

Source: `cinder/audio/engine.mojo`

Audio over miniaudio: buses, decoded and streamed sounds, one-shots, 3D spatialisation and a listener. The engine can run headless (no device) for tests and servers; frames are then pulled with `read_frames`. Sounds live in the shim (miniaudio structs are opaque to Mojo) and are tracked here with generational handles. Release sounds with `release` and destroy the buses you created with `destroy_bus`; `live_sounds()` and `live_buses()` must be zero before the engine is destroyed (the destructor still frees whatever is left). Sound-end and log callbacks post to the shim queue and are drained by `update`.

### struct `SoundHandle`

```mojo
struct SoundHandle(Equatable, ImplicitlyCopyable, Movable)
```

Generational handle to a sound owned by `AudioEngine`: a slot `index` and that slot's `generation`. It goes stale once the sound is released (one-shots are released by `AudioEngine.update` without any call of yours); `AudioEngine.is_valid` tells, and every method that takes a stale handle raises.

Fields:

- `index`: Int
- `generation`: Int

- **`null`** `null() -> SoundHandle`  
  The invalid handle (index -1); it converts to False.

### struct `Attenuation`

```mojo
struct Attenuation(Equatable, ImplicitlyCopyable, Movable)
```

Distance attenuation model of a spatial sound, mapped to miniaudio's models by `AudioEngine.set_attenuation`.

Fields:

- `value`: UInt8

- **`none`** `none() -> Attenuation`  
  No distance attenuation.
- **`inverse`** `inverse() -> Attenuation`  
  miniaudio's inverse-distance model; the `SoundDesc` default.
- **`linear`** `linear() -> Attenuation`  
  miniaudio's linear model.
- **`exponential`** `exponential() -> Attenuation`  
  miniaudio's exponential model.

### struct `SoundDesc`

```mojo
struct SoundDesc(Copyable, Movable)
```

How `AudioEngine.load` creates a sound. The constructor takes the file path, bus index, `streamed`, `looping` and `spatial`; the other fields keep their defaults until set on the value: volume 1, pitch 1, inverse attenuation with `min_distance` 1 m, `max_distance` 100 m and rolloff 1, not a one-shot. The attenuation settings apply only when `spatial` is set.

Fields:

- `path`: String
- `bus`: Int
- `streamed`: Bool — Decode while playing (music); otherwise the whole file is decoded up front.
- `looping`: Bool
- `spatial`: Bool
- `volume`: Float32
- `pitch`: Float32
- `attenuation`: Attenuation
- `min_distance`: Float32
- `max_distance`: Float32
- `rolloff`: Float32
- `one_shot`: Bool — Released automatically when it ends.

- **`__init__`** `__init__( out self, path: String, bus: Int = 0, streamed: Bool = False, looping: Bool = False, spatial: Bool = True, )`

### struct `AudioEngine`

```mojo
struct AudioEngine(Movable)
```

Owns the miniaudio engine, its mixing buses and every loaded sound. Bus 0 is the master bus and lives as long as the engine; buses made with `create_bus` are destroyed with `destroy_bus`. Sounds are addressed by `SoundHandle` and freed with `release` (one-shots by `update`); the destructor frees whatever is left, but `live_sounds()` and `live_buses()` should be zero first. The `NativePtr` library must outlive the engine. Call every method from the main thread: miniaudio's threads only post events, which `update` drains. The constructor raises when the event queue or the miniaudio engine cannot be created (for example no audio device without `headless`).

Fields:

- `lib`: NativePtr
- `queue`: Optional[Pointer[CnEventQueue, MutUntrackedOrigin]]
- `engine`: Optional[Pointer[ma_engine, MutUntrackedOrigin]]
- `buses`: List[Optional[Pointer[ma_sound_group, MutUntrackedOrigin]]] — One entry per bus index ever created; a destroyed bus leaves an empty entry so the other indices stay stable (indices are not reused).
- `bus_names`: List[String]
- `bus_parents`: List[Int] — Parent bus index of each bus (-1 for the master bus on the engine endpoint).
- `slots`: List[_Slot]
- `free_slots`: List[Int]
- `diagnostics`: List[LogRecord]
- `ended`: List[SoundHandle] — Sounds that finished since the last `take_ended`.
- `headless`: Bool
- `sample_rate`: Int
- `channels`: Int

- **`__init__`** `__init__( out self, lib: NativePtr, headless: Bool = False, sample_rate: Int = 48000, channels: Int = 2, ) raises`
- **`create_bus`** `create_bus(mut self, name: String, parent: Int = 0) raises -> Int`  
  A mixing bus under `parent` (0 = master, -1 = the engine endpoint). Returns its index; destroy it with `destroy_bus`. Raises when `parent` does not exist (or was destroyed) or miniaudio fails to create the group.
- **`destroy_bus`** `destroy_bus(mut self, bus: Int) raises`  
  Destroys a bus made with `create_bus`; its index stays invalid (indices are not reused) and `live_buses()` drops by one. Raises for the master bus (0, owned by the engine), a bus that does not exist or was already destroyed, and a bus that still has sounds or live child buses routed into it: release or destroy those first.
- **`find_bus`** `find_bus(self, name: String) -> Int`  
  Index of the first live bus called `name` (the master bus is 0), or -1 when there is none.
- **`set_bus_volume`** `set_bus_volume(self, bus: Int, volume: Float32) raises`  
  Sets the linear volume of bus `bus` (1 is unity), which scales every sound and bus routed through it. Raises when the bus does not exist.
- **`bus_volume`** `bus_volume(self, bus: Int) raises -> Float32`  
  Linear volume of bus `bus`. Raises when the bus does not exist.
- **`set_master_volume`** `set_master_volume(self, volume: Float32)`  
  Sets the engine's own output volume (linear), separate from the master bus volume; miniaudio's result code is ignored.
- **`live_buses`** `live_buses(self) -> Int`  
  Buses created with `create_bus` and not yet destroyed with `destroy_bus` (the master bus is the engine's own); should be zero before the engine is destroyed.
- **`load`** `load(mut self, desc: SoundDesc) raises -> SoundHandle`  
  Creates a sound from a file (WAV, FLAC, MP3, Vorbis). It starts stopped.
- **`play_one_shot`** `play_one_shot( mut self, var desc: SoundDesc, position: Vec3 ) raises -> SoundHandle`  
  Loads, positions and starts a sound that is released when it ends.
- **`release`** `release(mut self, handle: SoundHandle) raises`  
  Destroys the sound and invalidates `handle` and its copies; the slot is reused with a new generation. Raises on a stale handle, so do not release a one-shot that `update` already released.
- **`is_valid`** `is_valid(self, handle: SoundHandle) -> Bool`  
  Whether `handle` refers to a live sound (in range, same generation, not released). Never raises.
- **`live_sounds`** `live_sounds(self) -> Int`  
  Number of sounds not yet released, one-shots included; should be zero before the engine is destroyed.
- **`play`** `play(self, handle: SoundHandle) raises`  
  Starts the sound. Raises on a stale handle or when miniaudio fails to start it.
- **`stop`** `stop(self, handle: SoundHandle) raises`  
  Stops the sound without releasing it. Raises on a stale handle; miniaudio's result code is ignored.
- **`is_playing`** `is_playing(self, handle: SoundHandle) raises -> Bool`  
  Whether the sound is playing. Raises on a stale handle.
- **`at_end`** `at_end(self, handle: SoundHandle) raises -> Bool`  
  Whether the sound has reached the end of its data. Raises on a stale handle.
- **`seek`** `seek(self, handle: SoundHandle, frame: Int) raises`  
  Moves the playback cursor to PCM frame `frame`. Raises on a stale handle; miniaudio's result code is ignored.
- **`set_volume`** `set_volume(self, handle: SoundHandle, volume: Float32) raises`  
  Sets the sound's linear volume (1 is unity). Raises on a stale handle.
- **`set_pitch`** `set_pitch(self, handle: SoundHandle, pitch: Float32) raises`  
  Sets the sound's pitch multiplier (1 is the original pitch). Raises on a stale handle.
- **`set_pan`** `set_pan(self, handle: SoundHandle, pan: Float32) raises`  
  Sets the sound's stereo pan (-1 left, 0 centre, 1 right); the only positioning control of a non-spatial sound. Raises on a stale handle.
- **`set_looping`** `set_looping(self, handle: SoundHandle, looping: Bool) raises`  
  Turns looping on or off. Raises on a stale handle.
- **`fade`** `fade( self, handle: SoundHandle, from_volume: Float32, to_volume: Float32, milliseconds: Int, ) raises`  
  Fades the sound's volume from `from_volume` to `to_volume` over `milliseconds`. Raises on a stale handle.
- **`set_position`** `set_position(self, handle: SoundHandle, p: Vec3) raises`  
  Sets the sound's world position in metres (used while spatialisation is on). Raises on a stale handle.
- **`set_velocity`** `set_velocity(self, handle: SoundHandle, v: Vec3) raises`  
  Sets the sound's velocity in metres per second, used for the Doppler effect. Raises on a stale handle.
- **`set_spatial`** `set_spatial(self, handle: SoundHandle, enabled: Bool) raises`  
  Turns 3D spatialisation of the sound on or off. Raises on a stale handle.
- **`set_attenuation`** `set_attenuation( self, handle: SoundHandle, model: Attenuation, min_distance: Float32, max_distance: Float32, rolloff: Float32, ) raises`  
  Sets the distance attenuation: the `model`, `min_distance` and `max_distance` in metres and the `rolloff` factor. An unrecognised model value falls back to inverse. Raises on a stale handle.
- **`set_listener`** `set_listener( self, position: Vec3, forward: Vec3, up: Vec3 = Vec3.unit_y(), velocity: Vec3 = Vec3.zero(), )`  
  Places miniaudio's listener 0: `position` in metres, the `forward` direction it faces, the world `up` and its `velocity` in metres per second (Doppler). The engine convention is -Z forward and +Y up; `sync_audio` calls this from the `AudioListener` entity.
- **`update`** `update(mut self) raises -> Int`  
  Drains the shim queue: log messages become diagnostics, finished sounds are recorded (and one-shots released). Returns the number of sounds that ended.
- **`take_ended`** `take_ended(mut self) -> List[SoundHandle]`  
  Returns the handles of sounds that ended since the last call and clears the list. Handles of one-shots are already stale when they are returned.
- **`error_count`** `error_count(self) -> Int`  
  Number of error-level records in `diagnostics` (miniaudio log messages collected by `update`).
- **`read_frames`** `read_frames(self, frames: Int) raises -> List[Float32]`  
  Headless engines: mixes `frames` interleaved float frames (tests, offline rendering).


## `cinder.audio.sync`

Source: `cinder/audio/sync.mojo`

ECS <-> audio: `AudioSource` entities position their sounds, `AudioListener` places the listener (attach it to the camera entity so the listener follows the camera).

- `comptime AUDIO_SOURCE_COMPONENT_ID = 10` — ECS component id of `AudioSource`.
- `comptime AUDIO_LISTENER_COMPONENT_ID = 11` — ECS component id of `AudioListener`.
### struct `AudioSource`

```mojo
struct AudioSource(Component)
```

A spatial sound that follows its entity. `velocity` feeds the Doppler effect.

Fields:

- `sound`: SoundHandle
- `velocity`: Vec3

- **`component_id`** `component_id() -> Int`

### struct `AudioListener`

```mojo
struct AudioListener(Component)
```

Marks the entity whose world transform drives the listener (position, -Z forward, +Y up). With several, the first one found wins.

Fields:

- `velocity`: Vec3

- **`component_id`** `component_id() -> Int`

### def `sync_audio`

```mojo
def sync_audio(world: World, audio: AudioEngine) raises -> Int
```

Pushes entity transforms to the sounds and the listener. Returns the number of sources positioned; stale handles are skipped.
