luv

Workshop wiki

audio.lisp

hal/sdl/audio.lisp

system luv · 24 definitions · on GitHub

Sound out, through SDL3.

An audio-sink is one SDL audio stream bound to the default playback device: the caller pushes interleaved single-float frames at the rate it has them and SDL resamples, mixes, and paces them out to the hardware on its own thread. Everything here is thread-safe in SDL3, so a decoder may feed the sink from wherever it runs while the render thread reads the clock and turns the gain.

The clock is the point. How many frames have been handed over minus how many SDL still holds is how many have gone out the speaker, and that -- not a wall clock -- is what a picture should be shown against, because the ear notices a lip out of step long before the eye notices a picture held a frame too long.

in-package#:luv
cffi:defcstructsdl-audio-spec
format:int
channels:int
cffi:defcfun
"SDL_InitSubSystem"%sdl-init-subsystem
:bool
flags:uint32
cffi:defcfun
"SDL_OpenAudioDeviceStream"%sdl-open-audio-device-stream
:pointer
device:uint32
spec:pointer
callback:pointer
userdata:pointer
cffi:defcfun
"SDL_ResumeAudioStreamDevice"%sdl-resume-audio-stream-device
:bool
stream:pointer
cffi:defcfun
"SDL_PutAudioStreamData"%sdl-put-audio-stream-data
:bool
stream:pointer
buffer:pointer
length:int
cffi:defcfun
"SDL_GetAudioStreamQueued"%sdl-get-audio-stream-queued
:int
stream:pointer
cffi:defcfun
"SDL_ClearAudioStream"%sdl-clear-audio-stream
:bool
stream:pointer
cffi:defcfun
"SDL_SetAudioStreamGain"%sdl-set-audio-stream-gain
:bool
stream:pointer
gain:float
cffi:defcfun
"SDL_DestroyAudioStream"%sdl-destroy-audio-stream
:void
stream:pointer
cffi:defcfun
"SDL_GetError"%sdl-audio-get-error
:string
defconstant+sdl-init-audio+#x10
defconstant+sdl-audio-f32+#x8120"32-bit float, native (little) endian."
defclassaudio-sink
stream:initarg:stream:accessoraudio-sink-stream
rate:initarg:rate:readeraudio-sink-rate
channels:initarg:channels:readeraudio-sink-channels
frames-pushed:initform0:accessoraudio-sink-frames-pushed:documentation"Every frame ever handed to SDL."

The interleaved staging buffer, grown to the largest push so far.

staging:initformnil:accessoraudio-sink-staging
staging-frames:initform0:accessoraudio-sink-staging-frames
:documentation

One playback stream on the default audio device.

defunopen-audio-sink
&key
rate48000
channels2

Open a playback stream taking CHANNELS-channel float frames at rate.

The device is opened at whatever it prefers; SDL converts. Playback starts at once, so a sink with nothing queued is one playing silence.

with-sdl-native-environment
unless
%sdl-init-subsystem+sdl-init-audio+
error"SDL audio initialization failed: ~A"
%sdl-audio-get-error
cffi:with-foreign-object
spec'
setf
cffi:foreign-slot-valuespec''format
+sdl-audio-f32+
cffi:foreign-slot-valuespec''channels
channels
cffi:foreign-slot-valuespec''frequency
rate
let
stream
%sdl-open-audio-device-stream+sdl-audio-device-default-playback+spec
cffi:null-pointer
cffi:null-pointer
when
cffi:null-pointer-pstream
error"SDL could not open an audio stream: ~A"
%sdl-audio-get-error
%sdl-resume-audio-stream-devicestream
make-instance'audio-sink:streamstream:raterate:channelschannels
defunaudio-sink-open-p
sink
not
cffi:null-pointer-p
audio-sink-streamsink
defunclose-audio-sink
sink

Destroy sink's stream, dropping whatever was still queued. Idempotent.

when
%sdl-destroy-audio-stream
audio-sink-streamsink
setf
audio-sink-streamsink
cffi:null-pointer
when
audio-sink-stagingsink
cffi:foreign-free
audio-sink-stagingsink
setf
audio-sink-stagingsink
nil
audio-sink-staging-framessink
0
sink
defunensure-audio-sink-staging
sinkframes
when
<
audio-sink-staging-framessink
frames
when
audio-sink-stagingsink
cffi:foreign-free
audio-sink-stagingsink
setf
audio-sink-stagingsink
cffi:foreign-alloc:float:count
*frames
audio-sink-channelssink
audio-sink-staging-framessink
frames
audio-sink-stagingsink
defunpush-audio-sink-mono
sinksamplescount&key
left1.0
right1.0

Queue count mono samples for sink, placed between the channels by left and right. A two-channel sink gets the sample times left on the left and times right on the right; any other channel count gets it times left everywhere.

This is where a point source becomes a stereo image: the film's sound is one signal, and its place in the room is decided per push, so a listener who walks past the screen hears it cross over.

declare
type
simple-arraysingle-float
samples
typefixnumcount
let*
channels
audio-sink-channelssink
left
coerceleft'single-float
right
coerceright'single-float
declare
typefixnumchannels
typesingle-floatleftright
if
=channels2
dotimes
indexcount
let
sample
arefsamplesindex
setf
cffi:mem-arefstaging:float
*2index
cffi:mem-arefstaging:float
1+
*2index
*sampleright
dotimes
indexcount
let
sample
*left
arefsamplesindex
dotimes
channelchannels
setf
cffi:mem-arefstaging:float
+
*channelsindex
channel
sample
with-sdl-native-environment
unless
%sdl-put-audio-stream-data
audio-sink-streamsink
staging
*countchannels4
error"SDL refused audio data: ~A"
%sdl-audio-get-error
incf
audio-sink-frames-pushedsink
count
sink
defunaudio-sink-queued-frames
sink

How many frames sink is still holding, not yet out the speaker.

if
floor
%sdl-get-audio-stream-queued
audio-sink-streamsink
*4
audio-sink-channelssink
0
defunaudio-sink-played-frames
sink

How many frames have gone out: pushed less still queued. This is the sink's clock, in frames at its own rate.

max0
-
audio-sink-frames-pushedsink
defunclear-audio-sink
sink

Drop everything queued, so the next push is heard at once.

when

What is dropped was never played; the clock must not count it.

let
%sdl-clear-audio-stream
audio-sink-streamsink
setf
audio-sink-frames-pushedsink
played
sink
defun
setfaudio-sink-gain
gainsink

Set sink's overall loudness, 0 silent, 1 as decoded.

when
%sdl-set-audio-stream-gain
audio-sink-streamsink
coerce
max0.0gain
'single-float
gain