mp4-atom.lisp 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. ;;; -*- Mode: Lisp; show-trailing-whitespace: t; Base: 10; indent-tabs: nil; Syntax: ANSI-Common-Lisp; Package: MP4-ATOM; -*-
  2. ;;; Copyright (c) 2013, Mark VandenBrink. All rights reserved.
  3. (in-package #:mp4-atom)
  4. (log5:defcategory cat-log-mp4-atom)
  5. (defmacro log-mp4-atom (&rest log-stuff) `(log5:log-for (cat-log-mp4-atom) ,@log-stuff))
  6. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ATOMS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  7. ;;;
  8. ;;; A word about atoms (aka "boxes"). There are three kinds of atoms: ones that are containers, ones
  9. ;;; that are data, and ones that are both. A lot of the source code for taggers out there mostly ignore
  10. ;;; the third class and treat "container atoms that also have data" as a big blob of data that they
  11. ;;; rummage around in via indices. Seems sort of broken, IMHO, so we'll try to handle all three if
  12. ;;; at all possible.
  13. ;;;
  14. (defun as-int (str)
  15. "Given a 4-byte string, return an integer type equivalent.
  16. (eg (as-int \"hdlr\" == +audioprop-hdlr+))"
  17. (declare #.utils:*standard-optimize-settings*)
  18. (let ((int 0))
  19. (declare (integer int))
  20. (setf (ldb (byte 8 24) int) (char-code (aref str 0))
  21. (ldb (byte 8 16) int) (char-code (aref str 1))
  22. (ldb (byte 8 8) int) (char-code (aref str 2))
  23. (ldb (byte 8 0) int) (char-code (aref str 3)))
  24. int))
  25. (defun as-string (atom-type)
  26. (declare #.utils:*standard-optimize-settings*)
  27. (with-output-to-string (s nil)
  28. (write-char (code-char (ldb (byte 8 24) atom-type)) s)
  29. (write-char (code-char (ldb (byte 8 16) atom-type)) s)
  30. (write-char (code-char (ldb (byte 8 8) atom-type)) s)
  31. (write-char (code-char (ldb (byte 8 0) atom-type)) s)))
  32. (utils:memoize 'as-string)
  33. (defun mk-atom-class-name (name)
  34. (string-upcase (concatenate 'string "atom-" (as-string name))))
  35. (utils:memoize 'mk-atom-class-name)
  36. (eval-when (:compile-toplevel :load-toplevel :execute)
  37. (defun as-octet (c)
  38. "Used below so that we can create atom 'types' from char/ints"
  39. (declare #.utils:*standard-optimize-settings*)
  40. (cond ((typep c 'standard-char) (coerce (char-code c) '(unsigned-byte 8)))
  41. ((typep c 'integer) (coerce c '(unsigned-byte 8)))
  42. (t (error "can only handle characters and integers"))))
  43. (defmacro mk-mp4-atom-type (l1 l2 l3 l4)
  44. "Given 4 chars/ints, create a 32-bit word representing an atom 'type' (aka name)"
  45. `(let ((retval 0))
  46. (setf (ldb (byte 8 24) retval) ,(as-octet l1)
  47. (ldb (byte 8 16) retval) ,(as-octet l2)
  48. (ldb (byte 8 8) retval) ,(as-octet l3)
  49. (ldb (byte 8 0) retval) ,(as-octet l4))
  50. retval)))
  51. (defconstant +itunes-album+ (mk-mp4-atom-type #xa9 #\a #\l #\b) "text: album name")
  52. (defconstant +itunes-album-artist+ (mk-mp4-atom-type #\a #\A #\R #\T) "text: album artist")
  53. (defconstant +itunes-artist+ (mk-mp4-atom-type #xa9 #\A #\R #\T) "text: artist name")
  54. (defconstant +itunes-comment+ (mk-mp4-atom-type #xa9 #\c #\m #\t) "text: comment, commonly used by iTunes for sound info, etc")
  55. (defconstant +itunes-compilation+ (mk-mp4-atom-type #\c #\p #\i #\l) "byte/boolean: is this file part of a compilation?")
  56. (defconstant +itunes-composer+ (mk-mp4-atom-type #xa9 #\c #\o #\m) "text: composer name")
  57. (defconstant +itunes-copyright+ (mk-mp4-atom-type #\c #\p #\r #\t) "text: copyright info")
  58. (defconstant +itunes-cover-art+ (mk-mp4-atom-type #\c #\o #\v #\r) "octets: cover art, PNG, etc")
  59. (defconstant +itunes-disk+ (mk-mp4-atom-type #\d #\i #\s #\k) "octets: disk number, can be n of N")
  60. (defconstant +itunes-encoder+ (mk-mp4-atom-type #xa9 #\e #\n #\c) "text: who encoded")
  61. (defconstant +itunes-genre+ (mk-mp4-atom-type #\g #\n #\r #\e) "octets: genre of file")
  62. (defconstant +itunes-genre-x+ (mk-mp4-atom-type #xa9 #\n #\r #\e) "text: yet another genre atom")
  63. (defconstant +itunes-groups+ (mk-mp4-atom-type #xa9 #\g #\r #\p) "text: ???")
  64. (defconstant +itunes-lyrics+ (mk-mp4-atom-type #xa9 #\l #\y #\r) "text: lyrics tag")
  65. (defconstant +itunes-purchased-date+ (mk-mp4-atom-type #\p #\u #\r #\d) "text: when song was purchased")
  66. (defconstant +itunes-tempo+ (mk-mp4-atom-type #\t #\m #\p #\o) "octet: tempo of song")
  67. (defconstant +itunes-title+ (mk-mp4-atom-type #xa9 #\n #\a #\m) "text: title of song")
  68. (defconstant +itunes-tool+ (mk-mp4-atom-type #xa9 #\t #\o #\o) "text: what tool encoded this file")
  69. (defconstant +itunes-track+ (mk-mp4-atom-type #xa9 #\t #\r #\k) "octet: track number")
  70. (defconstant +itunes-track-n+ (mk-mp4-atom-type #\t #\r #\k #\n) "octet: yet another track number")
  71. (defconstant +itunes-writer+ (mk-mp4-atom-type #xa9 #\w #\r #\t) "text: who wrote the song")
  72. (defconstant +itunes-year+ (mk-mp4-atom-type #xa9 #\d #\a #\y) "text: year album was released")
  73. (defconstant +itunes-ilst-data+ (mk-mp4-atom-type #\d #\a #\t #\a) "Carries the actual data under an ilst atom")
  74. (defconstant +m4-ftyp+ (mk-mp4-atom-type #\f #\t #\y #\p) "This should be the first atom type found in file")
  75. (defconstant +audioprop-hdlr+ (mk-mp4-atom-type #\h #\d #\l #\r) "Found under trak.mdia and tells what kind of handler it is")
  76. (defconstant +audioprop-mdhd+ (mk-mp4-atom-type #\m #\d #\h #\d) "Found under trak.mdia and holds data to calculate length of audio")
  77. (defconstant +audioprop-stsd+ (mk-mp4-atom-type #\s #\t #\s #\d) "Container atom: found under trak.mdia.minf.stbl and holds bit-rate, etc")
  78. (defconstant +audioprop-mp4a+ (mk-mp4-atom-type #\m #\p #\4 #\a) "Found under trak.mdia.minf.stbl")
  79. (defconstant +audioprop-esds+ (mk-mp4-atom-type #\e #\s #\d #\s) "Found under trak.mdia.minf.stbl.mp4a")
  80. (defconstant +mp4-atom-ilst+ (mk-mp4-atom-type #\i #\l #\s #\t))
  81. (defconstant +mp4-atom-mdia+ (mk-mp4-atom-type #\m #\d #\i #\a))
  82. (defconstant +mp4-atom-meta+ (mk-mp4-atom-type #\m #\e #\t #\a))
  83. (defconstant +mp4-atom-minf+ (mk-mp4-atom-type #\m #\i #\n #\f))
  84. (defconstant +mp4-atom-moov+ (mk-mp4-atom-type #\m #\o #\o #\v))
  85. (defconstant +mp4-atom-stbl+ (mk-mp4-atom-type #\s #\t #\b #\l))
  86. (defconstant +mp4-atom-trak+ (mk-mp4-atom-type #\t #\r #\a #\k))
  87. (defconstant +mp4-atom-udta+ (mk-mp4-atom-type #\u #\d #\t #\a))
  88. ;; (defun atom-read-loop (mp4-file end func)
  89. ;; "Loop from start to end through a file and call FUNC for every ATOM we find. Used
  90. ;; at top-level and also for container ATOMs that need to read their contents."
  91. ;; (declare #.utils:*standard-optimize-settings*)
  92. ;; (log5:with-context "atom-read-loop"
  93. ;; (do ()
  94. ;; ((>= (stream-seek mp4-file) end))
  95. ;; (log-mp4-atom "atom-read-loop: @~:d before dispatch" (stream-seek mp4-file))
  96. ;; (funcall func)
  97. ;; (log-mp4-atom "atom-read-loop: @~:d after dispatch" (stream-seek mp4-file)))))
  98. (defclass mp4-atom ()
  99. ((atom-file-position :accessor atom-file-position :initarg :atom-file-position)
  100. (atom-size :accessor atom-size :initarg :atom-size)
  101. (atom-type :accessor atom-type :initarg :atom-type)
  102. (atom-children :accessor atom-children :initform nil))
  103. (:documentation "The minimal mp4-atom. Note: not all atoms have children, but we put them here anyway to make things 'simple'"))
  104. (defmethod addc ((me mp4-atom) value)
  105. "Want to add children atoms to end of ATOM-CHILDREN to preserve in-file order."
  106. (declare #.utils:*standard-optimize-settings*)
  107. (with-slots (atom-children) me
  108. (if (null atom-children)
  109. (setf atom-children (list value))
  110. (nconc atom-children (list value)))))
  111. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Concreate atoms ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  112. (defclass atom-skip (mp4-atom) ())
  113. (defmethod initialize-instance :after ((me atom-skip) &key (mp4-file nil) &allow-other-keys)
  114. "The 'skip' atom. Used when we want to capture the header of atom, but don't want/need
  115. to read the payload of an atom."
  116. (declare #.utils:*standard-optimize-settings*)
  117. (with-slots (atom-size atom-type) me
  118. (stream-seek mp4-file (- atom-size 8) :current)))
  119. (defclass atom-raw-mixin ()
  120. ((raw-data :accessor raw-data)))
  121. (defmethod initialize-instance :after ((me atom-raw-mixin) &key (mp4-file nil) &allow-other-keys)
  122. "The 'don't need to know contents, but want 'blob' of data read in' atom"
  123. (declare #.utils:*standard-optimize-settings*)
  124. (log5:with-context "atom-raw-mixin"
  125. (with-slots (raw-data atom-type atom-size) me
  126. (log-mp4-atom "atom-raw-mixin: reading in ~d raw bytes for ~a" (- atom-size 8) (vpprint me nil))
  127. (setf raw-data (stream-read-sequence mp4-file (- atom-size 8)))
  128. ;;(utils:dump-data "/tmp/o.txt" raw-data)
  129. )))
  130. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ILST ATOMS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  131. (defclass atom-ilst (mp4-atom) ())
  132. (defmethod initialize-instance :after ((me atom-ilst) &key (mp4-file nil) &allow-other-keys)
  133. "Construct an ilst atom. ILST atoms are containers that hold data elements related to tagging.
  134. Loop through this container and construct constituent atoms"
  135. (declare #.utils:*standard-optimize-settings*)
  136. (log5:with-context "atom-ilst-initializer"
  137. (with-slots (atom-size atom-type atom-children) me
  138. (log-mp4-atom "atom-ilst-init: found ilst atom <~a> @ ~:d, looping for ~:d bytes"
  139. (as-string atom-type) (stream-seek mp4-file) (- atom-size 8))
  140. (let ((end (+ (stream-seek mp4-file) (- atom-size 8))))
  141. (loop for current = (stream-seek mp4-file) then (stream-seek mp4-file)
  142. while (< current end) do
  143. (log-mp4-atom "at ~:d:~:d~%" current end)
  144. (addc me (make-mp4-atom mp4-file atom-type)))))))
  145. (defclass atom-©alb (atom-ilst) ())
  146. (defclass atom-aART (atom-ilst) ())
  147. (defclass atom-©art (atom-ilst) ())
  148. (defclass atom-©cmt (atom-ilst) ())
  149. (defclass atom-cpil (atom-ilst) ())
  150. (defclass atom-©com (atom-ilst) ())
  151. (defclass atom-cprt (atom-ilst) ())
  152. (defclass atom-covr (atom-ilst) ())
  153. (defclass atom-disk (atom-ilst) ())
  154. (defclass atom-©enc (atom-ilst) ())
  155. (defclass atom-gnre (atom-ilst) ())
  156. (defclass atom-©nre (atom-ilst) ())
  157. (defclass atom-©grp (atom-ilst) ())
  158. (defclass atom-©lyr (atom-ilst) ())
  159. (defclass atom-purd (atom-ilst) ())
  160. (defclass atom-tmpo (atom-ilst) ())
  161. (defclass atom-©nam (atom-ilst) ())
  162. (defclass atom-©too (atom-ilst) ())
  163. (defclass atom-©trk (atom-ilst) ())
  164. (defclass atom-trkn (atom-ilst) ())
  165. (defclass atom-©wrt (atom-ilst) ())
  166. (defclass atom-©day (atom-ilst) ())
  167. (defclass atom-data (mp4-atom)
  168. ((atom-version :accessor atom-version :initarg :atom-version)
  169. (atom-flags :accessor atom-flags :initarg :atom-flags)
  170. (atom-value :accessor atom-value :initarg :atom-value)
  171. (atom-parent-type :accessor atom-parent-type :initarg :atom-parent-type :initform nil))
  172. (:documentation "Represents the 'data' portion of ilst data atom"))
  173. (defmethod initialize-instance :after ((me atom-data) &key mp4-file &allow-other-keys)
  174. (declare #.utils:*standard-optimize-settings*)
  175. (log5:with-context "atom-data-init"
  176. (with-slots (atom-size atom-type atom-version atom-flags atom-value atom-parent-type) me
  177. (setf atom-version (stream-read-u8 mp4-file)
  178. atom-flags (stream-read-u24 mp4-file))
  179. (assert (= 0 (stream-read-u32 mp4-file)) () "a data atom lacks the required null field") ; XXX is this true?
  180. (log-mp4-atom "atom-data-init: size = ~:d, name = ~a, version = ~d, flags = ~x" atom-size (as-string atom-type) atom-version atom-flags)
  181. (setf atom-value (decode-ilst-data-atom atom-type me atom-parent-type mp4-file))
  182. (log-mp4-atom "atom-data-init: made an ilst atom-data: ~a" (vpprint me nil)))))
  183. ;;; the ILST atom decoders. First, a lot of the decoders do the same thing, so we define a macros
  184. ;;; and use those for the relevants atoms.
  185. (defgeneric decode-ilst-data-atom (type atom atom-parent-type mp4-file))
  186. ;;; Quicktime spec says strings are stored as UTF-8...
  187. (defmacro simple-text-decode (type)
  188. `(defmethod decode-ilst-data-atom ((type (eql +itunes-ilst-data+)) atom (atom-parent-type (eql ,type)) mp4-file)
  189. (stream-read-utf-8-string-with-len mp4-file (- (atom-size atom) 16))))
  190. (simple-text-decode +itunes-album+)
  191. (simple-text-decode +itunes-album-artist+)
  192. (simple-text-decode +itunes-artist+)
  193. (simple-text-decode +itunes-comment+)
  194. (simple-text-decode +itunes-composer+)
  195. (simple-text-decode +itunes-copyright+)
  196. (simple-text-decode +itunes-year+)
  197. (simple-text-decode +itunes-encoder+)
  198. (simple-text-decode +itunes-groups+)
  199. (simple-text-decode +itunes-genre-x+)
  200. (simple-text-decode +itunes-lyrics+)
  201. (simple-text-decode +itunes-purchased-date+)
  202. (simple-text-decode +itunes-title+)
  203. (simple-text-decode +itunes-tool+)
  204. (simple-text-decode +itunes-writer+)
  205. ;;; for reasons I'm not clear on, there may or may not be extra bytes after the data in these atoms
  206. ;;; hence, the seek at the end to get us by any unread bytes.
  207. (defmacro simple-a-b-decode (type)
  208. `(defmethod decode-ilst-data-atom ((type (eql +itunes-ilst-data+)) atom (atom-parent-type (eql ,type)) mp4-file)
  209. (let ((tmp (stream-read-u16 mp4-file)))
  210. (declare (ignore tmp)))
  211. ;(format t "ilist decode, parent = ~a: ~x~%" (as-string atom-parent-type) tmp))
  212. (let ((a) (b))
  213. (setf a (stream-read-u16 mp4-file)
  214. b (stream-read-u16 mp4-file))
  215. (stream-seek mp4-file (- (atom-size atom) 16 6) :current) ; seek to end of atom: 16 == header; 4 is a, b, skip read above
  216. (list a b))))
  217. (simple-a-b-decode +itunes-track+)
  218. (simple-a-b-decode +itunes-track-n+)
  219. (simple-a-b-decode +itunes-disk+)
  220. (defmacro simple-u16-decode (type)
  221. `(defmethod decode-ilst-data-atom ((type (eql +itunes-ilst-data+)) atom (atom-parent-type (eql ,type)) mp4-file)
  222. (declare (ignore atom))
  223. (stream-read-u16 mp4-file)))
  224. (simple-u16-decode +itunes-tempo+)
  225. (simple-u16-decode +itunes-genre+)
  226. (defmethod decode-ilst-data-atom ((type (eql +itunes-ilst-data+)) atom (atom-parent-type (eql +itunes-compilation+)) mp4-file)
  227. (declare (ignore atom))
  228. (stream-read-u8 mp4-file))
  229. (defmethod decode-ilst-data-atom ((type (eql +itunes-ilst-data+)) atom (atom-parent-type (eql +itunes-cover-art+)) mp4-file)
  230. (stream-read-sequence mp4-file (- (atom-size atom) 16)))
  231. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; AUDIO PROPERTY ATOMS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  232. ;;; the pure container classes we need recurse into
  233. (defclass atom-trak (mp4-atom) ())
  234. (defmethod initialize-instance :after ((me atom-trak) &key (mp4-file nil) &allow-other-keys)
  235. (read-container-atoms mp4-file me))
  236. (defclass atom-minf (mp4-atom) ())
  237. (defmethod initialize-instance :after ((me atom-minf) &key (mp4-file nil) &allow-other-keys)
  238. (read-container-atoms mp4-file me))
  239. (defclass atom-moov (mp4-atom) ())
  240. (defmethod initialize-instance :after ((me atom-moov) &key (mp4-file nil) &allow-other-keys)
  241. (read-container-atoms mp4-file me))
  242. (defclass atom-udta (mp4-atom) ())
  243. (defmethod initialize-instance :after ((me atom-udta) &key (mp4-file nil) &allow-other-keys)
  244. (read-container-atoms mp4-file me))
  245. (defclass atom-mdia (mp4-atom) ())
  246. (defmethod initialize-instance :after ((me atom-mdia) &key (mp4-file nil) &allow-other-keys)
  247. (read-container-atoms mp4-file me))
  248. (defclass atom-stbl (mp4-atom) ())
  249. (defmethod initialize-instance :after ((me atom-stbl) &key (mp4-file nil) &allow-other-keys)
  250. (read-container-atoms mp4-file me))
  251. (defclass atom-hdlr (mp4-atom)
  252. ((version :accessor version) ; 1 byte
  253. (flags :accessor flags) ; 3 bytes
  254. (qtype :accessor qtype) ; 4 bytes
  255. (mtype :accessor mtype) ; 4 bytes
  256. (resv :accessor resv) ; 4 bytes
  257. (rflag :accessor rflag) ; 4 bytes
  258. (rmask :accessor rmask) ; 4 bytes
  259. (mhdlr :accessor mhdlr))) ; null-terminated string (but we're reading it as octets)
  260. (defmethod initialize-instance :after ((me atom-hdlr) &key (mp4-file nil) &allow-other-keys)
  261. (with-slots (version flags qtype mtype resv rflag rmask mhdlr atom-size) me
  262. (setf version (stream-read-u8 mp4-file)
  263. flags (stream-read-u24 mp4-file)
  264. qtype (stream-read-u32 mp4-file)
  265. mtype (stream-read-u32 mp4-file)
  266. resv (stream-read-u32 mp4-file)
  267. rflag (stream-read-u32 mp4-file)
  268. rmask (stream-read-u32 mp4-file)
  269. mhdlr (stream-read-sequence mp4-file (- atom-size 32))))) ; 32 is 8-bytes of header plus fields above
  270. (defclass atom-mdhd (mp4-atom)
  271. ((version :accessor version)
  272. (flags :accessor flags)
  273. (c-time :accessor c-time)
  274. (m-time :accessor m-time)
  275. (scale :accessor scale)
  276. (duration :accessor duration)
  277. (lang :accessor lang)
  278. (quality :accessor quality)))
  279. (defmethod initialize-instance :after ((me atom-mdhd) &key (mp4-file nil) &allow-other-keys)
  280. (declare #.utils:*standard-optimize-settings*)
  281. (with-slots (version flags c-time m-time scale duration lang quality) me
  282. (setf version (stream-read-u8 mp4-file)
  283. flags (stream-read-u24 mp4-file)
  284. c-time (stream-read-u32 mp4-file)
  285. m-time (stream-read-u32 mp4-file)
  286. scale (stream-read-u32 mp4-file)
  287. duration (if (= 0 version) (stream-read-u32 mp4-file) (stream-read-u64 mp4-file))
  288. lang (stream-read-u16 mp4-file)
  289. quality (stream-read-u16 mp4-file))))
  290. (defclass atom-esds (mp4-atom)
  291. ((version :accessor version) ; 1 byte
  292. (flags :accessor flags) ; 3 bytes
  293. (esid :accessor esid) ; 2 bytes
  294. (s-priority :accessor s-priority) ; 1 byte
  295. (obj-id :accessor obj-id) ; 1 byte
  296. (s-type :accessor s-type) ; 1 byte (1 bit up-stream, 1-but reservered, 6-bits stream type
  297. (buf-size :accessor buf-size) ; 3 bytes
  298. (max-bit-rate :accessor max-bit-rate) ; 4 bytes
  299. (avg-bit-rate :accessor avg-bit-rate)) ; 4 bytes
  300. (:documentation "XXX-partial definition for Elementary Stream Descriptors (ESDs)"))
  301. ;;; 3 bytes extended descriptor type tag string = 3 * 8-bit hex value
  302. ;;; types are Start = 0x80 ; End = 0xFE
  303. ;;; then, one byte of length
  304. ;;; Note: start types are optional
  305. (defun read-descriptor-len (instream)
  306. "Get the ES descriptor's length."
  307. (declare #.utils:*standard-optimize-settings*)
  308. (let* ((tmp (stream-read-u8 instream))
  309. (len (logand tmp #x7f)))
  310. (declare (type (unsigned-byte 8) tmp))
  311. (while (not (zerop (logand #x80 tmp)))
  312. (setf tmp (stream-read-u8 instream)
  313. len (logior (ash len 7) (logand tmp #x7f))))
  314. len))
  315. ;;; one-byte descriptor tags
  316. (defconstant +mp4-odescrtag+ #x01)
  317. (defconstant +mp4-iodescrtag+ #x02)
  318. (defconstant +mp4-esdescrtag+ #x03)
  319. (defconstant +mp4-decconfigdescrtag+ #x04)
  320. (defconstant +mp4-decspecificdescrtag+ #x05)
  321. (defconstant +mp4-slconfigdescrtag+ #x06)
  322. (defconstant +mp4-contentiddescrtag+ #x07)
  323. (defconstant +mp4-supplcontentiddescrtag+ #x08)
  324. (defconstant +mp4-ipiptrdescrtag+ #x09)
  325. (defconstant +mp4-ipmpptrdescrtag+ #x0a)
  326. (defconstant +mp4-ipmpdescrtag+ #x0b)
  327. (defconstant +mp4-registrationdescrtag+ #x0d)
  328. (defconstant +mp4-esidincdescrtag+ #x0e)
  329. (defconstant +mp4-esidrefdescrtag+ #x0f)
  330. (defconstant +mp4-fileiodescrtag+ #x10)
  331. (defconstant +mp4-fileodescrtag+ #x11)
  332. (defconstant +mp4-extprofileleveldescrtag+ #x13)
  333. (defconstant +mp4-extdescrtagsstart+ #x80)
  334. (defconstant +mp4-extdescrtagsend+ #xfe)
  335. (defmethod initialize-instance :after ((me atom-esds) &key (mp4-file nil) &allow-other-keys)
  336. (declare #.utils:*standard-optimize-settings*)
  337. (with-slots (version flags esid s-priority obj-id s-type buf-size max-bit-rate avg-bit-rate) me
  338. (setf version (stream-read-u8 mp4-file)
  339. flags (stream-read-u24 mp4-file))
  340. (assert (= +MP4-ESDescrTag+ (stream-read-u8 mp4-file)) () "Expected description tag of ESDescrTag")
  341. (let* ((len (read-descriptor-len mp4-file))
  342. (end-of-atom (+ (stream-seek mp4-file) len)))
  343. (setf esid (stream-read-u16 mp4-file)
  344. s-priority (stream-read-u8 mp4-file))
  345. (assert (= +MP4-DecConfigDescrTag+ (stream-read-u8 mp4-file)) () "Expected tag type of DecConfigDescrTag")
  346. (setf len (read-descriptor-len mp4-file)
  347. obj-id (stream-read-u8 mp4-file)
  348. s-type (stream-read-u8 mp4-file)
  349. buf-size (stream-read-u24 mp4-file)
  350. max-bit-rate (stream-read-u32 mp4-file)
  351. avg-bit-rate (stream-read-u32 mp4-file))
  352. ;; Should do checking here and/or read rest of atom,
  353. ;; but for now, we have what we want, so just seek to end of atom
  354. (stream-seek mp4-file end-of-atom :start))))
  355. (defclass atom-stsd (mp4-atom)
  356. ((flags :accessor flags)
  357. (version :accessor version)
  358. (num-entries :accessor num-entries)))
  359. (defmethod initialize-instance :after ((me atom-stsd) &key (mp4-file nil) &allow-other-keys)
  360. (declare #.utils:*standard-optimize-settings*)
  361. (log5:with-context "atom-stsd"
  362. (with-slots (flags version num-entries) me
  363. (setf version (stream-read-u8 mp4-file)
  364. flags (stream-read-u24 mp4-file)
  365. num-entries (stream-read-u32 mp4-file))
  366. (log-mp4-atom "atom-stsd: version = ~d, flags = ~x, num-fields = ~d" version flags num-entries))))
  367. (defclass atom-mp4a (mp4-atom)
  368. ((reserved :accessor reserved) ; 6 bytes
  369. (d-ref-idx :accessor d-ref-idx) ; 2 bytes
  370. (version :accessor version) ; 2 bytes
  371. (revision :accessor revision) ; 2 bytes
  372. (vendor :accessor vendor) ; 4 bytes
  373. (num-chans :accessor num-chans) ; 2 bytes
  374. (samp-size :accessor samp-size) ; 2 bytes
  375. (comp-id :accessor comp-id) ; 2 bytes
  376. (packet-size :accessor packet-size) ; 2 bytes
  377. (samp-rate :accessor samp-rate))) ; 4 bytes
  378. (defmethod initialize-instance :after ((me atom-mp4a) &key (mp4-file nil) &allow-other-keys)
  379. (declare #.utils:*standard-optimize-settings*)
  380. (log5:with-context "atom-mp4a"
  381. (with-slots (reserved d-ref-idx version revision vendor num-chans samp-size comp-id packet-size samp-rate) me
  382. (setf reserved (stream-read-sequence mp4-file 6)
  383. d-ref-idx (stream-read-u16 mp4-file)
  384. version (stream-read-u16 mp4-file)
  385. revision (stream-read-u16 mp4-file)
  386. vendor (stream-read-u32 mp4-file)
  387. num-chans (stream-read-u16 mp4-file)
  388. samp-size (stream-read-u16 mp4-file)
  389. comp-id (stream-read-u16 mp4-file)
  390. packet-size (stream-read-u16 mp4-file)
  391. samp-rate (stream-read-u32 mp4-file)) ; fixed 16.16 floating point number
  392. (read-container-atoms mp4-file me))))
  393. (defun read-container-atoms (mp4-file parent-atom)
  394. "Loop through a container atom and add it's children to it"
  395. (declare #.utils:*standard-optimize-settings*)
  396. (with-slots (atom-children atom-file-position atom-of-interest atom-size atom-type atom-decoded) parent-atom
  397. (let ((end (+ atom-file-position atom-size)))
  398. (loop for current = (stream-seek mp4-file) then (stream-seek mp4-file)
  399. while (< current end) do
  400. (let ((child (make-mp4-atom mp4-file atom-type)))
  401. (log-mp4-atom "read-container-atoms: adding new child ~a" (vpprint child nil))
  402. (addc parent-atom child))))))
  403. (defclass atom-meta (mp4-atom)
  404. ((version :accessor version)
  405. (flags :accessor flags)))
  406. (defmethod initialize-instance :after ((me atom-meta) &key (mp4-file nil) &allow-other-keys)
  407. (declare #.utils:*standard-optimize-settings*)
  408. (with-slots (version flags) me
  409. (setf version (stream-read-u8 mp4-file)
  410. flags (stream-read-u24 mp4-file))
  411. (read-container-atoms mp4-file me)))
  412. (defun find-atom-class (id)
  413. "Search by concatenating 'atom-' with ID and look for that symbol in this package"
  414. (declare #.utils:*standard-optimize-settings*)
  415. (log5:with-context "find-atom-class"
  416. (log-mp4-atom "find-atom-class: looking for class <~a>" (as-string id))
  417. (let ((found-class-symbol (find-symbol (mk-atom-class-name id) :MP4-ATOM))
  418. (found-class))
  419. ;; if we found the class name, return the class (to be used for MAKE-INSTANCE)
  420. (when found-class-symbol
  421. (setf found-class (find-class found-class-symbol))
  422. (log-mp4-atom "find-atom-class: found class: ~a" found-class)
  423. (return-from find-atom-class (find-class found-class-symbol))))
  424. ;; didn't find a class, so return ATOM-SKIP class
  425. (log-mp4-atom "find-atom-class: class not found")
  426. 'atom-skip))
  427. (defun make-mp4-atom (mp4-file &optional parent-type)
  428. "Get current file position, read in size/type, then construct the correct atom."
  429. (declare #.utils:*standard-optimize-settings*)
  430. (log5:with-context "make-mp4-atom"
  431. (let* ((pos (stream-seek mp4-file))
  432. (siz (stream-read-u32 mp4-file))
  433. (typ (stream-read-u32 mp4-file))
  434. (atom))
  435. (declare (type integer pos siz typ))
  436. (log-mp4-atom "make-mp4-atom: @ pos = ~:d of size = ~:d and type = ~a" pos siz (as-string typ))
  437. (when (= 0 siz)
  438. (error "trying to make an atom ~a with size of 0 at offset ~:d in file ~a"
  439. (as-string typ) pos (stream-filename mp4-file)))
  440. (setf atom (make-instance (find-atom-class typ) :atom-size siz :atom-type typ :atom-file-position pos :mp4-file mp4-file :atom-parent-type parent-type))
  441. (log-mp4-atom "make-mp4-atom: made ~a" (vpprint atom nil))
  442. atom)))
  443. (defmethod vpprint ((me mp4-atom) stream)
  444. (format stream "~a" (with-output-to-string (s)
  445. (with-slots (atom-children atom-file-position atom-size atom-type) me
  446. (format s "ATOM: type: <~a> @ ~:d of size ~:d and child count of ~d"
  447. (as-string atom-type) atom-file-position atom-size (length atom-children)))
  448. (if (typep me 'atom-data)
  449. (with-slots (atom-version atom-flags atom-value atom-type atom-parent-type) me
  450. (format s " having ilst fields: atom-parent-type = ~a, verison = ~d, flags = ~x, data = ~x"
  451. (as-string atom-parent-type) atom-version atom-flags
  452. (if (typep atom-value 'array) (printable-array atom-value) atom-value)))))))
  453. (defun is-valid-m4-file (mp4-file)
  454. "Make sure this is an MP4 file. Quick check: is first atom (at file-offset 4) == FSTYP?
  455. Written in this fashion so as to be 'crash-proof' when passed an arbitrary file."
  456. (declare #.utils:*standard-optimize-settings*)
  457. (let ((valid)
  458. (size)
  459. (header))
  460. (when (> (stream-size mp4-file) 8)
  461. (unwind-protect
  462. (handler-case
  463. (progn
  464. (stream-seek mp4-file 0 :start)
  465. (setf size (stream-read-u32 mp4-file)
  466. header (stream-read-u32 mp4-file)
  467. valid (and (<= size (stream-size mp4-file))
  468. (= header +m4-ftyp+))))
  469. (condition (c)
  470. (utils:warn-user "File:~a~%is-valid-mp4-file got condition ~a" (stream-filename mp4-file) c)))
  471. (stream-seek mp4-file 0 :start)))
  472. valid))
  473. (defmethod find-mp4-atoms ((mp4-file mp4-file-stream))
  474. "Given a valid MP4 file MP4-FILE, look for the 'right' atoms and return them."
  475. (declare #.utils:*standard-optimize-settings*)
  476. (log5:with-context "find-mp4-atoms"
  477. (log-mp4-atom "find-mp4-atoms: ~a, before read-file loop, file-position = ~:d, end = ~:d"
  478. (stream-filename mp4-file) (stream-seek mp4-file) (stream-size mp4-file))
  479. (let ((atoms)
  480. (end (stream-size mp4-file)))
  481. (loop for current = (stream-seek mp4-file) then (stream-seek mp4-file)
  482. while (< current end) do
  483. (let ((new-atom (make-mp4-atom mp4-file)))
  484. (when new-atom
  485. (push new-atom atoms))))
  486. (setf (mp4-atoms mp4-file) (nreverse atoms))) ; preserve in-file-order
  487. (log-mp4-atom "find-mp4-atoms: returning list of size ~d" (length (mp4-atoms mp4-file)))))
  488. (defmethod map-mp4-atom ((atoms list) &key (func nil) (depth nil))
  489. "Given a list of atoms, call map-mp4-atom for each one"
  490. (declare #.utils:*standard-optimize-settings*)
  491. (log5:with-context "map-mp4-atom"
  492. (dolist (a atoms)
  493. (map-mp4-atom a :func func :depth depth))))
  494. (defmethod map-mp4-atom ((me mp4-atom) &key (func nil) (depth nil))
  495. "Traverse all atoms under a given atom"
  496. (declare #.utils:*standard-optimize-settings*)
  497. (log5:with-context "map-mp4-atom(single)"
  498. (labels ((_indented-atom (atom depth)
  499. (format t "~vt~a~%" (if (null depth) 0 depth) (vpprint atom nil))))
  500. (with-slots (atom-type atom-children) me
  501. (log-mp4-atom "map-mp4-atom: begining traversal with ~a, I have ~d children" (as-string atom-type) (length atom-children))
  502. (when (null func)
  503. (setf func #'_indented-atom))
  504. (funcall func me depth)
  505. (map-mp4-atom atom-children :func func :depth (if (null depth) nil (+ 1 depth)))))))
  506. (defmethod traverse ((me mp4-atom) path)
  507. (declare #.utils:*standard-optimize-settings*)
  508. (traverse (atom-children me) path))
  509. (defmethod traverse ((me list) path)
  510. "Used in finding nested atoms. Search MP4-ATOMS and if we find a match with first of path,
  511. call traverse atom (unless length of path == 1, in which case, we've found our match)"
  512. (declare #.utils:*standard-optimize-settings*)
  513. (log5:with-context "traverse"
  514. (log-mp4-atom "traverse: entering with ~a ~a" me path)
  515. (dolist (sibling me)
  516. (with-slots (atom-type atom-children) sibling
  517. (log-mp4-atom "traverse: comparing ~a to ~a" (as-string atom-type) (as-string (first path)))
  518. (when (= atom-type (first path))
  519. (cond
  520. ((= 1 (length path))
  521. (log-mp4-atom "traverse: matched: ~a" sibling)
  522. (return-from traverse sibling))
  523. (t
  524. (log-mp4-atom "traverse: path matches, recursing")
  525. (let ((found (traverse atom-children (rest path))))
  526. (if found (return-from traverse found))))))))
  527. (log-mp4-atom "traverse: ~a not found" path)
  528. nil))
  529. (defmethod tag-get-value (atoms node)
  530. "Helper function to extract text from ILST atom's data atom"
  531. (declare #.utils:*standard-optimize-settings*)
  532. (aif (traverse atoms
  533. (list +mp4-atom-moov+ +mp4-atom-udta+ +mp4-atom-meta+ +mp4-atom-ilst+ node +itunes-ilst-data+))
  534. (atom-value it)
  535. nil))
  536. (defun mp4-show-raw-tag-atoms (mp4-file-stream out-stream)
  537. (declare #.utils:*standard-optimize-settings*)
  538. (map-mp4-atom (traverse (mp4-atoms mp4-file-stream)
  539. (list +mp4-atom-moov+ +mp4-atom-udta+ +mp4-atom-meta+ +mp4-atom-ilst+))
  540. :depth 0
  541. :func (lambda (atom depth)
  542. (when (= (atom-type atom) +itunes-ilst-data+)
  543. (format out-stream "~vt~a~%" depth (vpprint atom nil))))))
  544. (defun find-all (base name)
  545. "Starting as BASE atom, recursively search for all instances of NAME"
  546. (declare #.utils:*standard-optimize-settings*)
  547. (let* ((search-name (if (typep name 'string) (as-int name) name))
  548. (found))
  549. (map-mp4-atom base
  550. :func (lambda (atom depth)
  551. (declare (ignore depth))
  552. (when (= (atom-type atom) search-name)
  553. (push atom found))))
  554. (nreverse found)))
  555. (defun get-audio-properties-atoms (mp4-file)
  556. "First, find all TRAKs under moov. For the one that contains a HDLR atom with DATA of 'soun',
  557. return trak.mdia.mdhd and trak.mdia.minf.stbl.stsd"
  558. (declare #.utils:*standard-optimize-settings*)
  559. (dolist (track (find-all (traverse (mp4-atoms mp4-file) (list +mp4-atom-moov+)) "trak"))
  560. (let ((hdlr (traverse track (list +mp4-atom-mdia+ +audioprop-hdlr+))))
  561. (when (and (not (null hdlr))
  562. (not (null (mtype hdlr)))
  563. (string= "soun" (as-string (mtype hdlr))))
  564. ;; we've found the correct track, extract atoms
  565. (return-from get-audio-properties-atoms (values (traverse track (list +mp4-atom-mdia+ +audioprop-mdhd+))
  566. (traverse track (list +mp4-atom-mdia+ +mp4-atom-minf+ +mp4-atom-stbl+ +audioprop-mp4a+))
  567. (traverse track (list +mp4-atom-mdia+ +mp4-atom-minf+ +mp4-atom-stbl+ +audioprop-mp4a+ +audioprop-esds+)))))))
  568. nil)
  569. (defclass audio-info ()
  570. ((seconds :accessor seconds :initform nil)
  571. (channels :accessor channels :initform nil)
  572. (bits-per-sample :accessor bits-per-sample :initform nil)
  573. (sample-rate :accessor sample-rate :initform nil)
  574. (max-bit-rate :accessor max-bit-rate :initform nil)
  575. (avg-bit-rate :accessor avg-bit-rate :initform nil))
  576. (:documentation "Holds extracted audio information about an MP4 file."))
  577. (defmethod vpprint ((me audio-info) stream)
  578. (with-slots (seconds channels bits-per-sample sample-rate max-bit-rate avg-bit-rate) me
  579. (format stream "sample rate: ~:d Hz, # channels: ~d, bits-per-sample: ~:d, max bit-rate: ~:d Kbps, avg bit-rate: ~:d Kbps, duration: ~:d:~2,'0d"
  580. (if sample-rate sample-rate 0)
  581. (if channels channels 0)
  582. (if bits-per-sample bits-per-sample 0)
  583. (if max-bit-rate (round (/ max-bit-rate 1000)) 0)
  584. (if avg-bit-rate (round (/ avg-bit-rate 1000)) 0)
  585. (if seconds (floor (/ seconds 60)) 0)
  586. (if seconds (round (mod seconds 60)) 0))))
  587. (defun get-mp4-audio-info (mp4-file)
  588. "MP4A audio info is held in under the trak.mdia.mdhd/trak.mdia.minf.stbl/trak.mdia.minf.stbl.mp4a atoms."
  589. (declare #.utils:*standard-optimize-settings*)
  590. (let ((info (make-instance 'audio-info)))
  591. (multiple-value-bind (mdhd mp4a esds) (get-audio-properties-atoms mp4-file)
  592. (with-slots (seconds channels bits-per-sample sample-rate max-bit-rate avg-bit-rate) info
  593. (when mdhd
  594. (setf seconds (/ (float (duration mdhd)) (float (scale mdhd)))))
  595. (when mp4a
  596. (setf channels (num-chans mp4a)
  597. bits-per-sample (samp-size mp4a))
  598. (let* ((upper (ash (samp-rate mp4a) -16))
  599. (lower (logand (samp-rate mp4a) #xffff)))
  600. (setf sample-rate (+ (float upper) (/ (float lower) 1000))))
  601. (when esds
  602. (setf avg-bit-rate (avg-bit-rate esds)
  603. max-bit-rate (max-bit-rate esds))))))
  604. info))