audio-play! returns the file id.
The file id is now a randomized number
This commit is contained in:
@@ -7,19 +7,39 @@
|
||||
@title{Asynchronous libao playback in Racket}
|
||||
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||
|
||||
@defmodule[(file "../libao-async-ffi-racket.rkt")]
|
||||
@defmodule[racket-audio/libao-async-ffi-racket]
|
||||
|
||||
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
|
||||
This module implements the asynchronous libao playback backend used by
|
||||
@racketmodname[racket-audio]. It 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.
|
||||
The module is intended as a low-level backend below the higher-level sound
|
||||
player. Client code creates one asynchronous audio handle, queues decoded PCM
|
||||
buffers together with playback position information, and lets a Racket worker
|
||||
thread feed libao. Higher-level player code should normally use the public
|
||||
player interface instead of calling this module directly.
|
||||
|
||||
@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 foreign @racket[ao_play] call is declared
|
||||
with @racket[#:blocking?], so a blocking write to the audio device does not
|
||||
unnecessarily hold up other Racket threads.
|
||||
|
||||
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 which track the output
|
||||
thread has reached.
|
||||
|
||||
@section{Basic example}
|
||||
|
||||
@@ -36,11 +56,13 @@ device with the requested precision.
|
||||
|
||||
(when h
|
||||
(ao_play_async h 1 0.0 180.0 (bytes-length pcm) pcm info)
|
||||
(ao_set_volume_async h 80.0)
|
||||
(ao_pause_async h #t)
|
||||
(ao_pause_async h #f)
|
||||
(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{.}
|
||||
as the last argument to @racket[ao_create_async] instead of @racket[#f].
|
||||
|
||||
@racketblock[
|
||||
(define h
|
||||
@@ -50,30 +72,31 @@ as the last argument to @racket[ao_create_async] instead of @racket[#f]
|
||||
|
||||
@defproc[(ao_version_async) exact-integer?]{
|
||||
Returns the implementation version of this asynchronous backend. The current
|
||||
module returns @racket[3]@elem{.}
|
||||
module returns @racket[3]. The value is useful for diagnostics when multiple
|
||||
asynchronous backends exist.
|
||||
}
|
||||
|
||||
@defproc[(ao_create_async
|
||||
[bits exact-integer?]
|
||||
[rate exact-integer?]
|
||||
[channels exact-integer?]
|
||||
[bits exact-positive-integer?]
|
||||
[rate exact-positive-integer?]
|
||||
[channels exact-positive-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.
|
||||
@racket[#f], the default live libao driver is opened. Otherwise the libao
|
||||
@tt{wav} driver is opened and 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 requested format is described by @racket[bits], @racket[rate],
|
||||
@racket[channels] and @racket[byte-format]. 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].
|
||||
|
||||
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.
|
||||
The result is an asynchronous audio handle, or @racket[#f] when no suitable
|
||||
libao device or output file could be opened. Handles should be closed with
|
||||
@racket[ao_stop_async]. 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]{
|
||||
@@ -85,30 +108,36 @@ 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.
|
||||
assembled for the next queued play item is also discarded. Already playing
|
||||
audio may still finish at the device level, depending on what libao and the
|
||||
operating system have accepted. This operation is used when playback is
|
||||
stopped, when the higher layer seeks, or when the current stream is replaced.
|
||||
}
|
||||
|
||||
@defproc[(ao_pause_async [handle any/c]
|
||||
[paused any/c]) any/c]{
|
||||
[paused (or/c boolean? integer?)])
|
||||
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.
|
||||
|
||||
Pausing does not prevent producers from queueing additional buffers. It only
|
||||
prevents the worker thread from taking more data from the queue.
|
||||
}
|
||||
|
||||
@section{Buffer descriptions}
|
||||
|
||||
@defproc[(make-buffer-info
|
||||
[type symbol?]
|
||||
[sample-bits exact-integer?]
|
||||
[sample-rate exact-integer?]
|
||||
[channels exact-integer?]
|
||||
[sample-bits exact-positive-integer?]
|
||||
[sample-rate exact-positive-integer?]
|
||||
[channels exact-positive-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.
|
||||
Constructs the format description passed to @racket[ao_play_async]. 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
|
||||
@@ -116,20 +145,23 @@ 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.
|
||||
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. 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?]
|
||||
[sample-bits exact-positive-integer?]
|
||||
[sample-rate exact-positive-integer?]
|
||||
[channels exact-positive-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
|
||||
Compatibility alias for @racket[make-buffer-info]. The name is kept so code
|
||||
that used the older FFI module can keep constructing buffer descriptions
|
||||
without changing call sites.
|
||||
}
|
||||
|
||||
@@ -140,8 +172,8 @@ without changing call sites.
|
||||
[music-id any/c]
|
||||
[at-second real?]
|
||||
[music-duration real?]
|
||||
[buf-size exact-integer?]
|
||||
[audio-buffer any/c]
|
||||
[buf-size exact-nonnegative-integer?]
|
||||
[audio-buffer (or/c bytes? any/c)]
|
||||
[buffer-info any/c])
|
||||
any/c]{
|
||||
Queues a PCM buffer for asynchronous playback. @racket[audio-buffer] may be a
|
||||
@@ -149,66 +181,71 @@ 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{.}
|
||||
The position values @racket[music-id], @racket[at-second] and
|
||||
@racket[music-duration] are copied into the queue element. When the worker
|
||||
thread starts playing that element, these values become visible through
|
||||
@racket[ao_is_at_music_id_async], @racket[ao_is_at_second_async] and
|
||||
@racket[ao_music_duration_async]. They do not affect sample conversion.
|
||||
|
||||
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.
|
||||
sample data is converted before queueing. Small input chunks are collected into
|
||||
larger queue elements. The target chunk size is controlled by
|
||||
@racket[ao-playback-buf-ms] and defaults to 150 milliseconds. Buffers with
|
||||
different @racket[music-id] values are not merged into the same output chunk.
|
||||
}
|
||||
|
||||
@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.
|
||||
most recently taken by the worker thread. This value is not measured by libao;
|
||||
it is the @racket[at-second] value supplied 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{.}
|
||||
@racket[ao_play_async].
|
||||
}
|
||||
|
||||
@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{.}
|
||||
@racket[music-duration] argument passed to @racket[ao_play_async].
|
||||
}
|
||||
|
||||
@defproc[(ao_bufsize_async [handle any/c]) exact-integer?]{
|
||||
@defproc[(ao_bufsize_async [handle any/c]) exact-nonnegative-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.
|
||||
taken by the worker thread or cleared. It 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-integer?]{
|
||||
@defproc[(ao_sample_queue_len [handle any/c]) exact-nonnegative-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{.}
|
||||
@racket[ao_play_async].
|
||||
}
|
||||
|
||||
@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.
|
||||
@defproc[(ao_reuse_buf_len [handle any/c]) exact-nonnegative-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.
|
||||
[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 @racket[10000]. Values very close to
|
||||
@racket[100.0] are normalized to exactly @racket[10000] to avoid unnecessary
|
||||
sample processing.
|
||||
|
||||
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
|
||||
@@ -220,10 +257,29 @@ 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?]{
|
||||
@defproc[(ao_real_output_bits_async [handle any/c])
|
||||
exact-nonnegative-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.
|
||||
32-bit to 24-bit or 16-bit output. 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 strategy}
|
||||
@@ -231,21 +287,20 @@ This may be lower than the requested width if device opening fell back from
|
||||
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.
|
||||
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.
|
||||
The worker thread waits for queue elements, observes the pause lock, applies
|
||||
volume when necessary, copies the chunk into foreign memory allocated as
|
||||
@racket['atomic-interior], 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.
|
||||
size is controlled by @racket[ao-playback-buf-ms]. 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
|
||||
@@ -260,6 +315,6 @@ The exported names are kept compatible with the old
|
||||
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.
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user