Log inGet started

audio

Updated 6 September 2026

The audio namespace — 61 functions.

globals/audio/decode

audio.decode(zaud: buffer | string) -> (string?, number?, number?)

Decode a ZAUD payload into interleaved f32 PCM. A PCM payload comes back as the frames its header accounts for, held to the whole frames the bytes behind it fill, so the sample count is always a whole number of channels and a consumer walking it channels at a time ends on a frame.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.

Returns (string?, number?, number?)(pcm, sampleRate, channels), or (nil, err).

globals/audio/device

audio.device() -> AudioDeviceStatus

What the engine's audio output is doing: state is "open" while a stream is running on an output device and "silent" while none is, and device names the device an open stream runs on. The counters record what the engine has been through keeping one open — faults a live stream reported, changes of the host's default output, reopens the engine made, failedOpens the platform refused, and the glitches a listener heard as dropouts, with lastError carrying what the platform said. A device that goes away leaves the mixer running and the engine opening a stream again as soon as one is there.

Returns AudioDeviceStatus — An AudioDeviceStatus.

local d = audio.device(); print(d.state, d.device, d.reopens)

globals/audio/encode

audio.encode(sourceBytes: buffer | string, opts: { [string]: any }?) -> (string?, string?)

Encode container audio bytes (ogg / mp3 / wav / flac) into a ZAUD payload. Every decoded sample must be finite; a source whose samples carry a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits.

Parameters

  • sourceBytes buffer | string — Encoded source audio bytes — a buffer or a binary string.
  • opts { [string]: any } (optional){ codec: "opus"|"pcm"?, bitrateKbps: number?, vbr: boolean?, sampleRate: number?, forceMono: boolean?, loopStart: number?, loopEnd: number? }

Returns (string?, string?) — The ZAUD bytes, or (nil, err).

local zaud = audio.encode(oggBytes, { bitrateKbps = 96 })

globals/audio/encodePcm

audio.encodePcm(pcm: any?, sampleRate: number, channels: number, opts: { [string]: any }?) -> (string?, string?)

Encode raw interleaved f32 PCM into a ZAUD payload. Every sample must be finite; a buffer carrying a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits, so a filter that diverged over part of a bake is caught before it is written. The sample count is a whole number of channels: a buffer with a tail over comes back as (nil, err) naming the whole frames it holds and the samples past them.

Parameters

  • pcm any (optional) — Interleaved f32 samples — a buffer or a binary string of little-endian f32, the shape microphone.samples and audio.decode hand back, or a flat number array. A byte payload's samples are its 4-byte lanes, and a length that stops partway through one comes back as (nil, err) naming the whole samples it holds and the bytes past them.
  • sampleRate number — Source sample rate in Hz.
  • channels number — 1 or 2, and a divisor of the sample count.
  • opts { [string]: any } (optional) — Same shape as audio.encode.

Returns (string?, string?) — The ZAUD bytes, or (nil, err).

local s = microphone.status(); local zaud = audio.encodePcm(microphone.samples(), s.sampleRate, 1)

globals/audio/info

audio.info(zaud: buffer | string) -> (AudioInfo?, string?)

Read a ZAUD payload's header. A PCM payload's samples are its bytes, and the header is read against them: a sample count differing from frames * channels comes back as (nil, err) naming both counts, and a sample carrying a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits, so the header handed back describes a clip that is as long as it says and can sound. The header describes the clip's shape — rate, channels, frames, duration, codec, loop points. What the samples do where a whole-clip loop wraps is a reading of its own, audio.loopSeam, which is the call that answers whether a bed cycles without a click.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.

Returns (AudioInfo?, string?) — An AudioInfo table, or (nil, err).

local info = audio.info(zaud); print(info.durationMs)

globals/audio/levels

audio.levels() -> AudioLevels

The master mix's peak and RMS over the meter's most recent closed window, measured without recording anything.

Returns AudioLevels — An AudioLevels.

local l = audio.levels(); print(l.peak, l.rms, l.windowMs)

globals/audio/listener

audio.listener() -> AudioListenerState

Where the scene is heard from, how many active listeners exist, and which entity's listener drives the ears.

