197 lines
6.6 KiB
Racket
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].
|