m4a.lisp 32 KB

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