Files
gemigreerd-racket-audio/scrbl/taglib.scrbl
T
2026-05-16 01:38:40 +02:00

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.