# tecs.audio Clips, voices, groups, limits, and entity-owned sound. [`Application`](/modules/Application/) exposes one [`Audio`](/modules/audio/#tecs.audio.Audio) as `app.audio`. A platform without an output device still provides the object, but `available` reports false and playback returns handle zero. ## Playback `load` returns a [`Future`](/modules/Future/) for one shared clip per path. Starting the load never blocks the frame. Reading a pending future's `value` does, so that dereference is for startup, tools, and tests rather than a frame. ```teal local step = app.audio:load("assets/sfx/step.ogg").value app.audio:setLimit( "footstep", { voices = 3, cooldown = 0.05, } ) local voice = app.audio:play( step, { gain = 0.7, group = "sfx", key = "footstep", pitchVariance = 0.1, } ) if voice ~= 0 then app.audio:stop(voice, 0.2) end ``` Clips shorter than `streamSeconds` stay resident and share decoded samples. Longer clips stream separately for each voice. `LoadOptions.stream` overrides that choice. Use `Audio.decoders()` when a game needs to report the formats available in the current build. A stale voice handle never controls a later voice that reuses its slot. Commands ignore stale handles and queries return false or nil. ## Groups and limits A group controls gain, mute, pause, resume, and stop. Its settings also apply to voices that join later. A key controls admission through a concurrent voice limit and cooldown. One voice may use both. Snapshots retain master and group settings. Pitch variance uses the world's snapshotted `tecs.audio` random stream. ## Placement `setPosition` uses right-handed coordinates with a listener fixed at the origin. The game chooses the listener, subtracts its position, and converts world units to an audio scale. `setStereo` assigns explicit left and right gains. The latest placement call replaces the earlier mode. ## Entity-owned sound An entity can carry [`Sound`](/modules/audio/#tecs.audio.Sound) instead of keeping a voice handle. The audio system starts it, follows writable playback fields, and stops it when the component or entity disappears. Write through `world:getMut`. A direct FFI write needs `world:markComponentDirty`, and `batchSpawn` must initialize every field. ## Module contents ### Constructors | Constructor | Description | | --- | --- | | [`newAudio`](/modules/audio/#tecs.audio.newAudio) | Builds the mixer a game plays sound through, opening the platform's default output. | ### Types | Type | Kind | Description | | --- | --- | --- | | [`Audio`](/modules/audio/#tecs.audio.Audio) | record | Represents one audio output and the voices sounding on it. | | [`Clip`](/modules/audio/#tecs.audio.Clip) | type | A loaded clip, or one still loading. | | [`Config`](/modules/audio/#tecs.audio.Config) | type | Configures newAudio. | | [`Device`](/modules/audio/#tecs.audio.Device) | record | Describes a physical device before the game opens it. | | [`Limit`](/modules/audio/#tecs.audio.Limit) | type | Defines how many voices a key allows and how soon it may repeat. | | [`LoadOptions`](/modules/audio/#tecs.audio.LoadOptions) | type | Configures Audio:load. | | [`Microphone`](/modules/audio/#tecs.audio.Microphone) | record | An open recording device, read by polling. | | [`MicrophoneConfig`](/modules/audio/#tecs.audio.MicrophoneConfig) | record | Configures openMicrophone. | | [`PlayOptions`](/modules/audio/#tecs.audio.PlayOptions) | type | Configures Audio:play. | | [`Sound`](/modules/audio/#tecs.audio.Sound) | record | Represents a sound attached to an entity. | | [`VoiceInfo`](/modules/audio/#tecs.audio.VoiceInfo) | type | Describes one voice returned by Audio:voices. | ### Functions | Function | Kind | Description | | --- | --- | --- | | [`openMicrophone`](/modules/audio/#tecs.audio.openMicrophone) | Static | Opens a microphone as interleaved native-endian 32-bit float samples. | | [`playbackDevices`](/modules/audio/#tecs.audio.playbackDevices) | Static | Returns the physical playback devices attached now. | | [`recordingDevices`](/modules/audio/#tecs.audio.recordingDevices) | Static | Returns the physical recording devices attached now. | ## Constructors ### tecs.audio.newAudio Static Builds the mixer a game plays sound through, opening the platform's default output. 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. ```teal function tecs.audio.newAudio(config: Audio.Config): Audio ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `config` | [`Audio.Config`](/modules/audio/#tecs.audio.Audio.Config) | Omitted takes the defaults. `maxVoices` outside 1 to 65535 raises, which is the one field here that does; every other one out of range is the platform's to refuse. | #### Returns | Type | Description | | --- | --- | | [`Audio`](/modules/audio/#tecs.audio.Audio) | The mixer, which the caller has to `destroy`. Check `available` for whether an output actually opened; it is false on a machine with no sound, and every call still succeeds and is silent. | ## Types ### tecs.audio.Audio record Represents one audio output and the voices sounding on it. 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. Milliseconds appear only below the platform seam. Every fade is a duration to run for, never a moment to finish at, so calling one twice restarts it rather than moving a deadline. * **Gain is linear amplitude, not decibels.** 0 is silence and 1 is the sound as recorded. Above 1 is louder and may clip. The level a voice reaches the hardware at is its own gain times its group's gain times the master gain, and a mute at either level composes as a zero without discarding the gain underneath it. * **Pitch is a playback rate, not an interval.** 1 preserves the original rate, 2 is an octave up and half as long, 0.5 an octave down and twice as long. It resamples, so it changes duration; there is no time-stretch here. * **Position is the mixer's frame, not the world's.** The mixer fixes the listener at the origin. x is positive to the right, y is positive **up**, and z is positive behind. World Y runs down, so a caller feeding world coordinates in has to flip Y as well as subtract whatever it decided is listening. See [`Sound`](/modules/audio/#tecs.audio.Sound). A voice handle is a generation and a slot packed into one integer. It stays meaningful for exactly as long as the voice sounds. When a voice ends naturally or either `stop` or `update` ends it, every call taking the handle becomes a no-op, and it never starts naming the next voice to take that slot. Nothing requires release, and holding a stale handle is not an error. Nothing here is thread safe, and nothing here calls back. Everything runs on the thread that calls it. ```teal record tecs.audio.Audio record Config frequency: integer channels: integer maxVoices: integer streamSeconds: number backend: Backend device: number end record Clip path: string id: integer status: string error: string duration: number resident: boolean end record LoadOptions stream: boolean end record PlayOptions gain: number loop: boolean loopStart: number start: number fadeIn: number pitch: number pitchVariance: number group: string key: string spatial: boolean x: number y: number z: number stereo: boolean left: number right: number end record Limit voices: integer cooldown: number end 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 record Sound is Component clip: number playing: number gain: number loop: number pitch: number spatial: number x: number y: number z: number group: number voice: number end available: boolean clipId: function(path: string): integer clipPath: function(id: integer): string decoders: function(): {string} groupId: function(name: string): integer groupName: function(id: integer): string of: function(world: types.World): Audio clearSpatial: function(self, handle: integer) clip: function(self, id: integer): Audio.Clip clips: function(self): {Audio.Clip} destroy: function(self) groupGain: function(self, name: string): number groupMuted: function(self, name: string): boolean groupPaused: function(self, name: string): boolean groups: function(self): {string} install: function(self, world: types.World) keyCount: function(self, key: string): integer keys: function(self): {string} limit: function(self, key: string): Audio.Limit load: function( self, path: string, options: Audio.LoadOptions ): Future loading: function(self): integer looping: function(self, handle: integer): boolean masterGain: function(self): number maxVoices: function(self): integer muted: function(self): boolean pause: function(self, handle: integer) paused: function(self, handle: integer): boolean pauseGroup: function(self, name: string) play: function( self, clip: Audio.Clip, options: Audio.PlayOptions ): integer playing: function(self, handle: integer): boolean reload: function(self, path: string): boolean, string resume: function(self, handle: integer) resumeGroup: function(self, name: string) seek: function(self, handle: integer, seconds: number): boolean setGain: function(self, handle: integer, gain: number) setGroupGain: function(self, name: string, gain: number) setGroupMuted: function(self, name: string, muted: boolean) setLimit: function(self, key: string, limit: Audio.Limit) setLoop: function(self, handle: integer, loop: boolean) setMasterGain: function(self, gain: number) setMuted: function(self, muted: boolean) setPitch: function(self, handle: integer, ratio: number) setPosition: function( self, handle: integer, x: number, y: number, z: number ) setStereo: function( self, handle: integer, left: number, right: number ) sounding: function(self): integer stop: function(self, handle: integer, fadeOut: number) stopAll: function(self, fadeOut: number) stopGroup: function(self, name: string, fadeOut: number) tell: function(self, handle: integer): number update: function(self, dt: number): integer voices: function(self): {Audio.VoiceInfo} waitForLoads: function(self, timeoutMs: number) end ``` #### tecs.audio.Audio.Config record Configures `newAudio` through optional fields. ```teal record tecs.audio.Audio.Config frequency: integer channels: integer maxVoices: integer streamSeconds: number backend: Backend device: number end ``` ##### tecs.audio.Audio.Config.frequency field Caller-writable. Sets the sample frequency in frames per second and defaults to 48000. ```teal tecs.audio.Audio.Config.frequency: integer ``` ##### tecs.audio.Audio.Config.channels field Caller-writable. Sets the number of output channels and defaults to two. ```teal tecs.audio.Audio.Config.channels: integer ``` ##### tecs.audio.Audio.Config.maxVoices field Caller-writable. Sets how many voices may sound at once and defaults to 32. ```teal tecs.audio.Audio.Config.maxVoices: integer ``` ##### tecs.audio.Audio.Config.streamSeconds field Caller-writable. Sets the duration in seconds at which a clip streams instead of remaining resident. Defaults to 10. ```teal tecs.audio.Audio.Config.streamSeconds: number ``` ##### tecs.audio.Audio.Config.backend field Caller-writable. Selects the output backend and defaults to the installed platform backend. ```teal tecs.audio.Audio.Config.backend: Backend ``` ##### tecs.audio.Audio.Config.device field Caller-writable. Selects a physical output device by an id from `tecs.audio.playbackDevices`. Omitted follows the operating-system default as it changes. ```teal tecs.audio.Audio.Config.device: number ``` #### tecs.audio.Audio.Clip record Represents one shared playable sound after its load. `load` settles a future with this only after reading succeeds. `clip` also exposes the cached instance while its load is pending or after it fails, which is why the status and error remain visible. Every load for the same path shares one instance. ```teal record tecs.audio.Audio.Clip path: string id: integer status: string error: string duration: number resident: boolean end ``` ##### tecs.audio.Audio.Clip.path field Read-only. Reports the path used to load the clip, exactly as given. This is a clip's identity, so two spellings of one file are two clips. ```teal tecs.audio.Audio.Clip.path: string ``` ##### tecs.audio.Audio.Clip.id field Read-only. Reports the `path` index carried by a [`Sound`](/modules/audio/#tecs.audio.Sound) component. ```teal tecs.audio.Audio.Clip.id: integer ``` ##### tecs.audio.Audio.Clip.status field Read-only. Reports `"pending"` while the loader reads the file, then `"ready"` or `"failed"`, and `"released"` once the owning [`Audio`](/modules/audio/#tecs.audio.Audio) is destroyed. These four words reach an agent over JSON-RPC, through the `audio` debug tool's clip list, so they are a compatibility surface rather than identifiers this tree renames in one commit. The first three are [`Future`](/modules/Future/)'s, because a clip's first three states are its load's. `"released"` has no counterpart there: a released clip is a load that succeeded and then had its samples given back, and a future has no word for that because a future ends at the answer. `"canceled"` never appears here, because the only thing that gives up a clip's load is `destroy`, which has written `"released"` by then. ```teal tecs.audio.Audio.Clip.status: string ``` ##### tecs.audio.Audio.Clip.error field Read-only. Reports the error when loading fails. ```teal tecs.audio.Audio.Clip.error: string ``` ##### tecs.audio.Audio.Clip.duration field Read-only. Reports the audio duration in seconds, or zero when the file cannot provide one. ```teal tecs.audio.Audio.Clip.duration: number ``` ##### tecs.audio.Audio.Clip.resident field Read-only. Reports whether decoded audio remains in memory. False identifies a clip each voice reads from the file for itself. ```teal tecs.audio.Audio.Clip.resident: boolean ``` #### tecs.audio.Audio.LoadOptions record Configures `load` through one optional field. ```teal record tecs.audio.Audio.LoadOptions stream: boolean end ``` ##### tecs.audio.Audio.LoadOptions.stream field Caller-writable. Forces streaming when true and residency when false. Left unset, the clip's duration decides against `streamSeconds`. ```teal tecs.audio.Audio.LoadOptions.stream: boolean ``` #### tecs.audio.Audio.PlayOptions record Configures `play` through optional fields. The whole table may be omitted. Read once, when the voice starts. Changing the table afterwards reaches nothing: use `setGain`, `setPitch`, `setLoop`, `setPosition` and `setStereo` on the handle instead. The table is not retained, so one a caller may fill and reuse one table for every play. ```teal record tecs.audio.Audio.PlayOptions gain: number loop: boolean loopStart: number start: number fadeIn: number pitch: number pitchVariance: number group: string key: string spatial: boolean x: number y: number z: number stereo: boolean left: number right: number end ``` ##### tecs.audio.Audio.PlayOptions.gain field Caller-writable. Sets linear gain from zero to one before group and master gains. Defaults to one. ```teal tecs.audio.Audio.PlayOptions.gain: number ``` ##### tecs.audio.Audio.PlayOptions.loop field Caller-writable. Repeats playback until stopped and defaults to false. ```teal tecs.audio.Audio.PlayOptions.loop: boolean ``` ##### tecs.audio.Audio.PlayOptions.loopStart field Caller-writable. Sets the position in seconds to which a repeat returns, so an intro can play once and the rest of it loop. Defaults to 0. ```teal tecs.audio.Audio.PlayOptions.loopStart: number ``` ##### tecs.audio.Audio.PlayOptions.start field Caller-writable. Sets where the first pass begins in seconds and defaults to zero. ```teal tecs.audio.Audio.PlayOptions.start: number ``` ##### tecs.audio.Audio.PlayOptions.fadeIn field Caller-writable. Sets the fade-in duration in seconds and defaults to zero. ```teal tecs.audio.Audio.PlayOptions.fadeIn: number ``` ##### tecs.audio.Audio.PlayOptions.pitch field Caller-writable. Sets playback rate and defaults to one. ```teal tecs.audio.Audio.PlayOptions.pitch: number ``` ##### tecs.audio.Audio.PlayOptions.pitchVariance field Caller-writable. Sets the fraction of `pitch` varied for each new voice. A value of 0.1 spreads voices over plus or minus a tenth. Defaults to 0. ```teal tecs.audio.Audio.PlayOptions.pitchVariance: number ``` ##### tecs.audio.Audio.PlayOptions.group field Caller-writable. Selects the group this voice joins and defaults to none. ```teal tecs.audio.Audio.PlayOptions.group: string ``` ##### tecs.audio.Audio.PlayOptions.key field Caller-writable. Selects the limit bucket counted by this voice and defaults to none. ```teal tecs.audio.Audio.PlayOptions.key: string ``` ##### tecs.audio.Audio.PlayOptions.spatial field Caller-writable. Enables spatial positioning. See [`Sound`](/modules/audio/#tecs.audio.Sound) for the coordinate system. ```teal tecs.audio.Audio.PlayOptions.spatial: boolean ``` ##### tecs.audio.Audio.PlayOptions.x field Caller-writable. Sets the position right of the listener and defaults to zero. ```teal tecs.audio.Audio.PlayOptions.x: number ``` ##### tecs.audio.Audio.PlayOptions.y field Caller-writable. Sets the position above the listener, which uses the opposite sign from world Y. Defaults to 0. ```teal tecs.audio.Audio.PlayOptions.y: number ``` ##### tecs.audio.Audio.PlayOptions.z field Caller-writable. Sets the position behind the listener and defaults to zero. ```teal tecs.audio.Audio.PlayOptions.z: number ``` ##### tecs.audio.Audio.PlayOptions.stereo field Caller-writable. Pins the voice to the front pair of speakers at `left` and `right`, which is a pan rather than a position. Ignored when `spatial` is set, because the mixer holds one placement per track. Both gains default to 1. ```teal tecs.audio.Audio.PlayOptions.stereo: boolean ``` ##### tecs.audio.Audio.PlayOptions.left field Caller-writable. Sets left-speaker gain on the same linear scale as `gain`. Negative reads as silence and above 1 is louder. Defaults to 1. ```teal tecs.audio.Audio.PlayOptions.left: number ``` ##### tecs.audio.Audio.PlayOptions.right field Caller-writable. Sets right-speaker gain on the same terms as `left`. ```teal tecs.audio.Audio.PlayOptions.right: number ``` #### tecs.audio.Audio.Limit record Defines what a key allows. ```teal record tecs.audio.Audio.Limit voices: integer cooldown: number end ``` ##### tecs.audio.Audio.Limit.voices field Caller-writable. Sets how many voices this key may hold at once. Zero or absent removes the ceiling. ```teal tecs.audio.Audio.Limit.voices: integer ``` ##### tecs.audio.Audio.Limit.cooldown field Caller-writable. Sets the cooldown in seconds after a voice starts. Zero disables the cooldown. ```teal tecs.audio.Audio.Limit.cooldown: number ``` #### tecs.audio.Audio.VoiceInfo record Describes one sounding voice for inspection. ```teal record tecs.audio.Audio.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 ``` ##### tecs.audio.Audio.VoiceInfo.handle field Read-only. Reports the handle accepted by `playing` and `stop`. ```teal tecs.audio.Audio.VoiceInfo.handle: integer ``` ##### tecs.audio.Audio.VoiceInfo.clip field Read-only. Reports the clip path, or nil after release. ```teal tecs.audio.Audio.VoiceInfo.clip: string ``` ##### tecs.audio.Audio.VoiceInfo.gain field Read-only. Reports the gain requested by the voice before group and master gain. ```teal tecs.audio.Audio.VoiceInfo.gain: number ``` ##### tecs.audio.Audio.VoiceInfo.applied field Read-only. Reports the gain applied to the track, including group gain and mute. The master gain is not in it: that is the mixer's own number and is not multiplied per voice. ```teal tecs.audio.Audio.VoiceInfo.applied: number ``` ##### tecs.audio.Audio.VoiceInfo.pitch field Read-only. Reports the playback rate carried by the track, so the pitch variance a value already includes any variance that `play` drew rather than only the requested rate. ```teal tecs.audio.Audio.VoiceInfo.pitch: number ``` ##### tecs.audio.Audio.VoiceInfo.group field Read-only. Reports the group tag, or nil for no group. ```teal tecs.audio.Audio.VoiceInfo.group: string ``` ##### tecs.audio.Audio.VoiceInfo.key field Read-only. Reports the limit bucket, or nil for no bucket. ```teal tecs.audio.Audio.VoiceInfo.key: string ``` ##### tecs.audio.Audio.VoiceInfo.paused field Read-only. Reports whether its own pause or its group holds it. ```teal tecs.audio.Audio.VoiceInfo.paused: boolean ``` ##### tecs.audio.Audio.VoiceInfo.stopping field Read-only. Reports whether a fade-out is stopping the voice. ```teal tecs.audio.Audio.VoiceInfo.stopping: boolean ``` ##### tecs.audio.Audio.VoiceInfo.owned field Read-only. Reports whether a [`Sound`](/modules/audio/#tecs.audio.Sound) component started the voice instead of `play`. ```teal tecs.audio.Audio.VoiceInfo.owned: boolean ``` ##### tecs.audio.Audio.VoiceInfo.loop field Read-only. Reports whether playback repeats at the end. ```teal tecs.audio.Audio.VoiceInfo.loop: boolean ``` ##### tecs.audio.Audio.VoiceInfo.spatial field Read-only. Reports whether the voice has a spatial position. This remains false when `stereo`: the mixer holds one placement per track. ```teal tecs.audio.Audio.VoiceInfo.spatial: boolean ``` ##### tecs.audio.Audio.VoiceInfo.x field Read-only. Reports the last horizontal position pushed in the mixer's coordinate system, positive right. Meaningless unless `spatial`. ```teal tecs.audio.Audio.VoiceInfo.x: number ``` ##### tecs.audio.Audio.VoiceInfo.y field Read-only. Reports the last vertical position pushed, positive up. It has no meaning unless `spatial`. ```teal tecs.audio.Audio.VoiceInfo.y: number ``` ##### tecs.audio.Audio.VoiceInfo.z field Read-only. Reports the last depth position pushed, positive behind. It has no meaning unless `spatial`. ```teal tecs.audio.Audio.VoiceInfo.z: number ``` ##### tecs.audio.Audio.VoiceInfo.stereo field Read-only. Reports whether the voice remains pinned to the front speaker pair. ```teal tecs.audio.Audio.VoiceInfo.stereo: boolean ``` ##### tecs.audio.Audio.VoiceInfo.left field Read-only. Reports the last left-speaker gain pushed. It has no meaning unless `stereo`. ```teal tecs.audio.Audio.VoiceInfo.left: number ``` ##### tecs.audio.Audio.VoiceInfo.right field Read-only. Reports the last right-speaker gain pushed. It has no meaning unless `stereo`. ```teal tecs.audio.Audio.VoiceInfo.right: number ``` #### tecs.audio.Audio.Sound record Represents a sound attached 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 mixer and does nothing else with it. `spatial` switches it on, and the mixer reads `x`, `y` and `z` in a right-handed system. The mixer fixes its listener at the origin, with x positive to the right, y positive up and z positive behind. So a caller with a camera and a world has three jobs this component does not do for it, and doing any of them here would fix answers that belong to a game: 1. Subtract the listener. There is no listener component and no rule that the camera is one, so whatever a game decides is listening is what it subtracts before writing these fields. 2. Choose a scale. The mixer attenuates with distance on its own, and how loud a sound a hundred world units away should be is a question about a game's units, not about audio. 3. Decide what a screen-space sound means. A sound on a layer that does not move with the camera has no world position to convert, and the answer is to leave `spatial` at zero. A name identifies a group, just as a path identifies a clip. An FFI component holds numbers, so what `group` carries is an interned index from `Audio.groupId` rather than the mixer's tag itself, and `Audio.groupName` reads it back. The index is this run's and no other, which is why `serialize` writes the name: an integer in a save file would name a different group the next time a build interned its names in a different order. The audio 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 to apply a change. Each of those costs one read and compare per sounding row per frame, which is the price of the component being live and is why `clip` and `group` are not on the list: both take an untag, a retag and a fresh input to change, neither is something a game writes often, and following them would put that cost on every row that never does. Moving a sound into another group is setting `group` and clearing `voice`, which is the same thing changing its clip takes. Disabling an entity silences its sound and re-enabling starts it again. Queries exclude [`Disabled`](/modules/ecs/#tecs.ecs.Disabled) unless they name it, so a disabled row is not followed and its voice is taken back; the audio pass clears the handle at the same moment, which is what lets the sound return with the entity rather than reading as a one-shot that had finished. Read-only. Exposes the [`Sound`](/modules/audio/#tecs.audio.Sound) component for queries and writes. See the record's own documentation for what its fields mean. ```teal record tecs.audio.Audio.Sound is Component clip: number playing: number gain: number loop: number pitch: number spatial: number x: number y: number z: number group: number voice: number end ``` ##### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | ##### tecs.audio.Audio.Sound.clip field Caller-writable. Selects a clip by its `Audio.clipId` index. Zero plays nothing. ```teal tecs.audio.Audio.Sound.clip: number ``` ##### tecs.audio.Audio.Sound.playing field Caller-writable. Uses nonzero to request playback and zero to stop. 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. ```teal tecs.audio.Audio.Sound.playing: number ``` ##### tecs.audio.Audio.Sound.gain field Caller-writable. Sets linear gain from zero to one before group and master gain. ```teal tecs.audio.Audio.Sound.gain: number ``` ##### tecs.audio.Audio.Sound.loop field Caller-writable. Uses nonzero to repeat. Clearing it part way through lets the voice play out to its end rather than cutting it. ```teal tecs.audio.Audio.Sound.loop: number ``` ##### tecs.audio.Audio.Sound.pitch field Caller-writable. Sets playback rate. One leaves it unchanged, while two plays an octave higher in half the time. ```teal tecs.audio.Audio.Sound.pitch: number ``` ##### tecs.audio.Audio.Sound.spatial field Caller-writable. Uses nonzero to read `x`, `y`, and `z`; zero mixes without a position. ```teal tecs.audio.Audio.Sound.spatial: number ``` ##### tecs.audio.Audio.Sound.x field Caller-writable. Sets the position right of the listener. ```teal tecs.audio.Audio.Sound.x: number ``` ##### tecs.audio.Audio.Sound.y field Caller-writable. Sets the position above the listener. ```teal tecs.audio.Audio.Sound.y: number ``` ##### tecs.audio.Audio.Sound.z field Caller-writable. Sets the position behind the listener. ```teal tecs.audio.Audio.Sound.z: number ``` ##### tecs.audio.Audio.Sound.group field Caller-writable. Selects a group by its `Audio.groupId` index. Zero joins no group. ```teal tecs.audio.Audio.Sound.group: number ``` ##### tecs.audio.Audio.Sound.voice field Engine-owned. Reports the voice assigned by the audio pass: zero before it starts and negative once a one-shot has finished. Written by the engine; setting it back to zero is how a game asks for the sound again. ```teal tecs.audio.Audio.Sound.voice: number ``` #### tecs.audio.Audio.available field Read-only. Reports whether an output opened. False identifies a machine with no sound, where every call here still works and nothing is heard. ```teal tecs.audio.Audio.available: boolean ``` #### tecs.audio.Audio.clipId Static Returns the index of a clip path and assigns one on first use. ```teal function tecs.audio.Audio.clipId(path: string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | `string` | Must be non-empty; an empty one raises. Not checked against the filesystem, so this hands out an index for a path that does not exist. | ##### Returns | Type | Description | | --- | --- | | `integer` | An index from 1 up, shared by every [`Audio`](/modules/audio/#tecs.audio.Audio) in the process. It belongs only to this run and means nothing in a file, which is why [`Sound`](/modules/audio/#tecs.audio.Sound) serializes the path instead. | #### tecs.audio.Audio.clipPath Static Returns the path represented by a clip index, or nil. ```teal function tecs.audio.Audio.clipPath(id: integer): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | Zero is the index of no clip and answers nil, as does any index never handed out. | ##### Returns | Type | Description | | --- | --- | | `string` | | #### tecs.audio.Audio.decoders Static Returns the decoders linked into this build in mixer order. What a build asked for and what it got are different questions: a decoder The mixer silently omits a decoder whose dependency the build could not find. This function answers the second question. ```teal function tecs.audio.Audio.decoders(): {string} ``` ##### Arguments None. ##### Returns | Type | Description | | --- | --- | | `{string}` | Decoder names such as `"WAV"` and `"OGG"`, in the mixer's own order rather than sorted, and empty when the mixer will not start at all. A fresh table each call. | #### tecs.audio.Audio.groupId Static Returns the index of a group name and assigns one on first use. What a [`Sound`](/modules/audio/#tecs.audio.Sound) carries in `group`. The index means nothing outside the run that handed it out, so it belongs in a component and never in a file. ```teal function tecs.audio.Audio.groupId(name: string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `name` | `string` | Must be non-empty; an empty one raises. A group needs no declaring: naming one here is all it takes to exist. | ##### Returns | Type | Description | | --- | --- | | `integer` | An index from 1 up, shared by every [`Audio`](/modules/audio/#tecs.audio.Audio) in the process. | #### tecs.audio.Audio.groupName Static Returns the name represented by a group index, or nil. ```teal function tecs.audio.Audio.groupName(id: integer): string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `id` | `integer` | Zero is the index of no group and answers nil, which is what a [`Sound`](/modules/audio/#tecs.audio.Sound) in no group carries. | ##### Returns | Type | Description | | --- | --- | | `string` | | #### tecs.audio.Audio.of Static Returns the audio installed into a world, or nil. 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. ```teal function tecs.audio.Audio.of(world: types.World): Audio ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `world` | [`types.World`](/modules/ecs/#tecs.World) | | ##### Returns | Type | Description | | --- | --- | | [`Audio`](/modules/audio/#tecs.audio.Audio) | nil before `install` and again after `destroy`, which removes the instance from every world. | #### tecs.audio.Audio:clearSpatial Instance Returns a voice to unpositioned mixing, out of either placement. One call answers for both, because clearing either mode in the mixer clears the other. A no-op for a handle that names nothing and for a voice that was never placed. ```teal function tecs.audio.Audio.clearSpatial(self, handle: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | ##### Returns None. #### tecs.audio.Audio:clip Instance Returns the clip represented by an index, or nil when this instance has not loaded it. ```teal function tecs.audio.Audio.clip(self, id: integer): Audio.Clip ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `id` | `integer` | From `Audio.clipId`, or from a `Sound.clip` field. Clip indices are every instance shares clip indices but not clips, so an index another instance loaded answers nil here. | ##### Returns | Type | Description | | --- | --- | | [`Audio.Clip`](/modules/audio/#tecs.audio.Audio.Clip) | | #### tecs.audio.Audio:clips Instance Returns every loaded clip in request order. For introspection: it builds a list per call, so nothing on a frame's path should read it. ```teal function tecs.audio.Audio.clips(self): {Audio.Clip} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `{`[`Audio.Clip`](/modules/audio/#tecs.audio.Audio.Clip)`}` | A fresh list each call, holding the live clip records rather than copies, so their `status` moves under a caller that keeps one. | #### tecs.audio.Audio:destroy Instance Stops everything and closes the output. Blocks until this instance's loads have settled, since a clip arriving afterwards would hold a pointer into a library that has released everything it made. A load the wait ran out of time on is given up instead. This method 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. ```teal function tecs.audio.Audio.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns None. #### tecs.audio.Audio:groupGain Instance Returns a group's gain, or one when none has been set. The level, not what is audible: a muted group still answers with the gain an unmute would put back. ```teal function tecs.audio.Audio.groupGain(self, name: string): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | ##### Returns | Type | Description | | --- | --- | | `number` | 1 for a group nothing has set, which is indistinguishable from one explicitly set to 1. | #### tecs.audio.Audio:groupMuted Instance Reports whether mute applies to a group. ```teal function tecs.audio.Audio.groupMuted(self, name: string): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.audio.Audio:groupPaused Instance Returns whether a group holds its current and future voices. ```teal function tecs.audio.Audio.groupPaused(self, name: string): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.audio.Audio:groups Instance Returns every group known to this instance, including any whose gain, mute, or pause has been set, and one a sounding voice is in. Sorted, so a caller reading it twice reads it the same way. ```teal function tecs.audio.Audio.groups(self): {string} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `{string}` | A fresh table each call, which the caller owns. A group whose only voice has ended and which nothing was set on drops out of it, so this is not a stable list of a game's groups. | #### tecs.audio.Audio:install Instance Adds the system that plays [`Sound`](/modules/audio/#tecs.audio.Sound) components, and the snapshot handler that carries the mixer. `update` is not added here. Reaping voices is not world work: it has to continue during a world pause, and an application drives it from the iteration instead. ```teal function tecs.audio.Audio.install(self, world: types.World) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `world` | [`types.World`](/modules/ecs/#tecs.World) | Repoints this instance's pitch variance at the world's `tecs.audio` random stream, so installing into a second world moves the variance to that one's. This interface expects one [`Audio`](/modules/audio/#tecs.audio.Audio) per world. | ##### Returns None. #### tecs.audio.Audio:keyCount Instance Returns how many voices a key holds now. ```teal function tecs.audio.Audio.keyCount(self, key: string): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `key` | `string` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Voices counted against the key, paused and fading ones included, since a slot is not given back until the voice ends. 0 for a key that has never held one. | #### tecs.audio.Audio:keys Instance Returns every key with a limit or counted voice, sorted. ```teal function tecs.audio.Audio.keys(self): {string} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `{string}` | A fresh table each call, which the caller owns. A key keeps its bucket once one exists, so a key that has played and stopped is still listed. | #### tecs.audio.Audio:limit Instance Returns the limit assigned to a key, or nil. ```teal function tecs.audio.Audio.limit(self, key: string): Audio.Limit ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `key` | `string` | | ##### Returns | Type | Description | | --- | --- | | [`Audio.Limit`](/modules/audio/#tecs.audio.Audio.Limit) | The record that was set, not a copy, so writing through it changes the limit in force. | #### tecs.audio.Audio:load Instance Queues a sound for loading and returns a future for its clip. Loading the same path twice returns distinct futures over one load and one eventual clip; a clip is the file, and playing it twice over is two voices reading one clip. Canceling one caller's future does not cancel another or the cached load. `options.stream` overrides the duration threshold that otherwise decides whether the clip is held in memory. ```teal function tecs.audio.Audio.load( self, path: string, options: Audio.LoadOptions ): Future ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `path` | `string` | Must be non-empty. Not checked here: a path that does not exist returns a failed future a frame or two later, so this never raises for a missing file. | | `options` | [`Audio.LoadOptions`](/modules/audio/#tecs.audio.Audio.LoadOptions) | | ##### Returns | Type | Description | | --- | --- | | [`Future`](/modules/Future/)`<`[`Audio.Clip`](/modules/audio/#tecs.audio.Audio.Clip)`>` | A caller-owned [`Future`](/modules/Future/) that settles with the cached clip after the load succeeds. Concurrent calls return distinct futures whose ready values are the same table. A later call returns an already-settled future. Only the first call reads `options`. | #### tecs.audio.Audio:loading Instance Returns the number of loads this instance is still waiting on. ```teal function tecs.audio.Audio.loading(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Loads not yet settled, which counts loads rather than clips. A failed load counts as settled, so zero means every `load` has an answer and not that every answer was a success. Zero after `waitForLoads` is what tells a finished wait from a timed-out one. | #### tecs.audio.Audio:looping Instance Returns whether a voice repeats at the end. ```teal function tecs.audio.Audio.looping(self, handle: integer): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | What this layer last told the mixer, not a reading from it. false for a handle that names nothing. | #### tecs.audio.Audio:masterGain Instance Returns master gain, whether or not mute holds it down. ```teal function tecs.audio.Audio.masterGain(self): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `number` | | #### tecs.audio.Audio:maxVoices Instance Returns the configured maximum number of simultaneous voices. ```teal function tecs.audio.Audio.maxVoices(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `integer` | The ceiling `play` refuses at, fixed for the instance's life. Not a hardware limit: it is what `newAudio` was given, defaulting to 32. | #### tecs.audio.Audio:muted Instance Returns whether master mute holds the output down. ```teal function tecs.audio.Audio.muted(self): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | | #### tecs.audio.Audio:pause Instance Holds a voice where it is. It keeps its slot until something resumes or stops it, because a paused voice has not finished. Neither this nor `resume` takes a fade, and that is a decision rather than an omission. The mixer ramps on a play and on a stop and nowhere else, so a faded pause has to be a ramp run from here: a list of voices in flight, a `setGain` per voice per frame, and the pause itself issued only once the ramp reaches zero, which makes this a command that takes effect later and needs its own answer for what a stop, a group gain or a second pause during the ramp means. Each step also lands on a frame boundary, so a quarter-second fade at 60 frames a second is fifteen steps on a stream running at 48000, and its length follows the frame rate rather than the clock. Drift and cost of exactly that kind are what handing fades to the mixer avoids, and one voice held for a menu is not worth giving it up. The mixer does offer the pieces to fade out and take a sound back where it left off: `tell` reads the position, `stop` fades out, and `play` returns with `start` and `fadeIn`. That re-reads the input rather than holding it, so it is a different thing from a pause, and which of the two a moment wants is a game's answer rather than this layer's. ```teal function tecs.audio.Audio.pause(self, handle: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | A no-op for a handle that names nothing, one already paused, and one fading out, since a paused fade would never finish. Not holder counted: one `resume` undoes any number of `pause` calls. | ##### Returns None. #### tecs.audio.Audio:paused Instance Returns whether a handle names a paused voice. ```teal function tecs.audio.Audio.paused(self, handle: integer): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | true whether the hold came from `pause` on this handle or from `pauseGroup` on its group; the two are not told apart. false for a handle that names nothing. | #### tecs.audio.Audio:pauseGroup Instance Holds every voice in a group where it is, and every voice that joins it later. The audio object records the pause as well as sending it, because the mixer's tag pause reaches only voices sounding at that moment. Without the record, a sound started into a paused group would be the one thing still heard. ```teal function tecs.audio.Audio.pauseGroup(self, name: string) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | Not holder counted: one `resumeGroup` undoes any number of these. A voice `resume`d out of a paused group stays going, and the group's hold still applies to whatever joins next. | ##### Returns None. #### tecs.audio.Audio:play Instance Plays `clip`, returning a handle, or zero when it did not start. Zero means the clip is not loaded, loading failed, a key's limit or cooldown declined it, every voice is busy, or the machine has no output. 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**; it never steals an older one. A sound that stops halfway through because something else started is harder to explain than one that never started, and the same reasoning is why a full voice pool declines rather than stealing. ```teal function tecs.audio.Audio.play( self, clip: Audio.Clip, options: Audio.PlayOptions ): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `clip` | [`Audio.Clip`](/modules/audio/#tecs.audio.Audio.Clip) | Accepts nil and returns zero, so callers may pass a `load` result directly without a guard. | | `options` | [`Audio.PlayOptions`](/modules/audio/#tecs.audio.Audio.PlayOptions) | The method reads this once and follows no later changes. | ##### Returns | Type | Description | | --- | --- | | `integer` | A handle for `stop`, `playing`, `setGain` and the rest, or zero when nothing started. It stays valid while the voice sounds and goes inert for good once it does not; it is never reissued for a later voice, and requires no release. | #### tecs.audio.Audio:playing Instance Returns whether a handle still names a sounding voice. A paused or fading voice still counts: it has not finished. ```teal function tecs.audio.Audio.playing(self, handle: integer): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | false for a handle whose voice has ended, for zero, and for one from another [`Audio`](/modules/audio/#tecs.audio.Audio). A voice that ended on its own reads true until the next `update` reaps it, since nothing here is told the instant it finished. | #### tecs.audio.Audio:reload Instance Re-reads a clip's file over the clip already loaded from it. The counterpart to `renderer.sprites:replaceImage`, and it keeps identity on the same terms. A clip's index is its path's, so an edited file comes back under the index every [`Sound`](/modules/audio/#tecs.audio.Sound) row already carries and nothing in the world is touched. A streamed clip holds nothing to replace. Each voice opens the file for itself, so the next one to start reads what is on disk now and this has only to say so. Voices already sounding read the stream they opened, which is what happens to a file edited under a running voice with or without a reload. The loader decodes a resident clip again and swaps it in. It stays resident whatever the new file's length would have chosen: rows already pointing at it were started against held samples, and turning it into a stream under them would change what a voice is, not what it sounds like. The clip it replaces is destroyed here, which is safe with voices still on it: the mixer counts a reference per track and frees at the last one, so a sound playing across the swap finishes on the samples it started with. 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 yet. ```teal function tecs.audio.Audio.reload(self, path: string): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `path` | `string` | The path originally used to load the clip. | ##### Returns | Type | Description | | --- | --- | | `boolean` | Whether the clip was re-read, and a reason when it was not. | | `string` | | #### tecs.audio.Audio:resume Instance Lets a paused voice carry on. ```teal function tecs.audio.Audio.resume(self, handle: integer) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | A no-op for a handle that names nothing and for one that is not paused. Resumes a voice its group paused, and the group's hold does not put it back; a voice starting into that group afterwards is still held. | ##### Returns None. #### tecs.audio.Audio:resumeGroup Instance Lets a paused group carry on, and lets later joiners start sounding. ```teal function tecs.audio.Audio.resumeGroup(self, name: string) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | ##### Returns None. #### tecs.audio.Audio:seek Instance Moves a voice's read position, in seconds from the start of its clip. False when the handle names nothing, or when the input cannot seek: a the decoder does not support seeking, or can only reach a time rather than an exact sample. ```teal function tecs.audio.Audio.seek( self, handle: integer, seconds: number ): boolean ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | | `seconds` | `number` | From the start of the clip, not from where the voice is now, and not from the loop point. Converted through the track's own rate, so it lands on a sample frame rather than on a mixer tick. | ##### Returns | Type | Description | | --- | --- | | `boolean` | false when the handle names nothing or the input refused the seek. A true does not promise the exact sample: some decoders reach only the nearest point they can. | #### tecs.audio.Audio:setGain Instance Sets a voice's gain, before its group and the master. ```teal function tecs.audio.Audio.setGain(self, handle: integer, gain: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | A no-op for one that names nothing. | | `gain` | `number` | Linear amplitude, not decibels: 0 is silence, 1 is the clip as recorded, above 1 is louder and may clip. Takes effect on the next buffer the mixer fills rather than ramping, so a large jump on a sounding voice can be audible as a step. | ##### Returns None. #### tecs.audio.Audio:setGroupGain Instance Scales every voice in a group, and every voice that joins it later. Composed here rather than through the mixer's per-tag gain, which writes each tagged track's own gain and would overwrite what the voice asked for. ```teal function tecs.audio.Audio.setGroupGain(self, name: string, gain: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | Needs no declaring, and setting a gain on a name nothing is in is how a game configures a group before anything joins it. Recorded against the name, so it outlives every voice that was in it. | | `gain` | `number` | Linear amplitude, multiplied with each voice's own gain. There is no removing it: set 1 to return the group to unscaled. | ##### Returns None. #### tecs.audio.Audio:setGroupMuted Instance Silences a group without discarding the level it was set to. Bookkeeping over the gains that already exist rather than anything the mixer receives: every voice in the group contributes zero while this holds, and an unmute puts each one back at its own gain times `groupGain(name)`. ```teal function tecs.audio.Audio.setGroupMuted( self, name: string, muted: boolean ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | | `muted` | `boolean` | | ##### Returns None. #### tecs.audio.Audio:setLimit Instance 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 and neither name has to know about the other. Passing nil removes the limit. Voices already sounding are left alone. When a key reaches its limit, `play` **drops the new voice**: it returns zero and leaves every sounding voice alone. ```teal function tecs.audio.Audio.setLimit( self, key: string, limit: Audio.Limit ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `key` | `string` | | | `limit` | [`Audio.Limit`](/modules/audio/#tecs.audio.Audio.Limit) | nil removes the limit. Lowering `voices` below what the key already holds does not stop any of them; it only refuses the next until enough have ended. The audio object measures `cooldown` from when a voice last started, against the time passed to `update`, so a game that never calls `update` never advances a cooldown. | ##### Returns None. #### tecs.audio.Audio:setLoop Instance Changes whether a voice repeats, part way through. The mixer replaces its starting repeat count, so 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 count from the next play. ```teal function tecs.audio.Audio.setLoop(self, handle: integer, loop: boolean) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | | `loop` | `boolean` | true repeats forever; there is no finite repeat count here. Setting it does not move the loop point. The voice keeps the `loopStart` from `play`. | ##### Returns None. #### tecs.audio.Audio:setMasterGain Instance Scales everything. One number on the mixer, so this costs the same whether one voice is sounding or every voice is. Setting this while muted changes the level a later unmute returns to and nothing that is audible now, which is what a volume slider moved with the sound off should do. ```teal function tecs.audio.Audio.setMasterGain(self, gain: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `gain` | `number` | Linear amplitude, not decibels, on the same scale as a voice's: 0 silences, 1 leaves the mix as mixed, above 1 is louder and may clip. | ##### Returns None. #### tecs.audio.Audio:setMuted Instance Silences everything without discarding the master gain. The mixer's own number again, so it costs one call however many voices are sounding. It does not fan out to the groups: `groupMuted` answers "is this group silenced", and writing every group's bit here would overwrite the answers an unmute has to put back. That is the same loss as setting a gain to zero, which is what a mute exists instead of. ```teal function tecs.audio.Audio.setMuted(self, muted: boolean) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `muted` | `boolean` | | ##### Returns None. #### tecs.audio.Audio:setPitch Instance Sets a voice's playback rate. A value of 1 preserves the original rate. ```teal function tecs.audio.Audio.setPitch(self, handle: integer, ratio: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | | `ratio` | `number` | A resampling rate, not an interval: 2 is an octave up and half as long, 0.5 an octave down and twice as long. It changes how long the clip takes, so a looped voice's period moves with it. | ##### Returns None. #### tecs.audio.Audio:setPosition Instance Places a voice in space. See [`Sound`](/modules/audio/#tecs.audio.Sound) for what the numbers mean. Replaces a pan set by `setStereo` rather than combining with it. Puts the voice in the mixer's 3D mode, which folds its input down to mono before placing it, so a stereo clip loses its stereo image when positioned. ```teal function tecs.audio.Audio.setPosition( self, handle: integer, x: number, y: number, z: number ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | | `x` | `number` | Positive to the right of the listener. The mixer fixes the listener at the origin. | | `y` | `number` | Positive **above** the listener. World Y runs down, so world coordinates need their Y negated on the way in. | | `z` | `number` | Positive behind the listener. | ##### Returns None. #### tecs.audio.Audio:setStereo Instance 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". `left` at 0.8 and `right` at 0.2 is a sound over to the left, whatever the speakers are. Negative reads as silence and above 1 is louder, on the same terms as a gain. Replaces a position set by `setPosition` rather than combining with it: the mixer holds one placement per track, and each call writes it. ```teal function tecs.audio.Audio.setStereo( self, handle: integer, left: number, right: number ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | | `left` | `number` | Gain out of the left speaker, on the same linear scale as `setGain`. It multiplies with the voice, group and master gains rather than replacing any of them, so a pan of 1 and 1 is not silence. | | `right` | `number` | Gain out of the right speaker. There is no normalization between the two: 1 and 1 is the sound at full out of both sides, not half out of each. | ##### Returns None. #### tecs.audio.Audio:sounding Instance Returns the number of voices sounding now, including paused and fading voices. ```teal function tecs.audio.Audio.sounding(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Slots in use, so it drops only when `update` reaps a voice, not the instant one ends. `play` declines once it reaches `maxVoices`. | #### tecs.audio.Audio:stop Instance Stops a voice. A handle to one that already ended does nothing. `fadeOut` seconds keeps the voice sounding while it fades, so `playing` stays true until `update` sees the mixer finish it. ```teal function tecs.audio.Audio.stop( self, handle: integer, fadeOut: number ) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | A stale one is a no-op, as is one already fading out: a second stop does not shorten a fade in progress. | | `fadeOut` | `number` | Seconds to ramp down over, a duration and not a moment to finish at. Omitted or zero stops at once and frees the slot within this call. The ramp is the mixer's, so it follows the audio clock rather than the frame rate. | ##### Returns None. #### tecs.audio.Audio:stopAll Instance Stops everything, over `fadeOut` seconds if that is given. A faded stop leaves each voice sounding until the mixer finishes it, so `update` reaps them instead of this call. The mixer owns the ramp, on the same terms as `stop` and `stopGroup`. Reaches every voice, whichever group it is in and whether a [`Sound`](/modules/audio/#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. ```teal function tecs.audio.Audio.stopAll(self, fadeOut: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `fadeOut` | `number` | Seconds, a duration. Omitted or zero frees every slot within this call. A positive one unpauses the voices it fades, because a paused voice does not advance and its fade would never finish. | ##### Returns None. #### tecs.audio.Audio:stopGroup Instance Ends every voice in a group, over `fadeOut` seconds if that is given. A faded stop leaves the voices sounding until they finish, so they are reaped by `update` rather than here. Stops the voices, not the group: a gain, mute or pause set on the name survives, and anything that joins afterwards starts under them. ```teal function tecs.audio.Audio.stopGroup(self, name: string, fadeOut: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `name` | `string` | | | `fadeOut` | `number` | Seconds, a duration. Omitted or zero frees the slots within this call. A positive one unpauses the voices it fades, since a paused voice's fade would never finish. | ##### Returns None. #### tecs.audio.Audio:tell Instance Returns the voice position in seconds, or nil. Nil when the handle names nothing or the mixer cannot say. A paused voice reports where it stopped, which with `seek` and `fadeIn` is what taking a sound back where it left off is built from. ```teal function tecs.audio.Audio.tell(self, handle: integer): number ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `handle` | `integer` | | ##### Returns | Type | Description | | --- | --- | | `number` | Seconds from the start of the clip, so a looped voice's reading falls back at each repeat rather than accumulating. nil when the handle names nothing and nil when the mixer will not say, which are not told apart here. | #### tecs.audio.Audio:update Instance Takes finished loads and reaps the voices the mixer has finished with. Call once per frame, with the frame's step. The step advances cooldowns. Nothing else here needs time: a fade is the mixer's to run, and a voice is over when the mixer 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. ```teal function tecs.audio.Audio.update(self, dt: number): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `dt` | `number` | Seconds since the last call. The audio pass adds it to the clock that governs cooldowns. measured against. Omitting it advances nothing, which is what a test stepping voices without time wants. | ##### Returns | Type | Description | | --- | --- | | `integer` | Voices still sounding after the reap, the same number `sounding` answers. | #### tecs.audio.Audio:voices Instance Returns every sounding voice, including paused and fading voices. For introspection, on the same terms as `clips`. The handles it reports are the ones `playing` and `stop` take, so callers can act on these results. ```teal function tecs.audio.Audio.voices(self): {Audio.VoiceInfo} ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | ##### Returns | Type | Description | | --- | --- | | `{`[`Audio.VoiceInfo`](/modules/audio/#tecs.audio.Audio.VoiceInfo)`}` | A fresh list of fresh records each call, ordered by voice slot, which is not the order the voices started in. It is a snapshot: nothing in it follows the voice afterwards. | #### tecs.audio.Audio:waitForLoads Instance Blocks until every load this instance has queued has settled. For startup and tests. A frame should let `update` resolve them instead. Every load settles, whatever it settled as, so one missing file does not leave the rest unwaited for. Waiting drains the loader, so loads another subsystem queued settle here as well and a slow one elsewhere lengthens this wait. It cannot shorten it, and an instance with nothing in flight drains nothing. ```teal function tecs.audio.Audio.waitForLoads(self, timeoutMs: number) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Audio` | | | `timeoutMs` | `number` | **Milliseconds**, unlike the seconds everything else on this surface takes, because it is a wait on the asset worker rather than an audio duration. Omitting it does not wait forever: it takes a 5000 ms default. Running out is not reported, so read `loading` afterwards to tell a finished wait from a timed-out one. | ##### Returns None. ### tecs.audio.Clip type A loaded clip, or one still loading. ```teal type tecs.audio.Clip = Audio.Clip ``` ### tecs.audio.Config type Configures `newAudio`. ```teal type tecs.audio.Config = Audio.Config ``` ### tecs.audio.Device record Describes a physical device before the game opens it. ```teal record tecs.audio.Device id: number name: string frequency: integer channels: integer end ``` #### tecs.audio.Device.id field Read-only. Reports the platform's numeric device identifier. ```teal tecs.audio.Device.id: number ``` #### tecs.audio.Device.name field Read-only. Reports the platform's display name for the device. ```teal tecs.audio.Device.name: string ``` #### tecs.audio.Device.frequency field Read-only. Reports the device's preferred samples per second. ```teal tecs.audio.Device.frequency: integer ``` #### tecs.audio.Device.channels field Read-only. Reports the device's preferred channels per frame. ```teal tecs.audio.Device.channels: integer ``` ### tecs.audio.Limit type Defines how many voices a key allows and how soon it may repeat. ```teal type tecs.audio.Limit = Audio.Limit ``` ### tecs.audio.LoadOptions type Configures `Audio:load`. ```teal type tecs.audio.LoadOptions = Audio.LoadOptions ``` ### tecs.audio.Microphone record An open recording device, read by polling. ```teal record tecs.audio.Microphone frequency: integer channels: integer availableFrames: function(self): integer destroy: function(self) pause: function(self): boolean, string read: function(self, maxFrames: integer): string, string resume: function(self): boolean, string end ``` #### tecs.audio.Microphone.frequency field Read-only. Reports samples per second after SDL converts from whatever the device actually runs at. This is the requested value, not the device's own. ```teal tecs.audio.Microphone.frequency: integer ``` #### tecs.audio.Microphone.channels field Read-only. Reports interleaved channels per frame after conversion. ```teal tecs.audio.Microphone.channels: integer ``` #### tecs.audio.Microphone:availableFrames Instance Complete sample frames ready to read without blocking. ```teal function tecs.audio.Microphone.availableFrames(self): integer ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Microphone` | | ##### Returns | Type | Description | | --- | --- | | `integer` | Whole frames only, so a partly arrived frame is not counted and reads as zero until the rest of it lands. Zero once destroyed, rather than an error. | #### tecs.audio.Microphone:destroy Instance Stops capture and closes the recording device. Safe more than once. Whatever was captured and not yet read is discarded with the stream, so a last `read` belongs before this rather than after. ```teal function tecs.audio.Microphone.destroy(self) ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Microphone` | | ##### Returns None. #### tecs.audio.Microphone:pause Instance Stops the device filling the stream, keeping whatever is already in it. ```teal function tecs.audio.Microphone.pause(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Microphone` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | False with a reason on failure, including for a microphone already destroyed. Pausing one already paused succeeds. | | `string` | The reason, when the first return is false. | #### tecs.audio.Microphone:read Instance Pulls up to `maxFrames` complete sample frames. ```teal function tecs.audio.Microphone.read( self, maxFrames: integer ): string, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Microphone` | | | `maxFrames` | `integer` | Omit to take everything ready now. Zero is allowed and takes nothing. A limit above what is ready takes what is ready rather than waiting for the rest. | ##### Returns | Type | Description | | --- | --- | | `string` | Interleaved native-endian float32 samples, as bytes in a string, `channels * 4` per frame. An empty string means nothing was ready and is not a failure. Nil on one, with the reason beside it: a destroyed microphone, a `maxFrames` that is not a non-negative integer, or SDL's own error. | | `string` | The reason, when the first return is nil. | #### tecs.audio.Microphone:resume Instance Starts the device filling the stream again after `pause`. ```teal function tecs.audio.Microphone.resume(self): boolean, string ``` ##### Arguments | Name | Type | Description | | --- | --- | --- | | `self` | `Microphone` | | ##### Returns | Type | Description | | --- | --- | | `boolean` | False with a reason on failure, including for a microphone already destroyed. | | `string` | The reason, when the first return is false. | ### tecs.audio.MicrophoneConfig record Configures `openMicrophone`. ```teal record tecs.audio.MicrophoneConfig device: number frequency: integer channels: integer end ``` #### tecs.audio.MicrophoneConfig.device field Caller-writable. Selects a physical device id from `recordingDevices`. Omitted uses the current system default. ```teal tecs.audio.MicrophoneConfig.device: number ``` #### tecs.audio.MicrophoneConfig.frequency field Caller-writable. Sets samples per second after SDL converts the device input. Defaults to 48000. ```teal tecs.audio.MicrophoneConfig.frequency: integer ``` #### tecs.audio.MicrophoneConfig.channels field Caller-writable. Sets interleaved channels. Defaults to one. ```teal tecs.audio.MicrophoneConfig.channels: integer ``` ### tecs.audio.PlayOptions type Configures `Audio:play`. ```teal type tecs.audio.PlayOptions = Audio.PlayOptions ``` ### tecs.audio.Sound record Represents a sound attached 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 mixer and does nothing else with it. `spatial` switches it on, and the mixer reads `x`, `y` and `z` in a right-handed system. The mixer fixes its listener at the origin, with x positive to the right, y positive up and z positive behind. So a caller with a camera and a world has three jobs this component does not do for it, and doing any of them here would fix answers that belong to a game: 1. Subtract the listener. There is no listener component and no rule that the camera is one, so whatever a game decides is listening is what it subtracts before writing these fields. 2. Choose a scale. The mixer attenuates with distance on its own, and how loud a sound a hundred world units away should be is a question about a game's units, not about audio. 3. Decide what a screen-space sound means. A sound on a layer that does not move with the camera has no world position to convert, and the answer is to leave `spatial` at zero. A name identifies a group, just as a path identifies a clip. An FFI component holds numbers, so what `group` carries is an interned index from `Audio.groupId` rather than the mixer's tag itself, and `Audio.groupName` reads it back. The index is this run's and no other, which is why `serialize` writes the name: an integer in a save file would name a different group the next time a build interned its names in a different order. The audio 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 to apply a change. Each of those costs one read and compare per sounding row per frame, which is the price of the component being live and is why `clip` and `group` are not on the list: both take an untag, a retag and a fresh input to change, neither is something a game writes often, and following them would put that cost on every row that never does. Moving a sound into another group is setting `group` and clearing `voice`, which is the same thing changing its clip takes. Disabling an entity silences its sound and re-enabling starts it again. Queries exclude [`Disabled`](/modules/ecs/#tecs.ecs.Disabled) unless they name it, so a disabled row is not followed and its voice is taken back; the audio pass clears the handle at the same moment, which is what lets the sound return with the entity rather than reading as a one-shot that had finished. Read-only. Exposes the `Sound` component that plays a clip from an entity. ```teal record tecs.audio.Sound is Component clip: number playing: number gain: number loop: number pitch: number spatial: number x: number y: number z: number group: number voice: number end ``` #### Interfaces | Interface | | --- | | [`Component`](/modules/ecs/#tecs.ecs.Component) | #### tecs.audio.Sound.clip field Caller-writable. Selects a clip by its `Audio.clipId` index. Zero plays nothing. ```teal tecs.audio.Sound.clip: number ``` #### tecs.audio.Sound.playing field Caller-writable. Uses nonzero to request playback and zero to stop. 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. ```teal tecs.audio.Sound.playing: number ``` #### tecs.audio.Sound.gain field Caller-writable. Sets linear gain from zero to one before group and master gain. ```teal tecs.audio.Sound.gain: number ``` #### tecs.audio.Sound.loop field Caller-writable. Uses nonzero to repeat. Clearing it part way through lets the voice play out to its end rather than cutting it. ```teal tecs.audio.Sound.loop: number ``` #### tecs.audio.Sound.pitch field Caller-writable. Sets playback rate. One leaves it unchanged, while two plays an octave higher in half the time. ```teal tecs.audio.Sound.pitch: number ``` #### tecs.audio.Sound.spatial field Caller-writable. Uses nonzero to read `x`, `y`, and `z`; zero mixes without a position. ```teal tecs.audio.Sound.spatial: number ``` #### tecs.audio.Sound.x field Caller-writable. Sets the position right of the listener. ```teal tecs.audio.Sound.x: number ``` #### tecs.audio.Sound.y field Caller-writable. Sets the position above the listener. ```teal tecs.audio.Sound.y: number ``` #### tecs.audio.Sound.z field Caller-writable. Sets the position behind the listener. ```teal tecs.audio.Sound.z: number ``` #### tecs.audio.Sound.group field Caller-writable. Selects a group by its `Audio.groupId` index. Zero joins no group. ```teal tecs.audio.Sound.group: number ``` #### tecs.audio.Sound.voice field Engine-owned. Reports the voice assigned by the audio pass: zero before it starts and negative once a one-shot has finished. Written by the engine; setting it back to zero is how a game asks for the sound again. ```teal tecs.audio.Sound.voice: number ``` ### tecs.audio.VoiceInfo type Describes one voice returned by `Audio:voices`. ```teal type tecs.audio.VoiceInfo = Audio.VoiceInfo ``` ## Functions ### tecs.audio.openMicrophone Static Opens a microphone as interleaved native-endian 32-bit float samples. The API installs no callback. The audio thread fills its own stream and the game pulls completed bytes from the main thread with `read`. ```teal function tecs.audio.openMicrophone( config: platformaudio.MicrophoneConfig ): platformaudio.Microphone, string ``` #### Arguments | Name | Type | Description | | --- | --- | --- | | `config` | [`platformaudio.MicrophoneConfig`](/modules/audio/#tecs.audio.MicrophoneConfig) | Omitted opens the default recording device at its own frequency and channel count. | #### Returns | Type | Description | | --- | --- | | [`platformaudio.Microphone`](/modules/audio/#tecs.audio.Microphone) | The open microphone, whose closing is the caller's through `destroy`. Nil on failure, and nothing is left open in that case. | | `string` | The reason, when the first return is nil. | ### tecs.audio.playbackDevices Static Returns the physical playback devices attached now. ```teal function tecs.audio.playbackDevices(): {platformaudio.Device}, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{`[`platformaudio.Device`](/modules/audio/#tecs.audio.Device)`}` | The devices, listed afresh each call and the caller's to keep. Empty rather than nil when the subsystem cannot start. | | `string` | SDL's reason, when something went wrong. | ### tecs.audio.recordingDevices Static Returns the physical recording devices attached now. ```teal function tecs.audio.recordingDevices(): {platformaudio.Device}, string ``` #### Arguments None. #### Returns | Type | Description | | --- | --- | | `{`[`platformaudio.Device`](/modules/audio/#tecs.audio.Device)`}` | The devices, listed afresh each call and the caller's to keep. Empty rather than nil when the subsystem cannot start. | | `string` | SDL's reason, when something went wrong. |