aboutsummaryrefslogtreecommitdiff
path: root/ogg-comment.el
blob: 74220fa8ae492b81bb8e89cf2286bd85d1870cdb (plain) (blame)
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 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.")

(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