(sigil audio sink)

Portable streaming audio-sink facade

This is the target-independent audio sink API. It presents ONE surface for pushing interleaved float32 PCM into a device, and selects its backend with cond-expand:

  • native (else): the SPSC ring living in (sigil audio), drained by the playback device (miniaudio)
  • wasm: the WebAudio bridge in (sigil browser audio-sink), provided by the sigil-wasm-audio package

This mirrors how (sigil websocket) selects a native socket stack vs the (sigil browser websocket) bridge. Application code imports THIS module and the same source compiles for both targets.

Context lifecycle

WebAudio requires an AudioContext that starts suspended and must be resumed from a user gesture (autoplay policy). The context ops model that. On native, the device layer owns a single always-running device, so the context ops are thin shims (open initializes the device; resume is a no-op; the sample rate is a fixed 44100). They exist so the SAME source compiles and runs on both targets behind this facade.

(import (sigil audio sink))
(define ctx (audio-context-open))
(audio-context-resume ctx)                  ; from a user gesture on wasm
(let ((rate (audio-context-sample-rate ctx))
      (s    (open-audio-stream channels: 2 buffer-frames: 8192)))
  (push-audio-samples s pcm n-frames)       ; interleaved f32
  (audio-stream-depth s))                    ; backpressure accounting

Streams below the device's rate

A producer that renders below the device's rate (a synth stepped down by (sigil audio ladder)) passes rate: to open-audio-stream, or calls set-audio-stream-rate! when it steps; the sink upsamples. Frames already queued keep the rate they were pushed at, so a step needs no reopen.

Cues

A cue is a short sound a game triggers: a tap, a hit. Pushed into a stream it would be heard one ring-depth late (a 256 ms ring is a 256 ms delay). Load it once and play it by handle instead; it is mixed at output time:

(define tap (load-audio-cue (render-tap rate) channels: 1))
(play-audio-cue tap volume: 0.8 pan: -0.2)

On the web the cue is mixed in an AudioWorklet in the next render quantum (2.7 ms at 48 kHz); natively it plays through play-sound, mixed in the device callback. Neither waits behind a stream's ring.

Underruns

audio-stream-underruns counts output frames a stream could not fill. audio-stream-underruns-sounding is the part a listener heard as a gap: the stream ran dry within 128 frames of a sample above -80 dB. The rest (audio-stream-underruns-idle) is a quiet or unfed stream running dry.

Exports

Open the audio context. On native this initializes the playback device and returns an opaque non-#f handle (the device layer owns one device, so the handle is a sentinel). On wasm this creates a suspended AudioContext and returns its id.

Resume a suspended context from a user gesture. Native contexts are never suspended, so this is a no-op that reports success.

The context sample rate in Hz. The device runs at a fixed 44100; a stream fed at another rate says so with set-audio-stream-rate!.

The context run state: one of 'suspended, 'running, 'closed. Native contexts are always 'running once opened.

The underrun frames nobody heard: a quiet (or never fed) stream ran dry.

audio-cue?procedure

Is x a cue handle from load-audio-cue?

Load a cue: interleaved float32 PCM at the context's sample rate, mono (channels: 1) or stereo (channels: 2, the default). The PCM is copied. Returns a cue handle, or #f.

Play a cue now. volume 0.0-1.0; pan -1.0 (left) to 1.0 (right). Returns #t if the play was accepted (16 sounds play at once natively; a play past that is refused).

Forget a cue: later plays are refused. Natively its PCM stays in memory until the program exits (see cues).

Silence every cue now playing. Natively this is stop-all-sounds: it stops every play-sound sound, cue or not.

The master cue gain, 0.0-1.0. Natively it scales every play-sound sound, including ones already playing.

How cues are played: 'device natively (the device callback).

audio-cue-lagprocedure

The measured wait of the last cue play, frames. Only the web worklet measures one; natively #f.

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)

(No description)