-
Notifications
You must be signed in to change notification settings - Fork 0
/
id3v2.lisp
406 lines (339 loc) · 14.2 KB
/
id3v2.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
(in-package :com.gigamonkeys.id3v2)
(define-binary-type unsigned-integer (bytes bits-per-byte)
(:reader (in)
(loop with value = 0
for low-bit downfrom (* bits-per-byte (1- bytes)) to 0 by bits-per-byte do
(setf (ldb (byte bits-per-byte low-bit) value) (read-byte in))
finally (return value)))
(:writer (out value)
(loop for low-bit downfrom (* bits-per-byte (1- bytes)) to 0 by bits-per-byte do
(write-byte (ldb (byte bits-per-byte low-bit) value) out))))
(define-binary-type u1 () (unsigned-integer :bytes 1 :bits-per-byte 8))
(define-binary-type u2 () (unsigned-integer :bytes 2 :bits-per-byte 8))
(define-binary-type u3 () (unsigned-integer :bytes 3 :bits-per-byte 8))
(define-binary-type u4 () (unsigned-integer :bytes 4 :bits-per-byte 8))
(define-binary-type id3-tag-size () (unsigned-integer :bytes 4 :bits-per-byte 7))
(define-binary-type generic-string (length character-type)
(:reader (in)
(let ((string (make-string length)))
(dotimes (i length)
(setf (char string i) (read-value character-type in)))
string))
(:writer (out string)
(dotimes (i length)
(write-value character-type out (char string i)))))
(define-binary-type generic-terminated-string (terminator character-type)
(:reader (in)
(with-output-to-string (s)
(loop for char = (read-value character-type in)
until (char= char terminator) do (write-char char s))))
(:writer (out string)
(loop for char across string
do (write-value character-type out char)
finally (write-value character-type out terminator))))
(define-binary-type iso-8859-1-char ()
(:reader (in)
(let ((code (read-byte in)))
(or (code-char code)
(error "Character code ~d not supported" code))))
(:writer (out char)
(let ((code (char-code char)))
(if (<= 0 code #xff)
(write-byte code out)
(error
"Illegal character for iso-8859-1 encoding: character: ~c with code: ~d"
char code)))))
(define-binary-type iso-8859-1-string (length)
(generic-string :length length :character-type 'iso-8859-1-char))
(define-binary-type iso-8859-1-terminated-string (terminator)
(generic-terminated-string :terminator terminator
:character-type 'iso-8859-1-char))
(define-binary-type ucs-2-char (swap)
(:reader (in)
(let ((code (read-value 'u2 in)))
(when swap (setf code (swap-bytes code)))
(or (code-char code) (error "Character code ~d not supported" code))))
(:writer (out char)
(let ((code (char-code char)))
(unless (<= 0 code #xffff)
(error "Illegal character for ucs-2 encoding: ~c with char-code: ~d"
char code))
(when swap (setf code (swap-bytes code)))
(write-value 'u2 out code))))
(defun swap-bytes (code)
(assert (<= code #xffff))
(rotatef (ldb (byte 8 0) code) (ldb (byte 8 8) code))
code)
(define-binary-type ucs-2-char-big-endian () (ucs-2-char :swap nil))
(define-binary-type ucs-2-char-little-endian () (ucs-2-char :swap t))
(defun ucs-2-char-type (byte-order-mark)
(ecase byte-order-mark
(#xfeff 'ucs-2-char-big-endian)
(#xfffe 'ucs-2-char-little-endian)))
(define-binary-type ucs-2-string (length)
(:reader (in)
(let ((byte-order-mark (read-value 'u2 in))
(characters (1- (/ length 2))))
(read-value
'generic-string in
:length characters
:character-type (ucs-2-char-type byte-order-mark))))
(:writer (out string)
(declare (ignore length))
(write-value 'u2 out #xfeff)
(write-value
'generic-string out string
:length (length string)
:character-type (ucs-2-char-type #xfeff))))
(define-binary-type ucs-2-terminated-string (terminator)
(:reader (in)
(let ((byte-order-mark (read-value 'u2 in)))
(read-value
'generic-terminated-string in
:terminator terminator
:character-type (ucs-2-char-type byte-order-mark))))
(:writer (out string)
(write-value 'u2 out #xfeff)
(write-value
'generic-terminated-string out string
:terminator terminator
:character-type (ucs-2-char-type #xfeff))))
(define-binary-type id3-frames (tag-size frame-type)
(:reader (in)
(loop with to-read = tag-size
while (plusp to-read)
for frame = (read-frame frame-type in)
while frame
do (decf to-read (+ (frame-header-size frame) (size frame)))
collect frame
finally (loop repeat (1- to-read) do (read-byte in))))
(:writer (out frames)
(loop with to-write = tag-size
for frame in frames
do (write-value frame-type out frame)
(decf to-write (+ (frame-header-size frame) (size frame)))
finally (loop repeat to-write do (write-byte 0 out)))))
(define-binary-type optional (type if)
(:reader (in)
(when if (read-value type in)))
(:writer (out value)
(when if (write-value type out value))))
(define-tagged-binary-class id3-tag ()
((identifier (iso-8859-1-string :length 3))
(major-version u1)
(revision u1)
(flags u1)
(size id3-tag-size))
(:dispatch
(ecase major-version
(2 'id3v2.2-tag)
(3 'id3v2.3-tag))))
(defun extended-p (flags) (logbitp 6 flags))
(defun crc-p (flags extra-flags)
(and (extended-p flags) (logbitp 15 extra-flags)))
(defun frame-compressed-p (flags) (logbitp 7 flags))
(defun frame-encrypted-p (flags) (logbitp 6 flags))
(defun frame-grouped-p (flags) (logbitp 5 flags))
(define-tagged-binary-class id3v2.2-frame ()
((id (frame-id :length 3))
(size u3))
(:dispatch (find-frame-class id)))
(define-tagged-binary-class id3v2.3-frame ()
((id (frame-id :length 4))
(size u4)
(flags u2)
(decompressed-size (optional :type 'u4 :if (frame-compressed-p flags)))
(encryption-scheme (optional :type 'u1 :if (frame-encrypted-p flags)))
(grouping-identity (optional :type 'u1 :if (frame-grouped-p flags))))
(:dispatch (find-frame-class id)))
(define-binary-class id3v2.2-tag (id3-tag)
((frames (id3-frames :tag-size size :frame-type 'id3v2.2-frame))))
(define-binary-class id3v2.3-tag (id3-tag)
((extended-header-size (optional :type 'u4 :if (extended-p flags)))
(extra-flags (optional :type 'u2 :if (extended-p flags)))
(padding-size (optional :type 'u4 :if (extended-p flags)))
(crc (optional :type 'u4 :if (crc-p flags extra-flags)))
(frames (id3-frames :tag-size size :frame-type 'id3v2.3-frame))))
(defgeneric frame-header-size (frame))
(defmethod frame-header-size ((frame id3v2.2-frame)) 6)
(defmethod frame-header-size ((frame id3v2.3-frame)) 10)
(defun read-id3 (file)
(with-open-file (in file :element-type '(unsigned-byte 8))
(read-value 'id3-tag in)))
(defun show-tag-header (file)
(with-slots (identifier major-version revision flags size) (read-id3 file)
(format t "~a ~d.~d ~8,'0b ~d bytes -- ~a~%"
identifier major-version revision flags size (enough-namestring file))))
(defun mp3-p (file)
(and
(not (directory-pathname-p file))
(string-equal "mp3" (pathname-type file))))
(defun show-tag-headers (dir)
(walk-directory dir #'show-tag-header :test #'mp3-p))
(defun count-versions (dir)
(let ((versions (mapcar #'(lambda (x) (cons x 0)) '(2 3 4))))
(flet ((count-version (file)
(incf (cdr (assoc (major-version (read-id3 file)) versions)))))
(walk-directory dir #'count-version :test #'mp3-p))
versions))
(defun id3-p (file)
(with-open-file (in file :element-type '(unsigned-byte 8))
(string= "ID3" (read-value 'iso-8859-1-string in :length 3))))
(define-tagged-binary-class id3-frame ()
((id (frame-id :length 3))
(size u3))
(:dispatch (find-frame-class id)))
(define-binary-class generic-frame ()
((data (raw-bytes :size (data-bytes (current-binary-object))))))
(define-binary-type raw-bytes (size)
(:reader (in)
(let ((buf (make-array size :element-type '(unsigned-byte 8))))
(read-sequence buf in)
buf))
(:writer (out buf)
(declare (ignore size))
(write-sequence buf out)))
(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)))))
(define-condition in-padding () ())
(define-binary-type frame-id (length)
(:reader (in)
(let ((first-byte (read-byte in)))
(when (= first-byte 0) (signal 'in-padding))
(let ((rest (read-value 'iso-8859-1-string in :length (1- length))))
(concatenate 'string (string (code-char first-byte)) rest))))
(:writer (out id)
(write-value 'iso-8859-1-string out id :length length)))
(defun read-frame (frame-type in)
(handler-case (read-value frame-type in)
(in-padding () nil)))
(defgeneric data-bytes (frame))
(defmethod data-bytes ((frame id3v2.2-frame))
(size frame))
(defmethod data-bytes ((frame id3v2.3-frame))
(let ((flags (flags frame)))
(- (size frame)
(if (frame-compressed-p flags) 4 0)
(if (frame-encrypted-p flags) 1 0)
(if (frame-grouped-p flags) 1 0))))
(define-binary-class generic-frame-v2.2 (id3v2.2-frame generic-frame) ())
(define-binary-class generic-frame-v2.3 (id3v2.3-frame generic-frame) ())
(defun frame-types (file)
(delete-duplicates (mapcar #'id (frames (read-id3 file))) :test #'string=))
(defun frame-types-in-dir (dir)
(let ((ids ()))
(flet ((collect (file)
(setf ids (nunion ids (frame-types file) :test #'string=))))
(walk-directory dir #'collect :test #'mp3-p))
ids))
(defun non-terminated-type (encoding)
(ecase encoding
(0 'iso-8859-1-string)
(1 'ucs-2-string)))
(defun terminated-type (encoding)
(ecase encoding
(0 'iso-8859-1-terminated-string)
(1 'ucs-2-terminated-string)))
(defun string-args (encoding length terminator)
(cond
(length
(values (non-terminated-type encoding) :length length))
(terminator
(values (terminated-type encoding) :terminator terminator))))
(define-binary-type id3-encoded-string (encoding length terminator)
(:reader (in)
(multiple-value-bind (type keyword arg)
(string-args encoding length terminator)
(read-value type in keyword arg)))
(:writer (out string)
(multiple-value-bind (type keyword arg)
(string-args encoding length terminator)
(write-value type out string keyword arg))))
(define-binary-class text-info-frame ()
((encoding u1)
(information (id3-encoded-string :encoding encoding :length (bytes-left 1)))))
(defun bytes-left (bytes-read)
(- (size (current-binary-object)) bytes-read))
(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) ())
(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)))))))
(defun encoded-string-length (string encoding terminated)
(let ((characters (+ (length string) (if terminated 1 0))))
(* characters (ecase encoding (0 1) (1 2)))))
(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 upto-null (string)
(subseq string 0 (position +null+ string)))
(defun find-frame (id3 ids)
(find-if #'(lambda (x) (find (id x) ids :test #'string=)) (frames id3)))
(defun get-text-info (id3 &rest ids)
(let ((frame (find-frame id3 ids)))
(when frame (upto-null (information frame)))))
(defun song (id3) (get-text-info id3 "TT2" "TIT2"))
(defun album (id3) (get-text-info id3 "TAL" "TALB"))
(defun artist (id3) (get-text-info id3 "TP1" "TPE1"))
(defun track (id3) (get-text-info id3 "TRK" "TRCK"))
(defun year (id3) (get-text-info id3 "TYE" "TYER" "TDRC"))
(defun genre (id3) (get-text-info id3 "TCO" "TCON"))
(defun translated-genre (id3)
(let ((genre (genre id3)))
(if (and genre (char= #\( (char genre 0)))
(translate-v1-genre genre)
genre)))
(defun translate-v1-genre (genre)
(aref *id3-v1-genres* (parse-integer genre :start 1 :junk-allowed t)))
(defparameter *id3-v1-genres*
#(
;; These are the official ID3v1 genres
"Blues" "Classic Rock" "Country" "Dance" "Disco" "Funk" "Grunge"
"Hip-Hop" "Jazz" "Metal" "New Age" "Oldies" "Other" "Pop" "R&B" "Rap"
"Reggae" "Rock" "Techno" "Industrial" "Alternative" "Ska"
"Death Metal" "Pranks" "Soundtrack" "Euro-Techno" "Ambient"
"Trip-Hop" "Vocal" "Jazz+Funk" "Fusion" "Trance" "Classical"
"Instrumental" "Acid" "House" "Game" "Sound Clip" "Gospel" "Noise"
"AlternRock" "Bass" "Soul" "Punk" "Space" "Meditative"
"Instrumental Pop" "Instrumental Rock" "Ethnic" "Gothic" "Darkwave"
"Techno-Industrial" "Electronic" "Pop-Folk" "Eurodance" "Dream"
"Southern Rock" "Comedy" "Cult" "Gangsta" "Top 40" "Christian Rap"
"Pop/Funk" "Jungle" "Native American" "Cabaret" "New Wave"
"Psychadelic" "Rave" "Showtunes" "Trailer" "Lo-Fi" "Tribal"
"Acid Punk" "Acid Jazz" "Polka" "Retro" "Musical" "Rock & Roll"
"Hard Rock"
;; These were made up by the authors of Winamp but backported into
;; the ID3 spec.
"Folk" "Folk-Rock" "National Folk" "Swing" "Fast Fusion"
"Bebob" "Latin" "Revival" "Celtic" "Bluegrass" "Avantgarde"
"Gothic Rock" "Progressive Rock" "Psychedelic Rock" "Symphonic Rock"
"Slow Rock" "Big Band" "Chorus" "Easy Listening" "Acoustic" "Humour"
"Speech" "Chanson" "Opera" "Chamber Music" "Sonata" "Symphony"
"Booty Bass" "Primus" "Porn Groove" "Satire" "Slow Jam" "Club"
"Tango" "Samba" "Folklore" "Ballad" "Power Ballad" "Rhythmic Soul"
"Freestyle" "Duet" "Punk Rock" "Drum Solo" "A capella" "Euro-House"
"Dance Hall"
;; These were also invented by the Winamp folks but ignored by the
;; ID3 authors.
"Goa" "Drum & Bass" "Club-House" "Hardcore" "Terror" "Indie"
"BritPop" "Negerpunk" "Polsk Punk" "Beat" "Christian Gangsta Rap"
"Heavy Metal" "Black Metal" "Crossover" "Contemporary Christian"
"Christian Rock" "Merengue" "Salsa" "Thrash Metal" "Anime" "Jpop"
"Synthpop"))