mp3 decoder documented

This commit is contained in:
2026-04-22 14:10:57 +02:00
parent 57eea1b000
commit 9b0df72d33
7 changed files with 200 additions and 39 deletions
+41 -27
View File
@@ -8,28 +8,30 @@
@title{audio-decoder}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule["audio-decoder.rkt"]
@defmodule["racket-sound/audio-decoder"]
This module provides a small abstraction layer over concrete audio
decoders. A decoder backend is selected from the filename extension
and is then used through a uniform interface for opening, reading,
seeking, and stopping.
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 also allows additional decoder backends to be registered
with @racket[audio-register-reader!].
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.
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?])
[stopper procedure?]
[ao-type symbol?])
struct?]{
Creates a reader descriptor.
@@ -46,7 +48,10 @@ The procedures are used as follows:
@item{@racket[seeker] seeks within the audio stream;}
@item{@racket[stopper] stops an active decode loop.}]
The built-in FLAC backend is registered in this way.
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?]
@@ -58,6 +63,8 @@ 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}
@@ -72,7 +79,8 @@ otherwise.
Returns the reader type stored in @racket[handle].
For the built-in FLAC backend this value is @racket['flac].
For the built-in readers this is either @racket['flac] or
@racket['mp3].
}
@section{Known extensions and validation}
@@ -81,8 +89,8 @@ For the built-in FLAC backend this value is @racket['flac].
Returns the list of known filename extensions.
The initial list contains @racket["flac"]. Additional extensions are
added when readers are registered with
The initial list contains @racket["flac"] and @racket["mp3"].
Additional extensions are added when readers are registered with
@racket[audio-register-reader!].
}
@@ -91,18 +99,18 @@ added when readers are registered with
Returns @racket[#t] if @racket[ext] denotes a known filename
extension, and @racket[#f] otherwise.
The argument is first converted to a string. If it starts with a dot,
that dot is removed. Matching is case-insensitive.
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 matches
a registered reader, and @racket[#f] otherwise.
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 performs the reader-specific validity check.
the matching reader and calls that reader's validity procedure.
}
@section{Opening and callbacks}
@@ -114,14 +122,14 @@ the matching reader and performs the reader-specific validity check.
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.
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 callback
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.
@@ -131,14 +139,16 @@ backend.
The stream-info callback is called as:
@racketblock[
(cb-stream-info audio-type handle meta)
(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];}
@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.}]
@@ -157,13 +167,14 @@ According to the source comments, @racket[meta] must contain at least:
The audio callback is called as:
@racketblock[
(cb-audio audio-type handle buf-info buffer buf-size)
(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;}
@@ -239,7 +250,10 @@ A backend integrated through this interface should provide:
@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{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 backend.
@racket[audio-open] in the same way as the built-in FLAC and MP3
backends.
+1 -1
View File
@@ -7,7 +7,7 @@
@title{libao}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule["libao.rkt"]
@defmodule["racket-sound/libao"]
This module provides a small high-level interface to an asynchronous
audio output backend. It opens a live output device, queues audio
+149
View File
@@ -0,0 +1,149 @@
#lang scribble/manual
@(require racket/base
(for-label racket/base
racket/path
"../mp3-decoder.rkt"))
@title{mp3-decoder}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[racket-sound/mp3-decoder]
This module provides an MP3 decoder implementation for
@racketmodname[racket-sound/audio-decoder]. It opens an MP3 file,
reports stream information through a callback, streams decoded PCM
buffers, and supports stopping and seeking.
Compared to @racketmodname[racket-sound/flac-decoder], this module
stores its stream information directly in a mutable hash and updates
that hash in place during decoding.
@section{Handles}
@defproc[(mp3-handle? [v any/c]) boolean?]{
Returns @racket[#t] if @racket[v] is an MP3 decoder handle, and
@racket[#f] otherwise.
}
@section{Validation}
@defproc[(mp3-valid? [file any/c]) boolean?]{
Returns @racket[#t].
This procedure does not inspect @racket[file]. It exists to satisfy the
reader interface expected by
@racketmodname[racket-sound/audio-decoder].
In practice, basic validation such as file existence and extension
checking is already performed by
@racketmodname[racket-sound/audio-decoder]. This procedure therefore
acts only as an additional hook and currently accepts all inputs.
}
@section{Opening}
@defproc[(mp3-open [mp3-file* (or/c path? string?)]
[cb-stream-info procedure?]
[cb-audio procedure?])
(or/c mp3-handle? #f)]{
Opens an MP3 decoder for @racket[mp3-file*].
If @racket[mp3-file*] is a path, it is converted with
@racket[path->string]. If the file does not exist, the result is
@racket[#f].
Otherwise a decoder handle is created and initialized. During
initialization, stream information is collected and stored in a
mutable hash in the handle.
The stream-info callback is invoked once, immediately after
initialization:
@racketblock[
(cb-stream-info info)
]
where @racket[info] is a mutable hash containing at least:
@itemlist[#:style 'compact
@item{@racket['duration] --- stream duration in seconds;}
@item{@racket['sample-rate] --- samples per second per channel;}
@item{@racket['channels] --- number of channels;}
@item{@racket['bits-per-sample] --- bits per sample;}
@item{@racket['bytes-per-sample] --- bytes per sample;}
@item{@racket['total-samples] --- total PCM sample count.}]
}
@section{Reading}
@defproc[(mp3-read [handle mp3-handle?]) any/c]{
Starts the decode loop for @racket[handle].
The loop repeatedly decodes audio chunks and invokes the audio
callback:
@racketblock[
(cb-audio info buffer size)
]
Before each callback, the @racket[info] hash is updated in place with:
@itemlist[#:style 'compact
@item{@racket['sample] --- current PCM sample position;}
@item{@racket['current-time] --- current playback time in seconds.}]
The loop terminates when either:
@itemlist[#:style 'compact
@item{the backend reports end-of-stream, or}
@item{a stop has been requested via @racket[mp3-stop].}]
If a stop is detected, the procedure returns
@racket['stopped-reading].
After termination, the underlying decoder is closed and released.
The return value is otherwise unspecified.
}
@section{Seeking}
@defproc[(mp3-seek [handle mp3-handle?]
[percentage number?])
void?]{
Seeks within the stream by percentage of the total length.
The @racket[percentage] argument is interpreted as a value between
@racket[0] and @racket[100]. The target position is computed from the
value stored under @racket['total-samples] in the stream-info hash.
If the total sample count is unavailable, this procedure has no effect.
}
@section{Stopping}
@defproc[(mp3-stop [handle mp3-handle?]) void?]{
Requests termination of an active @racket[mp3-read] loop.
The procedure sets an internal stop flag and waits until the read loop
has terminated, sleeping briefly between checks.
}
@section{Notes}
The stream-info hash is shared between initialization and decoding and
is updated in place during playback.
The audio buffer passed to the callback is managed by the decoder and
should be treated as transient data.
Although the handle contains a field related to deferred seeking, the
current implementation performs seeking directly in
@racket[mp3-seek].