259 lines
8.5 KiB
Racket
259 lines
8.5 KiB
Racket
#lang scribble/manual
|
|
|
|
@(require racket/base
|
|
(for-label racket/base
|
|
racket/path
|
|
"../audio-decoder.rkt"))
|
|
|
|
@title{audio-decoder}
|
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
|
|
|
@defmodule[racket-audio/audio-decoder]
|
|
|
|
This module provides a small abstraction layer over concrete audio
|
|
decoders. A backend is selected from the filename extension and is then
|
|
used through a uniform interface for opening, reading, seeking, and
|
|
stopping.
|
|
|
|
The module includes built-in readers for FLAC and MP3, and it allows
|
|
additional backends to be registered with
|
|
@racket[audio-register-reader!].
|
|
|
|
@section{Reader registration}
|
|
|
|
A reader descriptor stores the extensions handled by a backend together
|
|
with the procedures used to validate, open, read, seek, and stop that
|
|
backend, plus an audio-output type.
|
|
|
|
@defproc[(make-audio-reader [exts (listof string?)]
|
|
[valid? procedure?]
|
|
[open procedure?]
|
|
[reader procedure?]
|
|
[seeker procedure?]
|
|
[stopper procedure?]
|
|
[ao-type symbol?])
|
|
struct?]{
|
|
|
|
Creates a reader descriptor.
|
|
|
|
The @racket[exts] list contains the filename extensions handled by the
|
|
reader, without a leading dot. Matching is case-insensitive.
|
|
|
|
The procedures are used as follows:
|
|
|
|
@itemlist[#:style 'compact
|
|
@item{@racket[valid?] checks whether a file is valid for this reader;}
|
|
@item{@racket[open] opens a decoder for a file;}
|
|
@item{@racket[reader] reads or continues decoding;}
|
|
@item{@racket[seeker] seeks within the audio stream;}
|
|
@item{@racket[stopper] stops an active decode loop.}]
|
|
|
|
The @racket[ao-type] value describes the buffer format exposed to the
|
|
audio output layer. The source comments mention values such as
|
|
@racket['flac] and @racket['ao]. The value @racket['ao] means that the
|
|
buffer can be used directly by the audio-output backend.
|
|
}
|
|
|
|
@defproc[(audio-register-reader! [type symbol?]
|
|
[reader struct?])
|
|
void?]{
|
|
|
|
Registers @racket[reader] under @racket[type].
|
|
|
|
The extensions declared in @racket[reader] are appended to the list
|
|
returned by @racket[audio-known-exts?], and the reader becomes
|
|
available to @racket[audio-open].
|
|
|
|
This procedure is the extension point for custom audio decoders.
|
|
}
|
|
|
|
@section{Audio handles}
|
|
|
|
@defproc[(audio-handle? [v any/c]) boolean?]{
|
|
|
|
Returns @racket[#t] if @racket[v] is an audio handle, and @racket[#f]
|
|
otherwise.
|
|
}
|
|
|
|
@defproc[(audio-kind [handle audio-handle?]) symbol?]{
|
|
|
|
Returns the reader type stored in @racket[handle].
|
|
|
|
For the built-in readers this is either @racket['flac] or
|
|
@racket['mp3].
|
|
}
|
|
|
|
@section{Known extensions and validation}
|
|
|
|
@defproc[(audio-known-exts?) (listof string?)]{
|
|
|
|
Returns the list of known filename extensions.
|
|
|
|
The initial list contains @racket["flac"] and @racket["mp3"].
|
|
Additional extensions are added when readers are registered with
|
|
@racket[audio-register-reader!].
|
|
}
|
|
|
|
@defproc[(audio-valid-ext? [ext any/c]) boolean?]{
|
|
|
|
Returns @racket[#t] if @racket[ext] denotes a known filename
|
|
extension, and @racket[#f] otherwise.
|
|
|
|
The argument is converted to a string. If it starts with a dot, that
|
|
dot is removed. Matching is case-insensitive.
|
|
}
|
|
|
|
@defproc[(audio-file-valid? [file (or/c string? path?)]) boolean?]{
|
|
|
|
Returns @racket[#t] if @racket[file] has a known extension and the
|
|
matching registered reader reports the file as valid.
|
|
|
|
This procedure first derives the filename extension and checks it with
|
|
@racket[audio-valid-ext?]. If the extension is known, it then looks up
|
|
the matching reader and calls that reader's validity procedure.
|
|
}
|
|
|
|
@section{Opening and callbacks}
|
|
|
|
@defproc[(audio-open [audio-file (or/c string? path?)]
|
|
[cb-stream-info procedure?]
|
|
[cb-audio procedure?])
|
|
audio-handle?]{
|
|
|
|
Opens an audio decoder for @racket[audio-file].
|
|
|
|
If @racket[audio-file] is a path, it is converted to a string before it
|
|
is passed to the backend open procedure.
|
|
|
|
This procedure raises an exception if the file is not considered a
|
|
valid audio file, if the file does not exist, or if no registered
|
|
reader can be found for the file.
|
|
|
|
The returned handle stores the selected reader type, the two callback
|
|
procedures, the reader descriptor, and the driver-specific handle
|
|
returned by the backend open procedure.
|
|
|
|
The callback procedures are wrapped before they are passed to the
|
|
backend.
|
|
|
|
The stream-info callback is called as:
|
|
|
|
@racketblock[
|
|
(cb-stream-info audio-type ao-type handle meta)
|
|
]
|
|
|
|
where:
|
|
|
|
@itemlist[#:style 'compact
|
|
@item{@racket[audio-type] is the registered reader type, such as
|
|
@racket['flac] or @racket['mp3];}
|
|
@item{@racket[ao-type] is the audio-output type stored in the reader,
|
|
such as @racket['flac] or @racket['ao];}
|
|
@item{@racket[handle] is the generic @racket[audio-handle];}
|
|
@item{@racket[meta] is a hash table with stream metadata.}]
|
|
|
|
According to the source comments, @racket[meta] must contain at least:
|
|
|
|
@itemlist[#:style 'compact
|
|
@item{@racket['duration] --- duration of the audio in seconds, possibly
|
|
fractional;}
|
|
@item{@racket['bits-per-sample] --- number of audio bits per sample;}
|
|
@item{@racket['channels] --- number of audio channels;}
|
|
@item{@racket['sample-rate] --- number of samples per second per
|
|
channel;}
|
|
@item{@racket['total-samples] --- total number of samples in the
|
|
audio.}]
|
|
|
|
The audio callback is called as:
|
|
|
|
@racketblock[
|
|
(cb-audio audio-type ao-type handle buf-info buffer buf-size)
|
|
]
|
|
|
|
where:
|
|
|
|
@itemlist[#:style 'compact
|
|
@item{@racket[audio-type] is the registered reader type;}
|
|
@item{@racket[ao-type] is the audio-output type stored in the reader;}
|
|
@item{@racket[handle] is the generic @racket[audio-handle];}
|
|
@item{@racket[buf-info] is a hash table describing the audio buffer;}
|
|
@item{@racket[buffer] is a native buffer containing audio data;}
|
|
@item{@racket[buf-size] is the size of that buffer in bytes.}]
|
|
|
|
According to the source comments, the buffer is to be owned and
|
|
released by the decoder driver. The comments also note that the
|
|
@tt{ao-async} backend copies the data.
|
|
|
|
According to the source comments, @racket[buf-info] must contain at
|
|
least:
|
|
|
|
@itemlist[#:style 'compact
|
|
@item{@racket['duration] --- duration of the audio in seconds, possibly
|
|
fractional;}
|
|
@item{@racket['bits-per-sample] --- number of audio bits per sample;}
|
|
@item{@racket['channels] --- number of audio channels;}
|
|
@item{@racket['sample-rate] --- number of samples per second per
|
|
channel;}
|
|
@item{@racket['total-samples] --- total number of samples in the
|
|
audio;}
|
|
@item{@racket['sample] --- the current sample to which the audio
|
|
buffer applies.}]
|
|
}
|
|
|
|
@section{Reading, seeking, and stopping}
|
|
|
|
@defproc[(audio-read [handle audio-handle?]) void?]{
|
|
|
|
Calls the registered reader procedure for @racket[handle].
|
|
|
|
The concrete reader procedure receives the driver-specific handle
|
|
stored in the generic audio handle. Any result value produced by the
|
|
backend is discarded.
|
|
}
|
|
|
|
@defproc[(audio-seek [handle audio-handle?]
|
|
[percentage number?])
|
|
void?]{
|
|
|
|
Calls the registered seek procedure for @racket[handle].
|
|
|
|
The @racket[percentage] argument is passed unchanged to the backend
|
|
seek procedure.
|
|
|
|
In this abstraction layer, the parameter represents a relative
|
|
position in the full audio stream. A backend registered through
|
|
@racket[audio-register-reader!] is expected to follow that
|
|
interpretation.
|
|
}
|
|
|
|
@defproc[(audio-stop [handle audio-handle?]) void?]{
|
|
|
|
Calls the registered stop procedure for @racket[handle].
|
|
|
|
The concrete stop procedure receives the driver-specific handle stored
|
|
in the generic audio handle.
|
|
}
|
|
|
|
@section{Using custom decoders}
|
|
|
|
Custom audio decoders can be integrated by constructing a reader
|
|
descriptor with @racket[make-audio-reader] and registering it with
|
|
@racket[audio-register-reader!].
|
|
|
|
A backend integrated through this interface should provide:
|
|
|
|
@itemlist[#:style 'compact
|
|
@item{a list of handled filename extensions;}
|
|
@item{a file-validity procedure;}
|
|
@item{an open procedure that accepts a file path, a stream-info
|
|
callback, and an audio callback;}
|
|
@item{a read procedure that accepts the driver-specific handle;}
|
|
@item{a seek procedure that accepts the driver-specific handle and a
|
|
numeric relative position;}
|
|
@item{a stop procedure that accepts the driver-specific handle;}
|
|
@item{an audio-output type symbol describing the kind of buffers the
|
|
backend produces.}]
|
|
|
|
Once registered, files with matching extensions can be opened through
|
|
@racket[audio-open] in the same way as the built-in FLAC and MP3
|
|
backends. |