265 lines
11 KiB
Racket
265 lines
11 KiB
Racket
#lang scribble/manual
|
|
|
|
@(require (for-label racket/base
|
|
racket/contract
|
|
"../libao-async-ffi-racket.rkt"))
|
|
|
|
@title{Asynchronous libao playback in Racket}
|
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
|
|
|
@defmodule[(file "../libao-async-ffi-racket.rkt")]
|
|
|
|
This module is a pure Racket replacement for the older C based
|
|
@filepath{ao_playasync.c} backend. It exports the same Racket level API as
|
|
@filepath{libao-async-ffi.rkt} and still sends PCM to Xiph libao, but the
|
|
queue, worker thread, buffering, format conversion and volume scaling are all
|
|
implemented in Racket.
|
|
|
|
The module is meant to sit below the higher level sound player. Client code
|
|
creates one asynchronous audio handle, queues PCM buffers with playback
|
|
position information, and lets a Racket worker thread feed libao. The foreign
|
|
@racket[ao_play] call is declared with @racket[#:blocking?] so that a blocking
|
|
write to the audio device does not unnecessarily hold up other Racket threads.
|
|
|
|
@section{Basic example}
|
|
|
|
The normal live playback path opens the default libao driver. The output bit
|
|
format may be lower than the requested bit format when libao cannot open the
|
|
device with the requested precision.
|
|
|
|
@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) pcm info)
|
|
(ao_stop_async h))]
|
|
|
|
To write to a WAV file instead of the default live device, pass a path string
|
|
as the last argument to @racket[ao_create_async] instead of @racket[#f]
|
|
@elem{.}
|
|
|
|
@racketblock[
|
|
(define h
|
|
(ao_create_async 16 48000 2 'little-endian "test-output.wav"))]
|
|
|
|
@section{Playback handles}
|
|
|
|
@defproc[(ao_version_async) exact-integer?]{
|
|
Returns the implementation version of this asynchronous backend. The current
|
|
module returns @racket[3]@elem{.}
|
|
}
|
|
|
|
@defproc[(ao_create_async
|
|
[bits exact-integer?]
|
|
[rate exact-integer?]
|
|
[channels exact-integer?]
|
|
[byte-format (or/c 'little-endian 'big-endian 'native-endian)]
|
|
[wav-output-file (or/c #f path-string?)])
|
|
any/c]{
|
|
Creates an asynchronous audio handle. When @racket[wav-output-file] is
|
|
@racket[#f] the default live libao driver is opened. Otherwise the libao
|
|
@tt{wav} driver is opened and the samples are written to the named file.
|
|
|
|
The requested format is described by @racket[bits]@elem{,} @racket[rate]@elem{,}
|
|
@racket[channels] and @racket[byte-format]@elem{.} If the requested number of
|
|
bits cannot be opened and the request is wider than 24 or 16 bits, the module
|
|
tries 24-bit and then 16-bit output. The resulting device precision can be
|
|
queried with @racket[ao_real_output_bits_async]@elem{.}
|
|
|
|
The result is an asynchronous audio handle, or @racket[#f] when no device or
|
|
output file could be opened. Handles should be closed with
|
|
@racket[ao_stop_async]@elem{.} A finalizer is also registered as a safety net,
|
|
but explicit stopping is the intended lifecycle.
|
|
}
|
|
|
|
@defproc[(ao_stop_async [handle any/c]) any/c]{
|
|
Stops playback, clears queued data, wakes the worker thread when it is paused,
|
|
queues a stop command, waits for the worker thread to finish, closes the libao
|
|
device and marks the handle as invalid. The function returns the handle.
|
|
Calling the other playback functions on an invalid handle is an error.
|
|
}
|
|
|
|
@defproc[(ao_clear_async [handle any/c]) any/c]{
|
|
Clears queued audio data without closing the device. The buffer that is being
|
|
assembled for the next queued play item is also discarded. This is the
|
|
operation used when playback is stopped or when the higher layer seeks.
|
|
}
|
|
|
|
@defproc[(ao_pause_async [handle any/c]
|
|
[paused any/c]) any/c]{
|
|
Pauses or resumes the worker thread. A boolean value is used directly. An
|
|
integer value is accepted for compatibility with the old FFI layer, where
|
|
@racket[0] means resume and any other integer means pause.
|
|
}
|
|
|
|
@section{Buffer descriptions}
|
|
|
|
@defproc[(make-buffer-info
|
|
[type symbol?]
|
|
[sample-bits exact-integer?]
|
|
[sample-rate exact-integer?]
|
|
[channels exact-integer?]
|
|
[endianness (or/c 'little-endian 'big-endian 'native-endian)])
|
|
any/c]{
|
|
Constructs the format description passed to @racket[ao_play_async]@elem{.}
|
|
Only the constructor is exported. The struct predicate and accessors remain
|
|
private to this module, because the value is primarily a compatibility object
|
|
for the audio backend.
|
|
|
|
The @racket[type] field controls whether the incoming buffer is already
|
|
interleaved. Use @racket['interleaved] or the older name @racket['ao] for
|
|
ordinary PCM in frame order. Use @racket['planar] or the older name
|
|
@racket['flac] when each channel is stored as a separate plane. Planar input
|
|
is copied to an interleaved buffer before it is queued.
|
|
|
|
Samples are treated as signed integer PCM. @racket[sample-bits] must describe
|
|
whole bytes, such as 16, 24 or 32 bits. The conversion code uses the supplied
|
|
endianness when reading input samples and when writing converted output samples.
|
|
}
|
|
|
|
@defproc[(make-BufferInfo_t
|
|
[type symbol?]
|
|
[sample-bits exact-integer?]
|
|
[sample-rate exact-integer?]
|
|
[channels exact-integer?]
|
|
[endianness (or/c 'little-endian 'big-endian 'native-endian)])
|
|
any/c]{
|
|
Compatibility alias for @racket[make-buffer-info]@elem{.} The name is kept so
|
|
code that used the older FFI module can keep constructing buffer descriptions
|
|
without changing call sites.
|
|
}
|
|
|
|
@section{Queuing audio}
|
|
|
|
@defproc[(ao_play_async
|
|
[handle any/c]
|
|
[music-id any/c]
|
|
[at-second real?]
|
|
[music-duration real?]
|
|
[buf-size exact-integer?]
|
|
[audio-buffer any/c]
|
|
[buffer-info any/c])
|
|
any/c]{
|
|
Queues a PCM buffer for asynchronous playback. @racket[audio-buffer] may be a
|
|
byte string, or an internal reusable memory object produced by this backend.
|
|
External callers normally pass a byte string. @racket[buf-size] is the number
|
|
of valid bytes in the buffer.
|
|
|
|
The position values @racket[at-second]@elem{,} @racket[music-duration] and
|
|
@racket[music-id] are copied into the queue element. When the worker thread
|
|
starts playing that element, these values become visible through
|
|
@racket[ao_is_at_second_async]@elem{,} @racket[ao_music_duration_async] and
|
|
@racket[ao_is_at_music_id_async]@elem{.}
|
|
|
|
If the input buffer is planar, it is first converted to interleaved PCM. If the
|
|
input sample width or endianness differs from the opened output device, the
|
|
sample data is converted before queueing. Small input chunks are collected
|
|
into larger queue elements of roughly 250 milliseconds, unless the music id
|
|
changes or the current assembled buffer is full.
|
|
}
|
|
|
|
@section{Playback state}
|
|
|
|
@defproc[(ao_is_at_second_async [handle any/c]) real?]{
|
|
Returns the playback position, in seconds, associated with the queue element
|
|
most recently taken by the worker thread. This is not measured by libao; it is
|
|
the position reported by the producer of the PCM buffer.
|
|
}
|
|
|
|
@defproc[(ao_is_at_music_id_async [handle any/c]) any/c]{
|
|
Returns the music id associated with the queue element most recently taken by
|
|
the worker thread. The value is whatever was passed as @racket[music-id] to
|
|
@racket[ao_play_async]@elem{.}
|
|
}
|
|
|
|
@defproc[(ao_music_duration_async [handle any/c]) real?]{
|
|
Returns the duration value associated with the queue element most recently
|
|
taken by the worker thread. The value is copied from the
|
|
@racket[music-duration] argument passed to @racket[ao_play_async]@elem{.}
|
|
}
|
|
|
|
@defproc[(ao_bufsize_async [handle any/c]) exact-integer?]{
|
|
Returns the number of audio bytes currently counted as buffered by the async
|
|
queue administration. The value is updated when buffers are queued, combined,
|
|
taken by the worker thread or cleared.
|
|
}
|
|
|
|
@defproc[(ao_sample_queue_len [handle any/c]) exact-integer?]{
|
|
Returns the number of queue elements currently counted by the async queue
|
|
administration. Since the module combines small input buffers into larger
|
|
playback chunks, this is not the same as the number of calls made to
|
|
@racket[ao_play_async]@elem{.}
|
|
}
|
|
|
|
@defproc[(ao_reuse_buf_len [handle any/c]) exact-integer?]{
|
|
Returns the number of reusable byte buffers currently kept by the backend.
|
|
This is an implementation diagnostic. It is useful for checking whether the
|
|
reuse pool is being exercised, but it is not an audio latency measurement.
|
|
}
|
|
|
|
@section{Volume and output format}
|
|
|
|
@defproc[(ao_set_volume_async [handle any/c]
|
|
[percentage real?]) any/c]{
|
|
Sets the software volume. @racket[100.0] is normal volume,
|
|
@racket[50.0] is half volume and values above @racket[100.0] amplify the
|
|
samples. The implementation stores the setting as an integer scaled by 100,
|
|
so normal volume is represented internally as 10000.
|
|
|
|
Volume is applied by the worker thread immediately before copying the playback
|
|
chunk to the foreign buffer passed to libao. Scaled samples are clipped to the
|
|
signed range of the opened output sample width.
|
|
}
|
|
|
|
@defproc[(ao_volume_async [handle any/c]) real?]{
|
|
Returns the current software volume percentage. A result of @racket[100.0]
|
|
means normal volume.
|
|
}
|
|
|
|
@defproc[(ao_real_output_bits_async [handle any/c]) exact-integer?]{
|
|
Returns the actual number of bits per sample used by the opened libao device.
|
|
This may be lower than the requested width if device opening fell back from
|
|
32-bit to 24-bit or 16-bit output.
|
|
}
|
|
|
|
@section{Implementation strategy}
|
|
|
|
The module keeps libao as the only native audio backend, but moves the async
|
|
queue and playback thread from C to Racket. It initializes libao lazily when
|
|
the first handle or temporary driver query is opened. A small reference count
|
|
is used so @racket[ao_shutdown] is called when the last opened handle is
|
|
closed. A custodian and exit finalizer call shutdown as a last resort.
|
|
|
|
The worker thread is created with its own thread pool. It waits for queue
|
|
elements, observes the pause lock, applies volume when necessary, copies the
|
|
chunk into foreign memory allocated as @racket['atomic-interior]@elem{,} and calls
|
|
libao. The foreign @racket[ao_play] binding is marked as blocking. The
|
|
combination matters: libao may retain the pointer for the duration of the
|
|
call, and a blocking foreign call should not receive movable Racket byte
|
|
storage directly.
|
|
|
|
Small decoder buffers are combined before reaching libao. The target chunk
|
|
size is controlled by the internal @racket[ao-buf-ms] value, currently 250
|
|
milliseconds. The buffer reuse pool keeps allocated byte strings around for
|
|
future chunks, reducing allocation churn in long playback sessions.
|
|
|
|
The conversion path is intentionally narrow. Planar input is converted to
|
|
interleaved PCM. Sample width conversion is done with arithmetic shifts, and
|
|
endianness conversion is handled through Racket byte operations. This backend
|
|
therefore expects integer PCM with byte-aligned sample widths.
|
|
|
|
@section{Compatibility notes}
|
|
|
|
The exported names are kept compatible with the old
|
|
@filepath{libao-async-ffi.rkt} layer. In particular,
|
|
@racket[make-BufferInfo_t] remains available even though the actual value is a
|
|
Racket struct, not a C struct. The higher layers can therefore select this
|
|
module as an implementation backend without changing the playback API.
|
|
|
|
The queue state functions report the backend's own administration. They do
|
|
not query libao for device latency, and they do not know how many samples are
|
|
already buffered by the operating system or the audio driver. |