Returns AudioListenerState — An AudioListenerState.

local l = audio.listener(); print(l.present, l.count, l.entity)

globals/audio/loopSeam

audio.loopSeam(zaud: buffer | string) -> (AudioLoopSeam?, string?)

Measure what a clip's samples do where a whole-clip loop wraps, so a bed can be judged before anyone hears it tick. The wrap's own step (|x[1] - x[frames]|) is reported against the step the signal ordinarily makes between neighbouring samples, as ratio = step / meanStep — a figure in the units the signal itself moves in, so a quiet ambience and a loud drone are read the same way. A bed whose partials wrap reads near 1; one carrying a strike at its head and silence at its tail reads in the tens. ratio and the step / meanStep / maxStep beside it belong to the worst channel, channel names it, and channels carries every channel's own reading. seamless is ratio <= threshold, the same threshold asset.create("soundClip", ...) warns past. The reading is taken on the DECODED samples, so it answers for what the codec left behind and for a clip that arrived already encoded and whose source buffer nobody holds. Costs a decode of the whole payload; audio.info reads a header without one.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.

Returns (AudioLoopSeam?, string?) — An AudioLoopSeam table, or (nil, err).

local seam = audio.loopSeam(clipRef:getBytes()); print(seam.ratio, seam.seamless)

globals/audio/mixer

audio.mixer() -> AudioMixerLevels

The levels the mixer is applying to the mix right now: the master level, whether the mix is muted, and the level of every channel one has been set on. A channel absent from channels plays at unity, so a source naming it is heard at the volume it asks for.

Returns AudioMixerLevels — An AudioMixerLevels.

local m = audio.mixer(); print(m.master, m.muted, m.channels.music)

globals/audio/observe

audio.observe() -> AudioObservation

Report what the mixer is making audible right now, and why a source is not. One read covering every live voice with the mixer's own playback state and effective gain, the master mix's level, the mixer's voice accounting, the listener, the output device the mix is reaching, and what the subsystem costs. Answers in edit mode as well as play mode.

Returns AudioObservation — An AudioObservation.

local a = audio.observe(); print(a.audibleCount, a.levels.rms)
for _, v in audio.observe().voices do print(v.entity, v.mixerState, v.silence) end

globals/audio/peakSince

audio.peakSince(window: number) -> number?

The loudest peak the master mix reached across the meter's windows that closed after its windows count stood at window. audio.levels() carries the window that closed last, so a reader sees the windows its own frames happen to land on; this spans all of them, which is what measuring a sound shorter than the gap between two reads takes. Take the mark from audio.levels().windows before the sound starts, wait until windows has advanced past the sound's length, then read the span.

Parameters

  • window number — A windows count taken from audio.levels() earlier.

Returns number? — The loudest window peak in the span, or nil when the meter holds no peak for it — nothing has closed since window, or the span reaches further back than the meter's history of recent windows, so a reader that came back too late learns that instead of reading the maximum of the part that survived.

local mark = audio.levels().windows
local peak = audio.peakSince(mark)

globals/audio/profile

audio.profile() -> AudioProfile

What the audio subsystem has cost since the profiling window opened — the streaming pump, clip decode, clip encode, voice starts, and building the observation itself. Every total is a SUM across that window rather than a per-frame figure, and the window runs from the last audio.resetProfile() or from engine start. For what a frame costs now, reset, let frames pass, then divide by the frames the window reports.

Returns AudioProfile — An AudioProfile.

audio.resetProfile(); task.wait(1); local p = audio.profile()
print("per frame:", (p.pump.totalMs + p.observe.totalMs) / p.frames)

globals/audio/resetProfile

audio.resetProfile()

Open a new audio profiling window, discarding what the previous one measured. Call this before timing a stretch of frames: without it audio.profile() reports totals reaching back to engine start.

audio.resetProfile()

globals/audio/setChannelVolume

audio.setChannelVolume(channel: string, volume: number)

Set the level of one mixer channel — the channel an Audio component names, such as "sfx", "music" or "ambient", or any name the scene invents. It scales every voice on that channel and nothing else, reaches voices that are already playing, and comes back per voice as gain.channel. A channel no level has been set on plays at unity.

