mp3-frame.lisp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. ;;; -*- Mode: Lisp; show-trailing-whitespace: t; Base: 10; indent-tabs: nil; Syntax: ANSI-Common-Lisp; Package: MP3-FRAME; -*-
  2. ;;; Copyright (c) 2013, Mark VandenBrink. All rights reserved.
  3. (in-package #:mp3-frame)
  4. (log5:defcategory cat-log-mp3-frame)
  5. (defmacro log-mp3-frame (&rest log-stuff) `(log5:log-for (cat-log-mp3-frame) ,@log-stuff))
  6. (define-condition mp3-frame-condition ()
  7. ((location :initarg :location :reader location :initform nil)
  8. (object :initarg :object :reader object :initform nil)
  9. (messsage :initarg :message :reader message :initform "Undefined Condition"))
  10. (:report (lambda (condition stream)
  11. (format stream "mp3-frame condition at location: <~a> with object: <~a>: message: <~a>"
  12. (location condition) (object condition) (message condition)))))
  13. (defmethod print-object ((me mp3-frame-condition) stream)
  14. (format stream "location: <~a>, object: <~a>, message: <~a>" (location me) (object me) (message me)))
  15. (defparameter *pprint-mp3-frame* nil
  16. "Controls whether we pretty print frame data")
  17. (defclass mp3-id3-header ()
  18. ((version :accessor version :initarg :version :initform 0)
  19. (revision :accessor revision :initarg :revision :initform 0)
  20. (flags :accessor flags :initarg :flags :initform 0)
  21. (size :accessor size :initarg :size :initform 0)
  22. (ext-header :accessor ext-header :initarg :ext-header :initform nil)
  23. (frames :accessor frames :initarg :frames :initform nil)
  24. (v21-tag-header :accessor v21-tag-header :initarg :v21-tag-header :initform nil))
  25. (:documentation "The ID3 header, found at start of file"))
  26. (defmethod vpprint ((me mp3-id3-header) stream &key (indent 0))
  27. "Set *pprint-mp3-frame* to get pretty printing and call print-object via format"
  28. (let ((*pprint-mp3-frame* t))
  29. (format stream "~vt~a" (* indent 1) me)))
  30. (defun is-valid-mp3-file (mp3-file)
  31. "Make sure this is an MP3 file. Look for frames at begining and/or end"
  32. (log5:with-context "is-valid-mp3-file"
  33. (stream-seek mp3-file 0 :start)
  34. (let* ((id3 (stream-read-string mp3-file :size 3))
  35. (version (stream-read-u8 mp3-file))
  36. (tag))
  37. (stream-seek mp3-file 128 :end)
  38. (setf tag (stream-read-string mp3-file :size 3))
  39. (stream-seek mp3-file 0 :start)
  40. (log-mp3-frame "id3 = ~a, version = ~d" id3 version)
  41. (or (and (string= "ID3" id3)
  42. (or (= 2 version) (= 3 version) (= 4 version)))
  43. (string= tag "TAG")))))
  44. (defclass v21-tag-header ()
  45. ((songname :accessor songname :initarg :songname :initform nil)
  46. (artist :accessor artist :initarg :artist :initform nil)
  47. (album :accessor album :initarg :album :initform nil)
  48. (year :accessor year :initarg :year :initform nil)
  49. (comment :accessor comment :initarg :comment :initform nil)
  50. (genre :accessor genre :initarg :genre :initform nil))
  51. (:documentation "ID3 V2.1 old-style tag. If present, found in last 128 bytes of file."))
  52. (defmethod vpprint ((me v21-tag-header) stream &key (indent 0))
  53. "Set *pprint-mp3-frame* to get pretty printing and call print-object via format"
  54. (let ((*pprint-mp3-frame* t))
  55. (format stream "~vt~a" (* indent 1) me)))
  56. (defmethod print-object ((me v21-tag-header) stream)
  57. (if (null *pprint-mp3-frame*)
  58. (call-next-method)
  59. (with-slots (songname artist album year comment genre) me
  60. (format stream "songname = <~a>, artist = <~a>, album = <~a>, year = <~a>, comment = <~a>, genre = ~d"
  61. songname artist album year comment genre))))
  62. (defmethod initialize-instance ((me v21-tag-header) &key instream)
  63. "Read in a V2.1 tag. Caller will have stream-seek'ed file to correct location and ensured that TAG was present"
  64. (log5:with-context "v21-frame-initializer"
  65. (log-mp3-frame "reading v2.1 tag")
  66. (with-slots (songname artist album year comment genre) me
  67. (setf songname (stream-read-string instream :size 30 :terminators '(0)))
  68. (setf artist (stream-read-string instream :size 30 :terminators '(0)))
  69. (setf album (stream-read-string instream :size 30 :terminators '(0)))
  70. (setf year (stream-read-string instream :size 4 :terminators '(0)))
  71. (setf comment (stream-read-string instream :size 30 :terminators '(0)))
  72. (setf genre (stream-read-u8 instream))
  73. (log-mp3-frame "v21 tag: ~a" (vpprint me nil)))))
  74. (defclass mp3-ext-header ()
  75. ((size :accessor size :initarg :size :initform 0)
  76. (flags :accessor flags :initarg :flags :initform 0)
  77. (padding :accessor padding :initarg :padding :initform 0)
  78. (crc :accessor crc :initarg :crc :initform nil))
  79. (:documentation "class representing a V2.3/4 extended header"))
  80. (defmethod vpprint ((me mp3-ext-header) stream &key (indent 0))
  81. "Set *pprint-mp3-frame* to get pretty printing and call print-object via format"
  82. (let ((*pprint-mp3-frame* t))
  83. (format stream "~vt~a" (* indent 1) me)))
  84. (defmacro ext-header-crc-p (flags) `(logbitp 15 ,flags))
  85. (defmethod initialize-instance ((me mp3-ext-header) &key instream)
  86. "Read in the extended header. Caller will have stream-seek'ed to correct location in file."
  87. (with-slots (size flags padding crc) me
  88. (setf size (stream-read-u32 instream))
  89. (setf flags (stream-read-u16 instream))
  90. (setf padding (stream-read-u32 instream))
  91. (when (ext-header-crc-p flags)
  92. (setf crc (stream-read-u32 instream)))))
  93. (defmethod print-object ((me mp3-ext-header) stream)
  94. (if (null *pprint-mp3-frame*)
  95. (call-next-method)
  96. (with-slots (size flags padding crc) me
  97. (format stream "extended header: size: ~d, flags: ~x, padding ~:d, crc = ~x~%"
  98. size flags padding crc))))
  99. (defmacro header-unsynchronized-p (flags) `(logbitp 7 ,flags))
  100. (defmacro header-extended-p (flags) `(logbitp 6 ,flags))
  101. (defmacro header-experimental-p (flags) `(logbitp 5 ,flags))
  102. (defmacro header-footer-p (flags) `(logbitp 4 ,flags)) ;; N.B. *NOT* defined for 2.3 tags
  103. (defmacro print-header-flags (stream flags)
  104. `(format ,stream "0x~2,'0x: ~:[0/~;unsynchronized-frames/~]~:[0/~;extended-header/~]~:[0/~;expermental-tag/~]~:[0~;footer-present~]"
  105. ,flags
  106. (header-unsynchronized-p ,flags)
  107. (header-extended-p ,flags)
  108. (header-experimental-p ,flags)
  109. (header-footer-p ,flags)))
  110. (defmethod print-object ((me mp3-id3-header) stream)
  111. (if (null *pprint-mp3-frame*)
  112. (call-next-method)
  113. (with-slots (version revision flags v21-tag-header size ext-header frames) me
  114. (format stream "Header: version/revision: ~d/~d, flags: ~a, size = ~:d bytes; ~a; ~a"
  115. version revision (print-header-flags nil flags) size
  116. (if (header-extended-p flags)
  117. (concatenate 'string "Extended header: " (vpprint ext-header nil))
  118. "No extended header")
  119. (if v21-tag-header
  120. (concatenate 'string "V21 tag: " (vpprint v21-tag-header nil))
  121. "No v21 tag"))
  122. (when frames
  123. (format stream "~4tFrames[~d]:~%~{~8t~a~^~%~}" (length frames) frames)))))
  124. (defmethod initialize-instance :after ((me mp3-id3-header) &key instream &allow-other-keys)
  125. "Fill in an mp3-header from file."
  126. (log5:with-context "mp3-id3-header-initializer"
  127. (with-slots (version revision flags size ext-header frames v21-tag-header) me
  128. (stream-seek instream 128 :end)
  129. (when (string= "TAG" (stream-read-string instream :size 3))
  130. (log-mp3-frame "looking at last 128 bytes at ~:d to try to read id3v21 header" (stream-seek instream 0 :current))
  131. (handler-case
  132. (setf v21-tag-header (make-instance 'v21-tag-header :instream instream))
  133. (condition (c)
  134. (log-mp3-frame "reading v21 got condition: ~a" c))))
  135. (stream-seek instream 0 :start)
  136. (when (string= "ID3" (stream-read-string instream :size 3))
  137. (setf version (stream-read-u8 instream))
  138. (setf revision (stream-read-u8 instream))
  139. (setf flags (stream-read-u8 instream))
  140. (setf size (stream-read-sync-safe-u32 instream))
  141. (when (header-unsynchronized-p flags)
  142. (log-mp3-frame "unsync"))
  143. (assert (not (header-footer-p flags)) () "Can't decode ID3 footer's yet")
  144. (when (header-extended-p flags)
  145. (setf ext-header (make-instance 'mp3-extended-header :instream instream))))
  146. (log-mp3-frame "~a" (vpprint me nil)))))
  147. (defclass id3-frame ()
  148. ((pos :accessor pos :initarg :pos)
  149. (version :accessor version :initarg :version)
  150. (id :accessor id :initarg :id)
  151. (len :accessor len :initarg :len)
  152. (flags :accessor flags :initarg :flags :initform nil))
  153. (:documentation "Base class for an ID3 frame"))
  154. (defmacro frame-23-altertag-p (frame-flags) `(logbitp 15 ,frame-flags))
  155. (defmacro frame-23-alterfile-p (frame-flags) `(logbitp 14 ,frame-flags))
  156. (defmacro frame-23-readonly-p (frame-flags) `(logbitp 13 ,frame-flags))
  157. (defmacro frame-23-compress-p (frame-flags) `(logbitp 7 ,frame-flags))
  158. (defmacro frame-23-encrypt-p (frame-flags) `(logbitp 6 ,frame-flags))
  159. (defmacro frame-23-group-p (frame-flags) `(logbitp 5 ,frame-flags))
  160. (defmacro frame-24-altertag-p (frame-flags) `(logbitp 14 ,frame-flags))
  161. (defmacro frame-24-alterfile-p (frame-flags) `(logbitp 13 ,frame-flags))
  162. (defmacro frame-24-readonly-p (frame-flags) `(logbitp 12 ,frame-flags))
  163. (defmacro frame-24-groupid-p (frame-flags) `(logbitp 6 ,frame-flags))
  164. (defmacro frame-24-compress-p (frame-flags) `(logbitp 3 ,frame-flags))
  165. (defmacro frame-24-encrypt-p (frame-flags) `(logbitp 2 ,frame-flags))
  166. (defmacro frame-24-unsynch-p (frame-flags) `(logbitp 1 ,frame-flags))
  167. (defmacro frame-24-datalen-p (frame-flags) `(logbitp 0 ,frame-flags))
  168. (defun valid-frame-flags (header-version frame-flags)
  169. (ecase header-version
  170. (3 (zerop (logand #b0001111100011111 frame-flags)))
  171. (4 (zerop (logand #b1000111110110000 frame-flags)))))
  172. (defmethod print-object ((me id3-frame) stream)
  173. (if (null *pprint-mp3-frame*)
  174. (call-next-method)
  175. (with-slots (pos version valid-p id len flags) me
  176. (format stream "@offset: ~:d, version = ~d, id: ~s, len: ~:d "
  177. pos version id len)
  178. (if flags
  179. (ecase version
  180. (3 (format stream "flags: 0x~4,'0x: ~:[0/~;tag-alter-preservation/~]~:[0/~;file-alter-preservation/~]~:[0/~;read-only/~]~:[0/~;compress/~]~:[0/~;encypt/~]~:[0~;group~], "
  181. flags
  182. (frame-23-altertag-p flags)
  183. (frame-23-alterfile-p flags)
  184. (frame-23-readonly-p flags)
  185. (frame-23-compress-p flags)
  186. (frame-23-encrypt-p flags)
  187. (frame-23-group-p flags)))
  188. (4 (format stream "flags: 0x~4,'0x: ~:[0/~;tag-alter-preservation/~]~:[0/~;file-alter-preservation/~]~:[0/~;read-only/~]~:[0/~;group-id/~]~:[0/~;compress/~]~:[0/~;encypt/~]~:[0/~;unsynch/~]~:[0~;datalen~], "
  189. flags
  190. (frame-24-altertag-p flags)
  191. (frame-24-alterfile-p flags)
  192. (frame-24-readonly-p flags)
  193. (frame-24-groupid-p flags)
  194. (frame-24-compress-p flags)
  195. (frame-24-encrypt-p flags)
  196. (frame-24-unsynch-p flags)
  197. (frame-24-datalen-p flags))))))))
  198. (defclass raw-frame (id3-frame)
  199. ((octets :accessor octets :initform nil))
  200. (:documentation "Frame class that slurps in frame contents"))
  201. (defmethod initialize-instance :after ((me raw-frame) &key instream)
  202. (log5:with-context "raw-frame"
  203. (with-slots (len octets) me
  204. (log-mp3-frame "reading ~:d bytes from position ~:d" len (stream-seek instream 0 :current))
  205. (setf octets (stream-read-octets instream len)))))
  206. (defmethod print-object :after ((me raw-frame) stream)
  207. (if (null *pprint-mp3-frame*)
  208. (call-next-method)
  209. (with-slots (octets) me
  210. (let* ((len (length (slot-value me 'octets)))
  211. (print-len (min len 10))
  212. (printable-array (make-array print-len :displaced-to (slot-value me 'octets))))
  213. (format stream "[~:d of ~:d bytes] <~x>" print-len len printable-array)))))
  214. (defun find-mp3-frames (mp3-file)
  215. "With an open mp3-file, make sure it is in fact an MP3 file, then read it's header and frames"
  216. (log5:with-context "find-mp3-frames"
  217. (when (not (is-valid-mp3-file mp3-file))
  218. (log-mp3-frame "~a is not an mp3 file" (filename mp3-file))
  219. (error 'mp3-frame-condition :location "find-mp3-frames" :object (filename mp3-file) :message "is not an mp3 file"))
  220. (log-mp3-frame "~a is a valid mp3 file" (filename mp3-file))
  221. (setf (mp3-header mp3-file) (make-instance 'mp3-id3-header :instream mp3-file))
  222. (ccl:with-input-from-vector (v (stream-read-octets mp3-file (size (mp3-header mp3-file))
  223. :bits-per-byte (if (header-unsynchronized-p (flags (mp3-header mp3-file))) 7 8)))
  224. (block read-loop
  225. (loop
  226. (let ((this-frame (make-frame v)))
  227. (when (null this-frame)
  228. (return-from read-loop nil))
  229. (push this-frame (frames (mp3-header mp3-file)))))))))