audio-play! returns the file id.
The file id is now a randomized number
This commit is contained in:
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
(provide placed-player)
|
(provide placed-player)
|
||||||
|
|
||||||
|
(define get-current-seconds current-seconds)
|
||||||
|
|
||||||
(define (eq-seconds? s1 s2)
|
(define (eq-seconds? s1 s2)
|
||||||
(let ((s1* (inexact->exact (round s1)))
|
(let ((s1* (inexact->exact (round s1)))
|
||||||
(s2* (inexact->exact (round s2))))
|
(s2* (inexact->exact (round s2))))
|
||||||
@@ -317,7 +319,9 @@
|
|||||||
(dbg-sound "oke done")
|
(dbg-sound "oke done")
|
||||||
)
|
)
|
||||||
|
|
||||||
(set! current-file-id (+ current-file-id 1))
|
|
||||||
|
;(set! current-file-id (+ current-file-id 1))
|
||||||
|
(set! current-file-id (+ (* (get-current-seconds) 10000) (random 1000)))
|
||||||
(let ((f (build-path file)))
|
(let ((f (build-path file)))
|
||||||
(set! files-playing (cons
|
(set! files-playing (cons
|
||||||
(cons current-file-id f)
|
(cons current-file-id f)
|
||||||
@@ -328,7 +332,8 @@
|
|||||||
|
|
||||||
(when (eq? player-state 'stopped)
|
(when (eq? player-state 'stopped)
|
||||||
(set! player-state 'playing))
|
(set! player-state 'playing))
|
||||||
(audio-read-worker ao-dec current-file-id))
|
(audio-read-worker ao-dec current-file-id)
|
||||||
|
current-file-id)
|
||||||
|
|
||||||
(define (pause paused)
|
(define (pause paused)
|
||||||
(when (or (eq? player-state 'paused)
|
(when (or (eq? player-state 'paused)
|
||||||
@@ -435,8 +440,8 @@
|
|||||||
((eq? cmd 'open)
|
((eq? cmd 'open)
|
||||||
(do-rpc
|
(do-rpc
|
||||||
(let ((file (cadr data)))
|
(let ((file (cadr data)))
|
||||||
(start file)
|
(let ((id (start file)))
|
||||||
'(ok))))
|
(list (list 'ok id))))))
|
||||||
((eq? cmd 'seek)
|
((eq? cmd 'seek)
|
||||||
(do-rpc
|
(do-rpc
|
||||||
(let ((percentage (cadr data)))
|
(let ((percentage (cadr data)))
|
||||||
|
|||||||
+7
-2
@@ -143,6 +143,8 @@
|
|||||||
(set-audio-play-state! handle (evt-data e))
|
(set-audio-play-state! handle (evt-data e))
|
||||||
(cb-state* (evt-data e)))
|
(cb-state* (evt-data e)))
|
||||||
((is-event? e 'audio-done) (cb-eof*))
|
((is-event? e 'audio-done) (cb-eof*))
|
||||||
|
((is-event? e 'exception)
|
||||||
|
(err-sound "audio-player: exception event: ~a" e))
|
||||||
(else (warn-sound "audio-player: unknown event ~a" e))
|
(else (warn-sound "audio-player: unknown event ~a" e))
|
||||||
)
|
)
|
||||||
(loop))
|
(loop))
|
||||||
@@ -175,8 +177,11 @@
|
|||||||
)
|
)
|
||||||
|
|
||||||
(define/contract (audio-play! handle audio-file)
|
(define/contract (audio-play! handle audio-file)
|
||||||
(-> audio-play? path-string? symbol?)
|
(-> audio-play? path-string? number?)
|
||||||
((audio-play-rpc handle) 'open audio-file))
|
(let ((result ((audio-play-rpc handle) 'open audio-file)))
|
||||||
|
(when (eq? result 'error)
|
||||||
|
(error "Got an error from the placed audio player"))
|
||||||
|
(cadr result)))
|
||||||
|
|
||||||
(define/contract (audio-pause! handle paused)
|
(define/contract (audio-pause! handle paused)
|
||||||
(-> audio-play? boolean? symbol?)
|
(-> audio-play? boolean? symbol?)
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,7 @@
|
|||||||
"tests.rkt"
|
"tests.rkt"
|
||||||
)
|
)
|
||||||
|
|
||||||
(define place-mode #t)
|
(define place-mode #f)
|
||||||
|
|
||||||
(define run-queue #f)
|
(define run-queue #f)
|
||||||
(define (set-test a)
|
(define (set-test a)
|
||||||
@@ -60,13 +60,13 @@
|
|||||||
(if (null? play-queue)
|
(if (null? play-queue)
|
||||||
(audio-quit! h)
|
(audio-quit! h)
|
||||||
(begin
|
(begin
|
||||||
(audio-play! h (car play-queue))
|
(dbg-sound "audio-play! -> ~a" (audio-play! h (car play-queue)))
|
||||||
(set! play-queue (cdr play-queue))
|
(set! play-queue (cdr play-queue))
|
||||||
)
|
)
|
||||||
))
|
))
|
||||||
(when (eq? run-queue 'once)
|
(when (eq? run-queue 'once)
|
||||||
(set! run-queue #f)
|
(set! run-queue #f)
|
||||||
(audio-play! h test-file3))
|
(dbg-sound "audio-play! -> ~a" (audio-play! h (car play-queue))))
|
||||||
)
|
)
|
||||||
|
|
||||||
(define h (make-audio-player audio-player-state
|
(define h (make-audio-player audio-player-state
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ The important command-level behaviour is:
|
|||||||
@item{@racket['init] installs the reply and event channels and moves the
|
@item{@racket['init] installs the reply and event channels and moves the
|
||||||
player into the initialized command loop.}
|
player into the initialized command loop.}
|
||||||
@item{@racket['open] starts a decoder and a read worker. If another worker is
|
@item{@racket['open] starts a decoder and a read worker. If another worker is
|
||||||
still feeding audio, it is first interrupted and joined.}
|
still feeding audio, it is first interrupted and joined. Returns
|
||||||
|
a @tt{music id} for the given file.}
|
||||||
@item{@racket['pause] only changes @racket[player-state]. The worker observes
|
@item{@racket['pause] only changes @racket[player-state]. The worker observes
|
||||||
that state and applies @racket[ao-pause] to the output side.}
|
that state and applies @racket[ao-pause] to the output side.}
|
||||||
@item{@racket['seek] clears the async output queue and seeks the decoder, but
|
@item{@racket['seek] clears the async output queue and seeks the decoder, but
|
||||||
@@ -96,7 +97,8 @@ an RPC-style command is sent on the reply channel installed by
|
|||||||
@item{@racket[(list 'init ch-out ch-evt)] installs @racket[ch-out] and
|
@item{@racket[(list 'init ch-out ch-evt)] installs @racket[ch-out] and
|
||||||
@racket[ch-evt], and replies with @racket['(initialized)].}
|
@racket[ch-evt], and replies with @racket['(initialized)].}
|
||||||
@item{@racket[(list 'open file)] opens @racket[file], starts the decoder
|
@item{@racket[(list 'open file)] opens @racket[file], starts the decoder
|
||||||
worker, and replies with @racket['(ok)].}
|
worker, and replies with @racket[(list ok music-id)], where
|
||||||
|
@tt{music-id} is the given music id to the file to play.}
|
||||||
@item{@racket[(list 'pause paused?)] sets @racket[player-state] to
|
@item{@racket[(list 'pause paused?)] sets @racket[player-state] to
|
||||||
@racket['paused] or @racket['playing] when the player is already active,
|
@racket['paused] or @racket['playing] when the player is already active,
|
||||||
and replies with @racket['(ok)].}
|
and replies with @racket['(ok)].}
|
||||||
|
|||||||
@@ -76,10 +76,12 @@ is invalidated and @racket[audio-play?] returns @racket[#f].}
|
|||||||
|
|
||||||
@defproc[(audio-play! [player audio-play?]
|
@defproc[(audio-play! [player audio-play?]
|
||||||
[audio-file path-string?])
|
[audio-file path-string?])
|
||||||
symbol?]{
|
number?]{
|
||||||
Starts playback of @racket[audio-file]. The file is opened by the worker side,
|
Starts playback of @racket[audio-file]. The file is opened by the worker side,
|
||||||
a decoder is selected, and audio feeding begins asynchronously. In normal use
|
a decoder is selected, and audio feeding begins asynchronously. In normal use
|
||||||
the return value is @racket['ok].
|
the return value is @racket[music-id], where @tt{music-id} is the
|
||||||
|
given @tt{id}, a @tt{number? >= 1} to the file to play, which will be used when reporting in
|
||||||
|
callbacks about e.g. state.
|
||||||
|
|
||||||
Calling @racket[audio-play!] while another file is still active replaces the
|
Calling @racket[audio-play!] while another file is still active replaces the
|
||||||
current stream. The worker side interrupts the old decoder, clears the output
|
current stream. The worker side interrupts the old decoder, clears the output
|
||||||
|
|||||||
@@ -1,398 +0,0 @@
|
|||||||
#lang scribble/manual
|
|
||||||
|
|
||||||
@(require (for-label racket/base
|
|
||||||
;racket/contract
|
|
||||||
racket/path
|
|
||||||
ffi/unsafe
|
|
||||||
let-assert
|
|
||||||
early-return
|
|
||||||
"../ffmpeg-definitions.rkt"
|
|
||||||
"../private/cstruct-helper.rkt"))
|
|
||||||
|
|
||||||
@title[#:tag "ffmpeg-definitions"]{FFmpeg Decoder Definitions}
|
|
||||||
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
|
||||||
|
|
||||||
@defmodule[ffmpeg-definitions]
|
|
||||||
|
|
||||||
This module provides the direct FFmpeg-backed decoder layer used by the audio
|
|
||||||
pipeline. It is deliberately small and stateful. A caller creates one decoder
|
|
||||||
instance, opens one file on it, queries the selected audio stream, repeatedly
|
|
||||||
asks for the next PCM block, and closes the instance again.
|
|
||||||
|
|
||||||
The module does not expose FFmpeg metadata. It only exposes the information
|
|
||||||
needed for playback: stream count, sample rate, channel count, duration,
|
|
||||||
bitrate, decoded PCM data, and sample positions. The output format is fixed:
|
|
||||||
interleaved signed 32-bit PCM, four bytes per sample, using FFmpeg's
|
|
||||||
@tt{AV_SAMPLE_FMT_S32} sample format.
|
|
||||||
|
|
||||||
The FFmpeg libraries are loaded when the module is required. The module checks
|
|
||||||
that the runtime FFmpeg major versions are in the supported range configured by
|
|
||||||
the implementation. This binding targets the FFmpeg library major versions
|
|
||||||
used by FFmpeg 6, 7, and 8: @tt{libavutil} 58 to 60, @tt{libavcodec} 60 to 62,
|
|
||||||
@tt{libavformat} 60 to 62, and @tt{libswresample} 4 to 6. Unsupported runtime
|
|
||||||
versions fail early, before a decoder instance is used.
|
|
||||||
|
|
||||||
On Windows, the private library loader may download the bundled sound-library
|
|
||||||
set into Racket's add-on directory before the FFI libraries are opened. On
|
|
||||||
Unix-like systems, the FFmpeg libraries are expected to be installed by the
|
|
||||||
operating system or platform package manager and to be reachable by Racket's
|
|
||||||
FFI library search path.
|
|
||||||
|
|
||||||
@section{Layering}
|
|
||||||
|
|
||||||
This module is the low-level Racket FFI layer. It is normally wrapped by
|
|
||||||
@filepath{ffmpeg-ffi.rkt} and then by @filepath{ffmpeg-decoder.rkt}@elem{.}
|
|
||||||
The first wrapper adapts this module to the command protocol used by the audio
|
|
||||||
decoder frontend. The second wrapper exposes the callback-oriented decoder
|
|
||||||
interface used by the rest of the playback pipeline.
|
|
||||||
|
|
||||||
The distinction matters for buffer lifetime. At this level,
|
|
||||||
@racket[fmpg-buffer] returns the current buffer owned by the decoder instance.
|
|
||||||
The adapter in @filepath{ffmpeg-ffi.rkt} copies that buffer before passing it to
|
|
||||||
@filepath{ffmpeg-decoder.rkt}@elem{.} Code that uses this module directly must
|
|
||||||
copy the buffer itself when the bytes must survive the next decoder operation.
|
|
||||||
|
|
||||||
@section{Implementation strategy}
|
|
||||||
|
|
||||||
This module talks directly to the FFmpeg shared libraries through Racket's FFI.
|
|
||||||
There is no C shim that hides FFmpeg's structs or normalizes their layout. The
|
|
||||||
price of that choice is that the Racket side must know enough of the relevant C
|
|
||||||
struct layouts to read the fields used by the decoder. The benefit is that the
|
|
||||||
binding remains a Racket module with direct access to the platform FFmpeg
|
|
||||||
libraries.
|
|
||||||
|
|
||||||
@subsection{Versioned C struct layouts}
|
|
||||||
|
|
||||||
The module defines only partial FFmpeg structs. A partial definition includes
|
|
||||||
the fields that are actually read by this decoder and enough preceding fields to
|
|
||||||
compute their offsets. Fields that are not needed are represented only by their
|
|
||||||
C type, or by a repetition count such as @racket[(6 _int)]@elem{.} Tail fields
|
|
||||||
after the last required member are not described.
|
|
||||||
|
|
||||||
The helper module @filepath{private/cstruct-helper.rkt} provides
|
|
||||||
@racket[make-offsets] and @racket[def-cstruct]@elem{.} The
|
|
||||||
@racket[make-offsets] form computes offsets for a sequence of C field types,
|
|
||||||
while @racket[def-cstruct] expands to a @racket[define-cstruct] form whose
|
|
||||||
public fields are placed at those explicit offsets. This keeps the actual
|
|
||||||
accessors small while still accounting for skipped fields in the C layout.
|
|
||||||
|
|
||||||
The right layout is selected when the module is required, after the runtime
|
|
||||||
FFmpeg major versions have been read from the libraries. For the supported
|
|
||||||
range, @racket[_AVCodecParameters] uses one layout for
|
|
||||||
@tt{libavcodec} major version 60 and another for major versions 61 and 62.
|
|
||||||
Likewise, @racket[_AVFrame] uses one layout for @tt{libavutil} major version
|
|
||||||
58 and another for major versions 59 and 60. The other partial structs used by
|
|
||||||
this module are defined with a single layout across the supported versions.
|
|
||||||
|
|
||||||
This is why the version check is performed before normal decoder use. The
|
|
||||||
accessors are correct only for the FFmpeg major-version ranges for which the
|
|
||||||
partial layouts were written. If a future FFmpeg major release changes a
|
|
||||||
layout before one of the fields used here, the version range should be extended
|
|
||||||
only after the affected partial definitions have been checked.
|
|
||||||
|
|
||||||
@subsection{Sequential failure handling}
|
|
||||||
|
|
||||||
Most FFmpeg calls report ordinary failure through C-style return values or null
|
|
||||||
pointers. The implementation treats those results as normal control flow, not
|
|
||||||
as exceptional Racket failures. The @racket[let/assert] form is used for this
|
|
||||||
pattern. It behaves like a sequential binding form: each binding can be checked
|
|
||||||
immediately, and a failed check returns the specified failure value for the
|
|
||||||
whole form.
|
|
||||||
|
|
||||||
That style is used for setup paths such as opening a file, selecting stream
|
|
||||||
information, allocating the codec context, and initializing the resampler. It
|
|
||||||
keeps the success path linear while still giving each FFmpeg return value or
|
|
||||||
pointer a local check. Predicates such as @tt{a-!nullptr?}@elem{,}
|
|
||||||
@tt{a-nullptr?}@elem{,} @tt{a-true?}@elem{,} and @tt{a->=?} express the usual
|
|
||||||
FFmpeg checks directly next to the binding that produced the value.
|
|
||||||
|
|
||||||
For loops where decoding must stop immediately from a nested position, the
|
|
||||||
module uses @racket[define/return] from @racketmodname[define-return]@elem{.}
|
|
||||||
This gives functions such as @racket[fmpg-decode-next!] and the internal
|
|
||||||
resampler drain routine an explicit early-return continuation without using
|
|
||||||
exceptions for normal FFmpeg outcomes. The two helpers are implementation
|
|
||||||
dependencies; they are not re-exported by this module.
|
|
||||||
|
|
||||||
@section{Decoder instances}
|
|
||||||
|
|
||||||
A decoder instance is an opaque value returned by @racket[fmpg-init]@elem{.}
|
|
||||||
Its structure type and predicate are not exported. Pass the value back to the
|
|
||||||
functions in this module and do not inspect it directly. The contracts below
|
|
||||||
therefore use @racket[any/c] for the instance argument. Operationally, that
|
|
||||||
argument must be a value returned by @racket[fmpg-init]@elem{.}
|
|
||||||
|
|
||||||
The instance owns native FFmpeg resources: a format context, a codec context,
|
|
||||||
an audio frame, a resampler, and the Racket byte string used for the current
|
|
||||||
PCM block. Finalizers are installed as a last line of defence, but callers
|
|
||||||
should still call @racket[fmpg-close!] explicitly when playback stops or when
|
|
||||||
the file is no longer needed. Explicit close keeps the lifetime of native
|
|
||||||
resources predictable.
|
|
||||||
|
|
||||||
@defproc[(fmpg-init) any/c]{
|
|
||||||
Creates a new decoder instance. The result is an opaque instance value, or
|
|
||||||
@racket[#f] if the instance could not be created.
|
|
||||||
|
|
||||||
Creating the instance does not open a file. Use @racket[fmpg-open-file!]
|
|
||||||
before querying stream information or decoding audio.
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-open-file! [instance any/c]
|
|
||||||
[filename (or/c path? string?)])
|
|
||||||
(integer-in 0 1)]{
|
|
||||||
Opens @racket[filename] on @racket[instance]@elem{,} reads the stream
|
|
||||||
information, selects the best audio stream, initializes the codec context, and
|
|
||||||
initializes the resampler.
|
|
||||||
|
|
||||||
The function returns @racket[1] on success and @racket[0] on failure. On
|
|
||||||
failure, partially initialized native state is closed again.
|
|
||||||
|
|
||||||
An instance can only have one file open. Close it with @racket[fmpg-close!]
|
|
||||||
before opening another file on the same instance. A non-string, non-path
|
|
||||||
filename is treated as an open failure and returns @racket[0]@elem{.}
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-close! [instance any/c]) void?]{
|
|
||||||
Closes @racket[instance] if it is open and releases the native FFmpeg resources
|
|
||||||
owned by the instance. The stored audio information is reset. Calling this
|
|
||||||
function with @racket[#f] or with an already closed instance is harmless.
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-is-open [instance any/c]) (integer-in 0 1)]{
|
|
||||||
Returns @racket[1] when @racket[instance] is ready for decoding and
|
|
||||||
@racket[0] otherwise. An instance is ready only after a file has been opened,
|
|
||||||
a usable audio stream has been selected, and the decoder and resampler have
|
|
||||||
been initialized.
|
|
||||||
}
|
|
||||||
|
|
||||||
@section{Audio stream information}
|
|
||||||
|
|
||||||
The decoder selects one audio stream for playback using FFmpeg's best-stream
|
|
||||||
selection. The stream count reports how many audio streams were found in the
|
|
||||||
container, but decoding is performed only for the selected stream.
|
|
||||||
|
|
||||||
The term @italic{sample} in this module means a sample frame: one time step in
|
|
||||||
the audio stream, across all channels. For stereo 32-bit output, one sample
|
|
||||||
frame therefore occupies @racket[(* 2 4)] bytes in the returned PCM buffer.
|
|
||||||
|
|
||||||
@defproc[(fmpg-audio-stream-count [instance any/c])
|
|
||||||
exact-nonnegative-integer?]{
|
|
||||||
Returns the number of audio streams in the open container. If the instance is
|
|
||||||
not open, the result is @racket[0]@elem{.}
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-audio-sample-rate [instance any/c])
|
|
||||||
exact-nonnegative-integer?]{
|
|
||||||
Returns the selected audio stream's sample rate. If the instance is not ready,
|
|
||||||
the result is @racket[0]@elem{.}
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-audio-channels [instance any/c])
|
|
||||||
exact-nonnegative-integer?]{
|
|
||||||
Returns the selected audio stream's channel count. If the instance is not
|
|
||||||
ready, the result is @racket[0]@elem{.}
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-audio-bits-per-sample [instance any/c])
|
|
||||||
exact-positive-integer?]{
|
|
||||||
Returns the fixed output sample width in bits. The current output format is
|
|
||||||
32-bit signed PCM, so this function returns @racket[32]@elem{.} The value is
|
|
||||||
independent of the input file's original sample format and does not depend on
|
|
||||||
the instance state.
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-audio-bytes-per-sample [instance any/c])
|
|
||||||
exact-positive-integer?]{
|
|
||||||
Returns the fixed output sample width in bytes. The current output format is
|
|
||||||
32-bit signed PCM, so this function returns @racket[4]@elem{.} The value is
|
|
||||||
independent of the input file's original sample format and does not depend on
|
|
||||||
the instance state.
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-duration-ms [instance any/c]) exact-integer?]{
|
|
||||||
Returns the duration of the selected audio stream in milliseconds. If the
|
|
||||||
stream duration is not available, the container duration is used as a fallback.
|
|
||||||
If no duration can be determined, or when the instance is not ready, the result
|
|
||||||
is @racket[-1]@elem{.}
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-duration-samples [instance any/c]) exact-integer?]{
|
|
||||||
Returns the duration of the selected audio stream in sample frames. If the
|
|
||||||
stream duration is not available, the container duration is used as a fallback.
|
|
||||||
If no duration can be determined, or when the instance is not ready, the result
|
|
||||||
is @racket[-1]@elem{.}
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-file-bitrate [instance any/c]) exact-integer?]{
|
|
||||||
Returns the container bitrate in bits per second. If the bitrate is
|
|
||||||
unavailable or if the instance is not open, the result is @racket[-1]@elem{.}
|
|
||||||
}
|
|
||||||
|
|
||||||
@section{Decoding}
|
|
||||||
|
|
||||||
Decoding is block oriented. Each call to @racket[fmpg-decode-next!] clears the
|
|
||||||
previous PCM block and attempts to produce the next decoded block for the
|
|
||||||
selected audio stream. When the call returns @racket[1]@elem{,} the block can
|
|
||||||
be read with @racket[fmpg-buffer] and described with the buffer query
|
|
||||||
functions.
|
|
||||||
|
|
||||||
@defproc[(fmpg-decode-next! [instance any/c]) (integer-in 0 1)]{
|
|
||||||
Decodes until a block of PCM output is available or no more output can be
|
|
||||||
produced. The function returns @racket[1] when @racket[fmpg-buffer] contains a
|
|
||||||
non-empty PCM block. It returns @racket[0] when the instance is not ready, when
|
|
||||||
end of stream has been reached, or when FFmpeg reports an unrecoverable decode
|
|
||||||
error.
|
|
||||||
|
|
||||||
The function does not distinguish end of stream from a decode failure. The
|
|
||||||
intended playback loop treats @racket[0] as no further PCM block available for
|
|
||||||
this decoder instance.
|
|
||||||
|
|
||||||
Internally, decoding receives all currently available frames, reads packets for
|
|
||||||
the selected audio stream, sends those packets to the codec, converts decoded
|
|
||||||
frames through @tt{libswresample}@elem{,} and drains the resampler at end of
|
|
||||||
stream. Non-selected packets are skipped.
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-seek-ms! [instance any/c]
|
|
||||||
[target-pos-ms exact-nonnegative-integer?])
|
|
||||||
(integer-in 0 1)]{
|
|
||||||
Seeks the selected audio stream to @racket[target-pos-ms] milliseconds and
|
|
||||||
resets the decoder and resampler state. The function returns @racket[1] on
|
|
||||||
success and @racket[0] on failure.
|
|
||||||
|
|
||||||
Seeking uses FFmpeg's backward seek flag. After the seek, decoded audio before
|
|
||||||
the requested target sample is discarded so the next buffer starts at, or as
|
|
||||||
close as FFmpeg can provide to, the requested position.
|
|
||||||
}
|
|
||||||
|
|
||||||
@section{Decoded buffers}
|
|
||||||
|
|
||||||
The PCM buffer belongs to the decoder instance. It is replaced by the next
|
|
||||||
call to @racket[fmpg-decode-next!]@elem{,} @racket[fmpg-seek-ms!]@elem{,} or
|
|
||||||
@racket[fmpg-close!]@elem{.} Treat the returned byte string as read-only.
|
|
||||||
Copy it if it must outlive the next decoder operation or if another component
|
|
||||||
may mutate it.
|
|
||||||
|
|
||||||
@defproc[(fmpg-buffer [instance any/c]) (or/c bytes? #f)]{
|
|
||||||
Returns the current decoded PCM block as a byte string, or @racket[#f] when no
|
|
||||||
PCM block is available.
|
|
||||||
|
|
||||||
The byte string contains interleaved signed 32-bit samples. Its logical frame
|
|
||||||
count is available as the difference between @racket[fmpg-buffer-end-sample]
|
|
||||||
and @racket[fmpg-buffer-start-sample]@elem{.} Its byte size is also available
|
|
||||||
through @racket[fmpg-buffer-size]@elem{.}
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-buffer-size [instance any/c]) exact-nonnegative-integer?]{
|
|
||||||
Returns the number of valid bytes in the current PCM buffer. If no decoder
|
|
||||||
state is available, or if the size would not fit in the internal integer range,
|
|
||||||
the function returns @racket[0]@elem{.}
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-buffer-start-sample [instance any/c])
|
|
||||||
exact-nonnegative-integer?]{
|
|
||||||
Returns the first sample frame represented by the current PCM buffer. If no
|
|
||||||
decoder state is available, the result is @racket[0]@elem{.}
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-buffer-end-sample [instance any/c])
|
|
||||||
exact-nonnegative-integer?]{
|
|
||||||
Returns the half-open end position of the current PCM buffer: the first sample
|
|
||||||
frame after the current buffer. The number of sample frames in the buffer is
|
|
||||||
the end position minus @racket[fmpg-buffer-start-sample]@elem{.} If no decoder
|
|
||||||
state is available, the result is @racket[0]@elem{.}
|
|
||||||
}
|
|
||||||
|
|
||||||
@defproc[(fmpg-sample-position [instance any/c])
|
|
||||||
exact-nonnegative-integer?]{
|
|
||||||
Returns the decoder's next sample-frame position after the current output.
|
|
||||||
During normal decoding it is the same as @racket[fmpg-buffer-end-sample] for
|
|
||||||
the current buffer. After a seek, it is reset to the target position before
|
|
||||||
new audio is decoded.
|
|
||||||
}
|
|
||||||
|
|
||||||
@section{FFmpeg version information}
|
|
||||||
|
|
||||||
@defproc[(ffmpeg-version [lib (or/c 'avutil 'avcodec 'avformat
|
|
||||||
'swr 'swresample)])
|
|
||||||
(list/c exact-nonnegative-integer?
|
|
||||||
exact-nonnegative-integer?
|
|
||||||
exact-nonnegative-integer?)]{
|
|
||||||
Returns the runtime version of one FFmpeg library as a three-element list
|
|
||||||
containing the major, minor, and micro version numbers. The symbols
|
|
||||||
@racket['swr] and @racket['swresample] both refer to @tt{libswresample}@elem{.}
|
|
||||||
|
|
||||||
The function raises an exception for an unknown library symbol.
|
|
||||||
}
|
|
||||||
|
|
||||||
@section{Use through the decoder frontend}
|
|
||||||
|
|
||||||
The direct API above is normally wrapped by @filepath{ffmpeg-ffi.rkt} and by
|
|
||||||
@filepath{ffmpeg-decoder.rkt}@elem{.} The frontend function
|
|
||||||
@tt{ffmpeg-open} returns a handle or @racket[#f] when the file does not exist.
|
|
||||||
Its stream-info callback receives a mutable hash with at least these playback
|
|
||||||
keys:
|
|
||||||
|
|
||||||
@racketblock[
|
|
||||||
(list 'sample-rate
|
|
||||||
'channels
|
|
||||||
'bits-per-sample
|
|
||||||
'bytes-per-sample
|
|
||||||
'total-samples
|
|
||||||
'duration)]
|
|
||||||
|
|
||||||
The audio callback receives the same hash extended for the current buffer with
|
|
||||||
these keys:
|
|
||||||
|
|
||||||
@racketblock[
|
|
||||||
(list 'sample
|
|
||||||
'current-time)]
|
|
||||||
|
|
||||||
The hash is followed by a copied byte string and its valid byte count. The
|
|
||||||
copy is made by @filepath{ffmpeg-ffi.rkt}@elem{,} not by the low-level buffer
|
|
||||||
function itself.
|
|
||||||
|
|
||||||
The frontend's seek function accepts a percentage of the stream and translates
|
|
||||||
that percentage to a sample position. The adapter then translates the sample
|
|
||||||
position to milliseconds and calls @racket[fmpg-seek-ms!]@elem{.} This is why
|
|
||||||
the low-level module exposes millisecond seeking while the frontend exposes
|
|
||||||
percentage seeking.
|
|
||||||
|
|
||||||
@section{Example}
|
|
||||||
|
|
||||||
The following example opens a file, decodes all PCM blocks, and reports their
|
|
||||||
byte ranges and sample ranges. A real playback loop would pass each buffer to
|
|
||||||
the audio output layer before requesting the next block.
|
|
||||||
|
|
||||||
@racketblock[
|
|
||||||
(define dec (fmpg-init))
|
|
||||||
|
|
||||||
(when (and dec (= (fmpg-open-file! dec "track.ogg") 1))
|
|
||||||
(printf "~a Hz, ~a channels, ~a ms\n"
|
|
||||||
(fmpg-audio-sample-rate dec)
|
|
||||||
(fmpg-audio-channels dec)
|
|
||||||
(fmpg-duration-ms dec))
|
|
||||||
|
|
||||||
(let loop ()
|
|
||||||
(when (= (fmpg-decode-next! dec) 1)
|
|
||||||
(define pcm (fmpg-buffer dec))
|
|
||||||
(define size (fmpg-buffer-size dec))
|
|
||||||
(define start (fmpg-buffer-start-sample dec))
|
|
||||||
(define end (fmpg-buffer-end-sample dec))
|
|
||||||
(printf "decoded ~a bytes, samples [~a, ~a)\n"
|
|
||||||
size start end)
|
|
||||||
;; Pass pcm to the audio output layer here, or copy it if needed.
|
|
||||||
(loop)))
|
|
||||||
|
|
||||||
(fmpg-close! dec))
|
|
||||||
]
|
|
||||||
|
|
||||||
A simple seek flow looks the same after the seek succeeds. The following code
|
|
||||||
moves to 30 seconds and then requests the next decoded buffer.
|
|
||||||
|
|
||||||
@racketblock[
|
|
||||||
(when (= (fmpg-seek-ms! dec 30000) 1)
|
|
||||||
(when (= (fmpg-decode-next! dec) 1)
|
|
||||||
(define pcm (fmpg-buffer dec))
|
|
||||||
(define start (fmpg-buffer-start-sample dec))
|
|
||||||
(printf "first buffer after seek starts at sample ~a\n" start)))
|
|
||||||
]
|
|
||||||
@@ -7,19 +7,39 @@
|
|||||||
@title{Asynchronous libao playback in Racket}
|
@title{Asynchronous libao playback in Racket}
|
||||||
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
@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
|
This module implements the asynchronous libao playback backend used by
|
||||||
@filepath{ao_playasync.c} backend. It exports the same Racket level API as
|
@racketmodname[racket-audio]. It is a pure Racket replacement for the older C
|
||||||
@filepath{libao-async-ffi.rkt} and still sends PCM to Xiph libao, but the
|
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
|
queue, worker thread, buffering, format conversion and volume scaling are all
|
||||||
implemented in Racket.
|
implemented in Racket.
|
||||||
|
|
||||||
The module is meant to sit below the higher level sound player. Client code
|
The module is intended as a low-level backend below the higher-level sound
|
||||||
creates one asynchronous audio handle, queues PCM buffers with playback
|
player. Client code creates one asynchronous audio handle, queues decoded PCM
|
||||||
position information, and lets a Racket worker thread feed libao. The foreign
|
buffers together with playback position information, and lets a Racket worker
|
||||||
@racket[ao_play] call is declared with @racket[#:blocking?] so that a blocking
|
thread feed libao. Higher-level player code should normally use the public
|
||||||
write to the audio device does not unnecessarily hold up other Racket threads.
|
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}
|
@section{Basic example}
|
||||||
|
|
||||||
@@ -36,11 +56,13 @@ device with the requested precision.
|
|||||||
|
|
||||||
(when h
|
(when h
|
||||||
(ao_play_async h 1 0.0 180.0 (bytes-length pcm) pcm info)
|
(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))]
|
(ao_stop_async h))]
|
||||||
|
|
||||||
To write to a WAV file instead of the default live device, pass a path string
|
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]
|
as the last argument to @racket[ao_create_async] instead of @racket[#f].
|
||||||
@elem{.}
|
|
||||||
|
|
||||||
@racketblock[
|
@racketblock[
|
||||||
(define h
|
(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?]{
|
@defproc[(ao_version_async) exact-integer?]{
|
||||||
Returns the implementation version of this asynchronous backend. The current
|
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
|
@defproc[(ao_create_async
|
||||||
[bits exact-integer?]
|
[bits exact-positive-integer?]
|
||||||
[rate exact-integer?]
|
[rate exact-positive-integer?]
|
||||||
[channels exact-integer?]
|
[channels exact-positive-integer?]
|
||||||
[byte-format (or/c 'little-endian 'big-endian 'native-endian)]
|
[byte-format (or/c 'little-endian 'big-endian 'native-endian)]
|
||||||
[wav-output-file (or/c #f path-string?)])
|
[wav-output-file (or/c #f path-string?)])
|
||||||
any/c]{
|
any/c]{
|
||||||
Creates an asynchronous audio handle. When @racket[wav-output-file] is
|
Creates an asynchronous audio handle. When @racket[wav-output-file] is
|
||||||
@racket[#f] the default live libao driver is opened. Otherwise the libao
|
@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.
|
@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{,}
|
The requested format is described by @racket[bits], @racket[rate],
|
||||||
@racket[channels] and @racket[byte-format]@elem{.} If the requested number of
|
@racket[channels] and @racket[byte-format]. If the requested number of bits
|
||||||
bits cannot be opened and the request is wider than 24 or 16 bits, the module
|
cannot be opened and the request is wider than 24 or 16 bits, the module tries
|
||||||
tries 24-bit and then 16-bit output. The resulting device precision can be
|
24-bit and then 16-bit output. The resulting device precision can be queried
|
||||||
queried with @racket[ao_real_output_bits_async]@elem{.}
|
with @racket[ao_real_output_bits_async].
|
||||||
|
|
||||||
The result is an asynchronous audio handle, or @racket[#f] when no device or
|
The result is an asynchronous audio handle, or @racket[#f] when no suitable
|
||||||
output file could be opened. Handles should be closed with
|
libao 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,
|
@racket[ao_stop_async]. A finalizer is also registered as a safety net, but
|
||||||
but explicit stopping is the intended lifecycle.
|
explicit stopping is the intended lifecycle.
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(ao_stop_async [handle any/c]) any/c]{
|
@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]{
|
@defproc[(ao_clear_async [handle any/c]) any/c]{
|
||||||
Clears queued audio data without closing the device. The buffer that is being
|
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
|
assembled for the next queued play item is also discarded. Already playing
|
||||||
operation used when playback is stopped or when the higher layer seeks.
|
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]
|
@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
|
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
|
integer value is accepted for compatibility with the old FFI layer, where
|
||||||
@racket[0] means resume and any other integer means pause.
|
@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}
|
@section{Buffer descriptions}
|
||||||
|
|
||||||
@defproc[(make-buffer-info
|
@defproc[(make-buffer-info
|
||||||
[type symbol?]
|
[type symbol?]
|
||||||
[sample-bits exact-integer?]
|
[sample-bits exact-positive-integer?]
|
||||||
[sample-rate exact-integer?]
|
[sample-rate exact-positive-integer?]
|
||||||
[channels exact-integer?]
|
[channels exact-positive-integer?]
|
||||||
[endianness (or/c 'little-endian 'big-endian 'native-endian)])
|
[endianness (or/c 'little-endian 'big-endian 'native-endian)])
|
||||||
any/c]{
|
any/c]{
|
||||||
Constructs the format description passed to @racket[ao_play_async]@elem{.}
|
Constructs the format description passed to @racket[ao_play_async]. Only the
|
||||||
Only the constructor is exported. The struct predicate and accessors remain
|
constructor is exported. The struct predicate and accessors remain private to
|
||||||
private to this module, because the value is primarily a compatibility object
|
this module, because the value is primarily a compatibility object for the
|
||||||
for the audio backend.
|
audio backend.
|
||||||
|
|
||||||
The @racket[type] field controls whether the incoming buffer is already
|
The @racket[type] field controls whether the incoming buffer is already
|
||||||
interleaved. Use @racket['interleaved] or the older name @racket['ao] for
|
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
|
@racket['flac] when each channel is stored as a separate plane. Planar input
|
||||||
is copied to an interleaved buffer before it is queued.
|
is copied to an interleaved buffer before it is queued.
|
||||||
|
|
||||||
Samples are treated as signed integer PCM. @racket[sample-bits] must describe
|
The @racket[sample-bits], @racket[sample-rate] and @racket[channels] fields
|
||||||
whole bytes, such as 16, 24 or 32 bits. The conversion code uses the supplied
|
describe the format of the supplied buffer, not necessarily the format that will
|
||||||
endianness when reading input samples and when writing converted output samples.
|
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
|
@defproc[(make-BufferInfo_t
|
||||||
[type symbol?]
|
[type symbol?]
|
||||||
[sample-bits exact-integer?]
|
[sample-bits exact-positive-integer?]
|
||||||
[sample-rate exact-integer?]
|
[sample-rate exact-positive-integer?]
|
||||||
[channels exact-integer?]
|
[channels exact-positive-integer?]
|
||||||
[endianness (or/c 'little-endian 'big-endian 'native-endian)])
|
[endianness (or/c 'little-endian 'big-endian 'native-endian)])
|
||||||
any/c]{
|
any/c]{
|
||||||
Compatibility alias for @racket[make-buffer-info]@elem{.} The name is kept so
|
Compatibility alias for @racket[make-buffer-info]. The name is kept so code
|
||||||
code that used the older FFI module can keep constructing buffer descriptions
|
that used the older FFI module can keep constructing buffer descriptions
|
||||||
without changing call sites.
|
without changing call sites.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,8 +172,8 @@ without changing call sites.
|
|||||||
[music-id any/c]
|
[music-id any/c]
|
||||||
[at-second real?]
|
[at-second real?]
|
||||||
[music-duration real?]
|
[music-duration real?]
|
||||||
[buf-size exact-integer?]
|
[buf-size exact-nonnegative-integer?]
|
||||||
[audio-buffer any/c]
|
[audio-buffer (or/c bytes? any/c)]
|
||||||
[buffer-info any/c])
|
[buffer-info any/c])
|
||||||
any/c]{
|
any/c]{
|
||||||
Queues a PCM buffer for asynchronous playback. @racket[audio-buffer] may be a
|
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
|
External callers normally pass a byte string. @racket[buf-size] is the number
|
||||||
of valid bytes in the buffer.
|
of valid bytes in the buffer.
|
||||||
|
|
||||||
The position values @racket[at-second]@elem{,} @racket[music-duration] and
|
The position values @racket[music-id], @racket[at-second] and
|
||||||
@racket[music-id] are copied into the queue element. When the worker thread
|
@racket[music-duration] are copied into the queue element. When the worker
|
||||||
starts playing that element, these values become visible through
|
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], @racket[ao_is_at_second_async] and
|
||||||
@racket[ao_is_at_music_id_async]@elem{.}
|
@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
|
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
|
input sample width or endianness differs from the opened output device, the
|
||||||
sample data is converted before queueing. Small input chunks are collected
|
sample data is converted before queueing. Small input chunks are collected into
|
||||||
into larger queue elements of roughly 250 milliseconds, unless the music id
|
larger queue elements. The target chunk size is controlled by
|
||||||
changes or the current assembled buffer is full.
|
@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}
|
@section{Playback state}
|
||||||
|
|
||||||
@defproc[(ao_is_at_second_async [handle any/c]) real?]{
|
@defproc[(ao_is_at_second_async [handle any/c]) real?]{
|
||||||
Returns the playback position, in seconds, associated with the queue element
|
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
|
most recently taken by the worker thread. This value is not measured by libao;
|
||||||
the position reported by the producer of the PCM buffer.
|
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]{
|
@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
|
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
|
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?]{
|
@defproc[(ao_music_duration_async [handle any/c]) real?]{
|
||||||
Returns the duration value associated with the queue element most recently
|
Returns the duration value associated with the queue element most recently
|
||||||
taken by the worker thread. The value is copied from the
|
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
|
Returns the number of audio bytes currently counted as buffered by the async
|
||||||
queue administration. The value is updated when buffers are queued, combined,
|
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
|
Returns the number of queue elements currently counted by the async queue
|
||||||
administration. Since the module combines small input buffers into larger
|
administration. Since the module combines small input buffers into larger
|
||||||
playback chunks, this is not the same as the number of calls made to
|
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?]{
|
@defproc[(ao_reuse_buf_len [handle any/c]) exact-nonnegative-integer?]{
|
||||||
Returns the number of reusable byte buffers currently kept by the backend.
|
Returns the number of reusable byte buffers currently kept by the backend. This
|
||||||
This is an implementation diagnostic. It is useful for checking whether the
|
is an implementation diagnostic. It is useful for checking whether the reuse
|
||||||
reuse pool is being exercised, but it is not an audio latency measurement.
|
pool is being exercised, but it is not an audio latency measurement.
|
||||||
}
|
}
|
||||||
|
|
||||||
@section{Volume and output format}
|
@section{Volume and output format}
|
||||||
|
|
||||||
@defproc[(ao_set_volume_async [handle any/c]
|
@defproc[(ao_set_volume_async [handle any/c]
|
||||||
[percentage real?]) any/c]{
|
[percentage real?])
|
||||||
Sets the software volume. @racket[100.0] is normal volume,
|
any/c]{
|
||||||
@racket[50.0] is half volume and values above @racket[100.0] amplify the
|
Sets the software volume. @racket[100.0] is normal volume, @racket[50.0] is
|
||||||
samples. The implementation stores the setting as an integer scaled by 100,
|
half volume and values above @racket[100.0] amplify the samples. The
|
||||||
so normal volume is represented internally as 10000.
|
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
|
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
|
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.
|
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.
|
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
|
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}
|
@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
|
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
|
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
|
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
|
is used so @racket[ao_shutdown] is called when the last opened handle is closed.
|
||||||
closed. A custodian and exit finalizer call shutdown as a last resort.
|
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
|
The worker thread waits for queue elements, observes the pause lock, applies
|
||||||
elements, observes the pause lock, applies volume when necessary, copies the
|
volume when necessary, copies the chunk into foreign memory allocated as
|
||||||
chunk into foreign memory allocated as @racket['atomic-interior]@elem{,} and calls
|
@racket['atomic-interior], and calls libao. The foreign @racket[ao_play]
|
||||||
libao. The foreign @racket[ao_play] binding is marked as blocking. The
|
binding is marked as blocking. The combination matters: libao may retain the
|
||||||
combination matters: libao may retain the pointer for the duration of the
|
pointer for the duration of the call, and a blocking foreign call should not
|
||||||
call, and a blocking foreign call should not receive movable Racket byte
|
receive movable Racket byte storage directly.
|
||||||
storage directly.
|
|
||||||
|
|
||||||
Small decoder buffers are combined before reaching libao. The target chunk
|
Small decoder buffers are combined before reaching libao. The target chunk
|
||||||
size is controlled by the internal @racket[ao-buf-ms] value, currently 250
|
size is controlled by @racket[ao-playback-buf-ms]. The buffer reuse pool keeps
|
||||||
milliseconds. The buffer reuse pool keeps allocated byte strings around for
|
allocated byte strings around for future chunks, reducing allocation churn in
|
||||||
future chunks, reducing allocation churn in long playback sessions.
|
long playback sessions.
|
||||||
|
|
||||||
The conversion path is intentionally narrow. Planar input is converted to
|
The conversion path is intentionally narrow. Planar input is converted to
|
||||||
interleaved PCM. Sample width conversion is done with arithmetic shifts, and
|
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
|
Racket struct, not a C struct. The higher layers can therefore select this
|
||||||
module as an implementation backend without changing the playback API.
|
module as an implementation backend without changing the playback API.
|
||||||
|
|
||||||
The queue state functions report the backend's own administration. They do
|
The queue state functions report the backend's own administration. They do not
|
||||||
not query libao for device latency, and they do not know how many samples are
|
query libao for device latency, and they do not know how many samples are
|
||||||
already buffered by the operating system or the audio driver.
|
already buffered by the operating system or the audio driver.
|
||||||
Reference in New Issue
Block a user