Opus decoder is now documented

This commit is contained in:
2026-07-02 15:10:30 +02:00
parent 88f911d96e
commit 303f6957df
2 changed files with 258 additions and 12 deletions
+62 -12
View File
@@ -2,8 +2,10 @@
@(require racket/base @(require racket/base
(for-label racket/base (for-label racket/base
racket/contract
racket/path racket/path
"../audio-decoder.rkt")) "../audio-decoder.rkt"
"../audio-sniffer.rkt"))
@title{audio-decoder} @title{audio-decoder}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@@ -15,9 +17,9 @@ decoders. A backend is selected from the filename extension and is then
used through a uniform interface for opening, reading, seeking, and used through a uniform interface for opening, reading, seeking, and
stopping. stopping.
The module includes built-in readers for FLAC and MP3, and it allows The module includes built-in readers for FLAC, MP3, Opus via
additional backends to be registered with @tt{libopusfile}, and FFmpeg-backed formats. Additional backends can be
@racket[audio-register-reader!]. registered with @racket[audio-register-reader!].
@section{Reader registration} @section{Reader registration}
@@ -79,8 +81,8 @@ otherwise.
Returns the reader type stored in @racket[handle]. Returns the reader type stored in @racket[handle].
For the built-in readers this is either @racket['flac] or For the built-in readers this is usually @racket['flac], @racket['mp3],
@racket['mp3]. @racket['opusfile], or @racket['ffmpeg].
} }
@section{Known extensions and validation} @section{Known extensions and validation}
@@ -89,9 +91,56 @@ For the built-in readers this is either @racket['flac] or
Returns the list of known filename extensions. Returns the list of known filename extensions.
The initial list contains @racket["flac"] and @racket["mp3"]. The initial list contains the extensions handled by the built-in readers:
Additional extensions are added when readers are registered with @racket["flac"], @racket["mp3"], Opus/Ogg extensions such as
@racket[audio-register-reader!]. @racket["opus"], @racket["ogg"], and @racket["oga"], and the extensions
handled by the FFmpeg reader. Additional extensions are added when
readers are registered with @racket[audio-register-reader!].
}
@defproc[(audio-supported-extensions) (listof string?)]{
Returns the current list of supported filename extensions. This is the
same list returned by @racket[audio-known-exts?].
}
@section{Built-in decoder selection}
The built-in decoder table contains:
@itemlist[#:style 'compact
@item{@racket['flac] for FLAC files;}
@item{@racket['mp3] for MP3 files;}
@item{@racket['opusfile] for Ogg Opus streams through Xiph
@tt{libopusfile};}
@item{@racket['ffmpeg] for formats handled by the FFmpeg backend.}]
When a file is opened, the module first asks
@racket[audio-sniff-format/extension] for the detected format. Opus
streams are mapped to @racket['opusfile] when @tt{libopusfile} is
available and to @racket['ffmpeg] otherwise.
The Opusfile decoder has one global setting for its PCM output format:
@racket[current-opusfile-output-format]. This setting is re-exported by
this module for callers that use only the generic decoder API.
@defthing[current-opusfile-output-format procedure?]{
A global setting procedure for the Opusfile backend. Called without
arguments, it returns the current Opusfile output format. Called with one
argument, it sets the output format. Accepted values are @racket['s16]
and @racket['s24].
The default is @racket['s16]. Set it to @racket['s24] before opening or
reading an Opus file when the playback pipeline should receive packed
signed 24-bit PCM instead of signed 16-bit PCM.
}
@defproc[(opusfile-output-format? [v any/c]) boolean?]{
Returns @racket[#t] when @racket[v] is one of the supported Opusfile
output format symbols: @racket['s16] or @racket['s24].
} }
@defproc[(audio-valid-ext? [ext any/c]) boolean?]{ @defproc[(audio-valid-ext? [ext any/c]) boolean?]{
@@ -146,7 +195,8 @@ where:
@itemlist[#:style 'compact @itemlist[#:style 'compact
@item{@racket[audio-type] is the registered reader type, such as @item{@racket[audio-type] is the registered reader type, such as
@racket['flac] or @racket['mp3];} @racket['flac], @racket['mp3], @racket['opusfile], or
@racket['ffmpeg];}
@item{@racket[ao-type] is the audio-output type stored in the reader, @item{@racket[ao-type] is the audio-output type stored in the reader,
such as @racket['flac] or @racket['ao];} such as @racket['flac] or @racket['ao];}
@item{@racket[handle] is the generic @racket[audio-handle];} @item{@racket[handle] is the generic @racket[audio-handle];}
@@ -255,5 +305,5 @@ A backend integrated through this interface should provide:
backend produces.}] backend produces.}]
Once registered, files with matching extensions can be opened through Once registered, files with matching extensions can be opened through
@racket[audio-open] in the same way as the built-in FLAC and MP3 @racket[audio-open] in the same way as the built-in FLAC, MP3,
backends. Opusfile, and FFmpeg backends.
+196
View File
@@ -0,0 +1,196 @@
#lang scribble/manual
@(require racket/base
(for-label racket/base
racket/contract
racket/path
"../opusfile-decoder.rkt"))
@title{opusfile-decoder}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[racket-audio/opusfile-decoder]
This module provides an Opus decoder backend based on Xiph
@tt{libopusfile}. It opens Ogg Opus files, reports stream information
through a callback, streams decoded interleaved PCM buffers, and supports
stopping and seeking.
The module is intended to be used through
@racketmodname[racket-audio/audio-decoder], but its procedures can also
be used directly.
Opus decoding produces 48 kHz PCM. The original input rate stored in an
Opus file, if present, is not the decoder output sample rate.
@section{Availability}
@defproc[(opusfile-available?) boolean?]{
Returns @racket[#t] if @tt{libopusfile} and the native procedures used
by this backend could be loaded, and @racket[#f] otherwise.
The generic @racketmodname[racket-audio/audio-decoder] module prefers
this decoder for Opus streams when it is available. If it is not
available, Opus files may still be handled by the FFmpeg backend if that
backend is available.
}
@section{Output format setting}
@defproc[(opusfile-output-format? [v any/c]) boolean?]{
Returns @racket[#t] when @racket[v] is one of the supported Opus decoder
output formats: @racket['s16] or @racket['s24].
}
@defthing[current-opusfile-output-format procedure?]{
A global output-format setting for this decoder.
Called without arguments, it returns the current output format. Called
with one argument, it sets the current output format and returns the new
value. The accepted values are:
@itemlist[#:style 'compact
@item{@racket['s16] --- signed 16-bit interleaved PCM. This is the
default. The backend uses @tt{op_read}.}
@item{@racket['s24] --- packed signed 24-bit interleaved PCM in native
byte order. The backend uses @tt{op_read_float} and converts the
float samples to 24-bit PCM.}]
The setting is global. It is read when stream format hashes are produced
and when audio buffers are decoded. For normal use, set it before
opening or reading an Opus file.
Example:
@racketblock[
(current-opusfile-output-format 's24)
]
This binding is also re-exported by
@racketmodname[racket-audio/audio-decoder] and by
@racketmodname[racket-audio/main].
}
@section{Validation}
@defproc[(opusfile-valid? [audio-file any/c]) boolean?]{
Returns @racket[#t] when @tt{libopusfile} is available and
@racket[audio-file] exists.
This predicate is deliberately small. Detailed validation is performed
when the file is opened by @racket[opusfile-open]. The generic decoder
layer also performs extension and existence checks before opening a file.
}
@section{Opening}
@defproc[(opusfile-open [audio-file (or/c path? string?)]
[cb-stream-info procedure?]
[cb-audio procedure?])
(or/c struct? #f)]{
Opens @racket[audio-file] with @tt{libopusfile} and returns an opaque
Opus decoder handle. If @racket[audio-file] is a path, it is converted
with @racket[path->string]. If the file does not exist, the result is
@racket[#f].
If @tt{libopusfile} cannot be loaded, an exception is raised. If the
file exists but cannot be opened by @tt{libopusfile}, an exception is
raised with the native Opusfile error code.
The stream-info callback is called once after the file has been opened:
@racketblock[
(cb-stream-info info)
]
where @racket[info] is a mutable hash containing at least:
@itemlist[#:style 'compact
@item{@racket['duration] --- duration in seconds, based on the total
number of decoded PCM samples when available;}
@item{@racket['sample-rate] --- always @racket[48000];}
@item{@racket['channels] --- number of decoded channels;}
@item{@racket['bits-per-sample] --- @racket[16] for @racket['s16] and
@racket[24] for @racket['s24];}
@item{@racket['bytes-per-sample] --- @racket[2] for @racket['s16] and
@racket[3] for @racket['s24];}
@item{@racket['sample-format] --- the value of
@racket[current-opusfile-output-format];}
@item{@racket['total-samples] --- total number of decoded PCM samples,
or the value reported by @tt{libopusfile}.}]
}
@section{Reading}
@defproc[(opusfile-read [handle struct?]) any/c]{
Starts the decode loop for @racket[handle].
The loop repeatedly decodes audio blocks and invokes the audio callback:
@racketblock[
(cb-audio info buffer size)
]
where @racket[info] is the mutable stream-info hash, @racket[buffer] is
an interleaved PCM buffer, and @racket[size] is the buffer size in bytes.
Before each callback, the info hash is updated in place with:
@itemlist[#:style 'compact
@item{@racket['sample] --- the current decoded sample position;}
@item{@racket['current-time] --- the current decoded time in seconds.}]
The buffer format is determined by
@racket[current-opusfile-output-format]. In @racket['s16] mode the
buffer contains signed 16-bit PCM. In @racket['s24] mode the buffer
contains packed signed 24-bit PCM.
The loop also checks for a pending seek request. If a seek has been
requested with @racket[opusfile-seek], it is applied before the next
read from @tt{libopusfile}.
The loop terminates at end-of-stream or when a stop has been requested
with @racket[opusfile-stop]. After termination, the underlying
@tt{libopusfile} handle is freed.
}
@section[#:tag "opusfile-decoder-seeking"]{Seeking}
@defproc[(opusfile-seek [handle struct?]
[percentage number?])
void?]{
Requests a seek within the stream.
The @racket[percentage] argument represents a position relative to the
full stream, where @racket[0] is the start and @racket[100] is the end.
The value may be fractional and is clamped to that range.
If the total sample count is known and non-zero, the procedure computes
a target decoded sample and stores it as a pending seek request. The
actual native seek is performed later by @racket[opusfile-read].
}
@section{Stopping}
@defproc[(opusfile-stop [handle struct?]) void?]{
Requests termination of an active @racket[opusfile-read] loop.
The procedure sets an internal stop flag and waits until the read loop
has terminated, sleeping briefly between checks.
}
@section[#:tag "opusfile-decoder-notes"]{Notes}
The Opusfile backend is registered in
@racketmodname[racket-audio/audio-decoder] as reader type
@racket['opusfile] for files and streams recognized as Opus.
The generic decoder callbacks therefore receive @racket['opusfile] as
@racket[audio-type] and @racket['ao] as @racket[ao-type].