Files
racket-audio/scrbl/opusfile-decoder.scrbl
2026-08-09 19:55:28 +02:00

197 lines
6.6 KiB
Racket

#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].
}
@section[#:tag "opusfile-decoder-validation"]{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[#:tag "opusfile-decoder-opening"]{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[#:tag "opusfile-decoder-reading"]{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[#:tag "opusfile-decoder-stopping"]{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].