From 303f6957dff7bd3ea503f2f60a9e04a243299af1 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Thu, 2 Jul 2026 15:10:30 +0200 Subject: [PATCH] Opus decoder is now documented --- scrbl/audio-decoder.scrbl | 74 ++++++++++--- scrbl/opusfile-decoder.scrbl | 196 +++++++++++++++++++++++++++++++++++ 2 files changed, 258 insertions(+), 12 deletions(-) create mode 100644 scrbl/opusfile-decoder.scrbl diff --git a/scrbl/audio-decoder.scrbl b/scrbl/audio-decoder.scrbl index cc7f987..2f2394f 100644 --- a/scrbl/audio-decoder.scrbl +++ b/scrbl/audio-decoder.scrbl @@ -2,8 +2,10 @@ @(require racket/base (for-label racket/base + racket/contract racket/path - "../audio-decoder.rkt")) + "../audio-decoder.rkt" + "../audio-sniffer.rkt")) @title{audio-decoder} @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 stopping. -The module includes built-in readers for FLAC and MP3, and it allows -additional backends to be registered with -@racket[audio-register-reader!]. +The module includes built-in readers for FLAC, MP3, Opus via +@tt{libopusfile}, and FFmpeg-backed formats. Additional backends can be +registered with @racket[audio-register-reader!]. @section{Reader registration} @@ -79,8 +81,8 @@ otherwise. Returns the reader type stored in @racket[handle]. -For the built-in readers this is either @racket['flac] or -@racket['mp3]. +For the built-in readers this is usually @racket['flac], @racket['mp3], +@racket['opusfile], or @racket['ffmpeg]. } @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. -The initial list contains @racket["flac"] and @racket["mp3"]. -Additional extensions are added when readers are registered with -@racket[audio-register-reader!]. +The initial list contains the extensions handled by the built-in readers: +@racket["flac"], @racket["mp3"], Opus/Ogg extensions such as +@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?]{ @@ -146,7 +195,8 @@ where: @itemlist[#:style 'compact @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, such as @racket['flac] or @racket['ao];} @item{@racket[handle] is the generic @racket[audio-handle];} @@ -255,5 +305,5 @@ A backend integrated through this interface should provide: 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. \ No newline at end of file +@racket[audio-open] in the same way as the built-in FLAC, MP3, +Opusfile, and FFmpeg backends. \ No newline at end of file diff --git a/scrbl/opusfile-decoder.scrbl b/scrbl/opusfile-decoder.scrbl new file mode 100644 index 0000000..16797e5 --- /dev/null +++ b/scrbl/opusfile-decoder.scrbl @@ -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].