This commit is contained in:
2026-06-05 22:17:30 +02:00
6 changed files with 132 additions and 34 deletions
+7
View File
@@ -0,0 +1,7 @@
all:
@echo "make clean to cleanup bak/~ files"
clean:
rm -f *~ *.bak scrbl/*~ scrbl/*.bak private/*~ private/*.bak
+16 -1
View File
@@ -351,6 +351,14 @@
(define (volume percentage) (define (volume percentage)
(set! req-volume percentage)) (set! req-volume percentage))
(define (ao-buf-ms)
(ao-playback-buf-ms))
(define (ao-buf-ms! ms)
(let ((the-ms (if (< ms 50) 50 (if (> ms 1000) 1000 ms))))
(ao-set-playback-buf-ms! the-ms)
(ao-buf-ms)))
(define (state msg cb . force) (define (state msg cb . force)
(let ((h (make-hash))) (let ((h (make-hash)))
(with-mutex ao-mutex (with-mutex ao-mutex
@@ -384,7 +392,7 @@
(let ((m-id (hash-ref h 'at-music-id))) (let ((m-id (hash-ref h 'at-music-id)))
(unless (and (null? force) (or (eq? m-id #f) (= m-id 0))) (unless (and (null? force) (or (eq? m-id #f) (= m-id 0)))
(cb (list 'state h)))) (cb (list 'state (list h player-state)))))
) )
) )
@@ -418,6 +426,7 @@
(cond (cond
((eq? cmd 'quit) (do-rpc ((eq? cmd 'quit) (do-rpc
(stop-and-cleanup) (stop-and-cleanup)
(set! player-state 'quit)
(state "quit" evt 'force) (state "quit" evt 'force)
'(quit))) '(quit)))
((eq? cmd 'init) (do-rpc ((eq? cmd 'init) (do-rpc
@@ -474,6 +483,12 @@
(let ((st #f)) (let ((st #f))
(state "'state command" (λ (s) (set! st s)) 'force) (state "'state command" (λ (s) (set! st s)) 'force)
st))) st)))
((eq? cmd 'ao-buf-ms)
(do-rpc
(if (null? (cdr data))
(list (ao-buf-ms))
(list (ao-buf-ms! (cadr data))))
))
(else (else
(do-rpc (do-rpc
(list 'error (format "Unknown command ~a" cmd)))) (list 'error (format "Unknown command ~a" cmd))))
+15 -4
View File
@@ -30,6 +30,8 @@
audio-file audio-file
audio-play? audio-play?
audio-buf-seconds! audio-buf-seconds!
audio-ao-buf-ms!
audio-ao-buf-ms
audio-known-exts? audio-known-exts?
) )
@@ -128,7 +130,7 @@
(cmd-put (cons cmd args)) (ret-get)))) (cmd-put (cons cmd args)) (ret-get))))
(let* ((handle #f) (let* ((handle #f)
(cb-state* (λ (st) (cb-state handle st))) (cb-state* (λ (st st-hash) (cb-state handle st st-hash)))
(cb-eof* (λ () (cb-eof-stream handle)))) (cb-eof* (λ () (cb-eof-stream handle))))
(set! handle (make-audio-play #t (set! handle (make-audio-play #t
cb-state* cb-eof* cb-state* cb-eof*
@@ -142,10 +144,11 @@
(let loop () (let loop ()
(if (audio-play-valid? handle) (if (audio-play-valid? handle)
(let ((e (evt-get 500))) (let ((e (evt-get 500)))
(cond ((eq? e #f) (loop)) (cond ((eq? e #f) (void))
((is-event? e 'state) ((is-event? e 'state)
(set-audio-play-state! handle (evt-data e)) (let ((data (evt-data e)))
(cb-state* (evt-data e))) (set-audio-play-state! handle (car data))
(cb-state* (cadr data) (car data))))
((is-event? e 'audio-done) (cb-eof*)) ((is-event? e 'audio-done) (cb-eof*))
((is-event? e 'exception) ((is-event? e 'exception)
(err-sound "audio-player: exception event: ~a" e)) (err-sound "audio-player: exception event: ~a" e))
@@ -270,6 +273,14 @@
(until (if (< max min) (+ min 1) (if (> max 30) 30 max)))) (until (if (< max min) (+ min 1) (if (> max 30) 30 max))))
((audio-play-rpc handle) 'buf-seconds from until))) ((audio-play-rpc handle) 'buf-seconds from until)))
(define/contract (audio-ao-buf-ms! handle ms)
(-> audio-play? integer? (or/c integer? boolean?))
((audio-play-rpc handle) 'ao-buf-ms ms))
(define/contract (audio-ao-buf-ms handle)
(-> audio-play? (or/c integer? boolean?))
((audio-play-rpc handle) 'ao-buf-ms))
+9 -24
View File
@@ -1378,15 +1378,12 @@
(define (call-with-swr-output-buffer max-bytes proc) (define (call-with-swr-output-buffer max-bytes proc)
(early-return (early-return
;; Both allocations are native because swr_convert reads/writes C pointers. ;; Both allocations are native because swr_convert reads/writes C pointers.
((tmp (malloc max-bytes 'raw) ? (eq? tmp #f) => #f) ((tmp (malloc max-bytes 'atomic-interior) ? (eq? tmp #f) => #f)
(out-planes (malloc _pointer 1 'raw) (out-planes (malloc _pointer 1 'atomic-interior)
? (eq? out-planes #f) => #f ? (eq? out-planes #f) => #f)
~ (free tmp))
;; Interleaved output has one plane; out-planes[0] points to tmp. ;; Interleaved output has one plane; out-planes[0] points to tmp.
(do (ptr-set! out-planes _pointer 0 tmp)) (do (ptr-set! out-planes _pointer 0 tmp))
(result (proc tmp out-planes))) (result (proc tmp out-planes)))
(free out-planes)
(free tmp)
result) result)
) )
@@ -1511,27 +1508,20 @@
? (<= delay 0) => produced) ? (<= delay 0) => produced)
(max-bytes (av_samples_get_buffer_size #f channels delay FMPG_OUTPUT_FMT 1) (max-bytes (av_samples_get_buffer_size #f channels delay FMPG_OUTPUT_FMT 1)
? (<= max-bytes 0) => produced) ? (<= max-bytes 0) => produced)
(tmp (malloc max-bytes 'raw) (tmp (malloc max-bytes 'atomic-interior)
? (eq? tmp #f) => -1) ? (eq? tmp #f) => -1)
(out-planes (malloc _pointer 1 'raw) (out-planes (malloc _pointer 1 'atomic-interior)
? (eq? out-planes #f) => -1 ? (eq? out-planes #f) => -1)
~ (free tmp))
(do (ptr-set! out-planes _pointer 0 tmp)) (do (ptr-set! out-planes _pointer 0 tmp))
;; Null input with 0 samples asks swresample to flush delayed output. ;; Null input with 0 samples asks swresample to flush delayed output.
(out-samples (swr_convert swr-ctx out-planes delay #f 0) (out-samples (swr_convert swr-ctx out-planes delay #f 0)
? (<= out-samples 0) => produced ? (<= out-samples 0) => produced)
~ (begin
(free out-planes)
(free tmp)))
(used-bytes (av_samples_get_buffer_size #f channels out-samples (used-bytes (av_samples_get_buffer_size #f channels out-samples
FMPG_OUTPUT_FMT 1) FMPG_OUTPUT_FMT 1)
? (< used-bytes 0) => produced ? (< used-bytes 0) => produced)
~ (begin
(free out-planes)
(free tmp)))
;; If this is the first output, the buffer belongs to the current next-sample-pos. ;; If this is the first output, the buffer belongs to the current next-sample-pos.
(do (do
@@ -1542,17 +1532,12 @@
sample-rate*))))) sample-rate*)))))
(appended? (append-bytes! dec tmp used-bytes) (appended? (append-bytes! dec tmp used-bytes)
? (not appended?) => -1 ? (not appended?) => -1)
~ (begin
(free out-planes)
(free tmp)))
) )
(ds-last-samples! dec (+ (ds-last-samples dec) out-samples)) (ds-last-samples! dec (+ (ds-last-samples dec) out-samples))
(ds-next-sample-pos! dec (+ (ds-next-sample-pos dec) (ds-next-sample-pos! dec (+ (ds-next-sample-pos dec)
out-samples)) out-samples))
(free out-planes)
(free tmp)
(loop 1)) (loop 1))
) )
) )
+1 -1
View File
@@ -197,7 +197,7 @@
;; Playback buffer to send to libao in milliseconds ;; Playback buffer to send to libao in milliseconds
;; ------------------------------------------------------------------------- ;; -------------------------------------------------------------------------
(define ao-buf-ms 150) ;; Playback buffer of 0.15s (define ao-buf-ms 350) ;; Playback buffer of 0.35s
(define (ao-playback-buf-ms) (define (ao-playback-buf-ms)
ao-buf-ms) ao-buf-ms)
+84 -4
View File
@@ -36,11 +36,54 @@ all other procedures in this module.
The @racket[cb-state] callback is called as: The @racket[cb-state] callback is called as:
@racketblock[ @racketblock[
(cb-state player state-hash)] (cb-state player current-player-state state-hash)]
where @racket[player] is the player handle and @racket[state-hash] is the most where @racket[player] is the player handle,
recent state snapshot received from the worker side. The callback is called @racket[current-player-state] is the logical player state reported by the
from the event thread created by @racket[make-audio-player]. worker, and @racket[state-hash] is the most recent state snapshot received from
the worker side. The callback is called from the event thread created by
@racket[make-audio-player].
The worker-side player state is one of the following symbols:
@itemlist[
#:style 'compact
@item{@racket['stopped] -- no stream is currently playing. This is the
initial state of the placed player. The player also enters this state after
@racket[audio-stop!] or after the decoder has reached the end of the stream
and the libao output queue has drained.}
@item{@racket['playing] -- a stream is active. The decoder may still be
reading from the input file, or the decoder may already have finished while
libao is still playing queued PCM samples.}
@item{@racket['paused] -- playback is paused. The current stream is retained
and the libao output side is paused. Resuming playback moves the player back
to @racket['playing].}
@item{@racket['quit] -- the placed player has been asked to terminate. This
is the terminal state of the worker.}
]
The wrapper around the placed player may also report these states through
@racket[audio-state]:
@itemlist[
#:style 'compact
@item{@racket['initialized] -- the audio handle has been created, but no
worker-side state snapshot has been received yet.}
@item{@racket['invalid] -- the audio handle is no longer valid. This happens
after @racket[audio-quit!] or when the underlying place or thread has stopped.}
]
The @racket[state-hash] contains the detailed playback state reported by the
worker. It includes values such as the current playback position, stream
duration, buffer status, music id, and libao handle validity. Code that only
needs the logical playback state should use @racket[current-player-state]
instead of extracting it from the hash.
The @racket[cb-eof-stream] callback is called as: The @racket[cb-eof-stream] callback is called as:
@@ -52,6 +95,8 @@ that the decoder has finished queueing the stream. The audio device may still
have buffered samples to play, and the logical player state may move to have buffered samples to play, and the logical player state may move to
@racket['stopped] slightly later when the output queue has drained. @racket['stopped] slightly later when the output queue has drained.
End-of-stream is not represented as a separate player state.
When @racket[use-place] is true, @racket[make-audio-player] starts When @racket[use-place] is true, @racket[make-audio-player] starts
@racket[placed-player] with @racket[dynamic-place] and communicates with it @racket[placed-player] with @racket[dynamic-place] and communicates with it
through place channels. When @racket[use-place] is false, the same command loop through place channels. When @racket[use-place] is false, the same command loop
@@ -65,6 +110,7 @@ other active threads in the main VM. Those delays can otherwise be heard as
clicks, gaps, or stuttering playback. Thread mode is useful for debugging the clicks, gaps, or stuttering playback. Thread mode is useful for debugging the
protocol and callbacks, but it is not the preferred mode for robust playback.} protocol and callbacks, but it is not the preferred mode for robust playback.}
@defproc[(audio-play? [v any/c]) boolean?]{ @defproc[(audio-play? [v any/c]) boolean?]{
Returns @racket[#t] when @racket[v] is a currently valid audio player handle. Returns @racket[#t] when @racket[v] is a currently valid audio player handle.
@@ -157,6 +203,40 @@ is lowered to @racket[10]. A @racket[max] below @racket[min] is changed to
@racket[30]. The worker side applies its own safe ordering and clamping before @racket[30]. The worker side applies its own safe ordering and clamping before
using the values. In normal use the return value is @racket['ok].} using the values. In normal use the return value is @racket['ok].}
@deftogether[
(@defproc[(audio-ao-buf-ms! [handle audio-play?]
[ms integer?])
(or/c integer? boolean?)]
@defproc[(audio-ao-buf-ms [handle audio-play?])
(or/c integer? boolean?)])
]{
Sets or queries the libao output buffer size, expressed in milliseconds.
The @racket[audio-ao-buf-ms!] procedure forwards @racket[ms] to the audio
player backend by sending the @racket['ao-buf-ms] RPC command. This hooks
into the libao-side buffer configuration and can be used to tune the amount of
audio data that the output layer keeps ahead of playback.
The @racket[audio-ao-buf-ms] procedure queries the currently configured value
by sending the same RPC command without a new value.
The returned value is the value reported by the backend. Normally this is an
integer number of milliseconds. A boolean result indicates that the value could
not be set or queried, or that the backend reported a non-numeric status.
Larger buffer values can make playback more robust against short scheduling
delays, but also increase latency. Smaller values reduce latency, but may make
drop-outs more likely when the decoder or GUI thread is temporarily delayed.
The value is clamped between 50 and 1000ms.
@racketblock[
(audio-ao-buf-ms! player 500)
(audio-ao-buf-ms player)
]
}
@section{State snapshots} @section{State snapshots}
The player keeps a local cache of the most recent state snapshot received from The player keeps a local cache of the most recent state snapshot received from