Files
gemigreerd-racket-audio/scrbl/play-test.scrbl
T
2026-05-16 01:38:40 +02:00

207 lines
6.9 KiB
Racket

#lang scribble/manual
@(require (for-label racket/base
racket/path
early-return
simple-log
"../audio-player.rkt"))
@title{Playback Test Program}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[racket-audio/play-test]
The @racketmodname[racket-audio/play-test.rkt] module is a small integration test and
usage example for @racketmodname[racket-audio/audio-player]. It is not the public
playback API itself; normal applications should use @racketmodname[racket-audio/audio-player]
directly. This module shows how a program can create an audio player, observe
state updates, react to end-of-stream events, and use the EOF callback to drive
a simple playback queue.
The test is intentionally close to the way an application would use the high
level player API. It creates one player handle with @racket[make-audio-player],
prints compact progress information from the state callback, and starts the
next file from the EOF callback when queue mode is enabled.
@section{Purpose}
The test exercises three parts of the player wrapper:
@itemlist[#:style 'compact
@item{state callback handling, including cached position, duration, buffer
size, volume, and logical player state;}
@item{EOF callback handling, including starting another file after the
current stream has reached decoder end-of-stream;}
@item{place based playback through @racket[make-audio-player]'s
@racket[#:use-place] argument.}]
The file depends on @filepath{tests.rkt} for the concrete test files, such as
@racket[test-file2], @racket[test-file3], and @racket[test-file4]. The test
therefore documents the integration pattern rather than a portable standalone
program.
@section{Selecting the test mode}
The module contains a small mode variable:
@racketblock[
(define run-queue #f)
(define (set-test a)
(set! run-queue a))]
When @racket[run-queue] is @racket['queue], the EOF callback consumes files
from @racket[play-queue]. When it is @racket['once], the first EOF callback
starts @racket[test-file3] once and then disables that mode. With the default
@racket[#f] value, the final kickoff call does not start playback.
For queue playback, select queue mode before the kickoff call:
@racketblock[
(set-test 'queue)]
In the current test file the kickoff is performed by calling the EOF callback
manually at the end of the module. That is a convenient test idiom: the same
callback that advances the queue after a stream has finished is also reused to
start the first stream.
@section{Queue setup}
The queue itself is a simple list of path values supplied by
@filepath{tests.rkt}:
@racketblock[
(define play-queue (list test-file2 test-file3 test-file4))]
The queue is destructive in the ordinary Racket sense: each successful EOF
advance starts @racket[(car play-queue)] and then updates @racket[play-queue]
to @racket[(cdr play-queue)]. When the queue is empty, the callback shuts the
player down with @racket[audio-quit!].
@section{Formatting state output}
The helper @racket[to-time-str] turns a second count into a compact
@tt{mm:ss} string:
@racketblock[
(define (to-time-str s*)
(let* ((s (round s*))
(minutes (quotient s 60))
(seconds (remainder s 60)))
(sprintf "%02d:%02d" minutes seconds)))]
The state callback uses this helper to print progress lines that are easier to
read than raw seconds.
@section{State callback}
The state callback has the shape expected by @racket[make-audio-player]:
@racketblock[
(define (audio-player-state h st)
...)]
The first argument is the player handle and the second argument is the state
hash received from the worker side. The callback begins with an
@racket[early-return] guard:
@racketblock[
(early-return
((? (not (audio-play? h)) => 'done))
...)]
This avoids using a handle after it has been invalidated, for example after
@racket[audio-quit!]. The rest of the callback reads the current file name,
position, duration, logical player state, volume, buffer size, and diagnostic
message. It prints at most one line per rounded second by comparing the
current second with @racket[current-sec].
The output line is deliberately compact. It contains the current file name,
music id, playback time, duration, logical state, volume, buffer size, and the
message stored in the state hash.
@section{EOF callback and queue advancement}
The EOF callback is where the queue behaviour is implemented:
@racketblock[
(define (audio-player-eof h)
(dbg-sound "audio-player-eof called")
(when (eq? run-queue 'queue)
(if (null? play-queue)
(audio-quit! h)
(begin
(audio-play! h (car play-queue))
(set! play-queue (cdr play-queue))))))]
In queue mode, an empty queue means that playback is finished and the player is
closed with @racket[audio-quit!]. Otherwise the next file is started with
@racket[audio-play!] and removed from the queue.
The same callback also contains a small @racket['once] mode:
@racketblock[
(when (eq? run-queue 'once)
(set! run-queue #f)
(audio-play! h test-file3))]
That mode is useful when testing a single explicit transition from an EOF event
to a new file.
@section{Creating the player}
The test creates the player with the two callbacks and an explicit place-mode
flag:
@racketblock[
(define place-mode #t)
(define h
(make-audio-player audio-player-state
audio-player-eof
#:use-place place-mode))]
With @racket[place-mode] set to @racket[#t], the player runs the playback side
in a separate place. This is the normal robustness mode for audio playback,
because the decoder and audio feeder run in a separate Racket VM. Setting
@racket[place-mode] to @racket[#f] runs the same command loop in a Racket
thread with ordinary asynchronous channels, which can be easier to debug from
DrRacket.
@section{Starting the test}
At the end of the module, logging is sent to the display and the EOF callback
is called once by hand:
@racketblock[
(sl-log-to-display)
(audio-player-eof h)]
Calling @racket[audio-player-eof] manually may look unusual, but it keeps the
queue logic in one place. The first call starts the first queued file; later
calls are made by the player wrapper when the decoder reports end-of-stream.
A typical queue test therefore looks like this in the source:
@racketblock[
(set-test 'queue)
(sl-log-to-display)
(audio-player-eof h)]
@section{Integration pattern}
The important pattern for an application is not the global variables in the
test file, but the division of responsibility:
@itemlist[#:style 'compact
@item{create one player with @racket[make-audio-player];}
@item{keep display or application state in the state callback;}
@item{keep queue advancement in the EOF callback;}
@item{use @racket[audio-play!] to start the next file;}
@item{use @racket[audio-quit!] when the queue is exhausted.}]
An application will usually wrap the queue in its own data structure instead of
using a top-level mutable list, but the control flow is the same.