166 lines
4.9 KiB
Racket
166 lines
4.9 KiB
Racket
#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. |