(:writer (out string)
(multiple-value-bind (type keyword arg)
(string-args encoding length terminator)
(write-value type out string keyword arg))))
Now you can define a text-info
mixin class, much the way you defined generic- frame
earlier.
(define-binary-class text-info-frame ()
((encoding u1)
(information (id3-encoded-string :encoding encoding :length (bytes-left 1)))))
As when you defined generic-frame
, you need access to the size of the frame, in this case to compute the :length
argument to pass to id3-encoded-string
. Because you'll need to do a similar computation in the next class you define, you can go ahead and define a helper function, bytes-left
, that uses current-binary-object
to get at the size of the frame.
(defun bytes-left (bytes-read)
(- (size (current-binary-object)) bytes-read))
Now, as you did with the generic-frame
mixin, you can define two version-specific concrete classes with a minimum of duplicated code.
(define-binary-class text-info-frame-v2.2 (id3v2.2-frame text-info-frame) ())
(define-binary-class text-info-frame-v2.3 (id3v2.3-frame text-info-frame) ())
To wire these classes in, you need to modify find-frame-class
to return the appropriate class name when the ID indicates the frame is a text information frame, namely, whenever the ID starts with
(defun find-frame-class (name)
(cond
((and (char= (char name 0) #T)
(not (member name '('TXX' 'TXXX') :test #'string=)))
(ecase (length name)
(3 'text-info-frame-v2.2)
(4 'text-info-frame-v2.3)))
(t
(ecase (length name)
(3 'generic-frame-v2.2)
(4 'generic-frame-v2.3)))))
Another commonly used frame type is the comment frame, which is like a text information frame with a few extra fields. Like a text information frame, it starts with a single byte indicating the string encoding used in the frame. That byte is followed by a three-character ISO 8859-1 string (regardless of the value of the string encoding byte), which indicates what language the comment is in using an ISO-639-2 code, for example, 'eng' for English or 'jpn' for Japanese. That field is followed by two strings encoded as indicated by the first byte. The first is a null-terminated string containing a description of the comment. The second, which takes up the remainder of the frame, is the comment text itself.
(define-binary-class comment-frame ()
((encoding u1)
(language (iso-8859-1-string :length 3))
(description (id3-encoded-string :encoding encoding :terminator +null+))
(text (id3-encoded-string
:encoding encoding
:length (bytes-left
(+ 1 ; encoding
3 ; language
(encoded-string-length description encoding t)))))))
As in the definition of the text-info
mixin, you can use bytes-left
to compute the size of the final string. However, since the description
field is a variable-length string, the number of bytes read prior to the start of text
isn't a constant. To make matters worse, the number of bytes used to encode description
is dependent on the encoding. So, you should define a helper function that returns the number of bytes used to encode a string given the string, the encoding code, and a boolean indicating whether the string is terminated with an extra character.
(defun encoded-string-length (string encoding terminated)
(let ((characters (+ (length string) (if terminated 1 0))))
(* characters (ecase encoding (0 1) (1 2)))))
And, as before, you can define the concrete version-specific comment frame classes and wire them into find-frame-class
.
(define-binary-class comment-frame-v2.2 (id3v2.2-frame comment-frame) ())
(define-binary-class comment-frame-v2.3 (id3v2.3-frame comment-frame) ())
(defun find-frame-class (name)
(cond
((and (char= (char name 0) #T)
(not (member name '('TXX' 'TXXX') :test #'string=)))
(ecase (length name)
(3 'text-info-frame-v2.2)
(4 'text-info-frame-v2.3)))
((string= name 'COM') 'comment-frame-v2.2)
((string= name 'COMM') 'comment-frame-v2.3)
(t
(ecase (length name)
(3 'generic-frame-v2.2)
(4 'generic-frame-v2.3)))))
Now that you have the basic ability to read and write ID3 tags, you have a lot of directions you could take this code. If you want to develop a complete ID3 tag editor, you'll need to implement specific classes for all the frame types. You'd also need to define methods for manipulating the tag and frame objects in a consistent way (for instance, if you change the value of a string in a text-info-frame
, you'll likely need to adjust the size); as the code stands, there's nothing to make sure that happens.[279]
Or, if you just need to extract certain pieces of information about an MP3 file from its ID3 tag—as you will when you develop a streaming MP3 server in Chapters 27, 28, and 29—you'll need to write functions that find the appropriate frames and extract the information you want.
Finally, to make this production-quality code, you'd have to pore over the ID3 specs and deal with the details I