#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.