1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
|
;;; ogg-comment.el --- Read Ogg-Vorbis file headers.
;; Copyright (C) 2003, 2004, 2005, 2006, 2007 Free Software Foundation, Inc.
;; Filename: ogg-comment.el
;; Version: $Revision: 1.5 $
;; Author: lawrence mitchell <wence@gmx.li>
;; Maintainer: lawrence mitchell <wence@gmx.li>
;; Created: 2003-09-26
;; Keywords: music
;; This program is free software; you can redistribute it and/or
;; modify it under the terms of the GNU General Public License as
;; published by the Free Software Foundation; either version 3 of the
;; License, or (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful, but
;; WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
;; General Public License for more
;; details. http://www.gnu.org/copyleft/gpl.html
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If you did not, write to the Free Software
;; Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
;; 02110-1301 USA
;;; Commentary:
;; This file provides a minimal interface to reading the "comment"
;; section from an Ogg-Vorbis stream as defined in <URL:
;; http://www.xiph.org/ogg/vorbis/doc/Vorbis_I_spec.html>
;; It relies on all the comments being in the first 28kilobytes of
;; the file, thus removing the need to read the whole ogg file into
;; an Emacs buffer.
;; The implementation is rather "byte-oriented", due to the way the
;; Ogg-Vorbis file headers are specified. Any improvements in making
;; the implementation more emacsy would be welcomed.
;;; Installation:
;; To use, put this file somewhere in your `load-path' and do
;; (require 'ogg-comment).
;; You can then read ogg comments from a file by doing:
;; M-x oggc-show-header RET.
;;; History:
;;
;;; TODO:
;; o Read setup header, to get bitrate and such like.
;; o Make writing comments possible.
;;; Code:
(eval-when-compile
(defvar it)
(require 'cl))
(defconst oggc-ogg-header "OggS"
"The string indicating the start of an Ogg stream.")
(defconst oggc-identification-header "\001vorbis"
"The string indicating the start of the Ogg identification header.")
(defconst oggc-comment-header "\003vorbis"
"The string indicating the start of the Ogg comment header.")
(defconst oggc-setup-header "\005vorbis"
"The string indicating the start of the Ogg setup header.")
(defconst oggc-code-book-pattern "BCV"
"The string indicating the start of an Ogg code book.")
(defconst oggc-version "$Revision: 1.5 $"
"Ogg-comment's version number.")
(defmacro with-part-of-file (file-spec &rest body)
"Execute BODY in a buffer containing part of FILE.
BEG and END are as `insert-file-contents' (q.v.).
\(fn (FILE &optional BEG END) &rest BODY)"
(let (file beg end)
(setq file (pop file-spec))
(and file-spec (setq beg (pop file-spec)))
(and file-spec (setq end (pop file-spec)))
`(with-temp-buffer
(insert-file-contents-literally ,file nil ,beg ,end)
(goto-char (point-min))
,@body)))
(defmacro aif (test-form then &rest else)
"Like `if', but with `it' bound to the result of TEST-FORM.
`it' is accessible in the THEN and ELSE clauses.
Warning, non-hygienic by design.
\(fn TEST-FORM THEN &rest ELSE)"
`(let ((it ,test-form))
(if it
,then
,@else)))
(defun oggc-split-comment (comment)
"Split Ogg COMMENT into a (name, value) pair.
If possible (`ccl-execute-on-string' and `ccl-decode-mule-utf-8'
available), COMMENT is decoded into utf-8.
The name-part is converted to lowercase, to make sure case-differences
are ignored."
(setq comment (split-string comment "="))
(list (downcase (car comment))
(oggc-decode-utf-8 (or (cadr comment)
""))))
(defun oggc-encode-utf-8 (string)
"Encode STRING into utf-8."
(if (and (fboundp 'ccl-execute-on-string)
(boundp 'ccl-encode-mule-utf-8))
(ccl-execute-on-string ccl-encode-mule-utf-8
(make-vector 9 nil)
string)
string))
(defun oggc-decode-utf-8 (string)
"Decode STRING from utf-8."
(if (and (fboundp 'ccl-execute-on-string)
(boundp 'ccl-decode-mule-utf-8))
(ccl-execute-on-string ccl-decode-mule-utf-8
(make-vector 9 nil)
string)
string))
(defun oggc-read-string (length)
"Read a string from `point' of LENGTH characters.
Advances to (+ LENGTH (point))."
(buffer-substring-no-properties
(point) (goto-char (+ length (point)))))
(defun oggc-valid-ogg-stream-p ()
"Return non-nil if the current buffer contains a valid Ogg-Vorbis stream."
(or (search-forward oggc-ogg-header (min 100 (point-max)) t)
(error "File does not appear to be a valid ogg stream"))
(or (search-forward oggc-identification-header (min 300 (point-max)) t)
(error "Not a valid ogg stream")))
(defun oggc-comment-exists-p ()
"Return the value of `point' where comments are found in the current buffer."
(let ((max (save-excursion
(search-forward oggc-setup-header nil t)
(point))))
(and (search-forward oggc-comment-header max t)
(point))))
(defun oggc-bytes-to-lsb-int (n)
"Read N bytes as a LSB integer."
(loop for i from 0 below n
sum (* (expt 256 i)
(prog1 (char-after)
(forward-char 1)))))
(defun oggc-int-to-lsb-bytes (int n)
"Return a list of N bytes encoding INT as a LSB integer."
(nreverse (loop for i downfrom (1- n) to 0
for exp = (expt 256 i)
collect (floor int exp)
when (<= exp int)
do (setq int (/ int exp)))))
(defun oggc-construct-comment-field (comment-list)
"Construct an Ogg-Vorbis comment header from COMMENT-LIST.
COMMENT-LIST should be of the form (TITLE VALUE).
VALUE is encoded into UTF-8 if possible (`ccl-execute-on-string' and
`ccl-decode-mule-utf-8' available). The length of the thus ensuing
comment header is prepended to the string as a 4-byte lsb int."
(let* ((title (pop comment-list))
(value (pop comment-list)))
(setq title (concat title "="
(oggc-encode-utf-8 value)))
(concat (oggc-int-to-lsb-bytes (length title) 4)
title)))
(defun oggc-construct-vendor (vendor)
"Construct a vendor string from VENDOR."
(concat (oggc-int-to-lsb-bytes (length vendor) 4)
vendor))
;;; FIXME: This doesn't work!!
;;; Somehow, we need to modify one of the code-book headers to make
;;; note of the fact that the comment has changed. I can't see in
;;; the spec what needs to be done.
;;; This doesn't work even for the case where we don't change the
;;; length of the comment, just one character, e.g. tracknumber=1 to
;;; tracknumber=2.
(defun oggc-write-comments (file comments)
"Write COMMENTS to FILE.
COMMENTS should be as for `oggc-construct-comment-string' (q.v.)."
(with-temp-buffer
;; dog slow for large files.
;; an alternative would be to use head/tail/cut as needed to
;; split the file up and put it back together again.
(insert-file-contents-literally file)
(when (oggc-valid-ogg-stream-p)
(when (oggc-comment-exists-p)
(let ((vendor (save-excursion (oggc-read-vendor))))
(delete-region (point) (progn (oggc-read-comments (point))
(point)))
(insert (oggc-construct-vendor vendor)
(oggc-construct-comment-string comments))))
(write-region nil nil file))))
(defun oggc-construct-comment-string (comments)
"Construct a string off Ogg-Vorbis comment headers from COMMENTS.
COMMENTS should be an alist of the form:
((TITLE-1 VALUE-1)
(TITLE-2 VALUE-2))"
(concat (oggc-int-to-lsb-bytes (length comments) 4)
(mapconcat #'oggc-construct-comment-field comments "")))
(defun oggc-read-vendor ()
"Read an Ogg-Vorbis vendor string from the current buffer."
(let ((length (oggc-bytes-to-lsb-int 4)))
(oggc-read-string length)))
(defun oggc-read-comments (pos)
"Read Ogg-Vorbis comments, starting POS bytes from `point-min'."
(goto-char pos)
(let ((vendor (oggc-read-vendor))
(length (oggc-bytes-to-lsb-int 4))
comments)
(loop repeat length
for this-length = (oggc-bytes-to-lsb-int 4)
for c = (oggc-read-string this-length) do
(push (oggc-split-comment c) comments))
(list vendor (nreverse comments))))
(defun oggc-read-header (file)
"Read an Ogg-Vorbis header from FILE."
(with-part-of-file (file 0
;; Lets hope that the comments
;; aren't more than 28KB long.
(* 1024 28))
(when (oggc-valid-ogg-stream-p)
(aif (oggc-comment-exists-p)
(oggc-read-comments it)))))
(defun oggc-pretty-print-header (header)
"Print Ogg HEADER readably in a temporary buffer."
(let ((vendor (car header))
(comments (cadr header)))
(switch-to-buffer (get-buffer-create "*comments*"))
(erase-buffer)
(insert "Vendor: "vendor "\n")
(mapc #'(lambda (s)
(insert (car s) ": " (cadr s) "\n"))
comments)))
;;;###autoload
(defun oggc-show-header (file)
"Show a pretty printed representation of the Ogg Comments in FILE."
(interactive "FFile: ")
(oggc-pretty-print-header (oggc-read-header file)))
(provide 'ogg-comment)
;;; ogg-comment.el ends here
|