Parameters

  • channel string — The channel name, matching Audio.channel.
  • volume number — Channel level, 0..1.
audio.setChannelVolume("music", 0.3)
for _, v in audio.voices() do print(v.channel, v.gain.channel) end

globals/audio/setMasterVolume

audio.setMasterVolume(volume: number)

Set the master level of the mix, on the engine's 0..1 amplitude scale. It scales every voice whatever channel it plays on, reaches voices that are already playing, and comes back per voice as gain.master.

Parameters

  • volume number — Master level, 0..1.
audio.setMasterVolume(0.5)

globals/audio/setMuted

audio.setMuted(muted: boolean)

Silence or unsilence the whole mix. A muted mix sounds nothing whatever its master and channel levels read, every voice reports masterSilent, and unmuting hands the levels back untouched.

Parameters

  • muted boolean — Whether the mix is silenced.
audio.setMuted(true)

globals/audio/voice

audio.voice(entityId: string) -> AudioVoice?

The voice on one entity, or nil when that entity carries no audio source.

Parameters

  • entityId string — The entity's stable id.

Returns AudioVoice? — An AudioVoice, or nil.

local v = audio.voice(e.id); print(v and v.mixerState)

globals/audio/voiceAccounting

audio.voiceAccounting() -> AudioVoiceAccounting

How many voices the mixer can hold, how many are in use, how many are free — read off the mixer's own tracks, so the free count is the one a play call is granted or refused against. The two pools are reported apart: capacity / inUse / free are the main track, which carries the NON-spatial voices, while a spatial voice plays through its own sub-track and is counted by spatialInUse instead. sourcesHolding counts both pools from the sources that own them, so it equals inUse + spatialInUse while every voice answers to a source.

Returns AudioVoiceAccounting — An AudioVoiceAccounting.

local v = audio.voiceAccounting(); print(v.inUse .. "/" .. v.capacity)
local v = audio.voiceAccounting(); print(v.sourcesHolding - (v.inUse + v.spatialInUse))

globals/audio/voices

audio.voices() -> { AudioVoice }

Every live audio source with the mixer's opinion of it.

Returns { AudioVoice } — An array of AudioVoice.

for _, v in audio.voices() do print(v.clip, v.gain.effective) end

globals/audio/whySilent

audio.whySilent(entityId: string) -> (string?, string?)

Why the source on an entity is making no sound. Returns nil when it IS sounding, and one of noBackend, noDevice, notResident, neverStarted, refused, paused, ended, gainZero, channelSilent, masterSilent, outOfRange when it is not — the nearest cause, so the answer names the thing to change. A second return carries the mixer's own words when it refused the source, and "no audio source on this entity" when nothing there plays at all.

Parameters

  • entityId string — The entity's stable id.

Returns (string?, string?)(reason, detail).

local why = audio.whySilent(e.id); if why then print(why) end

modules/audio/README

require("@builtin/modules/api/engine/audio") -- audio (also available as global 'audio')

Engine-native audio: encode audio (or raw PCM) into the ZAUD compressed payload, decode/inspect it, set what the mix is heard at — a level per named channel, a master level and a mute — and observe what the mixer is making audible right now: live voices, why a source is silent, master levels, mixer voice accounting, listener state and subsystem cost.

Usage: local audio = require("@builtin/modules/api/engine/audio") Also available as global: audio

modules/audio/decode

decode(zaud: buffer | string): (string?, number?, number?)

Decode a ZAUD payload into interleaved f32 PCM. A PCM payload comes back as the frames its header accounts for, held to the whole frames the bytes behind it fill, so the sample count is always a whole number of channels and a consumer walking it channels at a time ends on a frame.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.

modules/audio/device

device(): AudioDeviceStatus

What the engine's audio output is doing: state is "open" while a stream is running on an output device and "silent" while none is, and device names the device an open stream runs on. The counters record what the engine has been through keeping one open — faults a live stream reported, changes of the host's default output, reopens the engine made, failedOpens the platform refused, and the glitches a listener heard as dropouts, with lastError carrying what the platform said. A device that goes away leaves the mixer running and the engine opening a stream again as soon as one is there.

local d = audio.device(); print(d.state, d.device, d.reopens)

