;;; ogg-comment.el --- Read Ogg-Vorbis file headers. ;; Copyright (C) 2003, 2004, 2005, 2006 Free Software Foundation, Inc. ;; Filename: ogg-comment.el ;; Version: $Revision: 1.5 $ ;; Author: lawrence mitchell ;; Maintainer: lawrence mitchell ;; 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 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 ;; 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