Files
gemigreerd-racket-audio/scrbl/libao-async-ffi-racket2.scrbl

306 lines
12 KiB
Racket

#lang scribble/manual
@(require (for-label racket/base
racket/contract
"../libao-async-ffi-racket.rkt"))
@title{Pure Racket Asynchronous libao Backend}
@defmodule[racket-audio/libao-async-ffi-racket]
This module implements the asynchronous libao playback backend used by
@racketmodname[racket-audio]. It provides the same public Racket API as the
older C-backed asynchronous player, but keeps the queueing, buffering,
conversion and worker-thread logic in Racket. The only foreign calls made by
this module are the direct calls into Xiph's libao library.
The module is intended as a low-level backend. Higher-level player code should
normally use the public audio-player interface instead of calling this module
directly. It is documented here because it defines the exact contract between
decoded PCM data and the libao output path.
@section{Overview}
The backend accepts decoded PCM buffers, converts them when needed, groups small
buffers into larger playback chunks, and sends those chunks to libao from a
dedicated Racket worker thread. The worker thread calls @racket[ao_play] as a
blocking foreign call, so other Racket threads and places do not have to wait
for the audio device to accept more data.
Incoming buffers may be interleaved or planar. Planar buffers, such as those
commonly produced by a FLAC decoder, are converted to interleaved PCM before
playback. If the requested sample width cannot be opened on the selected audio
device, the backend tries lower-width output formats and converts samples before
they are sent to libao.
The backend also maintains playback position metadata. Each queued buffer is
tagged with a music id, a current playback position and a duration. These
values are used by the higher-level player to report where the audio device is
in the current track.
@section{Buffer information}
@defproc[(make-buffer-info [type symbol?]
[sample-bits exact-positive-integer?]
[sample-rate exact-positive-integer?]
[channels exact-positive-integer?]
[endianness symbol?])
any/c]{
Creates a buffer description object for PCM data passed to
@racket[ao_play_async].
The @racket[type] field describes the memory layout. The supported values are
@racket['interleaved] for normal interleaved PCM and @racket['planar] for planar
PCM. For compatibility with older code, @racket['ao] is treated as interleaved
by convention and @racket['flac] is accepted as planar input.
The @racket[sample-bits], @racket[sample-rate] and @racket[channels] fields
describe the format of the supplied buffer, not necessarily the format that will
eventually be accepted by the device. The backend may convert the sample width
to the actual device width.
The @racket[endianness] field must be one of @racket['little-endian],
@racket['big-endian] or @racket['native-endian]. It is used when samples are
converted between different sample widths or byte orders.}
@defproc[(make-BufferInfo_t [type symbol?]
[sample-bits exact-positive-integer?]
[sample-rate exact-positive-integer?]
[channels exact-positive-integer?]
[endianness symbol?])
any/c]{
Compatibility alias for @racket[make-buffer-info]. The name matches the older
FFI module and the former C structure naming convention.}
@section{Creating and closing a backend}
@defproc[(ao_version_async) exact-integer?]{
Returns the version number of this asynchronous backend implementation. The
current implementation returns @racket[3]. The value is useful for diagnostics
when multiple asynchronous backend implementations exist.}
@defproc[(ao_create_async [bits exact-positive-integer?]
[rate exact-positive-integer?]
[channels exact-positive-integer?]
[byte-format symbol?]
[wav-output-file (or/c #f path-string?)])
any/c]{
Opens a libao output device and creates an asynchronous playback handle.
The @racket[bits], @racket[rate], @racket[channels] and @racket[byte-format]
arguments describe the preferred output format. The byte format must be one of
@racket['little-endian], @racket['big-endian] or @racket['native-endian].
When @racket[wav-output-file] is @racket[#f], the default live libao driver is
used. When it is a path string, the backend opens libao's @tt{wav} driver and
writes the audio stream to that file instead.
The backend first tries to open the requested sample width. If that fails and
the requested width is greater than 24 bits, it tries 24-bit output. If that
also fails and the requested width is greater than 16 bits, it tries 16-bit
output. The actual device width can be queried with
@racket[ao_real_output_bits_async].
The function returns a playback handle on success and @racket[#f] when no
suitable libao device could be opened.}
@defproc[(ao_stop_async [handle any/c]) any/c]{
Stops the worker thread, clears pending audio, closes the libao device and
invalidates @racket[handle].
The stop operation first clears all queued buffers, then queues an internal stop
command, waits for the playback thread to terminate, and finally closes the
underlying libao handle. Calling this function on an already invalid handle is
an error.}
@section{Submitting audio}
@defproc[(ao_play_async [handle any/c]
[music-id any/c]
[at-second real?]
[music-duration real?]
[buf-size exact-nonnegative-integer?]
[au-buf (or/c bytes? any/c)]
[info any/c])
void?]{
Queues a PCM buffer for asynchronous playback.
The @racket[music-id], @racket[at-second] and @racket[music-duration] values are
stored together with the queued buffer. They do not affect sample conversion,
but they allow the player to report the current track id, playback position and
track duration while the worker thread is playing the queued data.
The @racket[buf-size] argument gives the number of valid bytes in
@racket[au-buf]. The input buffer is copied into backend-owned memory before
the function returns, so the caller may reuse or discard the original byte
string after the call.
The @racket[info] argument should be created with @racket[make-buffer-info]. If
the buffer is planar, it is converted to interleaved PCM. If the buffer's
sample width or byte order differs from the actual libao device format, the
backend converts it before queueing.
The backend groups smaller buffers into larger playback chunks. This reduces
the number of calls to libao and helps prevent underruns. Buffers with
different @racket[music-id] values are not merged into the same output chunk.}
@defproc[(ao_clear_async [handle any/c]) any/c]{
Clears all queued audio buffers that have not yet been played.
The current aggregation buffer is also cleared. Already playing audio may still
finish at the device level, depending on what libao and the operating system
have accepted. This operation is used by higher-level code when stopping,
seeking or replacing the current stream.}
@section{Playback state}
@defproc[(ao_is_at_second_async [handle any/c]) real?]{
Returns the playback position associated with the most recently dequeued buffer.
This value is the @racket[at-second] value supplied to @racket[ao_play_async],
not a sample-accurate query into the audio device.}
@defproc[(ao_is_at_music_id_async [handle any/c]) any/c]{
Returns the music id associated with the most recently dequeued buffer. The
higher-level player uses this value to determine which track the output thread
has reached.}
@defproc[(ao_music_duration_async [handle any/c]) real?]{
Returns the duration associated with the most recently dequeued buffer. This is
the @racket[music-duration] value supplied to @racket[ao_play_async].}
@defproc[(ao_bufsize_async [handle any/c]) exact-nonnegative-integer?]{
Returns the number of queued PCM bytes that have been accepted by the backend
but not yet removed from the asynchronous queue. This is a backend queue size,
not the size of the operating-system or hardware audio buffer.}
@defproc[(ao_sample_queue_len [handle any/c]) exact-nonnegative-integer?]{
Returns the number of queued playback elements waiting in the backend queue.
This is mainly useful for diagnostics and tuning.}
@defproc[(ao_reuse_buf_len [handle any/c]) exact-nonnegative-integer?]{
Returns the number of reusable internal buffers currently kept by the backend.
This is a diagnostic value that can help detect excessive allocation or
unexpected buffer retention.}
@section{Pause and volume}
@defproc[(ao_pause_async [handle any/c]
[paused (or/c boolean? integer?)])
void?]{
Pauses or resumes the playback worker.
When @racket[paused] is @racket[#t], or an integer other than @racket[0], the
worker thread is blocked before it dequeues the next element. When
@racket[paused] is @racket[#f] or @racket[0], playback is resumed.
Pausing does not prevent producers from queueing additional buffers. It only
prevents the worker thread from taking more data from the queue.}
@defproc[(ao_set_volume_async [handle any/c]
[percentage real?])
void?]{
Sets the output volume as a percentage.
A value of @racket[100.0] means unchanged volume. Values below
@racket[100.0] attenuate the signal. Values above @racket[100.0] amplify the
signal and are clipped to the signed range of the actual device sample width.
Internally the value is stored as an integer in hundredths of a percent: for
example, @racket[100.0] becomes @racket[10000]. Values very close to
@racket[100.0] are normalized to exactly @racket[10000] to avoid unnecessary
sample processing.}
@defproc[(ao_volume_async [handle any/c]) real?]{
Returns the currently configured output volume percentage.}
@section{Output format}
@defproc[(ao_real_output_bits_async [handle any/c])
exact-nonnegative-integer?]{
Returns the actual sample width opened on the libao device.
This may be lower than the requested width passed to @racket[ao_create_async].
For example, a request for 32-bit output may result in a 24-bit or 16-bit device
when the default libao driver cannot open the preferred format. In that case,
@racket[ao_play_async] converts the incoming samples before playback.}
@section{Playback buffer tuning}
@defproc[(ao-playback-buf-ms) exact-nonnegative-integer?]{
Returns the target size, in milliseconds, of the playback chunks that the
backend sends to libao. The default is @racket[150].}
@defproc[(ao-set-playback-buf-ms! [ms exact-nonnegative-integer?])
void?]{
Sets the target playback chunk size in milliseconds.
Larger values reduce the number of calls to libao and may help prevent audible
glitches when decoders produce many small buffers. Smaller values reduce
latency but increase scheduling pressure on the Racket worker thread and on the
audio backend.}
@section{Implementation notes}
The worker thread is created with its own thread pool and uses libao's
@racket[ao_play] through a blocking FFI call. Before calling libao, the worker
copies the queued bytes into memory allocated with @racket['atomic-interior].
This is important because a blocking foreign call must not be handed a pointer
to movable Racket memory that could be relocated by the garbage collector while
the foreign function is still using it.
The backend keeps a small pool of previously allocated buffers. Buffers created
internally for conversion or aggregation can be reused after playback. This
reduces allocation pressure during continuous playback.
The module initializes libao when the first handle is opened and shuts libao
down when the last handle is closed. This keeps libao lifetime management local
to the backend and avoids repeated global initialization during normal playback.
@section{Example}
@racketblock[
(define h
(ao_create_async 32 44100 2 'native-endian #f))
(define info
(make-buffer-info 'interleaved 32 44100 2 'native-endian))
(when h
(ao_play_async h
1
0.0
180.0
(bytes-length pcm-bytes)
pcm-bytes
info)
(ao_set_volume_async h 80.0)
(ao_pause_async h #t)
(ao_pause_async h #f)
(ao_stop_async h))
]
The example opens the default live libao device, queues one interleaved
32-bit PCM buffer, lowers the volume to 80 percent, briefly pauses and resumes
the worker, and finally closes the backend.