| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- ;;; -*- Mode: Lisp; show-trailing-whitespace: t; Base: 10; indent-tabs: nil; Syntax: ANSI-Common-Lisp; Package: MP3-FRAME; -*-
- ;;; Copyright (c) 2013, Mark VandenBrink. All rights reserved.
- (in-package #:mp3-frame)
- (log5:defcategory cat-log-mp3-frame)
- (defmacro log-mp3-frame (&rest log-stuff) `(log5:log-for (cat-log-mp3-frame) ,@log-stuff))
- (define-condition mp3-frame-condition ()
- ((location :initarg :location :reader location :initform nil)
- (object :initarg :object :reader object :initform nil)
- (messsage :initarg :message :reader message :initform "Undefined Condition"))
- (:report (lambda (condition stream)
- (format stream "mp3-frame condition at location: <~a> with object: <~a>: message: <~a>"
- (location condition) (object condition) (message condition)))))
- (defmethod print-object ((me mp3-frame-condition) stream)
- (format stream "location: <~a>, object: <~a>, message: <~a>" (location me) (object me) (message me)))
- (defparameter *pprint-mp3-frame* nil
- "Controls whether we pretty print frame data")
- (defclass mp3-id3-header ()
- ((version :accessor version :initarg :version :initform 0)
- (revision :accessor revision :initarg :revision :initform 0)
- (flags :accessor flags :initarg :flags :initform 0)
- (size :accessor size :initarg :size :initform 0)
- (ext-header :accessor ext-header :initarg :ext-header :initform nil)
- (frames :accessor frames :initarg :frames :initform nil)
- (v21-tag-header :accessor v21-tag-header :initarg :v21-tag-header :initform nil))
- (:documentation "The ID3 header, found at start of file"))
- (defmethod vpprint ((me mp3-id3-header) stream &key (indent 0))
- "Set *pprint-mp3-frame* to get pretty printing and call print-object via format"
- (let ((*pprint-mp3-frame* t))
- (format stream "~vt~a" (* indent 1) me)))
- (defun is-valid-mp3-file (mp3-file)
- "Make sure this is an MP3 file. Look for frames at begining and/or end"
- (log5:with-context "is-valid-mp3-file"
- (stream-seek mp3-file 0 :start)
- (let* ((id3 (stream-read-string mp3-file :size 3))
- (version (stream-read-u8 mp3-file))
- (tag))
- (stream-seek mp3-file 128 :end)
- (setf tag (stream-read-string mp3-file :size 3))
- (stream-seek mp3-file 0 :start)
- (log-mp3-frame "id3 = ~a, version = ~d" id3 version)
- (or (and (string= "ID3" id3)
- (or (= 2 version) (= 3 version) (= 4 version)))
- (string= tag "TAG")))))
- (defclass v21-tag-header ()
- ((songname :accessor songname :initarg :songname :initform nil)
- (artist :accessor artist :initarg :artist :initform nil)
- (album :accessor album :initarg :album :initform nil)
- (year :accessor year :initarg :year :initform nil)
- (comment :accessor comment :initarg :comment :initform nil)
- (genre :accessor genre :initarg :genre :initform nil))
- (:documentation "ID3 V2.1 old-style tag. If present, found in last 128 bytes of file."))
- (defmethod vpprint ((me v21-tag-header) stream &key (indent 0))
- "Set *pprint-mp3-frame* to get pretty printing and call print-object via format"
- (let ((*pprint-mp3-frame* t))
- (format stream "~vt~a" (* indent 1) me)))
- (defmethod print-object ((me v21-tag-header) stream)
- (if (null *pprint-mp3-frame*)
- (call-next-method)
- (with-slots (songname artist album year comment genre) me
- (format stream "songname = <~a>, artist = <~a>, album = <~a>, year = <~a>, comment = <~a>, genre = ~d"
- songname artist album year comment genre))))
- (defmethod initialize-instance ((me v21-tag-header) &key instream)
- "Read in a V2.1 tag. Caller will have stream-seek'ed file to correct location and ensured that TAG was present"
- (log5:with-context "v21-frame-initializer"
- (log-mp3-frame "reading v2.1 tag")
- (with-slots (songname artist album year comment genre) me
- (setf songname (stream-read-string instream :size 30 :terminators '(0)))
- (setf artist (stream-read-string instream :size 30 :terminators '(0)))
- (setf album (stream-read-string instream :size 30 :terminators '(0)))
- (setf year (stream-read-string instream :size 4 :terminators '(0)))
- (setf comment (stream-read-string instream :size 30 :terminators '(0)))
- (setf genre (stream-read-u8 instream))
- (log-mp3-frame "v21 tag: ~a" (vpprint me nil)))))
- (defclass mp3-ext-header ()
- ((size :accessor size :initarg :size :initform 0)
- (flags :accessor flags :initarg :flags :initform 0)
- (padding :accessor padding :initarg :padding :initform 0)
- (crc :accessor crc :initarg :crc :initform nil))
- (:documentation "class representing a V2.3/4 extended header"))
- (defmethod vpprint ((me mp3-ext-header) stream &key (indent 0))
- "Set *pprint-mp3-frame* to get pretty printing and call print-object via format"
- (let ((*pprint-mp3-frame* t))
- (format stream "~vt~a" (* indent 1) me)))
- (defmacro ext-header-crc-p (flags) `(logbitp 15 ,flags))
- (defmethod initialize-instance ((me mp3-ext-header) &key instream)
- "Read in the extended header. Caller will have stream-seek'ed to correct location in file."
- (with-slots (size flags padding crc) me
- (setf size (stream-read-u32 instream))
- (setf flags (stream-read-u16 instream))
- (setf padding (stream-read-u32 instream))
- (when (ext-header-crc-p flags)
- (setf crc (stream-read-u32 instream)))))
- (defmethod print-object ((me mp3-ext-header) stream)
- (if (null *pprint-mp3-frame*)
- (call-next-method)
- (with-slots (size flags padding crc) me
- (format stream "extended header: size: ~d, flags: ~x, padding ~:d, crc = ~x~%"
- size flags padding crc))))
- (defmacro header-unsynchronized-p (flags) `(logbitp 7 ,flags))
- (defmacro header-extended-p (flags) `(logbitp 6 ,flags))
- (defmacro header-experimental-p (flags) `(logbitp 5 ,flags))
- (defmacro header-footer-p (flags) `(logbitp 4 ,flags)) ;; N.B. *NOT* defined for 2.3 tags
- (defmacro print-header-flags (stream flags)
- `(format ,stream "0x~2,'0x: ~:[0/~;unsynchronized-frames/~]~:[0/~;extended-header/~]~:[0/~;expermental-tag/~]~:[0~;footer-present~]"
- ,flags
- (header-unsynchronized-p ,flags)
- (header-extended-p ,flags)
- (header-experimental-p ,flags)
- (header-footer-p ,flags)))
- (defmethod print-object ((me mp3-id3-header) stream)
- (if (null *pprint-mp3-frame*)
- (call-next-method)
- (with-slots (version revision flags v21-tag-header size ext-header frames) me
- (format stream "Header: version/revision: ~d/~d, flags: ~a, size = ~:d bytes; ~a; ~a"
- version revision (print-header-flags nil flags) size
- (if (header-extended-p flags)
- (concatenate 'string "Extended header: " (vpprint ext-header nil))
- "No extended header")
- (if v21-tag-header
- (concatenate 'string "V21 tag: " (vpprint v21-tag-header nil))
- "No v21 tag"))
- (when frames
- (format stream "~4tFrames[~d]:~%~{~8t~a~^~%~}" (length frames) frames)))))
- (defmethod initialize-instance :after ((me mp3-id3-header) &key instream &allow-other-keys)
- "Fill in an mp3-header from file."
- (log5:with-context "mp3-id3-header-initializer"
- (with-slots (version revision flags size ext-header frames v21-tag-header) me
- (stream-seek instream 128 :end)
- (when (string= "TAG" (stream-read-string instream :size 3))
- (log-mp3-frame "looking at last 128 bytes at ~:d to try to read id3v21 header" (stream-seek instream 0 :current))
- (handler-case
- (setf v21-tag-header (make-instance 'v21-tag-header :instream instream))
- (condition (c)
- (log-mp3-frame "reading v21 got condition: ~a" c))))
- (stream-seek instream 0 :start)
- (when (string= "ID3" (stream-read-string instream :size 3))
- (setf version (stream-read-u8 instream))
- (setf revision (stream-read-u8 instream))
- (setf flags (stream-read-u8 instream))
- (setf size (stream-read-sync-safe-u32 instream))
- (when (header-unsynchronized-p flags) (log-mp3-frame "unsync"))
- (assert (not (header-footer-p flags)) () "Can't decode ID3 footer's yet")
- (when (header-extended-p flags)
- (setf ext-header (make-instance 'mp3-extended-header :instream instream))))
- (log-mp3-frame "~a" (vpprint me nil)))))
- (defclass id3-frame ()
- ((pos :accessor pos :initarg :pos)
- (version :accessor version :initarg :version)
- (id :accessor id :initarg :id)
- (len :accessor len :initarg :len)
- (flags :accessor flags :initarg :flags :initform nil))
- (:documentation "Base class for an ID3 frame"))
- (defmacro frame-23-altertag-p (frame-flags) `(logbitp 15 ,frame-flags))
- (defmacro frame-23-alterfile-p (frame-flags) `(logbitp 14 ,frame-flags))
- (defmacro frame-23-readonly-p (frame-flags) `(logbitp 13 ,frame-flags))
- (defmacro frame-23-compress-p (frame-flags) `(logbitp 7 ,frame-flags))
- (defmacro frame-23-encrypt-p (frame-flags) `(logbitp 6 ,frame-flags))
- (defmacro frame-23-group-p (frame-flags) `(logbitp 5 ,frame-flags))
- (defmacro frame-24-altertag-p (frame-flags) `(logbitp 14 ,frame-flags))
- (defmacro frame-24-alterfile-p (frame-flags) `(logbitp 13 ,frame-flags))
- (defmacro frame-24-readonly-p (frame-flags) `(logbitp 12 ,frame-flags))
- (defmacro frame-24-groupid-p (frame-flags) `(logbitp 6 ,frame-flags))
- (defmacro frame-24-compress-p (frame-flags) `(logbitp 3 ,frame-flags))
- (defmacro frame-24-encrypt-p (frame-flags) `(logbitp 2 ,frame-flags))
- (defmacro frame-24-unsynch-p (frame-flags) `(logbitp 1 ,frame-flags))
- (defmacro frame-24-datalen-p (frame-flags) `(logbitp 0 ,frame-flags))
- (defun valid-frame-flags (header-version frame-flags)
- (ecase header-version
- (3 (zerop (logand #b0001111100011111 frame-flags)))
- (4 (zerop (logand #b1000111110110000 frame-flags)))))
- (defmethod print-object ((me id3-frame) stream)
- (if (null *pprint-mp3-frame*)
- (call-next-method)
- (with-slots (pos version valid-p id len flags) me
- (format stream "@offset: ~:d, version = ~d, id: ~s, len: ~:d "
- pos version id len)
- (if flags
- (ecase version
- (3 (format stream "flags: 0x~4,'0x: ~:[0/~;tag-alter-preservation/~]~:[0/~;file-alter-preservation/~]~:[0/~;read-only/~]~:[0/~;compress/~]~:[0/~;encypt/~]~:[0~;group~], "
- flags
- (frame-23-altertag-p flags)
- (frame-23-alterfile-p flags)
- (frame-23-readonly-p flags)
- (frame-23-compress-p flags)
- (frame-23-encrypt-p flags)
- (frame-23-group-p flags)))
- (4 (format stream "flags: 0x~4,'0x: ~:[0/~;tag-alter-preservation/~]~:[0/~;file-alter-preservation/~]~:[0/~;read-only/~]~:[0/~;group-id/~]~:[0/~;compress/~]~:[0/~;encypt/~]~:[0/~;unsynch/~]~:[0~;datalen~], "
- flags
- (frame-24-altertag-p flags)
- (frame-24-alterfile-p flags)
- (frame-24-readonly-p flags)
- (frame-24-groupid-p flags)
- (frame-24-compress-p flags)
- (frame-24-encrypt-p flags)
- (frame-24-unsynch-p flags)
- (frame-24-datalen-p flags))))))))
- (defclass raw-frame (id3-frame)
- ((octets :accessor octets :initform nil))
- (:documentation "Frame class that slurps in frame contents"))
- (defmethod initialize-instance :after ((me raw-frame) &key instream)
- (log5:with-context "raw-frame"
- (with-slots (len octets) me
- (log-mp3-frame "reading ~:d bytes from position ~:d" len (stream-seek instream 0 :current))
- (setf octets (stream-read-octets instream len)))))
- (defmethod print-object :after ((me raw-frame) stream)
- (if (null *pprint-mp3-frame*)
- (call-next-method)
- (with-slots (octets) me
- (let* ((len (length (slot-value me 'octets)))
- (print-len (min len 10))
- (printable-array (make-array print-len :displaced-to (slot-value me 'octets))))
- (format stream "[~:d of ~:d bytes] <~x>" print-len len printable-array)))))
- (defun find-mp3-frames (mp3-file)
- "With an open mp3-file, make sure it is in fact an MP3 file, then read it's header and frames, returning both"
- (log5:with-context "find-mp3-frames"
- (when (not (is-valid-mp3-file mp3-file))
- (log-mp3-frame "~a is not an mp3 file" (filename mp3-file))
- (error 'mp3-frame-condition :location "find-mp3-frames" :object (filename mp3-file) :message "is not an mp3 file"))
- (log-mp3-frame "~a is a valid mp3 file" (filename mp3-file))
- (let ((header (make-instance 'mp3-id3-header :instream mp3-file))
- (mem-stream)
- (this-frame)
- (frames))
- (declare (ignore mem-stream this-frame frames))
- (setf (slot-value mp3-file 'mp3-header) header)
- (assert header () "Must have a header to continue!")
- header)))
- ;; (if (header-unsynchronized-p header)
- ;; (setf mem-stream (stream-read-sync-safe-octets instream (size header)))
- ;; (setf mem-stream instream))
- ;; ;; NB from this point, always read from mem-stream (see IF above)
- ;; (block read-loop
- ;; (loop
- ;; (setf this-frame (make-frame header mem-stream))
- ;; (when (null this-frame)
- ;; (return-from read-loop nil))
- ;; (push this-frame frames)))
- ;; (setf (slot-value (slot-value mp3-header 'header) 'frames) frames)
- ;; (log-mp3-frame "~a" (vpprint (slot-value mp3-header 'header) nil))))))
|