taglib documentation.

This commit is contained in:
2026-06-08 13:26:10 +02:00
parent 5eefacacba
commit b979be540e
3 changed files with 204 additions and 83 deletions
+2 -2
View File
@@ -62,8 +62,8 @@ file extension. Pass @racket['opus] or @racket['flac] to force a backend.
When @racket[copy-tags?] is true, common textual tags and an embedded picture
are copied from the source file to the destination file. Opus comments and
cover art are written before encoding starts through @tt{libopusenc}. FLAC
metadata is copied after the encoded file has been written, using the TagLib
wrapper.
metadata is copied after the encoded file has been written, using the
read-write API from @racketmodname[racket-audio/taglib].
When @racket[progress-callback] is a procedure, it is called with a progress
hash during encoding. Progress is based on the number of input frames read from
+200 -80
View File
@@ -9,48 +9,72 @@
@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.
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.
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 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 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.
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{Reading metadata}
@section{Opening and closing tag handles}
@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.
@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.
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].}
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.
@defproc[(tags-valid? [tags any/c]) boolean?]{
Returns @racket[#t] when @racket[id3-tags] successfully opened the file and
TagLib reported it as valid.}
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[
(define tags (id3-tags "song.mp3"))
(when (tags-valid? tags)
(printf "~a - ~a\n" (tags-artist tags) (tags-title tags)))]
(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}
@@ -59,32 +83,52 @@ TagLib reported it as valid.}
@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?])]
@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.
are returned as the empty string.}
@deftogether[
(@defproc[(tags-year [tags any/c]) integer?]
@defproc[(tags-track [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].
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].
(@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.}
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.
@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}
@@ -92,10 +136,11 @@ parsed as a number, the result may be @racket[#f]. Use @racket[tags-keys] and
(@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?])]
@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].
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}
@@ -109,9 +154,39 @@ 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[
(for ([key (in-list (tags-keys tags))])
(printf "~a: ~s\n" key (tags-ref tags key)))]
(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.
@@ -120,8 +195,8 @@ those values as lists instead of joining them into one string.
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].
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
@@ -130,14 +205,15 @@ 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)])]
@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.
recognized.}
@defproc[(tags-picture->bitmap [tags any/c])
(or/c (is-a?/c bitmap%) #f)]{
@@ -150,41 +226,85 @@ Reads the embedded picture bytes with @racket[read-bitmap] and returns a
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.}
@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 ext (tags-picture->ext tags))
(define cover
(make-tags-picture "image/jpeg" 3 (file->bytes "cover.jpg")
#:description "Cover"))
(when ext
(tags-picture->file tags
(format "cover.~a" ext)))]
(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-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.
(@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['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 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{Example}
@racketblock[
@@ -209,10 +329,10 @@ 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.
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