| 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 |