modules/audio/encode

encode(sourceBytes: buffer | string, opts: { [string]: any }?): (string?, string?)

Encode container audio bytes (ogg / mp3 / wav / flac) into a ZAUD payload. Every decoded sample must be finite; a source whose samples carry a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits.

Parameters

  • sourceBytes buffer | string — Encoded source audio bytes — a buffer or a binary string.
  • opts { [string]: any }? (optional){ codec: "opus"|"pcm"?, bitrateKbps: number?, vbr: boolean?, sampleRate: number?, forceMono: boolean?, loopStart: number?, loopEnd: number? }
local zaud = audio.encode(oggBytes, { bitrateKbps = 96 })

modules/audio/encodePcm

encodePcm(pcm: any, sampleRate: number, channels: number, opts: { [string]: any }?): (string?, string?)

Encode raw interleaved f32 PCM into a ZAUD payload. Every sample must be finite; a buffer carrying a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits, so a filter that diverged over part of a bake is caught before it is written. The sample count is a whole number of channels: a buffer with a tail over comes back as (nil, err) naming the whole frames it holds and the samples past them.

Parameters

  • pcm any (optional) — Interleaved f32 samples — a buffer or a binary string of little-endian f32, the shape microphone.samples and audio.decode hand back, or a flat number array. A byte payload's samples are its 4-byte lanes, and a length that stops partway through one comes back as (nil, err) naming the whole samples it holds and the bytes past them.
  • sampleRate number — Source sample rate in Hz.
  • channels number — 1 or 2, and a divisor of the sample count.
  • opts { [string]: any }? (optional) — Same shape as audio.encode.
local s = microphone.status(); local zaud = audio.encodePcm(microphone.samples(), s.sampleRate, 1)

modules/audio/info

info(zaud: buffer | string): (AudioInfo?, string?)

Read a ZAUD payload's header. A PCM payload's samples are its bytes, and the header is read against them: a sample count differing from frames * channels comes back as (nil, err) naming both counts, and a sample carrying a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits, so the header handed back describes a clip that is as long as it says and can sound. The header describes the clip's shape — rate, channels, frames, duration, codec, loop points. What the samples do where a whole-clip loop wraps is a reading of its own, audio.loopSeam, which is the call that answers whether a bed cycles without a click.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.
local info = audio.info(zaud); print(info.durationMs)

modules/audio/levels

levels(): AudioLevels

The master mix's peak and RMS over the meter's most recent closed window, measured without recording anything.

local l = audio.levels(); print(l.peak, l.rms, l.windowMs)

modules/audio/listener

listener(): AudioListenerState

Where the scene is heard from, how many active listeners exist, and which entity's listener drives the ears.

local l = audio.listener(); print(l.present, l.count, l.entity)

modules/audio/loopSeam

loopSeam(zaud: buffer | string): (AudioLoopSeam?, string?)

Measure what a clip's samples do where a whole-clip loop wraps, so a bed can be judged before anyone hears it tick. The wrap's own step (|x[1] - x[frames]|) is reported against the step the signal ordinarily makes between neighbouring samples, as ratio = step / meanStep — a figure in the units the signal itself moves in, so a quiet ambience and a loud drone are read the same way. A bed whose partials wrap reads near 1; one carrying a strike at its head and silence at its tail reads in the tens. ratio and the step / meanStep / maxStep beside it belong to the worst channel, channel names it, and channels carries every channel's own reading. seamless is ratio <= threshold, the same threshold asset.create("soundClip", ...) warns past. The reading is taken on the DECODED samples, so it answers for what the codec left behind and for a clip that arrived already encoded and whose source buffer nobody holds. Costs a decode of the whole payload; audio.info reads a header without one.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.
local seam = audio.loopSeam(clipRef:getBytes()); print(seam.ratio, seam.seamless)

modules/audio/mixer

mixer(): AudioMixerLevels

The levels the mixer is applying to the mix right now: the master level, whether the mix is muted, and the level of every channel one has been set on. A channel absent from channels plays at unity, so a source naming it is heard at the volume it asks for.

local m = audio.mixer(); print(m.master, m.muted, m.channels.music)

modules/audio/observe

