aboutsummaryrefslogtreecommitdiff
path: root/ogg-comment.el
diff options
context:
space:
mode:
authorforcer <forcer>2005-09-11 20:05:00 +0000
committerforcer <mwolson@gnu.org>2005-09-11 20:05:00 +0000
commitbb65333ef00df02dbf6bd83294b4df49e64ea325 (patch)
tree5435715fe823d566ac5494bc672088522af5a763 /ogg-comment.el
Initial commit (CVS 2005-09-11)
darcs-hash:20050911200506-2189f-48a136015e33465c3cf09940ce935ec2203df463.gz
Diffstat (limited to 'ogg-comment.el')
-rw-r--r--ogg-comment.el271
1 files changed, 271 insertions, 0 deletions
diff --git a/ogg-comment.el b/ogg-comment.el
new file mode 100644
index 0000000..39a490a
--- /dev/null
+++ b/ogg-comment.el
@@ -0,0 +1,271 @@
+;;; ogg-comment.el --- Read Ogg-Vorbis file headers.
+
+;; Copyright (C) 2003, 2004 lawrence mitchell <wence@gmx.li>
+;; 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
+
+;; COPYRIGHT NOTICE
+
+;; 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 2 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.")
+
+(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)))
+
+(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)))
+
+(provide 'ogg-comment)
+
+;;; ogg-comment.el ends here