Òpus toevoeging via xiph library
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
#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.
|
||||
Reference in New Issue
Block a user