observe(): AudioObservation

Report what the mixer is making audible right now, and why a source is not. One read covering every live voice with the mixer's own playback state and effective gain, the master mix's level, the mixer's voice accounting, the listener, the output device the mix is reaching, and what the subsystem costs. Answers in edit mode as well as play mode.

local a = audio.observe(); print(a.audibleCount, a.levels.rms)
for _, v in audio.observe().voices do print(v.entity, v.mixerState, v.silence) end

modules/audio/peakSince

peakSince(window: number): number?

The loudest peak the master mix reached across the meter's windows that closed after its windows count stood at window. audio.levels() carries the window that closed last, so a reader sees the windows its own frames happen to land on; this spans all of them, which is what measuring a sound shorter than the gap between two reads takes. Take the mark from audio.levels().windows before the sound starts, wait until windows has advanced past the sound's length, then read the span.

Parameters

  • window number — A windows count taken from audio.levels() earlier.
local mark = audio.levels().windows
local peak = audio.peakSince(mark)

modules/audio/profile

profile(): AudioProfile

What the audio subsystem has cost since the profiling window opened — the streaming pump, clip decode, clip encode, voice starts, and building the observation itself. Every total is a SUM across that window rather than a per-frame figure, and the window runs from the last audio.resetProfile() or from engine start. For what a frame costs now, reset, let frames pass, then divide by the frames the window reports.

audio.resetProfile(); task.wait(1); local p = audio.profile()
print("per frame:", (p.pump.totalMs + p.observe.totalMs) / p.frames)

modules/audio/resetProfile

resetProfile()

Open a new audio profiling window, discarding what the previous one measured. Call this before timing a stretch of frames: without it audio.profile() reports totals reaching back to engine start.

audio.resetProfile()

modules/audio/setChannelVolume

setChannelVolume(channel: string, volume: number)

Set the level of one mixer channel — the channel an Audio component names, such as "sfx", "music" or "ambient", or any name the scene invents. It scales every voice on that channel and nothing else, reaches voices that are already playing, and comes back per voice as gain.channel. A channel no level has been set on plays at unity.

Parameters

  • channel string — The channel name, matching Audio.channel.
  • volume number — Channel level, 0..1.
audio.setChannelVolume("music", 0.3)
for _, v in audio.voices() do print(v.channel, v.gain.channel) end

modules/audio/setMasterVolume

setMasterVolume(volume: number)

Set the master level of the mix, on the engine's 0..1 amplitude scale. It scales every voice whatever channel it plays on, reaches voices that are already playing, and comes back per voice as gain.master.

Parameters

  • volume number — Master level, 0..1.
audio.setMasterVolume(0.5)

modules/audio/setMuted

setMuted(muted: boolean)

Silence or unsilence the whole mix. A muted mix sounds nothing whatever its master and channel levels read, every voice reports masterSilent, and unmuting hands the levels back untouched.

Parameters

  • muted boolean — Whether the mix is silenced.
audio.setMuted(true)

modules/audio/voice

voice(entityId: string): AudioVoice?

The voice on one entity, or nil when that entity carries no audio source.

Parameters

  • entityId string — The entity's stable id.
local v = audio.voice(e.id); print(v and v.mixerState)

modules/audio/voiceAccounting

voiceAccounting(): AudioVoiceAccounting

How many voices the mixer can hold, how many are in use, how many are free — read off the mixer's own tracks, so the free count is the one a play call is granted or refused against. The two pools are reported apart: capacity / inUse / free are the main track, which carries the NON-spatial voices, while a spatial voice plays through its own sub-track and is counted by spatialInUse instead. sourcesHolding counts both pools from the sources that own them, so it equals inUse + spatialInUse while every voice answers to a source.

local v = audio.voiceAccounting(); print(v.inUse .. "/" .. v.capacity)
local v = audio.voiceAccounting(); print(v.sourcesHolding - (v.inUse + v.spatialInUse))

modules/audio/voices

voices(): { AudioVoice }

Every live audio source with the mixer's opinion of it.

for _, v in audio.voices() do print(v.clip, v.gain.effective) end

modules/audio/whySilent

whySilent(entityId: string): (string?, string?)

