fordon_greeman / voidfiles

Branch: 2079ec5
Switch branches/tags
main
theidioticdev add emacs 2079ec5 22h ago
324 lines · 13.9 KB Raw Blame History
1 ;;; emms-info-native-ogg.el --- EMMS info functions for Ogg files -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2020-2023 Free Software Foundation, Inc.
4
5 ;; Author: Petteri Hintsanen <petterih@iki.fi>
6
7 ;; This file is part of EMMS.
8
9 ;; EMMS is free software; you can redistribute it and/or modify it
10 ;; under the terms of the GNU General Public License as published by
11 ;; the Free Software Foundation; either version 3, or (at your option)
12 ;; any later version.
13
14 ;; EMMS is distributed in the hope that it will be useful, but WITHOUT
15 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16 ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
17 ;; License for more details.
18
19 ;; You should have received a copy of the GNU General Public License
20 ;; along with EMMS; see the file COPYING. If not, write to the Free
21 ;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
22 ;; MA 02110-1301, USA.
23
24 ;;; Commentary:
25
26 ;; This file contains functions for extracting metadata from Ogg
27 ;; files, specifically from encapsulated Vorbis and Opus streams.
28 ;; Only elementary streams are supported.
29 ;;
30 ;; Ogg code is based on its programming documentation available at
31 ;; https://xiph.org/ogg/doc/.
32 ;;
33 ;; Vorbis code is based on xiph.org's Vorbis I specification,
34 ;; available at https://xiph.org/vorbis/doc/Vorbis_I_spec.html. See
35 ;; also emms-info-native-vorbis.el.
36 ;;
37 ;; Opus code is based on RFC 7845; see
38 ;; https://tools.ietf.org/html/rfc7845.html and
39 ;; emms-info-native-opus.el.
40
41 ;;; Code:
42
43 (require 'emms-info-native-opus)
44 (require 'emms-info-native-vorbis)
45 (require 'bindat)
46
47 (defconst emms-info-native-ogg--page-size 65307
48 "Maximum size for a single Ogg container page.")
49
50 (defconst emms-info-native-ogg--max-peek-size (* 16 1024 1024)
51 "Maximum buffer size for metadata decoding.
52 Functions in `emms-info-native-ogg' read certain amounts of data
53 into a temporary buffer while decoding metadata. This variable
54 controls the maximum size of that buffer: if more than
55 `emms-info-native-ogg--max-peek-size' bytes are needed, an error
56 is signaled.
57
58 Technically metadata blocks can have almost arbitrary lengths,
59 but in practice processing must be constrained to prevent memory
60 exhaustion in case of garbled or malicious inputs.")
61
62 (defconst emms-info-native-ogg--magic-pattern "OggS"
63 "Ogg format magic capture pattern.")
64
65 (defconst emms-info-native-ogg--page-bindat-spec
66 (if (eval-when-compile (fboundp 'bindat-type))
67 (bindat-type
68 (capture-pattern str 4)
69 (_ unit (unless (equal capture-pattern emms-info-native-ogg--magic-pattern)
70 (error "Ogg framing mismatch: expected `%s', got `%s'"
71 emms-info-native-ogg--magic-pattern
72 capture-pattern)))
73 (stream-structure-version u8)
74 (_ unit (unless (= stream-structure-version 0)
75 (error "Ogg version mismatch: expected 0, got %d"
76 stream-structure-version)))
77 (header-type-flag u8)
78 (granule-position sint 64 'le)
79 (stream-serial-number uintr 32)
80 (page-sequence-no uintr 32)
81 (page-checksum uintr 32)
82 (page-segments u8)
83 (segment-table vec page-segments)
84 (payload str (seq-reduce #'+ segment-table 0)))
85 ;; For Emacsen older than 28
86 '((capture-pattern str 4)
87 (eval (unless (equal last emms-info-native-ogg--magic-pattern)
88 (error "Ogg framing mismatch: expected `%s', got `%s'"
89 emms-info-native-ogg--magic-pattern
90 last)))
91 (stream-structure-version u8)
92 (eval (unless (= last 0)
93 (error "Ogg version mismatch: expected 0, got %s" last)))
94 (header-type-flag u8)
95 (granule-position-bytes vec 8)
96 (granule-position eval (emms-from-twos-complement
97 (emms-le-to-int last) 64))
98 (stream-serial-number u32r)
99 (page-sequence-no u32r)
100 (page-checksum u32r)
101 (page-segments u8)
102 (segment-table vec (page-segments))
103 (payload str (eval (seq-reduce #'+ last 0)))))
104 "Ogg page structure specification.")
105
106 (defconst emms-info-native-ogg--crc-table
107 [#x00000000 #x04C11DB7 #x09823B6E #x0D4326D9 #x130476DC
108 #x17C56B6B #x1A864DB2 #x1E475005 #x2608EDB8 #x22C9F00F
109 #x2F8AD6D6 #x2B4BCB61 #x350C9B64 #x31CD86D3 #x3C8EA00A
110 #x384FBDBD #x4C11DB70 #x48D0C6C7 #x4593E01E #x4152FDA9
111 #x5F15ADAC #x5BD4B01B #x569796C2 #x52568B75 #x6A1936C8
112 #x6ED82B7F #x639B0DA6 #x675A1011 #x791D4014 #x7DDC5DA3
113 #x709F7B7A #x745E66CD #x9823B6E0 #x9CE2AB57 #x91A18D8E
114 #x95609039 #x8B27C03C #x8FE6DD8B #x82A5FB52 #x8664E6E5
115 #xBE2B5B58 #xBAEA46EF #xB7A96036 #xB3687D81 #xAD2F2D84
116 #xA9EE3033 #xA4AD16EA #xA06C0B5D #xD4326D90 #xD0F37027
117 #xDDB056FE #xD9714B49 #xC7361B4C #xC3F706FB #xCEB42022
118 #xCA753D95 #xF23A8028 #xF6FB9D9F #xFBB8BB46 #xFF79A6F1
119 #xE13EF6F4 #xE5FFEB43 #xE8BCCD9A #xEC7DD02D #x34867077
120 #x30476DC0 #x3D044B19 #x39C556AE #x278206AB #x23431B1C
121 #x2E003DC5 #x2AC12072 #x128E9DCF #x164F8078 #x1B0CA6A1
122 #x1FCDBB16 #x018AEB13 #x054BF6A4 #x0808D07D #x0CC9CDCA
123 #x7897AB07 #x7C56B6B0 #x71159069 #x75D48DDE #x6B93DDDB
124 #x6F52C06C #x6211E6B5 #x66D0FB02 #x5E9F46BF #x5A5E5B08
125 #x571D7DD1 #x53DC6066 #x4D9B3063 #x495A2DD4 #x44190B0D
126 #x40D816BA #xACA5C697 #xA864DB20 #xA527FDF9 #xA1E6E04E
127 #xBFA1B04B #xBB60ADFC #xB6238B25 #xB2E29692 #x8AAD2B2F
128 #x8E6C3698 #x832F1041 #x87EE0DF6 #x99A95DF3 #x9D684044
129 #x902B669D #x94EA7B2A #xE0B41DE7 #xE4750050 #xE9362689
130 #xEDF73B3E #xF3B06B3B #xF771768C #xFA325055 #xFEF34DE2
131 #xC6BCF05F #xC27DEDE8 #xCF3ECB31 #xCBFFD686 #xD5B88683
132 #xD1799B34 #xDC3ABDED #xD8FBA05A #x690CE0EE #x6DCDFD59
133 #x608EDB80 #x644FC637 #x7A089632 #x7EC98B85 #x738AAD5C
134 #x774BB0EB #x4F040D56 #x4BC510E1 #x46863638 #x42472B8F
135 #x5C007B8A #x58C1663D #x558240E4 #x51435D53 #x251D3B9E
136 #x21DC2629 #x2C9F00F0 #x285E1D47 #x36194D42 #x32D850F5
137 #x3F9B762C #x3B5A6B9B #x0315D626 #x07D4CB91 #x0A97ED48
138 #x0E56F0FF #x1011A0FA #x14D0BD4D #x19939B94 #x1D528623
139 #xF12F560E #xF5EE4BB9 #xF8AD6D60 #xFC6C70D7 #xE22B20D2
140 #xE6EA3D65 #xEBA91BBC #xEF68060B #xD727BBB6 #xD3E6A601
141 #xDEA580D8 #xDA649D6F #xC423CD6A #xC0E2D0DD #xCDA1F604
142 #xC960EBB3 #xBD3E8D7E #xB9FF90C9 #xB4BCB610 #xB07DABA7
143 #xAE3AFBA2 #xAAFBE615 #xA7B8C0CC #xA379DD7B #x9B3660C6
144 #x9FF77D71 #x92B45BA8 #x9675461F #x8832161A #x8CF30BAD
145 #x81B02D74 #x857130C3 #x5D8A9099 #x594B8D2E #x5408ABF7
146 #x50C9B640 #x4E8EE645 #x4A4FFBF2 #x470CDD2B #x43CDC09C
147 #x7B827D21 #x7F436096 #x7200464F #x76C15BF8 #x68860BFD
148 #x6C47164A #x61043093 #x65C52D24 #x119B4BE9 #x155A565E
149 #x18197087 #x1CD86D30 #x029F3D35 #x065E2082 #x0B1D065B
150 #x0FDC1BEC #x3793A651 #x3352BBE6 #x3E119D3F #x3AD08088
151 #x2497D08D #x2056CD3A #x2D15EBE3 #x29D4F654 #xC5A92679
152 #xC1683BCE #xCC2B1D17 #xC8EA00A0 #xD6AD50A5 #xD26C4D12
153 #xDF2F6BCB #xDBEE767C #xE3A1CBC1 #xE760D676 #xEA23F0AF
154 #xEEE2ED18 #xF0A5BD1D #xF464A0AA #xF9278673 #xFDE69BC4
155 #x89B8FD09 #x8D79E0BE #x803AC667 #x84FBDBD0 #x9ABC8BD5
156 #x9E7D9662 #x933EB0BB #x97FFAD0C #xAFB010B1 #xAB710D06
157 #xA6322BDF #xA2F33668 #xBCB4666D #xB8757BDA #xB5365D03
158 #xB1F740B4]
159 "Lookup table for calculating Ogg checksums.")
160
161 (defun emms-info-native-ogg-decode-metadata (filename stream-type)
162 "Read and decode metadata from Ogg file FILENAME.
163 The file is assumed to contain a single stream of type
164 STREAM-TYPE, which must either `vorbis' or `opus'.
165
166 Return comments in a list of (FIELD . VALUE) cons cells.
167 Additionally return stream duration in `playing-time' field.
168
169 See `emms-info-native-vorbis--split-comment' for details."
170 (let* ((packets
171 (emms-info-native-ogg--read-and-decode-packets filename 2))
172 (headers
173 (emms-info-native-ogg--decode-headers packets stream-type))
174 (user-comments
175 (bindat-get-field headers 'comment-header 'user-comments))
176 (comments
177 (emms-info-native-vorbis-extract-comments user-comments))
178 (last-page
179 (emms-info-native-ogg--read-and-decode-last-page filename))
180 (granule-pos
181 (alist-get 'granule-position last-page))
182 (sample-rate
183 (if (eq stream-type 'vorbis)
184 (bindat-get-field headers
185 'identification-header
186 'sample-rate)
187 ;; Opus assumes a fixed sample rate of 48 kHz for granule
188 ;; position.
189 48000))
190 (playing-time
191 (when (and granule-pos (> granule-pos 0))
192 (/ granule-pos sample-rate))))
193 (nconc comments
194 (when playing-time
195 (list (cons "playing-time" playing-time))))))
196
197 (defun emms-info-native-ogg--read-and-decode-packets (filename packets)
198 "Read and decode PACKETS packets from Ogg file FILENAME.
199 Read in data from the start of FILENAME, remove Ogg packet
200 frames, and concatenate payloads until at least PACKETS number of
201 packets have been decoded. Return the decoded packets in a
202 string, concatenated.
203
204 Read data in `emms-info-native-ogg--page-size' chunks. If more
205 than `emms-info-native-ogg--max-peek-size' bytes of data would be
206 read, signal an error.
207
208 Only elementary streams are supported, that is, FILENAME should
209 contain only a single logical stream. Note that this assumption
210 is not verified: with non-elementary streams packets from
211 different streams will be mixed together without an error."
212 (let ((num-packets 0) (offset 0) (stream (list)))
213 (while (< num-packets packets)
214 (when (> offset emms-info-native-ogg--max-peek-size)
215 (error "Ogg payload is too large"))
216 (let ((page (emms-info-native-ogg--read-and-decode-page filename offset)))
217 (setq num-packets (+ num-packets
218 (emms-info-native-ogg--num-packets page)))
219 (setq offset (+ offset
220 (bindat-length
221 emms-info-native-ogg--page-bindat-spec page)))
222 (push (bindat-get-field page 'payload) stream)))
223 (reverse (mapconcat #'nreverse stream nil))))
224
225 (defun emms-info-native-ogg--read-and-decode-page (filename offset)
226 "Read and decode a single Ogg page from FILENAME.
227 Starting reading data from byte offset OFFSET.
228
229 Return the plist from `emms-info-native-ogg--decode-page'."
230 (with-temp-buffer
231 (set-buffer-multibyte nil)
232 (insert-file-contents-literally
233 filename nil offset (+ offset emms-info-native-ogg--page-size))
234 (bindat-unpack emms-info-native-ogg--page-bindat-spec
235 (buffer-string))))
236
237 (defun emms-info-native-ogg--num-packets (page)
238 "Return the number of packets in Ogg page PAGE.
239 PAGE must correspond to `emms-info-native-ogg--page-bindat-spec'."
240 ;; Every element that is less than 255 in the segment table
241 ;; represents a packet boundary.
242 (length (seq-filter (lambda (elt) (< elt 255))
243 (bindat-get-field page 'segment-table))))
244
245 (defun emms-info-native-ogg--decode-headers (packets stream-type)
246 "Decode first two stream headers from PACKETS for STREAM-TYPE.
247 STREAM-TYPE must be either `vorbis' or `opus'.
248
249 Return a structure that corresponds to either
250 `emms-info-native-opus--headers-bindat-spec' or
251 `emms-info-native-vorbis--headers-bindat-spec'."
252 (cond ((eq stream-type 'vorbis)
253 (bindat-unpack emms-info-native-vorbis--headers-bindat-spec
254 packets))
255 ((eq stream-type 'opus)
256 (bindat-unpack emms-info-native-opus--headers-bindat-spec
257 packets))
258 (t (error "Unknown stream type %s" stream-type))))
259
260 (defun emms-info-native-ogg--read-and-decode-last-page (filename)
261 "Read and decode the last Ogg page from FILENAME.
262 Return the page in bindat type structure."
263 (with-temp-buffer
264 (let* ((length (file-attribute-size
265 (file-attributes
266 (file-truename filename))))
267 (begin (max 0 (- length emms-info-native-ogg--page-size))))
268 (set-buffer-multibyte nil)
269 (insert-file-contents-literally filename nil begin length)
270 (emms-info-native-ogg--decode-last-page))))
271
272 (defun emms-info-native-ogg--decode-last-page ()
273 "Find and return the last valid Ogg page from the current buffer.
274 Ensure page synchronization by verifying page checksum.
275
276 Return the page in bindat type structure. If there is no valid
277 Ogg page in the buffer, return nil."
278 (let (page)
279 (goto-char (point-max))
280 (while (and (not page)
281 (search-backward emms-info-native-ogg--magic-pattern nil t))
282 (setq page (emms-info-native-ogg--verify-page)))
283 (when (and page
284 (> (logand (alist-get 'header-type-flag page) #x04) 0))
285 page)))
286
287 (defun emms-info-native-ogg--verify-page ()
288 "Verify Ogg page starting from point.
289 Unpack page into `emms-info-native-ogg--page-bindat-spec'
290 structure and calculate its checksum. Return the page if the
291 checksum is correct, or nil if the checksum does not match or the
292 page is otherwise invalid."
293 (ignore-errors
294 (let* ((offset (point))
295 (page
296 (bindat-unpack emms-info-native-ogg--page-bindat-spec
297 (buffer-string)
298 (1- offset)))
299 (num-bytes
300 (bindat-length emms-info-native-ogg--page-bindat-spec page))
301 (buf
302 (buffer-substring-no-properties offset
303 (+ offset num-bytes)))
304 (checksum
305 (emms-info-native-ogg--checksum (concat (substring buf 0 22)
306 [0 0 0 0]
307 (substring buf 26)))))
308 (when (= (alist-get 'page-checksum page) checksum) page))))
309
310 (defun emms-info-native-ogg--checksum (bytes)
311 "Calculate and return Ogg checksum for BYTES.
312 See URL `https://xiph.org/vorbis/doc/framing.html' for details on
313 checksum."
314 (let ((crc 0))
315 (dotimes (n (length bytes))
316 (setq crc (logxor (logand (ash crc 8) #xffffffff)
317 (aref emms-info-native-ogg--crc-table
318 (logxor (ash crc -24)
319 (aref bytes n))))))
320 crc))
321
322 (provide 'emms-info-native-ogg)
323
324 ;;; emms-info-native-ogg.el ends here