ソースを参照

Changed over to (specially patched) flexi-streams and reworked logic to decouple streams from tag info

Mark VandenBrink 12 年 前
コミット
7a7720cf6b
9 ファイル変更601 行追加574 行削除
  1. 12 5
      README.md
  2. 132 136
      abstract-tag.lisp
  3. 166 198
      audio-streams.lisp
  4. 38 26
      flac-frame.lisp
  5. 89 63
      id3-frame.lisp
  6. 29 21
      mp4-atom.lisp
  7. 31 21
      mpeg.lisp
  8. 42 59
      packages.lisp
  9. 62 45
      taglib-tests.lisp

+ 12 - 5
README.md

@@ -6,15 +6,16 @@ A pure Lisp implementation for reading audio tags and audio information.
 
 Currently reads MP3/MP4/FLAC audio files.
 
-**Mostly complete.  Your mileage may vary.
-**Runs (in single-thread mode) under CCL, SBCL, ECL, CLISP, and ABCL
-  Note: CCL works well.  I'm still in progress of making the others work well. YMMV.
+Runs (in single-thread mode) under CCL, SBCL, CLISP, and ABCL
+Note: my primary Lisp variant is CCL, so it's the most tested; however,
+this code should run on any Lisp that is supported by FLEXI-STREAMS
 
 # Dependencies
 
 All avalailable via quicklisp
 
-* optima and optima.ppcre (for quick parsing of FLAC tags)
+* optima and optima.ppcre: for quick parsing of FLAC tags
+* flexi-streams: for in-memory streams
 
 # References
 
@@ -175,7 +176,13 @@ Header: version/revision: 3/0, flags: 0x00: 0/0/0/0, size = 11,899 bytes; No ext
 
 ## Experimental Stuff
 
-I've recently added some (very) rudimentary multi-threading (see taglib-tests.lisp) using the CHANL package.  First, the filesystem
+**Multi-threading currently broken.
+
+I've recently added some (very) rudimentary multi-threading (see taglib-tests.lisp) using the CHANL package.
+
+CURRENTLY BROKEN
+
+First, the filesystem
 walker (main thread) walks the requested directory, adding each filename to an unbounded channel (\*channel\*).  The main thread then sends
 \*MAX-THREADS\* \*END-THREAD\* symbols, creates \*MAX-THREADS\* worker threads who read from the channel, and then sits in a loop reading
 from \*dead-channel\* until it has done \*MAX-THREADS\* recv's.

+ 132 - 136
abstract-tag.lisp

