diff --git a/info.rkt b/info.rkt index bbe6fa4..09ec679 100644 --- a/info.rkt +++ b/info.rkt @@ -8,17 +8,15 @@ (define scribblings '( - ("scrbl/flac-decoder.scrbl" () (library)) ("scrbl/libao.scrbl" () (library)) - ;("scrbl/racket-sound.scrbl" () (library) "racket-sound") - ;("scrbl/liboa.scrbl" () (library) "racket-sound/liboa/libao.rkt") - ;("scrbl/flac-decoder.scrbl" () (library) "flac-decoder.rkt") - ;("scrbl/taglib.scrbl" () (library) "racket-sound/libtag/taglib.rkt") + ("scrbl/audio-decoder.scrbl" () (library)) + ("scrbl/flac-decoder.scrbl" () (library)) + ("scrbl/mp3-decoder.scrbl" () (library)) ) ) (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 diff --git a/libmpg123-ffi.rkt b/libmpg123-ffi.rkt index 14afefa..e202c0c 100644 --- a/libmpg123-ffi.rkt +++ b/libmpg123-ffi.rkt @@ -1,4 +1,4 @@ -(module libflac-ffi racket/base +(module libmpg123-ffi racket/base (require ffi/unsafe ffi/unsafe/define diff --git a/main.rkt b/main.rkt index bacb65a..40c27f4 100644 --- a/main.rkt +++ b/main.rkt @@ -1,12 +1,12 @@ #lang racket/base (require "libao.rkt" - "flac-decoder.rkt" + "audio-decoder.rkt" "taglib.rkt" ) (provide (all-from-out "libao.rkt") - (all-from-out "flac-decoder.rkt") + (all-from-out "audio-decoder.rkt") (all-from-out "taglib.rkt") ) diff --git a/mp3-decoder.rkt b/mp3-decoder.rkt index ffeb3d3..c776352 100644 --- a/mp3-decoder.rkt +++ b/mp3-decoder.rkt @@ -112,8 +112,8 @@ (unless (or (eq? total-samples #f) (= total-samples -1)) - (let ((sample (inexact->exact (round * (exact->inexact (/ percentage 100.0)) total-samples)))) - ((mp3-handle-if handle) 'seek sample)) + (let ((sample (inexact->exact (round (* (exact->inexact (/ percentage 100.0)) total-samples))))) + (set-mp3-handle-seek! handle sample)) ) ) ) diff --git a/scrbl/audio-decoder.scrbl b/scrbl/audio-decoder.scrbl index 19ec2ea..5b046ce 100644 --- a/scrbl/audio-decoder.scrbl +++ b/scrbl/audio-decoder.scrbl @@ -8,28 +8,30 @@ @title{audio-decoder} @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 -decoders. A decoder backend is selected from the filename extension -and is then used through a uniform interface for opening, reading, -seeking, and stopping. +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 also allows additional decoder backends to be registered -with @racket[audio-register-reader!]. +The module includes built-in readers for FLAC and MP3, and it allows +additional backends to be registered with +@racket[audio-register-reader!]. @section{Reader registration} -A reader descriptor stores the extensions handled by a backend -together with the procedures used to validate, open, read, seek, -and stop that backend. +A reader descriptor stores the extensions handled by a backend together +with the procedures used to validate, open, read, seek, and stop that +backend, plus an audio-output type. @defproc[(make-audio-reader [exts (listof string?)] [valid? procedure?] [open procedure?] [reader procedure?] [seeker procedure?] - [stopper procedure?]) + [stopper procedure?] + [ao-type symbol?]) struct?]{ Creates a reader descriptor. @@ -46,7 +48,10 @@ The procedures are used as follows: @item{@racket[seeker] seeks within the audio stream;} @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?] @@ -58,6 +63,8 @@ Registers @racket[reader] under @racket[type]. The extensions declared in @racket[reader] are appended to the list returned by @racket[audio-known-exts?], and the reader becomes available to @racket[audio-open]. + +This procedure is the extension point for custom audio decoders. } @section{Audio handles} @@ -72,7 +79,8 @@ otherwise. 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} @@ -81,8 +89,8 @@ For the built-in FLAC backend this value is @racket['flac]. Returns the list of known filename extensions. -The initial list contains @racket["flac"]. Additional extensions are -added when readers are registered with +The initial list contains @racket["flac"] and @racket["mp3"]. +Additional extensions are added when readers are registered with @racket[audio-register-reader!]. } @@ -91,18 +99,18 @@ added when readers are registered with Returns @racket[#t] if @racket[ext] denotes a known filename extension, and @racket[#f] otherwise. -The argument is first converted to a string. If it starts with a dot, -that dot is removed. Matching is case-insensitive. +The argument is converted to a string. If it starts with a dot, that +dot is removed. Matching is case-insensitive. } @defproc[(audio-file-valid? [file (or/c string? path?)]) boolean?]{ -Returns @racket[#t] if @racket[file] has a known extension and matches -a registered reader, and @racket[#f] otherwise. +Returns @racket[#t] if @racket[file] has a known extension and the +matching registered reader reports the file as valid. This procedure first derives the filename extension and checks it with @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} @@ -114,14 +122,14 @@ the matching reader and performs the reader-specific validity check. Opens an audio decoder for @racket[audio-file]. -If @racket[audio-file] is a path, it is converted to a string before -it is passed to the backend open procedure. +If @racket[audio-file] is a path, it is converted to a string before it +is passed to the backend open procedure. 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 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 returned by the backend open procedure. @@ -131,14 +139,16 @@ backend. The stream-info callback is called as: @racketblock[ -(cb-stream-info audio-type handle meta) +(cb-stream-info audio-type ao-type handle meta) ] where: @itemlist[#:style 'compact @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[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: @racketblock[ -(cb-audio audio-type handle buf-info buffer buf-size) +(cb-audio audio-type ao-type handle buf-info buffer buf-size) ] where: @itemlist[#:style 'compact @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[buf-info] is a hash table describing the audio buffer;} @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 seek procedure that accepts the driver-specific handle and a 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 -@racket[audio-open] in the same way as the built-in FLAC backend. \ No newline at end of file +@racket[audio-open] in the same way as the built-in FLAC and MP3 +backends. \ No newline at end of file diff --git a/scrbl/libao.scrbl b/scrbl/libao.scrbl index 06ea729..0879fc6 100644 --- a/scrbl/libao.scrbl +++ b/scrbl/libao.scrbl @@ -7,7 +7,7 @@ @title{libao} @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 audio output backend. It opens a live output device, queues audio diff --git a/scrbl/mp3-decoder.scrbl b/scrbl/mp3-decoder.scrbl new file mode 100644 index 0000000..73b0efc --- /dev/null +++ b/scrbl/mp3-decoder.scrbl @@ -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]. \ No newline at end of file