# `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.