Why the source on an entity is making no sound. Returns nil when it IS sounding, and one of noBackend, noDevice, notResident, neverStarted, refused, paused, ended, gainZero, channelSilent, masterSilent, outOfRange when it is not — the nearest cause, so the answer names the thing to change. A second return carries the mixer's own words when it refused the source, and "no audio source on this entity" when nothing there plays at all.

Parameters

  • entityId string — The entity's stable id.
local why = audio.whySilent(e.id); if why then print(why) end

typed/builtin//modules/api/engine/audio/audio/decode

audio.decode(zaud: buffer | string) -> (string?, number?, number?)

Decode a ZAUD payload into interleaved f32 PCM. A PCM payload comes back as the frames its header accounts for, held to the whole frames the bytes behind it fill, so the sample count is always a whole number of channels and a consumer walking it channels at a time ends on a frame.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.

Returns (string?, number?, number?)(pcm, sampleRate, channels), or (nil, err).

typed/builtin//modules/api/engine/audio/audio/device

audio.device() -> AudioDeviceStatus

What the engine's audio output is doing: state is "open" while a stream is running on an output device and "silent" while none is, and device names the device an open stream runs on. The counters record what the engine has been through keeping one open — faults a live stream reported, changes of the host's default output, reopens the engine made, failedOpens the platform refused, and the glitches a listener heard as dropouts, with lastError carrying what the platform said. A device that goes away leaves the mixer running and the engine opening a stream again as soon as one is there.

Returns AudioDeviceStatus — An AudioDeviceStatus.

local d = audio.device(); print(d.state, d.device, d.reopens)

typed/builtin//modules/api/engine/audio/audio/encode

audio.encode(sourceBytes: buffer | string, opts: { [string]: any }?) -> (string?, string?)

Encode container audio bytes (ogg / mp3 / wav / flac) into a ZAUD payload. Every decoded sample must be finite; a source whose samples carry a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits.

Parameters

  • sourceBytes buffer | string — Encoded source audio bytes — a buffer or a binary string.
  • opts { [string]: any } (optional){ codec: "opus"|"pcm"?, bitrateKbps: number?, vbr: boolean?, sampleRate: number?, forceMono: boolean?, loopStart: number?, loopEnd: number? }

Returns (string?, string?) — The ZAUD bytes, or (nil, err).

local zaud = audio.encode(oggBytes, { bitrateKbps = 96 })

typed/builtin//modules/api/engine/audio/audio/encodePcm

audio.encodePcm(pcm: any?, sampleRate: number, channels: number, opts: { [string]: any }?) -> (string?, string?)

Encode raw interleaved f32 PCM into a ZAUD payload. Every sample must be finite; a buffer carrying a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits, so a filter that diverged over part of a bake is caught before it is written. The sample count is a whole number of channels: a buffer with a tail over comes back as (nil, err) naming the whole frames it holds and the samples past them.

Parameters

  • pcm any (optional) — Interleaved f32 samples — a buffer or a binary string of little-endian f32, the shape microphone.samples and audio.decode hand back, or a flat number array. A byte payload's samples are its 4-byte lanes, and a length that stops partway through one comes back as (nil, err) naming the whole samples it holds and the bytes past them.
  • sampleRate number — Source sample rate in Hz.
  • channels number — 1 or 2, and a divisor of the sample count.
  • opts { [string]: any } (optional) — Same shape as audio.encode.

Returns (string?, string?) — The ZAUD bytes, or (nil, err).

local s = microphone.status(); local zaud = audio.encodePcm(microphone.samples(), s.sampleRate, 1)

typed/builtin//modules/api/engine/audio/audio/info

audio.info(zaud: buffer | string) -> (AudioInfo?, string?)

Read a ZAUD payload's header. A PCM payload's samples are its bytes, and the header is read against them: a sample count differing from frames * channels comes back as (nil, err) naming both counts, and a sample carrying a NaN or an infinity comes back as (nil, err) naming how many fail and where the first one sits, so the header handed back describes a clip that is as long as it says and can sound. The header describes the clip's shape — rate, channels, frames, duration, codec, loop points. What the samples do where a whole-clip loop wraps is a reading of its own, audio.loopSeam, which is the call that answers whether a bed cycles without a click.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.

