207 lines
6.9 KiB
Racket
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.
|