mp4-atom.lisp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  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. (define-condition mp4-atom-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 "mp4-atom condition at location: <~a> with object: <~a>: message: <~a>"
  12. (location condition) (object condition) (message condition)))))
  13. (defmethod print-object ((me mp4-atom-condition) stream)
  14. (format stream "location: <~a>, object: <~a>, message: <~a>" (location me) (object me) (message me)))
  15. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ATOMS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  16. ;;;
  17. ;;; A word about atoms (aka "boxes"). There are three kinds of atoms: ones that are containers, ones
  18. ;;; that are data, and ones that are both. A lot of the source code for taggers out there mostly ignore
  19. ;;; the third class and treat "container atoms that also have data" as a big blob of data that they
  20. ;;; rummage around in via indices. Seems sort of broke, IMHO, so we'll try to handle all three if
  21. ;;; at all possible.
  22. ;;;
  23. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  24. (defun as-int (str)
  25. "Given a 4-byte string, return an integer type equivalent.
  26. (eg (as-int \"hdlr\" == +audioprop-hdlr+))"
  27. (let ((int 0))
  28. (declare (integer int))
  29. (setf (ldb (byte 8 24) int) (char-code (aref str 0)))
  30. (setf (ldb (byte 8 16) int) (char-code (aref str 1)))
  31. (setf (ldb (byte 8 8) int) (char-code (aref str 2)))
  32. (setf (ldb (byte 8 0) int) (char-code (aref str 3)))
  33. int))
  34. (defmethod as-string ((atom-type integer))
  35. "Given an integer representing an atom type, return the string form"
  36. (with-output-to-string (s nil)
  37. (write-char (code-char (ldb (byte 8 24) atom-type)) s)
  38. (write-char (code-char (ldb (byte 8 16) atom-type)) s)
  39. (write-char (code-char (ldb (byte 8 8) atom-type)) s)
  40. (write-char (code-char (ldb (byte 8 0) atom-type)) s)))
  41. (eval-when (:compile-toplevel :load-toplevel :execute)
  42. (defun as-octet (c)
  43. "Used below so that we can create atom 'types' from char/ints"
  44. (cond ((typep c 'standard-char) (coerce (char-code c) '(unsigned-byte 8)))
  45. ((typep c 'integer) (coerce c '(unsigned-byte 8)))
  46. (t (error "can any handle characters and integers"))))
  47. (defmacro mk-mp4-atom-type (l1 l2 l3 l4)
  48. "Given 4 chars/ints, create a 32-bit word representing an atom 'type' (aka name)"
  49. `(let ((retval 0))
  50. (setf (ldb (byte 8 24) retval) ,(as-octet l1))
  51. (setf (ldb (byte 8 16) retval) ,(as-octet l2))
  52. (setf (ldb (byte 8 8) retval) ,(as-octet l3))
  53. (setf (ldb (byte 8 0) retval) ,(as-octet l4))
  54. retval)))
  55. (defconstant +itunes-album+ (mk-mp4-atom-type #xa9 #\a #\l #\b) "text: album name")
  56. (defconstant +itunes-album-artist+ (mk-mp4-atom-type #\a #\A #\R #\T) "text: album artist")
  57. (defconstant +itunes-artist+ (mk-mp4-atom-type #xa9 #\A #\R #\T) "text: artist name")
  58. (defconstant +itunes-comment+ (mk-mp4-atom-type #xa9 #\c #\m #\t) "text: comment, commonly used by iTunes for sound info, etc")
  59. (defconstant +itunes-compilation+ (mk-mp4-atom-type #\c #\p #\i #\l) "byte/boolean: is this file part of a compilation?")
  60. (defconstant +itunes-composer+ (mk-mp4-atom-type #xa9 #\c #\o #\m) "text: composer name")
  61. (defconstant +itunes-copyright+ (mk-mp4-atom-type #\c #\p #\r #\t) "text: copyright info")
  62. (defconstant +itunes-cover-art+ (mk-mp4-atom-type #\c #\o #\v #\r) "octets: cover art, PNG, etc")
  63. (defconstant +itunes-disk+ (mk-mp4-atom-type #\d #\i #\s #\k) "octets: disk number, can be n of N")
  64. (defconstant +itunes-encoder+ (mk-mp4-atom-type #xa9 #\e #\n #\c) "text: who encoded")
  65. (defconstant +itunes-genre+ (mk-mp4-atom-type #\g #\n #\r #\e) "octets: genre of file")
  66. (defconstant +itunes-genre-x+ (mk-mp4-atom-type #xa9 #\n #\r #\e) "text: yet another genre atom")
  67. (defconstant +itunes-groups+ (mk-mp4-atom-type #xa9 #\g #\r #\p) "text: ???")
  68. (defconstant +itunes-lyrics+ (mk-mp4-atom-type #xa9 #\l #\y #\r) "text: lyrics tag")
  69. (defconstant +itunes-purchased-date+ (mk-mp4-atom-type #\p #\u #\r #\d) "text: when song was purchased")
  70. (defconstant +itunes-tempo+ (mk-mp4-atom-type #\t #\m #\p #\o) "octet: tempo of song")
  71. (defconstant +itunes-title+ (mk-mp4-atom-type #xa9 #\n #\a #\m) "text: title of song")
  72. (defconstant +itunes-tool+ (mk-mp4-atom-type #xa9 #\t #\o #\o) "text: what tool encoded this file")
  73. (defconstant +itunes-track+ (mk-mp4-atom-type #xa9 #\t #\r #\k) "octet: track number")
  74. (defconstant +itunes-track-n+ (mk-mp4-atom-type #\t #\r #\k #\n) "octet: yet another track number")
  75. (defconstant +itunes-writer+ (mk-mp4-atom-type #xa9 #\w #\r #\t) "text: who wrote the song")
  76. (defconstant +itunes-year+ (mk-mp4-atom-type #xa9 #\d #\a #\y) "text: year album was released")
  77. (defconstant +itunes-ilst-data+ (mk-mp4-atom-type #\d #\a #\t #\a) "Carries the actual data under an ilst atom")
  78. (defconstant +m4-ftyp+ (mk-mp4-atom-type #\f #\t #\y #\p) "This should be the first atom type found in file")
  79. (defconstant +audioprop-hdlr+ (mk-mp4-atom-type #\h #\d #\l #\r) "Found under trak.mdia and tells what kind of handler it is")
  80. (defconstant +audioprop-mdhd+ (mk-mp4-atom-type #\m #\d #\h #\d) "Found under trak.mdia and holds data to calculate length of audio")
  81. (defconstant +audioprop-stsd+ (mk-mp4-atom-type #\s #\t #\s #\d) "Container atom: found under trak.mdia.minf.stbl and holds bit-rate, etc")
  82. (defconstant +audioprop-mp4a+ (mk-mp4-atom-type #\m #\p #\4 #\a) "Found under trak.mdia.minf.stbl.stsd")
  83. (defconstant +audioprop-essd+ (mk-mp4-atom-type #\e #\s #\s #\d) "Found under trak.mdia.minf.stbl.stsd.mp4a")
  84. (defconstant +mp4-atom-ilst+ (mk-mp4-atom-type #\i #\l #\s #\t))
  85. (defconstant +mp4-atom-mdia+ (mk-mp4-atom-type #\m #\d #\i #\a))
  86. (defconstant +mp4-atom-meta+ (mk-mp4-atom-type #\m #\e #\t #\a))
  87. (defconstant +mp4-atom-minf+ (mk-mp4-atom-type #\m #\i #\n #\f))
  88. (defconstant +mp4-atom-moov+ (mk-mp4-atom-type #\m #\o #\o #\v))
  89. (defconstant +mp4-atom-stbl+ (mk-mp4-atom-type #\s #\t #\b #\l))
  90. (defconstant +mp4-atom-trak+ (mk-mp4-atom-type #\t #\r #\a #\k))
  91. (defconstant +mp4-atom-udta+ (mk-mp4-atom-type #\u #\d #\t #\a))
  92. (defun atom-read-loop (mp4-file end func)
  93. "Loop from start to end through a file and call FUNC for every ATOM we find. Used
  94. at top-level and also for container ATOMs that need to read their contents."
  95. (log5:with-context "atom-read-loop"
  96. (do ()
  97. ((>= (stream-seek mp4-file 0 :current) end))
  98. (log-mp4-atom "atom-read-loop: @~:d before dispatch" (stream-seek mp4-file 0 :current))
  99. (funcall func)
  100. (log-mp4-atom "atom-read-loop: @~:d after dispatch" (stream-seek mp4-file 0 :current)))))
  101. (defclass mp4-atom ()
  102. ((atom-file-position :accessor atom-file-position :initarg :atom-file-position)
  103. (atom-size :accessor atom-size :initarg :atom-size)
  104. (atom-type :accessor atom-type :initarg :atom-type)
  105. (atom-children :accessor atom-children :initform (make-mp4-atom-collection)))
  106. (:documentation "The minimal mp4-atom. Note: not all atoms have children, but we put them here anyway to make things 'simple'"))
  107. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; A collection of atoms (siblings) ;;;;;;;;;;;;;;;;;;;;
  108. (defclass atom-collection ()
  109. ((atoms :accessor atoms :initform nil))
  110. (:documentation "A collection of sibling atoms"))
  111. (defun make-mp4-atom-collection () (make-instance 'atom-collection))
  112. (defmethod add ((me atom-collection) new-atom)
  113. "Adds new atom to the *end* (need to keep them in order we found them in the file) of this collection"
  114. (log5:with-context "add-atom-collection"
  115. (with-slots (atoms) me
  116. ;(log-mp4-atom "adding ~a to atom collection: ~a" new-atom atoms)
  117. (setf atoms (append atoms (list new-atom)))
  118. ;(log-mp4-atom "collection now: ~a" atoms)
  119. )))
  120. (defmethod size ((me atom-collection))
  121. "Returns the number of atoms in this collection"
  122. (length (slot-value me 'atoms)))
  123. (defmethod map-mp4-atom ((me atom-collection) &key (func nil) (depth nil))
  124. "Given a collection of atoms, call map-mp4-atom for each one"
  125. (log5:with-context "map-mp4-atom(collection)"
  126. (log-mp4-atom "map-mp4-atom: mapping collection: ~a" (slot-value me 'atoms))
  127. (dolist (a (slot-value me 'atoms))
  128. (map-mp4-atom a :func func :depth depth))))
  129. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Concreate atoms ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  130. (defclass atom-skip (mp4-atom) ())
  131. (defmethod initialize-instance :after ((me atom-skip) &key (mp4-file nil) &allow-other-keys)
  132. "The 'skip' atom. Used when we want to capture the header of atom, but don't want/need
  133. to read the payload of an atom."
  134. (with-slots (atom-size atom-type) me
  135. (stream-seek mp4-file (- atom-size 8) :current)))
  136. (defclass atom-raw-mixin ()
  137. ((raw-data :accessor raw-data)))
  138. (defmethod initialize-instance :after ((me atom-raw-mixin) &key (mp4-file nil) &allow-other-keys)
  139. "The 'don't need to know contents, but want 'blob' of data read in' atom"
  140. (log5:with-context "atom-raw-mixin"
  141. (with-slots (raw-data atom-type atom-size) me
  142. (log-mp4-atom "atom-raw-mixin: reading in ~d raw bytes for ~a" (- atom-size 8) (vpprint me nil))
  143. (setf raw-data (stream-read-sequence mp4-file (- atom-size 8)))
  144. ;;(utils::dump-data "/tmp/o.txt" raw-data)
  145. )))
  146. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ILST ATOMS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  147. (defclass atom-ilst (mp4-atom) ())
  148. (defmethod initialize-instance :after ((me atom-ilst) &key (mp4-file nil) &allow-other-keys)
  149. "Construct an ilst atom. ILST atoms are containers that hold data elements related to tagging.
  150. Loop through this container and construct constituent atoms"
  151. (log5:with-context "atom-ilst-initializer"
  152. (with-slots (atom-size atom-type atom-children) me
  153. (log-mp4-atom "atom-ilst-init: found ilst atom <~a> @ ~:d, looping for ~:d bytes"
  154. (as-string atom-type) (stream-seek mp4-file 0 :current) (- atom-size 8))
  155. (atom-read-loop mp4-file (+ (stream-seek mp4-file 0 :current) (- atom-size 8))
  156. (lambda ()
  157. (let ((child (make-mp4-atom mp4-file atom-type)))
  158. ;(log-mp4-atom "adding new child ~a" (vpprint child nil))
  159. (add atom-children child))))))
  160. ;(log-mp4-atom "returning ilst atom: ~a" (vpprint me nil))
  161. )
  162. (defclass atom-©alb (atom-ilst) ())
  163. (defclass atom-aART (atom-ilst) ())
  164. (defclass atom-©art (atom-ilst) ())
  165. (defclass atom-©cmt (atom-ilst) ())
  166. (defclass atom-cpil (atom-ilst) ())
  167. (defclass atom-©com (atom-ilst) ())
  168. (defclass atom-cprt (atom-ilst) ())
  169. (defclass atom-covr (atom-ilst) ())
  170. (defclass atom-disk (atom-ilst) ())
  171. (defclass atom-©enc (atom-ilst) ())
  172. (defclass atom-gnre (atom-ilst) ())
  173. (defclass atom-©nre (atom-ilst) ())
  174. (defclass atom-©grp (atom-ilst) ())
  175. (defclass atom-©lyr (atom-ilst) ())
  176. (defclass atom-purd (atom-ilst) ())
  177. (defclass atom-tmpo (atom-ilst) ())
  178. (defclass atom-©nam (atom-ilst) ())
  179. (defclass atom-©too (atom-ilst) ())
  180. (defclass atom-©trk (atom-ilst) ())
  181. (defclass atom-trkn (atom-ilst) ())
  182. (defclass atom-©wrt (atom-ilst) ())
  183. (defclass atom-©day (atom-ilst) ())
  184. (defclass atom-data (mp4-atom)
  185. ((atom-version :accessor atom-version :initarg :atom-version)
  186. (atom-flags :accessor atom-flags :initarg :atom-flags)
  187. (atom-value :accessor atom-value :initarg :atom-value)
  188. (atom-parent-type :accessor atom-parent-type :initarg :atom-parent-type :initform nil))
  189. (:documentation "Represents the 'data' portion of ilst data atom"))
  190. (defmethod initialize-instance :after ((me atom-data) &key mp4-file &allow-other-keys)
  191. (log5:with-context "atom-data-init"
  192. (with-slots (atom-size atom-type atom-version atom-flags atom-value atom-parent-type) me
  193. (setf atom-version (stream-read-u8 mp4-file))
  194. (setf atom-flags (stream-read-u24 mp4-file))
  195. (assert (= 0 (stream-read-u32 mp4-file)) () "a data atom lacks the required null field") ; XXX is this true?
  196. (log-mp4-atom "atom-data-init: size = ~:d, name = ~a, version = ~d, flags = ~x" atom-size (as-string atom-type) atom-version atom-flags)
  197. (setf atom-value (decode-ilst-data-atom atom-type me atom-parent-type mp4-file))
  198. (log-mp4-atom "atom-data-init: made an ilst atom-data: ~a" (vpprint me nil)))))
  199. ;;; the ILST atom decoders. First, a lot of the decoders do the same thing, so we define a macros
  200. ;;; and use those for the relevants atoms.
  201. (defgeneric decode-ilst-data-atom (type atom atom-parent-type mp4-file))
  202. (defmacro simple-text-decode (type)
  203. `(defmethod decode-ilst-data-atom ((type (eql +itunes-ilst-data+)) atom (atom-parent-type (eql ,type)) mp4-file)
  204. (stream-read-string-with-len mp4-file (- (atom-size atom) 16))))
  205. (simple-text-decode +itunes-album+)
  206. (simple-text-decode +itunes-album-artist+)
  207. (simple-text-decode +itunes-artist+)
  208. (simple-text-decode +itunes-comment+)
  209. (simple-text-decode +itunes-composer+)
  210. (simple-text-decode +itunes-copyright+)
  211. (simple-text-decode +itunes-year+)
  212. (simple-text-decode +itunes-encoder+)
  213. (simple-text-decode +itunes-groups+)
  214. (simple-text-decode +itunes-genre-x+)
  215. (simple-text-decode +itunes-lyrics+)
  216. (simple-text-decode +itunes-purchased-date+)
  217. (simple-text-decode +itunes-title+)
  218. (simple-text-decode +itunes-tool+)
  219. (simple-text-decode +itunes-writer+)
  220. (defmacro simple-a-b-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) ; throw away XXX Why?
  224. (let ((a) (b))
  225. (setf a (stream-read-u16 mp4-file))
  226. (setf b (stream-read-u16 mp4-file))
  227. (stream-read-u16 mp4-file) ; throw away XXX Why?
  228. (list a b))))
  229. (simple-a-b-decode +itunes-track+)
  230. (simple-a-b-decode +itunes-track-n+)
  231. (defmethod decode-ilst-data-atom ((type (eql +itunes-ilst-data+)) atom (atom-parent-type (eql +itunes-disk+)) mp4-file)
  232. (declare (ignore atom))
  233. (stream-read-u16 mp4-file) ; throw away XXX Why?
  234. (let ((a) (b))
  235. (setf a (stream-read-u16 mp4-file))
  236. (setf b (stream-read-u16 mp4-file))
  237. (list a b)))
  238. (defmacro simple-u16-decode (type)
  239. `(defmethod decode-ilst-data-atom ((type (eql +itunes-ilst-data+)) atom (atom-parent-type (eql ,type)) mp4-file)
  240. (declare (ignore atom))
  241. (stream-read-u16 mp4-file)))
  242. (simple-u16-decode +itunes-tempo+)
  243. (simple-u16-decode +itunes-genre+)
  244. (defmethod decode-ilst-data-atom ((type (eql +itunes-ilst-data+)) atom (atom-parent-type (eql +itunes-compilation+)) mp4-file)
  245. (declare (ignore atom))
  246. (stream-read-u8 mp4-file))
  247. (defmethod decode-ilst-data-atom ((type (eql +itunes-ilst-data+)) atom (atom-parent-type (eql +itunes-cover-art+)) mp4-file)
  248. (stream-read-sequence mp4-file (- (atom-size atom) 16)))
  249. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; AUDIO PROPERTY ATOMS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  250. ;; (defclass mp4-hdlr-atom (atom-raw-mixin mp4-atom)()) ; XXX need to get real format for this...
  251. ;;; song length is seconds is (float duration) / (float scale)
  252. ;; (defclass atom-mdhd (mp4-atom)
  253. ;; ((version :accessor version)
  254. ;; (flags :accessor flags)
  255. ;; (c-time :accessor c-time)
  256. ;; (m-time :accessor m-time)
  257. ;; (scale :accessor scale)
  258. ;; (duration :accessor duration)
  259. ;; (lang :accessor lang)
  260. ;; (quality :accessor quality)))
  261. ;; (defmethod initialize-instance :after ((me atom-mdhd) &key (mp4-file nil) &allow-other-keys)
  262. ;; (with-slots (version flags c-time m-time scale duration lang quality) me
  263. ;; (setf version (stream-read-u8 mp4-file))
  264. ;; (setf flags (stream-read-u24 mp4-file))
  265. ;; (setf c-time (stream-read-u32 mp4-file))
  266. ;; (setf m-time (stream-read-u32 mp4-file))
  267. ;; (setf scale (stream-read-u32 mp4-file))
  268. ;; (setf duration (if (= 0 version) (stream-read-u32 mp4-file) (stream-read-u64 mp4-file)))
  269. ;; (setf lang (stream-read-u16 mp4-file))
  270. ;; (setf quality (stream-read-u16 mp4-file))))
  271. ;; (defclass atom-stsd (mp4-atom)
  272. ;; ((flags :accessor flags)
  273. ;; (version :accessor version)
  274. ;; (num-entries :accessor num-entries)))
  275. ;; (defmethod initialize-instance :after ((me atom-stsd) &key (mp4-file nil) &allow-other-keys)
  276. ;; (log5:with-context "atom-stsd"
  277. ;; (with-slots (flags version num-entries)
  278. ;; (setf version (stream-read-u8 mp4-file))
  279. ;; (setf flags (stream-read-u24 mp4-file))
  280. ;; (setf num-entries (stream-read-u32 mp4-file))
  281. ;; (log-mp4-atom "atom-stsd: version = ~d, flags = ~x, num-fields = ~d" version flags num-entries))))
  282. ;; (defclass atom-mp4a (mp4-atom)
  283. ;; ((reserved :accessor reserved) ; 6 bytes
  284. ;; (d-ref-idx :accessor d-ref-idx) ; 2 bytes
  285. ;; (version :accessor version) ; 2 bytes
  286. ;; (revision :accessor revision) ; 2 bytes
  287. ;; (vendor :accessor vendor) ; 4 bytes
  288. ;; (num-chans :accessor num-chans) ; 2 bytes
  289. ;; (samp-size :accessor samp-size) ; 2 bytes
  290. ;; (comp-id :accessor comp-id) ; 2 bytes
  291. ;; (packet-size :accessor packet-size) ; 2 bytes
  292. ;; (samp-rate :accessor samp-rate) ; 4 bytes
  293. ;; (defmethod initialize-instance :after ((me atom-mp4a) &key (mp4-file nil) &allow-other-keys)
  294. ;; (log5:with-context "atom-mp4a"
  295. ;; (with-slots (reserved d-ref-idx version revision vendor num-chans samp-size comp-id packet-size samp-rate) me
  296. ;; (setf reserved (stream-read-sequence mp4-file 6))
  297. ;; (setf d-ref-idx (stream-read-u16 mp4-file))
  298. ;; (setf version (stream-read-u16 mp4-file))
  299. ;; (setf revision (stream-read-u16 mp4-file))
  300. ;; (setf vendor (stream-read-u32 mp4-file))
  301. ;; (setf num-chans (stream-read-u16 mp4-file))
  302. ;; (setf samp-size (stream-read-u16 mp4-file))
  303. ;; (setf comp-id (stream-read-u16 mp4-file))
  304. ;; (setf packet-size (stream-read-u16 mp4-file))
  305. ;; (setf samp-rate (stream-read-u32 mp4-file)))))
  306. (defun read-container-atoms (mp4-file parent-atom)
  307. "loop through a container atom and add it's children to it"
  308. (with-slots (atom-children atom-file-position atom-of-interest atom-size atom-type atom-decoded) parent-atom
  309. (atom-read-loop mp4-file (+ atom-file-position atom-size)
  310. (lambda ()
  311. (let ((child (make-mp4-atom mp4-file atom-type)))
  312. (log-mp4-atom "read-container-atoms: adding new child ~a" (vpprint child nil))
  313. (add atom-children child))))))
  314. (defclass atom-meta (mp4-atom)
  315. ((version :accessor version)
  316. (flags :accessor flags)))
  317. (defmethod initialize-instance :after ((me atom-meta) &key (mp4-file nil) &allow-other-keys)
  318. (with-slots (version flags) me
  319. (setf version (stream-read-u8 mp4-file))
  320. (setf flags (stream-read-u24 mp4-file))
  321. (read-container-atoms mp4-file me)))
  322. (defclass atom-moov (mp4-atom) ())
  323. (defmethod initialize-instance :after ((me atom-moov) &key (mp4-file nil) &allow-other-keys)
  324. (read-container-atoms mp4-file me))
  325. (defclass atom-udta (mp4-atom) ())
  326. (defmethod initialize-instance :after ((me atom-udta) &key (mp4-file nil) &allow-other-keys)
  327. (read-container-atoms mp4-file me))
  328. (defclass atom-mdia (mp4-atom) ())
  329. (defmethod initialize-instance :after ((me atom-mdia) &key (mp4-file nil) &allow-other-keys)
  330. (read-container-atoms mp4-file me))
  331. (defun find-atom-class (id)
  332. "Search by concatenating 'atom-' with ID and look for that symbol in this package"
  333. (log5:with-context "find-atom-class"
  334. (log-mp4-atom "find-atom-class: looking for class <~a>" (as-string id))
  335. (let ((found-class-symbol (find-symbol (string-upcase (concatenate 'string "atom-" (as-string id))) :MP4-ATOM))
  336. (found-class))
  337. ;; if we found the class name, return the class (to be used for MAKE-INSTANCE)
  338. (when found-class-symbol
  339. (setf found-class (find-class found-class-symbol))
  340. (log-mp4-atom "find-atom-class: found class: ~a" found-class)
  341. (return-from find-atom-class (find-class found-class-symbol))))
  342. ;; didn't find a class, so return ATOM-SKIP class
  343. (log-mp4-atom "find-atom-class: class not found")
  344. 'atom-skip))
  345. (defun make-mp4-atom (mp4-file &optional parent-type)
  346. "Get current file position, read in size/type, then construct the correct atom."
  347. (log5:with-context "make-mp4-atom"
  348. (let* ((pos (stream-seek mp4-file 0 :current))
  349. (siz (stream-read-u32 mp4-file))
  350. (typ (stream-read-u32 mp4-file))
  351. (atom))
  352. (declare (type integer pos siz typ))
  353. (log-mp4-atom "make-mp4-atom: @ pos = ~:d of size = ~:d and type = ~a" pos siz (as-string typ))
  354. (when (= 0 siz)
  355. (error "trying to make an atom ~a with size of 0 at offset ~:d in ~a, ammending size to be 8"
  356. (as-string typ) pos (stream-filename mp4-file)))
  357. (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))
  358. (log-mp4-atom "make-mp4-atom: made ~a" (vpprint atom nil))
  359. atom)))
  360. (defmethod vpprint ((me mp4-atom) stream)
  361. (format stream "~a" (with-output-to-string (s)
  362. (with-slots (atom-children atom-file-position atom-size atom-type) me
  363. (format s "ATOM: type: <~a> @ ~:d of size ~:d and child count of ~d"
  364. (as-string atom-type) atom-file-position atom-size (size atom-children)))
  365. (if (typep me 'atom-data)
  366. (with-slots (atom-version atom-flags atom-value atom-type atom-parent-type) me
  367. (format s " having ilst fields: atom-parent-type = ~a, verison = ~d, flags = ~x, data = ~x"
  368. (as-string atom-parent-type) atom-version atom-flags
  369. (if (typep atom-value 'array) (printable-array atom-value) atom-value)))))))
  370. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  371. (defun is-valid-m4-file (mp4-file)
  372. "Make sure this is an MP4 file. Quick check: is first atom (at file-offset 4) == FSTYP?"
  373. (stream-seek mp4-file 0 :start)
  374. (let* ((size (stream-read-u32 mp4-file))
  375. (header (stream-read-u32 mp4-file)))
  376. (declare (ignore size))
  377. (stream-seek mp4-file 0 :start)
  378. (= header +m4-ftyp+)))
  379. (defun find-mp4-atoms (mp4-file)
  380. "Given a valid MP4 file mp4-file, look for the 'right' atoms and return them."
  381. (log5:with-context "find-mp4-atoms"
  382. (when (not (is-valid-m4-file mp4-file))
  383. (error 'mp4-atom-condition :location "find-mp4-atoms" :object mp4-file :message "is not an mp4-file" ))
  384. (log-mp4-atom "find-mp4-atoms: ~a, before read-file loop, file-position = ~:d, end = ~:d"
  385. (stream-filename mp4-file) (stream-seek mp4-file 0 :current) (stream-size mp4-file))
  386. (setf (mp4-atoms mp4-file) (make-mp4-atom-collection))
  387. (atom-read-loop mp4-file (stream-size mp4-file)
  388. (lambda ()
  389. (let ((new-atom (make-mp4-atom mp4-file)))
  390. (when new-atom
  391. (add (mp4-atoms mp4-file) new-atom)))))
  392. (log-mp4-atom "find-mp4-atoms: returning atom-collection of size ~d" (size (mp4-atoms mp4-file)))))
  393. (defmethod map-mp4-atom ((me mp4-atom) &key (func nil) (depth nil))
  394. "traverse all atoms under a given atom"
  395. (log5:with-context "map-mp4-atom(single)"
  396. (labels ((_indented-atom (atom depth)
  397. (format t "~vt~a~%" (if (null depth) 0 depth) (vpprint atom nil))))
  398. (with-slots (atom-type atom-children) me
  399. (log-mp4-atom "map-mp4-atom: begining traversal with ~a, I have ~d children" (as-string atom-type) (size atom-children))
  400. (when (null func)
  401. (setf func #'_indented-atom))
  402. (funcall func me depth)
  403. (map-mp4-atom atom-children :func func :depth (if (null depth) nil (+ 1 depth)))))))
  404. (defmethod traverse ((me mp4-atom) path)
  405. "Used in finding nested atoms.
  406. Given an atom and a path, if atom-type matches first element of path, then we've found our match."
  407. (log5:with-context "traverse-atom"
  408. (log-mp4-atom "traverse (mp4-atom): entered with ~a ~a" (as-string (atom-type me)) path)
  409. (cond ((null path)
  410. (error "Path exhausted in travese atom") ; don't think this can happen?
  411. nil)
  412. ((= (atom-type me) (first path))
  413. (log-mp4-atom "traverse (mp4-atom): current path matches thus far ~a ~a" (atom-type me) path)
  414. (cond
  415. ((= 1 (length path))
  416. (log-mp4-atom "traverse (mp4-atom): length of path is 1, so found!")
  417. (return-from traverse me)))))
  418. (log-mp4-atom "traverse (mp4-atom): current path doesn't match ~a ~a" (atom-type me) path)
  419. nil))
  420. (defmethod traverse ((me atom-collection) path)
  421. "Used in finding nested atoms. Seach the collection and if we find a match with first of path,
  422. call traverse atom (unless length of path == 1, in which case, we've found our match)"
  423. (log5:with-context "traverse-atom-collection"
  424. (log-mp4-atom "traverse (atom-collection): entering with ~a ~a" me path)
  425. (dolist (sibling (atoms me)) ; cleaner than using map-mp4-atom, but still a kludge
  426. (with-slots (atom-type atom-children) sibling
  427. (log-mp4-atom "traverse (atom-collection): looking at ~x::~x" atom-type (first path))
  428. (when (= atom-type (first path))
  429. (cond
  430. ((= 1 (length path))
  431. (log-mp4-atom "traverse (atom-collection): found ~a" sibling)
  432. (return-from traverse sibling))
  433. (t
  434. (log-mp4-atom "traverse (atom-collection): path matches, calling traverse atom with ~a, ~a" atom-children (rest path))
  435. (let ((found (traverse atom-children (rest path))))
  436. (if found (return-from traverse found))))))))
  437. (log-mp4-atom "traverse (atom-collection): looked at all, found nothing")
  438. nil))
  439. (defmethod tag-get-value (atoms node)
  440. "Helper function to extract text from atom's data atom"
  441. (let ((atom (traverse atoms
  442. (list +mp4-atom-moov+ +mp4-atom-udta+ +mp4-atom-meta+ +mp4-atom-ilst+ node +itunes-ilst-data+))))
  443. (if atom
  444. (atom-value atom)
  445. nil)))
  446. (defun mp4-show-raw-tag-atoms (mp4-file-stream)
  447. (map-mp4-atom (mp4-atom::traverse (mp4-atoms mp4-file-stream)
  448. (list +mp4-atom-moov+ +mp4-atom-udta+ +mp4-atom-meta+ +mp4-atom-ilst+))
  449. :depth 0
  450. :func (lambda (atom depth)
  451. (when (= (atom-type atom) +itunes-ilst-data+)
  452. (format t "~vt~a~%" depth (vpprint atom nil))))))
  453. (defun find-all (base name)
  454. "Starting as BASE atom, recursively search for all instances of NAME"
  455. (let* ((search-name (if (typep name 'string) (as-int name) name))
  456. (found))
  457. (map-mp4-atom base
  458. :func (lambda (atom depth)
  459. (declare (ignore depth))
  460. (when (= (atom-type atom) search-name)
  461. (push atom found))))
  462. found))
  463. ;; (defun get-audio-properties-atoms (mp4-file)
  464. ;; "First, find all TRAKs under moov. For the one that contains a HDLR atom with DATA of 'soun',
  465. ;; return trak.mdia.mdhd and trak.mdia.minf.stbl.stsd"
  466. ;; (dolist (track (find-all (traverse (mp4-atoms mp4-file) (list +mp4-atom-moov+)) "trak"))
  467. ;; (format t "track = ~a~%" track)
  468. ;; (let ((hdlr (traverse track (list +mp4-atom-mdia+ +audioprop-hdlr+))))
  469. ;; (format t "hdlr = ~a~%" hdlr)
  470. ;; (when (and (not (null hdlr))
  471. ;; (string= "soun" (subseq (data hdlr) 8 12)))
  472. ;; ;; we've found the correct track, extract atoms
  473. ;; (return-from get-audio-properties-atoms (values (traverse track (list +mp4-atom-mdia+ +audioprop-mdhd+))
  474. ;; (traverse track (list +mp4-atom-mdia+ +mp4-atom-minf+ +mp4-atom-stbl+ +audioprop-stsd+)))))))
  475. ;; nil)