# `tecs.platform.audiobackend` The command and observation contract between Tecs audio policy and a mixer. Tecs owns voice slots, groups, keyed limits and every decision about what should sound. A backend owns decoding, device output and sample mixing, and learns what Tecs decided from one array of plain commands per frame. Nothing crosses the seam except that array, the finished handles drained back, and the two flushing queries that need an answer in the same call. Recording and device naming sit here for the same reason. A recording stream has the playback hazard from the other side: captured frames arrive on a thread the Lua virtual machine never created, so they reach Nupp only by a `read` on the frame thread emptying a buffer the backend filled. Nothing on this contract is a function the backend calls. A command names its voice by the packed handle Tecs already hands to game code, so a backend stores no slot table of its own and an observation about a voice that has since been recycled is recognized as stale by the generation inside the handle rather than by anything the backend tracked. ```nupp local backend = tecs.platform.audiobackend.silent() local buffer = {tecs.platform.audiobackend.newCommand()} buffer[1].kind = tecs.platform.audiobackend.SET_MASTER_GAIN buffer[1].gain = 0.5 backend.submit(buffer, 1) ``` ## Constructors ### `newCommand` _constructor_ ```nupp function newCommand(): Command ``` Creates one zeroed command, for a caller filling a reusable buffer. #### Returns | Type | Description | | --- | --- | | `Command` | a command whose every field is at rest | ## Types ### `Backend` _record_ ```nupp record Backend failure: (function(): string?)? available: boolean frequency: integer channels: integer submit: function(commands: {Command}, count: integer): nil drain: function(out: {integer}): integer loadClip: function(path: string, clip: integer, mode: LoadMode, streamSeconds: number): (ClipInfo?, string?) releaseClip: function(clip: integer): nil position: function(handle: integer): number? seek: function(handle: integer, seconds: number): boolean decoders: function(): {string} close: function(): nil end ``` Mixes sound for one output. Every field is a plain function rather than a method, so a test stub is a record of closures and nothing has to be subclassed to observe the command stream Tecs produces. #### Methods ##### `submit` ```nupp submit: function(commands: {Command}, count: integer): nil ``` Read-only. Applies the first `count` commands of `commands` in order. The array stays the caller's, and a backend copies whatever it needs before returning. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `commands` | `{Command}` | | | `count` | `integer` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | ##### `drain` ```nupp drain: function(out: {integer}): integer ``` Read-only. Fills `out` with the handles of voices that have finished since the previous drain, and returns how many it wrote. Writes at most `#out` when the array is preallocated by the caller. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `out` | `{integer}` | | ###### Returns | Type | Description | | --- | --- | | `integer` | | ##### `loadClip` ```nupp loadClip: function(path: string, clip: integer, mode: LoadMode, streamSeconds: number): (ClipInfo?, string?) ``` Read-only. Reads a file and holds it under `clip`, returning what the load settled on, or nil and a reason. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `path` | `string` | | | `clip` | `integer` | | | `mode` | `LoadMode` | | | `streamSeconds` | `number` | | ###### Returns | Type | Description | | --- | --- | | `ClipInfo?` | | | `string?` | | ##### `releaseClip` ```nupp releaseClip: function(clip: integer): nil ``` Read-only. Drops whatever a clip index holds. Voices already reading it finish on the samples they started with. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `clip` | `integer` | | ###### Returns | Type | Description | | --- | --- | | `nil` | | ##### `position` ```nupp position: function(handle: integer): number? ``` Read-only. Reports a voice's read position in seconds, or nil when the handle names nothing and when the input will not say. Flushes queued commands first, because the answer has to include them. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `handle` | `integer` | | ###### Returns | Type | Description | | --- | --- | | `number?` | | ##### `seek` ```nupp seek: function(handle: integer, seconds: number): boolean ``` Read-only. Moves a voice's read position to `seconds` from the start of its clip, and reports whether the input accepted it. Flushes queued commands first. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `handle` | `integer` | | | `seconds` | `number` | | ###### Returns | Type | Description | | --- | --- | | `boolean` | | ##### `decoders` ```nupp decoders: function(): {string} ``` Read-only. Reports the decoder names this build linked, in the backend's own order. ###### Returns | Type | Description | | --- | --- | | `{string}` | | ##### `close` ```nupp close: function(): nil ``` Read-only. Stops everything and closes the output. Calling it twice is harmless. ###### Returns | Type | Description | | --- | --- | | `nil` | | #### Fields ##### `failure` ```nupp failure: (function(): string?)? ``` Read-only. Reports terminal stream failure, or nil while healthy. Omitted by backends without a device. The frame thread polls; no callback enters Nupp. ##### `available` ```nupp available: boolean ``` Read-only. Reports whether a real output opened. A false here still takes every command and answers every query; nothing is heard. ##### `frequency` ```nupp frequency: integer ``` Read-only. Reports the output frequency in frames per second. ##### `channels` ```nupp channels: integer ``` Read-only. Reports the number of output channels. ### `Capture` _record_ ```nupp record Capture failure: (function(): string?)? frequency: integer channels: integer availableFrames: function(): integer overruns: function(): integer read: function(maxFrames: integer): (string?, string?) readInto: function(out: {number}, maxFrames: integer): integer write: function(samples: {number}, frames: integer): integer pause: function(): (boolean, string?) resume: function(): (boolean, string?) close: function(): nil end ``` Captures sound from one recording device, pulled rather than pushed. Nothing arrives until a read asks for it. The backend fills a bounded buffer from its own thread and this side empties it, which is the same shape the playback seam has and for the same reason: a device thread must never enter Nupp. Every field is a plain function rather than a method, so a device-free test is a record of closures. #### Methods ##### `availableFrames` ```nupp availableFrames: function(): integer ``` Read-only. Reports the complete frames ready to read without waiting. ###### Returns | Type | Description | | --- | --- | | `integer` | | ##### `overruns` ```nupp overruns: function(): integer ``` Read-only. Reports the frames dropped because nothing read them in time, counted since the capture opened. ###### Returns | Type | Description | | --- | --- | | `integer` | | ##### `read` ```nupp read: function(maxFrames: integer): (string?, string?) ``` Read-only. Takes up to `maxFrames` frames as interleaved native-endian float32 bytes, and returns nil with a reason when nothing could be read. An empty string means nothing was ready and is not a failure. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `maxFrames` | `integer` | | ###### Returns | Type | Description | | --- | --- | | `string?` | | | `string?` | | ##### `readInto` ```nupp readInto: function(out: {number}, maxFrames: integer): integer ``` Read-only. Takes up to `maxFrames` frames into `out` as interleaved samples, writing no more than `#out` holds, and returns the frames written. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `out` | `{number}` | | | `maxFrames` | `integer` | | ###### Returns | Type | Description | | --- | --- | | `integer` | | ##### `write` ```nupp write: function(samples: {number}, frames: integer): integer ``` Read-only. Puts `frames` interleaved frames in where a device would, and returns how many were accepted. Only a device-free capture has anything to feed it, and it is how a test drives this contract. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `samples` | `{number}` | | | `frames` | `integer` | | ###### Returns | Type | Description | | --- | --- | | `integer` | | ##### `pause` ```nupp pause: function(): (boolean, string?) ``` Read-only. Stops the device filling the buffer, keeping what is already in it. ###### Returns | Type | Description | | --- | --- | | `boolean` | | | `string?` | | ##### `resume` ```nupp resume: function(): (boolean, string?) ``` Read-only. Starts the device filling the buffer again. ###### Returns | Type | Description | | --- | --- | | `boolean` | | | `string?` | | ##### `close` ```nupp close: function(): nil ``` Read-only. Stops capture and closes the device. Calling it twice is harmless, and whatever was captured and not read goes with it. ###### Returns | Type | Description | | --- | --- | | `nil` | | #### Fields ##### `failure` ```nupp failure: (function(): string?)? ``` Read-only. Reports terminal stream failure. Buffered frames may still drain; resume must fail until the caller closes and reopens the capture. ##### `frequency` ```nupp frequency: integer ``` Read-only. Reports the frames per second a read answers in, which is what the caller asked for rather than what the hardware runs at. ##### `channels` ```nupp channels: integer ``` Read-only. Reports the interleaved channels per frame a read answers with, which is what the caller asked for rather than what the hardware runs at. ### `ClipInfo` _record_ ```nupp record ClipInfo duration: number resident: boolean end ``` Reports what a load settled on. #### Fields ##### `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 samples stay in memory and are shared by every voice reading the clip. ### `Command` _record_ ```nupp record Command kind: integer handle: integer clip: integer flags: integer gain: number pitch: number start: number fade: number loopStart: number x: number y: number z: number end ``` Requests one operation from a backend. One record covers every operation, because the array crossing the seam has to be one contiguous run of identical elements. A field a command does not name carries whatever the previous use of that record left behind, so a backend reads only the fields its `kind` claims. #### Fields ##### `kind` ```nupp kind: integer ``` Caller-writable. Selects the operation from this module's kind constants. ##### `handle` ```nupp handle: integer ``` Caller-writable. Names the voice as the packed handle Tecs issued, and zero for an operation on the whole output. ##### `clip` ```nupp clip: integer ``` Caller-writable. Selects the clip a `PLAY` reads, by the interned index `tecs.audio.clipId` assigned. ##### `flags` ```nupp flags: integer ``` Caller-writable. Carries the `LOOP`, `SPATIAL` and `STEREO` bits. ##### `gain` ```nupp gain: number ``` Caller-writable. Sets linear amplitude for `PLAY`, `SET_GAIN` and `SET_MASTER_GAIN`. ##### `pitch` ```nupp pitch: number ``` Caller-writable. Sets the playback rate for `PLAY` and `SET_PITCH`. ##### `start` ```nupp start: number ``` Caller-writable. Sets where a `PLAY` begins, in seconds. ##### `fade` ```nupp fade: number ``` Caller-writable. Sets the fade duration in seconds, running in on a `PLAY` and out on a `STOP`. ##### `loopStart` ```nupp loopStart: number ``` Caller-writable. Sets the position in seconds a repeat returns to. ##### `x` ```nupp x: number ``` Caller-writable. Sets the position right of the listener, or the left-speaker gain for `SET_STEREO`. ##### `y` ```nupp y: number ``` Caller-writable. Sets the position above the listener, or the right-speaker gain for `SET_STEREO`. ##### `z` ```nupp z: number ``` Caller-writable. Sets the position behind the listener. ### `Device` _record_ ```nupp record Device id: integer name: string frequency: integer channels: integer end ``` Names one physical audio device. The field names are a compatibility surface because a game may persist a chosen device. `id`, `name`, `frequency`, and `channels` therefore keep stable spellings. #### 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. ### `Devices` _record_ ```nupp record Devices playback: function(): ({Device}, string?) recording: function(): ({Device}, string?) open: function( device: integer, deviceName: string?, frequency: integer, channels: integer, bufferFrames: integer ): (Capture?, string?) end ``` Names the devices attached now and opens one for recording. Separate from [`Backend`](tecs.platform.audiobackend.Backend) because listing devices must not open a mixer, and because a game may want to name devices before it decides to make any sound at all. #### Methods ##### `playback` ```nupp playback: function(): ({Device}, string?) ``` Read-only. Names every playback device attached now, as a snapshot rather than a subscription. Returns an empty listing rather than nil when nothing could be asked, with the reason beside it. ###### Returns | Type | Description | | --- | --- | | `{Device}` | | | `string?` | | ##### `recording` ```nupp recording: function(): ({Device}, string?) ``` Read-only. Names every recording device attached now, on the same terms as `playback`. ###### Returns | Type | Description | | --- | --- | | `{Device}` | | | `string?` | | ##### `open` ```nupp open: function( device: integer, deviceName: string?, frequency: integer, channels: integer, bufferFrames: integer ): (Capture?, string?) ``` Read-only. Opens a recording device as interleaved float32 samples at `frequency` and `channels`, holding `bufferFrames` frames. A non-empty `deviceName` selects a device and wins over `device`; neither given takes the platform's default. Returns nil and a reason when nothing opened. ###### Arguments | Name | Type | Description | | --- | --- | --- | | `device` | `integer` | | | `deviceName` | `string?` | | | `frequency` | `integer` | | | `channels` | `integer` | | | `bufferFrames` | `integer` | | ###### Returns | Type | Description | | --- | --- | | `Capture?` | | | `string?` | | ### `LoadMode` _type_ ```nupp type LoadMode = "auto" | "resident" | "stream" ``` Selects residency for a load. `"auto"` measures the clip's duration against the configured streaming threshold, which is what a game that states no preference gets. ## Functions ### `silent` _function_ ```nupp function silent(): Backend ``` Creates a backend that accepts everything and produces no sound. What a machine with no output device gets, and what a headless test gets when it does not care about the command stream. Every call succeeds so that a game needs no branch for the silent case. #### Returns | Type | Description | | --- | --- | | `Backend` | a backend reporting `available` as false | ### `silentDevices` _function_ ```nupp function silentDevices(): Devices ``` Creates a provider that names no devices and opens none. What a build with no audio library gets, and what a device-free test that wants the "nothing is attached" answer reaches for. Both listings are empty with a reason, and `open` refuses rather than handing back a capture that captures nothing, because a game asking for a microphone wants to know it did not get one. That is the one place this contract does not follow `silent`, which does hand back a mixer: a game that cannot make a noise carries on, and a game that cannot hear has to be told. #### Returns | Type | Description | | --- | --- | | `Devices` | a provider whose listings are empty and whose open always refuses | ## Values ### `CLEAR_PLACEMENT` _variable_ ```nupp const CLEAR_PLACEMENT: integer ``` Returns a voice to unpositioned mixing. ### `LOOP` _variable_ ```nupp const LOOP: integer ``` Marks a command that repeats forever. ### `PAUSE` _variable_ ```nupp const PAUSE: integer ``` Holds a voice where it is. ### `PLAY` _variable_ ```nupp const PLAY: integer ``` Starts a voice on the named clip. ### `RESUME` _variable_ ```nupp const RESUME: integer ``` Lets a paused voice carry on. ### `SET_GAIN` _variable_ ```nupp const SET_GAIN: integer ``` Sets the amplitude a voice reaches the mix at. ### `SET_LOOP` _variable_ ```nupp const SET_LOOP: integer ``` Sets whether a voice repeats, reading the `LOOP` flag. ### `SET_MASTER_GAIN` _variable_ ```nupp const SET_MASTER_GAIN: integer ``` Scales the whole output. ### `SET_PITCH` _variable_ ```nupp const SET_PITCH: integer ``` Sets a voice's playback rate. ### `SET_POSITION` _variable_ ```nupp const SET_POSITION: integer ``` Places a voice at `x`, `y` and `z`. ### `SET_STEREO` _variable_ ```nupp const SET_STEREO: integer ``` Pins a voice to the front pair at the gains in `x` and `y`. ### `SPATIAL` _variable_ ```nupp const SPATIAL: integer ``` Marks a command carrying a position. ### `STEREO` _variable_ ```nupp const STEREO: integer ``` Marks a command carrying explicit speaker gains. ### `STOP` _variable_ ```nupp const STOP: integer ``` Ends a voice, over `fade` seconds when that is positive. ### `STREAM` _variable_ ```nupp const STREAM: integer ``` Marks a `PLAY` that reads its input from the file rather than from held samples.