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 -1
View File
@@ -26,7 +26,8 @@ For encoding, also install:
The Opus encoder backend uses libopusenc directly. The FLAC encoder backend The Opus encoder backend uses libopusenc directly. The FLAC encoder backend
uses libFLAC directly. FLAC sample-rate conversion uses the existing FFmpeg uses libFLAC directly. FLAC sample-rate conversion uses the existing FFmpeg
swresample layer. swresample layer. Metadata and cover-art copying use the TagLib wrapper; the
public `taglib.rkt` API also supports read-write tag editing.
## macOS ## macOS
+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 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 are copied from the source file to the destination file. Opus comments and
cover art are written before encoding starts through @tt{libopusenc}. FLAC cover art are written before encoding starts through @tt{libopusenc}. FLAC
metadata is copied after the encoded file has been written, using the TagLib metadata is copied after the encoded file has been written, using the
wrapper. read-write API from @racketmodname[racket-audio/taglib].
When @racket[progress-callback] is a procedure, it is called with a progress 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 hash during encoding. Progress is based on the number of input frames read from
+199 -79
View File
@@ -9,48 +9,72 @@
@title{TagLib Metadata} @title{TagLib Metadata}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[racket-audio/taglib] @defmodule[racket-audio/taglib]
The @racketmodname[racket-audio/taglib] module provides the high level metadata 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 API used by the audio package. It wraps the lower level TagLib C FFI module and
and presents a small, read-only Racket API for common tags, audio properties, presents a Racket API for common tags, generic properties, audio properties, and
generic properties, and embedded cover art. embedded cover art.
Calling @racket[id3-tags] opens the file through TagLib, copies the values that The module can be used in two modes. The default mode is read-only and returns
are needed on the Racket side, reads the optional embedded picture, frees the a snapshot of the metadata. A handle opened with @racket[#:mode 'read-write]
native TagLib objects, and returns an opaque tag handle. The handle is keeps the native TagLib file open and can be modified with the setter
therefore a snapshot of the metadata at the time it was read. It does not keep procedures documented below. Changes are written to the media file by calling
the media file or the native TagLib handle open. @racket[tags-save!].
The name @racket[id3-tags] is historical. The module uses TagLib to open the The name @racket[id3-tags] is historical. The implementation uses TagLib, so
file, so the usable file types are the file types supported by the TagLib the usable file types are the file types supported by the TagLib library
library available at run time. This module is not a tag editor; it only reads available at run time.
metadata.
@section{Reading metadata} @section{Opening and closing tag handles}
@defproc[(id3-tags [file path-string?]) any/c]{ @defproc[(id3-tags [file path-string?]
Reads metadata from @racket[file] and returns an opaque tag handle. The [#:mode mode (or/c 'read 'read-only 'read-write 'write) 'read])
argument may be a path or a string. On Windows, the implementation retries any/c]{
with the wide-character TagLib open function when the normal open function does Opens @racket[file] through TagLib and returns an opaque tag handle. In the
not produce a valid TagLib file. 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 In read-write mode, the native TagLib file remains open. Setter procedures may
file cannot be opened, @racket[id3-tags] still returns a handle, but then be used to modify fields, properties, and pictures. Call
@racket[tags-valid?] returns @racket[#f]. Other accessors then return their @racket[tags-save!] to write changes and @racket[tags-close!] to close the
default values, such as @racket[""], @racket[-1], @racket['()], or native handle.
@racket[#f].}
@defproc[(tags-valid? [tags any/c]) boolean?]{ On Windows, the implementation retries with the wide-character TagLib open
Returns @racket[#t] when @racket[id3-tags] successfully opened the file and function when the normal open function does not produce a valid TagLib file.}
TagLib reported it as valid.}
@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[ @racketblock[
(define tags (id3-tags "song.mp3")) (call-with-id3-tags "track.flac"
(lambda (tags)
(when (tags-valid? tags) (when (tags-valid? tags)
(printf "~a - ~a\n" (tags-artist tags) (tags-title tags)))] (tags-title! tags "New title")
(tags-save! tags)))
#:mode 'read-write)]
@section{Common tag fields} @section{Common tag fields}
@@ -59,32 +83,52 @@ TagLib reported it as valid.}
@defproc[(tags-album [tags any/c]) string?] @defproc[(tags-album [tags any/c]) string?]
@defproc[(tags-artist [tags any/c]) string?] @defproc[(tags-artist [tags any/c]) string?]
@defproc[(tags-comment [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 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[ @deftogether[
(@defproc[(tags-year [tags any/c]) integer?] (@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 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[ @deftogether[
(@defproc[(tags-composer [tags any/c]) (@defproc[(tags-title! [tags any/c] [value (or/c string? 'clear)]) void?]
(or/c string? (listof string?))] @defproc[(tags-album! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-album-artist [tags any/c]) @defproc[(tags-artist! [tags any/c] [value (or/c string? 'clear)]) void?]
(or/c string? (listof string?))] @defproc[(tags-comment! [tags any/c] [value (or/c string? 'clear)]) void?]
@defproc[(tags-disc-number [tags any/c]) @defproc[(tags-genre! [tags any/c] [value (or/c string? 'clear)]) void?])]{
(or/c number? #f)])] Set common textual fields on a read-write handle. Passing @racket['clear]
Return selected values from the generic TagLib property store. The composer is clears the field. Call @racket[tags-save!] to persist the change.}
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 @deftogether[
and the empty string when it is missing. The disc number is parsed from the (@defproc[(tags-year! [tags any/c] [value (or/c exact-nonnegative-integer? 'clear)]) void?]
first property value and defaults to @racket[-1]. If the stored value cannot be @defproc[(tags-track! [tags any/c] [value (or/c exact-nonnegative-integer? 'clear)]) void?])]{
parsed as a number, the result may be @racket[#f]. Use @racket[tags-keys] and Set numeric common fields on a read-write handle. Passing @racket['clear]
@racket[tags-ref] for direct access to the complete generic property store. 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} @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-length [tags any/c]) integer?]
@defproc[(tags-sample-rate [tags any/c]) integer?] @defproc[(tags-sample-rate [tags any/c]) integer?]
@defproc[(tags-bit-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 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 Hz, bit rate in kbit/s, and number of channels. These values are read-only
@racket[-1]. properties of the media stream, not editable tags. Missing values are returned
as @racket[-1].}
@section{Generic properties} @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 property was not found. Use lower-case symbol keys, matching the values
returned by @racket[tags-keys].} 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[ @racketblock[
(for ([key (in-list (tags-keys tags))]) (call-with-id3-tags "track.flac"
(printf "~a: ~s\n" key (tags-ref tags key)))] (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 Generic properties may contain multiple values for a single key. The API keeps
those values as lists instead of joining them into one string. 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 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 value is returned by @racket[tags-picture] and can be inspected with
the picture procedures documented below. When no picture is available, the the picture procedures documented below. It can also be written to another
picture-related procedures return @racket[#f]. file with @racket[tags-picture!] or @racket[tags-append-picture!].
@defproc[(tags-picture [tags any/c]) (or/c any/c #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 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[ @deftogether[
(@defproc[(tags-picture->kind [tags any/c]) (or/c integer? #f)] (@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->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->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 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 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 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 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 @racket['jpg], @racket['png], or @racket[#f] when the MIME type is not
recognized. recognized.}
@defproc[(tags-picture->bitmap [tags any/c]) @defproc[(tags-picture->bitmap [tags any/c])
(or/c (is-a?/c bitmap%) #f)]{ (or/c (is-a?/c bitmap%) #f)]{
@@ -150,41 +226,85 @@ Reads the embedded picture bytes with @racket[read-bitmap] and returns a
boolean?]{ boolean?]{
Writes the embedded picture bytes to @racket[path] in binary mode, replacing an 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 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 @racket[#f] when the tag handle has no picture.}
automatically; use @racket[tags-picture->ext] when the caller wants to choose an
extension from the MIME type.} @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[ @racketblock[
(define ext (tags-picture->ext tags)) (define cover
(make-tags-picture "image/jpeg" 3 (file->bytes "cover.jpg")
#:description "Cover"))
(when ext (call-with-id3-tags "track.flac"
(tags-picture->file tags (lambda (tags)
(format "cover.~a" ext)))] (tags-picture! tags cover)
(tags-save! tags))
#:mode 'read-write)]
@section{Picture values} @section{Picture values}
@deftogether[ @deftogether[
(@defproc[(id3-picture-mimetype [picture any/c]) string?] (@defproc[(id3-picture? [v any/c]) boolean?]
@defproc[(id3-picture-kind [picture any/c]) integer?] @defproc[(id3-picture-mimetype [picture id3-picture?]) string?]
@defproc[(id3-picture-size [picture any/c]) integer?] @defproc[(id3-picture-kind [picture id3-picture?]) integer?]
@defproc[(id3-picture-bytes [picture any/c]) bytes?])] @defproc[(id3-picture-size [picture id3-picture?]) integer?]
Access the fields of a picture value returned by @racket[tags-picture]. These @defproc[(id3-picture-bytes [picture id3-picture?]) bytes?]
procedures are useful when the caller wants to process the image bytes directly @defproc[(id3-picture-description [picture id3-picture?]) string?])]{
instead of converting them to a bitmap or writing them to a file. 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} @section{Converting to a hash}
@defproc[(tags->hash [tags any/c]) hash?]{ @defproc[(tags->hash [tags any/c]) hash?]{
Returns a mutable hash containing the core values copied from the tag handle. Returns a mutable hash containing the core values copied from the tag handle.
The hash contains the keys @racket['valid?], @racket['title], @racket['album], The hash contains the keys @racket['valid?], @racket['read-write?],
@racket['artist], @racket['comment], @racket['composer], @racket['genre], @racket['closed?], @racket['title], @racket['album], @racket['artist],
@racket['year], @racket['track], @racket['length], @racket['sample-rate], @racket['comment], @racket['composer], @racket['genre], @racket['year],
@racket['bit-rate], @racket['channels], @racket['picture], and @racket['keys]. @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 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 property values are not expanded into the hash; use @racket[tags-ref] for those
values.} 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} @section{Example}
@racketblock[ @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 native TagLib calls are delegated to @racketmodname["taglib-ffi.rkt"], but
callers normally should not use that lower level module directly. callers normally should not use that lower level module directly.
The tag handle is implemented as a small Racket object with a private dispatch A read-only tag handle is a Racket-side snapshot. A read-write tag handle keeps
procedure. The native TagLib file is not stored in the handle. This keeps the the native TagLib file open until @racket[tags-close!] is called. Setter
public API simple and prevents native resources from leaking into application procedures update both the native file and the Racket-side cache; the changes
code. are persisted only after @racket[tags-save!] succeeds.
The implementation normalizes generic property names by lower-casing TagLib The implementation normalizes generic property names by lower-casing TagLib
property keys and converting them to symbols. Values remain lists of strings property keys and converting them to symbols. Values remain lists of strings