220 lines
8.9 KiB
Racket
220 lines
8.9 KiB
Racket
#lang scribble/manual
|
|
|
|
@(require (for-label racket/base
|
|
racket/contract
|
|
racket/path
|
|
racket/draw
|
|
"../taglib.rkt"))
|
|
|
|
@title{TagLib Metadata}
|
|
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
|
|
|
|
|
|
@defmodule[racket-audio/taglib]
|
|
|
|
The @racketmodname[racket-audio/taglib] module provides the high level metadata
|
|
reader used by the audio package. It wraps the lower level TagLib FFI module
|
|
and presents a small, read-only Racket API for common tags, audio properties,
|
|
generic properties, and embedded cover art.
|
|
|
|
Calling @racket[id3-tags] opens the file through TagLib, copies the values that
|
|
are needed on the Racket side, reads the optional embedded picture, frees the
|
|
native TagLib objects, and returns an opaque tag handle. The handle is
|
|
therefore a snapshot of the metadata at the time it was read. It does not keep
|
|
the media file or the native TagLib handle open.
|
|
|
|
The name @racket[id3-tags] is historical. The module uses TagLib to open the
|
|
file, so the usable file types are the file types supported by the TagLib
|
|
library available at run time. This module is not a tag editor; it only reads
|
|
metadata.
|
|
|
|
@section{Reading metadata}
|
|
|
|
@defproc[(id3-tags [file path-string?]) any/c]{
|
|
Reads metadata from @racket[file] and returns an opaque tag handle. The
|
|
argument may be a path or a string. On Windows, the implementation retries
|
|
with the wide-character TagLib open function when the normal open function does
|
|
not produce a valid TagLib file.
|
|
|
|
The returned handle is passed to the other procedures in this module. If the
|
|
file cannot be opened, @racket[id3-tags] still returns a handle, but
|
|
@racket[tags-valid?] returns @racket[#f]. Other accessors then return their
|
|
default values, such as @racket[""], @racket[-1], @racket['()], or
|
|
@racket[#f].}
|
|
|
|
@defproc[(tags-valid? [tags any/c]) boolean?]{
|
|
Returns @racket[#t] when @racket[id3-tags] successfully opened the file and
|
|
TagLib reported it as valid.}
|
|
|
|
@racketblock[
|
|
(define tags (id3-tags "song.mp3"))
|
|
|
|
(when (tags-valid? tags)
|
|
(printf "~a - ~a\n" (tags-artist tags) (tags-title tags)))]
|
|
|
|
@section{Common tag fields}
|
|
|
|
@deftogether[
|
|
(@defproc[(tags-title [tags any/c]) string?]
|
|
@defproc[(tags-album [tags any/c]) string?]
|
|
@defproc[(tags-artist [tags any/c]) string?]
|
|
@defproc[(tags-comment [tags any/c]) string?]
|
|
@defproc[(tags-genre [tags any/c]) string?])]
|
|
Return the common textual fields from the TagLib tag interface. Missing fields
|
|
are returned as the empty string.
|
|
|
|
@deftogether[
|
|
(@defproc[(tags-year [tags any/c]) integer?]
|
|
@defproc[(tags-track [tags any/c]) integer?])]
|
|
Return the year and track number from the common TagLib tag interface. Missing
|
|
numeric values are returned as @racket[-1].
|
|
|
|
@deftogether[
|
|
(@defproc[(tags-composer [tags any/c])
|
|
(or/c string? (listof string?))]
|
|
@defproc[(tags-album-artist [tags any/c])
|
|
(or/c string? (listof string?))]
|
|
@defproc[(tags-disc-number [tags any/c])
|
|
(or/c number? #f)])]
|
|
Return selected values from the generic TagLib property store. The composer is
|
|
read from the lower-case @racket['composer] key, the album artist from
|
|
@racket['albumartist], and the disc number from @racket['discnumber].
|
|
|
|
Composer and album artist return a list of strings when the property is present
|
|
and the empty string when it is missing. The disc number is parsed from the
|
|
first property value and defaults to @racket[-1]. If the stored value cannot be
|
|
parsed as a number, the result may be @racket[#f]. Use @racket[tags-keys] and
|
|
@racket[tags-ref] for direct access to the complete generic property store.
|
|
|
|
@section{Audio properties}
|
|
|
|
@deftogether[
|
|
(@defproc[(tags-length [tags any/c]) integer?]
|
|
@defproc[(tags-sample-rate [tags any/c]) integer?]
|
|
@defproc[(tags-bit-rate [tags any/c]) integer?]
|
|
@defproc[(tags-channels [tags any/c]) integer?])]
|
|
Return audio properties reported by TagLib: length in seconds, sample rate in
|
|
Hz, bit rate in kbit/s, and number of channels. Missing values are returned as
|
|
@racket[-1].
|
|
|
|
@section{Generic properties}
|
|
|
|
@defproc[(tags-keys [tags any/c]) (listof symbol?)]{
|
|
Returns the generic TagLib property keys found in the file. Keys are
|
|
lower-cased and converted to symbols.}
|
|
|
|
@defproc[(tags-ref [tags any/c] [key symbol?])
|
|
(or/c (listof string?) #f)]{
|
|
Returns the list of values associated with @racket[key], or @racket[#f] when the
|
|
property was not found. Use lower-case symbol keys, matching the values
|
|
returned by @racket[tags-keys].}
|
|
|
|
@racketblock[
|
|
(for ([key (in-list (tags-keys tags))])
|
|
(printf "~a: ~s\n" key (tags-ref tags key)))]
|
|
|
|
Generic properties may contain multiple values for a single key. The API keeps
|
|
those values as lists instead of joining them into one string.
|
|
|
|
@section{Embedded pictures}
|
|
|
|
The module represents embedded artwork as an opaque @deftech{picture value}.
|
|
The picture value is returned by @racket[tags-picture] and can be inspected with
|
|
the picture procedures documented below. When no picture is available, the
|
|
picture-related procedures return @racket[#f].
|
|
|
|
@defproc[(tags-picture [tags any/c]) (or/c any/c #f)]{
|
|
Returns the embedded picture value, or @racket[#f] when the file has no picture
|
|
that the underlying FFI layer could read.}
|
|
|
|
@deftogether[
|
|
(@defproc[(tags-picture->kind [tags any/c]) (or/c integer? #f)]
|
|
@defproc[(tags-picture->mimetype [tags any/c]) (or/c string? #f)]
|
|
@defproc[(tags-picture->size [tags any/c]) (or/c integer? #f)]
|
|
@defproc[(tags-picture->ext [tags any/c]) (or/c symbol? #f)])]
|
|
Return selected information about the embedded picture. The kind is the
|
|
numeric picture type reported by the FFI layer. The MIME type is the stored
|
|
MIME type, such as @racket["image/jpeg"] or @racket["image/png"]. The size is
|
|
the number of bytes in the embedded image. The extension helper returns
|
|
@racket['jpg], @racket['png], or @racket[#f] when the MIME type is not
|
|
recognized.
|
|
|
|
@defproc[(tags-picture->bitmap [tags any/c])
|
|
(or/c (is-a?/c bitmap%) #f)]{
|
|
Reads the embedded picture bytes with @racket[read-bitmap] and returns a
|
|
@racket[bitmap%] object. If there is no embedded picture, the result is
|
|
@racket[#f].}
|
|
|
|
@defproc[(tags-picture->file [tags any/c]
|
|
[path path-string?])
|
|
boolean?]{
|
|
Writes the embedded picture bytes to @racket[path] in binary mode, replacing an
|
|
existing file. The procedure returns @racket[#t] when a picture was written and
|
|
@racket[#f] when the tag handle has no picture. The file name is not adjusted
|
|
automatically; use @racket[tags-picture->ext] when the caller wants to choose an
|
|
extension from the MIME type.}
|
|
|
|
@racketblock[
|
|
(define ext (tags-picture->ext tags))
|
|
|
|
(when ext
|
|
(tags-picture->file tags
|
|
(format "cover.~a" ext)))]
|
|
|
|
@section{Picture values}
|
|
|
|
@deftogether[
|
|
(@defproc[(id3-picture-mimetype [picture any/c]) string?]
|
|
@defproc[(id3-picture-kind [picture any/c]) integer?]
|
|
@defproc[(id3-picture-size [picture any/c]) integer?]
|
|
@defproc[(id3-picture-bytes [picture any/c]) bytes?])]
|
|
Access the fields of a picture value returned by @racket[tags-picture]. These
|
|
procedures are useful when the caller wants to process the image bytes directly
|
|
instead of converting them to a bitmap or writing them to a file.
|
|
|
|
@section{Converting to a hash}
|
|
|
|
@defproc[(tags->hash [tags any/c]) hash?]{
|
|
Returns a mutable hash containing the core values copied from the tag handle.
|
|
The hash contains the keys @racket['valid?], @racket['title], @racket['album],
|
|
@racket['artist], @racket['comment], @racket['composer], @racket['genre],
|
|
@racket['year], @racket['track], @racket['length], @racket['sample-rate],
|
|
@racket['bit-rate], @racket['channels], @racket['picture], and @racket['keys].
|
|
|
|
The hash is intended as a convenient snapshot for application code. Generic
|
|
property values are not expanded into the hash; use @racket[tags-ref] for those
|
|
values.}
|
|
|
|
@section{Example}
|
|
|
|
@racketblock[
|
|
(define tags (id3-tags "track.flac"))
|
|
|
|
(cond
|
|
[(not (tags-valid? tags))
|
|
(printf "No readable tags\n")]
|
|
[else
|
|
(printf "Title: ~a\n" (tags-title tags))
|
|
(printf "Artist: ~a\n" (tags-artist tags))
|
|
(printf "Album: ~a\n" (tags-album tags))
|
|
(printf "Length: ~a seconds\n" (tags-length tags))
|
|
|
|
(when (tags-picture tags)
|
|
(define ext (or (tags-picture->ext tags) 'bin))
|
|
(tags-picture->file tags (format "cover.~a" ext)))])]
|
|
|
|
@section{Implementation notes}
|
|
|
|
This chapter documents the public @racketmodname["taglib.rkt"] layer. The
|
|
native TagLib calls are delegated to @racketmodname["taglib-ffi.rkt"], but
|
|
callers normally should not use that lower level module directly.
|
|
|
|
The tag handle is implemented as a small Racket object with a private dispatch
|
|
procedure. The native TagLib file is not stored in the handle. This keeps the
|
|
public API simple and prevents native resources from leaking into application
|
|
code.
|
|
|
|
The implementation normalizes generic property names by lower-casing TagLib
|
|
property keys and converting them to symbols. Values remain lists of strings
|
|
because TagLib properties may contain multiple values for one key.
|