initial import from racket-sound -> racket-audio

This commit is contained in:
2026-05-04 12:07:45 +02:00
parent f500f1711b
commit 87980f508a
28 changed files with 6282 additions and 16 deletions
+259
View File
@@ -0,0 +1,259 @@
#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[(file "../audio-decoder.rkt")]
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.