mp3-frame.lisp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. ;;; -*- Mode: Lisp; show-trailing-whitespace: t; Base: 10; indent-tabs: nil; Syntax: ANSI-Common-Lisp; Package: MP-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. (seek mp3-file 0 :start)
  34. (let* ((id3 (read-string mp3-file :size 3))
  35. (version (read-u8 mp3-file))
  36. (tag))
  37. (seek mp3-file 128 :end)
  38. (setf tag (read-string mp3-file :size 3))
  39. (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 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 (read-string instream :size 30 :terminators '(0)))
  68. (setf artist (read-string instream :size 30 :terminators '(0)))
  69. (setf album (read-string instream :size 30 :terminators '(0)))
  70. (setf year (read-string instream :size 4 :terminators '(0)))
  71. (setf comment (read-string instream :size 30 :terminators '(0)))
  72. (setf genre (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 seek'ed to correct location in file."
  87. (with-slots (size flags padding crc) me
  88. (setf size (read-u32 instream))
  89. (setf flags (read-u16 instream))
  90. (setf padding (read-u32 instream))
  91. (when (ext-header-crc-p flags)
  92. (setf crc (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. (seek instream 128 :end)
  129. (when (string= "TAG" (read-string instream :size 3))
  130. (log-mp3-frame "looking at last 128 bytes at ~:d to try to read id3v21 header" (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. (seek instream 0 :start)
  136. (when (string= "ID3" (read-string instream :size 3))
  137. (setf version (read-u8 instream))
  138. (setf revision (read-u8 instream))
  139. (setf flags (read-u8 instream))
  140. (setf size (mp3-file:read-sync-safe-u32 instream))
  141. (when (header-unsynchronized-p flags) (log-mp3-frame "unsync"))
  142. (assert (not (header-footer-p flags)) () "Can't decode ID3 footer's yet")
  143. (when (header-extended-p flags)
  144. (setf ext-header (make-instance 'mp3-extended-header :instream instream))))
  145. (log-mp3-frame "~a" (vpprint me nil)))))
  146. (defclass id3-frame ()
  147. ((pos :accessor pos :initarg :pos)
  148. (version :accessor version :initarg :version)
  149. (id :accessor id :initarg :id)
  150. (len :accessor len :initarg :len)
  151. (flags :accessor flags :initarg :flags :initform nil))
  152. (:documentation "Base class for an ID3 frame"))
  153. (defmacro frame-23-altertag-p (frame-flags) `(logbitp 15 ,frame-flags))
  154. (defmacro frame-23-alterfile-p (frame-flags) `(logbitp 14 ,frame-flags))
  155. (defmacro frame-23-readonly-p (frame-flags) `(logbitp 13 ,frame-flags))
  156. (defmacro frame-23-compress-p (frame-flags) `(logbitp 7 ,frame-flags))
  157. (defmacro frame-23-encrypt-p (frame-flags) `(logbitp 6 ,frame-flags))
  158. (defmacro frame-23-group-p (frame-flags) `(logbitp 5 ,frame-flags))
  159. (defmacro frame-24-altertag-p (frame-flags) `(logbitp 14 ,frame-flags))
  160. (defmacro frame-24-alterfile-p (frame-flags) `(logbitp 13 ,frame-flags))
  161. (defmacro frame-24-readonly-p (frame-flags) `(logbitp 12 ,frame-flags))
  162. (defmacro frame-24-groupid-p (frame-flags) `(logbitp 6 ,frame-flags))
  163. (defmacro frame-24-compress-p (frame-flags) `(logbitp 3 ,frame-flags))
  164. (defmacro frame-24-encrypt-p (frame-flags) `(logbitp 2 ,frame-flags))
  165. (defmacro frame-24-unsynch-p (frame-flags) `(logbitp 1 ,frame-flags))
  166. (defmacro frame-24-datalen-p (frame-flags) `(logbitp 0 ,frame-flags))
  167. (defun valid-frame-flags (header-version frame-flags)
  168. (ecase header-version
  169. (3 (zerop (logand #b0001111100011111 frame-flags)))
  170. (4 (zerop (logand #b1000111110110000 frame-flags)))))
  171. (defmethod print-object ((me id3-frame) stream)
  172. (if (null *pprint-mp3-frame*)
  173. (call-next-method)
  174. (with-slots (pos version valid-p id len flags) me
  175. (format stream "@offset: ~:d, version = ~d, id: ~s, len: ~:d "
  176. pos version id len)
  177. (if flags
  178. (ecase version
  179. (3 (format stream "flags: 0x~4,'0x: ~:[0/~;tag-alter-preservation/~]~:[0/~;file-alter-preservation/~]~:[0/~;read-only/~]~:[0/~;compress/~]~:[0/~;encypt/~]~:[0~;group~], "
  180. flags
  181. (frame-23-altertag-p flags)
  182. (frame-23-alterfile-p flags)
  183. (frame-23-readonly-p flags)
  184. (frame-23-compress-p flags)
  185. (frame-23-encrypt-p flags)
  186. (frame-23-group-p flags)))
  187. (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~], "
  188. flags
  189. (frame-24-altertag-p flags)
  190. (frame-24-alterfile-p flags)
  191. (frame-24-readonly-p flags)
  192. (frame-24-groupid-p flags)
  193. (frame-24-compress-p flags)
  194. (frame-24-encrypt-p flags)
  195. (frame-24-unsynch-p flags)
  196. (frame-24-datalen-p flags))))))))
  197. (defclass raw-frame (id3-frame)
  198. ((octets :accessor octets :initform nil))
  199. (:documentation "Frame class that slurps in frame contents"))
  200. (defmethod initialize-instance :after ((me raw-frame) &key instream)
  201. (log5:with-context "raw-frame"
  202. (with-slots (len octets) me
  203. (log-mp3-frame "reading ~:d bytes from position ~:d" len (seek instream 0 :current))
  204. (setf octets (read-octets instream len)))))
  205. (defmethod print-object :after ((me raw-frame) stream)
  206. (if (null *pprint-mp3-frame*)
  207. (call-next-method)
  208. (with-slots (octets) me
  209. (let* ((len (length (slot-value me 'octets)))
  210. (print-len (min len 10))
  211. (printable-array (make-array print-len :displaced-to (slot-value me 'octets))))
  212. (format stream "[~:d of ~:d bytes] <~x>" print-len len printable-array)))))
  213. (defun find-id3-frames (header instream)
  214. "Loop thru all the frames in INSTREAM based on information from HEADER"
  215. (labels ((read-and-de-sync (instream len)
  216. "Used to undo sync-safe when the header says false syncs have been removed from the tags"
  217. (let* ((last-byte-was-FF nil)
  218. (byte nil)
  219. (synced-frame-data (with-binary-output-to-vector (out)
  220. (dotimes (i len)
  221. (setf byte (read-byte instream))
  222. (if last-byte-was-FF
  223. (if (not (zerop byte))
  224. (write-byte byte out))
  225. (write-byte byte out))
  226. (setf last-byte-was-FF (= byte #xFF))))))
  227. synced-frame-data)))
  228. (log5:with-context "find-id3-frames"
  229. nil)))
  230. ;; (let ((mem-stream)
  231. ;; (first-byte))
  232. ;; (if (header-unsynchronized-p header)
  233. ;; (setf mem-stream (read-and-desync instream (size header)))
  234. ;; (log-mp3-frame "Looking for frames: header = ~a, starting position = ~:d" (mp3-frame:vpprint header nil) (seek instream 0 :current))
  235. ;; (loop
  236. ;; (let ((first-byte (read-u8 instream)))
  237. ;; (when (
  238. ;; ; (if (header-unsynchronized-p (flags header))
  239. ;; (do* ((pos (seek instream 0 :current))
  240. ;; (frame)
  241. ;; (end (+ pos (size header))))
  242. ;; ((>= pos end))
  243. ;; nil))
  244. (defun find-mp3-frames (mp3-file)
  245. "With an open mp3-file, make sure it is in fact an MP3 file, then read it's header and frames, returning both"
  246. (log5:with-context "find-mp3-frames"
  247. (when (not (is-valid-mp3-file mp3-file))
  248. (log-mp3-frame "~a is not an mp3 file" (filename mp3-file))
  249. (error 'mp3-frame-condition :location "find-mp3-frames" :object (filename mp3-file) :message "is not an mp3 file"))
  250. (log-mp3-frame "~a is a valid mp3 file" (filename mp3-file))
  251. (let* ((header (make-instance 'mp3-id3-header :instream mp3-file))
  252. (frames (find-id3-frames header mp3-file)))
  253. (log-mp3-frame "Header: ~a, frames = ~a" header frames)
  254. (setf (slot-value header 'frames) frames))))