@@ -30,7 +30,8 @@
     "Merengue" "Salsa" "Thrash Metal" "Anime" "Jpop" "Synthpop"))
 
 (defun find-genre (name)
-  "For debug purpose only: test function to return index of genre, given a name. ignores case and returns first complete match"
+  "For debug purpose only: test function to return index of genre, given a name.
+Ignores case and returns first complete match"
   (let ((i 0)
         (match-str (string-downcase name)))
     (loop for s across *id3v1-genres* do
@@ -46,8 +47,7 @@
         "BAD GENRE"
         (aref *id3v1-genres* n)))
 
-;;; The abstract tag interface
-(defgeneric album (stream))
+;;;; The abstract tag interface
 (defgeneric album (stream))
 (defgeneric artist (stream))
 (defgeneric comment (stream))
@@ -65,86 +65,81 @@
 (defgeneric tempo (stream))
 (defgeneric genre (stream))
 
-;;;;;;;;;;;;;;;;;;;; MP3 ;;;;;;;;;;;;;;;;;;;;
-(defun get-frames (stream names)
-  "Given a MP3-STREAM, search its frames for NAMES.  Return file-order list of matching frames"
-  (let (found-frames)
-    (map-id3-frames stream
-                    :func (lambda (f)
-                            (when (member (id f) names :test #'string=)
-                              (push f found-frames))))
-    (nreverse found-frames)))
-
-(defmethod cover ((me mp3-file-stream))
+;;;; MP3
+(defmethod cover ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
   (let ((pictures)
-        (frames (get-frames me '("PIC" "APIC"))))
+        (frames (id3-frame:get-frames me '("PIC" "APIC"))))
     (when frames
       (dolist (f frames)
-        (push (picture-info f) pictures)))
+        (push (id3-frame:picture-info f) pictures)))
     pictures))
 
-(defmethod album ((me mp3-file-stream))
+(defmethod album ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TAL" "TALB"))))
+  (let ((frames (id3-frame:get-frames me '("TAL" "TALB"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one album tag")
-      (return-from album (info (first frames)))))
-  (if (v21-tag-header (id3-header me))
-      (album (v21-tag-header (id3-header me)))
+      (return-from album (id3-frame:info (first frames)))))
+  (if (id3-frame:v21-tag-header (id3-frame:id3-header me))
+      (id3-frame:album (id3-frame:v21-tag-header (id3-frame:id3-header me)))
       nil))
 
-(defmethod artist ((me mp3-file-stream))
+(defmethod artist ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TP1" "TPE1"))))
+  (let ((frames (id3-frame:get-frames me '("TP1" "TPE1"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one artist tag")
-      (return-from artist (info (first frames)))))
-  (if (v21-tag-header (id3-header me))
-      (artist (v21-tag-header (id3-header me)))
+      (return-from artist (id3-frame:info (first frames)))))
+  (if (id3-frame:v21-tag-header (id3-frame:id3-header me))
+      (id3-frame:artist (id3-frame:v21-tag-header (id3-frame:id3-header me)))
       nil))
 
-(defmethod comment ((me mp3-file-stream))
+(defmethod comment ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("COM" "COMM"))))
+  (let ((frames (id3-frame:get-frames me '("COM" "COMM"))))
     (when frames
       (let ((new-frames))
         (dolist (f frames)
-          (push (list (encoding f) (lang f) (desc f) (val f)) new-frames))
+          (push (list (id3-frame:encoding f)
+                      (id3-frame:lang f)
+                      (id3-frame:desc f)
+                      (id3-frame:val f)) new-frames))
         (return-from comment new-frames))))
-  (if (v21-tag-header (id3-header me))
-      (comment (v21-tag-header (id3-header me)))
+  (if (id3-frame:v21-tag-header (id3-frame:id3-header me))
+      (id3-frame:comment (id3-frame:v21-tag-header (id3-frame:id3-header me)))
       nil))
 
-(defmethod year ((me mp3-file-stream))
+(defmethod year ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TRD" "TDRC"))))
+  (let ((frames (id3-frame:get-frames me '("TRD" "TDRC"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one year tag")
-      (return-from year (info (first frames)))))
-  (if (v21-tag-header (id3-header me))
-      (year (v21-tag-header (id3-header me)))
+      (return-from year (id3-frame:info (first frames)))))
+  (if (id3-frame:v21-tag-header (id3-frame:id3-header me))
+      (id3-frame:year (id3-frame:v21-tag-header (id3-frame:id3-header me)))
       nil))
 
-(defmethod title ((me mp3-file-stream))
+(defmethod title ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TT2" "TIT2"))))
+  (let ((frames (id3-frame:get-frames me '("TT2" "TIT2"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one title tag")
-      (return-from title (info (first frames)))))
-  (if (v21-tag-header (id3-header me))
-      (title (v21-tag-header (id3-header me)))
+      (return-from title (id3-frame:info (first frames)))))
+  (if (id3-frame:v21-tag-header (id3-frame:id3-header me))
+      (id3-frame:title (id3-frame:v21-tag-header (id3-frame:id3-header me)))
       nil))
 
-(defmethod genre ((me mp3-file-stream))
+(defmethod genre ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TCO" "TCON"))))
+  (let ((frames (id3-frame:get-frames me '("TCO" "TCON"))))
     (when frames
       (when (> (length frames) 1)
-        (warn-user "file ~a has more than one genre frame, will only use the first" (stream-filename me)))
+        (warn-user "file ~a has more than one genre frame, will only use the first"
+                   (id3-frame:filename me)))
       (let ((count)
             (end)
-            (str (info (first frames))))
+            (str (id3-frame:info (first frames))))
 
         ;; For V23/V24 TCON frames, a genre can be pretty gnarly.
         ;; if the first byte of the TCON INFO field is a '(', what is between this '('
@@ -161,88 +156,88 @@
           (setf str (get-id3v1-genre (parse-integer (subseq str 1 end)))))
         (return-from genre str))))
 
-  (if (v21-tag-header (id3-header me))
-      (get-id3v1-genre (genre (v21-tag-header (id3-header me))))
+  (if (id3-frame:v21-tag-header (id3-frame:id3-header me))
+      (get-id3v1-genre (genre (id3-frame:v21-tag-header (id3-frame:id3-header me))))
       nil))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; no V2.1 tags for any of these ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(defmethod album-artist ((me mp3-file-stream))
+(defmethod album-artist ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TP2" "TPE2"))))
+  (let ((frames (id3-frame:get-frames me '("TP2" "TPE2"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one album-artist tag")
-      (return-from album-artist (info (first frames)))))
+      (return-from album-artist (id3-frame:info (first frames)))))
   nil)
 
-(defmethod composer ((me mp3-file-stream))
+(defmethod composer ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TCM" "TCOM"))))
+  (let ((frames (id3-frame:get-frames me '("TCM" "TCOM"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one composer tag")
-      (return-from composer (info (first frames)))))
+      (return-from composer (id3-frame:info (first frames)))))
   nil)
 
-(defmethod copyright ((me mp3-file-stream))
+(defmethod copyright ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TCR" "TCOP"))))
+  (let ((frames (id3-frame:get-frames me '("TCR" "TCOP"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one copyright tag")
-      (return-from copyright (info (first frames)))))
+      (return-from copyright (id3-frame:info (first frames)))))
   nil)
 
-(defmethod encoder ((me mp3-file-stream))
+(defmethod encoder ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TEN" "TENC"))))
+  (let ((frames (id3-frame:get-frames me '("TEN" "TENC"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one encoder tag")
-      (return-from encoder (info (first frames)))))
+      (return-from encoder (id3-frame:info (first frames)))))
   nil)
 
-(defmethod groups ((me mp3-file-stream))
+(defmethod groups ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TT1" "TTE1"))))
+  (let ((frames (id3-frame:get-frames me '("TT1" "TTE1"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one group tag")
-      (return-from groups (info (first frames)))))
+      (return-from groups (id3-frame:info (first frames)))))
   nil)
 
-(defmethod lyrics ((me mp3-file-stream))
+(defmethod lyrics ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("ULT" "USLT"))))
+  (let ((frames (id3-frame:get-frames me '("ULT" "USLT"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one lyrics tag")
-      (return-from lyrics (val (first frames)))))
+      (return-from lyrics (id3-frame:val (first frames)))))
   nil)
 
-(defmethod writer ((me mp3-file-stream))
+(defmethod writer ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TCM" "TCOM"))))
+  (let ((frames (id3-frame:get-frames me '("TCM" "TCOM"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one composer tag")
-      (return-from writer (info (first frames)))))
+      (return-from writer (id3-frame:info (first frames)))))
   nil)
 
-(defmethod compilation ((me mp3-file-stream))
+(defmethod compilation ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TCMP" "TCP"))))
+  (let ((frames (id3-frame:get-frames me '("TCMP" "TCP"))))
     (if frames
-      (info (first frames))
+        (id3-frame:info (first frames))
       "no")))
 
-(defmethod disk ((me mp3-file-stream))
+(defmethod disk ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TPA" "TPOS"))))
+  (let ((frames (id3-frame:get-frames me '("TPA" "TPOS"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one disk number tag")
-      (return-from disk (mk-lst (info (first frames))))))
+      (return-from disk (mk-lst (id3-frame:info (first frames))))))
   nil)
 
-(defmethod tempo ((me mp3-file-stream))
+(defmethod tempo ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TBP" "TBPM"))))
+  (let ((frames (id3-frame:get-frames me '("TBP" "TBPM"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one tempo tag")
-      (return-from tempo (info (first frames)))))
+      (return-from tempo (id3-frame:info (first frames)))))
   nil)
 
 (defun mk-lst (str)
@@ -252,24 +247,24 @@
         (list str)
         (list (subseq str 0 pos) (subseq str (+ 1 pos))))))
 
-(defmethod track ((me mp3-file-stream))
+(defmethod track ((me id3-frame:mp3-file))
   (declare #.utils:*standard-optimize-settings*)
-  (let ((frames (get-frames me '("TRK" "TRCK"))))
+  (let ((frames (id3-frame:get-frames me '("TRK" "TRCK"))))
     (when frames
       (assert (= 1 (length frames)) () "There can be only one track number tag")
-      (return-from track (mk-lst (info (first frames))))))
+      (return-from track (mk-lst (id3-frame:info (first frames))))))
   nil)
 
-(defmethod show-tags ((me mp3-file-stream) &key (raw *raw-tags*))
-  "Show the tags for an mp3-file.  If RAW is non-nil, dump all the frames; else, print out a subset."
+(defmethod show-tags ((me id3-frame:mp3-file) &key (raw *raw-tags*))
+  "Show the tags for an MP3.  If RAW is non-nil, dump all the frames; else, print out a subset."
   (declare #.utils:*standard-optimize-settings*)
   (if raw
-      (format t "~a~%~a~%" (stream-filename me)
+      (format t "~a~%~a~%" (id3-frame:filename me)
               (with-output-to-string (s)
-                (when (audio-info me)
-                  (mpeg::vpprint (audio-info me) s)
+                (when (id3-frame:audio-info me)
+                  (mpeg::vpprint (id3-frame:audio-info me) s)
                   (format s "~%"))
-                (vpprint (id3-header me) s)))
+                (id3-frame:vpprint (id3-frame:id3-header me) s)))
       (let ((album (album me))
             (album-artist (album-artist me))
             (artist (artist me))
@@ -288,9 +283,9 @@
             (track (track me))
             (writer (writer me))
             (year (year me)))
-        (format t "~a~%~a~%" (stream-filename me)
-                (if (audio-info me)
-                    (mpeg::vpprint (audio-info me) nil) ""))
+        (format t "~a~%~a~%" (id3-frame:filename me)
+                (if (id3-frame:audio-info me)
+                    (mpeg::vpprint (id3-frame:audio-info me) nil) ""))
         (when album (format t "~4talbum: ~a~%" album))
         (when album-artist (format t "~4talbum-artist: ~a~%" album-artist))
         (when artist (format t "~4tartist: ~a~%" artist))
@@ -311,45 +306,46 @@
         (when year (format t "~4tyear: ~a~%" year)))))
 
 ;;;;;;;;;;;;;;;;;;;; MP4 ;;;;;;;;;;;;;;;;;;;;
-(defmethod album        ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-album+))
-(defmethod album-artist ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-album-artist+))
-(defmethod artist       ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-artist+))
-(defmethod comment      ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-comment+))
-(defmethod composer     ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-composer+))
-(defmethod copyright    ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-copyright+))
-;;;(defmethod cover        ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-cover-art+))
-(defmethod year         ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-year+))
-(defmethod encoder      ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-encoder+))
-(defmethod groups       ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-groups+))
-(defmethod lyrics       ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-lyrics+))
-(defmethod title        ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-title+))
-(defmethod writer       ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-writer+))
-(defmethod compilation  ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-compilation+))
-(defmethod disk         ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-disk+))
-(defmethod tempo        ((me mp4-file-stream)) (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-tempo+))
-(defmethod genre        ((me mp4-file-stream))
-  (let ((genre   (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-genre+))
-        (genre-x (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-genre-x+)))
+(defmethod album        ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-album+))
+(defmethod album-artist ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-album-artist+))
+(defmethod artist       ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-artist+))
+(defmethod comment      ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-comment+))
+(defmethod composer     ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-composer+))
+(defmethod copyright    ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-copyright+))
+;;;(defmethod cover        ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-cover-art+))
+(defmethod year         ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-year+))
+(defmethod encoder      ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-encoder+))
+(defmethod groups       ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-groups+))
+(defmethod lyrics       ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-lyrics+))
+(defmethod title        ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-title+))
+(defmethod writer       ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-writer+))
+(defmethod compilation  ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-compilation+))
+(defmethod disk         ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-disk+))
+(defmethod tempo        ((me mp4-atom:mp4-file)) (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-tempo+))
+(defmethod genre        ((me mp4-atom:mp4-file))
+  (let ((genre   (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-genre+))
+        (genre-x (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-genre-x+)))
     (assert (not (and genre genre-x)))
     (cond
       (genre   (format nil "~d (~a)" genre (get-id3v1-genre (1- genre))))
       (genre-x genre-x)
       (t       "not present"))))
-(defmethod track ((me mp4-file-stream))
-  (let ((track   (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-track+))
-        (track-n (mp4-atom:tag-get-value (mp4-atoms me) mp4-atom:+itunes-track-n+)))
+(defmethod track ((me mp4-atom:mp4-file))
+  (let ((track   (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-track+))
+        (track-n (mp4-atom:tag-get-value (mp4-atom:mp4-atoms me) mp4-atom:+itunes-track-n+)))
     (assert (not (and track track-n)))
     (if track
         track
         track-n)))
 
-(defmethod show-tags ((me mp4-file-stream) &key (raw *raw-tags*))
-  "Show the tags for an MP4-FILE. If RAW is non-nil, dump the DATA atoms; else show subset of DATA atoms"
-  (format t "~a~%" (stream-filename me))
+(defmethod show-tags ((me mp4-atom:mp4-file) &key (raw *raw-tags*))
+  "Show the tags for an MP4-FILE. If RAW is non-nil, dump the DATA atoms;
+else show subset of DATA atoms"
+  (format t "~a~%" (mp4-atom:filename me))
   (if raw
       (progn
-        (if (audio-info me)
-            (mp4-atom:vpprint (audio-info me) t))
+        (if (mp4-atom:audio-info me)
+            (mp4-atom:vpprint (mp4-atom:audio-info me) t))
         (mp4-atom:mp4-show-raw-tag-atoms me t))
       (let ((album (album me))
             (album-artist (album-artist me))
@@ -370,8 +366,8 @@
             (writer (writer me))
             (year (year me)))
 
-        (if (audio-info me)
-          (mp4-atom:vpprint (audio-info me) t))
+        (if (mp4-atom:audio-info me)
+            (mp4-atom:vpprint (mp4-atom:audio-info me) t))
 
         (when album (format t "~&~4talbum: ~a~%" album))
         (when album-artist (format t "~4talbum-artist: ~a~%" album-artist))
@@ -394,26 +390,26 @@
 
 ;;;;;;;;;;;;;;;;;;;; FLAC ;;;;;;;;;;;;;;;;;;;;
 (defmacro get-flac-tag-info (stream name)
-  `(flac-frame:flac-get-tag (flac-tags ,stream) ,name))
-
-(defmethod album        ((me flac-file-stream)) (get-flac-tag-info me "album"))
-(defmethod artist       ((me flac-file-stream)) (get-flac-tag-info me "artist"))
-(defmethod album-artist ((me flac-file-stream)) (get-flac-tag-info me "album artist"))
-(defmethod comment      ((me flac-file-stream)) (get-flac-tag-info me "comment"))
-(defmethod composer     ((me flac-file-stream)) (get-flac-tag-info me "composer"))
-(defmethod copyright    ((me flac-file-stream)) (get-flac-tag-info me "copyright"))
-(defmethod disk         ((me flac-file-stream)) (get-flac-tag-info me "disk"))
-(defmethod encoder      ((me flac-file-stream)) (get-flac-tag-info me "encoder"))
-(defmethod year         ((me flac-file-stream)) (get-flac-tag-info me "date"))
-(defmethod title        ((me flac-file-stream)) (get-flac-tag-info me "title"))
-(defmethod genre        ((me flac-file-stream)) (get-flac-tag-info me "genre"))
-(defmethod track        ((me flac-file-stream)) (let ((tr (get-flac-tag-info me "tracknumber"))
+  `(flac-frame:flac-get-tag (flac-frame:flac-tags ,stream) ,name))
+
+(defmethod album        ((me flac-frame:flac-file)) (get-flac-tag-info me "album"))
+(defmethod artist       ((me flac-frame:flac-file)) (get-flac-tag-info me "artist"))
+(defmethod album-artist ((me flac-frame:flac-file)) (get-flac-tag-info me "album artist"))
+(defmethod comment      ((me flac-frame:flac-file)) (get-flac-tag-info me "comment"))
+(defmethod composer     ((me flac-frame:flac-file)) (get-flac-tag-info me "composer"))
+(defmethod copyright    ((me flac-frame:flac-file)) (get-flac-tag-info me "copyright"))
+(defmethod disk         ((me flac-frame:flac-file)) (get-flac-tag-info me "disk"))
+(defmethod encoder      ((me flac-frame:flac-file)) (get-flac-tag-info me "encoder"))
+(defmethod year         ((me flac-frame:flac-file)) (get-flac-tag-info me "date"))
+(defmethod title        ((me flac-frame:flac-file)) (get-flac-tag-info me "title"))
+(defmethod genre        ((me flac-frame:flac-file)) (get-flac-tag-info me "genre"))
+(defmethod track        ((me flac-frame:flac-file)) (let ((tr (get-flac-tag-info me "tracknumber"))
                                                       (tn (get-flac-tag-info me "tracktotal")))
                                                   (if tn (list tr tn) tr)))
 
-(defmethod show-tags ((me flac-file-stream) &key (raw *raw-tags*))
+(defmethod show-tags ((me flac-frame:flac-file) &key (raw *raw-tags*))
   "Show the tags for a FLAC-FILE."
-  (format t "~a~%" (stream-filename me))
+  (format t "~a~%" (flac-frame:filename me))
   (if raw
       (flac-frame:flac-show-raw-tag me t)
       (let ((album (album me))
@@ -428,8 +424,8 @@
             (track (track me))
             (year (year me)))
 
-        (if (audio-info me)
-            (flac-frame:vpprint (audio-info me) t))
+        (if (flac-frame:audio-info me)
+            (flac-frame:vpprint (flac-frame:audio-info me) t))
 
         (when album (format t "~&~4talbum: ~a~%" album))
         (when album-artist (format t "~4talbum-artist: ~a~%" album-artist))

+ 166 - 198
audio-streams.lisp

@@ -4,167 +4,130 @@
 (in-package #:audio-streams)
 
 (deftype octet () '(unsigned-byte 8))
+(deftype octets () '(simple-array octet (*)))
 (defmacro make-octets (len) `(make-array ,len :element-type 'octet))
 
-(defclass mem-stream ()
-   ((stream-filename :accessor stream-filename :initform nil :initarg :stream-filename :documentation "if set, then MMAP file")
-    (index           :accessor index           :initform 0)
-    (stream-size     :accessor stream-size     :initform 0)
-    (vect            :accessor vect            :initform nil :initarg :vect :documentation "if set, then the vector we want STREAM-ize"))
-   (:documentation "A thin-wrapper class over mmaped-files and/or vectors."))
-
-(defmacro with-mem-stream-slots ((instance) &body body)
-  `(with-slots (stream-filename index stream-size vect) ,instance
-     (declare (fixnum index stream-size)
-              (type (or (array (unsigned-byte 8) 1) null) vect))
-     ,@body))
-
-(defun make-mem-stream (v) (make-instance 'mem-stream :vect v))
-(defun make-mmap-stream (f) (make-instance 'mem-stream :stream-filename f))
-
-(defmethod initialize-instance :after ((stream mem-stream) &key)
-  "Stream initializer. If STREAM-FILENAME is set, MMAP a the file. Else, we assume VECT was set."
-  (with-mem-stream-slots (stream)
-    (when stream-filename
-      #+CCL (setf vect (ccl:map-file-to-octet-vector stream-filename))
-      #-CCL (setf vect (alexandria:read-file-into-byte-vector stream-filename))
-      )
-    (setf stream-size (length vect))))
-
-(defmethod stream-close ((stream mem-stream))
-  "Close a stream, making the underlying object (file or vector) inaccessible."
+(defun make-audio-stream (arg)
+  "Creates a stream for ARG"
   (declare #.utils:*standard-optimize-settings*)
-  (with-mem-stream-slots (stream)
-    #+CCL (when stream-filename (ccl:unmap-octet-vector vect))
-    (setf vect nil)))
-
-;;; finding out current file position is so common, we also
-;;; provide a macro.  For some reason, SBCL claims this
-;;; macro doesn't exist, so I just defun'ed it.
-#+CCL (defmacro stream-here (stream) `(index ,stream))
-#-CCL (defun stream-here (stream) (index stream))
-
-(defmethod stream-seek ((stream mem-stream) &optional (offset 0) (from :current))
-  "Set INDEX to requested value.  No error checking done here, but subsequent reads will fail if INDEX is out-of-bounds.
-As a convenience, OFFSET and FROM are optional, so (STREAM-SEEK stream) returns the current read-offset in stream."
+  (labels ((make-file-stream (name)
+             (let ((fd (open name :direction :input :element-type 'octet)))
+               (if fd
+                   (flex:make-flexi-stream fd :element-type 'octet)
+                   nil))))
+    (etypecase arg
+      (string (make-file-stream arg))
+      (pathname (make-file-stream arg))
+      (octets (flex:make-in-memory-input-stream arg)))))
+
+(defgeneric stream-size (stream))
+
+(defmethod stream-size ((stream flex:flexi-input-stream))
+  (declare #.utils:*standard-optimize-settings*)
+  (file-length (flex:flexi-stream-stream stream)))
+
+(defmethod stream-size ((stream flex:in-memory-stream))
+  (declare #.utils:*standard-optimize-settings*)
+  (flex::vector-stream-end stream))
+
+(defgeneric stream-filename (stream))
+
+(defmethod stream-filename ((stream flex:flexi-stream))
+  (declare #.utils:*standard-optimize-settings*)
+  (pathname (flex:flexi-stream-stream stream)))
+
+(defgeneric stream-seek (stream &optional offset from))
+
+(defmethod stream-seek ((stream flex:flexi-stream)
+                        &optional (offset 0) (from :current))
+  "Move the FILE-POSITION of a file"
   (declare #.utils:*standard-optimize-settings*)
   (declare (fixnum offset))
-  (with-mem-stream-slots (stream)
-    (ecase from
-      (:start                  ; INDEX set to OFFSET from start of stream
-       (setf index offset))
-      (:current                ; INDEX set relative to current INDEX, by OFFSET bytes
-       (if (zerop offset)
-           index
-           (incf index offset)))
-      (:end                    ; INDEX set to OFFSET from end of stream
-       (setf index (- stream-size offset))))))
+  (ecase from
+    (:start (file-position stream offset))
+    (:current (file-position stream (+ (file-position stream) offset)))
+    (:end (file-position stream (- (stream-size stream)
+                                   offset)))))
+
+(defmethod stream-seek ((stream flex:in-memory-input-stream)
+                        &optional (offset 0) (from :current))
+  "Move the index of an in-memory stream"
+  (declare #.utils:*standard-optimize-settings*)
+  (ecase from
+    (:start (file-position stream offset))
+    (:current (file-position stream (+ (file-position stream) offset)))
+    (:end (file-position stream (- (stream-size stream) offset)))))
 
 (declaim (inline read-n-bytes))
 
-(defun read-n-bytes (stream n-bytes &key (bits-per-byte 8) (endian :little-endian))
-  "Returns a FIXNUM constructed by reading N-BYTES.  BITS-PER-BYTE contols how many bits should be used from each read byte."
+(defun read-n-bytes (stream n-bytes
+                     &key (bits-per-byte 8) (endian :little-endian))
+  "Returns a FIXNUM constructed by reading N-BYTES.  BITS-PER-BYTE controls how
+many bits should be used from each read byte."
   (declare #.utils:*standard-optimize-settings*)
   (declare (fixnum n-bytes))
-  (with-mem-stream-slots (stream)
-    (when (<= (+ index n-bytes) stream-size)
-      (ecase endian
-        (:little-endian
-         (loop with value = 0
-               for low-bit downfrom (* bits-per-byte (1- n-bytes)) to 0 by bits-per-byte do
-                 (setf (ldb (byte bits-per-byte low-bit) value) (aref vect index))
-                 (incf index)
-               finally (return-from read-n-bytes value)))
-        (:big-endian
-         (loop with value = 0
-               for low-bit upfrom 0 to (* bits-per-byte (1- n-bytes)) by bits-per-byte do
-                 (setf (ldb (byte bits-per-byte low-bit) value) (aref vect index))
-                 (incf index)
-               finally (return-from read-n-bytes value))))))
-    nil)
+
+  (ecase endian
+    (:little-endian
+     (loop with value = 0
+           for low-bit downfrom (* bits-per-byte (1- n-bytes)) to 0
+             by bits-per-byte do
+               (awhen (read-byte stream nil nil)
+                 (setf (ldb (byte bits-per-byte low-bit) value) it))
+           finally (return-from read-n-bytes value)))
+    (:big-endian
+     (loop with value = 0
+           for low-bit upfrom 0 to (* bits-per-byte (1- n-bytes))
+             by bits-per-byte do
+               (awhen (read-byte stream nil nil)
+                 (setf (ldb (byte bits-per-byte low-bit) value) it))
+           finally (return-from read-n-bytes value)))))
 
 (defun stream-read-u8 (stream)
   (declare #.utils:*standard-optimize-settings*)
-  (with-mem-stream-slots (stream)
-    (if (<= (+ index 1) stream-size)
-        (let ((val (aref vect index)))
-          (incf index)
-          val)
-        nil)))
-
-(defun stream-read-u16  (stream &key (bits-per-byte 8) (endian :little-endian)) (read-n-bytes stream 2  :bits-per-byte bits-per-byte :endian endian))
-(defun stream-read-u24  (stream &key (bits-per-byte 8) (endian :little-endian)) (read-n-bytes stream 3  :bits-per-byte bits-per-byte :endian endian))
-(defun stream-read-u32  (stream &key (bits-per-byte 8) (endian :little-endian)) (read-n-bytes stream 4  :bits-per-byte bits-per-byte :endian endian))
-(defun stream-read-u64  (stream &key (bits-per-byte 8) (endian :little-endian)) (read-n-bytes stream 8  :bits-per-byte bits-per-byte :endian endian))
-(defun stream-read-u128 (stream &key (bits-per-byte 8) (endian :little-endian)) (read-n-bytes stream 16 :bits-per-byte bits-per-byte :endian endian))
-
-(defmethod stream-read-sequence ((stream mem-stream) size &key (bits-per-byte 8))
-  "Read in a sequence of octets at BITS-PER-BYTE.  If BITS-PER-BYTE == 8, then simply return
-a displaced array from STREAMs underlying vector.  If it is == 7, then we have to create a new vector and read into that."
+  (read-byte stream nil nil))
+
+(defun stream-read-u16  (stream &key (bits-per-byte 8) (endian :little-endian))
+  (read-n-bytes stream 2  :bits-per-byte bits-per-byte :endian endian))
+(defun stream-read-u24  (stream &key (bits-per-byte 8) (endian :little-endian))
+  (read-n-bytes stream 3  :bits-per-byte bits-per-byte :endian endian))
+(defun stream-read-u32  (stream &key (bits-per-byte 8) (endian :little-endian))
+  (read-n-bytes stream 4  :bits-per-byte bits-per-byte :endian endian))
+(defun stream-read-u64  (stream &key (bits-per-byte 8) (endian :little-endian))
+  (read-n-bytes stream 8  :bits-per-byte bits-per-byte :endian endian))
+(defun stream-read-u128 (stream &key (bits-per-byte 8) (endian :little-endian))
+  (read-n-bytes stream 16 :bits-per-byte bits-per-byte :endian endian))
+
+(defun stream-read-sequence (stream size &key (bits-per-byte 8))
+  "Read in a sequence of octets at BITS-PER-BYTE"
   (declare #.utils:*standard-optimize-settings*)
-  (with-mem-stream-slots (stream)
-      (when (> (+ index size) stream-size)
-        (setf size (- stream-size index)))
-      (ecase bits-per-byte
-        (8 (let ((octets (make-array size :element-type 'octet :displaced-to vect :displaced-index-offset index :adjustable nil)))
-             (incf index size)
-             (values octets size)))
-        (7
-         (let* ((last-byte-was-FF nil)
-                (byte nil)
-                (octets (flexi-streams:with-output-to-sequence (out)
-                          (dotimes (i size)
-                            (setf byte (stream-read-u8 stream))
-                            (if last-byte-was-FF
-                                (if (not (zerop byte))
-                                    (write-byte byte out))
-                                (write-byte byte out))
-                            (setf last-byte-was-FF (= byte #xFF))))))
-           (values octets size))))))
-
-(defclass mp3-file-stream (mem-stream)
-  ((id3-header :accessor id3-header :initform nil :documentation "holds all the ID3 info")
-   (audio-info :accessor audio-info :initform nil :documentation "holds the bit-rate, etc info"))
-  (:documentation "Stream for parsing MP3 files"))
-
-(defclass mp4-file-stream (mem-stream)
-  ((mp4-atoms  :accessor mp4-atoms  :initform nil :documentation "holds tree of parsed MP4 atoms/boxes")
-   (audio-info :accessor audio-info :initform nil :documentation "holds the bit-rate, etc info"))
-  (:documentation "Stream for parsing MP4 audio files"))
-
-(defclass flac-file-stream (mem-stream)
-  ((flac-headers :accessor flac-headers :initform nil :documentation "holds all the flac headers in file")
-   (audio-info   :accessor audio-info   :initform nil :documentation "parsed audio info")
-   (flac-tags    :accessor flac-tags    :initform nil :documentation "parsed comment tags."))
-  (:documentation "Stream for parsing flac files"))
-
-(defun make-file-stream (filename)
-  "Convenience function for creating a file stream. Detects file type and returns proper type stream."
-  (declare #.utils:*standard-optimize-settings*)
-  (let* ((new-stream (make-mmap-stream filename))
-         (ret-stream))
-
-    ;; detect file type and make RET-STREAM.  if we don't recognize stream, RET-STREAM will be NULL
-    (cond ((mp4-atom:is-valid-m4-file new-stream)
-           (setf ret-stream (make-instance 'mp4-file-stream :vect (vect new-stream) :stream-filename (stream-filename new-stream))))
-          ((flac-frame:is-valid-flac-file new-stream)
-           (setf ret-stream (make-instance 'flac-file-stream :vect (vect new-stream) :stream-filename (stream-filename new-stream))))
-          ((id3-frame:is-valid-mp3-file new-stream)
-           (setf ret-stream (make-instance 'mp3-file-stream :vect (vect new-stream) :stream-filename (stream-filename new-stream)))))
-    (stream-close new-stream)
-    ret-stream))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Strings ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+  (ecase bits-per-byte
+    (8 (let ((octets (make-octets size)))
+         (values octets (read-sequence octets stream))))
+    (7 (let* ((last-byte-was-FF nil)
+              (byte nil)
+              (octets (flex:with-output-to-sequence (out)
+                        (dotimes (i size)
+                          (setf byte (stream-read-u8 stream))
+                          (if last-byte-was-FF
+                              (if (not (zerop byte))
+                                  (write-byte byte out))
+                              (write-byte byte out))
+                          (setf last-byte-was-FF (= byte #xFF))))))
+         (values octets size)))))
+
+;;;; Strings
 
 ;;; Decode octets as an iso-8859-1 string (encoding == 0)
 (defun stream-decode-iso-string (octets &key (start 0) (end (length octets)))
   (declare #.utils:*standard-optimize-settings*)
-  (flexi-streams:octets-to-string octets :start start :end end :external-format :iso-8859-1))
+  (flex:octets-to-string octets :start start
+                                         :end end :external-format :iso-8859-1))
 
 ;;;
-;;; XXX: Coded this way because I can't seem to get a simple :external-format :ucs-2 to work correctly
-;;; AND some taggers encode a UCS-2 empty string w/o a byte-order mark (i.e. null strings are
-;;; sometimes encoded as #(00 00))
+;;; XXX: Coded this way because I can't seem to get a simple :external-format
+;;; :ucs-2 to work correctly AND some taggers encode a UCS-2 empty string w/o
+;;; a byte-order mark (i.e. null strings are sometimes encoded as #(00 00))
 (defun stream-decode-ucs-string (octets &key (start 0) (end (length octets)))
   "Decode octets as a UCS string with a BOM (encoding == 1)"
   (declare #.utils:*standard-optimize-settings*)
@@ -173,7 +136,9 @@ a displaced array from STREAMs underlying vector.  If it is == 7, then we have t
                (setf (ldb (byte 8 0) retval) (aref octets 1)
                      (ldb (byte 8 8) retval) (aref octets 0))
                (when (not (or (= #xfffe retval) (= #xfeff retval)))
-                 (error "Got invalid byte-order mark of ~x in STREAM-DECODE-UCS-STRING" retval))
+                 (error
+                  "Got invalid byte-order mark of ~x in STREAM-DECODE-UCS-STRING"
+                  retval))
                retval)))
 
     ;; special case: empty (and mis-coded) string
@@ -183,26 +148,37 @@ a displaced array from STREAMs underlying vector.  If it is == 7, then we have t
            ;;
            ;; else, we have a (hopefully) properly encoded string
            (when (oddp end)
-             (warn-user "Malformed UCS string, length (~d) is odd---fixing by decrementing by 1" end)
+             (warn-user
+              "Malformed UCS string, length (~d) is odd---decrementing by 1"
+              end)
              (setf end (1- end)))
 
            (let ((bom (get-byte-order-mark octets)))
              (ecase (the fixnum bom)
-               (#xfffe (flexi-streams:octets-to-string octets :start (+ 2 start) :end end :external-format :ucs-2le))
-               (#xfeff (flexi-streams:octets-to-string octets :start (+ 2 start) :end end :external-format :ucs-2be))
+               (#xfffe (flex:octets-to-string octets
+                                              :start (+ 2 start)
+                                              :end end
+                                              :external-format :ucs-2le))
+               (#xfeff (flex:octets-to-string octets
+                                              :start (+ 2 start)
+                                              :end end
+                                              :external-format :ucs-2be))
                (0      (make-string 0))))))))
 
 (defun stream-decode-ucs-be-string (octets &key (start 0) (end (length octets)))
   "Decode octets as a UCS-BE string (encoding == 2)"
   (declare #.utils:*standard-optimize-settings*)
-  (flexi-streams:octets-to-string octets :start start :end end :external-format :ucs-2be))
+  (flex:octets-to-string octets :start start
+                                :end end :external-format :ucs-2be))
 
 (defun stream-decode-utf-8-string (octets &key (start 0) (end (length octets)))
   "Decode octets as a utf-8 string"
   (declare #.utils:*standard-optimize-settings*)
-  (flexi-streams:octets-to-string octets :start start :end end :external-format :utf-8))
+  (flex:octets-to-string octets :start start :end end :external-format :utf-8))
 
-(defun stream-decode-string (octets &key (start 0) (end (length octets)) (encoding 0))
+(defun stream-decode-string (octets &key (start 0)
+                                         (end (length octets))
+                                         (encoding 0))
   "Decode octets depending on encoding"
   (declare #.utils:*standard-optimize-settings*)
   (ecase encoding
@@ -211,27 +187,27 @@ a displaced array from STREAMs underlying vector.  If it is == 7, then we have t
     (2 (stream-decode-ucs-be-string octets :start start :end end))
     (3 (stream-decode-utf-8-string octets  :start start :end end))))
 
-(defmethod stream-read-iso-string-with-len ((instream mem-stream) len)
+(defun stream-read-iso-string-with-len (instream len)
   "Read an iso-8859-1 string of length 'len' (encoding = 0)"
   (declare #.utils:*standard-optimize-settings*)
   (stream-decode-iso-string (stream-read-sequence instream len)))
 
-(defmethod stream-read-ucs-string-with-len ((instream mem-stream) len)
+(defun stream-read-ucs-string-with-len (instream len)
   "Read an ucs-2 string of length 'len' (encoding = 1)"
   (declare #.utils:*standard-optimize-settings*)
   (stream-decode-ucs-string (stream-read-sequence instream len)))
 
-(defmethod stream-read-ucs-be-string-with-len ((instream mem-stream) len)
+(defun stream-read-ucs-be-string-with-len (instream len)
   "Read an ucs-2-be string of length 'len' (encoding = 2)"
   (declare #.utils:*standard-optimize-settings*)
   (stream-decode-ucs-be-string (stream-read-sequence instream len)))
 
-(defmethod stream-read-utf-8-string-with-len ((instream mem-stream) len)
+(defun stream-read-utf-8-string-with-len (instream len)
   "Read an utf-8 string of length 'len' (encoding = 3)"
   (declare #.utils:*standard-optimize-settings*)
   (stream-decode-utf-8-string  (stream-read-sequence instream len)))
 
-(defmethod stream-read-string-with-len ((instream mem-stream) len &key (encoding 0))
+(defun stream-read-string-with-len (instream len &key (encoding 0))
   "Read in a string of a given encoding of length 'len'"
   (declare #.utils:*standard-optimize-settings*)
   (ecase encoding
@@ -240,10 +216,10 @@ a displaced array from STREAMs underlying vector.  If it is == 7, then we have t
     (2 (stream-read-ucs-be-string-with-len instream len))
     (3 (stream-read-utf-8-string-with-len instream len))))
 
-(defmethod stream-read-iso-string ((instream mem-stream))
-  "Read in a null terminated iso-8859-1 string"
+(defun stream-read-iso-string (instream)
+  "Read in a null-terminated iso-8859-1 string"
   (declare #.utils:*standard-optimize-settings*)
-  (let ((octets (flexi-streams:with-output-to-sequence (out)
+  (let ((octets (flex:with-output-to-sequence (out)
                   (do ((b (stream-read-u8 instream) (stream-read-u8 instream)))
                       (nil)
                     (when (zerop b)
@@ -251,10 +227,10 @@ a displaced array from STREAMs underlying vector.  If it is == 7, then we have t
                     (write-byte b out)))))
     (stream-decode-iso-string octets)))
 
-(defmethod stream-read-ucs-string ((instream mem-stream))
-  "Read in a null terminated UCS string."
+(defun stream-read-ucs-string (instream)
+  "Read in a null-terminated UCS string."
   (declare #.utils:*standard-optimize-settings*)
-  (let ((octets (flexi-streams:with-output-to-sequence (out)
+  (let ((octets (flex:with-output-to-sequence (out)
                   (do* ((b0 (stream-read-u8 instream)
                             (stream-read-u8 instream))
                         (b1 (stream-read-u8 instream)
@@ -266,10 +242,10 @@ a displaced array from STREAMs underlying vector.  If it is == 7, then we have t
                     (write-byte b1 out)))))
     (stream-decode-ucs-string octets)))
 
-(defmethod stream-read-ucs-be-string ((instream mem-stream))
-  "Read in a null terminated UCS-BE string."
+(defun stream-read-ucs-be-string (instream)
+  "Read in a null-terminated UCS-BE string."
   (declare #.utils:*standard-optimize-settings*)
-  (let ((octets (flexi-streams:with-output-to-sequence (out)
+  (let ((octets (flex:with-output-to-sequence (out)
                   (do* ((b0 (stream-read-u8 instream)
                             (stream-read-u8 instream))
                         (b1 (stream-read-u8 instream)
@@ -281,10 +257,10 @@ a displaced array from STREAMs underlying vector.  If it is == 7, then we have t
                     (write-byte b1 out)))))
     (stream-decode-ucs-be-string octets)))
 
-(defmethod stream-read-utf-8-string ((instream mem-stream))
-  "Read in a null terminated utf-8 string (encoding == 3)"
+(defun stream-read-utf-8-string (instream)
+  "Read in a null-terminated utf-8 string (encoding == 3)"
   (declare #.utils:*standard-optimize-settings*)
-  (let ((octets (flexi-streams:with-output-to-sequence (out)
+  (let ((octets (flex:with-output-to-sequence (out)
                   (do ((b (stream-read-u8 instream)
                           (stream-read-u8 instream)))
                       (nil)
@@ -293,8 +269,8 @@ a displaced array from STREAMs underlying vector.  If it is == 7, then we have t
                     (write-byte b out)))))
     (stream-decode-utf-8-string octets)))
 
-(defmethod stream-read-string ((instream mem-stream) &key (encoding 0))
-  "Read in a null terminated string of a given encoding."
+(defun stream-read-string (instream &key (encoding 0))
+  "Read in a null-terminated string of a given encoding."
   (declare #.utils:*standard-optimize-settings*)
   (ecase encoding
     (0 (stream-read-iso-string    instream))
@@ -302,36 +278,28 @@ a displaced array from STREAMs underlying vector.  If it is == 7, then we have t
     (2 (stream-read-ucs-be-string instream))
     (3 (stream-read-utf-8-string  instream))))
 
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; Files ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-(defvar *get-audio-info* t "controls whether the parsing functions also parse audio info like bit-rate, etc")
+;;;; Files
+(defvar *get-audio-info* t
+  "controls whether the parsing functions parse audio info like bit-rate, etc")
 
-(defmethod parse-audio-file ((stream mp4-file-stream) &key (get-audio-info *get-audio-info*) &allow-other-keys)
-  "Parse an MP4A file by reading its ATOMS and decoding them."
-  (declare #.utils:*standard-optimize-settings*)
-  (handler-case
-      (progn
-        (mp4-atom:find-mp4-atoms stream)
-        (when get-audio-info
-          (setf (audio-info stream) (mp4-atom:get-mp4-audio-info stream))))
-    (condition (c)
-      (utils:warn-user "make-mp4-stream got condition: ~a" c))))
-
-(defmethod parse-audio-file ((stream flac-file-stream) &key (get-audio-info *get-audio-info*) &allow-other-keys)
-  "Parse a flac file by reading its headers and decoding them."
-  (declare #.utils:*standard-optimize-settings*)
-  (declare (ignore get-audio-info)) ; audio info comes for "free" by parsing headers
-  (handler-case
-      (flac-frame:find-flac-frames stream)
-    (condition (c)
-      (utils:warn-user "make-flac-stream got condition: ~a" c))))
-
-(defmethod parse-audio-file ((stream mp3-file-stream) &key (get-audio-info *get-audio-info*) &allow-other-keys)
-  "Parse an MP3 file by reading its FRAMES and decoding them."
+(defun open-audio-file (filename &optional (get-audio-info *get-audio-info*))
+  "Open and parse FILENAME"
   (declare #.utils:*standard-optimize-settings*)
-  (handler-case
-      (progn
-        (id3-frame:find-id3-frames stream)
-        (when get-audio-info
-          (setf (audio-info stream) (mpeg:get-mpeg-audio-info stream))))
-    (condition (c)
-      (utils:warn-user "make-mp3-stream got condition: ~a" c))))
+  (let ((stream)
+        (info))
+
+    (unwind-protect
+         (progn
+           (setf stream (make-audio-stream filename))
+           (when stream
+             (setf info
+                   (cond ((id3-frame:is-valid-mp3-file stream)
+                          (id3-frame:parse-audio-file stream get-audio-info))
+                         ((mp4-atom:is-valid-m4-file stream)
+                          (mp4-atom:parse-audio-file stream get-audio-info))
+                         ((flac-frame:is-valid-flac-file stream)
+                          (flac-frame:parse-audio-file stream get-audio-info))
+                         (t nil)))))
+      (when stream
+        (close stream)))
+      info))

+ 38 - 26
flac-frame.lisp

@@ -36,13 +36,9 @@
   (stream-seek flac-file 0 :start)
   (let ((valid nil))
     (when (> (stream-size flac-file) 4)
-      (unwind-protect
-           (handler-case
-               (let ((hdr (stream-read-string-with-len flac-file 4)))
-                 (setf valid (string= "fLaC" hdr)))
-             (condition (c)
-               (utils:warn-user "is-valid-flac-file: got condition ~a" c)))
-        (stream-seek flac-file 0 :start)))
+      (let ((hdr (stream-read-string-with-len flac-file 4)))
+        (setf valid (string= "fLaC" hdr))))
+    (stream-seek flac-file 0 :start)
     valid))
 
 (defun make-flac-header (stream)
@@ -50,7 +46,7 @@
   (declare #.utils:*standard-optimize-settings*)
   (let* ((header (stream-read-u32 stream))
          (flac-header (make-instance 'flac-header
-                                     :pos (- (stream-here stream) 4)
+                                     :pos (- (stream-seek stream) 4)
                                      :last-bit (utils:get-bitfield header 31 1)
                                      :header-type (utils:get-bitfield header 30 7)
                                      :header-len (utils:get-bitfield header 23 24))))
@@ -92,25 +88,41 @@
     (setf (comments tags) (nreverse (comments tags)))
     tags))
 
-(defmethod find-flac-frames ((stream flac-file-stream))
-  "Loop through file and find all FLAC headers. If we find comment or audio-info headers, go ahead and parse them too."
+(defclass flac-file ()
+  ((filename     :accessor filename :initform nil :initarg :filename
+                 :documentation "filename that was parsed")
+   (flac-headers :accessor flac-headers :initform nil
+                 :documentation "holds all the flac headers in file")
+   (audio-info   :accessor audio-info   :initform nil
+                 :documentation "parsed audio info")
+   (flac-tags    :accessor flac-tags    :initform nil
+                 :documentation "parsed comment tags."))
+  (:documentation "Stream for parsing flac files"))
+
+(defun parse-audio-file (instream &optional (get-audio-info nil))
+  "Loop through file and find all FLAC headers. If we find comment or audio-info
+headers, go ahead and parse them too."
   (declare #.utils:*standard-optimize-settings*)
-  (stream-seek stream 4 :start)
-
-  (handler-case
-      (let (headers)
-        (loop for h = (make-flac-header stream) then (make-flac-header stream) do
-          (push h headers)
-          (cond
-            ((= +metadata-comment+ (header-type h))
-             (setf (flac-tags stream) (flac-get-tags stream)))
-            ((= +metadata-streaminfo+ (header-type h))
-             (setf (audio-info stream) (get-flac-audio-info stream)))
-            (t (stream-seek stream (header-len h) :current)))
-          (when (not (zerop (last-bit h))) (return)))
-        (setf (flac-headers stream) (nreverse headers)))
-    (condition (c)
-      (utils:warn-user "find-flac-frames got condition ~a" c))))
+  (declare (ignore get-audio-info)) ; audio info comes for "free"
+
+  (stream-seek instream 4 :start)
+
+  (let ((parsed-info (make-instance 'flac-file
+                                    :filename (stream-filename instream))))
+    (let (headers)
+      (loop for h = (make-flac-header instream)
+              then (make-flac-header instream) do
+                (push h headers)
+                (cond
+                  ((= +metadata-comment+ (header-type h))
+                   (setf (flac-tags parsed-info) (flac-get-tags instream)))
+                  ((= +metadata-streaminfo+ (header-type h))
+                   (setf (audio-info parsed-info) (get-flac-audio-info instream)))
+                  (t (stream-seek instream (header-len h) :current)))
+                (when (not (zerop (last-bit h)))
+                  (return)))
+      (setf (flac-headers parsed-info) (nreverse headers)))
+    parsed-info))
 
 (defclass flac-audio-properties ()
   ((min-block-size  :accessor min-block-size  :initarg :min-block-size  :initform 0)

+ 89 - 63
id3-frame.lisp

@@ -14,36 +14,6 @@
    (v21-tag-header :accessor v21-tag-header :initarg :v21-tag-header :initform nil :documentation "old-style v2.1 header (if present)"))
   (:documentation "The ID3 header, found at start of file"))
 
-(defun is-valid-mp3-file (mp3-file)
-  "Make sure this is an MP3 file. Look for ID3 header at begining (versions 2, 3, 4) and/or end (version 2.1)
-Written in this fashion so as to be 'crash-proof' when passed an arbitrary file."
-  (declare #.utils:*standard-optimize-settings*)
-
-  (let ((id3)
-        (valid nil)
-        (version)
-        (tag))
-
-    (when (> (stream-size mp3-file) 4)
-      (unwind-protect
-           (handler-case
-               (progn
-                 (stream-seek mp3-file 0 :start)
-                 (setf id3     (stream-read-string-with-len mp3-file 3)
-                       version (stream-read-u8 mp3-file))
-                 (when (> (stream-size mp3-file) 128)
-                   (stream-seek mp3-file 128 :end)
-                   (setf tag (stream-read-string-with-len mp3-file 3)))
-
-                 (setf valid (or (and (string= "ID3" id3)
-                                      (or (= 2 version) (= 3 version) (= 4 version)))
-                                 (string= tag "TAG"))))
-             (condition (c)
-               (utils:warn-user "is-valid-mp3-file got condition ~a" c)
-               (setf valid nil)))
-        (stream-seek mp3-file 0 :start)))
-    valid))
-
  (defclass v21-tag-header ()
    ((title    :accessor title    :initarg :title    :initform nil)
     (artist   :accessor artist   :initarg :artist   :initform nil)
@@ -240,9 +210,9 @@ NB: 2.3 and 2.4 extended flags are different..."
 ;;; the bytes an raw octets.
 (defun get-name-value-pair (instream len name-encoding value-encoding)
   (declare #.utils:*standard-optimize-settings*)
-  (let* ((old-pos  (stream-here instream))
+  (let* ((old-pos  (stream-seek instream))
          (name     (stream-read-string instream :encoding name-encoding))
-         (name-len (- (stream-here instream) old-pos))
+         (name-len (- (stream-seek instream) old-pos))
          (value))
 
     (setf value (if (>= value-encoding 0)
@@ -910,7 +880,7 @@ NB: 2.3 and 2.4 extended flags are different..."
 (defun make-frame (version instream fn)
   "Create an appropriate mp3 frame by reading data from INSTREAM."
   (declare #.utils:*standard-optimize-settings*)
-  (let* ((pos  (stream-here instream))
+  (let* ((pos  (stream-seek instream))
          (byte (stream-read-u8 instream))
          frame-name frame-len frame-flags frame-class)
 
@@ -934,22 +904,57 @@ NB: 2.3 and 2.4 extended flags are different..."
 
     ;; edge case where found a frame name, but it is not valid or where making this frame
     ;; would blow past the end of the file/buffer
-    (when (or (> (+ (stream-here instream) frame-len) (stream-size instream))
+    (when (or (> (+ (stream-seek instream) frame-len) (stream-size instream))
               (null frame-class))
       (error "bad frame at position ~d found: ~a" pos frame-name))
 
     (make-instance frame-class :pos pos :version version :id frame-name :len frame-len :flags frame-flags :instream instream)))
 
-(defmethod find-id3-frames ((mp3-file mp3-file-stream))
-  "With an open mp3-file, make sure it is in fact an MP3 file, then read its header and frames"
+(defun is-valid-mp3-file (instream)
+  "Make sure this is an MP3 file. Look for ID3 header at begining (versions 2,
+ 3, 4) and/or end (version 2.1) Written in this fashion so as to be
+ 'crash-proof' when passed an arbitrary file."
+  (declare #.utils:*standard-optimize-settings*)
+
+  (let ((id3)
+        (valid nil)
+        (version)
+        (tag))
+
+    (when (> (stream-size instream) 4)
+      (stream-seek instream 0 :start)
+      (setf id3     (stream-read-string-with-len instream 3)
+            version (stream-read-u8 instream))
+      (when (> (stream-size instream) 128)
+        (stream-seek instream 128 :end)
+        (setf tag (stream-read-string-with-len instream 3)))
+
+      (setf valid (or (and (string= "ID3" id3)
+                           (or (= 2 version) (= 3 version) (= 4 version)))
+                      (string= tag "TAG"))))
+    (stream-seek instream 0 :start)
+    valid))
+
+(defclass mp3-file ()
+  ((filename   :accessor filename :initform nil :initarg :filename
+               :documentation "filename that was parsed")
+   (id3-header :accessor id3-header :initform nil
+               :documentation "holds all the ID3 info")
+   (audio-info :accessor audio-info :initform nil
+               :documentation "holds the bit-rate, etc info"))
+  (:documentation "Output of parsing MP3 files"))
+
+(defun parse-audio-file (instream &optional get-audio-info)
+  "Parse an MP3 file"
   (declare #.utils:*standard-optimize-settings*)
   (labels ((read-loop (version stream)
              (let (frames this-frame)
                (do ()
-                   ((>= (stream-here stream) (stream-size stream)))
+                   ((>= (stream-seek stream) (stream-size stream)))
                  (handler-case
                      (progn
-                       (setf this-frame (make-frame version stream (stream-filename mp3-file)))
+                       (setf this-frame (make-frame version stream
+                                                    (stream-filename instream)))
                        (when (null this-frame)
                          (return-from read-loop (values t (nreverse frames))))
 
@@ -958,29 +963,50 @@ NB: 2.3 and 2.4 extended flags are different..."
                      (utils:warn-user "find-id3-frame got condition ~a" c)
                      (return-from read-loop (values nil (nreverse frames))))))
 
-               (values t (nreverse frames))))) ; reverse this so we have frames in "file order"
-
-    (setf (id3-header mp3-file) (make-instance 'id3-header :instream mp3-file))
-    (with-slots (size ext-header frames flags version) (id3-header mp3-file)
-
-      ;; At this point, we switch from reading the file stream and create a memory stream
-      ;; rationale: it may need to be unsysnc'ed and it helps prevent run-away reads with
-      ;; mis-formed frames
-      (when (not (zerop size))
-        (let ((mem-stream (make-mem-stream (stream-read-sequence mp3-file size
-                                                                 :bits-per-byte (if (header-unsynchronized-p flags) 7 8)))))
-
-          ;; Must make extended header here since it is subject to unsynchronization.
-          (when (header-extended-p flags)
-            (setf ext-header (make-instance 'id3-ext-header :instream mem-stream :version version)))
-
-          ;; Start reading frames from memory stream
-          (multiple-value-bind (_ok _frames) (read-loop version mem-stream)
-            (if (not _ok)
-                (warn-user "File ~a had errors finding mp3 frames. potentially missed frames!" (stream-filename mp3-file)))
-            (setf frames _frames)
-            _ok))))))
-
-(defun map-id3-frames (mp3-file &key (func (constantly t)))
+               (values t (nreverse frames))))) ; frames in "file order"
+
+    (let ((parsed-info (make-instance 'mp3-file
+                                      :filename (stream-filename instream))))
+      (setf (id3-header parsed-info) (make-instance 'id3-header :instream instream))
+      (with-slots (size ext-header frames flags version) (id3-header parsed-info)
+
+        ;; At this point, we switch from reading the file stream and create a
+        ;; memory stream rationale: it may need to be unsysnc'ed and it helps
+        ;; prevent run-away reads with mis-formed frames
+        (when (not (zerop size))
+          (let ((mem-stream
+                  (make-audio-stream (stream-read-sequence
+                                      instream size
+                                      :bits-per-byte
+                                      (if (header-unsynchronized-p flags) 7 8)))))
+
+            ;; Make extended header here since it is subject to unsynchronization.
+            (when (header-extended-p flags)
+              (setf ext-header (make-instance 'id3-ext-header
+                                              :instream mem-stream
+                                              :version version)))
+
+            ;; Start reading frames from memory stream
+            (multiple-value-bind (_ok _frames) (read-loop version mem-stream)
+              (if (not _ok)
+                  (warn-user
+                   "File ~a had errors finding mp3 frames. potentially missed frames!"
+                   (stream-filename instream)))
+              (setf frames _frames))))
+        (when get-audio-info
+          (mpeg:get-mpeg-audio-info instream parsed-info))
+        parsed-info))))
+
+(defun map-id3-frames (mp3 &key (func (constantly t)))
   "Iterates through the ID3 frames found in an MP3 file"
-  (mapcar func (frames (id3-header mp3-file))))
+  (mapcar func (frames (id3-header mp3))))
+
+(defun get-frames (mp3 names)
+  "Given a MP3 file's info, search its frames for NAMES.
+Return file-order list of matching frames"
+  (let (found-frames)
+    (map-id3-frames mp3
+                    :func (lambda (f)
+                            (when (member (id f) names :test #'string=)
+                              (push f found-frames))))
+    (nreverse found-frames)))

+ 29 - 21
mp4-atom.lisp

@@ -143,7 +143,7 @@ to read the payload of an atom."
   (declare #.utils:*standard-optimize-settings*)
   (with-mp4-atom-slots (me)
     (loop for end = (+ atom-file-pos atom-size)
-          for current = (stream-here mp4-file) then (stream-here mp4-file)
+          for current = (stream-seek mp4-file) then (stream-seek mp4-file)
           while (< current end) do
             (make-mp4-atom mp4-file me))))
 
@@ -323,7 +323,7 @@ to read the payload of an atom."
           flags (stream-read-u24 mp4-file))
     (assert (= +MP4-ESDescrTag+ (stream-read-u8 mp4-file)) () "Expected description tag of ESDescrTag")
     (let* ((len (read-descriptor-len mp4-file))
-           (end-of-atom (+ (stream-here mp4-file) len)))
+           (end-of-atom (+ (stream-seek mp4-file) len)))
       (setf esid (stream-read-u16 mp4-file)
             s-priority (stream-read-u8 mp4-file))
       (assert (= +MP4-DecConfigDescrTag+ (stream-read-u8 mp4-file)) () "Expected tag type of DecConfigDescrTag")
@@ -407,7 +407,7 @@ reading the container atoms"
 (defun make-mp4-atom (mp4-file parent)
   "Get current file position, read in size/type, then construct the correct atom."
   (declare #.utils:*standard-optimize-settings*)
-  (let* ((pos (stream-here mp4-file))
+  (let* ((pos (stream-seek mp4-file))
          (siz (stream-read-u32 mp4-file))
          (typ (stream-read-u32 mp4-file))
          (atom))
@@ -445,33 +445,41 @@ Written in this fashion so as to be 'crash-proof' when passed an arbitrary file.
         (size)
         (header))
     (when (> (stream-size mp4-file) 8)
-      (unwind-protect
-           (handler-case
-               (progn
-                 (stream-seek mp4-file 0 :start)
-                 (setf size   (stream-read-u32 mp4-file)
-                       header (stream-read-u32 mp4-file)
-                       valid  (and (<= size (stream-size mp4-file))
-                                   (= header +m4-ftyp+))))
-             (condition (c)
-               (utils:warn-user "File:~a~%is-valid-mp4-file got condition ~a" (stream-filename mp4-file) c)))
-
-        (stream-seek mp4-file 0 :start)))
+      (stream-seek mp4-file 0 :start)
+      (setf size   (stream-read-u32 mp4-file)
+            header (stream-read-u32 mp4-file)
+            valid  (and (<= size (stream-size mp4-file))
+                        (= header +m4-ftyp+))))
+    (stream-seek mp4-file 0 :start)
     valid))
 
-(defmethod find-mp4-atoms ((mp4-file mp4-file-stream))
-  "Given a valid MP4 file MP4-FILE, look for the 'right' atoms and return them."
+(defclass mp4-file ()
+  ((filename   :accessor filename :initform nil :initarg :filename
+               :documentation "filename that was parsed")
+   (mp4-atoms  :accessor mp4-atoms  :initform nil
+               :documentation "holds tree of parsed MP4 atoms/boxes")
+   (audio-info :accessor audio-info :initform nil
+               :documentation "holds the bit-rate, etc info"))
+  (:documentation "Stream for parsing MP4 audio files"))
+
+(defun parse-audio-file (instream &optional (get-audio-info nil))
+  "Given a valid MP4 file, look for the 'right' atoms and return them."
   (declare #.utils:*standard-optimize-settings*)
-  (stream-seek mp4-file 0 :start)
+  (stream-seek instream 0 :start)
   (setf *in-progress* nil)
 
   ;; Construct our fake "root" for our tree, which recursively reads all atoms
   (tree:make-node (make-instance 'mp4-container-atom
                                  :atom-type +root+
                                  :atom-file-pos 0
-                                 :atom-size (stream-size mp4-file)
-                                 :mp4-file mp4-file))
-    (setf (mp4-atoms mp4-file) *tree*))
+                                 :atom-size (stream-size instream)
+                                 :mp4-file instream))
+  (let ((parsed-info (make-instance 'mp4-file
+                                    :filename (stream-filename instream))))
+    (setf (mp4-atoms parsed-info) *tree*)
+    (when get-audio-info
+      (setf (audio-info parsed-info) (get-mp4-audio-info parsed-info)))
+    parsed-info))
 
 (defparameter *ilst-data* (list +root+ +mp4-atom-moov+ +mp4-atom-udta+
                                 +mp4-atom-meta+ +mp4-atom-ilst+ nil

+ 31 - 21
mpeg.lisp

@@ -187,7 +187,7 @@
   (handler-case
       (with-frame-slots (me)
         (when (null hdr-u32)            ; has header already been read in?
-          (setf pos     (stream-here instream)
+          (setf pos     (stream-seek instream)
                 hdr-u32 (stream-read-u32 instream))
           (when (null hdr-u32)
             (return-from load-frame nil)))
@@ -326,7 +326,7 @@ Bits   1-0 (2  bits): the emphasis"
                      (= (aref payload (+ i 3)) (char-code #\o))))
 
         (setf vbr (make-instance 'vbr-info))
-        (let ((v (make-mem-stream (payload me))))
+        (let ((v (make-audio-stream (payload me))))
           (stream-seek v i :start)      ; seek to Xing/Info offset
           (setf (tag vbr)   (stream-read-iso-string-with-len v 4)
                 (flags vbr) (stream-read-u32 v))
@@ -353,7 +353,7 @@ Bits   1-0 (2  bits): the emphasis"
     (format stream "tag = ~a, flags = 0x~x, frame~p = ~:d, bytes = ~:d, tocs = ~d, scale = ~d, "
             tag flags frames frames bytes tocs scale)))
 
-(defun find-first-sync (in)
+(defun find-first-sync (instream)
   (declare #.utils:*standard-optimize-settings*)
   (let ((hdr-u32)
         (count 0)
@@ -361,22 +361,23 @@ Bits   1-0 (2  bits): the emphasis"
 
     (handler-case
         (loop
-          (setf pos     (stream-here in)
-                hdr-u32 (stream-read-u32 in))
+          (setf pos     (stream-seek instream)
+                hdr-u32 (stream-read-u32 instream))
           (when (null hdr-u32)
             (return-from find-first-sync nil))
           (incf count)
 
           (when (= (logand hdr-u32 #xffe00000) #xffe00000) ; magic number is potential sync frame header
             (let ((hdr (make-instance 'frame :hdr-u32 hdr-u32 :pos pos)))
-              (if (load-frame hdr :instream in :read-payload t)
+              (if (load-frame hdr :instream instream :read-payload t)
                   (progn
-                    (check-vbr hdr (stream-filename in))
+                    (check-vbr hdr (stream-filename instream))
                     (return-from find-first-sync hdr))))))
       (condition (c) (progn
                        (warn-user "Condtion <~a> signaled while looking for first sync" c)
                        (error c))))
     nil))
+
 (defmethod next-frame ((me frame) &key instream read-payload)
   "Get next frame.  If READ-PAYLOAD is true, read in contents for frame, else, seek to next frame header."
   (declare #.utils:*standard-optimize-settings*)
@@ -423,15 +424,16 @@ Bits   1-0 (2  bits): the emphasis"
             (round (/ bit-rate 1000))
             (floor (/ len 60)) (round (mod len 60)))))
 
-(defun calc-bit-rate-exhaustive (in start info)
-  "Map every MPEG frame in IN and calculate the bit-rate"
+(defun calc-bit-rate-exhaustive (instream start info)
+  "Map every MPEG frame in INSTREAM and calculate the bit-rate"
   (declare #.utils:*standard-optimize-settings*)
   (let ((total-len      0)
         (last-bit-rate  nil)
         (bit-rate-total 0)
         (vbr            nil))
+
     (with-slots (is-vbr sample-rate bit-rate len version layer n-frames) info
-      (map-frames in (lambda (f)
+      (map-frames instream (lambda (f)
                        (incf n-frames)
                        (incf total-len (float (/ (samples f) (sample-rate f))))
                        (incf bit-rate-total (bit-rate f))
@@ -449,16 +451,17 @@ Bits   1-0 (2  bits): the emphasis"
       (setf is-vbr   t
             len      total-len
             bit-rate (float (/ bit-rate-total n-frames))))))
-(defun get-mpeg-audio-info (in &key) ;; (max-frames *max-frames-to-read*))
+
+(defun get-mpeg-audio-info (instream mp3-file)
   "Get MPEG Layer 3 audio information.
  If the first MPEG frame we find is a Xing/Info header, return that as info.
  Else, we assume CBR and calculate the duration, etc."
   (declare #.utils:*standard-optimize-settings*)
-  (let ((first-frame (find-first-sync in))
+  (let ((first-frame (find-first-sync instream))
         (info        (make-instance 'mpeg-audio-info)))
 
     (when (null first-frame)
-      (return-from get-mpeg-audio-info nil))
+      (return-from get-mpeg-audio-info))
 
     (with-slots (is-vbr sample-rate bit-rate len version layer n-frames) info
       (setf version     (version first-frame)
@@ -468,21 +471,28 @@ Bits   1-0 (2  bits): the emphasis"
       (if (vbr first-frame)
           ;; found a Xing header, now check to see if it is correct
           (if (zerop (frames (vbr first-frame)))
-              (calc-bit-rate-exhaustive in (pos first-frame) info) ; Xing header broken, read all frames to calc
+              ;; Xing header broken, read all frames to calc
+              (calc-bit-rate-exhaustive instream (pos first-frame) info)
               ;; Good Xing header, use info in VBR to calc
               (setf n-frames 1
                     is-vbr   t
-                    len      (float (* (frames (vbr first-frame)) (/ (samples first-frame) (sample-rate first-frame))))
+                    len      (float (* (frames (vbr first-frame))
+                                       (/ (samples first-frame)
+                                          (sample-rate first-frame))))
                     bit-rate (float (/ (* 8 (bytes (vbr first-frame))) len))))
 
-          ;; No Xing header found.  Assume CBR and calculate based on first frame
+          ;; No Xing header found. Assume CBR and calculate based on first frame
           (let* ((first (pos first-frame))
-                 (last (- (audio-streams:stream-size in) (if (id3-frame::v21-tag-header (id3-header in)) 128 0)))
-                 (n-fr (round (/ (float (- last first)) (float (size first-frame)))))
-                 (n-sec (round (/ (float (* (size first-frame) n-fr)) (float (* 125 (float (/ (bit-rate first-frame) 1000))))))))
+                 (last (- (stream-size instream)
+                          (if (id3-frame:v21-tag-header
+                               (id3-frame:id3-header mp3-file)) 128 0)))
+                 (n-fr (round (/ (float (- last first))
+                                 (float (size first-frame)))))
+                 (n-sec (round (/ (float (* (size first-frame) n-fr))
+                                  (float (* 125 (float
+                                                 (/ (bit-rate first-frame) 1000))))))))
             (setf is-vbr   nil
                   n-frames 1
                   len      n-sec
                   bit-rate (float (bit-rate first-frame))))))
-
-    info))
+    (setf (id3-frame:audio-info mp3-file) info)))

+ 42 - 59
packages.lisp

@@ -23,79 +23,62 @@
   (:use #:common-lisp :utils))
 
 (defpackage #:audio-streams
-  (:export #:octets #:make-octets *get-audio-info* #:audio-stream-condition
-           #:mp3-file-stream #:mp4-file-stream #:base-mem-stream #:flac-file-stream #:flac-tags
-           #:id3-header #:audio-info #:mp4-atoms #:flac-headers
-           #:parse-mp3-file #:parse-mp4-file #:parse-audio-file #:parse-flac-file #:flac-tags
-           #:make-mem-stream #:make-file-stream #:stream-filename
-           #:stream-read-u8 #:stream-read-u16 #:stream-read-u24 #:stream-read-u32 #:stream-read-u64 #:stream-read-u128 #:stream-read-octets
-           #:stream-decode-iso-string #:stream-deocode-ucs-string #:stream-decode-ucs-be-string
-           #:stream-decode-utf-8-string #:stream-decode-string #:stream-read-iso-string-with-len
-           #:stream-read-ucs-string-with-len #:stream-read-ucs-be-string-with-len
-           #:stream-read-utf-8-string-with-len #:stream-read-string-with-len
-           #:stream-read-iso-string #:stream-read-ucs-string #:stream-read-ucs-be-string
+  (:export #:octets #:octets #:make-octets *get-audio-info*
+           #:make-audio-stream #:stream-filename #:stream-read-u8
+           #:stream-read-u16 #:stream-read-u24 #:stream-read-u32
+           #:stream-read-u64 #:stream-read-u128 #:stream-read-octets
+           #:stream-decode-iso-string #:stream-deocode-ucs-string
+           #:stream-decode-ucs-be-string #:stream-decode-utf-8-string
+           #:stream-decode-string #:stream-read-iso-string-with-len
+           #:stream-read-ucs-string-with-len
+           #:stream-read-ucs-be-string-with-len
+           #:stream-read-utf-8-string-with-len
+           #:stream-read-string-with-len #:stream-read-iso-string
+           #:stream-read-ucs-string #:stream-read-ucs-be-string
            #:stream-read-utf-8-string #:stream-read-string
            #:stream-read-string #:stream-read-sequence #:stream-size
-           #:stream-seek #:stream-here #:stream-close)
+           #:stream-seek #:open-audio-file)
   (:use #:common-lisp #:utils))
 
 (defpackage #:flac-frame
-  (:export #:flac-frame-condition #:flac-header #:vpprint #:is-valid-flac-file #:find-flac-frames
-           #:get-flac-audio-info #:flac-get-tag #:get-flac-audio-info #:flac-show-raw-tag)
+  (:export #:flac-frame-condition #:flac-header #:vpprint
+           #:is-valid-flac-file #:find-flac-frames #:get-flac-audio-info
+           #:flac-get-tag #:get-flac-audio-info #:flac-show-raw-tag
+           #:parse-audio-file #:flac-file #:flac-headers #:audio-info
+           #:flac-tags #:filename)
   (:use #:common-lisp #:utils #:audio-streams))
 
 (defpackage #:mp4-atom
-  (:export #:mp4-atom #:map-mp4-atom #:find-mp4-atoms #:traverse #:mp4-atom-condition
-           #:atom-file-pos #:atom-children #:atom-size #:atom-of-interest #:atom-decoded
-           #:atom-type #:vpprint #:*tag-path* #:tag-get-value #:mp4-atom-condition
-           #:mp4-show-raw-tag-atoms #:get-mp4-audio-info #:is-valid-m4-file
-           #:+itunes-album+
-           #:+itunes-album-artist+
-           #:+itunes-artist+
-           #:+itunes-comment+
-           #:+itunes-composer+
-           #:+itunes-copyright+
-           #:+itunes-year+
-           #:+itunes-encoder+
-           #:+itunes-groups+
-           #:+itunes-lyrics+
-           #:+itunes-purchased-date+
-           #:+itunes-title+
-           #:+itunes-tool+
-           #:+itunes-writer+
-           #:+itunes-compilation+
-           #:+itunes-cover-art+
-           #:+itunes-disk+
-           #:+itunes-genre+
-           #:+itunes-genre-x+
-           #:+itunes-tempo+
-           #:+itunes-track+
-           #:+itunes-track-n+)
+  (:export #:mp4-atom #:map-mp4-atom #:find-mp4-atoms #:traverse
+           #:mp4-atom-condition #:atom-file-pos #:atom-children #:atom-size
+           #:atom-of-interest #:atom-decoded #:atom-type #:vpprint #:*tag-path*
+           #:tag-get-value #:mp4-atom-condition #:mp4-show-raw-tag-atoms
+           #:get-mp4-audio-info #:is-valid-m4-file #:+itunes-album+
+           #:+itunes-album-artist+ #:+itunes-artist+ #:+itunes-comment+
+           #:+itunes-composer+ #:+itunes-copyright+ #:+itunes-year+
+           #:+itunes-encoder+ #:+itunes-groups+ #:+itunes-lyrics+
+           #:+itunes-purchased-date+ #:+itunes-title+ #:+itunes-tool+
+           #:+itunes-writer+ #:+itunes-compilation+ #:+itunes-cover-art+
+           #:+itunes-disk+ #:+itunes-genre+ #:+itunes-genre-x+ #:+itunes-tempo+
+           #:+itunes-track+ #:+itunes-track-n+ #:parse-audio-file #:mp4-file
+           #:mp4-atoms #:audio-info #:filename)
   (:use #:common-lisp #:audio-streams #:utils))
 
 (defpackage #:id3-frame
-  (:export #:id3-frame #:find-id3-frames #:id3-frame-condition #:vpprint #:header
-           #:get-frame-info #:is-valid-mp3-file #:encoding #:lang #:desc #:val
-           #:comment #:artist #:album #:year #:comment #:year #:map-id3-frames
-           #:frames #:year #:title #:genre #:id #:v21-tag-header #:info #:version
-           #:picture-info)
+  (:export #:id3-frame #:find-id3-frames #:id3-frame-condition #:vpprint
+           #:header #:get-frame-info #:is-valid-mp3-file #:encoding #:lang
+           #:desc #:val #:comment #:artist #:album #:year #:comment #:year
+           #:map-id3-frames #:frames #:year #:title #:genre #:id
+           #:mp3-file #:id3-header #:audio-info #:parse-audio-file
+           #:v21-tag-header #:info #:version #:picture-info #:get-frames
+           #:filename)
   (:use #:common-lisp #:audio-streams #:utils #:iso-639-2))
 
-;; (defpackage #:mp3-tag
-;;   (:export #:show-tags #:get-id3v1-genre)
-;;   (:use #:common-lisp #:audio-streams #:id3-frame #:utils))
-
-;; (defpackage #:mp4-tag
-;;   (:export #:show-tags #:album #:album-artist #:artist #:comment #:composer #:copyright #:created
-;;            #:encoder #:groups #:lyrics #:purd #:title #:tool #:writer)
-;;   (:use #:common-lisp #:audio-streams #:utils))
-
 (defpackage #:abstract-tag
-  (:export #:show-tags #:get-id3v1-genre
-           #:album #:album-artist #:artist #:comment #:composer #:copyright #:created
-           #:encoder #:groups #:lyrics #:purd #:title #:tool #:writer)
-  (:use #:common-lisp #:audio-streams #:id3-frame #:utils))
-
+  (:export #:show-tags #:get-id3v1-genre #:album #:album-artist #:artist
+           #:comment #:composer #:copyright #:created #:encoder #:groups
+           #:lyrics #:title #:tool #:writer)
+  (:use #:common-lisp #:audio-streams #:utils))
 
 (defpackage #:mpeg
   (:export #:get-mpeg-audio-info #:vpprint)

+ 62 - 45
taglib-tests.lisp

@@ -3,17 +3,20 @@
 (in-package #:cl-user)
 
 (defpackage #:taglib-tests
-  (:use #:common-lisp #:audio-streams))
+  (:use #:common-lisp #:audio-streams #:utils))
 
 (in-package #:taglib-tests)
 
 ;;; some convenient songs to parse
-(defparameter *song-m4a*  "/home/markv/Music/Queen/Queen I/01 Keep Yourself Alive.m4a")
-(defparameter *song-mp3*  "/home/markv/Music/Queen/Sheer Heart Attack/07 In The Lap Of The Gods.mp3")
-(defparameter *song-flac* "/home/markv/Music/Frank Zappa/Baby Snakes/02. Baby Snakes.flac")
+(defparameter *song-m4a*
+  "/home/markv/Music/Queen/Queen I/01 Keep Yourself Alive.m4a")
+(defparameter *song-mp3*
+  "/home/markv/Music/Queen/Sheer Heart Attack/07 In The Lap Of The Gods.mp3")
+(defparameter *song-flac*
+  "/home/markv/Music/Frank Zappa/Baby Snakes/02. Baby Snakes.flac")
 
 ;;;
-;;; Set the pathname (aka filename) encoding in CCL for appropriate platform
+;;; Set the pathname (aka filename) encoding for appropriate platform
 ;;;
 ;;; A note re filesystem encoding: my music collection is housed on a Mac and shared via SAMBA.
 ;;; In order to make sure we get valid pathnames, we need to set CCL's filesystem encoding to
@@ -26,22 +29,14 @@
 (defun set-pathname-encoding-for-osx ()   (set-pathname-encoding :utf-8))
 (defun set-pathname-encoding-for-linux () (set-pathname-encoding nil))
 
-(defun do-audio-file (&key (file *song-m4a*) (func (constantly t)))
-  "Parse one audio file (with condition handling)."
-  (let ((foo))
-    (unwind-protect
-         (handler-case
-             (progn
-               (setf foo (make-file-stream file))
-               (when foo
-                 (parse-audio-file foo))    ; only call parse-audio if we got back a known file type
-               (funcall func foo))          ; call func even if foo is null so it can account for unkown file types
-           (condition (c)
-             (utils:warn-user "File: ~a~%Got condition: <~a>" file c)))
-      (when foo
-        (stream-close foo)))))
-
-(defun do-audio-dir (&key (dir "/home/markv/Music/Queen") (file-system-encoding :utf-8)
+(defun do-audio-file (&key (file *song-m4a*)
+                           (func #'abstract-tag:show-tags))
+  "Parse one audio file and display the tags"
+  (awhen (open-audio-file file)
+    (funcall func it)))
+
+(defun do-audio-dir (&key (dir "/home/markv/Music/Queen")
+                          (file-system-encoding :utf-8)
                           (func #'abstract-tag:show-tags))
   "Walk :DIR and FUNCALL specified function for each file audio found."
   (set-pathname-encoding file-system-encoding)
@@ -49,23 +44,32 @@
         (flac-count 0)
         (mp4-count 0)
         (other-count 0))
-    (cl-fad:walk-directory dir (lambda (f)
-                                 (do-audio-file :file f
-                                   :func (lambda (s)
-                                           (cond ((typep s 'mp3-file-stream)  (incf mp3-count))
-                                                 ((typep s 'flac-file-stream) (incf flac-count))
-                                                 ((typep s 'mp4-file-stream)  (incf mp4-count))
-                                                 ((null s)                    (incf other-count)))
-                                           (when (and (not (null s)) func) (funcall func s))))))
+    (cl-fad:walk-directory dir
+                           (lambda (f)
+                             (do-audio-file :file f
+                               :func (lambda (s)
+                                       (cond ((typep s 'id3-frame:mp3-file)
+                                              (incf mp3-count))
+                                             ((typep s 'flac-frame:flac-file)
+                                              (incf flac-count))
+                                             ((typep s 'mp4-atom:mp4-file)
+                                              (incf mp4-count))
+                                             ((null s)
+                                              (incf other-count)))
+                                       (when (and (not (null s)) func)
+                                         (funcall func s))))))
 
     (format t "~&~:d MP3s, ~:d MP4s, ~:d FLACs, ~:d Others, for a total of ~:d~%"
-            mp3-count mp4-count flac-count other-count (+ mp3-count mp4-count flac-count other-count))))
+            mp3-count mp4-count flac-count other-count
+            (+ mp3-count mp4-count flac-count other-count))))
 
-(defun time-test (&key (dir "/home/markv/Music/Queen") (file-system-encoding :utf-8) (do-audio-processing t))
+(defun time-test (&key (dir "/home/markv/Music/Queen")
+                       (file-system-encoding :utf-8) (do-audio-processing t))
   "Time parsing of DIR."
   (set-pathname-encoding file-system-encoding)
   (let ((audio-streams:*get-audio-info* do-audio-processing))
-    (time (do-audio-dir :dir dir :file-system-encoding file-system-encoding :func nil))))
+    (time (do-audio-dir :dir dir
+            :file-system-encoding file-system-encoding :func nil))))
 
 ;;;;;;;;;;;;;;;;;;;; multi-thread code below ;;;;;;;;;;;;;;;;;;;;
 (defparameter *end-thread*  #xdeadbeef)
@@ -79,8 +83,9 @@
   mp4-count
   other-count)
 
-(defun mp-do-audio-dir (&key (dir "/home/markv/Music/Queen") (file-system-encoding :utf-8)
-                                                     (func #'abstract-tag:show-tags))
+(defun mp-do-audio-dir (&key (dir "/home/markv/Music/Queen")
+                             (file-system-encoding :utf-8)
+                             (func #'abstract-tag:show-tags))
   "Walk :DIR and FUNCALL specified function for each file audio found."
   (set-pathname-encoding file-system-encoding)
   (let ((channel (make-instance 'chanl:unbounded-channel))
@@ -97,21 +102,29 @@
     (labels ((thread-reader ()
                (declare (special *me*))
                (let ((f)
-                     (results (make-chanl-results :name *me* :mp3-count 0 :flac-count 0 :mp4-count 0 :other-count 0)))
+                     (results (make-chanl-results :name *me* :mp3-count 0
+                                                  :flac-count 0 :mp4-count 0
+                                                  :other-count 0)))
                  (loop
                    (with-slots (name mp3-count mp4-count flac-count other-count) results
                      (setf f (chanl:recv channel))
                      (when (and (typep f 'integer)
                                 (= f *end-thread*))
-                       (chanl:send dead-channel results) ; send structure of stats back to parent
+                       (chanl:send dead-channel results)
                        (return-from thread-reader nil))
 
-                     (do-audio-file :file f :func (lambda (s)
-                                              (cond ((typep s 'mp3-file-stream)  (incf mp3-count))
-                                                    ((typep s 'flac-file-stream) (incf flac-count))
-                                                    ((typep s 'mp4-file-stream)  (incf mp4-count))
-                                                    ((null s)                    (incf other-count)))
-                                              (when (and (not (null s)) func) (funcall func s)))))))))
+                     (do-audio-file :file f
+                       :func (lambda (s)
+                               (cond ((typep s 'id3-frame:mp3-file)
+                                      (incf mp3-count))
+                                     ((typep s 'flac-frame:flac-file)
+                                      (incf flac-count))
+                                     ((typep s 'mp4-atom:mp4-file)
+                                      (incf mp4-count))
+                                     ((null s)
+                                      (incf other-count)))
+                               (when (and (not (null s)) func)
+                                 (funcall func s)))))))))
 
       ;; first, add all files in DIR to CHANNEL
       (cl-fad:walk-directory dir (lambda (f) (chanl:send channel f)))
@@ -122,7 +135,9 @@
       (dotimes (i *max-threads*)
         (chanl:send channel *end-thread*))
       (dotimes (i *max-threads*)
-        (chanl:pcall #'thread-reader :initial-bindings `((*me* ,(format nil "reader-thread-~d" i)))))
+        (chanl:pcall
+         #'thread-reader
+         :initial-bindings `((*me* ,(format nil "reader-thread-~d" i)))))
 
       ;; sit in loop until we read *MAX-THREADS* results
       (block thread-reap
@@ -152,9 +167,11 @@
 
       (format t "All threads done~%")
       (format t "~&~:d MP3s, ~:d MP4s, ~:d FLACS, ~:d Others, for a total of ~:d files~%"
-              mp3-count mp4-count flac-count other-count (+ mp3-count mp4-count flac-count other-count)))))
+              mp3-count mp4-count flac-count other-count
+              (+ mp3-count mp4-count flac-count other-count)))))
 
-(defun mp-time-test (&key (dir "/home/markv/Music/Queen") (file-system-encoding :utf-8) (do-audio-processing t))
+(defun mp-time-test (&key (dir "/home/markv/Music/Queen")
+                          (file-system-encoding :utf-8) (do-audio-processing t))
   "Time parsing of DIR."
   (set-pathname-encoding file-system-encoding)
   (let ((audio-streams:*get-audio-info* do-audio-processing))