306 lines
12 KiB
Racket
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. |