mp3 decoder documented
This commit is contained in:
@@ -8,17 +8,15 @@
|
|||||||
|
|
||||||
(define scribblings
|
(define scribblings
|
||||||
'(
|
'(
|
||||||
("scrbl/flac-decoder.scrbl" () (library))
|
|
||||||
("scrbl/libao.scrbl" () (library))
|
("scrbl/libao.scrbl" () (library))
|
||||||
;("scrbl/racket-sound.scrbl" () (library) "racket-sound")
|
("scrbl/audio-decoder.scrbl" () (library))
|
||||||
;("scrbl/liboa.scrbl" () (library) "racket-sound/liboa/libao.rkt")
|
("scrbl/flac-decoder.scrbl" () (library))
|
||||||
;("scrbl/flac-decoder.scrbl" () (library) "flac-decoder.rkt")
|
("scrbl/mp3-decoder.scrbl" () (library))
|
||||||
;("scrbl/taglib.scrbl" () (library) "racket-sound/libtag/taglib.rkt")
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
(define deps
|
(define deps
|
||||||
'("racket/gui" "racket/base" "racket" "finalizer" "draw-lib" "net-lib" "simple-log")
|
'("racket/gui" "racket/base" "racket" "finalizer" "draw-lib" "net-lib" "simple-log" "racket-sprintf")
|
||||||
)
|
)
|
||||||
|
|
||||||
(define build-deps
|
(define build-deps
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
(module libflac-ffi racket/base
|
(module libmpg123-ffi racket/base
|
||||||
|
|
||||||
(require ffi/unsafe
|
(require ffi/unsafe
|
||||||
ffi/unsafe/define
|
ffi/unsafe/define
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
#lang racket/base
|
#lang racket/base
|
||||||
|
|
||||||
(require "libao.rkt"
|
(require "libao.rkt"
|
||||||
"flac-decoder.rkt"
|
"audio-decoder.rkt"
|
||||||
"taglib.rkt"
|
"taglib.rkt"
|
||||||
)
|
)
|
||||||
|
|
||||||
(provide (all-from-out "libao.rkt")
|
(provide (all-from-out "libao.rkt")
|
||||||
(all-from-out "flac-decoder.rkt")
|
(all-from-out "audio-decoder.rkt")
|
||||||
(all-from-out "taglib.rkt")
|
(all-from-out "taglib.rkt")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -112,8 +112,8 @@
|
|||||||
(unless (or
|
(unless (or
|
||||||
(eq? total-samples #f)
|
(eq? total-samples #f)
|
||||||
(= total-samples -1))
|
(= total-samples -1))
|
||||||
(let ((sample (inexact->exact (round * (exact->inexact (/ percentage 100.0)) total-samples))))
|
(let ((sample (inexact->exact (round (* (exact->inexact (/ percentage 100.0)) total-samples)))))
|
||||||
((mp3-handle-if handle) 'seek sample))
|
(set-mp3-handle-seek! handle sample))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
+41
-27
@@ -8,28 +8,30 @@
|
|||||||
@title{audio-decoder}
|
@title{audio-decoder}
|
||||||
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||||
|
|
||||||
@defmodule["audio-decoder.rkt"]
|
@defmodule["racket-sound/audio-decoder"]
|
||||||
|
|
||||||
This module provides a small abstraction layer over concrete audio
|
This module provides a small abstraction layer over concrete audio
|
||||||
decoders. A decoder backend is selected from the filename extension
|
decoders. A backend is selected from the filename extension and is then
|
||||||
and is then used through a uniform interface for opening, reading,
|
used through a uniform interface for opening, reading, seeking, and
|
||||||
seeking, and stopping.
|
stopping.
|
||||||
|
|
||||||
The module also allows additional decoder backends to be registered
|
The module includes built-in readers for FLAC and MP3, and it allows
|
||||||
with @racket[audio-register-reader!].
|
additional backends to be registered with
|
||||||
|
@racket[audio-register-reader!].
|
||||||
|
|
||||||
@section{Reader registration}
|
@section{Reader registration}
|
||||||
|
|
||||||
A reader descriptor stores the extensions handled by a backend
|
A reader descriptor stores the extensions handled by a backend together
|
||||||
together with the procedures used to validate, open, read, seek,
|
with the procedures used to validate, open, read, seek, and stop that
|
||||||
and stop that backend.
|
backend, plus an audio-output type.
|
||||||
|
|
||||||
@defproc[(make-audio-reader [exts (listof string?)]
|
@defproc[(make-audio-reader [exts (listof string?)]
|
||||||
[valid? procedure?]
|
[valid? procedure?]
|
||||||
[open procedure?]
|
[open procedure?]
|
||||||
[reader procedure?]
|
[reader procedure?]
|
||||||
[seeker procedure?]
|
[seeker procedure?]
|
||||||
[stopper procedure?])
|
[stopper procedure?]
|
||||||
|
[ao-type symbol?])
|
||||||
struct?]{
|
struct?]{
|
||||||
|
|
||||||
Creates a reader descriptor.
|
Creates a reader descriptor.
|
||||||
@@ -46,7 +48,10 @@ The procedures are used as follows:
|
|||||||
@item{@racket[seeker] seeks within the audio stream;}
|
@item{@racket[seeker] seeks within the audio stream;}
|
||||||
@item{@racket[stopper] stops an active decode loop.}]
|
@item{@racket[stopper] stops an active decode loop.}]
|
||||||
|
|
||||||
The built-in FLAC backend is registered in this way.
|
The @racket[ao-type] value describes the buffer format exposed to the
|
||||||
|
audio output layer. The source comments mention values such as
|
||||||
|
@racket['flac] and @racket['ao]. The value @racket['ao] means that the
|
||||||
|
buffer can be used directly by the audio-output backend.
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(audio-register-reader! [type symbol?]
|
@defproc[(audio-register-reader! [type symbol?]
|
||||||
@@ -58,6 +63,8 @@ Registers @racket[reader] under @racket[type].
|
|||||||
The extensions declared in @racket[reader] are appended to the list
|
The extensions declared in @racket[reader] are appended to the list
|
||||||
returned by @racket[audio-known-exts?], and the reader becomes
|
returned by @racket[audio-known-exts?], and the reader becomes
|
||||||
available to @racket[audio-open].
|
available to @racket[audio-open].
|
||||||
|
|
||||||
|
This procedure is the extension point for custom audio decoders.
|
||||||
}
|
}
|
||||||
|
|
||||||
@section{Audio handles}
|
@section{Audio handles}
|
||||||
@@ -72,7 +79,8 @@ otherwise.
|
|||||||
|
|
||||||
Returns the reader type stored in @racket[handle].
|
Returns the reader type stored in @racket[handle].
|
||||||
|
|
||||||
For the built-in FLAC backend this value is @racket['flac].
|
For the built-in readers this is either @racket['flac] or
|
||||||
|
@racket['mp3].
|
||||||
}
|
}
|
||||||
|
|
||||||
@section{Known extensions and validation}
|
@section{Known extensions and validation}
|
||||||
@@ -81,8 +89,8 @@ For the built-in FLAC backend this value is @racket['flac].
|
|||||||
|
|
||||||
Returns the list of known filename extensions.
|
Returns the list of known filename extensions.
|
||||||
|
|
||||||
The initial list contains @racket["flac"]. Additional extensions are
|
The initial list contains @racket["flac"] and @racket["mp3"].
|
||||||
added when readers are registered with
|
Additional extensions are added when readers are registered with
|
||||||
@racket[audio-register-reader!].
|
@racket[audio-register-reader!].
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,18 +99,18 @@ added when readers are registered with
|
|||||||
Returns @racket[#t] if @racket[ext] denotes a known filename
|
Returns @racket[#t] if @racket[ext] denotes a known filename
|
||||||
extension, and @racket[#f] otherwise.
|
extension, and @racket[#f] otherwise.
|
||||||
|
|
||||||
The argument is first converted to a string. If it starts with a dot,
|
The argument is converted to a string. If it starts with a dot, that
|
||||||
that dot is removed. Matching is case-insensitive.
|
dot is removed. Matching is case-insensitive.
|
||||||
}
|
}
|
||||||
|
|
||||||
@defproc[(audio-file-valid? [file (or/c string? path?)]) boolean?]{
|
@defproc[(audio-file-valid? [file (or/c string? path?)]) boolean?]{
|
||||||
|
|
||||||
Returns @racket[#t] if @racket[file] has a known extension and matches
|
Returns @racket[#t] if @racket[file] has a known extension and the
|
||||||
a registered reader, and @racket[#f] otherwise.
|
matching registered reader reports the file as valid.
|
||||||
|
|
||||||
This procedure first derives the filename extension and checks it with
|
This procedure first derives the filename extension and checks it with
|
||||||
@racket[audio-valid-ext?]. If the extension is known, it then looks up
|
@racket[audio-valid-ext?]. If the extension is known, it then looks up
|
||||||
the matching reader and performs the reader-specific validity check.
|
the matching reader and calls that reader's validity procedure.
|
||||||
}
|
}
|
||||||
|
|
||||||
@section{Opening and callbacks}
|
@section{Opening and callbacks}
|
||||||
@@ -114,14 +122,14 @@ the matching reader and performs the reader-specific validity check.
|
|||||||
|
|
||||||
Opens an audio decoder for @racket[audio-file].
|
Opens an audio decoder for @racket[audio-file].
|
||||||
|
|
||||||
If @racket[audio-file] is a path, it is converted to a string before
|
If @racket[audio-file] is a path, it is converted to a string before it
|
||||||
it is passed to the backend open procedure.
|
is passed to the backend open procedure.
|
||||||
|
|
||||||
This procedure raises an exception if the file is not considered a
|
This procedure raises an exception if the file is not considered a
|
||||||
valid audio file, if the file does not exist, or if no registered
|
valid audio file, if the file does not exist, or if no registered
|
||||||
reader can be found for the file.
|
reader can be found for the file.
|
||||||
|
|
||||||
The returned handle stores the selected reader type, the callback
|
The returned handle stores the selected reader type, the two callback
|
||||||
procedures, the reader descriptor, and the driver-specific handle
|
procedures, the reader descriptor, and the driver-specific handle
|
||||||
returned by the backend open procedure.
|
returned by the backend open procedure.
|
||||||
|
|
||||||
@@ -131,14 +139,16 @@ backend.
|
|||||||
The stream-info callback is called as:
|
The stream-info callback is called as:
|
||||||
|
|
||||||
@racketblock[
|
@racketblock[
|
||||||
(cb-stream-info audio-type handle meta)
|
(cb-stream-info audio-type ao-type handle meta)
|
||||||
]
|
]
|
||||||
|
|
||||||
where:
|
where:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@itemlist[#:style 'compact
|
||||||
@item{@racket[audio-type] is the registered reader type, such as
|
@item{@racket[audio-type] is the registered reader type, such as
|
||||||
@racket['flac];}
|
@racket['flac] or @racket['mp3];}
|
||||||
|
@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];}
|
@item{@racket[handle] is the generic @racket[audio-handle];}
|
||||||
@item{@racket[meta] is a hash table with stream metadata.}]
|
@item{@racket[meta] is a hash table with stream metadata.}]
|
||||||
|
|
||||||
@@ -157,13 +167,14 @@ According to the source comments, @racket[meta] must contain at least:
|
|||||||
The audio callback is called as:
|
The audio callback is called as:
|
||||||
|
|
||||||
@racketblock[
|
@racketblock[
|
||||||
(cb-audio audio-type handle buf-info buffer buf-size)
|
(cb-audio audio-type ao-type handle buf-info buffer buf-size)
|
||||||
]
|
]
|
||||||
|
|
||||||
where:
|
where:
|
||||||
|
|
||||||
@itemlist[#:style 'compact
|
@itemlist[#:style 'compact
|
||||||
@item{@racket[audio-type] is the registered reader type;}
|
@item{@racket[audio-type] is the registered reader type;}
|
||||||
|
@item{@racket[ao-type] is the audio-output type stored in the reader;}
|
||||||
@item{@racket[handle] is the generic @racket[audio-handle];}
|
@item{@racket[handle] is the generic @racket[audio-handle];}
|
||||||
@item{@racket[buf-info] is a hash table describing the audio buffer;}
|
@item{@racket[buf-info] is a hash table describing the audio buffer;}
|
||||||
@item{@racket[buffer] is a native buffer containing audio data;}
|
@item{@racket[buffer] is a native buffer containing audio data;}
|
||||||
@@ -239,7 +250,10 @@ A backend integrated through this interface should provide:
|
|||||||
@item{a read procedure that accepts the driver-specific handle;}
|
@item{a read procedure that accepts the driver-specific handle;}
|
||||||
@item{a seek procedure that accepts the driver-specific handle and a
|
@item{a seek procedure that accepts the driver-specific handle and a
|
||||||
numeric relative position;}
|
numeric relative position;}
|
||||||
@item{a stop procedure that accepts the driver-specific handle.}]
|
@item{a stop procedure that accepts the driver-specific handle;}
|
||||||
|
@item{an audio-output type symbol describing the kind of buffers the
|
||||||
|
backend produces.}]
|
||||||
|
|
||||||
Once registered, files with matching extensions can be opened through
|
Once registered, files with matching extensions can be opened through
|
||||||
@racket[audio-open] in the same way as the built-in FLAC backend.
|
@racket[audio-open] in the same way as the built-in FLAC and MP3
|
||||||
|
backends.
|
||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
@title{libao}
|
@title{libao}
|
||||||
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||||
|
|
||||||
@defmodule["libao.rkt"]
|
@defmodule["racket-sound/libao"]
|
||||||
|
|
||||||
This module provides a small high-level interface to an asynchronous
|
This module provides a small high-level interface to an asynchronous
|
||||||
audio output backend. It opens a live output device, queues audio
|
audio output backend. It opens a live output device, queues audio
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
#lang scribble/manual
|
||||||
|
|
||||||
|
@(require racket/base
|
||||||
|
(for-label racket/base
|
||||||
|
racket/path
|
||||||
|
"../mp3-decoder.rkt"))
|
||||||
|
|
||||||
|
@title{mp3-decoder}
|
||||||
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
||||||
|
|
||||||
|
@defmodule[racket-sound/mp3-decoder]
|
||||||
|
|
||||||
|
This module provides an MP3 decoder implementation for
|
||||||
|
@racketmodname[racket-sound/audio-decoder]. It opens an MP3 file,
|
||||||
|
reports stream information through a callback, streams decoded PCM
|
||||||
|
buffers, and supports stopping and seeking.
|
||||||
|
|
||||||
|
Compared to @racketmodname[racket-sound/flac-decoder], this module
|
||||||
|
stores its stream information directly in a mutable hash and updates
|
||||||
|
that hash in place during decoding.
|
||||||
|
|
||||||
|
@section{Handles}
|
||||||
|
|
||||||
|
@defproc[(mp3-handle? [v any/c]) boolean?]{
|
||||||
|
|
||||||
|
Returns @racket[#t] if @racket[v] is an MP3 decoder handle, and
|
||||||
|
@racket[#f] otherwise.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Validation}
|
||||||
|
|
||||||
|
@defproc[(mp3-valid? [file any/c]) boolean?]{
|
||||||
|
|
||||||
|
Returns @racket[#t].
|
||||||
|
|
||||||
|
This procedure does not inspect @racket[file]. It exists to satisfy the
|
||||||
|
reader interface expected by
|
||||||
|
@racketmodname[racket-sound/audio-decoder].
|
||||||
|
|
||||||
|
In practice, basic validation such as file existence and extension
|
||||||
|
checking is already performed by
|
||||||
|
@racketmodname[racket-sound/audio-decoder]. This procedure therefore
|
||||||
|
acts only as an additional hook and currently accepts all inputs.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Opening}
|
||||||
|
|
||||||
|
@defproc[(mp3-open [mp3-file* (or/c path? string?)]
|
||||||
|
[cb-stream-info procedure?]
|
||||||
|
[cb-audio procedure?])
|
||||||
|
(or/c mp3-handle? #f)]{
|
||||||
|
|
||||||
|
Opens an MP3 decoder for @racket[mp3-file*].
|
||||||
|
|
||||||
|
If @racket[mp3-file*] is a path, it is converted with
|
||||||
|
@racket[path->string]. If the file does not exist, the result is
|
||||||
|
@racket[#f].
|
||||||
|
|
||||||
|
Otherwise a decoder handle is created and initialized. During
|
||||||
|
initialization, stream information is collected and stored in a
|
||||||
|
mutable hash in the handle.
|
||||||
|
|
||||||
|
The stream-info callback is invoked once, immediately after
|
||||||
|
initialization:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(cb-stream-info info)
|
||||||
|
]
|
||||||
|
|
||||||
|
where @racket[info] is a mutable hash containing at least:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket['duration] --- stream duration in seconds;}
|
||||||
|
@item{@racket['sample-rate] --- samples per second per channel;}
|
||||||
|
@item{@racket['channels] --- number of channels;}
|
||||||
|
@item{@racket['bits-per-sample] --- bits per sample;}
|
||||||
|
@item{@racket['bytes-per-sample] --- bytes per sample;}
|
||||||
|
@item{@racket['total-samples] --- total PCM sample count.}]
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Reading}
|
||||||
|
|
||||||
|
@defproc[(mp3-read [handle mp3-handle?]) any/c]{
|
||||||
|
|
||||||
|
Starts the decode loop for @racket[handle].
|
||||||
|
|
||||||
|
The loop repeatedly decodes audio chunks and invokes the audio
|
||||||
|
callback:
|
||||||
|
|
||||||
|
@racketblock[
|
||||||
|
(cb-audio info buffer size)
|
||||||
|
]
|
||||||
|
|
||||||
|
Before each callback, the @racket[info] hash is updated in place with:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{@racket['sample] --- current PCM sample position;}
|
||||||
|
@item{@racket['current-time] --- current playback time in seconds.}]
|
||||||
|
|
||||||
|
The loop terminates when either:
|
||||||
|
|
||||||
|
@itemlist[#:style 'compact
|
||||||
|
@item{the backend reports end-of-stream, or}
|
||||||
|
@item{a stop has been requested via @racket[mp3-stop].}]
|
||||||
|
|
||||||
|
If a stop is detected, the procedure returns
|
||||||
|
@racket['stopped-reading].
|
||||||
|
|
||||||
|
After termination, the underlying decoder is closed and released.
|
||||||
|
|
||||||
|
The return value is otherwise unspecified.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Seeking}
|
||||||
|
|
||||||
|
@defproc[(mp3-seek [handle mp3-handle?]
|
||||||
|
[percentage number?])
|
||||||
|
void?]{
|
||||||
|
|
||||||
|
Seeks within the stream by percentage of the total length.
|
||||||
|
|
||||||
|
The @racket[percentage] argument is interpreted as a value between
|
||||||
|
@racket[0] and @racket[100]. The target position is computed from the
|
||||||
|
value stored under @racket['total-samples] in the stream-info hash.
|
||||||
|
|
||||||
|
If the total sample count is unavailable, this procedure has no effect.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Stopping}
|
||||||
|
|
||||||
|
@defproc[(mp3-stop [handle mp3-handle?]) void?]{
|
||||||
|
|
||||||
|
Requests termination of an active @racket[mp3-read] loop.
|
||||||
|
|
||||||
|
The procedure sets an internal stop flag and waits until the read loop
|
||||||
|
has terminated, sleeping briefly between checks.
|
||||||
|
}
|
||||||
|
|
||||||
|
@section{Notes}
|
||||||
|
|
||||||
|
The stream-info hash is shared between initialization and decoding and
|
||||||
|
is updated in place during playback.
|
||||||
|
|
||||||
|
The audio buffer passed to the callback is managed by the decoder and
|
||||||
|
should be treated as transient data.
|
||||||
|
|
||||||
|
Although the handle contains a field related to deferred seeking, the
|
||||||
|
current implementation performs seeking directly in
|
||||||
|
@racket[mp3-seek].
|
||||||
Reference in New Issue
Block a user