initial import from racket-sound -> racket-audio
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
#lang scribble/manual
|
||||
|
||||
@(require racket/base
|
||||
(for-label racket/base
|
||||
racket/contract
|
||||
racket/path
|
||||
"../ffmpeg-ffi.rkt"))
|
||||
|
||||
@title{FFmpeg FFI}
|
||||
@author{@author+email["Hans Dijkema" "hans@dijkewijk.nl"]}
|
||||
|
||||
@defmodule[(file "../ffmpeg-ffi.rkt")]
|
||||
|
||||
This module provides the low-level Racket FFI binding for the native
|
||||
FFmpeg audio shim. The native shim exposes an opaque FFmpeg instance and
|
||||
keeps all decoder state inside that instance.
|
||||
|
||||
The output format of the native shim is signed 32-bit interleaved PCM.
|
||||
The buffer returned by the native layer is copied into Racket-managed
|
||||
memory before it is passed to higher layers.
|
||||
|
||||
@defproc[(fmpg-ffi-decoder-handler) procedure?]{
|
||||
Creates a new FFmpeg decoder command handler.
|
||||
|
||||
The returned procedure manages one native FFmpeg instance. Commands are
|
||||
sent as a symbol followed by command-specific arguments.
|
||||
|
||||
@itemlist[
|
||||
#:style 'compact
|
||||
@item{@racket['new] creates the native FFmpeg instance and returns @racket[#t].}
|
||||
@item{@racket['delete] frees the native FFmpeg instance and returns @racket[#t].}
|
||||
@item{@racket['init] opens a file and fetches stream and metadata information.}
|
||||
@item{@racket['close] closes the currently opened file.}
|
||||
@item{@racket['format] calls a format callback with the current stream format.}
|
||||
@item{@racket['info] writes stream information to the sound logger.}
|
||||
@item{@racket['read] decodes the next audio block.}
|
||||
@item{@racket['seek] seeks to an absolute PCM sample position.}
|
||||
@item{@racket['tell] returns the current PCM sample position.}
|
||||
@item{@racket['file] returns the currently opened filename.}
|
||||
@item{@racket['metadata] returns a hash with file metadata.}
|
||||
]
|
||||
}
|
||||
|
||||
@section{Command Interface}
|
||||
|
||||
The command handler is used as follows:
|
||||
|
||||
@racketblock[
|
||||
(define h (fmpg-ffi-decoder-handler))
|
||||
|
||||
(h 'new)
|
||||
(h 'init filename)
|
||||
(h 'read audio-callback format-callback)
|
||||
(h 'close)
|
||||
(h 'delete)
|
||||
]
|
||||
|
||||
The @racket['new] command must be called before @racket['init]. A
|
||||
handler owns at most one native FFmpeg instance. Calling @racket['new]
|
||||
twice without @racket['delete] raises an error.
|
||||
|
||||
@section{Format Callback}
|
||||
|
||||
The @racket['format] command and the first @racket['read] call report
|
||||
the stream format by calling the supplied callback as follows:
|
||||
|
||||
@racketblock[
|
||||
(format-callback pcm-pos
|
||||
sample-rate
|
||||
channels
|
||||
bits-per-sample
|
||||
bytes-per-sample
|
||||
pcm-length)
|
||||
]
|
||||
|
||||
The @racket[pcm-pos] argument is the current PCM sample position.
|
||||
The @racket[pcm-length] argument is the total number of PCM samples, or
|
||||
@racket[-1] when this is not known.
|
||||
|
||||
@section{Reading Audio}
|
||||
|
||||
The @racket['read] command decodes one audio block. It expects an audio
|
||||
callback and a format callback:
|
||||
|
||||
@racketblock[
|
||||
(h 'read audio-callback format-callback)
|
||||
]
|
||||
|
||||
On the first read, the format callback is called before audio data is
|
||||
returned. If decoding produces data, the audio callback is called as:
|
||||
|
||||
@racketblock[
|
||||
(audio-callback 'data pcm-pos buffer size)
|
||||
]
|
||||
|
||||
The @racket[pcm-pos] argument is the absolute sample position of the
|
||||
first sample frame in the buffer. The @racket[buffer] argument points to
|
||||
a copied PCM buffer, and @racket[size] is the buffer size in bytes.
|
||||
|
||||
When the stream ends, the callback is called as:
|
||||
|
||||
@racketblock[
|
||||
(audio-callback 'done -1 #f 0)
|
||||
]
|
||||
|
||||
The command returns @racket[#t].
|
||||
|
||||
@section{Seeking}
|
||||
|
||||
The @racket['seek] command takes an absolute PCM sample position:
|
||||
|
||||
@racketblock[
|
||||
(h 'seek pcm-pos)
|
||||
]
|
||||
|
||||
The sample position is converted to milliseconds using the current
|
||||
sample rate and is then passed to the native FFmpeg shim. After seeking,
|
||||
the current PCM position is updated from the native decoder.
|
||||
|
||||
@section{Metadata}
|
||||
|
||||
The @racket['metadata] command returns a mutable hash with the following
|
||||
keys:
|
||||
|
||||
@itemlist[
|
||||
#:style 'compact
|
||||
@item{@racket['title]}
|
||||
@item{@racket['author]}
|
||||
@item{@racket['album]}
|
||||
@item{@racket['genre]}
|
||||
@item{@racket['comment]}
|
||||
@item{@racket['copyright]}
|
||||
@item{@racket['year]}
|
||||
@item{@racket['track]}
|
||||
@item{@racket['bitrate]}
|
||||
@item{@racket['duration-ms]}
|
||||
@item{@racket['audio-streams]}
|
||||
]
|
||||
|
||||
Missing string fields are returned as empty strings. Missing numeric
|
||||
fields are returned as @racket[-1].
|
||||
|
||||
@section{Native Library}
|
||||
|
||||
The module loads a shared library named @racket["ffmpeg_audio"] or
|
||||
@racket["libffmpeg_audio"] using @racket[get-lib].
|
||||
|
||||
The native layer is expected to provide an instance-only FFmpeg API.
|
||||
The relevant C-side properties are:
|
||||
|
||||
@itemlist[
|
||||
#:style 'compact
|
||||
@item{decoder state is stored in an opaque @tt{fmpg_instance};}
|
||||
@item{output is signed 32-bit interleaved PCM;}
|
||||
@item{the native buffer remains valid only until the next decode, seek,
|
||||
close or free call;}
|
||||
@item{Racket copies the buffer before passing it upward.}
|
||||
]
|
||||
|
||||
@section{Errors}
|
||||
|
||||
Native failures are reported as Racket errors. Examples include failure
|
||||
to allocate the native instance, failure to open a file and failure to
|
||||
seek to a requested sample position.
|
||||
|
||||
Unknown commands also raise an error.
|
||||
Reference in New Issue
Block a user