Returns (AudioInfo?, string?) — An AudioInfo table, or (nil, err).

local info = audio.info(zaud); print(info.durationMs)

typed/builtin//modules/api/engine/audio/audio/levels

audio.levels() -> AudioLevels

The master mix's peak and RMS over the meter's most recent closed window, measured without recording anything.

Returns AudioLevels — An AudioLevels.

local l = audio.levels(); print(l.peak, l.rms, l.windowMs)

typed/builtin//modules/api/engine/audio/audio/listener

audio.listener() -> AudioListenerState

Where the scene is heard from, how many active listeners exist, and which entity's listener drives the ears.

Returns AudioListenerState — An AudioListenerState.

local l = audio.listener(); print(l.present, l.count, l.entity)

typed/builtin//modules/api/engine/audio/audio/loopSeam

audio.loopSeam(zaud: buffer | string) -> (AudioLoopSeam?, string?)

Measure what a clip's samples do where a whole-clip loop wraps, so a bed can be judged before anyone hears it tick. The wrap's own step (|x[1] - x[frames]|) is reported against the step the signal ordinarily makes between neighbouring samples, as ratio = step / meanStep — a figure in the units the signal itself moves in, so a quiet ambience and a loud drone are read the same way. A bed whose partials wrap reads near 1; one carrying a strike at its head and silence at its tail reads in the tens. ratio and the step / meanStep / maxStep beside it belong to the worst channel, channel names it, and channels carries every channel's own reading. seamless is ratio <= threshold, the same threshold asset.create("soundClip", ...) warns past. The reading is taken on the DECODED samples, so it answers for what the codec left behind and for a clip that arrived already encoded and whose source buffer nobody holds. Costs a decode of the whole payload; audio.info reads a header without one.

Parameters

  • zaud buffer | string — A ZAUD payload — a buffer or a binary string.

Returns (AudioLoopSeam?, string?) — An AudioLoopSeam table, or (nil, err).

local seam = audio.loopSeam(clipRef:getBytes()); print(seam.ratio, seam.seamless)

typed/builtin//modules/api/engine/audio/audio/mixer

audio.mixer() -> AudioMixerLevels

The levels the mixer is applying to the mix right now: the master level, whether the mix is muted, and the level of every channel one has been set on. A channel absent from channels plays at unity, so a source naming it is heard at the volume it asks for.

Returns AudioMixerLevels — An AudioMixerLevels.

local m = audio.mixer(); print(m.master, m.muted, m.channels.music)

typed/builtin//modules/api/engine/audio/audio/observe

audio.observe() -> AudioObservation

Report what the mixer is making audible right now, and why a source is not. One read covering every live voice with the mixer's own playback state and effective gain, the master mix's level, the mixer's voice accounting, the listener, the output device the mix is reaching, and what the subsystem costs. Answers in edit mode as well as play mode.

Returns AudioObservation — An AudioObservation.

local a = audio.observe(); print(a.audibleCount, a.levels.rms)
for _, v in audio.observe().voices do print(v.entity, v.mixerState, v.silence) end

typed/builtin//modules/api/engine/audio/audio/peakSince

audio.peakSince(window: number) -> number?

The loudest peak the master mix reached across the meter's windows that closed after its windows count stood at window. audio.levels() carries the window that closed last, so a reader sees the windows its own frames happen to land on; this spans all of them, which is what measuring a sound shorter than the gap between two reads takes. Take the mark from audio.levels().windows before the sound starts, wait until windows has advanced past the sound's length, then read the span.

Parameters

  • window number — A windows count taken from audio.levels() earlier.

Returns number? — The loudest window peak in the span, or nil when the meter holds no peak for it — nothing has closed since window, or the span reaches further back than the meter's history of recent windows, so a reader that came back too late learns that instead of reading the maximum of the part that survived.

local mark = audio.levels().windows
local peak = audio.peakSince(mark)

typed/builtin//modules/api/engine/audio/audio/profile

audio.profile() -> AudioProfile

