# `tecs.audio`
Clips, voices, groups, keyed limits, and entity-owned sound.
Tecs decides what should sound and a backend makes the noise. Voice slots,
group settings, admission limits and every handle a game holds live here; the
backend under `tecs.platform.audiobackend` receives one array of commands per
frame and hands back the voices it has finished with. A machine with no output
device still gets a working object: `available` reports false, `play` answers
zero, and nothing raises.
## Playback
`load` reads a file and returns one shared clip per path. Clips shorter than
`streamSeconds` hold decoded samples that every voice reading them shares, and
longer clips stream separately for each voice. `LoadOptions.stream` overrides
that choice.
```nupp
local mixer = tecs.audio.newAudio()
local step = assert(mixer:load("assets/sfx/step.ogg"))
mixer:setLimit("footstep", {voices = 3, cooldown = 0.05})
local voice = mixer:play(step, {gain = 0.7, group = "sfx", key = "footstep", pitchVariance = 0.1})
if voice ~= 0 then
mixer:stop(voice, 0.2)
end
```
Units, in one place, because none of them is in a signature. Time is seconds
everywhere on this surface: a fade, a seek, a loop point, a cooldown, a clip's
duration. Every fade is a duration to run for rather than a moment to finish
at, so calling one twice restarts it. Gain is linear amplitude rather than
decibels, where zero is silence and one is the sound as recorded, and the level
a voice reaches the output at is its own gain times its group's gain times the
master gain. Pitch is a playback rate rather than an interval, so two is an
octave up and half as long. Position is the backend's frame rather than the
world's: the listener sits at the origin, x is positive right, y is positive
up, and z is positive behind.
A voice handle packs a slot with its generation, so a handle stays meaningful
for exactly as long as its voice sounds and never starts naming the voice that
takes the slot next. Holding a stale one is not an error and requires no
release.
## Groups and limits
A group name controls gain, mute, pause, resume and stop, and its settings
apply to voices that join later as well as to the ones sounding now. A key
controls admission through a concurrent voice limit and a cooldown. One voice
may carry both, and neither name has to know about the other. Reaching a limit
drops the new voice rather than stealing an older one.
## Entity-owned sound
An entity can carry [`Sound`](tecs.audio.Sound) instead of a game keeping a
handle. The audio pass starts it, follows the writable playback fields, and
stops it when the component or the entity disappears. Write through
`world:getMut`.
## Devices and recording
`playbackDevices` and `recordingDevices` name what is attached now, and
`openMicrophone` opens one for capture. A [`Microphone`](tecs.audio.Microphone)
is pulled rather than pushed: the backend fills a bounded buffer from its own
thread and `read` empties it on the frame thread, so nothing a device thread
runs ever enters Nupp.
```nupp
for _, device in ipairs(tecs.audio.recordingDevices()) do
print(device.id, device.name, device.frequency, device.channels)
end
local microphone = assert(tecs.audio.openMicrophone({channels = 1}))
local block = assert(microphone:read())
microphone:destroy()
```
Listing devices opens no mixer and no microphone, and opening a microphone opens
no mixer, so a game that only wants to name devices pays for nothing else.
## The frame
`update` is not a world system, and `install` deliberately does not add one.
Reaping voices has to continue while a world is paused, so an application
drives it from the iteration instead.
A native stream error permanently fails that output. The callback publishes a
flag; the next `update` sets `available` false, records `failureReason`, discards
queued starts and releases every active voice without replay. Destroy the mixer
and create a new one to reopen an output. Starting without an output remains a
separate silent fallback, not a device-loss event.
A failed microphone keeps already buffered frames. Once drained, `read` returns
nil and the failure reason, while the value-only `readInto` raises that reason.
`failure` reports loss even before the buffer drains. `resume` refuses a failed
stream; destroy it and open a new microphone. Native capture supports one to
eight channels. The public argument range remains one to 32 for compatibility;
a valid request above eight returns an operational refusal from the native
opener. Device enumeration is snapshot-only and provides no change
notifications. Listing devices and observing failure of an active stream do not
depend on notifications.
## Constructors
### `newAudio` _constructor_
```nupp
function newAudio(config: Config?): Audio
```
Creates the mixer a game plays sound through.
Never raises for want of hardware. A machine with no sound card gets an
object whose calls all succeed and produce nothing, because few games
require an audio device while many test machines lack one.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `config` | `Config?` | the settings, or nil for the defaults |
#### Returns
| Type | Description |
| --- | --- |
| `Audio` | the mixer, which the caller has to `destroy` |
#### Raises
- when `maxVoices` falls outside one to 65535
## Types
### `Audio` _record_
```nupp
record Audio
failureReason: string?
available: boolean
flush: function(exclusive self: Audio): nil
destroy: function(exclusive self: Audio): nil
decoders: function(borrows self: Audio): {string}
load: function(exclusive self: Audio, path: string, options: LoadOptions?): (Clip?, string?)
clip: function(borrows self: Audio, id: integer): Clip?
clips: function(borrows self: Audio): {Clip}
reload: function(exclusive self: Audio, path: string): (boolean, string?)
play: function(exclusive self: Audio, clip: Clip?, options: PlayOptions?): integer
stop: function(exclusive self: Audio, handle: integer, fadeOut: number?): nil
stopAll: function(exclusive self: Audio, fadeOut: number?): nil
playing: function(borrows self: Audio, handle: integer): boolean
paused: function(borrows self: Audio, handle: integer): boolean
pause: function(exclusive self: Audio, handle: integer): nil
resume: function(exclusive self: Audio, handle: integer): nil
setGain: function(exclusive self: Audio, handle: integer, gain: number): nil
setPitch: function(exclusive self: Audio, handle: integer, ratio: number): nil
setLoop: function(exclusive self: Audio, handle: integer, loop: boolean): nil
looping: function(borrows self: Audio, handle: integer): boolean
seek: function(exclusive self: Audio, handle: integer, seconds: number): boolean
tell: function(exclusive self: Audio, handle: integer): number?
setPosition: function(exclusive self: Audio, handle: integer, x: number, y: number, z: number): nil
setStereo: function(exclusive self: Audio, handle: integer, left: number, right: number): nil
clearSpatial: function(exclusive self: Audio, handle: integer): nil
setMasterGain: function(exclusive self: Audio, gain: number): nil
masterGain: function(borrows self: Audio): number
setMuted: function(exclusive self: Audio, muted: boolean): nil
muted: function(borrows self: Audio): boolean
sounding: function(borrows self: Audio): integer
maxVoices: function(borrows self: Audio): integer
setGroupGain: function(exclusive self: Audio, name: string, gain: number): nil
groupGain: function(borrows self: Audio, name: string): number
setGroupMuted: function(exclusive self: Audio, name: string, muted: boolean): nil
groupMuted: function(borrows self: Audio, name: string): boolean
pauseGroup: function(exclusive self: Audio, name: string): nil
resumeGroup: function(exclusive self: Audio, name: string): nil
groupPaused: function(borrows self: Audio, name: string): boolean
groups: function(borrows self: Audio): {string}
stopGroup: function(exclusive self: Audio, name: string, fadeOut: number?): nil
setLimit: function(exclusive self: Audio, key: string, limit: Limit?): nil
limit: function(borrows self: Audio, key: string): Limit?
keyCount: function(borrows self: Audio, key: string): integer
keys: function(borrows self: Audio): {string}
voiceList: function(borrows self: Audio): {VoiceInfo}
update: function(exclusive self: Audio, dt: number?): integer
end
```
One output and the voices sounding on it.
Nothing here is thread safe and nothing here calls back. Every method runs
on the thread that calls it, and the backend's own audio thread reaches
Nupp only by leaving finished handles where `update` collects them.
#### Methods
##### `flush`
```nupp
flush: function(exclusive self: Audio): nil
```
Sends every queued command to the backend.
`update` and the audio pass both call this, so a game following the ordinary
frame never needs it. Call it directly only to close the gap between a
command and the buffer it reaches.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `destroy`
```nupp
destroy: function(exclusive self: Audio): nil
```
Stops everything and closes the output.
Stops every voice without a fade, moves every clip to `"released"`, and
removes this mixer from every world that installed it. There is no
reopening: make a new instance instead.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `decoders`
```nupp
decoders: function(borrows self: Audio): {string}
```
Returns the decoder names this build linked.
What a build asked for and what it got are different questions, and this
answers the second.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
###### Returns
| Type | Description |
| --- | --- |
| `{string}` | |
##### `load`
```nupp
load: function(exclusive self: Audio, path: string, options: LoadOptions?): (Clip?, string?)
```
Loads a sound and returns its cached clip.
Loading the same path twice returns the same clip: a clip is the file, and
playing it twice is two voices reading one clip. `options.stream` overrides
the duration threshold that otherwise decides whether it stays in memory.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `path` | `string` | |
| `options` | `LoadOptions?` | |
###### Returns
| Type | Description |
| --- | --- |
| `Clip?` | |
| `string?` | |
###### Raises
- when the path is empty
##### `clip`
```nupp
clip: function(borrows self: Audio, id: integer): Clip?
```
Returns the clip an index names.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
| `id` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `Clip?` | |
##### `clips`
```nupp
clips: function(borrows self: Audio): {Clip}
```
Returns every loaded clip in index order.
For introspection: it builds a list per call, so nothing on a frame's path
should read it.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
###### Returns
| Type | Description |
| --- | --- |
| `{Clip}` | |
##### `reload`
```nupp
reload: function(exclusive self: Audio, path: string): (boolean, string?)
```
Re-reads a clip's file over the clip already loaded from it.
A clip's index is its path's, so an edited file comes back under the index
every [`Sound`](tecs.audio.Sound) row already carries and nothing in the
world is touched. A streamed clip holds nothing to replace, so this reports
success and the next voice to start reads what is on disk now.
Blocking, like every other reload: it is a debug operation, and answering
before the file has been read would report a success that had not happened.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `path` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
| `string?` | |
##### `play`
```nupp
play: function(exclusive self: Audio, clip: Clip?, options: PlayOptions?): integer
```
Plays a clip and returns a handle, or zero when nothing started.
Zero means the clip is not loaded, loading failed, a key's limit or cooldown
declined it, or every voice is busy. None of those is worth raising over: a
sound that does not play is not a reason for a frame to stop.
A key's limit drops the new voice and never steals an older one, which is
the same reasoning a full voice pool follows.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `clip` | `Clip?` | |
| `options` | `PlayOptions?` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `stop`
```nupp
stop: function(exclusive self: Audio, handle: integer, fadeOut: number?): nil
```
Stops a voice. A handle to one that has already ended does nothing.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `handle` | `integer` | |
| `fadeOut` | `number?` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `stopAll`
```nupp
stopAll: function(exclusive self: Audio, fadeOut: number?): nil
```
Stops every voice, over `fadeOut` seconds when that is given.
Reaches every voice, whichever group it is in and whether a
[`Sound`](tecs.audio.Sound) component started it or `play` did. A row still
asking to sound starts a fresh voice on the next audio pass, so this
silences a world rather than keeping it silent.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `fadeOut` | `number?` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `playing`
```nupp
playing: function(borrows self: Audio, handle: integer): boolean
```
Reports whether a handle still names a sounding voice.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
| `handle` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `paused`
```nupp
paused: function(borrows self: Audio, handle: integer): boolean
```
Reports whether a handle names a paused voice.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
| `handle` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `pause`
```nupp
pause: function(exclusive self: Audio, handle: integer): nil
```
Holds a voice where it is, keeping its slot until something resumes or
stops it.
Neither this nor `resume` takes a fade, and that is a decision rather than
an omission. A faded pause would be a ramp run from here, a command that
takes effect later, lands on frame boundaries rather than the audio clock,
and needs its own answer for what a stop or a group gain during the ramp
means. A game wanting that builds it from `tell`, `stop` with a fade, and a
later `play` with `start` and `fadeIn`.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `handle` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `resume`
```nupp
resume: function(exclusive self: Audio, handle: integer): nil
```
Lets a paused voice carry on.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `handle` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `setGain`
```nupp
setGain: function(exclusive self: Audio, handle: integer, gain: number): nil
```
Sets a voice's gain, before its group's and the master's.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `handle` | `integer` | |
| `gain` | `number` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `setPitch`
```nupp
setPitch: function(exclusive self: Audio, handle: integer, ratio: number): nil
```
Sets a voice's playback rate.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `handle` | `integer` | |
| `ratio` | `number` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `setLoop`
```nupp
setLoop: function(exclusive self: Audio, handle: integer, loop: boolean): nil
```
Changes whether a voice repeats, part way through.
Clearing this on a looping piece of music lets it play out to its end rather
than cutting it, and setting it on a one-shot keeps it going. It reaches a
sounding voice only: a stopped one takes its answer from the next play.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `handle` | `integer` | |
| `loop` | `boolean` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `looping`
```nupp
looping: function(borrows self: Audio, handle: integer): boolean
```
Reports whether a voice repeats at the end.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
| `handle` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `seek`
```nupp
seek: function(exclusive self: Audio, handle: integer, seconds: number): boolean
```
Moves a voice's read position.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `handle` | `integer` | |
| `seconds` | `number` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `tell`
```nupp
tell: function(exclusive self: Audio, handle: integer): number?
```
Returns a voice's read position in seconds.
A paused voice reports where it stopped, which with `seek` and a later
`play` carrying `start` and `fadeIn` is what taking a sound back where it
left off is built from.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `handle` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `number?` | |
##### `setPosition`
```nupp
setPosition: function(exclusive self: Audio, handle: integer, x: number, y: number, z: number): nil
```
Places a voice in space.
Replaces a pan set by `setStereo` rather than combining with it. Positioning
folds the input down to mono before placing it, so a stereo clip loses its
stereo image when positioned.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `handle` | `integer` | |
| `x` | `number` | |
| `y` | `number` | |
| `z` | `number` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `setStereo`
```nupp
setStereo: function(exclusive self: Audio, handle: integer, left: number, right: number): nil
```
Pins a voice to the front pair of speakers at explicit gains.
A pan rather than a position, and usually what a game laid out on a plane
wants: there is no listener to subtract and no distance model to argue with,
only how much of this comes out of each side. Replaces a position set by
`setPosition` rather than combining with it.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `handle` | `integer` | |
| `left` | `number` | |
| `right` | `number` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `clearSpatial`
```nupp
clearSpatial: function(exclusive self: Audio, handle: integer): nil
```
Returns a voice to unpositioned mixing, out of either placement.
One call answers for both, because a voice holds one placement.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `handle` | `integer` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `setMasterGain`
```nupp
setMasterGain: function(exclusive self: Audio, gain: number): nil
```
Scales everything.
One number on the output, so this costs the same whether one voice is
sounding or every voice is. Setting it while muted changes the level a later
unmute returns to and nothing audible now, which is what a volume slider
moved with the sound off should do.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `gain` | `number` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `masterGain`
```nupp
masterGain: function(borrows self: Audio): number
```
Returns master gain, whether or not mute holds it down.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
###### Returns
| Type | Description |
| --- | --- |
| `number` | |
##### `setMuted`
```nupp
setMuted: function(exclusive self: Audio, muted: boolean): nil
```
Silences everything without discarding the master gain.
It does not fan out to the groups: `groupMuted` answers whether a group is
silenced, and writing every group's bit here would overwrite the answers an
unmute has to put back.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `muted` | `boolean` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `muted`
```nupp
muted: function(borrows self: Audio): boolean
```
Reports whether master mute holds the output down.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `sounding`
```nupp
sounding: function(borrows self: Audio): integer
```
Returns the number of voices sounding now, paused and fading ones included.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `maxVoices`
```nupp
maxVoices: function(borrows self: Audio): integer
```
Returns the configured ceiling on simultaneous voices.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `setGroupGain`
```nupp
setGroupGain: function(exclusive self: Audio, name: string, gain: number): nil
```
Scales every voice in a group, and every voice that joins it later.
Composed here rather than sent as one number for the group, because a
per-group gain on the backend would overwrite what each voice asked for.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `name` | `string` | |
| `gain` | `number` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `groupGain`
```nupp
groupGain: function(borrows self: Audio, name: string): number
```
Returns a group's gain.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
| `name` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `number` | |
##### `setGroupMuted`
```nupp
setGroupMuted: function(exclusive self: Audio, name: string, muted: boolean): nil
```
Silences a group without discarding the level it was set to.
Every voice in the group contributes zero while this holds, and an unmute
puts each one back at its own gain times `groupGain`.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `name` | `string` | |
| `muted` | `boolean` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `groupMuted`
```nupp
groupMuted: function(borrows self: Audio, name: string): boolean
```
Reports whether mute applies to a group.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
| `name` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `pauseGroup`
```nupp
pauseGroup: function(exclusive self: Audio, name: string): nil
```
Holds every voice in a group, and every voice that joins it later.
The mixer records the pause as well as sending it, because a voice started
into a paused group would otherwise be the one thing still heard.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `name` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `resumeGroup`
```nupp
resumeGroup: function(exclusive self: Audio, name: string): nil
```
Lets a paused group carry on, and lets later joiners start sounding.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `name` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `groupPaused`
```nupp
groupPaused: function(borrows self: Audio, name: string): boolean
```
Reports whether a group holds its current and future voices.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
| `name` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
##### `groups`
```nupp
groups: function(borrows self: Audio): {string}
```
Returns every group this mixer knows about, sorted.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
###### Returns
| Type | Description |
| --- | --- |
| `{string}` | |
##### `stopGroup`
```nupp
stopGroup: function(exclusive self: Audio, name: string, fadeOut: number?): nil
```
Ends every voice in a group, over `fadeOut` seconds when that is given.
Stops the voices rather than the group: a gain, mute or pause set on the
name survives, and anything joining afterwards starts under them.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `name` | `string` | |
| `fadeOut` | `number?` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `setLimit`
```nupp
setLimit: function(exclusive self: Audio, key: string, limit: Limit?): nil
```
Caps how many voices a key may hold and how often it may start one.
A key is not a group. A group says where a sound's gain comes from and what
a pause reaches; a key says how many of one sound the mix will carry. The
two are set independently on `play`, so "at most three footsteps at once,
all of them in the effects group" is the ordinary case.
Reaching a limit drops the new voice: `play` returns zero and leaves every
sounding voice alone.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `key` | `string` | |
| `limit` | `Limit?` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
##### `limit`
```nupp
limit: function(borrows self: Audio, key: string): Limit?
```
Returns the limit assigned to a key.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
| `key` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `Limit?` | |
##### `keyCount`
```nupp
keyCount: function(borrows self: Audio, key: string): integer
```
Returns how many voices a key holds now.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
| `key` | `string` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `keys`
```nupp
keys: function(borrows self: Audio): {string}
```
Returns every key with a limit or a counted voice, sorted.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
###### Returns
| Type | Description |
| --- | --- |
| `{string}` | |
##### `voiceList`
```nupp
voiceList: function(borrows self: Audio): {VoiceInfo}
```
Returns every sounding voice, paused and fading ones included.
For introspection, on the same terms as `clips`. The handles it reports are
the ones `playing` and `stop` take, so a caller can act on these results.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Audio` | |
###### Returns
| Type | Description |
| --- | --- |
| `{VoiceInfo}` | |
##### `update`
```nupp
update: function(exclusive self: Audio, dt: number?): integer
```
Sends queued commands and reaps the voices the backend has finished with.
Call once per frame with the frame's step. The step advances cooldowns, and
nothing else here needs time: a fade is the backend's to run, and a voice is
over when the backend says so rather than when a clock here says it should
be.
Not a world system, and `install` deliberately does not add one: reaping has
to continue during a world pause, so an application drives this from the
iteration instead.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Audio` | |
| `dt` | `number?` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
#### Fields
##### `failureReason`
```nupp
failureReason: string?
```
Read-only. Reports terminal output loss observed by `update`. Nil until
a device fails. Loss clears voices, prevents playback and requires a new
mixer; there is no automatic reopen or replay on the default device.
##### `available`
```nupp
available: boolean
```
Read-only. Reports whether an output opened and remains usable as of
the last `update`. False also describes a device-free mixer.
### `Clip` _record_
```nupp
record Clip
path: string
id: integer
status: string
error: string?
duration: number
resident: boolean
end
```
Reports one shared playable sound after its load.
#### Fields
##### `path`
```nupp
path: string
```
Read-only. Reports the path the clip was loaded from, exactly as given.
This is a clip's identity, so two spellings of one file are two clips.
##### `id`
```nupp
id: integer
```
Read-only. Reports the index a [`Sound`](tecs.audio.Sound) carries.
##### `status`
```nupp
status: string
```
Read-only. Reports `"ready"`, `"failed"` or `"released"`. These words
reach an agent through the debug server's clip list, so they are a
compatibility surface rather than identifiers this tree renames.
`"released"` means the load succeeded and the owning mixer later
returned its samples.
##### `error`
```nupp
error: string?
```
Read-only. Reports the reason when loading failed, and nil otherwise.
##### `duration`
```nupp
duration: number
```
Read-only. Reports the audio duration in seconds, and zero when the
input cannot say.
##### `resident`
```nupp
resident: boolean
```
Read-only. Reports whether decoded audio stays in memory. False
identifies a clip each voice reads from the file for itself.
### `Config` _type_
```nupp
type Config = {
--- Caller-writable. Sets the sample frequency in frames per second, and
--- defaults to 48000.
frequency: integer?,
--- Caller-writable. Sets the number of output channels, and defaults to
--- two.
channels: integer?,
--- Caller-writable. Sets how many voices may sound at once, and defaults
--- to 32.
maxVoices: integer?,
--- Caller-writable. Sets the duration in seconds past which a clip streams
--- instead of staying resident, and defaults to ten.
streamSeconds: number?,
--- Caller-writable. Supplies the backend instead of opening the native
--- one, which is what a device-free test uses.
backend: audiobackend.Backend?
}
```
Configures `newAudio`.
### `Device` _record_
```nupp
record Device
id: integer
name: string
frequency: integer
channels: integer
end
```
Names one physical audio device.
Re-exported from the backend contract so a game reaches it as
`tecs.audio.Device`.
#### Fields
##### `id`
```nupp
id: integer
```
Read-only. Reports the device's position in the listing that produced
it, from one up. Zero is never assigned and means "the platform's
default" wherever a device is selected, so an id is only meaningful for
the listing it came from.
##### `name`
```nupp
name: string
```
Read-only. Reports the platform's display name for the device. This is
the only durable way to name a device, because an id moves when a device
is attached or removed.
##### `frequency`
```nupp
frequency: integer
```
Read-only. Reports the device's preferred frames per second, and zero
when it will not say.
##### `channels`
```nupp
channels: integer
```
Read-only. Reports the device's preferred channels per frame, and zero
when it will not say.
### `Limit` _type_
```nupp
type Limit = {
--- Caller-writable. Sets how many voices this key may hold at once. Zero
--- or absent removes the ceiling.
voices: integer?,
--- Caller-writable. Sets the cooldown in seconds after a voice starts.
--- Zero disables the cooldown.
cooldown: number?
}
```
Defines what one key allows.
### `LoadOptions` _type_
```nupp
type LoadOptions = {
--- Caller-writable. Forces streaming when true and residency when false.
--- Left unset, the clip's duration decides against `streamSeconds`.
stream: boolean?
}
```
Configures `Audio.load`.
### `Microphone` _record_
```nupp
record Microphone
frequency: integer
channels: integer
failure: function(borrows self: Microphone): string?
availableFrames: function(borrows self: Microphone): integer
overruns: function(borrows self: Microphone): integer
read: function(exclusive self: Microphone, maxFrames: integer?): (string?, string?)
readInto: function(exclusive self: Microphone, out: {number}, maxFrames: integer?): integer
pause: function(exclusive self: Microphone): (boolean, string?)
resume: function(exclusive self: Microphone): (boolean, string?)
destroy: function(exclusive self: Microphone): nil
end
```
An open recording device, pulled rather than pushed.
Capture is running by the time one of these exists and the backend's own
thread is filling a buffer behind it. Nothing arrives until `read` asks, and
nothing that thread runs enters Nupp: a device thread is one the Lua virtual
machine never created, so the frames cross by this side emptying a buffer
rather than by anything calling in.
The buffer is bounded. When a game stops reading, capture drops the oldest
frames and counts them in `overruns`, preserving the most recent audio
without unbounded memory or latency growth.
#### Methods
##### `failure`
```nupp
failure: function(borrows self: Microphone): string?
```
Read-only. Reports terminal capture failure, or nil while the stream is
healthy. Frames already buffered remain readable; after draining, `read`
returns nil and this reason, and `readInto` raises it. A zero-frame read
remains a no-op. Recovery requires closing and reopening the microphone.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Microphone` | |
###### Returns
| Type | Description |
| --- | --- |
| `string?` | |
##### `availableFrames`
```nupp
availableFrames: function(borrows self: Microphone): integer
```
Read-only. Reports the complete sample frames ready to read without
waiting, so a partly arrived frame is not counted. Zero once destroyed,
rather than an error.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Microphone` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `overruns`
```nupp
overruns: function(borrows self: Microphone): integer
```
Read-only. Reports the frames dropped because nothing read them in time,
counted since the microphone opened. Zero once destroyed.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows self` | `Microphone` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `read`
```nupp
read: function(exclusive self: Microphone, maxFrames: integer?): (string?, string?)
```
Read-only. Pulls up to `maxFrames` complete frames as interleaved
native-endian float32 samples, `channels * 4` bytes per frame. Omitting
`maxFrames` takes everything ready now, and a limit above what is ready
takes what is ready rather than waiting. An empty string means nothing
was ready and is not a failure. Returns nil and the reason for a
destroyed microphone or a backend failure, and raises when `maxFrames`
is not a non-negative integer.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Microphone` | |
| `maxFrames` | `integer?` | |
###### Returns
| Type | Description |
| --- | --- |
| `string?` | |
| `string?` | |
##### `readInto`
```nupp
readInto: function(exclusive self: Microphone, out: {number}, maxFrames: integer?): integer
```
Read-only. Pulls up to `maxFrames` complete frames into `out` as
interleaved samples from index one, writing no more frames than `out`
holds, and returns how many it wrote. This is the allocation-free form
of `read`, for a game draining a microphone every frame.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Microphone` | |
| `out` | `{number}` | |
| `maxFrames` | `integer?` | |
###### Returns
| Type | Description |
| --- | --- |
| `integer` | |
##### `pause`
```nupp
pause: function(exclusive self: Microphone): (boolean, string?)
```
Read-only. Stops the device filling the buffer, keeping whatever is
already in it. Pausing one already paused succeeds; a destroyed
microphone reports false with a reason.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Microphone` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
| `string?` | |
##### `resume`
```nupp
resume: function(exclusive self: Microphone): (boolean, string?)
```
Read-only. Starts the device filling the buffer again after `pause`. A
destroyed microphone reports false with a reason.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Microphone` | |
###### Returns
| Type | Description |
| --- | --- |
| `boolean` | |
| `string?` | |
##### `destroy`
```nupp
destroy: function(exclusive self: Microphone): nil
```
Read-only. Stops capture and closes the recording device. Safe more than
once. Whatever was captured and not yet read is discarded with the
buffer, so a last `read` belongs before this rather than after.
###### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive self` | `Microphone` | |
###### Returns
| Type | Description |
| --- | --- |
| `nil` | |
#### Fields
##### `frequency`
```nupp
frequency: integer
```
Read-only. Reports the frames per second `read` answers in. This is the
requested value, not the device's own.
##### `channels`
```nupp
channels: integer
```
Read-only. Reports the interleaved channels per frame `read` answers
with. This is the requested value, not the device's own.
### `MicrophoneConfig` _type_
```nupp
type MicrophoneConfig = {
--- Caller-writable. Selects a physical device by the `id` a
--- [`Device`](tecs.audio.Device) from `recordingDevices` reported. Omitted
--- or zero uses the system's current default. An id names a position in
--- the listing that produced it, so a game keeping a choice across runs
--- keeps `deviceName` instead.
device: integer?,
--- Caller-writable. Selects a physical device by the `name` a
--- [`Device`](tecs.audio.Device) reported, and wins over `device`. This is
--- the durable way to name a device, because a name survives a run and an
--- id moves when something is attached or removed.
deviceName: string?,
--- Caller-writable. Sets the frames per second `read` answers in, and
--- defaults to 48000. The backend converts the device's own rate, so this
--- is what arrives rather than what the hardware runs at.
frequency: integer?,
--- Caller-writable. Sets the interleaved channels `read` answers with, and
--- defaults to one. The backend folds or spreads the device's own channels
--- to reach it. The native backend carries at most eight, so a count above
--- that and inside the accepted range is refused with a reason rather than
--- raised.
channels: integer?,
--- Caller-writable. Sets how many frames the capture holds before the
--- oldest are dropped, and defaults to one second at `frequency`.
bufferFrames: integer?,
--- Caller-writable. Supplies the device provider instead of resolving the
--- native one, which is what a device-free test uses.
devices: audiobackend.Devices?
}
```
Configures `openMicrophone`.
### `PlayOptions` _type_
```nupp
type PlayOptions = {
--- Caller-writable. Sets linear gain before group and master gain, and
--- defaults to one.
gain: number?,
--- Caller-writable. Repeats playback until stopped, and defaults to false.
loop: boolean?,
--- Caller-writable. Sets the position in seconds a repeat returns to, so
--- an intro can play once and the rest of it loop. Defaults to zero.
loopStart: number?,
--- Caller-writable. Sets where the first pass begins in seconds, and
--- defaults to zero.
start: number?,
--- Caller-writable. Sets the fade-in duration in seconds, and defaults to
--- zero.
fadeIn: number?,
--- Caller-writable. Sets the playback rate, and defaults to one.
pitch: number?,
--- Caller-writable. Sets the fraction of `pitch` varied for each new
--- voice, so 0.1 spreads voices over plus or minus a tenth. Defaults to
--- zero.
pitchVariance: number?,
--- Caller-writable. Selects the group this voice joins, and defaults to
--- none.
group: string?,
--- Caller-writable. Selects the limit bucket this voice counts against,
--- and defaults to none.
key: string?,
--- Caller-writable. Enables spatial positioning. See
--- [`Sound`](tecs.audio.Sound) for the coordinate system.
spatial: boolean?,
--- Caller-writable. Sets the position right of the listener.
x: number?,
--- Caller-writable. Sets the position above the listener.
y: number?,
--- Caller-writable. Sets the position behind the listener.
z: number?,
--- Caller-writable. Pins the voice to the front pair of speakers, which is
--- a pan rather than a position. Ignored when `spatial` is set, because
--- the backend holds one placement per voice.
stereo: boolean?,
--- Caller-writable. Sets left-speaker gain on the same linear scale as
--- `gain`, and defaults to one.
left: number?,
--- Caller-writable. Sets right-speaker gain on the same terms as `left`.
right: number?
}
```
Configures `Audio.play`.
Read once, when the voice starts. Changing the record afterwards reaches
nothing: use the handle setters instead. Nothing here is retained, so one
record may be filled and reused for every play.
### `Sound` _struct_
```nupp
struct Sound
clip: integer
playing: boolean
gain: number
loop: boolean
pitch: number
spatial: boolean
x: number
y: number
z: number
group: integer
voice: integer
end
```
`@derive(nupp.derive.Debug, nupp.derive.Serde)`
Attaches a sound to an entity.
Presence is the instruction: an entity carrying this with a loaded clip
starts sounding on the next audio pass and stops when the component or the
entity goes away. That is what makes sound an entity rather than a handle a
game has to remember to release, and it is why despawning something
mid-sound does the obvious thing.
The audio pass sends position to the backend and does nothing else with it.
A caller feeding world coordinates has three jobs this component does not do
for it: subtract whatever it decided is listening, choose a scale between
world units and the backend's, and decide that a screen-space sound has no
world position at all and leaves `spatial` false.
The pass follows `playing`, `gain`, `loop`, `pitch` and the position for as
long as the voice sounds, so writing any of them is enough and nothing has
to restart the voice. `clip` and `group` are deliberately not followed:
both take a stop and a fresh start to change anyway, and following them
would put a read and a compare on every row that never does. Moving a sound
into another group is setting `group` and clearing `voice`.
Disabling an entity silences its sound and re-enabling starts it again.
#### Fields
##### `clip`
```nupp
clip: integer
```
Caller-writable. Selects a clip by its `clipId` index. Zero plays
nothing.
##### `playing`
```nupp
playing: boolean
```
Caller-writable. Requests playback. This field gives an instruction
rather than a report: clearing it stops the voice and setting it again
starts one, including on a one-shot that has already run out.
##### `gain`
```nupp
gain: number
```
Caller-writable. Sets linear gain before group and master gain.
##### `loop`
```nupp
loop: boolean
```
Caller-writable. Repeats playback. Clearing it part way through lets
the voice play out to its end rather than cutting it.
##### `pitch`
```nupp
pitch: number
```
Caller-writable. Sets the playback rate. One leaves it unchanged, while
two plays an octave higher in half the time.
##### `spatial`
```nupp
spatial: boolean
```
Caller-writable. Reads `x`, `y` and `z` when true, and mixes without a
position when false.
##### `x`
```nupp
x: number
```
Caller-writable. Sets the position right of the listener.
##### `y`
```nupp
y: number
```
Caller-writable. Sets the position above the listener, which is the
opposite sign from world Y.
##### `z`
```nupp
z: number
```
Caller-writable. Sets the position behind the listener.
##### `group`
```nupp
group: integer
```
Caller-writable. Selects a group by its `groupId` index. Zero joins no
group.
##### `voice`
```nupp
voice: integer
```
Engine-owned. Reports the voice the audio pass assigned: zero before it
starts and negative once a one-shot has finished. Ordinary game code
reads it and writes only zero, which is how it asks for the sound again.
### `VoiceInfo` _record_
```nupp
record VoiceInfo
handle: integer
clip: string?
gain: number
applied: number
pitch: number
group: string?
key: string?
paused: boolean
stopping: boolean
owned: boolean
loop: boolean
spatial: boolean
x: number
y: number
z: number
stereo: boolean
left: number
right: number
end
```
Describes one sounding voice for inspection.
#### Fields
##### `handle`
```nupp
handle: integer
```
Read-only. Reports the handle `playing` and `stop` accept.
##### `clip`
```nupp
clip: string?
```
Read-only. Reports the clip path, or nil after release.
##### `gain`
```nupp
gain: number
```
Read-only. Reports the gain the voice requested, before group and
master gain.
##### `applied`
```nupp
applied: number
```
Read-only. Reports the gain sent for the voice, group gain and group
mute included. Master gain is not in it: that is one number on the
output rather than something multiplied per voice.
##### `pitch`
```nupp
pitch: number
```
Read-only. Reports the playback rate sent, so the value already
includes whatever variance `play` drew.
##### `group`
```nupp
group: string?
```
Read-only. Reports the group name, or nil for no group.
##### `key`
```nupp
key: string?
```
Read-only. Reports the limit bucket, or nil for no bucket.
##### `paused`
```nupp
paused: boolean
```
Read-only. Reports whether its own pause or its group holds it.
##### `stopping`
```nupp
stopping: boolean
```
Read-only. Reports whether a fade-out is stopping the voice.
##### `owned`
```nupp
owned: boolean
```
Read-only. Reports whether a [`Sound`](tecs.audio.Sound) component
started the voice instead of `play`.
##### `loop`
```nupp
loop: boolean
```
Read-only. Reports whether playback repeats at the end.
##### `spatial`
```nupp
spatial: boolean
```
Read-only. Reports whether the voice carries a position. This stays
false when `stereo` does not, because a voice holds one placement.
##### `x`
```nupp
x: number
```
Read-only. Reports the last horizontal position sent, positive right.
It has no meaning unless `spatial`.
##### `y`
```nupp
y: number
```
Read-only. Reports the last vertical position sent, positive up. It has
no meaning unless `spatial`.
##### `z`
```nupp
z: number
```
Read-only. Reports the last depth position sent, positive behind. It
has no meaning unless `spatial`.
##### `stereo`
```nupp
stereo: boolean
```
Read-only. Reports whether the voice is pinned to the front pair.
##### `left`
```nupp
left: number
```
Read-only. Reports the last left-speaker gain sent. It has no meaning
unless `stereo`.
##### `right`
```nupp
right: number
```
Read-only. Reports the last right-speaker gain sent. It has no meaning
unless `stereo`.
## Functions
### `clipId` _function_
```nupp
function clipId(path: string): integer
```
Returns the index of a clip path, assigning one the first time it is seen.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `path` | `string` | the non-empty path, which is not checked against the filesystem |
#### Returns
| Type | Description |
| --- | --- |
| `integer` | an index from one up, shared by every mixer in the process and meaningless in a file, which is why `Sound` serializes the path |
#### Raises
- when the path is empty
### `clipPath` _function_
```nupp
function clipPath(id: integer): string?
```
Returns the path a clip index represents.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `id` | `integer` | the index, where zero is the index of no clip at all |
#### Returns
| Type | Description |
| --- | --- |
| `string?` | the path, or nil for zero and for an index never handed out |
### `groupId` _function_
```nupp
function groupId(name: string): integer
```
Returns the index of a group name, assigning one the first time it is seen.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `name` | `string` | the non-empty name, which needs no declaring: naming one here is all it takes for the group to exist |
#### Returns
| Type | Description |
| --- | --- |
| `integer` | an index from one up, shared by every mixer in the process |
#### Raises
- when the name is empty
### `groupName` _function_
```nupp
function groupName(id: integer): string?
```
Returns the name a group index represents.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `id` | `integer` | the index, where zero is the index of no group at all |
#### Returns
| Type | Description |
| --- | --- |
| `string?` | the name, or nil for zero and for an index never handed out |
### `install` _function_
```nupp
function install(exclusive world: ecs.World, config: Config?): Audio
```
Creates a mixer, installs it into a world, and returns it.
Adds the system that plays [`Sound`](tecs.audio.Sound) components and the
snapshot handler that carries master and group settings.
Pitch variance uses the world's named tecs.audio random stream, so reseeding
and snapshot restore also restart future pitch draws. Standalone mixers made
with newAudio keep an independent Nupp generator.
`update` is not added here. Reaping voices is not world work: it has to
continue during a world pause, so an application drives it from the
iteration instead.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `exclusive world` | `ecs.World` | the world to install into, which expects one mixer |
| `config` | `Config?` | the mixer settings, or nil for the defaults |
#### Returns
| Type | Description |
| --- | --- |
| `Audio` | the new mixer, also available through `of` |
#### Raises
- when the world already has a mixer installed
### `of` _function_
```nupp
function of(borrows world: ecs.World): Audio?
```
Returns the mixer installed into a world.
What lets something holding only the world reach the mixer, which is what
the debug tools have and what a game writing its own systems often has too.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `borrows world` | `ecs.World` | the world to inspect |
#### Returns
| Type | Description |
| --- | --- |
| `Audio?` | the mixer, or nil before `install` and again after `destroy` |
### `openMicrophone` _function_
```nupp
function openMicrophone(config: MicrophoneConfig?): Microphone?, string?
```
Opens a microphone as interleaved native-endian 32-bit float samples.
No callback is installed anywhere. The backend's own thread fills a bounded
buffer and the game pulls completed frames from the frame thread with
[`Microphone.read`](tecs.audio.Microphone), because a device thread is one
the Lua virtual machine never created and entering Nupp from it is
undefined.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `config` | `MicrophoneConfig?` | the settings, or nil to open the system's current default recording device at 48000 Hz in mono with a one-second buffer |
#### Returns
| Type | Description |
| --- | --- |
| `Microphone?` | an open microphone, already recording: capture is running before this returns, so frames accumulate from here whether or not anything reads them. Nil on failure, with the reason beside it, and nothing is left open in that case. |
| `string?` | the reason, when the first return is nil |
#### Raises
- when `frequency` is not a positive integer, when `channels` is not an integer from 1 to 32, or when `bufferFrames` is not a positive integer
### `playbackDevices` _function_
```nupp
function playbackDevices(devices: audiobackend.Devices?): {Device}, string?
```
Names the playback devices attached now.
A snapshot rather than a subscription: devices come and go while a game
runs, so an id held across a hotplug may name nothing. A
[`Device`](tecs.audio.Device) `name` is what survives a run.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `devices` | `audiobackend.Devices?` | the provider to ask, or nil for the platform's own |
#### Returns
| Type | Description |
| --- | --- |
| `{Device}` | the devices, listed afresh each call and the caller's to keep. Empty rather than nil when nothing could be asked, so a caller that only iterates needs no nil check. |
| `string?` | the reason, when something went wrong. Nil on success, including for a machine that genuinely has no playback device. |
### `recordingDevices` _function_
```nupp
function recordingDevices(devices: audiobackend.Devices?): {Device}, string?
```
Names the recording devices attached now.
#### Arguments
| Name | Type | Description |
| --- | --- | --- |
| `devices` | `audiobackend.Devices?` | the provider to ask, or nil for the platform's own |
#### Returns
| Type | Description |
| --- | --- |
| `{Device}` | the devices, read as `playbackDevices` reads its own |
| `string?` | the reason, when something went wrong |
## Values
### `SoundComponent` _variable_
```nupp
const SoundComponent: ecs.ComponentDefinition
```
The process-wide `Sound` component definition.
Its saved fields carry the clip path and the group name rather than their
indices, because an index belongs to the run that handed it out and a file
carrying one would name whatever the next run interned in its place. The
voice is not saved: a snapshot records that an entity has a sound, not how
far through it the backend had got, so it starts again.