2 Audio Player
| (require racket-audio/audio-player) | package: racket-audio |
The racket-audio/audio-player module is the high level interface for audio playback. It hides the command protocol of racket-audio/audio-placed-player, creates the playback place or thread, receives asynchronous events, and exposes a small handle-based API for starting, pausing, seeking, stopping, and observing playback.
The player is asynchronous. Playback commands are sent to a worker side and normally return after the command has been accepted, not after all audio has finished playing. State changes and end-of-stream notifications are delivered through callbacks supplied when the player is created.
2.1 Creating a player
procedure
(make-audio-player cb-state cb-eof-stream #:use-place use-place) → audio-play? cb-state : procedure? cb-eof-stream : procedure? use-place : boolean?
The cb-state callback is called as:
(cb-state player current-player-state state-hash)
where player is the player handle, current-player-state is the logical player state reported by the worker, and state-hash is the most recent state snapshot received from the worker side. The callback is called from the event thread created by make-audio-player.
The worker-side player state is one of the following symbols:
'stopped – no stream is currently playing. This is the initial state of the placed player. The player also enters this state after audio-stop! or after the decoder has reached the end of the stream and the libao output queue has drained.
'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.
'paused – playback is paused. The current stream is retained and the libao output side is paused. Resuming playback moves the player back to 'playing.
'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 audio-state:
'initialized – the audio handle has been created, but no worker-side state snapshot has been received yet.
'invalid – the audio handle is no longer valid. This happens after audio-quit! or when the underlying place or thread has stopped.
The 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 current-player-state instead of extracting it from the hash.
The cb-eof-stream callback is called as:
(cb-eof-stream player)
when the decoder reports that the current stream has been read. This means 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 'stopped slightly later when the output queue has drained.
End-of-stream is not represented as a separate player state.
When use-place is true, make-audio-player starts placed-player with dynamic-place and communicates with it through place channels. When use-place is false, the same command loop is started in an ordinary Racket thread and communicates through async channels. The default value is (place-enabled?).
The place-based mode is the normal playback mode. A place gives the audio side a separate Racket VM, so decoding and buffer feeding are less exposed to scheduling delays caused by DrRacket, GUI event handling, debugging, logging, or 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.
procedure
(audio-play? v) → boolean?
v : any/c
The predicate is intentionally stricter than merely recognizing the underlying structure. After audio-quit! or after the worker has died, the handle is invalidated and audio-play? returns #f.
2.2 Basic playback
procedure
(audio-play! player audio-file) → number?
player : audio-play? audio-file : path-string?
Calling audio-play! while another file is still active replaces the current stream. The worker side interrupts the old decoder, clears the output queue, waits for the old worker thread, and then starts the new stream.
procedure
(audio-pause! player paused?) → symbol?
player : audio-play? paused? : boolean?
Pause is implemented as player state observed by the worker. The worker translates the state to calls to the asynchronous audio backend.
procedure
(audio-paused? player) → boolean?
player : audio-play?
procedure
(audio-stop! player) → symbol?
player : audio-play?
The player remains valid after audio-stop!, so another audio-play! call can be used to start a new file.
procedure
(audio-quit! player) → (or/c number? boolean? symbol?)
player : audio-play?
After this procedure returns, the command loop in the place or thread is expected to terminate. Most other operations require audio-play? and therefore should not be used on the handle after quitting. The implementation also registers a finalizer that sends 'quit when a still-valid handle is collected, but explicit shutdown with audio-quit! is preferred.
2.3 Position, volume, and buffering
procedure
(audio-seek! player percentage) → symbol?
player : audio-play? percentage : (and/c number? (>=/c 0) (<=/c 100))
Seeking does not change the logical playback state. A playing stream remains playing, and a paused stream remains paused.
procedure
(audio-volume! player percentage) → symbol?
player : audio-play? percentage : (and/c number? (>=/c 0))
procedure
(audio-volume player) → number?
player : audio-play?
procedure
(audio-buf-seconds! player min max) → (or/c symbol? boolean?)
player : audio-play? min : number? max : number?
The wrapper normalizes the values before sending the command. A min below 1 is raised to 1, and a min above 10 is lowered to 10. A max below min is changed to (+ min 1), and a max above 30 is lowered to 30. The worker side applies its own safe ordering and clamping before using the values. In normal use the return value is 'ok.
procedure
(audio-ao-buf-ms! handle ms) → (or/c integer? boolean?)
handle : audio-play? ms : integer?
procedure
(audio-ao-buf-ms handle) → (or/c integer? boolean?)
handle : audio-play?
The audio-ao-buf-ms! procedure forwards ms to the audio player backend by sending the '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 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.
(audio-ao-buf-ms! player 500) (audio-ao-buf-ms player)
2.4 State snapshots
The player keeps a local cache of the most recent state snapshot received from the worker. The cache is updated by an event thread created by make-audio-player. The state accessor procedures below read this local cache; they do not synchronously ask the worker for a fresh state.
Before the first state event has arrived, most accessors return #f. The logical state accessor returns 'initialized for a valid handle whose state hash does not yet contain a 'state entry. If the worker dies or the handle is explicitly invalidated, audio-state returns 'invalid.
procedure
(audio-full-state player) → hash?
player : audio-play?
procedure
(audio-state player) → symbol?
player : audio-play?
procedure
(audio-at-second player) → (or/c number? boolean?)
player : audio-play?
procedure
(audio-duration player) → (or/c number? boolean?)
player : audio-play?
procedure
(audio-file player) → (or/c path-string? boolean?)
player : audio-play?
procedure
(audio-music-id player) → (or/c number? boolean?)
player : audio-play?
procedure
(audio-bits player) → (or/c number? boolean?)
player : audio-play?
procedure
(audio-rate player) → (or/c number? boolean?)
player : audio-play?
procedure
(audio-channels player) → (or/c number? boolean?)
player : audio-play?
procedure
(audio-decoder player) → (or/c symbol? boolean?)
player : audio-play?
2.5 Events and callbacks
The wrapper receives asynchronous events from the worker side. A state event updates the cached state hash and calls cb-state. An 'audio-done event calls cb-eof-stream. Unknown events are reported through the module’s warning mechanism.
Callbacks run in the event thread owned by the player handle. They should therefore be quick, should not block for long periods, and should avoid performing complicated UI work directly. A GUI program can use the callbacks to enqueue work onto the GUI eventspace instead.
The RPC command path is protected by a mutex in the wrapper. This allows different application threads to call playback procedures on the same handle without interleaving the command and reply parts of a single RPC.
2.6 Example
The following example creates a player, prints state changes, plays a file, and then shuts the player down explicitly.
For a larger integration example, see "play-test.rkt". The queue variant in that file, selected with (set-test 'queue), is documented separately in "play-test.scrbl".
#lang racket/base (require "audio-player.rkt") (define player (make-audio-player (lambda (p st) (printf "state: ~a at ~a seconds\n" (hash-ref st 'state #f) (hash-ref st 'at-second #f))) (lambda (p) (printf "decoder reached end of stream\n")))) (audio-play! player "track.flac") ;; Later, for example in response to user input: (audio-pause! player #t) (audio-pause! player #f) (audio-seek! player 50) (audio-volume! player 80) (audio-stop! player) ;; When the player is no longer needed: (audio-quit! player)
For debugging the worker in the same Racket VM, create the player with #:use-place #f:
(define debug-player (make-audio-player (lambda (p st) (void)) (lambda (p) (void)) #:use-place #f))
This uses the same command loop and event handling, but starts the worker side in a normal Racket thread instead of a place.