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.
local backend <const> = tecs.platform.audiobackend.silent()
local buffer <const> = {tecs.platform.audiobackend.newCommand()}
buffer[1].kind = tecs.platform.audiobackend.SET_MASTER_GAIN
buffer[1].gain = 0.5
backend.submit(buffer, 1)Module contents
Constructors
| Constructor | Description |
|---|---|
newCommand | Creates one zeroed command, for a caller filling a reusable buffer. |
Types
| Type | Kind | Description |
|---|---|---|
Backend | record | Mixes sound for one output. |
Capture | record | Captures sound from one recording device, pulled rather than pushed. |
ClipInfo | record | Reports what a load settled on. |
Command | record | Requests one operation from a backend. |
Device | record | Names one physical audio device. |
Devices | record | Names the devices attached now and opens one for recording. |
LoadMode | type | Selects residency for a load. |
Functions
| Function | Kind | Description |
|---|---|---|
silent | function | Creates a backend that accepts everything and produces no sound. |
silentDevices | function | Creates a provider that names no devices and opens none. |
Values
| Value | Kind | Description |
|---|---|---|
CLEAR_PLACEMENT | variable | Returns a voice to unpositioned mixing. |
LOOP | variable | Marks a command that repeats forever. |
PAUSE | variable | Holds a voice where it is. |
PLAY | variable | Starts a voice on the named clip. |
RESUME | variable | Lets a paused voice carry on. |
SET_GAIN | variable | Sets the amplitude a voice reaches the mix at. |
SET_LOOP | variable | Sets whether a voice repeats, reading the LOOP flag. |
SET_MASTER_GAIN | variable | Scales the whole output. |
SET_PITCH | variable | Sets a voice's playback rate. |
SET_POSITION | variable | Places a voice at x, y and z. |
SET_STEREO | variable | Pins a voice to the front pair at the gains in x and y. |
SPATIAL | variable | Marks a command carrying a position. |
STEREO | variable | Marks a command carrying explicit speaker gains. |
STOP | variable | Ends a voice, over fade seconds when that is positive. |
STREAM | variable | Marks a PLAY that reads its input from the file rather than from held samples. |
Constructors#
newCommandconstructor#
function newCommand(): CommandCreates one zeroed command, for a caller filling a reusable buffer.
Returns
| Type | Description |
|---|---|
Command | a command whose every field is at rest |
Types#
Backendrecord#
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
endMixes 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#
submit: function(commands: {Command}, count: integer): nilRead-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#
drain: function(out: {integer}): integerRead-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#
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#
releaseClip: function(clip: integer): nilRead-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#
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#
seek: function(handle: integer, seconds: number): booleanRead-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#
decoders: function(): {string}Read-only. Reports the decoder names this build linked, in the backend's own order.
Returns
| Type | Description |
|---|---|
{string} |
close#
close: function(): nilRead-only. Stops everything and closes the output. Calling it twice is harmless.
Returns
| Type | Description |
|---|---|
nil |
Fields
failure#
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#
available: booleanRead-only. Reports whether a real output opened. A false here still takes every command and answers every query; nothing is heard.
Capturerecord#
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
endCaptures 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#
availableFrames: function(): integerRead-only. Reports the complete frames ready to read without waiting.
Returns
| Type | Description |
|---|---|
integer |
overruns#
overruns: function(): integerRead-only. Reports the frames dropped because nothing read them in time, counted since the capture opened.
Returns
| Type | Description |
|---|---|
integer |
read#
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#
readInto: function(out: {number}, maxFrames: integer): integerRead-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#
write: function(samples: {number}, frames: integer): integerRead-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#
pause: function(): (boolean, string?)Read-only. Stops the device filling the buffer, keeping what is already in it.
Returns
| Type | Description |
|---|---|
boolean | |
string? |
resume#
resume: function(): (boolean, string?)Read-only. Starts the device filling the buffer again.
Returns
| Type | Description |
|---|---|
boolean | |
string? |
close#
close: function(): nilRead-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#
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#
frequency: integerRead-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#
channels: integerRead-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.
ClipInforecord#
Reports what a load settled on.
Fields
duration#
duration: numberRead-only. Reports the audio duration in seconds, and zero when the input cannot say.
resident#
resident: booleanRead-only. Reports whether decoded samples stay in memory and are shared by every voice reading the clip.
Commandrecord#
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
endRequests 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
handle#
handle: integerCaller-writable. Names the voice as the packed handle Tecs issued, and zero for an operation on the whole output.
clip#
clip: integerCaller-writable. Selects the clip a PLAY reads, by the interned index tecs.audio.clipId assigned.
fade#
fade: numberCaller-writable. Sets the fade duration in seconds, running in on a PLAY and out on a STOP.
x#
x: numberCaller-writable. Sets the position right of the listener, or the left-speaker gain for SET_STEREO.
y#
y: numberCaller-writable. Sets the position above the listener, or the right-speaker gain for SET_STEREO.
Devicerecord#
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#
id: integerRead-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#
name: stringRead-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#
frequency: integerRead-only. Reports the device's preferred frames per second, and zero when it will not say.
channels#
channels: integerRead-only. Reports the device's preferred channels per frame, and zero when it will not say.
Devicesrecord#
record Devices
playback: function(): ({Device}, string?)
recording: function(): ({Device}, string?)
open: function(
device: integer,
deviceName: string?,
frequency: integer,
channels: integer,
bufferFrames: integer
): (Capture?, string?)
endNames the devices attached now and opens one for recording.
Separate from 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#
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#
recording: function(): ({Device}, string?)Read-only. Names every recording device attached now, on the same terms as playback.
Returns
| Type | Description |
|---|---|
{Device} | |
string? |
open#
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? |
LoadModetype#
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#
silentfunction#
function silent(): BackendCreates 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 |
silentDevicesfunction#
function silentDevices(): DevicesCreates 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_PLACEMENTvariable#
const CLEAR_PLACEMENT: integerReturns a voice to unpositioned mixing.
LOOPvariable#
const LOOP: integerMarks a command that repeats forever.
PAUSEvariable#
const PAUSE: integerHolds a voice where it is.
PLAYvariable#
const PLAY: integerStarts a voice on the named clip.
RESUMEvariable#
const RESUME: integerLets a paused voice carry on.
SET_GAINvariable#
const SET_GAIN: integerSets the amplitude a voice reaches the mix at.
SET_LOOPvariable#
const SET_LOOP: integerSets whether a voice repeats, reading the LOOP flag.
SET_MASTER_GAINvariable#
const SET_MASTER_GAIN: integerScales the whole output.
SET_PITCHvariable#
const SET_PITCH: integerSets a voice's playback rate.
SET_POSITIONvariable#
const SET_POSITION: integerPlaces a voice at x, y and z.
SET_STEREOvariable#
const SET_STEREO: integerPins a voice to the front pair at the gains in x and y.
SPATIALvariable#
const SPATIAL: integerMarks a command carrying a position.
STEREOvariable#
const STEREO: integerMarks a command carrying explicit speaker gains.
STOPvariable#
const STOP: integerEnds a voice, over fade seconds when that is positive.
STREAMvariable#
const STREAM: integerMarks a PLAY that reads its input from the file rather than from held samples.