m4a.lisp 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. ;;; -*- Mode: Lisp; show-trailing-whitespace: t; Base: 10; indent-tabs: nil; Syntax: ANSI-Common-Lisp; Package: M4A; -*-
  2. ;;; Copyright (c) 2013, Mark VandenBrink. All rights reserved.
  3. (in-package #:m4a)
  4. ;;;; ATOMS
  5. ;;;
  6. ;;; A word about atoms (aka "boxes"). There are three kinds of atoms: ones that
  7. ;;; are containers, ones that are data, and ones that are both. A lot of the
  8. ;;; taggers out there mostly ignore third class and treat "container atoms that
  9. ;;; also have data" as a big blob of data that they rummage around in via
  10. ;;; indices. Seems sort of broken, IMHO, so we'll try to handle all three if
  11. ;;; at all possible.
  12. ;;;
  13. (defun mk-atom-class-name (name)
  14. "Create an atom class name by concatenating ATOM- with NAME, preserving case"
  15. (declare #.utils:*standard-optimize-settings*)
  16. (mkstr "atom-" name))
  17. (memoize 'mk-atom-class-name)
  18. (eval-when (:compile-toplevel :load-toplevel :execute)
  19. (defun as-octets (str)
  20. (declare #.utils:*standard-optimize-settings*)
  21. (let ((ret 0))
  22. (setf (ldb (byte 8 24) ret) (char-code (aref str 0)))
  23. (setf (ldb (byte 8 16) ret) (char-code (aref str 1)))
  24. (setf (ldb (byte 8 8) ret) (char-code (aref str 2)))
  25. (setf (ldb (byte 8 0) ret) (char-code (aref str 3)))
  26. ret))
  27. (defun as-string (atom-type)
  28. "Given an integer, return the string representation"
  29. (declare #.utils:*standard-optimize-settings*)
  30. (with-output-to-string (s nil)
  31. (write-char (code-char (ldb (byte 8 24) atom-type)) s)
  32. (write-char (code-char (ldb (byte 8 16) atom-type)) s)
  33. (write-char (code-char (ldb (byte 8 8) atom-type)) s)
  34. (write-char (code-char (ldb (byte 8 0) atom-type)) s)))
  35. (defun as-octet (c)
  36. "Used below so that we can create atom 'types' from char/ints"
  37. (declare #.utils:*standard-optimize-settings*)
  38. (cond ((typep c 'standard-char) (coerce (char-code c) '(unsigned-byte 8)))
  39. ((typep c 'integer) (coerce c '(unsigned-byte 8)))
  40. (t (error "can only handle characters and integers"))))
  41. (defmacro mk-mp4-atom-type (l1 l2 l3 l4)
  42. "Given 4 chars/ints, create a string for the name"
  43. `(with-output-to-string (s nil)
  44. (write-char (code-char ,(as-octet l1)) s)
  45. (write-char (code-char ,(as-octet l2)) s)
  46. (write-char (code-char ,(as-octet l3)) s)
  47. (write-char (code-char ,(as-octet l4)) s))))
  48. (defparameter *in-progress* nil "the node currently being worked upon")
  49. (defparameter *tree* nil "the root of the atom tree being constructed")
  50. ;;;; Atoms
  51. (defclass mp4-atom ()
  52. ((atom-file-pos :accessor atom-file-pos :initarg :atom-file-pos :initform nil)
  53. (atom-size :accessor atom-size :initarg :atom-size :initform nil)
  54. (atom-type :accessor atom-type :initarg :atom-type :initform nil))
  55. (:documentation "The minimal mp4-atom."))
  56. (defmethod initialize-instance :around ((me mp4-atom) &key &allow-other-keys)
  57. (declare #.utils:*standard-optimize-settings*)
  58. (let* ((old *in-progress*)
  59. (*in-progress* (tree:make-node me)))
  60. (if old
  61. (progn
  62. (tree:add-child old *in-progress*)
  63. (call-next-method))
  64. (let ((*tree* *in-progress*)) ; must use dynamic binding for multi-threading
  65. (call-next-method)
  66. ;; HACK ALERT: this is the top of the tree, so stuff the root
  67. ;; in the TREE slot of a container atom
  68. (setf (tree me) *tree*)))))
  69. (defmacro with-mp4-atom-slots ((instance) &body body)
  70. `(with-slots (atom-file-pos atom-size atom-type) ,instance
  71. ,@body))
  72. ;;;; Concrete atoms
  73. (defclass atom-skip (mp4-atom) ())
  74. (defmethod initialize-instance :after ((me atom-skip) &key mp4-file &allow-other-keys)
  75. "The 'skip' atom. Used when we want to capture the header of atom, but don't want/need
  76. to read the payload of an atom."
  77. (declare #.utils:*standard-optimize-settings*)
  78. (with-mp4-atom-slots (me)
  79. (stream-seek mp4-file (- atom-size 8) :current)))
  80. (defclass atom-container (mp4-atom)
  81. ((tree :accessor tree :documentation "Note: this is ONLY set for the ROOT atom"))
  82. (:documentation "An atom that 'contains' other atoms"))
  83. (defmethod initialize-instance :after ((me atom-container) &key mp4-file &allow-other-keys)
  84. (declare #.utils:*standard-optimize-settings*)
  85. (with-mp4-atom-slots (me)
  86. (loop for end = (+ atom-file-pos atom-size)
  87. for current = (stream-seek mp4-file) then (stream-seek mp4-file)
  88. while (< current end) do
  89. (make-mp4-atom mp4-file me))))
  90. ;;;; ILST ATOMS (ie atoms related to tagging)
  91. ;(defclass |atom-ilst| (atom-container) ())
  92. (eval-when (:compile-toplevel :load-toplevel :execute)
  93. (defun generate-it (name type super-class)
  94. (let ((class-name (if super-class
  95. `(defclass ,(mksym nil "atom-" type) (,super-class) ())))
  96. (constant `(defconstant* ,(mksym t "+itunes-" name "+") ,type))
  97. (exported `(export ',(mksym t "+itunes-" name "+"))))
  98. `(progn ,constant ,exported ,class-name)))
  99. (defmacro ncls-atom-entry (a b) `(list ,a ,b nil)) ; no superclass
  100. (defmacro ilst-atom-entry (a b) `(list ,a ,b '|atom-ilst|)) ; itunes tagging
  101. (defmacro skip-atom-entry (a b) `(list ,a ,b 'atom-skip)) ; read and skip
  102. (defmacro cont-atom-entry (a b) `(list ,a ,b 'atom-container)) ; contains other atoms, but no data
  103. (defparameter *handled-atoms*
  104. (list (ncls-atom-entry "root" "ROOT") ; pseudo-atom
  105. (ncls-atom-entry "ilst-data" "data")
  106. (ncls-atom-entry "atom-handler" "hdlr")
  107. (ncls-atom-entry "media-header" "mdhd")
  108. (ncls-atom-entry "sample-table-desc" "stsd")
  109. (ncls-atom-entry "mp4a-codec" "mp4a")
  110. (ncls-atom-entry "elementary-stream-descriptor" "esds")
  111. (ncls-atom-entry "item-list" "ilst")
  112. (ncls-atom-entry "metadata" "meta")
  113. (ilst-atom-entry "account-type" "akID")
  114. (ilst-atom-entry "album" "©alb")
  115. (ilst-atom-entry "album-artist" "aART")
  116. (ilst-atom-entry "artist" "©ART")
  117. (ilst-atom-entry "at-id" "atID")
  118. (ilst-atom-entry "cn-id" "cnID")
  119. (ilst-atom-entry "comment" "©cmt")
  120. (ilst-atom-entry "compilation" "cpil")
  121. (ilst-atom-entry "composer" "©com")
  122. (ilst-atom-entry "content-rating" "rtng")
  123. (ilst-atom-entry "copyright" "cprt")
  124. (ilst-atom-entry "cover-art" "covr")
  125. (ilst-atom-entry "disk" "disk")
  126. (ilst-atom-entry "encoder" "©enc")
  127. (ilst-atom-entry "flavor" "flvr")
  128. (ilst-atom-entry "gapless-playback" "pgap")
  129. (ilst-atom-entry "ge-id" "geID")
  130. (ilst-atom-entry "genre" "gnre")
  131. (ilst-atom-entry "genre-x" "©gen")
  132. (ilst-atom-entry "groups" "©grp")
  133. (ilst-atom-entry "lyrics" "©lyr")
  134. (ilst-atom-entry "media-type" "stik")
  135. (ilst-atom-entry "purchase-account" "apID")
  136. (ilst-atom-entry "purchased-date" "purd")
  137. (ilst-atom-entry "sort-album" "soal")
  138. (ilst-atom-entry "sort-album-artist" "soaa")
  139. (ilst-atom-entry "sort-artist" "soar")
  140. (ilst-atom-entry "sort-composer" "soco")
  141. (ilst-atom-entry "sort-name" "sonm")
  142. (ilst-atom-entry "store" "sfID")
  143. (ilst-atom-entry "tempo" "tmpo")
  144. (ilst-atom-entry "title" "©nam")
  145. (ilst-atom-entry "tool" "©too")
  146. (ilst-atom-entry "track" "©trk")
  147. (ilst-atom-entry "track-n" "trkn")
  148. (ilst-atom-entry "writer" "©wrt")
  149. (ilst-atom-entry "unique-id" "xid ") ; Note: space at the end
  150. (ilst-atom-entry "year" "©day")
  151. (skip-atom-entry "composer-id" "cmID")
  152. (skip-atom-entry "mean" "mean")
  153. (skip-atom-entry "data-ref-url" "url ")
  154. (skip-atom-entry "data-ref-alis" "alis")
  155. (skip-atom-entry "data-ref-rsrc" "rsrc")
  156. (skip-atom-entry "unk-drm" "drms")
  157. (skip-atom-entry "edit-atom" "edts")
  158. (skip-atom-entry "free" "free")
  159. (skip-atom-entry "file-type" "ftyp")
  160. (skip-atom-entry "unk-iods" "iods")
  161. (skip-atom-entry "media-data-atom" "mdat")
  162. (skip-atom-entry "movie-header" "mvhd")
  163. (skip-atom-entry "name-atom" "name")
  164. (skip-atom-entry "unk-pinf" "pinf")
  165. (skip-atom-entry "play-list-id" "plID")
  166. (skip-atom-entry "unk-sbtd" "sbtd")
  167. (skip-atom-entry "sound-media-header" "smhd")
  168. (skip-atom-entry "sample-table-chunk-offset" "stco")
  169. (skip-atom-entry "sample-table-sample" "stsc")
  170. (skip-atom-entry "sample-table-size" "stsz")
  171. (skip-atom-entry "sample-table-time" "stts")
  172. (skip-atom-entry "track-header" "tkhd")
  173. (cont-atom-entry "free-form" "----")
  174. (cont-atom-entry "data-info" "dinf")
  175. (cont-atom-entry "item-list" "ilst")
  176. (cont-atom-entry "media" "mdia")
  177. (cont-atom-entry "media-info" "minf")
  178. (cont-atom-entry "movie" "moov")
  179. (cont-atom-entry "sample-table" "stbl")
  180. (cont-atom-entry "trak-header" "trak")
  181. (cont-atom-entry "user-data" "udta"))
  182. "This is the list of atoms we 'handle', where 'handle' may well indeed mean to
  183. skip or just define a constant. Each entry is constant-name/class-name.
  184. From this data structure we auto-generate (optionally) class-names
  185. and (always) defconstants.")
  186. (defmacro mk-handled-atoms-constants-and-classes ()
  187. `(progn
  188. ,@(loop for e in *handled-atoms*
  189. collect
  190. (generate-it (first e) (second e) (third e))))))
  191. ;;; generate the handled atoms/constants/exports
  192. (mk-handled-atoms-constants-and-classes)
  193. (defclass |atom-data| (mp4-atom)
  194. ((atom-version :accessor atom-version :initarg :atom-version :initform nil)
  195. (atom-flags :accessor atom-flags :initarg :atom-flags :initform nil)
  196. (atom-locale :accessor atom-locale :initarg :atom-locale :initform nil)
  197. (atom-value :accessor atom-value :initarg :atom-value :initform nil))
  198. (:documentation "Represents the 'data' portion of ilst data atom"))
  199. (defmethod initialize-instance :after ((me |atom-data|) &key mp4-file parent &allow-other-keys)
  200. (declare #.utils:*standard-optimize-settings*)
  201. (with-slots (atom-size atom-type atom-version atom-flags atom-value atom-locale) me
  202. (setf atom-version (stream-read-u8 mp4-file)
  203. atom-flags (stream-read-u24 mp4-file)
  204. atom-locale (stream-read-u32 mp4-file))
  205. ;(dbg 'atom-data-init atom-version atom-flags atom-locale)
  206. ;; Ideally, we would be able to read the atom's value by looking
  207. ;; solely at the atom-flags; however, when atom-flags == 0, then
  208. ;; things get crazy---I can NOT for the life of me figure out
  209. ;; the trck/trkn/disk integer format, hence the mess below.
  210. (setf atom-value
  211. (ecase atom-flags
  212. (1
  213. (stream-read-utf-8-string mp4-file (- atom-size 16)))
  214. ((13 14)
  215. (stream-read-sequence mp4-file (- atom-size 16)))
  216. ((0 21)
  217. (cond ; messy!
  218. ((member (atom-type parent)
  219. (list +itunes-track+ +itunes-track-n+ +itunes-disk+)
  220. :test #'string=)
  221. (stream-read-u16 mp4-file) ; throw away
  222. (let* ((a (stream-read-u16 mp4-file))
  223. (b (stream-read-u16 mp4-file)))
  224. (stream-seek mp4-file (- atom-size 16 6) :current)
  225. (list a b)))
  226. (t (case (- atom-size 16)
  227. (4 (stream-read-u32 mp4-file))
  228. (2 (stream-read-u16 mp4-file))
  229. (1 (stream-read-u8 mp4-file))
  230. (otherwise
  231. ;;(warn-user "file: ~a~%unknown atom-flags: ~a, reading ~:d octets"
  232. ;; audio-streams:*current-file* atom-flags (- atom-size 16))
  233. (stream-read-sequence mp4-file (- atom-size 16)))))))))))
  234. ;(defclass |atom-dinf| (atom-container) ())
  235. (defclass |atom-dref| (atom-container)
  236. ((version :accessor version)
  237. (flags :accessor flags)
  238. (num-entries :accessor num-entries))
  239. (:documentation "data reference atom"))
  240. (defmethod initialize-instance :around ((me |atom-dref|) &key mp4-file &allow-other-keys)
  241. (declare #.utils:*standard-optimize-settings*)
  242. (with-slots (version flags num-entries entries) me
  243. (setf version (stream-read-u8 mp4-file)
  244. flags (stream-read-u24 mp4-file)
  245. num-entries (stream-read-u32 mp4-file))
  246. (call-next-method)))
  247. ;; (loop for i from 0 to (1- num-entries) do
  248. ;; (setf (aref entries i)
  249. ;; (make-instance 'data-reference
  250. ;; :mp4-file mp4-file)))))
  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 &allow-other-keys)
  261. (declare #.utils:*standard-optimize-settings*)
  262. (with-slots (version flags qtype mtype resv rflag rmask mhdlr atom-size) me
  263. (setf version (stream-read-u8 mp4-file)
  264. flags (stream-read-u24 mp4-file)
  265. qtype (stream-read-u32 mp4-file)
  266. mtype (stream-read-u32 mp4-file)
  267. resv (stream-read-u32 mp4-file)
  268. rflag (stream-read-u32 mp4-file)
  269. rmask (stream-read-u32 mp4-file)
  270. mhdlr (stream-read-sequence mp4-file (- atom-size 32))) ; 32 is 8-bytes of header plus fields above
  271. ))
  272. (defclass |atom-mdhd| (mp4-atom)
  273. ((version :accessor version)
  274. (flags :accessor flags)
  275. (c-time :accessor c-time)
  276. (m-time :accessor m-time)
  277. (scale :accessor scale)
  278. (duration :accessor duration)
  279. (lang :accessor lang)
  280. (quality :accessor quality)))
  281. (defmethod initialize-instance :after ((me |atom-mdhd|) &key mp4-file &allow-other-keys)
  282. (declare #.utils:*standard-optimize-settings*)
  283. (with-slots (version flags c-time m-time scale duration lang quality) me
  284. (setf version (stream-read-u8 mp4-file)
  285. flags (stream-read-u24 mp4-file)
  286. c-time (stream-read-u32 mp4-file)
  287. m-time (stream-read-u32 mp4-file)
  288. scale (stream-read-u32 mp4-file)
  289. duration (if (= 0 version) (stream-read-u32 mp4-file) (stream-read-u64 mp4-file))
  290. lang (stream-read-u16 mp4-file)
  291. quality (stream-read-u16 mp4-file))))
  292. (defclass |atom-esds| (mp4-atom)
  293. ((version :accessor version) ; 1 byte
  294. (flags :accessor flags) ; 3 bytes
  295. (esid :accessor esid) ; 2 bytes
  296. (s-priority :accessor s-priority) ; 1 byte
  297. (obj-id :accessor obj-id) ; 1 byte
  298. (s-type :accessor s-type) ; 1 byte (1 bit up-stream, 1-but reserved, 6-bits stream type
  299. (buf-size :accessor buf-size) ; 3 bytes
  300. (max-bit-rate :accessor max-bit-rate) ; 4 bytes
  301. (avg-bit-rate :accessor avg-bit-rate)) ; 4 bytes
  302. (:documentation "XXX-partial definition for Elementary Stream Descriptors (ESDs)"))
  303. ;;; 3 bytes extended descriptor type tag string = 3 * 8-bit hex value
  304. ;;; types are Start = 0x80 ; End = 0xFE
  305. ;;; then, one byte of length
  306. ;;; Note: start types are optional
  307. (defun read-descriptor-len (instream)
  308. "Get the ES descriptor's length."
  309. (declare #.utils:*standard-optimize-settings*)
  310. (let* ((tmp (stream-read-u8 instream))
  311. (len (logand tmp #x7f)))
  312. (declare (type (unsigned-byte 8) tmp))
  313. (while (not (zerop (logand #x80 tmp)))
  314. (setf tmp (stream-read-u8 instream)
  315. len (logior (ash len 7) (logand tmp #x7f))))
  316. len))
  317. ;;; one-byte descriptor tags
  318. (defconstant* +mp4-odescrtag+ #x01)
  319. (defconstant* +mp4-iodescrtag+ #x02)
  320. (defconstant* +mp4-esdescrtag+ #x03)
  321. (defconstant* +mp4-decconfigdescrtag+ #x04)
  322. (defconstant* +mp4-decspecificdescrtag+ #x05)
  323. (defconstant* +mp4-slconfigdescrtag+ #x06)
  324. (defconstant* +mp4-contentiddescrtag+ #x07)
  325. (defconstant* +mp4-supplcontentiddescrtag+ #x08)
  326. (defconstant* +mp4-ipiptrdescrtag+ #x09)
  327. (defconstant* +mp4-ipmpptrdescrtag+ #x0a)
  328. (defconstant* +mp4-ipmpdescrtag+ #x0b)
  329. (defconstant* +mp4-registrationdescrtag+ #x0d)
  330. (defconstant* +mp4-esidincdescrtag+ #x0e)
  331. (defconstant* +mp4-esidrefdescrtag+ #x0f)
  332. (defconstant* +mp4-fileiodescrtag+ #x10)
  333. (defconstant* +mp4-fileodescrtag+ #x11)
  334. (defconstant* +mp4-extprofileleveldescrtag+ #x13)
  335. (defconstant* +mp4-extdescrtagsstart+ #x80)
  336. (defconstant* +mp4-extdescrtagsend+ #xfe)
  337. (defmethod initialize-instance :after ((me |atom-esds|) &key mp4-file &allow-other-keys)
  338. (declare #.utils:*standard-optimize-settings*)
  339. (with-slots (version flags esid s-priority obj-id s-type buf-size max-bit-rate avg-bit-rate) me
  340. (setf version (stream-read-u8 mp4-file)
  341. flags (stream-read-u24 mp4-file))
  342. (assert (= +MP4-ESDescrTag+ (stream-read-u8 mp4-file)) () "Expected description tag of ESDescrTag")
  343. (let* ((len (read-descriptor-len mp4-file))
  344. (end-of-atom (+ (stream-seek mp4-file) len)))
  345. (setf esid (stream-read-u16 mp4-file)
  346. s-priority (stream-read-u8 mp4-file))
  347. (assert (= +MP4-DecConfigDescrTag+ (stream-read-u8 mp4-file)) () "Expected tag type of DecConfigDescrTag")
  348. (setf len (read-descriptor-len mp4-file)
  349. obj-id (stream-read-u8 mp4-file)
  350. s-type (stream-read-u8 mp4-file)
  351. buf-size (stream-read-u24 mp4-file)
  352. max-bit-rate (stream-read-u32 mp4-file)
  353. avg-bit-rate (stream-read-u32 mp4-file))
  354. ;; Should do checking here and/or read rest of atom,
  355. ;; but for now, we have what we want, so just seek to end of atom
  356. (stream-seek mp4-file end-of-atom :start))))
  357. (defclass |atom-stsd| (mp4-atom)
  358. ((flags :accessor flags)
  359. (version :accessor version)
  360. (num-entries :accessor num-entries)))
  361. (defmethod initialize-instance :after ((me |atom-stsd|) &key mp4-file &allow-other-keys)
  362. (declare #.utils:*standard-optimize-settings*)
  363. (with-slots (flags version num-entries) me
  364. (setf version (stream-read-u8 mp4-file)
  365. flags (stream-read-u24 mp4-file)
  366. num-entries (stream-read-u32 mp4-file))))
  367. (defclass |atom-mp4a| (atom-container)
  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 :around ((me |atom-mp4a|) &key mp4-file &allow-other-keys)
  379. "Note: this MUST be an AROUND method so that the atom's data can be read in before
  380. reading the container atoms"
  381. (declare #.utils:*standard-optimize-settings*)
  382. (with-slots (reserved d-ref-idx version revision vendor num-chans samp-size comp-id packet-size samp-rate) me
  383. (setf reserved (stream-read-sequence mp4-file 6)
  384. d-ref-idx (stream-read-u16 mp4-file)
  385. version (stream-read-u16 mp4-file)
  386. revision (stream-read-u16 mp4-file)
  387. vendor (stream-read-u32 mp4-file)
  388. num-chans (stream-read-u16 mp4-file)
  389. samp-size (stream-read-u16 mp4-file)
  390. comp-id (stream-read-u16 mp4-file)
  391. packet-size (stream-read-u16 mp4-file)
  392. samp-rate (stream-read-u32 mp4-file))) ; fixed 16.16 floating point number
  393. (call-next-method))
  394. (defclass |atom-meta| (atom-container)
  395. ((version :accessor version)
  396. (flags :accessor flags)))
  397. (defmethod initialize-instance :around ((me |atom-meta|) &key mp4-file &allow-other-keys)
  398. (declare #.utils:*standard-optimize-settings*)
  399. (with-slots (version flags) me
  400. (setf version (stream-read-u8 mp4-file)
  401. flags (stream-read-u24 mp4-file)))
  402. (call-next-method))
  403. (defun find-atom-class (id)
  404. "Search by concatenating 'atom-' with ID and look for that symbol in this package"
  405. (declare #.utils:*standard-optimize-settings*)
  406. (let ((found-class-symbol (find-symbol (mk-atom-class-name id) :M4A)))
  407. ;(dbg 'find-atom-class id found-class-symbol)
  408. ;; if we found the class name, return the class (to be used for MAKE-INSTANCE)
  409. (when found-class-symbol
  410. (return-from find-atom-class (find-class found-class-symbol)))
  411. ;; didn't find a class, so return ATOM-SKIP class
  412. (warn-user "file ~a~%Unknown atom type <~a> encountered~%"
  413. audio-streams:*current-file* id)
  414. 'atom-skip))
  415. (memoize 'find-atom-class)
  416. (defparameter *stop-on-count* 0)
  417. (defun make-mp4-atom (mp4-file parent)
  418. "Get current file position, read in size/type, then construct the correct atom."
  419. (declare #.utils:*standard-optimize-settings*)
  420. (let* ((pos (stream-seek mp4-file))
  421. (siz (stream-read-u32 mp4-file))
  422. (typ (as-string (stream-read-u32 mp4-file)))
  423. (atom))
  424. (declare (fixnum pos siz))
  425. (when (= 0 siz)
  426. (error "trying to make an atom ~a with size of 0 at offset ~:d in file ~a"
  427. typ pos (stream-filename mp4-file)))
  428. ;(dbg 'make-mp4-atom pos siz typ)
  429. ;(incf *stop-on-count*)
  430. ;(when (> *stop-on-count* 100)
  431. ;(break))
  432. (setf atom (make-instance (find-atom-class typ)
  433. :atom-size siz
  434. :atom-type typ
  435. :atom-file-pos pos
  436. :parent parent
  437. :mp4-file mp4-file))
  438. ;(dbg 'make-mp4-atom (type-of atom) (vpprint atom nil))
  439. atom))
  440. (defmethod vpprint ((me mp4-atom) stream)
  441. "Pretty print an atom"
  442. (declare #.utils:*standard-optimize-settings*)
  443. (format stream "~a"
  444. (with-output-to-string (s)
  445. (with-mp4-atom-slots (me)
  446. (format s "Atom ~a @ ~:d of size ~:d"
  447. atom-type atom-file-pos atom-size))
  448. (typecase me
  449. (|atom-data|
  450. (with-slots (atom-version atom-flags atom-value atom-type) me
  451. (format s ", ilst fields: verison = ~d, flags = ~x, data = ~a"
  452. atom-version atom-flags
  453. (if (typep atom-value 'octets)
  454. (printable-array atom-value)
  455. atom-value))))))))
  456. (defun is-valid-m4-file (mp4-file)
  457. "Make sure this is an MP4 file. Quick check: is first atom type
  458. (at file-offset 4) == 'FSTYP'?"
  459. (declare #.utils:*standard-optimize-settings*)
  460. (let ((valid)
  461. (size)
  462. (header))
  463. (when (> (stream-size mp4-file) 8)
  464. (stream-seek mp4-file 0 :start)
  465. (setf size (stream-read-u32 mp4-file)
  466. header (as-string (stream-read-u32 mp4-file))
  467. valid (and (<= size (stream-size mp4-file))
  468. (string= header +itunes-file-type+))))
  469. (stream-seek mp4-file 0 :start)
  470. valid))
  471. (defclass mp4-file ()
  472. ((filename :accessor filename :initform nil :initarg :filename
  473. :documentation "filename that was parsed")
  474. (mp4-atoms :accessor mp4-atoms :initform nil
  475. :documentation "holds tree of parsed MP4 atoms/boxes")
  476. (audio-info :accessor audio-info :initform nil
  477. :documentation "holds the bit-rate, etc info"))
  478. (:documentation "For parsing MP4 audio files"))
  479. (defun parse-audio-file (instream &optional (get-audio-info nil))
  480. "Given a valid MP4 file, look for the 'right' atoms and return them."
  481. (declare #.utils:*standard-optimize-settings*)
  482. (stream-seek instream 0 :start)
  483. (let* ((*in-progress* nil) ; dynamic binding for multi-threading
  484. (parsed-info (make-instance 'mp4-file
  485. :filename (stream-filename instream))))
  486. (setf (mp4-atoms parsed-info)
  487. (tree (make-instance 'atom-container
  488. :atom-type +itunes-root+
  489. :atom-file-pos 0
  490. :atom-size (stream-size instream)
  491. :mp4-file instream)))
  492. (when get-audio-info
  493. (setf (audio-info parsed-info) (get-mp4-audio-info parsed-info)))
  494. parsed-info))
  495. (defparameter *ilst-data* (list +itunes-root+ +itunes-movie+ +itunes-user-data+
  496. +itunes-metadata+ +itunes-item-list+ nil)
  497. "iTunes artist/album/etc path. The 5th element should be set to
  498. one of the +iTunes- constants")
  499. (defun tag-get-value (mp4-file atom-type)
  500. "Helper function to extract text from ILST atom's data atom"
  501. (declare #.utils:*standard-optimize-settings*)
  502. (setf (nth 5 *ilst-data*) atom-type)
  503. ;(dbg 'tag-get-value *ilst-data*)
  504. (aif (tree:at-path (mp4-atoms mp4-file) *ilst-data*
  505. (lambda (x y)
  506. (string= (atom-type (tree:data x)) y)))
  507. (let ((ret))
  508. ;; NB: only the COVR atom can have more than one data atom,
  509. ;; and it can be "data", "name", or "itif"(???).
  510. (loop for e = (tree:first-child it)
  511. then (tree:next-sibling e)
  512. until (null e) do
  513. (when (typep (tree:data e) '|atom-data|)
  514. (push (atom-value (tree:data e)) ret)))
  515. (nreverse ret))
  516. nil))
  517. (defun mp4-show-raw-tag-atoms (mp4-file-stream out-stream)
  518. "Show all the iTunes data atoms"
  519. (declare #.utils:*standard-optimize-settings*)
  520. (let ((top-node
  521. (tree:at-path (mp4-atoms mp4-file-stream)
  522. (list +itunes-root+ +itunes-movie+ +itunes-user-data+
  523. +itunes-metadata+ +itunes-item-list+)
  524. (lambda (x y)
  525. (string= (atom-type (tree:data x)) y)))))
  526. (loop for node = (tree:first-child top-node)
  527. then (tree:next-sibling node) until (null node) do
  528. (format out-stream "~2t~a" (vpprint (tree:data node) nil))
  529. (aif (tree:first-child node)
  530. (format out-stream ", Data ~a~%" (vpprint (tree:data it) nil))
  531. (format out-stream "~%")))))
  532. (defun get-audio-properties-atoms (mp4-file)
  533. "Get the audio property atoms from MP4-FILE.
  534. MP4A audio info is held in under root.moov.trak.mdia.mdhd,
  535. root.moov.trak.mdia.minf.stbl.mp4a, and root.moov.trak.mdia.minf.stbl.mp4a.esds"
  536. (declare #.utils:*standard-optimize-settings*)
  537. (let ((mdhd
  538. (tree:find-tree
  539. (mp4-atoms mp4-file)
  540. (lambda (x)
  541. (string= (atom-type (tree:data x)) +itunes-media-header+))))
  542. (mp4a (tree:find-tree
  543. (mp4-atoms mp4-file)
  544. (lambda (x)
  545. (string= (atom-type (tree:data x)) +itunes-mp4a-codec+))))
  546. (esds (tree:find-tree
  547. (mp4-atoms mp4-file)
  548. (lambda (x)
  549. (string= (atom-type (tree:data x)) +itunes-elementary-stream-descriptor+)))))
  550. (if (and mdhd mp4a esds)
  551. (values (tree:data (first mdhd))
  552. (tree:data (first mp4a))
  553. (tree:data (first esds)))
  554. nil)))
  555. (defclass audio-info ()
  556. ((seconds :accessor seconds :initform nil)
  557. (channels :accessor channels :initform nil)
  558. (bits-per-sample :accessor bits-per-sample :initform nil)
  559. (sample-rate :accessor sample-rate :initform nil)
  560. (max-bit-rate :accessor max-bit-rate :initform nil)
  561. (avg-bit-rate :accessor avg-bit-rate :initform nil))
  562. (:documentation "Holds extracted audio information about an MP4 file."))
  563. (defmethod vpprint ((me audio-info) stream)
  564. "Pretty print audio information"
  565. (declare #.utils:*standard-optimize-settings*)
  566. (with-slots (seconds channels bits-per-sample sample-rate max-bit-rate avg-bit-rate) me
  567. (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"
  568. (if sample-rate sample-rate 0)
  569. (if channels channels 0)
  570. (if bits-per-sample bits-per-sample 0)
  571. (if max-bit-rate (round (/ max-bit-rate 1000)) 0)
  572. (if avg-bit-rate (round (/ avg-bit-rate 1000)) 0)
  573. (if seconds (floor (/ seconds 60)) 0)
  574. (if seconds (round (mod seconds 60)) 0))))
  575. (defun get-mp4-audio-info (mp4-file)
  576. "Find and parse the audio information in MP4-FILE"
  577. (declare #.utils:*standard-optimize-settings*)
  578. (let ((info (make-instance 'audio-info)))
  579. (multiple-value-bind (mdhd mp4a esds) (get-audio-properties-atoms mp4-file)
  580. (with-slots (seconds channels bits-per-sample sample-rate max-bit-rate avg-bit-rate) info
  581. (when mdhd
  582. (setf seconds (/ (float (duration mdhd)) (float (scale mdhd)))))
  583. (when mp4a
  584. (setf channels (num-chans mp4a)
  585. bits-per-sample (samp-size mp4a))
  586. (let* ((upper (ash (samp-rate mp4a) -16))
  587. (lower (logand (samp-rate mp4a) #xffff)))
  588. (setf sample-rate (+ (float upper) (/ (float lower) 1000))))
  589. (when esds
  590. (setf avg-bit-rate (avg-bit-rate esds)
  591. max-bit-rate (max-bit-rate esds))))))
  592. info))
  593. (defun map-mp4-atoms (m4a &key (func nil))
  594. "Visit each atom we found in M4A"
  595. (declare #.utils:*standard-optimize-settings*)
  596. (let ((count 0))
  597. (labels ((_internal-print (atom depth)
  598. (format t "~vt~a~%" depth (vpprint atom nil))
  599. (incf count)))
  600. (when (null func)
  601. (setf func #'_internal-print))
  602. (tree:traverse
  603. (m4a:mp4-atoms m4a)
  604. (lambda (node depth)
  605. (funcall func (tree:data node) depth))))
  606. (when count
  607. (format t "~:d atom~p found~%" count count))))