Files
gemigreerd-racket-audio/scrbl/taglib.scrbl
T
2026-06-08 13:45:54 +02:00

340 lines
14 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
API used by the audio package. It wraps the lower level TagLib C FFI module and
presents a Racket API for common tags, generic properties, audio properties, and
embedded cover art.
The module can be used in two modes. The default mode is read-only and returns
a snapshot of the metadata. A handle opened with @racket[#:mode 'read-write]
keeps the native TagLib file open and can be modified with the setter
procedures documented below. Changes are written to the media file by calling
@racket[tags-save!].
The name @racket[id3-tags] is historical. The implementation uses TagLib, so
the usable file types are the file types supported by the TagLib library
available at run time.
@section{Opening and closing tag handles}
@defproc[(id3-tags [file path-string?]
[#:mode mode (or/c 'read 'read-only 'read-write 'write) 'read])
any/c]{
Opens @racket[file] through TagLib and returns an opaque tag handle. In the
default read-only mode, the module copies the values needed on the Racket side,
frees the native TagLib objects, and returns a snapshot handle.
In read-write mode, the native TagLib file remains open. Setter procedures may
then be used to modify fields, properties, and pictures. Call
@racket[tags-save!] to write changes and @racket[tags-close!] to close the
native handle.
On Windows, the implementation retries with the wide-character TagLib open
function when the normal open function does not produce a valid TagLib file.}
@defproc[(call-with-id3-tags [file path-string?]
[proc procedure?]
[#:mode mode (or/c 'read 'read-only 'read-write 'write) 'read])
any/c]{
Opens @racket[file], calls @racket[proc] with the tag handle, and closes the
handle afterwards with @racket[tags-close!]. This is most useful for
read-write code because it avoids leaking the native TagLib file handle.}
@deftogether[
(@defproc[(tags-valid? [tags any/c]) boolean?]
@defproc[(tags-read-write? [tags any/c]) boolean?]
@defproc[(tags-closed? [tags any/c]) boolean?])]{
Return handle state. @racket[tags-valid?] reports whether TagLib opened the
file successfully. @racket[tags-read-write?] reports whether the handle was
opened in read-write mode. @racket[tags-closed?] reports whether the native
TagLib file handle has been closed.}
@deftogether[
(@defproc[(tags-save! [tags any/c]) boolean?]
@defproc[(tags-close! [tags any/c]) void?])]{
@racket[tags-save!] writes pending changes for a read-write handle to the media
file. @racket[tags-close!] closes the native TagLib file handle. Closing a
read-only snapshot is harmless.
}
@racketblock[
(call-with-id3-tags "track.flac"
(lambda (tags)
(when (tags-valid? tags)
(tags-title! tags "New title")
(tags-save! tags)))
#:mode 'read-write)]
@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-title! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-album! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-artist! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-comment! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-genre! [tags any/c] [value (or/c string? 'clear)]) void?])]{
Set common textual fields on a read-write handle. Passing @racket['clear]
clears the field. Call @racket[tags-save!] to persist the change.}
@deftogether[
(@defproc[(tags-year! [tags any/c] [value (or/c exact-nonnegative-integer? 'clear)]) void?]
@defproc[(tags-track! [tags any/c] [value (or/c exact-nonnegative-integer? 'clear)]) void?])]{
Set numeric common fields on a read-write handle. Passing @racket['clear]
writes zero through the TagLib C API and updates the Racket-side cache to
@racket[-1].}
@section{Selected generic fields}
@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 @racket['composer] key, the album artist from
@racket['albumartist], and the disc number from @racket['discnumber]. Use
@racket[tags-keys] and @racket[tags-ref] for direct access to the complete
generic property store.}
@deftogether[
(@defproc[(tags-composer! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-album-artist! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-disc-number! [tags any/c]
[value (or/c exact-nonnegative-integer? string? 'clear)])
void?])]{
Set selected generic properties on a read-write handle. The disc number may be
provided as a number or as the exact string that should be written.}
@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. These values are read-only
properties of the media stream, not editable tags. 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].}
@defproc[(tags-set! [tags any/c]
[key (or/c symbol? string?)]
[value (or/c string? 'clear)])
void?]{
Sets a generic property on a read-write handle. Symbol keys are converted to
upper-case TagLib property names; string keys are passed as supplied. Passing
@racket['clear] clears the property.}
@defproc[(tags-set-values! [tags any/c]
[key (or/c symbol? string?)]
[values (or/c (listof string?) 'clear)])
void?]{
Replaces a generic property with zero or more values. Passing @racket['clear]
removes the property.}
@defproc[(tags-append! [tags any/c]
[key (or/c symbol? string?)]
[value string?])
void?]{
Appends a value to a generic property on a read-write handle.}
@defproc[(tags-clear! [tags any/c]
[key (or/c symbol? string?)])
void?]{
Clears a generic property on a read-write handle.}
@racketblock[
(call-with-id3-tags "track.flac"
(lambda (tags)
(tags-set-values! tags 'composer '("Johann Sebastian Bach"))
(tags-set! tags 'discnumber "1")
(tags-save! tags))
#:mode 'read-write)]
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. It can also be written to another
file with @racket[tags-picture!] or @racket[tags-append-picture!].
@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->description [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.}
@defproc[(make-tags-picture [mimetype string?]
[kind integer?]
[data (or/c bytes? (is-a?/c bitmap%))]
[#:description description string? ""])
id3-picture?]{
Creates a picture value from encoded image bytes or from a @racket[bitmap%].
The MIME type should normally be @racket["image/jpeg"] or @racket["image/png"].}
@defproc[(make-tags-picture-from-bitmap [bitmap (is-a?/c bitmap%)]
[kind integer?]
[#:mimetype mimetype string? "image/png"]
[#:description description string? ""])
id3-picture?]{
Creates a picture value by encoding @racket[bitmap] as PNG or JPEG.}
@deftogether[
(@defproc[(tags-picture! [tags any/c]
[picture (or/c id3-picture? 'clear)])
void?]
@defproc[(tags-append-picture! [tags any/c]
[picture id3-picture?])
void?]
@defproc[(tags-clear-picture! [tags any/c]) void?])]{
Set, append, or clear embedded artwork on a read-write handle. The procedures
use TagLib complex properties underneath. Call @racket[tags-save!] to persist
the change.}
@racketblock[
(define cover
(make-tags-picture "image/jpeg" 3 (file->bytes "cover.jpg")
#:description "Cover"))
(call-with-id3-tags "track.flac"
(lambda (tags)
(tags-picture! tags cover)
(tags-save! tags))
#:mode 'read-write)]
@section{Picture values}
@deftogether[
(@defproc[(id3-picture? [v any/c]) boolean?]
@defproc[(id3-picture-mimetype [picture id3-picture?]) string?]
@defproc[(id3-picture-kind [picture id3-picture?]) integer?]
@defproc[(id3-picture-size [picture id3-picture?]) integer?]
@defproc[(id3-picture-bytes [picture id3-picture?]) bytes?]
@defproc[(id3-picture-description [picture id3-picture?]) string?])]{
Access the fields of a picture value. These procedures are useful when the
caller wants to process the image bytes directly or pass a picture to another
component.}
@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['read-write?],
@racket['closed?], @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{Copying tags and pictures}
The encoder pipeline uses this module for metadata transfer. For FLAC output,
@racket[audio-encode] first writes the audio stream and then opens the resulting
file with @racket[id3-tags] in read-write mode to copy tags and pictures through
TagLib. For Opus output, comments and pictures are supplied to
@tt{libopusenc} before encoding starts, because OpusTags are written at the
start of the Ogg Opus stream.
Applications that need explicit metadata editing should use the read-write API
directly, as in the examples above.
@section[#:tag "taglib-example"]{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.
A read-only tag handle is a Racket-side snapshot. A read-write tag handle keeps
the native TagLib file open until @racket[tags-close!] is called. Setter
procedures update both the native file and the Racket-side cache; the changes
are persisted only after @racket[tags-save!] succeeds.
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.