#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.