From 73e778e4a58e6e3443a5c1f38d8312de540992c5 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Mon, 18 May 2026 16:36:19 +0200 Subject: [PATCH 1/6] ao buffer timing --- audio-placed-player.rkt | 14 ++++++++++++++ audio-player.rkt | 10 ++++++++++ libao-async-ffi-racket.rkt | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/audio-placed-player.rkt b/audio-placed-player.rkt index 2e08e47..2eb40f0 100644 --- a/audio-placed-player.rkt +++ b/audio-placed-player.rkt @@ -351,6 +351,14 @@ (define (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) (let ((h (make-hash))) (with-mutex ao-mutex @@ -474,6 +482,12 @@ (let ((st #f)) (state "'state command" (λ (s) (set! st s)) 'force) st))) + ((eq? cmd 'ao-buf-ms) + (do-rpc + (if (null? (cdr data)) + (list (ao-buf-ms)) + (list (ao-buf-ms! (cadr data)))) + )) (else (do-rpc (list 'error (format "Unknown command ~a" cmd)))) diff --git a/audio-player.rkt b/audio-player.rkt index 0a69e0f..55c518f 100644 --- a/audio-player.rkt +++ b/audio-player.rkt @@ -30,6 +30,8 @@ audio-file audio-play? audio-buf-seconds! + audio-ao-buf-ms! + audio-ao-buf-ms audio-known-exts? ) @@ -269,6 +271,14 @@ (let ((from (if (< min 1) 1 (if (> min 10) 10 min))) (until (if (< max min) (+ min 1) (if (> max 30) 30 max)))) ((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)) diff --git a/libao-async-ffi-racket.rkt b/libao-async-ffi-racket.rkt index 3cc4cc6..6a8c015 100644 --- a/libao-async-ffi-racket.rkt +++ b/libao-async-ffi-racket.rkt @@ -197,7 +197,7 @@ ;; 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) ao-buf-ms) From 360b9eea471f0d8cae6568852233aa95701bfd4b Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Tue, 19 May 2026 13:35:40 +0200 Subject: [PATCH 2/6] documented some new functions --- scrbl/audio-player.scrbl | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scrbl/audio-player.scrbl b/scrbl/audio-player.scrbl index d59d215..127b5a4 100644 --- a/scrbl/audio-player.scrbl +++ b/scrbl/audio-player.scrbl @@ -157,6 +157,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 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} The player keeps a local cache of the most recent state snapshot received from From ef686722032f02e124a95c7fd371af03131d01a6 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Tue, 19 May 2026 13:43:07 +0200 Subject: [PATCH 3/6] change player-state to (quote quit), if quit is called. This helps with the last state event, because the audio handle can already have been invalideted --- audio-placed-player.rkt | 1 + 1 file changed, 1 insertion(+) diff --git a/audio-placed-player.rkt b/audio-placed-player.rkt index 2eb40f0..cb035ab 100644 --- a/audio-placed-player.rkt +++ b/audio-placed-player.rkt @@ -426,6 +426,7 @@ (cond ((eq? cmd 'quit) (do-rpc (stop-and-cleanup) + (set! player-state 'quit) (state "quit" evt 'force) '(quit))) ((eq? cmd 'init) (do-rpc From 2daaafb229c0c64afafeef2a74ec6192a709c769 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Tue, 19 May 2026 14:30:57 +0200 Subject: [PATCH 4/6] better state reporting --- audio-placed-player.rkt | 2 +- audio-player.rkt | 9 ++++--- scrbl/audio-player.scrbl | 54 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/audio-placed-player.rkt b/audio-placed-player.rkt index cb035ab..bb8306a 100644 --- a/audio-placed-player.rkt +++ b/audio-placed-player.rkt @@ -392,7 +392,7 @@ (let ((m-id (hash-ref h 'at-music-id))) (unless (and (null? force) (or (eq? m-id #f) (= m-id 0))) - (cb (list 'state h)))) + (cb (list 'state (list h player-state))))) ) ) diff --git a/audio-player.rkt b/audio-player.rkt index 55c518f..fd07437 100644 --- a/audio-player.rkt +++ b/audio-player.rkt @@ -130,7 +130,7 @@ (cmd-put (cons cmd args)) (ret-get)))) (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)))) (set! handle (make-audio-play #t cb-state* cb-eof* @@ -144,10 +144,11 @@ (let loop () (if (audio-play-valid? handle) (let ((e (evt-get 500))) - (cond ((eq? e #f) (loop)) + (cond ((eq? e #f) (void)) ((is-event? e 'state) - (set-audio-play-state! handle (evt-data e)) - (cb-state* (evt-data e))) + (let ((data (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 'exception) (err-sound "audio-player: exception event: ~a" e)) diff --git a/scrbl/audio-player.scrbl b/scrbl/audio-player.scrbl index 127b5a4..6a76353 100644 --- a/scrbl/audio-player.scrbl +++ b/scrbl/audio-player.scrbl @@ -36,11 +36,54 @@ all other procedures in this module. The @racket[cb-state] callback is called as: @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 -recent state snapshot received from the worker side. The callback is called -from the event thread created by @racket[make-audio-player]. +where @racket[player] is the player handle, +@racket[current-player-state] is the logical player state reported by the +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: @@ -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 @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 @racket[placed-player] with @racket[dynamic-place] and communicates with it 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 protocol and callbacks, but it is not the preferred mode for robust playback.} + @defproc[(audio-play? [v any/c]) boolean?]{ Returns @racket[#t] when @racket[v] is a currently valid audio player handle. From 9f5c4d3efc98b2cc7602d94f8c7ae5b24d553c9b Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Tue, 19 May 2026 15:10:28 +0200 Subject: [PATCH 5/6] make clean added --- Makefile | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 Makefile diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ad561e6 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ + + +all: + @echo "make clean to cleanup bak/~ files" + +clean: + rm -f *~ *.bak scrbl/*~ scrbl/*.bak private/*~ private/*.bak From bfed212346430038ccf5e0a0696b136668f48102 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Tue, 19 May 2026 15:14:23 +0200 Subject: [PATCH 6/6] changed malloc 'raw to malloc 'atomic-interior --- ffmpeg-definitions.rkt | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/ffmpeg-definitions.rkt b/ffmpeg-definitions.rkt index af5e6e9..2f1d257 100644 --- a/ffmpeg-definitions.rkt +++ b/ffmpeg-definitions.rkt @@ -1378,15 +1378,12 @@ (define (call-with-swr-output-buffer max-bytes proc) (early-return ;; Both allocations are native because swr_convert reads/writes C pointers. - ((tmp (malloc max-bytes 'raw) ? (eq? tmp #f) => #f) - (out-planes (malloc _pointer 1 'raw) - ? (eq? out-planes #f) => #f - ~ (free tmp)) + ((tmp (malloc max-bytes 'atomic-interior) ? (eq? tmp #f) => #f) + (out-planes (malloc _pointer 1 'atomic-interior) + ? (eq? out-planes #f) => #f) ;; Interleaved output has one plane; out-planes[0] points to tmp. (do (ptr-set! out-planes _pointer 0 tmp)) (result (proc tmp out-planes))) - (free out-planes) - (free tmp) result) ) @@ -1511,27 +1508,20 @@ ? (<= delay 0) => produced) (max-bytes (av_samples_get_buffer_size #f channels delay FMPG_OUTPUT_FMT 1) ? (<= max-bytes 0) => produced) - (tmp (malloc max-bytes 'raw) + (tmp (malloc max-bytes 'atomic-interior) ? (eq? tmp #f) => -1) - (out-planes (malloc _pointer 1 'raw) - ? (eq? out-planes #f) => -1 - ~ (free tmp)) + (out-planes (malloc _pointer 1 'atomic-interior) + ? (eq? out-planes #f) => -1) (do (ptr-set! out-planes _pointer 0 tmp)) ;; Null input with 0 samples asks swresample to flush delayed output. (out-samples (swr_convert swr-ctx out-planes delay #f 0) - ? (<= out-samples 0) => produced - ~ (begin - (free out-planes) - (free tmp))) + ? (<= out-samples 0) => produced) (used-bytes (av_samples_get_buffer_size #f channels out-samples FMPG_OUTPUT_FMT 1) - ? (< used-bytes 0) => produced - ~ (begin - (free out-planes) - (free tmp))) + ? (< used-bytes 0) => produced) ;; If this is the first output, the buffer belongs to the current next-sample-pos. (do @@ -1542,17 +1532,12 @@ sample-rate*))))) (appended? (append-bytes! dec tmp used-bytes) - ? (not appended?) => -1 - ~ (begin - (free out-planes) - (free tmp))) + ? (not appended?) => -1) ) (ds-last-samples! dec (+ (ds-last-samples dec) out-samples)) (ds-next-sample-pos! dec (+ (ds-next-sample-pos dec) out-samples)) - (free out-planes) - (free tmp) (loop 1)) ) )