Documentation added
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
#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.
|
||||
Reference in New Issue
Block a user