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

ConstructorDescription
newCommandCreates one zeroed command, for a caller filling a reusable buffer.

Types

TypeKindDescription
BackendrecordMixes sound for one output.
CapturerecordCaptures sound from one recording device, pulled rather than pushed.
ClipInforecordReports what a load settled on.
CommandrecordRequests one operation from a backend.
DevicerecordNames one physical audio device.
DevicesrecordNames the devices attached now and opens one for recording.
LoadModetypeSelects residency for a load.

Functions

FunctionKindDescription
silentfunctionCreates a backend that accepts everything and produces no sound.
silentDevicesfunctionCreates a provider that names no devices and opens none.

Values

ValueKindDescription
CLEAR_PLACEMENTvariableReturns a voice to unpositioned mixing.
LOOPvariableMarks a command that repeats forever.
PAUSEvariableHolds a voice where it is.
PLAYvariableStarts a voice on the named clip.
RESUMEvariableLets a paused voice carry on.
SET_GAINvariableSets the amplitude a voice reaches the mix at.
SET_LOOPvariableSets whether a voice repeats, reading the LOOP flag.
SET_MASTER_GAINvariableScales the whole output.
SET_PITCHvariableSets a voice's playback rate.
SET_POSITIONvariablePlaces a voice at x, y and z.
SET_STEREOvariablePins a voice to the front pair at the gains in x and y.
SPATIALvariableMarks a command carrying a position.
STEREOvariableMarks a command carrying explicit speaker gains.
STOPvariableEnds a voice, over fade seconds when that is positive.
STREAMvariableMarks a PLAY that reads its input from the file rather than from held samples.

Constructors#

newCommandconstructor#

function newCommand(): Command

Creates one zeroed command, for a caller filling a reusable buffer.

Returns

TypeDescription
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
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#
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
NameTypeDescription
commands{Command}
countinteger
Returns
TypeDescription
nil
drain#
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
NameTypeDescription
out{integer}
Returns
TypeDescription
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
NameTypeDescription
pathstring
clipinteger
modeLoadMode
streamSecondsnumber
Returns
TypeDescription
ClipInfo?
string?
releaseClip#
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
NameTypeDescription
clipinteger
Returns
TypeDescription
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
NameTypeDescription
handleinteger
Returns
TypeDescription
number?
seek#
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
NameTypeDescription
handleinteger
secondsnumber
Returns
TypeDescription
boolean
decoders#
decoders: function(): {string}

Read-only. Reports the decoder names this build linked, in the backend's own order.

Returns
TypeDescription
{string}
close#
close: function(): nil

Read-only. Stops everything and closes the output. Calling it twice is harmless.

Returns
TypeDescription
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: boolean

Read-only. Reports whether a real output opened. A false here still takes every command and answers every query; nothing is heard.

frequency#
frequency: integer

Read-only. Reports the output frequency in frames per second.

channels#
channels: integer

Read-only. Reports the number of output channels.

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
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#
availableFrames: function(): integer

Read-only. Reports the complete frames ready to read without waiting.

Returns
TypeDescription
integer
overruns#
overruns: function(): integer

Read-only. Reports the frames dropped because nothing read them in time, counted since the capture opened.

Returns
TypeDescription
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
NameTypeDescription
maxFramesinteger
Returns
TypeDescription
string?
string?
readInto#
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
NameTypeDescription
out{number}
maxFramesinteger
Returns
TypeDescription
integer
write#
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
NameTypeDescription
samples{number}
framesinteger
Returns
TypeDescription
integer
pause#
pause: function(): (boolean, string?)

Read-only. Stops the device filling the buffer, keeping what is already in it.

Returns
TypeDescription
boolean
string?
resume#
resume: function(): (boolean, string?)

Read-only. Starts the device filling the buffer again.

Returns
TypeDescription
boolean
string?
close#
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
TypeDescription
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: 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#
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.

ClipInforecord#

record ClipInfo
    duration: number
    resident: boolean
end

Reports what a load settled on.

Fields

duration#
duration: number

Read-only. Reports the audio duration in seconds, and zero when the input cannot say.

resident#
resident: boolean

Read-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
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#
kind: integer

Caller-writable. Selects the operation from this module's kind constants.

handle#
handle: integer

Caller-writable. Names the voice as the packed handle Tecs issued, and zero for an operation on the whole output.

clip#
clip: integer

Caller-writable. Selects the clip a PLAY reads, by the interned index tecs.audio.clipId assigned.

flags#
flags: integer

Caller-writable. Carries the LOOP, SPATIAL and STEREO bits.

gain#
gain: number

Caller-writable. Sets linear amplitude for PLAY, SET_GAIN and SET_MASTER_GAIN.

pitch#
pitch: number

Caller-writable. Sets the playback rate for PLAY and SET_PITCH.

start#
start: number

Caller-writable. Sets where a PLAY begins, in seconds.

fade#
fade: number

Caller-writable. Sets the fade duration in seconds, running in on a PLAY and out on a STOP.

loopStart#
loopStart: number

Caller-writable. Sets the position in seconds a repeat returns to.

x#
x: number

Caller-writable. Sets the position right of the listener, or the left-speaker gain for SET_STEREO.

y#
y: number

Caller-writable. Sets the position above the listener, or the right-speaker gain for SET_STEREO.

z#
z: number

Caller-writable. Sets the position behind the listener.

Devicerecord#

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#
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#
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#
frequency: integer

Read-only. Reports the device's preferred frames per second, and zero when it will not say.

channels#
channels: integer

Read-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?)
end

Names 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
TypeDescription
{Device}
string?
recording#
recording: function(): ({Device}, string?)

Read-only. Names every recording device attached now, on the same terms as playback.

Returns
TypeDescription
{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
NameTypeDescription
deviceinteger
deviceNamestring?
frequencyinteger
channelsinteger
bufferFramesinteger
Returns
TypeDescription
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(): 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

TypeDescription
Backend

a backend reporting available as false

silentDevicesfunction#

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

TypeDescription
Devices

a provider whose listings are empty and whose open always refuses

Values#

CLEAR_PLACEMENTvariable#

const CLEAR_PLACEMENT: integer

Returns a voice to unpositioned mixing.

LOOPvariable#

const LOOP: integer

Marks a command that repeats forever.

PAUSEvariable#

const PAUSE: integer

Holds a voice where it is.

PLAYvariable#

const PLAY: integer

Starts a voice on the named clip.

RESUMEvariable#

const RESUME: integer

Lets a paused voice carry on.

SET_GAINvariable#

const SET_GAIN: integer

Sets the amplitude a voice reaches the mix at.

SET_LOOPvariable#

const SET_LOOP: integer

Sets whether a voice repeats, reading the LOOP flag.

SET_MASTER_GAINvariable#

const SET_MASTER_GAIN: integer

Scales the whole output.

SET_PITCHvariable#

const SET_PITCH: integer

Sets a voice's playback rate.

SET_POSITIONvariable#

const SET_POSITION: integer

Places a voice at x, y and z.

SET_STEREOvariable#

const SET_STEREO: integer

Pins a voice to the front pair at the gains in x and y.

SPATIALvariable#

const SPATIAL: integer

Marks a command carrying a position.

STEREOvariable#

const STEREO: integer

Marks a command carrying explicit speaker gains.

STOPvariable#

const STOP: integer

Ends a voice, over fade seconds when that is positive.

STREAMvariable#

const STREAM: integer

Marks a PLAY that reads its input from the file rather than from held samples.