What the audio subsystem has cost since the profiling window opened — the streaming pump, clip decode, clip encode, voice starts, and building the observation itself. Every total is a SUM across that window rather than a per-frame figure, and the window runs from the last audio.resetProfile() or from engine start. For what a frame costs now, reset, let frames pass, then divide by the frames the window reports.

Returns AudioProfile — An AudioProfile.

audio.resetProfile(); task.wait(1); local p = audio.profile()
print("per frame:", (p.pump.totalMs + p.observe.totalMs) / p.frames)

typed/builtin//modules/api/engine/audio/audio/resetProfile

audio.resetProfile()

Open a new audio profiling window, discarding what the previous one measured. Call this before timing a stretch of frames: without it audio.profile() reports totals reaching back to engine start.

audio.resetProfile()

typed/builtin//modules/api/engine/audio/audio/setChannelVolume

audio.setChannelVolume(channel: string, volume: number)

Set the level of one mixer channel — the channel an Audio component names, such as "sfx", "music" or "ambient", or any name the scene invents. It scales every voice on that channel and nothing else, reaches voices that are already playing, and comes back per voice as gain.channel. A channel no level has been set on plays at unity.

Parameters

  • channel string — The channel name, matching Audio.channel.
  • volume number — Channel level, 0..1.
audio.setChannelVolume("music", 0.3)
for _, v in audio.voices() do print(v.channel, v.gain.channel) end

typed/builtin//modules/api/engine/audio/audio/setMasterVolume

audio.setMasterVolume(volume: number)

Set the master level of the mix, on the engine's 0..1 amplitude scale. It scales every voice whatever channel it plays on, reaches voices that are already playing, and comes back per voice as gain.master.

Parameters

  • volume number — Master level, 0..1.
audio.setMasterVolume(0.5)

typed/builtin//modules/api/engine/audio/audio/setMuted

audio.setMuted(muted: boolean)

Silence or unsilence the whole mix. A muted mix sounds nothing whatever its master and channel levels read, every voice reports masterSilent, and unmuting hands the levels back untouched.

Parameters

  • muted boolean — Whether the mix is silenced.
audio.setMuted(true)

typed/builtin//modules/api/engine/audio/audio/voice

audio.voice(entityId: string) -> AudioVoice?

The voice on one entity, or nil when that entity carries no audio source.

Parameters

  • entityId string — The entity's stable id.

Returns AudioVoice? — An AudioVoice, or nil.

local v = audio.voice(e.id); print(v and v.mixerState)

typed/builtin//modules/api/engine/audio/audio/voiceAccounting

audio.voiceAccounting() -> AudioVoiceAccounting

How many voices the mixer can hold, how many are in use, how many are free — read off the mixer's own tracks, so the free count is the one a play call is granted or refused against. The two pools are reported apart: capacity / inUse / free are the main track, which carries the NON-spatial voices, while a spatial voice plays through its own sub-track and is counted by spatialInUse instead. sourcesHolding counts both pools from the sources that own them, so it equals inUse + spatialInUse while every voice answers to a source.

Returns AudioVoiceAccounting — An AudioVoiceAccounting.

local v = audio.voiceAccounting(); print(v.inUse .. "/" .. v.capacity)
local v = audio.voiceAccounting(); print(v.sourcesHolding - (v.inUse + v.spatialInUse))

typed/builtin//modules/api/engine/audio/audio/voices

audio.voices() -> { AudioVoice }

Every live audio source with the mixer's opinion of it.

Returns { AudioVoice } — An array of AudioVoice.

for _, v in audio.voices() do print(v.clip, v.gain.effective) end

typed/builtin//modules/api/engine/audio/audio/whySilent

audio.whySilent(entityId: string) -> (string?, string?)

Why the source on an entity is making no sound. Returns nil when it IS sounding, and one of noBackend, noDevice, notResident, neverStarted, refused, paused, ended, gainZero, channelSilent, masterSilent, outOfRange when it is not — the nearest cause, so the answer names the thing to change. A second return carries the mixer's own words when it refused the source, and "no audio source on this entity" when nothing there plays at all.

Parameters

  • entityId string — The entity's stable id.

Returns (string?, string?)(reason, detail).

local why = audio.whySilent(e.id); if why then print(why) end
  • api
  • reference