| 1 | ;;; emms-info-native-mp3.el --- EMMS info functions for MP3 files -*- lexical-binding: t; -*- |
| 2 | |
| 3 | ;; Copyright (C) 2020-2024 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 MP3 files |
| 27 | ;; with ID3v2 tags. The code is based on ID3v2 Informal Standards, |
| 28 | ;; see https://id3lib.sourceforge.net/id3/ |
| 29 | |
| 30 | ;; All ID3v2 versions should be recognized, but many features like |
| 31 | ;; CRC, compression and encryption are not supported. Since MP3 does |
| 32 | ;; not have a generally agreed-upon format for specifying the stream |
| 33 | ;; length, the reported playing time is only an estimate. For |
| 34 | ;; constant bit rate streams the estimate is usually accurate, but for |
| 35 | ;; variable bit rate streams it may be completely wrong, especially if |
| 36 | ;; there are no Xing/VBRI headers embedded in the file. |
| 37 | |
| 38 | ;; For technical details on MP3 duration estimation, see |
| 39 | ;; https://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header. |
| 40 | |
| 41 | ;;; Code: |
| 42 | |
| 43 | (require 'emms) |
| 44 | (require 'bindat) |
| 45 | (eval-when-compile (require 'subr-x)) |
| 46 | |
| 47 | |
| 48 | ;;; ID3 code |
| 49 | |
| 50 | (defvar emms-info-native-id3v2--version 0 |
| 51 | "Last decoded ID3v2 version.") |
| 52 | |
| 53 | (defconst emms-info-native-id3v2--magic-pattern "ID3" |
| 54 | "ID3v2 header magic pattern.") |
| 55 | |
| 56 | (defconst emms-info-native-id3v2--header-bindat-spec |
| 57 | (if (eval-when-compile (fboundp 'bindat-type)) |
| 58 | (bindat-type |
| 59 | (file-identifier str 3) |
| 60 | (_ unit (unless (equal file-identifier emms-info-native-id3v2--magic-pattern) |
| 61 | (error "ID3v2 framing mismatch: expected `%s', got `%s'" |
| 62 | emms-info-native-id3v2--magic-pattern |
| 63 | file-identifier))) |
| 64 | (version u8) |
| 65 | (_ unit (progn (setq emms-info-native-id3v2--version version) nil)) |
| 66 | (revision u8) |
| 67 | (flags bits 1) |
| 68 | (size-bytes vec 4) |
| 69 | (size unit (emms-info-native-id3v2--checked-size 'tag size-bytes))) |
| 70 | '((file-identifier str 3) |
| 71 | (eval (unless (equal last emms-info-native-id3v2--magic-pattern) |
| 72 | (error "ID3v2 framing mismatch: expected `%s', got `%s'" |
| 73 | emms-info-native-id3v2--magic-pattern |
| 74 | last))) |
| 75 | (version u8) |
| 76 | (eval (setq emms-info-native-id3v2--version last)) |
| 77 | (revision u8) |
| 78 | (flags bits 1) |
| 79 | (size-bytes vec 4) |
| 80 | (size eval (emms-info-native-id3v2--checked-size 'tag last)))) |
| 81 | "ID3v2 header specification.") |
| 82 | |
| 83 | (defconst emms-info-native-id3v2--frame-header-bindat-spec |
| 84 | (if (eval-when-compile (fboundp 'bindat-type)) |
| 85 | (bindat-type |
| 86 | (id str (if (= emms-info-native-id3v2--version 2) 3 4)) |
| 87 | (_ unit (unless (emms-info-native-id3v2--valid-frame-id-p id) |
| 88 | (error "ID3v2 frame id `%s' is invalid" id))) |
| 89 | (size-bytes vec (if (= emms-info-native-id3v2--version 2) 3 4)) |
| 90 | (size unit (emms-info-native-id3v2--checked-size 'frame size-bytes)) |
| 91 | (flags bits (if (= emms-info-native-id3v2--version 2) 0 2))) |
| 92 | '((id str (eval (if (= emms-info-native-id3v2--version 2) 3 4))) |
| 93 | (eval (unless (emms-info-native-id3v2--valid-frame-id-p last) |
| 94 | (error "ID3v2 frame id `%s' is invalid" last))) |
| 95 | (size-bytes vec (eval (if (= emms-info-native-id3v2--version 2) 3 4))) |
| 96 | (size eval (emms-info-native-id3v2--checked-size 'frame last)) |
| 97 | (flags bits (eval (if (= emms-info-native-id3v2--version 2) 0 2))))) |
| 98 | "ID3v2 frame header specification.") |
| 99 | |
| 100 | (defconst emms-info-native-id3v2--frame-to-info |
| 101 | '(("TAL" . "album") |
| 102 | ("TALB" . "album") |
| 103 | ("TPE2" . "albumartist") |
| 104 | ("TSO2" . "albumartistsort") |
| 105 | ("TSOA" . "albumsort") |
| 106 | ("TP1" . "artist") |
| 107 | ("TPE1" . "artist") |
| 108 | ("TSOP" . "artistsort") |
| 109 | ("TCM" . "composer") |
| 110 | ("TCOM" . "composer") |
| 111 | ("TSOC" . "composersort") |
| 112 | ("TDRC" . "date") |
| 113 | ("TPA" . "discnumber") |
| 114 | ("TPOS" . "discnumber") |
| 115 | ("TCON" . genre) |
| 116 | ("TPUB" . "label") |
| 117 | ("TDOR" . "originaldate") |
| 118 | ("TOR" . "originalyear") |
| 119 | ("TORY" . "originalyear") |
| 120 | ("TIT2" . "title") |
| 121 | ("TT2" . "title") |
| 122 | ("TSOT" . "titlesort") |
| 123 | ("TRK" . "tracknumber") |
| 124 | ("TRCK" . "tracknumber") |
| 125 | ("TYE" . "year") |
| 126 | ("TYER" . "year") |
| 127 | ("TXXX" . user-defined)) |
| 128 | "Mapping from ID3v2 frame identifiers to EMMS info fields. |
| 129 | |
| 130 | Sources: |
| 131 | |
| 132 | - URL `https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html' |
| 133 | - URL `http://wiki.hydrogenaud.io/index.php?title=Foobar2000:ID3_Tag_Mapping'") |
| 134 | |
| 135 | (defconst emms-info-id3v1--genres |
| 136 | '((0 . "Blues") |
| 137 | (1 . "Classic Rock") |
| 138 | (2 . "Country") |
| 139 | (3 . "Dance") |
| 140 | (4 . "Disco") |
| 141 | (5 . "Funk") |
| 142 | (6 . "Grunge") |
| 143 | (7 . "Hip-Hop") |
| 144 | (8 . "Jazz") |
| 145 | (9 . "Metal") |
| 146 | (10 . "New Age") |
| 147 | (11 . "Oldies") |
| 148 | (12 . "Other") |
| 149 | (13 . "Pop") |
| 150 | (14 . "R&B") |
| 151 | (15 . "Rap") |
| 152 | (16 . "Reggae") |
| 153 | (17 . "Rock") |
| 154 | (18 . "Techno") |
| 155 | (19 . "Industrial") |
| 156 | (20 . "Alternative") |
| 157 | (21 . "Ska") |
| 158 | (22 . "Death Metal") |
| 159 | (23 . "Pranks") |
| 160 | (24 . "Soundtrack") |
| 161 | (25 . "Euro-Techno") |
| 162 | (26 . "Ambient") |
| 163 | (27 . "Trip-Hop") |
| 164 | (28 . "Vocal") |
| 165 | (29 . "Jazz+Funk") |
| 166 | (30 . "Fusion") |
| 167 | (31 . "Trance") |
| 168 | (32 . "Classical") |
| 169 | (33 . "Instrumental") |
| 170 | (34 . "Acid") |
| 171 | (35 . "House") |
| 172 | (36 . "Game") |
| 173 | (37 . "Sound Clip") |
| 174 | (38 . "Gospel") |
| 175 | (39 . "Noise") |
| 176 | (40 . "AlternRock") |
| 177 | (41 . "Bass") |
| 178 | (42 . "Soul") |
| 179 | (43 . "Punk") |
| 180 | (44 . "Space") |
| 181 | (45 . "Meditative") |
| 182 | (46 . "Instrumental Pop") |
| 183 | (47 . "Instrumental Rock") |
| 184 | (48 . "Ethnic") |
| 185 | (49 . "Gothic") |
| 186 | (50 . "Darkwave") |
| 187 | (51 . "Techno-Industrial") |
| 188 | (52 . "Electronic") |
| 189 | (53 . "Pop-Folk") |
| 190 | (54 . "Eurodance") |
| 191 | (55 . "Dream") |
| 192 | (56 . "Southern Rock") |
| 193 | (57 . "Comedy") |
| 194 | (58 . "Cult") |
| 195 | (59 . "Gangsta") |
| 196 | (60 . "Top 40") |
| 197 | (61 . "Christian Rap") |
| 198 | (62 . "Pop/Funk") |
| 199 | (63 . "Jungle") |
| 200 | (64 . "Native American") |
| 201 | (65 . "Cabaret") |
| 202 | (66 . "New Wave") |
| 203 | (67 . "Psychadelic") |
| 204 | (68 . "Rave") |
| 205 | (69 . "Showtunes") |
| 206 | (70 . "Trailer") |
| 207 | (71 . "Lo-Fi") |
| 208 | (72 . "Tribal") |
| 209 | (73 . "Acid Punk") |
| 210 | (74 . "Acid Jazz") |
| 211 | (75 . "Polka") |
| 212 | (76 . "Retro") |
| 213 | (77 . "Musical") |
| 214 | (78 . "Rock & Roll") |
| 215 | (79 . "Hard Rock") |
| 216 | (80 . "Folk") |
| 217 | (81 . "Folk-Rock") |
| 218 | (82 . "National Folk") |
| 219 | (83 . "Swing") |
| 220 | (84 . "Fast Fusion") |
| 221 | (85 . "Bebob") |
| 222 | (86 . "Latin") |
| 223 | (87 . "Revival") |
| 224 | (88 . "Celtic") |
| 225 | (89 . "Bluegrass") |
| 226 | (90 . "Avantgarde") |
| 227 | (91 . "Gothic Rock") |
| 228 | (92 . "Progressive Rock") |
| 229 | (93 . "Psychedelic Rock") |
| 230 | (94 . "Symphonic Rock") |
| 231 | (95 . "Slow Rock") |
| 232 | (96 . "Big Band") |
| 233 | (97 . "Chorus") |
| 234 | (98 . "Easy Listening") |
| 235 | (99 . "Acoustic") |
| 236 | (100 . "Humour") |
| 237 | (101 . "Speech") |
| 238 | (102 . "Chanson") |
| 239 | (103 . "Opera") |
| 240 | (104 . "Chamber Music") |
| 241 | (105 . "Sonata") |
| 242 | (106 . "Symphony") |
| 243 | (107 . "Booty Bass") |
| 244 | (108 . "Primus") |
| 245 | (109 . "Porn Groove") |
| 246 | (110 . "Satire") |
| 247 | (111 . "Slow Jam") |
| 248 | (112 . "Club") |
| 249 | (113 . "Tango") |
| 250 | (114 . "Samba") |
| 251 | (115 . "Folklore") |
| 252 | (116 . "Ballad") |
| 253 | (117 . "Power Ballad") |
| 254 | (118 . "Rhythmic Soul") |
| 255 | (119 . "Freestyle") |
| 256 | (120 . "Duet") |
| 257 | (121 . "Punk Rock") |
| 258 | (122 . "Drum Solo") |
| 259 | (123 . "A cappella") |
| 260 | (124 . "Euro-House") |
| 261 | (125 . "Dance Hall")) |
| 262 | "ID3v1 genres.") |
| 263 | |
| 264 | (defconst emms-info-native-id3v2--text-encodings |
| 265 | '((0 . latin-1) |
| 266 | (1 . utf-16) |
| 267 | (2 . uft-16be) |
| 268 | (3 . utf-8)) |
| 269 | "ID3v2 text encodings.") |
| 270 | |
| 271 | (defun emms-info-native-id3v2--valid-frame-id-p (id) |
| 272 | "Return t if ID is a proper ID3v2 frame identifier, nil otherwise." |
| 273 | (if (= emms-info-native-id3v2--version 2) |
| 274 | (string-match "^[A-Z0-9]\\{3\\}$" id) |
| 275 | (string-match "^[A-Z0-9]\\{4\\}$" id))) |
| 276 | |
| 277 | (defun emms-info-native-id3v2--checked-size (elt bytes) |
| 278 | "Calculate ID3v2 element ELT size from BYTES. |
| 279 | ELT must be either `tag' or `frame'. |
| 280 | |
| 281 | Return the size. Signal an error if the size is zero." |
| 282 | (let ((size (cond ((eq elt 'tag) |
| 283 | (emms-info-native-id3v2--decode-size bytes t)) |
| 284 | ((eq elt 'frame) |
| 285 | (if (= emms-info-native-id3v2--version 4) |
| 286 | (emms-info-native-id3v2--decode-size bytes t) |
| 287 | (emms-info-native-id3v2--decode-size bytes nil)))))) |
| 288 | (if (zerop size) |
| 289 | (error "ID3v2 tag/frame size is zero") |
| 290 | size))) |
| 291 | |
| 292 | (defun emms-info-native-id3v2--decode-size (bytes syncsafe) |
| 293 | "Decode ID3v2 element size from BYTES. |
| 294 | Depending on SYNCSAFE, BYTES are interpreted as 7- or 8-bit |
| 295 | bytes, MSB first. |
| 296 | |
| 297 | Return the decoded size." |
| 298 | (let ((num-bits (if syncsafe 7 8)) |
| 299 | (mask (if syncsafe #x7f #xff))) |
| 300 | (apply '+ (seq-map-indexed (lambda (elt idx) |
| 301 | (* (expt 2 (* num-bits idx)) |
| 302 | (logand elt mask))) |
| 303 | (reverse bytes))))) |
| 304 | |
| 305 | (defun emms-info-native-id3v2--decode-header (filename) |
| 306 | "Read and decode ID3v2 header from FILENAME." |
| 307 | (with-temp-buffer |
| 308 | (set-buffer-multibyte nil) |
| 309 | (insert-file-contents-literally filename nil 0 10) |
| 310 | (bindat-unpack emms-info-native-id3v2--header-bindat-spec |
| 311 | (buffer-string)))) |
| 312 | |
| 313 | (defun emms-info-native-id3v2--checked-ext-header-size (filename) |
| 314 | "Read and decode ID3v2 extended header size from FILENAME. |
| 315 | Return the size. Signal an error if the size is zero." |
| 316 | (with-temp-buffer |
| 317 | (set-buffer-multibyte nil) |
| 318 | (insert-file-contents-literally filename nil 10 14) |
| 319 | (emms-info-native-id3v2--checked-size 'frame (buffer-string)))) |
| 320 | |
| 321 | (defun emms-info-native-id3v2--decode-frames (filename begin end unsync) |
| 322 | "Read and decode ID3v2 text frames from FILENAME. |
| 323 | BEGIN should be the offset of first byte of the first frame, and |
| 324 | END should be the offset after the complete ID3v2 tag. |
| 325 | |
| 326 | If UNSYNC is non-nil, the frames are assumed to have gone through |
| 327 | unsynchronization and decoded as such. |
| 328 | |
| 329 | Return metadata in a list of (FIELD . VALUE) cons cells." |
| 330 | (let ((offset begin) |
| 331 | (limit (- end (emms-info-native-id3v2--frame-header-size))) |
| 332 | comments) |
| 333 | (ignore-errors |
| 334 | (while (< offset limit) |
| 335 | (let* ((frame-data (emms-info-native-id3v2--decode-frame |
| 336 | filename offset unsync)) |
| 337 | (next-frame-offset (car frame-data)) |
| 338 | (comment (cdr frame-data))) |
| 339 | (when comment (push comment comments)) |
| 340 | (setq offset next-frame-offset)))) |
| 341 | comments)) |
| 342 | |
| 343 | (defun emms-info-native-id3v2--frame-header-size () |
| 344 | "Return the last decoded header size in bytes." |
| 345 | (if (= emms-info-native-id3v2--version 2) 6 10)) |
| 346 | |
| 347 | (defun emms-info-native-id3v2--decode-frame (filename offset unsync) |
| 348 | "Read and decode a single ID3v2 frame from FILENAME. |
| 349 | Start reading the frame from byte offset OFFSET. See |
| 350 | `emms-info-native-id3v2--read-frame-data' for details on UNSYNC. |
| 351 | |
| 352 | Skip frames that do not map to any info-id in |
| 353 | `emms-info-native-id3v2--frame-to-info'. |
| 354 | |
| 355 | Return cons cell (NEXT . FRAME), where NEXT is the offset of the |
| 356 | next frame (if any) and FRAME is the decoded frame. See |
| 357 | `emms-info-native-id3v2--decode-frame-data'." |
| 358 | (let* ((decoded (emms-info-native-id3v2--decode-frame-header |
| 359 | filename offset)) |
| 360 | (data-offset (car decoded)) |
| 361 | (header (cdr decoded)) |
| 362 | (frame-id (bindat-get-field header 'id)) |
| 363 | (info-id (cdr (assoc frame-id emms-info-native-id3v2--frame-to-info))) |
| 364 | (size (bindat-get-field header 'size))) |
| 365 | (if (or info-id unsync) |
| 366 | ;; Note that if unsync is non-nil, we have to always read the |
| 367 | ;; frame to determine next-frame-offset. |
| 368 | (let* ((data (emms-info-native-id3v2--read-frame-data |
| 369 | filename data-offset size unsync)) |
| 370 | (next-frame-offset (car data)) |
| 371 | (value (emms-info-native-id3v2--decode-frame-data |
| 372 | (cdr data) info-id))) |
| 373 | (cons next-frame-offset value)) |
| 374 | ;; Skip the frame. |
| 375 | (cons (+ data-offset size) nil)))) |
| 376 | |
| 377 | (defun emms-info-native-id3v2--decode-frame-header (filename begin) |
| 378 | "Read and decode ID3v2 frame header from FILENAME. |
| 379 | Start reading from byte offset BEGIN. |
| 380 | |
| 381 | Return a cons cell (OFFSET . HEADER), where OFFSET is the byte |
| 382 | offset after the frame header, and HEADER is the decoded frame |
| 383 | header." |
| 384 | (with-temp-buffer |
| 385 | (set-buffer-multibyte nil) |
| 386 | (let ((end (+ begin (emms-info-native-id3v2--frame-header-size)))) |
| 387 | (insert-file-contents-literally filename nil begin end) |
| 388 | (cons end |
| 389 | (bindat-unpack emms-info-native-id3v2--frame-header-bindat-spec |
| 390 | (buffer-string)))))) |
| 391 | |
| 392 | (defun emms-info-native-id3v2--read-frame-data (filename begin num-bytes unsync) |
| 393 | "Read NUM-BYTES of raw ID3v2 frame data from FILENAME. |
| 394 | Start reading from offset BEGIN. If UNSYNC is non-nil, all \"FF |
| 395 | 00\" byte combinations are replaced by \"FF\". Replaced byte |
| 396 | pairs are counted as one, instead of two, towards NUM-BYTES. |
| 397 | |
| 398 | Return a cons cell (OFFSET . DATA), where OFFSET is the byte |
| 399 | offset after NUM-BYTES bytes have been read, and DATA is the raw |
| 400 | data." |
| 401 | (with-temp-buffer |
| 402 | (set-buffer-multibyte nil) |
| 403 | (if unsync |
| 404 | ;; Reverse unsynchronization. |
| 405 | (let ((peek-end (+ begin (* 2 num-bytes))) |
| 406 | (end num-bytes)) |
| 407 | (insert-file-contents-literally filename nil begin peek-end) |
| 408 | (goto-char (point-min)) |
| 409 | (while (and (re-search-forward (string 255 0) nil t) |
| 410 | (< (point) end)) |
| 411 | (replace-match (string 255)) |
| 412 | (setq end (1+ end))) |
| 413 | (delete-region (1+ num-bytes) (point-max)) |
| 414 | (cons (+ begin end) (buffer-string))) |
| 415 | ;; No unsynchronization: read the data as-is. |
| 416 | (let ((end (+ begin num-bytes))) |
| 417 | (insert-file-contents-literally filename nil begin end) |
| 418 | (cons end (buffer-string)))))) |
| 419 | |
| 420 | (defun emms-info-native-id3v2--decode-frame-data (data info-id) |
| 421 | "Decode ID3v2 text frame data DATA. |
| 422 | If INFO-ID is `user-defined', assume that DATA is a TXXX frame |
| 423 | with key/value-pair. Extract the key and, if it is a mapped |
| 424 | element in `emms-info-native-id3v2--frame-to-info', use it as INFO-ID. |
| 425 | |
| 426 | If INFO-ID is `genre', assume that DATA is either an integral |
| 427 | ID3v1 genre reference or a plain genre string. In the former |
| 428 | case map the reference to a string via `emms-info-id3v1--genres'; |
| 429 | in the latter case use the genre string verbatim. |
| 430 | |
| 431 | Return a cons cell (INFO-ID . VALUE) where VALUE is the decoded |
| 432 | string, or nil if the decoding failed." |
| 433 | (when info-id |
| 434 | (let ((str (emms-info-native-id3v2--decode-string data))) |
| 435 | (cond ((string-empty-p str) nil) |
| 436 | ((stringp info-id) (cons info-id str)) |
| 437 | ((eq info-id 'genre) |
| 438 | (if (string-match "^(?\\([0-9]+\\))?" str) |
| 439 | (let ((v1-genre |
| 440 | (assoc (string-to-number (match-string 1 str)) |
| 441 | emms-info-id3v1--genres))) |
| 442 | (when v1-genre (cons "genre" (cdr v1-genre)))) |
| 443 | (cons "genre" str))) |
| 444 | ((eq info-id 'user-defined) |
| 445 | (let* ((key-val (split-string str (string 0))) |
| 446 | (key (downcase (car key-val))) |
| 447 | (val (cadr key-val))) |
| 448 | (when (and (rassoc key emms-info-native-id3v2--frame-to-info) |
| 449 | (not (string-empty-p val))) |
| 450 | (cons key val)))))))) |
| 451 | |
| 452 | (defun emms-info-native-id3v2--decode-string (bytes) |
| 453 | "Decode ID3v2 text information from BYTES. |
| 454 | Remove the terminating null byte, if any. |
| 455 | |
| 456 | Return the text as string." |
| 457 | (let* ((encoding (emms-info-native-id3v2--text-encoding bytes)) |
| 458 | (decoded (decode-coding-string (seq-rest bytes) encoding))) |
| 459 | (if (and (> (length decoded) 0) |
| 460 | (equal (substring decoded -1) "\0")) |
| 461 | (substring decoded 0 -1) |
| 462 | decoded))) |
| 463 | |
| 464 | (defun emms-info-native-id3v2--text-encoding (bytes) |
| 465 | "Return the encoding for text information BYTES." |
| 466 | (alist-get (seq-first bytes) |
| 467 | emms-info-native-id3v2--text-encodings)) |
| 468 | |
| 469 | |
| 470 | ;;; MP3 code |
| 471 | |
| 472 | (defconst emms-info-native-mp3--versions |
| 473 | '((0 . mpeg25) |
| 474 | (1 . reserved) |
| 475 | (2 . mpeg2) |
| 476 | (3 . mpeg1)) |
| 477 | "MPEG versions.") |
| 478 | |
| 479 | (defconst emms-info-native-mp3--layers |
| 480 | '((0 . reserved) |
| 481 | (1 . layerIII) |
| 482 | (2 . layerII) |
| 483 | (3 . layerI)) |
| 484 | "MPEG Audio Layers.") |
| 485 | |
| 486 | (defconst emms-info-native-mp3--channel-modes |
| 487 | '((0 . stereo) |
| 488 | (1 . joint-stereo) |
| 489 | (2 . dual-channel) |
| 490 | (3 . single-channel)) |
| 491 | "MPEG channel modes.") |
| 492 | |
| 493 | (defconst emms-info-native-mp3--bit-rates |
| 494 | '((mpeg1-layerI free 32 64 96 128 160 192 224 256 288 320 352 384 416 448 reserved) |
| 495 | (mpeg1-layerII free 32 48 56 64 80 96 112 128 160 192 224 256 320 384 reserved) |
| 496 | (mpeg1-layerIII free 32 40 48 56 64 80 96 112 128 160 192 224 256 320 reserved) |
| 497 | (mpeg2x-layerI free 32 48 56 64 80 96 112 128 144 160 176 192 224 256 reserved) |
| 498 | (mpeg2x-layerII-III free 8 16 24 32 40 48 56 64 80 96 112 128 144 160 reserved)) |
| 499 | "Bit rates for each MPEG version/layer combination.") |
| 500 | |
| 501 | (defconst emms-info-native-mp3--samples-per-frame |
| 502 | '((layerI . 384) |
| 503 | (layerII . 1152) |
| 504 | (layerIII-mpeg1 . 1152) |
| 505 | (layerIII-mpeg2x . 576)) |
| 506 | "Samples per frame for each MPEG version/layer combination.") |
| 507 | |
| 508 | (defconst emms-info-native-mp3--sample-rates |
| 509 | '((mpeg1 44100 48000 32000 reserved) |
| 510 | (mpeg2 22050 24000 16000 reserved) |
| 511 | (mpeg25 11025 12000 8000 reserved)) |
| 512 | "Sample rate for each MPEG version/layer combination.") |
| 513 | |
| 514 | (defconst emms-info-native-mp3--vbri-header-bindat-spec |
| 515 | (if (eval-when-compile (fboundp 'bindat-type)) |
| 516 | (bindat-type |
| 517 | (id str 4) |
| 518 | (version uint 16) |
| 519 | (delay uint 16) |
| 520 | (quality uint 16) |
| 521 | (bytes uint 32) |
| 522 | (frames uint 32)) |
| 523 | '((id str 4) |
| 524 | (version u16) |
| 525 | (delay u16) |
| 526 | (quality u16) |
| 527 | (bytes u32) |
| 528 | (frames u32))) |
| 529 | "VBR header, VBRI format. |
| 530 | This specification is purposefully incomplete, as we are |
| 531 | interested only in the frame count.") |
| 532 | |
| 533 | (defconst emms-info-native-mp3--xing-header-bindat-spec |
| 534 | (if (eval-when-compile (fboundp 'bindat-type)) |
| 535 | (bindat-type |
| 536 | (id vec 4) |
| 537 | (flags bits 4) |
| 538 | (frames uint 32)) |
| 539 | '((id vec 4) |
| 540 | (flags bits 4) |
| 541 | (frames u32))) |
| 542 | "VBR header, Xing/Info format. |
| 543 | This specification is purposefully incomplete, as we are |
| 544 | interested only in the frame count.") |
| 545 | |
| 546 | (defun emms-info-native-mp3-decode-metadata (filename) |
| 547 | "Read and decode metadata from MP3 file FILENAME. |
| 548 | Return metadata in a list of (FIELD . VALUE) cons cells, or nil |
| 549 | in case of errors or if there were no known fields in FILENAME. |
| 550 | Also try to estimate the stream duration, and return it in |
| 551 | `playing-time' field if successful. |
| 552 | |
| 553 | See `emms-info-native-id3v2--frame-to-info' for recognized fields." |
| 554 | (let* (emms-info-native-id3v2--version |
| 555 | (header (emms-info-native-id3v2--decode-header filename)) |
| 556 | (tag-size (bindat-get-field header 'size)) |
| 557 | (unsync (memq 7 (bindat-get-field header 'flags))) |
| 558 | (offset 10)) |
| 559 | (when (memq 6 (bindat-get-field header 'flags)) |
| 560 | ;; Skip the extended header. |
| 561 | (setq offset (+ offset |
| 562 | (emms-info-native-id3v2--checked-ext-header-size |
| 563 | filename)))) |
| 564 | (let ((tags |
| 565 | (emms-info-native-id3v2--decode-frames |
| 566 | filename offset (+ tag-size 10) unsync)) |
| 567 | (playtime |
| 568 | (emms-info-native-mp3--decode-duration filename (+ tag-size 10)))) |
| 569 | (nconc tags (when playtime |
| 570 | (list (cons "playing-time" playtime))))))) |
| 571 | |
| 572 | (defun emms-info-native-mp3--decode-duration (filename offset) |
| 573 | "Decode or estimate stream duration for MP3 file FILENAME. |
| 574 | Start looking for necessary headers from byte offset OFFSET. |
| 575 | |
| 576 | Return the duration in seconds, or nil in case of errors." |
| 577 | (with-temp-buffer |
| 578 | (set-buffer-multibyte nil) |
| 579 | (insert-file-contents-literally filename nil offset (+ offset 1024)) |
| 580 | (let* ((header |
| 581 | (emms-info-native-mp3--find-and-decode-frame-header)) |
| 582 | (samples-per-frame |
| 583 | (alist-get 'samples-per-frame header)) |
| 584 | (sample-rate |
| 585 | (alist-get 'sample-rate header)) |
| 586 | (bit-rate |
| 587 | (alist-get 'bit-rate header)) |
| 588 | (frames |
| 589 | (or (emms-info-native-mp3--find-and-decode-xing-header) |
| 590 | (emms-info-native-mp3--find-and-decode-vbri-header)))) |
| 591 | (cond ((and frames samples-per-frame (numberp sample-rate)) |
| 592 | ;; The file has a usable VBR (Xing, Info or VBRI) header. |
| 593 | (/ (* frames samples-per-frame) sample-rate)) |
| 594 | (bit-rate |
| 595 | ;; The file does not have a usable VBR header, therefore |
| 596 | ;; estimate the duration. |
| 597 | (emms-info-native-mp3--estimate-duration filename bit-rate)))))) |
| 598 | |
| 599 | (defun emms-info-native-mp3--find-and-decode-frame-header () |
| 600 | "Find and decode MP3 audio frame header from the current buffer. |
| 601 | Return the decoded header in an alist, or nil if a header cannot |
| 602 | be found or decoded. |
| 603 | |
| 604 | See `emms-info-native-mp3--decode-frame-header' for details." |
| 605 | (let (header) |
| 606 | (goto-char (point-min)) |
| 607 | (ignore-errors |
| 608 | (while (and (not header) |
| 609 | (search-forward (string 255) nil t)) |
| 610 | (let ((bytes |
| 611 | (emms-be-to-int |
| 612 | (buffer-substring-no-properties (- (point) 1) |
| 613 | (+ (point) 3))))) |
| 614 | (setq header |
| 615 | (emms-info-native-mp3--decode-frame-header bytes))))) |
| 616 | header)) |
| 617 | |
| 618 | (defun emms-info-native-mp3--decode-frame-header (header) |
| 619 | "Decode 32-bit numeric HEADER data. |
| 620 | Pack its elements to an alist and return the list. Return nil if |
| 621 | HEADER does not have MP3 sync bits set." |
| 622 | (when (= (logand header #xffe00000) #xffe00000) |
| 623 | (let* ((version-bits |
| 624 | (emms-extract-bits header 19 20)) |
| 625 | (layer-bits |
| 626 | (emms-extract-bits header 17 18)) |
| 627 | (crc-bit |
| 628 | (emms-extract-bits header 16)) |
| 629 | (bit-rate-bits |
| 630 | (emms-extract-bits header 12 15)) |
| 631 | (sample-rate-bits |
| 632 | (emms-extract-bits header 10 11)) |
| 633 | (padding-bit |
| 634 | (emms-extract-bits header 9)) |
| 635 | (private-bit |
| 636 | (emms-extract-bits header 8)) |
| 637 | (channel-mode-bits |
| 638 | (emms-extract-bits header 6 7)) |
| 639 | (mode-extension-bits |
| 640 | (emms-extract-bits header 4 5)) |
| 641 | (copyright-bit |
| 642 | (emms-extract-bits header 3)) |
| 643 | (original-bit |
| 644 | (emms-extract-bits header 2)) |
| 645 | (emphasis-bits |
| 646 | (emms-extract-bits header 0 1)) |
| 647 | (version |
| 648 | (alist-get version-bits |
| 649 | emms-info-native-mp3--versions)) |
| 650 | (layer |
| 651 | (alist-get layer-bits |
| 652 | emms-info-native-mp3--layers)) |
| 653 | (channel-mode |
| 654 | (alist-get channel-mode-bits |
| 655 | emms-info-native-mp3--channel-modes)) |
| 656 | (sample-rate |
| 657 | (nth sample-rate-bits |
| 658 | (alist-get version |
| 659 | emms-info-native-mp3--sample-rates))) |
| 660 | (samples-per-frame |
| 661 | (emms-info-native-mp3--samples-per-frame |
| 662 | version layer)) |
| 663 | (bit-rate |
| 664 | (emms-info-native-mp3--decode-bit-rate |
| 665 | version layer bit-rate-bits))) |
| 666 | (list (cons 'version version) |
| 667 | (cons 'layer layer) |
| 668 | (cons 'crc crc-bit) |
| 669 | (cons 'bit-rate bit-rate) |
| 670 | (cons 'sample-rate sample-rate) |
| 671 | (cons 'samples-per-frame samples-per-frame) |
| 672 | (cons 'padding padding-bit) |
| 673 | (cons 'private private-bit) |
| 674 | (cons 'channel-mode channel-mode) |
| 675 | (cons 'mode-extension mode-extension-bits) |
| 676 | (cons 'copyright copyright-bit) |
| 677 | (cons 'emphasis emphasis-bits) |
| 678 | (cons 'original original-bit))))) |
| 679 | |
| 680 | (defun emms-info-native-mp3--find-and-decode-xing-header () |
| 681 | "Find and decode Xing VBR header from the current buffer. |
| 682 | Return the number of frames in the stream, or nil if a header |
| 683 | cannot be found or decoded." |
| 684 | (goto-char (point-min)) |
| 685 | (when (re-search-forward "Xing\\|Info" (point-max) t) |
| 686 | (let ((header |
| 687 | (bindat-unpack emms-info-native-mp3--xing-header-bindat-spec |
| 688 | (buffer-string) |
| 689 | (1- (match-beginning 0))))) |
| 690 | (when (memq 0 (bindat-get-field header 'flags)) |
| 691 | (bindat-get-field header 'frames))))) |
| 692 | |
| 693 | (defun emms-info-native-mp3--find-and-decode-vbri-header () |
| 694 | "Find and decode VBRI header from the current buffer. |
| 695 | Return the number of frames in the stream, or nil if a header |
| 696 | cannot be found or decoded." |
| 697 | (goto-char (point-min)) |
| 698 | (when (re-search-forward "VBRI" (point-max) t) |
| 699 | (let ((header |
| 700 | (bindat-unpack emms-info-native-mp3--vbri-header-bindat-spec |
| 701 | (buffer-string) |
| 702 | (1- (match-beginning 0))))) |
| 703 | (bindat-get-field header 'frames)))) |
| 704 | |
| 705 | (defun emms-info-native-mp3--estimate-duration (filename bitrate) |
| 706 | "Estimate stream duration for FILENAME. |
| 707 | Assume constant encoding bit rate of BITRATE kilobits per second. |
| 708 | Return the estimated stream duration in seconds, or nil in case |
| 709 | of errors." |
| 710 | (let ((size |
| 711 | (file-attribute-size |
| 712 | (file-attributes (file-chase-links filename))))) |
| 713 | (when (and size (numberp bitrate)) |
| 714 | (/ (* 8 size) (* 1000 bitrate))))) |
| 715 | |
| 716 | (defun emms-info-native-mp3--decode-bit-rate (version layer bits) |
| 717 | "Return the bit rate for MPEG VERSION/LAYER combination. |
| 718 | BITS is the bitrate index from MP3 header." |
| 719 | (cond ((eq version 'mpeg1) |
| 720 | (cond ((eq layer 'layerI) |
| 721 | (nth bits |
| 722 | (alist-get 'mpeg1-layerI |
| 723 | emms-info-native-mp3--bit-rates))) |
| 724 | ((eq layer 'layerII) |
| 725 | (nth bits |
| 726 | (alist-get 'mpeg1-layerII |
| 727 | emms-info-native-mp3--bit-rates))) |
| 728 | ((eq layer 'layerIII) |
| 729 | (nth bits |
| 730 | (alist-get 'mpeg1-layerIII |
| 731 | emms-info-native-mp3--bit-rates))))) |
| 732 | (t (cond ((eq layer 'layerI) |
| 733 | (nth bits |
| 734 | (alist-get 'mpeg2x-layerI |
| 735 | emms-info-native-mp3--bit-rates))) |
| 736 | (t (nth bits |
| 737 | (alist-get 'mpeg2x-layerII-III |
| 738 | emms-info-native-mp3--bit-rates))))))) |
| 739 | |
| 740 | (defun emms-info-native-mp3--samples-per-frame (version layer) |
| 741 | "Return the samples per frame for MPEG VERSION/LAYER combination." |
| 742 | (cond ((eq layer 'layerIII) |
| 743 | (cond ((eq version 'mpeg1) |
| 744 | (alist-get 'layerIII-mpeg1 |
| 745 | emms-info-native-mp3--samples-per-frame)) |
| 746 | (t (alist-get 'layerIII-mpeg2x |
| 747 | emms-info-native-mp3--samples-per-frame)))) |
| 748 | ((eq layer 'layerII) |
| 749 | (alist-get 'layerII |
| 750 | emms-info-native-mp3--samples-per-frame)) |
| 751 | ((eq layer 'layerI) |
| 752 | (alist-get 'layerI |
| 753 | emms-info-native-mp3--samples-per-frame)))) |
| 754 | |
| 755 | (provide 'emms-info-native-mp3) |
| 756 | |
| 757 | ;;; emms-info-native-mp3.el ends here |