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)
|
||||
|
||||
(define get-current-seconds current-seconds)
|
||||
|
||||
(define (eq-seconds? s1 s2)
|
||||
(let ((s1* (inexact->exact (round s1)))
|
||||
(s2* (inexact->exact (round s2))))
|
||||
@@ -317,7 +319,9 @@
|
||||
(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)))
|
||||
(set! files-playing (cons
|
||||
(cons current-file-id f)
|
||||
@@ -328,7 +332,8 @@
|
||||
|
||||
(when (eq? player-state 'stopped)
|
||||
(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)
|
||||
(when (or (eq? player-state 'paused)
|
||||
@@ -435,8 +440,8 @@
|
||||
((eq? cmd 'open)
|
||||
(do-rpc
|
||||
(let ((file (cadr data)))
|
||||
(start file)
|
||||
'(ok))))
|
||||
(let ((id (start file)))
|
||||
(list (list 'ok id))))))
|
||||
((eq? cmd 'seek)
|
||||
(do-rpc
|
||||
(let ((percentage (cadr data)))
|
||||
|
||||
+7
-2
@@ -143,6 +143,8 @@
|
||||
(set-audio-play-state! handle (evt-data e))
|
||||
(cb-state* (evt-data e)))
|
||||
((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))
|
||||
)
|
||||
(loop))
|
||||
@@ -175,8 +177,11 @@
|
||||
)
|
||||
|
||||
(define/contract (audio-play! handle audio-file)
|
||||
(-> audio-play? path-string? symbol?)
|
||||
((audio-play-rpc handle) 'open audio-file))
|
||||
(-> audio-play? path-string? number?)
|
||||
(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)
|
||||
(-> audio-play? boolean? symbol?)
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@
|
||||
"tests.rkt"
|
||||
)
|
||||
|
||||
(define place-mode #t)
|
||||
(define place-mode #f)
|
||||
|
||||
(define run-queue #f)
|
||||
(define (set-test a)
|
||||
@@ -60,13 +60,13 @@
|
||||
(if (null? play-queue)
|
||||
(audio-quit! h)
|
||||
(begin
|
||||
(audio-play! h (car play-queue))
|
||||
(dbg-sound "audio-play! -> ~a" (audio-play! h (car play-queue)))
|
||||
(set! play-queue (cdr play-queue))
|
||||
)
|
||||
))
|
||||
(when (eq? run-queue 'once)
|
||||
(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
|
||||
|
||||
@@ -72,7 +72,8 @@ The important command-level behaviour is:
|
||||
@item{@racket['init] installs the reply and event channels and moves the
|
||||
player into the initialized command loop.}
|
||||
@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
|
||||
that state and applies @racket[ao-pause] to the output side.}
|
||||
@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
|
||||
@racket[ch-evt], and replies with @racket['(initialized)].}
|
||||
@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
|
||||
@racket['paused] or @racket['playing] when the player is already active,
|
||||
and replies with @racket['(ok)].}
|
||||
|
||||
@@ -76,10 +76,12 @@ is invalidated and @racket[audio-play?] returns @racket[#f].}
|
||||
|
||||
@defproc[(audio-play! [player audio-play?]
|
||||
[audio-file path-string?])
|
||||
symbol?]{
|
||||
number?]{
|
||||
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
|
||||
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
|
||||
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}
|
||||
@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
|
||||
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