[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/emms 9d71a515f7 09/42: Split emms-info-native to severa
|
From: |
ELPA Syncer |
|
Subject: |
[elpa] externals/emms 9d71a515f7 09/42: Split emms-info-native to several files |
|
Date: |
Wed, 1 Nov 2023 15:57:59 -0400 (EDT) |
branch: externals/emms
commit 9d71a515f73e5366b8bc2c5e2b209d833ce9eaf8
Author: Petteri Hintsanen <petterih@iki.fi>
Commit: Petteri Hintsanen <petterih@iki.fi>
Split emms-info-native to several files
Hopefully this will make things easier to understand and maintain.
---
emms-info-flac.el | 169 ++++++
emms-info-mp3.el | 724 ++++++++++++++++++++++
emms-info-native.el | 1304 +---------------------------------------
emms-info-ogg.el | 326 ++++++++++
emms-info-opus.el | 98 +++
emms-info-spc.el | 2 +-
emms-info-vorbis.el | 189 ++++++
emms.el | 36 ++
test/emms-info-flac-tests.el | 58 ++
test/emms-info-mp3-tests.el | 104 ++++
test/emms-info-native-tests.el | 272 ---------
test/emms-info-ogg-tests.el | 128 ++++
test/emms-info-vorbis-tests.el | 56 ++
test/emms-tests.el | 40 ++
14 files changed, 1950 insertions(+), 1556 deletions(-)
diff --git a/emms-info-flac.el b/emms-info-flac.el
new file mode 100644
index 0000000000..743abe44bb
--- /dev/null
+++ b/emms-info-flac.el
@@ -0,0 +1,169 @@
+;;; emms-info-flac.el --- EMMS info functions for FLAC files -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2020-2023 Free Software Foundation, Inc.
+
+;; Author: Petteri Hintsanen <petterih@iki.fi>
+
+;; This file is part of EMMS.
+
+;; EMMS 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 3, or (at your option)
+;; any later version.
+
+;; EMMS 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.
+
+;; You should have received a copy of the GNU General Public License
+;; along with EMMS; see the file COPYING. If not, write to the Free
+;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+;; MA 02110-1301, USA.
+
+;;; Commentary:
+
+;; This file contains functions for extracting metadata from FLAC
+;; files in their native encapsulation format. The code is based on
+;; xiph.org's FLAC format specification, available at
+;; https://xiph.org/flac/format.html.
+
+;;; Code:
+
+(require 'bindat)
+(require 'emms)
+(require 'emms-info-vorbis)
+(eval-when-compile
+ (require 'cl-lib))
+
+(defconst emms-info-flac--max-peek-size (* 2048 1024)
+ "Maximum buffer size for metadata decoding.
+Functions in `emms-info-flac' read certain amounts of data into a
+temporary buffer while decoding metadata. This variable controls
+the maximum size of that buffer: if more than
+`emms-info-flac--max-peek-size' bytes are needed, an error is
+signaled.
+
+Technically metadata blocks can have almost arbitrary lengths,
+but in practice processing must be constrained to prevent memory
+exhaustion in case of garbled or malicious inputs.")
+
+(defconst emms-info-flac--meta-header-bindat-spec
+ '((flags u8)
+ (length u24)
+ (eval (when (or (> last emms-info-flac--max-peek-size)
+ (= last 0))
+ (error "FLAC block length %s is invalid" last))))
+ "FLAC metadata block header specification.")
+
+(defconst emms-info-flac--stream-info-block-bindat-spec
+ '((min-block-size u16)
+ (max-block-size u16)
+ (min-frame-size u24)
+ (max-frame-size u24)
+ (sample-metadata vec 8)
+ (md5 vec 16))
+ "FLAC stream info block specification.")
+
+(defconst emms-info-flac--comment-block-bindat-spec
+ '((vendor-length u32r)
+ (eval (when (> last emms-info-vorbis--max-vendor-length)
+ (error "FLAC vendor length %s is too long" last)))
+ (vendor-string vec (vendor-length))
+ (user-comments-list-length u32r)
+ (eval (when (> last emms-info-vorbis--max-comments)
+ (error "FLAC user comment list length %s is too long"
+ last)))
+ (user-comments repeat
+ (user-comments-list-length)
+ (struct emms-info-vorbis--comment-field-bindat-spec)))
+ "FLAC Vorbis comment block specification.")
+
+(defun emms-info-flac-decode-metadata (filename)
+ "Read and decode metadata from FLAC file FILENAME.
+Return comments in a list of (FIELD . VALUE) cons cells.
+Additionally return stream duration in `playing-time' field.
+
+See `emms-info-vorbis-extract-comments' for details."
+ (unless (emms-info-flac--has-signature filename)
+ (error "Invalid FLAC stream"))
+ (let* ((blocks
+ (emms-info-flac--decode-meta-blocks
+ (emms-info-flac--file-inserter filename)))
+ (comment-block
+ (bindat-unpack emms-info-flac--comment-block-bindat-spec
+ (car blocks)))
+ (stream-info-block
+ (bindat-unpack emms-info-flac--stream-info-block-bindat-spec
+ (cadr blocks)))
+ (user-comments
+ (bindat-get-field comment-block 'user-comments))
+ (comments
+ (emms-info-vorbis-extract-comments user-comments))
+ (playing-time
+ (emms-info-flac--decode-duration
+ (emms-be-to-int
+ (bindat-get-field stream-info-block 'sample-metadata)))))
+ (nconc comments
+ (when playing-time
+ (list (cons "playing-time" playing-time))))))
+
+(defun emms-info-flac--has-signature (filename)
+ "Check for FLAC stream marker at the beginning of FILENAME.
+Return t if there is a valid stream marker, nil otherwise."
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally filename nil 0 4)
+ (looking-at "fLaC")))
+
+(defun emms-info-flac--file-inserter (filename)
+ "Return a function that reads and inserts bytes from FILENAME.
+This is meant for `emms-info-flac--decode-meta-blocks'."
+ (lambda (offset end)
+ (insert-file-contents-literally filename nil offset end t)))
+
+(defun emms-info-flac--decode-meta-blocks (read-func)
+ "Decode metadata blocks from data supplied by READ-FUNC.
+Go through each metadata block looking for comment and stream
+info blocks. Extract and return them in a list, if found."
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (let (comment-block stream-info-block last-flag (offset 4))
+ (while (not last-flag)
+ (funcall read-func offset (cl-incf offset 4))
+ (let* ((header
+ (bindat-unpack emms-info-flac--meta-header-bindat-spec
+ (buffer-string)))
+ (end (+ offset (bindat-get-field header 'length)))
+ (flags (bindat-get-field header 'flags))
+ (block-type (logand flags #x7F)))
+ (setq last-flag (> (logand flags #x80) 0))
+ (when (> block-type 6)
+ (error "FLAC block type error: expected <= 6, got %s"
+ block-type))
+ (when (= block-type 0)
+ ;; Stream info block found, extract it.
+ (funcall read-func offset end)
+ (setq stream-info-block (vconcat (buffer-string))))
+ (when (= block-type 4)
+ ;; Comment block found, extract it.
+ (funcall read-func offset end)
+ (setq comment-block (vconcat (buffer-string))))
+ (setq offset end)))
+ (list comment-block stream-info-block))))
+
+(defun emms-info-flac--decode-duration (sample-meta)
+ "Decode stream duration from SAMPLE-META.
+SAMPLE-META should be a part of stream info metadata block. See
+`emms-info-flac--stream-info-block-bindat-spec'.
+
+Return the duration in seconds, or nil if it is not available."
+ (let ((sample-rate (emms-extract-bits sample-meta 44 63))
+ (num-samples (emms-extract-bits sample-meta 0 35)))
+ (when (and (> sample-rate 0)
+ (> num-samples 0))
+ (/ num-samples sample-rate))))
+
+(provide 'emms-info-flac)
+
+;;; emms-info-flac.el ends here
diff --git a/emms-info-mp3.el b/emms-info-mp3.el
new file mode 100644
index 0000000000..7fab134e06
--- /dev/null
+++ b/emms-info-mp3.el
@@ -0,0 +1,724 @@
+;;; emms-info-mp3.el --- EMMS info functions for MP3 files -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2020-2023 Free Software Foundation, Inc.
+
+;; Author: Petteri Hintsanen <petterih@iki.fi>
+
+;; This file is part of EMMS.
+
+;; EMMS 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 3, or (at your option)
+;; any later version.
+
+;; EMMS 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.
+
+;; You should have received a copy of the GNU General Public License
+;; along with EMMS; see the file COPYING. If not, write to the Free
+;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+;; MA 02110-1301, USA.
+
+;;; Commentary:
+
+;; This file contains functions for extracting metadata from MP3 files
+;; with id3v2 tags. The code is based on id3v2 Informal Standards,
+;; see https://id3lib.sourceforge.net/id3/
+
+;; All id3v2 versions should be recognized, but many features like
+;; CRC, compression and encryption are not supported. Since MP3 does
+;; not have a generally agreed-upon format for specifying the stream
+;; length, the reported playing time is only an estimate. For
+;; constant bit rate streams the estimate is usually accurate, but for
+;; variable bit rate streams it may be completely wrong, especially if
+;; there are no Xing/VBRI headers embedded in the file.
+
+;; For technical details on MP3 duration estimation, see
+;; https://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header.
+
+;;; Code:
+
+(require 'bindat)
+(require 'emms)
+(eval-when-compile
+ (require 'cl-lib))
+
+
+;;; id3 code
+
+(defvar emms-info-id3v2--version 0
+ "Last decoded id3v2 version.
+This is a kludge; it is needed because bindat spec cannot refer
+outside itself.")
+
+(defconst emms-info-id3v2--header-bindat-spec
+ '((file-identifier vec 3)
+ (eval (unless (equal last emms-info-id3v2--magic-pattern)
+ (error "id3v2 framing mismatch: expected `%s', got `%s'"
+ emms-info-id3v2--magic-pattern
+ last)))
+ (version u8)
+ (eval (setq emms-info-id3v2--version last))
+ (revision u8)
+ (flags bits 1)
+ (size-bytes vec 4)
+ (size eval (emms-info-id3v2--checked-size 'tag last)))
+ "id3v2 header specification.")
+
+(defconst emms-info-id3v2--magic-pattern
+ (string-to-vector "ID3")
+ "id3v2 header magic pattern.")
+
+(defconst emms-info-id3v2--frame-header-bindat-spec
+ '((id str (eval (if (= emms-info-id3v2--version 2) 3 4)))
+ (eval (unless (emms-info-id3v2--valid-frame-id-p last)
+ (error "id3v2 frame id `%s' is invalid" last)))
+ (size-bytes vec (eval (if (= emms-info-id3v2--version 2) 3 4)))
+ (size eval (emms-info-id3v2--checked-size 'frame last))
+ (flags bits (eval (if (= emms-info-id3v2--version 2) 0 2))))
+ "id3v2 frame header specification.")
+
+(defconst emms-info-id3v2--frame-to-info
+ '(("TAL" . "album")
+ ("TALB" . "album")
+ ("TPE2" . "albumartist")
+ ("TSO2" . "albumartistsort")
+ ("TSOA" . "albumsort")
+ ("TP1" . "artist")
+ ("TPE1" . "artist")
+ ("TSOP" . "artistsort")
+ ("TCM" . "composer")
+ ("TCOM" . "composer")
+ ("TSOC" . "composersort")
+ ("TDRC" . "date")
+ ("TPA" . "discnumber")
+ ("TPOS" . "discnumber")
+ ("TCON" . genre)
+ ("TPUB" . "label")
+ ("TDOR" . "originaldate")
+ ("TOR" . "originalyear")
+ ("TORY" . "originalyear")
+ ("TIT2" . "title")
+ ("TT2" . "title")
+ ("TSOT" . "titlesort")
+ ("TRK" . "tracknumber")
+ ("TRCK" . "tracknumber")
+ ("TYE" . "year")
+ ("TYER" . "year")
+ ("TXXX" . user-defined))
+ "Mapping from id3v2 frame identifiers to EMMS info fields.
+
+Sources:
+
+- URL `https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html'
+- URL `http://wiki.hydrogenaud.io/index.php?title=Foobar2000:ID3_Tag_Mapping'")
+
+(defconst emms-info-id3v1--genres
+ '((0 . "Blues")
+ (1 . "Classic Rock")
+ (2 . "Country")
+ (3 . "Dance")
+ (4 . "Disco")
+ (5 . "Funk")
+ (6 . "Grunge")
+ (7 . "Hip-Hop")
+ (8 . "Jazz")
+ (9 . "Metal")
+ (10 . "New Age")
+ (11 . "Oldies")
+ (12 . "Other")
+ (13 . "Pop")
+ (14 . "R&B")
+ (15 . "Rap")
+ (16 . "Reggae")
+ (17 . "Rock")
+ (18 . "Techno")
+ (19 . "Industrial")
+ (20 . "Alternative")
+ (21 . "Ska")
+ (22 . "Death Metal")
+ (23 . "Pranks")
+ (24 . "Soundtrack")
+ (25 . "Euro-Techno")
+ (26 . "Ambient")
+ (27 . "Trip-Hop")
+ (28 . "Vocal")
+ (29 . "Jazz+Funk")
+ (30 . "Fusion")
+ (31 . "Trance")
+ (32 . "Classical")
+ (33 . "Instrumental")
+ (34 . "Acid")
+ (35 . "House")
+ (36 . "Game")
+ (37 . "Sound Clip")
+ (38 . "Gospel")
+ (39 . "Noise")
+ (40 . "AlternRock")
+ (41 . "Bass")
+ (42 . "Soul")
+ (43 . "Punk")
+ (44 . "Space")
+ (45 . "Meditative")
+ (46 . "Instrumental Pop")
+ (47 . "Instrumental Rock")
+ (48 . "Ethnic")
+ (49 . "Gothic")
+ (50 . "Darkwave")
+ (51 . "Techno-Industrial")
+ (52 . "Electronic")
+ (53 . "Pop-Folk")
+ (54 . "Eurodance")
+ (55 . "Dream")
+ (56 . "Southern Rock")
+ (57 . "Comedy")
+ (58 . "Cult")
+ (59 . "Gangsta")
+ (60 . "Top 40")
+ (61 . "Christian Rap")
+ (62 . "Pop/Funk")
+ (63 . "Jungle")
+ (64 . "Native American")
+ (65 . "Cabaret")
+ (66 . "New Wave")
+ (67 . "Psychadelic")
+ (68 . "Rave")
+ (69 . "Showtunes")
+ (70 . "Trailer")
+ (71 . "Lo-Fi")
+ (72 . "Tribal")
+ (73 . "Acid Punk")
+ (74 . "Acid Jazz")
+ (75 . "Polka")
+ (76 . "Retro")
+ (77 . "Musical")
+ (78 . "Rock & Roll")
+ (79 . "Hard Rock")
+ (80 . "Folk")
+ (81 . "Folk-Rock")
+ (82 . "National Folk")
+ (83 . "Swing")
+ (84 . "Fast Fusion")
+ (85 . "Bebob")
+ (86 . "Latin")
+ (87 . "Revival")
+ (88 . "Celtic")
+ (89 . "Bluegrass")
+ (90 . "Avantgarde")
+ (91 . "Gothic Rock")
+ (92 . "Progressive Rock")
+ (93 . "Psychedelic Rock")
+ (94 . "Symphonic Rock")
+ (95 . "Slow Rock")
+ (96 . "Big Band")
+ (97 . "Chorus")
+ (98 . "Easy Listening")
+ (99 . "Acoustic")
+ (100 . "Humour")
+ (101 . "Speech")
+ (102 . "Chanson")
+ (103 . "Opera")
+ (104 . "Chamber Music")
+ (105 . "Sonata")
+ (106 . "Symphony")
+ (107 . "Booty Bass")
+ (108 . "Primus")
+ (109 . "Porn Groove")
+ (110 . "Satire")
+ (111 . "Slow Jam")
+ (112 . "Club")
+ (113 . "Tango")
+ (114 . "Samba")
+ (115 . "Folklore")
+ (116 . "Ballad")
+ (117 . "Power Ballad")
+ (118 . "Rhythmic Soul")
+ (119 . "Freestyle")
+ (120 . "Duet")
+ (121 . "Punk Rock")
+ (122 . "Drum Solo")
+ (123 . "A cappella")
+ (124 . "Euro-House")
+ (125 . "Dance Hall"))
+ "id3v1 genres.")
+
+(defconst emms-info-id3v2--text-encodings
+ '((0 . latin-1)
+ (1 . utf-16)
+ (2 . uft-16be)
+ (3 . utf-8))
+ "id3v2 text encodings.")
+
+(defun emms-info-id3v2--valid-frame-id-p (id)
+ "Return t if ID is a proper id3v2 frame identifier, nil otherwise."
+ (if (= emms-info-id3v2--version 2)
+ (string-match "^[A-Z0-9]\\{3\\}$" id)
+ (string-match "^[A-Z0-9]\\{4\\}$" id)))
+
+(defun emms-info-id3v2--checked-size (elt bytes)
+ "Calculate id3v2 element ELT size from BYTES.
+ELT must be either `tag' or `frame'.
+
+Return the size. Signal an error if the size is zero."
+ (let ((size (cond ((eq elt 'tag)
+ (emms-info-id3v2--decode-size bytes t))
+ ((eq elt 'frame)
+ (if (= emms-info-id3v2--version 4)
+ (emms-info-id3v2--decode-size bytes t)
+ (emms-info-id3v2--decode-size bytes nil))))))
+ (if (zerop size)
+ (error "id3v2 tag/frame size is zero")
+ size)))
+
+(defun emms-info-id3v2--decode-size (bytes syncsafe)
+ "Decode id3v2 element size from BYTES.
+Depending on SYNCSAFE, BYTES are interpreted as 7- or 8-bit
+bytes, MSB first.
+
+Return the decoded size."
+ (let ((num-bits (if syncsafe 7 8))
+ (mask (if syncsafe #x7f #xff)))
+ (apply '+ (seq-map-indexed (lambda (elt idx)
+ (* (expt 2 (* num-bits idx))
+ (logand elt mask)))
+ (reverse bytes)))))
+
+(defun emms-info-id3v2--decode-header (filename)
+ "Read and decode id3v2 header from FILENAME."
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally filename nil 0 10)
+ (bindat-unpack emms-info-id3v2--header-bindat-spec
+ (buffer-string))))
+
+(defun emms-info-id3v2--checked-ext-header-size (filename)
+ "Read and decode id3v2 extended header size from FILENAME.
+Return the size. Signal an error if the size is zero."
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally filename nil 10 14)
+ (emms-info-id3v2--checked-size 'frame (buffer-string))))
+
+(defun emms-info-id3v2--decode-frames (filename begin end unsync)
+ "Read and decode id3v2 text frames from FILENAME.
+BEGIN should be the offset of first byte of the first frame, and
+END should be the offset after the complete id3v2 tag.
+
+If UNSYNC is t, the frames are assumed to have gone through
+unsynchronization and decoded as such.
+
+Return metadata in a list of (FIELD . VALUE) cons cells."
+ (let ((offset begin)
+ (limit (- end (emms-info-id3v2--frame-header-size)))
+ comments)
+ (ignore-errors
+ (while (< offset limit)
+ (let* ((frame-data (emms-info-id3v2--decode-frame
+ filename offset unsync))
+ (next-frame-offset (car frame-data))
+ (comment (cdr frame-data)))
+ (when comment (push comment comments))
+ (setq offset next-frame-offset))))
+ comments))
+
+(defun emms-info-id3v2--frame-header-size ()
+ "Return the last decoded header size in bytes."
+ (if (= emms-info-id3v2--version 2) 6 10))
+
+(defun emms-info-id3v2--decode-frame (filename offset unsync)
+ "Read and decode a single id3v2 frame from FILENAME.
+Start reading the frame from byte offset OFFSET. See
+`emms-info-id3v2--read-frame-data' for details on UNSYNC.
+
+Skip frames that do not map to any info-id in
+`emms-info-id3v2--frame-to-info'.
+
+Return cons cell (NEXT . FRAME), where NEXT is the offset of the
+next frame (if any) and FRAME is the decoded frame. See
+`emms-info-id3v2--decode-frame-data'."
+ (let* ((decoded (emms-info-id3v2--decode-frame-header
+ filename offset))
+ (data-offset (car decoded))
+ (header (cdr decoded))
+ (frame-id (bindat-get-field header 'id))
+ (info-id (cdr (assoc frame-id emms-info-id3v2--frame-to-info)))
+ (size (bindat-get-field header 'size)))
+ (if (or info-id unsync)
+ ;; Note that if unsync is t, we have to always read the frame
+ ;; to determine next-frame-offset.
+ (let* ((data (emms-info-id3v2--read-frame-data
+ filename data-offset size unsync))
+ (next-frame-offset (car data))
+ (value (emms-info-id3v2--decode-frame-data
+ (cdr data) info-id)))
+ (cons next-frame-offset value))
+ ;; Skip the frame.
+ (cons (+ data-offset size) nil))))
+
+(defun emms-info-id3v2--decode-frame-header (filename begin)
+ "Read and decode id3v2 frame header from FILENAME.
+Start reading from byte offset BEGIN.
+
+Return a cons cell (OFFSET . HEADER), where OFFSET is the byte
+offset after the frame header, and HEADER is the decoded frame
+header."
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (let ((end (+ begin (emms-info-id3v2--frame-header-size))))
+ (insert-file-contents-literally filename nil begin end)
+ (cons end
+ (bindat-unpack emms-info-id3v2--frame-header-bindat-spec
+ (buffer-string))))))
+
+(defun emms-info-id3v2--read-frame-data (filename begin num-bytes unsync)
+ "Read NUM-BYTES of raw id3v2 frame data from FILENAME.
+Start reading from offset BEGIN. If UNSYNC is t, all \"FF 00\"
+byte combinations are replaced by \"FF\". Replaced byte pairs
+are counted as one, instead of two, towards NUM-BYTES.
+
+Return a cons cell (OFFSET . DATA), where OFFSET is the byte
+offset after NUM-BYTES bytes have been read, and DATA is the raw
+data."
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (if unsync
+ ;; Reverse unsynchronization.
+ (let ((peek-end (+ begin (* 2 num-bytes)))
+ (end num-bytes))
+ (insert-file-contents-literally filename nil begin peek-end)
+ (goto-char (point-min))
+ (while (and (re-search-forward (string 255 0) nil t)
+ (< (point) end))
+ (replace-match (string 255))
+ (cl-incf end 1))
+ (delete-region (1+ num-bytes) (point-max))
+ (cons (+ begin end) (buffer-string)))
+ ;; No unsynchronization: read the data as-is.
+ (let ((end (+ begin num-bytes)))
+ (insert-file-contents-literally filename nil begin end)
+ (cons end (buffer-string))))))
+
+(defun emms-info-id3v2--decode-frame-data (data info-id)
+ "Decode id3v2 text frame data DATA.
+If INFO-ID is `user-defined', assume that DATA is a TXXX frame
+with key/value-pair. Extract the key and, if it is a mapped
+element in `emms-info-id3v2--frame-to-info', use it as INFO-ID.
+
+If INFO-ID is `genre', assume that DATA is either an integral
+id3v1 genre reference or a plain genre string. In the former
+case map the reference to a string via `emms-info-id3v1--genres';
+in the latter case use the genre string verbatim.
+
+Return a cons cell (INFO-ID . VALUE) where VALUE is the decoded
+string."
+ (when info-id
+ (let ((str (emms-info-id3v2--decode-string data)))
+ (cond ((stringp info-id) (cons info-id str))
+ ((eq info-id 'genre)
+ (if (string-match "^(?\\([0-9]+\\))?" str)
+ (let ((v1-genre
+ (assoc (string-to-number (match-string 1 str))
+ emms-info-id3v1--genres)))
+ (when v1-genre (cons "genre" (cdr v1-genre))))
+ (cons "genre" str)))
+ ((eq info-id 'user-defined)
+ (let* ((key-val (split-string str (string 0)))
+ (key (downcase (car key-val)))
+ (val (cadr key-val)))
+ (when (rassoc key emms-info-id3v2--frame-to-info)
+ (cons key val))))))))
+
+(defun emms-info-id3v2--decode-string (bytes)
+ "Decode id3v2 text information from BYTES.
+Remove the terminating null byte, if any.
+
+Return the text as string."
+ (let* ((encoding (emms-info-id3v2--text-encoding bytes))
+ (string (mapconcat #'byte-to-string (seq-rest bytes) ""))
+ (decoded (decode-coding-string string encoding)))
+ (when (> (length decoded) 0)
+ (if (equal (substring decoded -1) "\0")
+ (substring decoded 0 -1)
+ decoded))))
+
+(defun emms-info-id3v2--text-encoding (bytes)
+ "Return the encoding for text information BYTES."
+ (alist-get (seq-first bytes)
+ emms-info-id3v2--text-encodings))
+
+
+;;; MP3 code
+
+(defconst emms-info-mp3--versions
+ '((0 . mpeg25)
+ (1 . reserved)
+ (2 . mpeg2)
+ (3 . mpeg1))
+ "MPEG versions.")
+
+(defconst emms-info-mp3--layers
+ '((0 . reserved)
+ (1 . layerIII)
+ (2 . layerII)
+ (3 . layerI))
+ "MPEG Audio Layers.")
+
+(defconst emms-info-mp3--channel-modes
+ '((0 . stereo)
+ (1 . joint-stereo)
+ (2 . dual-channel)
+ (3 . single-channel))
+ "MPEG channel modes.")
+
+(defconst emms-info-mp3--bit-rates
+ '((mpeg1-layerI free 32 64 96 128 160 192 224 256 288 320
352 384 416 448 reserved)
+ (mpeg1-layerII free 32 48 56 64 80 96 112 128 160 192
224 256 320 384 reserved)
+ (mpeg1-layerIII free 32 40 48 56 64 80 96 112 128 160
192 224 256 320 reserved)
+ (mpeg2x-layerI free 32 48 56 64 80 96 112 128 144 160
176 192 224 256 reserved)
+ (mpeg2x-layerII-III free 8 16 24 32 40 48 56 64 80 96
112 128 144 160 reserved))
+ "Bit rates for each MPEG version/layer combination.")
+
+(defconst emms-info-mp3--samples-per-frame
+ '((layerI . 384)
+ (layerII . 1152)
+ (layerIII-mpeg1 . 1152)
+ (layerIII-mpeg2x . 576))
+ "Samples per frame for each MPEG version/layer combination.")
+
+(defconst emms-info-mp3--sample-rates
+ '((mpeg1 44100 48000 32000 reserved)
+ (mpeg2 22050 24000 16000 reserved)
+ (mpeg25 11025 12000 8000 reserved))
+ "Sample rate for each MPEG version/layer combination.")
+
+(defconst emms-info-mp3--vbri-header-bindat-spec
+ '((id vec 4)
+ (version u16)
+ (delay u16)
+ (quality u16)
+ (bytes u32)
+ (frames u32))
+ "VBR header, VBRI format.
+This specification is purposefully incomplete, as we are
+interested only in the frame count.")
+
+(defconst emms-info-mp3--xing-header-bindat-spec
+ '((id vec 4)
+ (flags bits 4)
+ (frames u32))
+ "VBR header, Xing/Info format.
+This specification is purposefully incomplete, as we are
+interested only in the frame count.")
+
+(defun emms-info-mp3-decode-metadata (filename)
+ "Read and decode metadata from MP3 file FILENAME.
+Return metadata in a list of (FIELD . VALUE) cons cells, or nil
+in case of errors or if there were no known fields in FILENAME.
+Also try to estimate the stream duration, and return it in
+`playing-time' field if successful.
+
+See `emms-info-id3v2--frame-to-info' for recognized fields."
+ (let* (emms-info-id3v2--version
+ (header (emms-info-id3v2--decode-header filename))
+ (tag-size (bindat-get-field header 'size))
+ (unsync (memq 7 (bindat-get-field header 'flags)))
+ (offset 10))
+ (when (memq 6 (bindat-get-field header 'flags))
+ ;; Skip the extended header.
+ (cl-incf offset
+ (emms-info-id3v2--checked-ext-header-size filename)))
+ (let ((tags
+ (emms-info-id3v2--decode-frames
+ filename offset (+ tag-size 10) unsync))
+ (playtime
+ (emms-info-mp3--decode-duration filename (+ tag-size 10))))
+ (nconc tags (when playtime
+ (list (cons "playing-time" playtime)))))))
+
+(defun emms-info-mp3--decode-duration (filename offset)
+ "Decode or estimate stream duration for MP3 file FILENAME.
+Start looking for necessary headers from byte offset OFFSET.
+
+Return the duration in secods, or nil in case of errors."
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally filename nil offset (+ offset 1024))
+ (let* ((header
+ (emms-info-mp3--find-and-decode-frame-header))
+ (samples-per-frame
+ (alist-get 'samples-per-frame header))
+ (sample-rate
+ (alist-get 'sample-rate header))
+ (bit-rate
+ (alist-get 'bit-rate header))
+ (frames
+ (or (emms-info-mp3--find-and-decode-xing-header)
+ (emms-info-mp3--find-and-decode-vbri-header))))
+ (cond ((and frames samples-per-frame sample-rate)
+ ;; The file has a usable VBR (Xing, Info or VBRI) header.
+ (/ (* frames samples-per-frame) sample-rate))
+ (bit-rate
+ ;; The file does not have a usable VBR header, therefore
+ ;; estimate the duration.
+ (emms-info-mp3--estimate-duration filename bit-rate))))))
+
+(defun emms-info-mp3--find-and-decode-frame-header ()
+ "Find and decode MP3 audio frame header from the current buffer.
+Return the decoded header in an alist, or nil if a header cannot
+be found or decoded.
+
+See `emms-info-mp3--decode-frame-header' for details."
+ (let (header)
+ (goto-char (point-min))
+ (ignore-errors
+ (while (and (not header)
+ (search-forward (string 255) nil t))
+ (let ((bytes
+ (emms-be-to-int
+ (buffer-substring-no-properties (- (point) 1)
+ (+ (point) 3)))))
+ (setq header
+ (emms-info-mp3--decode-frame-header bytes)))))
+ header))
+
+(defun emms-info-mp3--decode-frame-header (header)
+ "Decode 32-bit numeric HEADER data.
+Pack its elements to an alist and return the list. Return nil if
+HEADER does not have MP3 sync bits set."
+ (when (= (logand header #xffe00000) #xffe00000)
+ (let* ((version-bits
+ (emms-extract-bits header 19 20))
+ (layer-bits
+ (emms-extract-bits header 17 18))
+ (crc-bit
+ (emms-extract-bits header 16))
+ (bit-rate-bits
+ (emms-extract-bits header 12 15))
+ (sample-rate-bits
+ (emms-extract-bits header 10 11))
+ (padding-bit
+ (emms-extract-bits header 9))
+ (private-bit
+ (emms-extract-bits header 8))
+ (channel-mode-bits
+ (emms-extract-bits header 6 7))
+ (mode-extension-bits
+ (emms-extract-bits header 4 5))
+ (copyright-bit
+ (emms-extract-bits header 3))
+ (original-bit
+ (emms-extract-bits header 2))
+ (emphasis-bits
+ (emms-extract-bits header 0 1))
+ (version
+ (alist-get version-bits
+ emms-info-mp3--versions))
+ (layer
+ (alist-get layer-bits
+ emms-info-mp3--layers))
+ (channel-mode
+ (alist-get channel-mode-bits
+ emms-info-mp3--channel-modes))
+ (sample-rate
+ (nth sample-rate-bits
+ (alist-get version
+ emms-info-mp3--sample-rates)))
+ (samples-per-frame
+ (emms-info-mp3--samples-per-frame
+ version layer))
+ (bit-rate
+ (emms-info-mp3--decode-bit-rate
+ version layer bit-rate-bits)))
+ (list (cons 'version version)
+ (cons 'layer layer)
+ (cons 'crc crc-bit)
+ (cons 'bit-rate bit-rate)
+ (cons 'sample-rate sample-rate)
+ (cons 'samples-per-frame samples-per-frame)
+ (cons 'padding padding-bit)
+ (cons 'private private-bit)
+ (cons 'channel-mode channel-mode)
+ (cons 'mode-extension mode-extension-bits)
+ (cons 'copyright copyright-bit)
+ (cons 'emphasis emphasis-bits)
+ (cons 'original original-bit)))))
+
+(defun emms-info-mp3--find-and-decode-xing-header ()
+ "Find and decode Xing VBR header from the current buffer.
+Return the number of frames in the stream, or nil if a header
+cannot be found or decoded."
+ (goto-char (point-min))
+ (when (re-search-forward "Xing\\|Info" (point-max) t)
+ (let ((header
+ (bindat-unpack emms-info-mp3--xing-header-bindat-spec
+ (buffer-string)
+ (1- (match-beginning 0)))))
+ (when (memq 0 (bindat-get-field header 'flags))
+ (bindat-get-field header 'frames)))))
+
+(defun emms-info-mp3--find-and-decode-vbri-header ()
+ "Find and decode VBRI header from the current buffer.
+Return the number of frames in the stream, or nil if a header
+cannot be found or decoded."
+ (goto-char (point-min))
+ (when (re-search-forward "VBRI" (point-max) t)
+ (let ((header
+ (bindat-unpack emms-info-mp3--vbri-header-bindat-spec
+ (buffer-string)
+ (1- (match-beginning 0)))))
+ (bindat-get-field header 'frames))))
+
+(defun emms-info-mp3--estimate-duration (filename bitrate)
+ "Estimate stream duration for FILENAME.
+Assume constant encoding bit rate of BITRATE kilobits per second.
+Return the estimated stream duration in seconds, or nil in case
+of errors."
+ (let ((size
+ (file-attribute-size
+ (file-attributes (file-chase-links filename)))))
+ (when bitrate (/ (* 8 size) (* 1000 bitrate)))))
+
+(defun emms-info-mp3--decode-bit-rate (version layer bits)
+ "Return the bit rate for MPEG VERSION/LAYER combination.
+BITS is the bitrate index from MP3 header."
+ (cond ((eq version 'mpeg1)
+ (cond ((eq layer 'layerI)
+ (nth bits
+ (alist-get 'mpeg1-layerI
+ emms-info-mp3--bit-rates)))
+ ((eq layer 'layerII)
+ (nth bits
+ (alist-get 'mpeg1-layerII
+ emms-info-mp3--bit-rates)))
+ ((eq layer 'layerIII)
+ (nth bits
+ (alist-get 'mpeg1-layerIII
+ emms-info-mp3--bit-rates)))))
+ (t (cond ((eq layer 'layerI)
+ (nth bits
+ (alist-get 'mpeg2x-layerI
+ emms-info-mp3--bit-rates)))
+ (t (nth bits
+ (alist-get 'mpeg2x-layerII-III
+ emms-info-mp3--bit-rates)))))))
+
+(defun emms-info-mp3--samples-per-frame (version layer)
+ "Return the samples per frame for MPEG VERSION/LAYER combination."
+ (cond ((eq layer 'layerIII)
+ (cond ((eq version 'mpeg1)
+ (alist-get 'layerIII-mpeg1
+ emms-info-mp3--samples-per-frame))
+ (t (alist-get 'layerIII-mpeg2x
+ emms-info-mp3--samples-per-frame))))
+ ((eq layer 'layerII)
+ (alist-get 'layerII
+ emms-info-mp3--samples-per-frame))
+ ((eq layer 'layerI)
+ (alist-get 'layerI
+ emms-info-mp3--samples-per-frame))))
+
+(provide 'emms-info-mp3)
+
+;;; emms-info-mp3.el ends here
diff --git a/emms-info-native.el b/emms-info-native.el
index b193aaa4f6..605c4f8094 100644
--- a/emms-info-native.el
+++ b/emms-info-native.el
@@ -34,22 +34,17 @@
;; The following file formats are supported:
;;
;; - Vorbis: Ogg Vorbis I Profile, filename extension `.ogg',
-;; elementary streams only. Based on xiph.org's Vorbis I
-;; specification, see URL
-;; `https://xiph.org/vorbis/doc/Vorbis_I_spec.html'.
+;; elementary streams only.
;;
;; - Opus: Ogg Opus profile, filename extension `.opus', elementary
-;; streams only. Based on RFC 7845, see URL
-;; `https://tools.ietf.org/html/rfc7845.html'.
+;; streams only.
;;
;; - FLAC streams in native encapsulation format, filename extension
-;; `.flac'. Based on xiph.org's FLAC format specification, see URL
-;; `https://xiph.org/flac/format.html'.
+;; `.flac'.
;;
;; - MP3 files with extension `.mp3' and id3v2 tags. All id3v2
-;; versions should work, but many features like CRC, compression and
-;; encryption are not supported. Based on id3v2 Informal Standards,
-;; see URL `https://id3.org'.
+;; versions should work, but many features like compression and
+;; encryption are not supported.
;;
;; - SPC files with extension `.spc' and id666 tags. This is an audio
;; file based on a memory dump from an SPC700, a special audio chip
@@ -57,1286 +52,28 @@
;;
;; Format detection is based solely on filename extension, which is
;; matched case-insensitively.
-;;
-;; For technical details on MP3 duration estimation, see URL
-;; `https://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header'.
;;; Code:
-(require 'bindat)
-(require 'cl-lib)
(require 'emms-info)
+(require 'emms-info-flac)
+(require 'emms-info-ogg)
+(require 'emms-info-mp3)
(require 'emms-info-spc)
-(require 'seq)
-(require 'subr-x)
-
-(defconst emms-info-native--max-peek-size (* 2048 1024)
- "Maximum buffer size for metadata decoding.
-Functions called by `emms-info-native' read certain amounts of
-data into a temporary buffer while decoding metadata. This
-variable controls the maximum size of that buffer: if more than
-`emms-info-native--max-peek-size' bytes are needed, an error is
-signaled.
-
-Technically metadata blocks can have almost arbitrary lengths,
-but in practice processing must be constrained to prevent memory
-exhaustion in case of garbled or malicious inputs.")
-
-(defvar emms-info-native--opus-channel-count 0
- "Last decoded Opus channel count.
-This is a kludge; it is needed because bindat spec cannot refer
-outside itself.")
-
-(defvar emms-info-native--id3v2-version 0
- "Last decoded id3v2 version.
-This is a kludge; it is needed because bindat spec cannot refer
-outside itself.")
-
-
-;;;; Vorbis code
-
-(defconst emms-info-native--max-num-vorbis-comments 1024
- "Maximum number of Vorbis comment fields in a stream.
-Technically a single Vorbis stream may have up to 2^32 comments,
-but in practice processing must be constrained to prevent memory
-exhaustion in case of garbled or malicious inputs.
-
-This limit is used with Opus and FLAC streams as well, since
-their comments have almost the same format as Vorbis.")
-
-(defconst emms-info-native--max-vorbis-comment-size (* 64 1024)
- "Maximum length for a single Vorbis comment field.
-Technically a single Vorbis comment may have a length up to 2^32
-bytes, but in practice processing must be constrained to prevent
-memory exhaustion in case of garbled or malicious inputs.
-
-This limit is used with Opus and FLAC streams as well, since
-their comments have almost the same format as Vorbis.")
-
-(defconst emms-info-native--max-vorbis-vendor-length 1024
- "Maximum length of Vorbis vendor string.
-Technically a vendor string can be up to 2^32 bytes long, but in
-practice processing must be constrained to prevent memory
-exhaustion in case of garbled or malicious inputs.
-
-This limit is used with Opus and FLAC streams as well, since
-their comments have almost the same format as Vorbis.")
-
-(defconst emms-info-native--accepted-vorbis-fields
- '("album"
- "albumartist"
- "albumartistsort"
- "albumsort"
- "artist"
- "artistsort"
- "composer"
- "composersort"
- "date"
- "discnumber"
- "genre"
- "label"
- "originaldate"
- "originalyear"
- "performer"
- "title"
- "titlesort"
- "tracknumber"
- "year")
- "EMMS info fields that are extracted from Vorbis comments.")
-
-(defconst emms-info-native--vorbis-headers-bindat-spec
- '((identification-header struct
emms-info-native--vorbis-identification-header-bindat-spec)
- (comment-header struct
emms-info-native--vorbis-comment-header-bindat-spec))
- "Specification for first two Vorbis header packets.
-They are always an identification header followed by a comment
-header.")
-
-(defconst emms-info-native--vorbis-identification-header-bindat-spec
- '((packet-type u8)
- (eval (unless (= last 1)
- (error "Vorbis header type mismatch: expected 1, got %s"
- last)))
- (vorbis vec 6)
- (eval (unless (equal last emms-info-native--vorbis-magic-pattern)
- (error "Vorbis framing mismatch: expected `%s', got `%s'"
- emms-info-native--vorbis-magic-pattern
- last)))
- (vorbis-version u32r)
- (eval (unless (= last 0)
- (error "Vorbis version mismatch: expected 0, got %s"
- last)))
- (channel-count u8)
- (sample-rate u32r)
- (bitrate-maximum u32r)
- (bitrate-nominal u32r)
- (bitrate-minimum u32r)
- (blocksize u8)
- (framing-flag u8)
- (eval (unless (= last 1))
- (error "Vorbis framing bit mismatch: expected 1, got %s"
- last)))
- "Vorbis identification header specification.")
-
-(defconst emms-info-native--vorbis-magic-pattern
- (string-to-vector "vorbis")
- "Header packet magic pattern.")
-
-(defconst emms-info-native--vorbis-comment-header-bindat-spec
- '((packet-type u8)
- (eval (unless (= last 3)
- (error "Vorbis header type mismatch: expected 3, got %s"
- last)))
- (vorbis vec 6)
- (eval (unless (equal last emms-info-native--vorbis-magic-pattern)
- (error "Vorbis framing mismatch: expected `%s', got `%s'"
- emms-info-native--vorbis-magic-pattern
- last)))
- (vendor-length u32r)
- (eval (when (> last emms-info-native--max-vorbis-vendor-length)
- (error "Vorbis vendor length %s is too long" last)))
- (vendor-string vec (vendor-length))
- (user-comments-list-length u32r)
- (eval (when (> last emms-info-native--max-num-vorbis-comments)
- (error "Vorbis user comment list length %s is too long"
- last)))
- (user-comments repeat
- (user-comments-list-length)
- (struct emms-info-native--vorbis-comment-field-bindat-spec))
- (framing-bit u8)
- (eval (unless (= last 1))
- (error "Vorbis framing bit mismatch: expected 1, got %s"
- last)))
- "Vorbis comment header specification.")
-
-(defconst emms-info-native--vorbis-comment-field-bindat-spec
- '((length u32r)
- (eval (when (> last emms-info-native--max-vorbis-comment-size)
- (error "Vorbis comment length %s is too long" last)))
- (user-comment vec (length)))
- "Vorbis comment field specification.")
-
-(defun emms-info-native--extract-vorbis-comments (user-comments)
- "Return a decoded list of comments from USER-COMMENTS.
-USER-COMMENTS should be a list of Vorbis comments according to
-`user-comments' field in
-`emms-info-native--vorbis-comment-header-bindat-spec',
-`emms-info-native--opus-comment-header-bindat-spec' or
-`emms-info-native--flac-comment-block-bindat-spec'.
-
-Return comments in a list of (FIELD . VALUE) cons cells. Only
-FIELDs that are listed in
-`emms-info-native--accepted-vorbis-fields' are returned."
- (let (comments)
- (dolist (user-comment user-comments)
- (let* ((comment (cdr (assoc 'user-comment user-comment)))
- (pair (emms-info-native--split-vorbis-comment comment)))
- (push pair comments)))
- (seq-filter (lambda (elt)
- (member (car elt)
- emms-info-native--accepted-vorbis-fields))
- comments)))
-
-(defun emms-info-native--split-vorbis-comment (comment)
- "Split Vorbis comment to a field-value pair.
-Vorbis comments are of form `FIELD=VALUE'. FIELD is a
-case-insensitive field name with a restricted set of ASCII
-characters. VALUE is an arbitrary UTF-8 encoded octet stream.
-Comments with empty FIELD or VALUE are ignored.
-
-Return a cons cell (FIELD . VALUE), where FIELD is converted to
-lower case and VALUE is the decoded value."
- (let ((comment-string (decode-coding-string (mapconcat
- #'byte-to-string
- comment
- nil)
- 'utf-8)))
- (when (string-match "^\\(.+?\\)=\\(.+\\)$" comment-string)
- (cons (downcase (match-string 1 comment-string))
- (match-string 2 comment-string)))))
-
-
-;;;; Opus code
-
-(defconst emms-info-native--opus-headers-bindat-spec
- '((identification-header struct
emms-info-native--opus-identification-header-bindat-spec)
- (comment-header struct emms-info-native--opus-comment-header-bindat-spec))
- "Specification for two first Opus header packets.
-They are always an identification header followed by a comment
-header.")
-
-(defconst emms-info-native--opus-identification-header-bindat-spec
- '((opus-head vec 8)
- (eval (unless (equal last emms-info-native--opus-head-magic-pattern)
- (error "Opus framing mismatch: expected `%s', got `%s'"
- emms-info-native--opus-head-magic-pattern
- last)))
- (opus-version u8)
- (eval (unless (< last 16)
- (error "Opus version mismatch: expected < 16, got %s"
- last)))
- (channel-count u8)
- (eval (setq emms-info-native--opus-channel-count last))
- (pre-skip u16r)
- (sample-rate u32r)
- (output-gain u16r)
- (channel-mapping-family u8)
- (union (channel-mapping-family)
- (0 nil)
- (t (struct emms-info-native--opus-channel-mapping-table))))
- "Opus identification header specification.")
-
-(defconst emms-info-native--opus-head-magic-pattern
- (string-to-vector "OpusHead")
- "Opus identification header magic pattern.")
-
-(defconst emms-info-native--opus-channel-mapping-table
- '((stream-count u8)
- (coupled-count u8)
- (channel-mapping vec (eval emms-info-native--opus-channel-count)))
- "Opus channel mapping table specification.")
-
-(defconst emms-info-native--opus-comment-header-bindat-spec
- '((opus-tags vec 8)
- (eval (unless (equal last emms-info-native--opus-tags-magic-pattern)
- (error "Opus framing mismatch: expected `%s', got `%s'"
- emms-info-native--opus-tags-magic-pattern
- last)))
- (vendor-length u32r)
- (eval (when (> last emms-info-native--max-vorbis-vendor-length)
- (error "Opus vendor length %s is too long" last)))
- (vendor-string vec (vendor-length))
- (user-comments-list-length u32r)
- (eval (when (> last emms-info-native--max-num-vorbis-comments)
- (error "Opus user comment list length %s is too long"
- last)))
- (user-comments repeat
- (user-comments-list-length)
- (struct
emms-info-native--vorbis-comment-field-bindat-spec)))
- "Opus comment header specification.")
-
-(defconst emms-info-native--opus-tags-magic-pattern
- (string-to-vector "OpusTags")
- "Opus comment header magic pattern.")
-
-
-;;;; Ogg code
-
-(defconst emms-info-native--ogg-page-size 65307
- "Maximum size for a single Ogg container page.")
-
-(defconst emms-info-native--ogg-page-bindat-spec
- '((capture-pattern vec 4)
- (eval (unless (equal last emms-info-native--ogg-magic-pattern)
- (error "Ogg framing mismatch: expected `%s', got `%s'"
- emms-info-native--ogg-magic-pattern
- last)))
- (stream-structure-version u8)
- (eval (unless (= last 0)
- (error "Ogg version mismatch: expected 0, got %s" last)))
- (header-type-flag u8)
- (granule-position vec 8)
- (stream-serial-number u32r)
- (page-sequence-no u32r)
- (page-checksum u32r)
- (page-segments u8)
- (segment-table vec (page-segments))
- (payload vec (eval (seq-reduce #'+ last 0))))
- "Ogg page structure specification.")
-
-(defconst emms-info-native--ogg-magic-pattern
- (string-to-vector "OggS")
- "Ogg format magic capture pattern.")
-
-(defconst emms-info-native--ogg-crc-table
- [#x00000000 #x04C11DB7 #x09823B6E #x0D4326D9 #x130476DC
- #x17C56B6B #x1A864DB2 #x1E475005 #x2608EDB8 #x22C9F00F
- #x2F8AD6D6 #x2B4BCB61 #x350C9B64 #x31CD86D3 #x3C8EA00A
- #x384FBDBD #x4C11DB70 #x48D0C6C7 #x4593E01E #x4152FDA9
- #x5F15ADAC #x5BD4B01B #x569796C2 #x52568B75 #x6A1936C8
- #x6ED82B7F #x639B0DA6 #x675A1011 #x791D4014 #x7DDC5DA3
- #x709F7B7A #x745E66CD #x9823B6E0 #x9CE2AB57 #x91A18D8E
- #x95609039 #x8B27C03C #x8FE6DD8B #x82A5FB52 #x8664E6E5
- #xBE2B5B58 #xBAEA46EF #xB7A96036 #xB3687D81 #xAD2F2D84
- #xA9EE3033 #xA4AD16EA #xA06C0B5D #xD4326D90 #xD0F37027
- #xDDB056FE #xD9714B49 #xC7361B4C #xC3F706FB #xCEB42022
- #xCA753D95 #xF23A8028 #xF6FB9D9F #xFBB8BB46 #xFF79A6F1
- #xE13EF6F4 #xE5FFEB43 #xE8BCCD9A #xEC7DD02D #x34867077
- #x30476DC0 #x3D044B19 #x39C556AE #x278206AB #x23431B1C
- #x2E003DC5 #x2AC12072 #x128E9DCF #x164F8078 #x1B0CA6A1
- #x1FCDBB16 #x018AEB13 #x054BF6A4 #x0808D07D #x0CC9CDCA
- #x7897AB07 #x7C56B6B0 #x71159069 #x75D48DDE #x6B93DDDB
- #x6F52C06C #x6211E6B5 #x66D0FB02 #x5E9F46BF #x5A5E5B08
- #x571D7DD1 #x53DC6066 #x4D9B3063 #x495A2DD4 #x44190B0D
- #x40D816BA #xACA5C697 #xA864DB20 #xA527FDF9 #xA1E6E04E
- #xBFA1B04B #xBB60ADFC #xB6238B25 #xB2E29692 #x8AAD2B2F
- #x8E6C3698 #x832F1041 #x87EE0DF6 #x99A95DF3 #x9D684044
- #x902B669D #x94EA7B2A #xE0B41DE7 #xE4750050 #xE9362689
- #xEDF73B3E #xF3B06B3B #xF771768C #xFA325055 #xFEF34DE2
- #xC6BCF05F #xC27DEDE8 #xCF3ECB31 #xCBFFD686 #xD5B88683
- #xD1799B34 #xDC3ABDED #xD8FBA05A #x690CE0EE #x6DCDFD59
- #x608EDB80 #x644FC637 #x7A089632 #x7EC98B85 #x738AAD5C
- #x774BB0EB #x4F040D56 #x4BC510E1 #x46863638 #x42472B8F
- #x5C007B8A #x58C1663D #x558240E4 #x51435D53 #x251D3B9E
- #x21DC2629 #x2C9F00F0 #x285E1D47 #x36194D42 #x32D850F5
- #x3F9B762C #x3B5A6B9B #x0315D626 #x07D4CB91 #x0A97ED48
- #x0E56F0FF #x1011A0FA #x14D0BD4D #x19939B94 #x1D528623
- #xF12F560E #xF5EE4BB9 #xF8AD6D60 #xFC6C70D7 #xE22B20D2
- #xE6EA3D65 #xEBA91BBC #xEF68060B #xD727BBB6 #xD3E6A601
- #xDEA580D8 #xDA649D6F #xC423CD6A #xC0E2D0DD #xCDA1F604
- #xC960EBB3 #xBD3E8D7E #xB9FF90C9 #xB4BCB610 #xB07DABA7
- #xAE3AFBA2 #xAAFBE615 #xA7B8C0CC #xA379DD7B #x9B3660C6
- #x9FF77D71 #x92B45BA8 #x9675461F #x8832161A #x8CF30BAD
- #x81B02D74 #x857130C3 #x5D8A9099 #x594B8D2E #x5408ABF7
- #x50C9B640 #x4E8EE645 #x4A4FFBF2 #x470CDD2B #x43CDC09C
- #x7B827D21 #x7F436096 #x7200464F #x76C15BF8 #x68860BFD
- #x6C47164A #x61043093 #x65C52D24 #x119B4BE9 #x155A565E
- #x18197087 #x1CD86D30 #x029F3D35 #x065E2082 #x0B1D065B
- #x0FDC1BEC #x3793A651 #x3352BBE6 #x3E119D3F #x3AD08088
- #x2497D08D #x2056CD3A #x2D15EBE3 #x29D4F654 #xC5A92679
- #xC1683BCE #xCC2B1D17 #xC8EA00A0 #xD6AD50A5 #xD26C4D12
- #xDF2F6BCB #xDBEE767C #xE3A1CBC1 #xE760D676 #xEA23F0AF
- #xEEE2ED18 #xF0A5BD1D #xF464A0AA #xF9278673 #xFDE69BC4
- #x89B8FD09 #x8D79E0BE #x803AC667 #x84FBDBD0 #x9ABC8BD5
- #x9E7D9662 #x933EB0BB #x97FFAD0C #xAFB010B1 #xAB710D06
- #xA6322BDF #xA2F33668 #xBCB4666D #xB8757BDA #xB5365D03
- #xB1F740B4]
- "Lookup table for calculating Ogg checksums.")
-
-(defun emms-info-native--decode-ogg-metadata (filename stream-type)
- "Read and decode metadata from Ogg file FILENAME.
-The file is assumed to contain a single stream of type
-STREAM-TYPE, which must either `vorbis' or `opus'.
-
-Return comments in a list of (FIELD . VALUE) cons cells. See
-`emms-info-native--split-vorbis-comment' for details."
- (let* ((packets (emms-info-native--decode-ogg-packets filename 2))
- (headers (emms-info-native--decode-ogg-headers packets
- stream-type))
- (rate (bindat-get-field headers 'identification-header 'sample-rate))
- (commdata (bindat-get-field headers 'comment-header 'user-comments))
- (lastpage (emms-info-native--read-and-decode-last-ogg-page filename))
- (granpos (cdr (assoc 'granule-position lastpage)))
- (playtime (emms-info-native--decode-ogg-granule-position granpos
rate))
- (comments (emms-info-native--extract-vorbis-comments commdata)))
- (nconc comments (when playtime (list (cons "playing-time" playtime))))))
-
-(defun emms-info-native--decode-ogg-packets (filename packets)
- "Read and decode packets from Ogg file FILENAME.
-Read in data from the start of FILENAME, remove Ogg packet
-frames, and concatenate payloads until at least PACKETS number of
-packets have been decoded. Return the decoded packets in a
-vector, concatenated.
-
-Data is read in `emms-info-native--ogg-page-size' chunks. If the
-total length of concatenated packets becomes greater than
-`emms-info-native--max-peek-size', an error is signaled.
-
-Only elementary streams are supported, that is, FILENAME should
-contain only a single logical stream. Note that this assumption
-is not verified: with non-elementary streams packets from
-different streams will be mixed together without an error."
- (let ((num-packets 0)
- (offset 0)
- (stream (vector)))
- (while (< num-packets packets)
- (let ((page (emms-info-native--read-and-decode-ogg-page filename
- offset)))
- (cl-incf num-packets (or (plist-get page :num-packets) 0))
- (cl-incf offset (plist-get page :num-bytes))
- (setq stream (vconcat stream (plist-get page :stream)))
- (when (> (length stream) emms-info-native--max-peek-size)
- (error "Ogg payload is too large"))))
- stream))
-
-(defun emms-info-native--read-and-decode-ogg-page (filename offset)
- "Read and decode a single Ogg page from FILENAME.
-Starting reading data from byte offset OFFSET.
-
-Return the plist from `emms-info-native--decode-ogg-page'."
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (insert-file-contents-literally filename
- nil
- offset
- (+ offset
- emms-info-native--ogg-page-size))
;FIXME: may go over file size
- (emms-info-native--decode-ogg-page (buffer-string))))
-
-(defun emms-info-native--decode-ogg-page (bytes)
- "Decode a single Ogg page from a sequence of BYTES.
-Return a plist (:num-packets N :num-bytes B :stream S), where N
-is the number of packets in the page, B is the size of the page
-in bytes, and S is the unframed logical bitstream in a vector.
-Note that N can be zero."
- (let* ((page (bindat-unpack emms-info-native--ogg-page-bindat-spec bytes))
- (num-packets (emms-info-native--num-of-packets page))
- (num-bytes (bindat-length emms-info-native--ogg-page-bindat-spec
page))
- (stream (bindat-get-field page 'payload)))
- (list :num-packets num-packets
- :num-bytes num-bytes
- :stream stream)))
-
-(defun emms-info-native--num-of-packets (page)
- "Return the number of packets in Ogg page PAGE.
-PAGE must correspond to
-`emms-info-native--ogg-page-bindat-spec'."
- ;; Every element that is less than 255 in the segment table
- ;; represents a packet boundary.
- (length (seq-filter (lambda (elt) (< elt 255))
- (bindat-get-field page 'segment-table))))
-
-(defun emms-info-native--decode-ogg-headers (packets stream-type)
- "Decode first two stream headers from PACKETS for STREAM-TYPE.
-STREAM-TYPE must be either `vorbis' or `opus'.
-
-Return a structure that corresponds to either
-`emms-info-native--opus-headers-bindat-spec' or
-`emms-info-native--vorbis-headers-bindat-spec'."
- (cond ((eq stream-type 'vorbis)
- (bindat-unpack emms-info-native--vorbis-headers-bindat-spec
- packets))
- ((eq stream-type 'opus)
- (let (emms-info-native--opus-channel-count)
- (bindat-unpack emms-info-native--opus-headers-bindat-spec
- packets)))
- (t (error "Unknown stream type %s" stream-type))))
-
-(defun emms-info-native--read-and-decode-last-ogg-page (filename)
- "Read and decode the last Ogg page from FILENAME.
-Return the page in bindat type structure."
- (with-temp-buffer
- (let* ((length (file-attribute-size (file-attributes filename)))
- (begin (max 0 (- length emms-info-native--ogg-page-size))))
- (set-buffer-multibyte nil)
- (insert-file-contents-literally filename nil begin length)
- (emms-info-native--decode-last-ogg-page))))
-
-(defun emms-info-native--decode-last-ogg-page ()
- "Find and return the last valid Ogg page from the current buffer.
-Ensure page synchronization by verifying Ogg page checksum.
-Return the page in bindat type structure. If there is no valid
-Ogg page in the buffer, return nil."
- (let (page)
- (goto-char (point-max))
- (while (and (not page)
- (search-backward (concat emms-info-native--ogg-magic-pattern)
nil t))
- (setq page (condition-case nil
- (emms-info-native--verify-ogg-page)
- (error nil))))
- (when (and page
- (> (logand (cdr (assoc 'header-type-flag page)) #x04) 0))
- page)))
-
-(defun emms-info-native--verify-ogg-page ()
- "Verify Ogg page starting from point.
-Unpack page into `emms-info-native--ogg-page-bindat-spec'
-structure and calculate its checksum. Return the page if the
-checksum is correct, or nil if the checksum does not match."
- (let* ((offset (point))
- (page (bindat-unpack emms-info-native--ogg-page-bindat-spec
- (buffer-string)
- (1- offset)))
- (num-bytes (bindat-length emms-info-native--ogg-page-bindat-spec
page))
- (buf (buffer-substring-no-properties offset (+ offset num-bytes)))
- (checksum (emms-info-native--calculate-ogg-checksum (concat
(substring buf 0 22)
- [0 0 0 0]
- (substring buf
26)))))
- (when (= (cdr (assoc 'page-checksum page)) checksum) page)))
-
-(defun emms-info-native--calculate-ogg-checksum (bytes)
- "Calculate and return Ogg checksum for BYTES.
-See URL `https://xiph.org/vorbis/doc/framing.html' for details on
-checksum."
- (let ((crc 0))
- (dotimes (n (length bytes))
- (setq crc (logxor (logand (ash crc 8) #xffffffff)
- (aref emms-info-native--ogg-crc-table
- (logxor (ash crc -24)
- (aref bytes n))))))
- crc))
-
-(defun emms-info-native--decode-ogg-granule-position (vec rate)
- "Decode Ogg granule position vector VEC for sampling rate RATE.
-Granule position is 64-bit little-endian signed integer counting
-the number of PCM samples per channel. If granule position is
--1, it was for a partial packet and hence invalid. In that case
-return nil."
- (let* ((int (emms-info-native--lsb-to-integer vec))
- (pos (emms-info-native--unsigned-to-signed int 64)))
- (unless (= pos -1) (/ pos rate))))
-
-(defun emms-info-native--lsb-to-integer (vec)
- (apply '+ (seq-map-indexed (lambda (elt idx)
- (* (expt 2 (* 8 idx)) elt))
- vec)))
-
-(defun emms-info-native--unsigned-to-signed (num bits)
- (let ((bitmask (1- (expt 2 bits)))
- (max (1- (expt 2 (1- bits)))))
- (if (> num max)
- (* -1 (1+ (logand (lognot num) bitmask)))
- num)))
-
-
-;;;; FLAC code
-
-(defconst emms-info-native--flac-metadata-block-header-bindat-spec
- '((flags u8)
- (length u24)
- (eval (when (or (> last emms-info-native--max-peek-size)
- (= last 0))
- (error "FLAC block length %s is invalid" last))))
- "FLAC metadata block header specification.")
-
-(defconst emms-info-native--flac-stream-info-block-bindat-spec
- '((min-block-size u16)
- (max-block-size u16)
- (min-frame-size u24)
- (max-frame-size u24)
- (sample-metadata vec 8)
- (md5 vec 16))
- "FLAC stream info block specification.")
-
-(defconst emms-info-native--flac-comment-block-bindat-spec
- '((vendor-length u32r)
- (eval (when (> last emms-info-native--max-vorbis-vendor-length)
- (error "FLAC vendor length %s is too long" last)))
- (vendor-string vec (vendor-length))
- (user-comments-list-length u32r)
- (eval (when (> last emms-info-native--max-num-vorbis-comments)
- (error "FLAC user comment list length %s is too long"
- last)))
- (user-comments repeat
- (user-comments-list-length)
- (struct
emms-info-native--vorbis-comment-field-bindat-spec)))
- "FLAC Vorbis comment block specification.")
-
-(defun emms-info-native--decode-flac-metadata (filename)
- "Read and decode metadata from FLAC file FILENAME.
-Return comments in a list of (FIELD . VALUE) cons cells. Only
-FIELDs that are listed in
-`emms-info-native--accepted-vorbis-fields' are returned.
-
-Also decode and return playing time in `playing-time' field, if
-it is available in the stream info block."
- (unless (emms-info-native--has-flac-signature filename)
- (error "Invalid FLAC stream"))
- (let* ((blocks (emms-info-native--decode-flac-meta-blocks
(emms-info-native--file-inserter filename)))
- (comment-block (bindat-unpack
emms-info-native--flac-comment-block-bindat-spec (car blocks)))
- (stream-info-block (bindat-unpack
emms-info-native--flac-stream-info-block-bindat-spec (cadr blocks)))
- (user-comments (bindat-get-field comment-block 'user-comments))
- (comments (emms-info-native--extract-vorbis-comments user-comments))
- (playtime (emms-info-native--decode-flac-playtime
- (emms-info-native--msb-to-integer
- (bindat-get-field stream-info-block 'sample-metadata)))))
- (nconc comments (when playtime (list (cons "playing-time" playtime))))))
-
-(defun emms-info-native--has-flac-signature (filename)
- "Check for FLAC stream marker at the beginning of FILENAME.
-Return t if there is a valid stream marker, nil otherwise."
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (insert-file-contents-literally filename nil 0 4)
- (looking-at "fLaC")))
-
-(defun emms-info-native--file-inserter (filename)
- "Return a function that reads and inserts bytes from FILENAME.
-This is meant for `emms-info-native--decode-flac-meta-blocks'."
- (lambda (offset end replace)
- (insert-file-contents-literally filename nil offset end replace)))
-
-(defun emms-info-native--decode-flac-meta-blocks (read-func)
- "Read and decode a comment block from FLAC file FILENAME.
-Return the comment block data in a vector.
-
-TODO: fix docstring"
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (let (comment-block
- stream-info-block
- last-flag
- (offset 4))
- (while (not last-flag)
- (funcall read-func offset (cl-incf offset 4) t)
- (let* ((header (bindat-unpack
emms-info-native--flac-metadata-block-header-bindat-spec
- (buffer-string)))
- (end (+ offset (bindat-get-field header 'length)))
- (flags (bindat-get-field header 'flags))
- (block-type (logand flags #x7F)))
- (setq last-flag (> (logand flags #x80) 0))
- (when (> block-type 6)
- (error "FLAC block type error: expected <= 6, got %s"
- block-type))
- (when (= block-type 0)
- ;; Stream info block found, extract it.
- (funcall read-func offset end t)
- (setq stream-info-block (vconcat (buffer-string))))
- (when (= block-type 4)
- ;; Comment block found, extract it.
- (funcall read-func offset end t)
- (setq comment-block (vconcat (buffer-string))))
- (setq offset end)))
- (list comment-block stream-info-block))))
-
-(defun emms-info-native--decode-flac-playtime (stream-info)
- (let ((sample-rate (emms-info-native--extract-bits stream-info 44 63))
- (num-samples (emms-info-native--extract-bits stream-info 0 35)))
- (when (and (> sample-rate 0)
- (> num-samples 0))
- (/ num-samples sample-rate))))
-
-
-;;;; MP3 code
-
-(defconst emms-info-native--id3v2-header-bindat-spec
- '((file-identifier vec 3)
- (eval (unless (equal last emms-info-native--id3v2-magic-pattern)
- (error "id3v2 framing mismatch: expected `%s', got `%s'"
- emms-info-native--id3v2-magic-pattern
- last)))
- (version u8)
- (eval (setq emms-info-native--id3v2-version last))
- (revision u8)
- (flags bits 1)
- (size-bytes vec 4)
- (size eval (emms-info-native--checked-id3v2-size 'tag last)))
- "id3v2 header specification.")
-
-(defconst emms-info-native--id3v2-magic-pattern
- (string-to-vector "ID3")
- "id3v2 header magic pattern.")
-
-(defconst emms-info-native--id3v2-frame-header-bindat-spec
- '((id str (eval (if (= emms-info-native--id3v2-version 2) 3 4)))
- (eval (unless (emms-info-native--valid-id3v2-frame-id-p last)
- (error "id3v2 frame id `%s' is invalid" last)))
- (size-bytes vec (eval (if (= emms-info-native--id3v2-version 2) 3 4)))
- (size eval (emms-info-native--checked-id3v2-size 'frame last))
- (flags bits (eval (if (= emms-info-native--id3v2-version 2) 0 2))))
- "id3v2 frame header specification.")
-
-(defconst emms-info-native--id3v2-frame-to-info
- '(("TAL" . "album")
- ("TALB" . "album")
- ("TPE2" . "albumartist")
- ("TSO2" . "albumartistsort")
- ("TSOA" . "albumsort")
- ("TP1" . "artist")
- ("TPE1" . "artist")
- ("TSOP" . "artistsort")
- ("TCM" . "composer")
- ("TCOM" . "composer")
- ("TSOC" . "composersort")
- ("TDRC" . "date")
- ("TPA" . "discnumber")
- ("TPOS" . "discnumber")
- ("TCON" . genre)
- ("TPUB" . "label")
- ("TDOR" . "originaldate")
- ("TOR" . "originalyear")
- ("TORY" . "originalyear")
- ("TIT2" . "title")
- ("TT2" . "title")
- ("TSOT" . "titlesort")
- ("TRK" . "tracknumber")
- ("TRCK" . "tracknumber")
- ("TYE" . "year")
- ("TYER" . "year")
- ("TXXX" . user-defined))
- "Mapping from id3v2 frame identifiers to EMMS info fields.
-
-Sources:
-
-- URL `https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html'
-- URL `http://wiki.hydrogenaud.io/index.php?title=Foobar2000:ID3_Tag_Mapping'")
-
-(defconst emms-info-native--id3v1-genres
- '((0 . "Blues")
- (1 . "Classic Rock")
- (2 . "Country")
- (3 . "Dance")
- (4 . "Disco")
- (5 . "Funk")
- (6 . "Grunge")
- (7 . "Hip-Hop")
- (8 . "Jazz")
- (9 . "Metal")
- (10 . "New Age")
- (11 . "Oldies")
- (12 . "Other")
- (13 . "Pop")
- (14 . "R&B")
- (15 . "Rap")
- (16 . "Reggae")
- (17 . "Rock")
- (18 . "Techno")
- (19 . "Industrial")
- (20 . "Alternative")
- (21 . "Ska")
- (22 . "Death Metal")
- (23 . "Pranks")
- (24 . "Soundtrack")
- (25 . "Euro-Techno")
- (26 . "Ambient")
- (27 . "Trip-Hop")
- (28 . "Vocal")
- (29 . "Jazz+Funk")
- (30 . "Fusion")
- (31 . "Trance")
- (32 . "Classical")
- (33 . "Instrumental")
- (34 . "Acid")
- (35 . "House")
- (36 . "Game")
- (37 . "Sound Clip")
- (38 . "Gospel")
- (39 . "Noise")
- (40 . "AlternRock")
- (41 . "Bass")
- (42 . "Soul")
- (43 . "Punk")
- (44 . "Space")
- (45 . "Meditative")
- (46 . "Instrumental Pop")
- (47 . "Instrumental Rock")
- (48 . "Ethnic")
- (49 . "Gothic")
- (50 . "Darkwave")
- (51 . "Techno-Industrial")
- (52 . "Electronic")
- (53 . "Pop-Folk")
- (54 . "Eurodance")
- (55 . "Dream")
- (56 . "Southern Rock")
- (57 . "Comedy")
- (58 . "Cult")
- (59 . "Gangsta")
- (60 . "Top 40")
- (61 . "Christian Rap")
- (62 . "Pop/Funk")
- (63 . "Jungle")
- (64 . "Native American")
- (65 . "Cabaret")
- (66 . "New Wave")
- (67 . "Psychadelic")
- (68 . "Rave")
- (69 . "Showtunes")
- (70 . "Trailer")
- (71 . "Lo-Fi")
- (72 . "Tribal")
- (73 . "Acid Punk")
- (74 . "Acid Jazz")
- (75 . "Polka")
- (76 . "Retro")
- (77 . "Musical")
- (78 . "Rock & Roll")
- (79 . "Hard Rock")
- (80 . "Folk")
- (81 . "Folk-Rock")
- (82 . "National Folk")
- (83 . "Swing")
- (84 . "Fast Fusion")
- (85 . "Bebob")
- (86 . "Latin")
- (87 . "Revival")
- (88 . "Celtic")
- (89 . "Bluegrass")
- (90 . "Avantgarde")
- (91 . "Gothic Rock")
- (92 . "Progressive Rock")
- (93 . "Psychedelic Rock")
- (94 . "Symphonic Rock")
- (95 . "Slow Rock")
- (96 . "Big Band")
- (97 . "Chorus")
- (98 . "Easy Listening")
- (99 . "Acoustic")
- (100 . "Humour")
- (101 . "Speech")
- (102 . "Chanson")
- (103 . "Opera")
- (104 . "Chamber Music")
- (105 . "Sonata")
- (106 . "Symphony")
- (107 . "Booty Bass")
- (108 . "Primus")
- (109 . "Porn Groove")
- (110 . "Satire")
- (111 . "Slow Jam")
- (112 . "Club")
- (113 . "Tango")
- (114 . "Samba")
- (115 . "Folklore")
- (116 . "Ballad")
- (117 . "Power Ballad")
- (118 . "Rhythmic Soul")
- (119 . "Freestyle")
- (120 . "Duet")
- (121 . "Punk Rock")
- (122 . "Drum Solo")
- (123 . "A cappella")
- (124 . "Euro-House")
- (125 . "Dance Hall"))
- "id3v1 genres.")
-
-(defconst emms-info-native--id3v2-text-encodings
- '((0 . latin-1)
- (1 . utf-16)
- (2 . uft-16be)
- (3 . utf-8))
- "id3v2 text encodings.")
-
-(defconst emms-info-native--mp3-audio-versions
- '((0 . mpeg25)
- (1 . reserved)
- (2 . mpeg2)
- (3 . mpeg1)))
-
-(defconst emms-info-native--mp3-layers
- '((0 . reserved)
- (1 . layerIII)
- (2 . layerII)
- (3 . layerI)))
-
-(defconst emms-info-native--mp3-channel-modes
- '((0 . stereo)
- (1 . joint-stereo)
- (2 . dual-channel)
- (3 . single-channel)))
-
-(defconst emms-info-native--mp3-bit-rates
- '((mpeg1-layerI 'free 32 64 96 128 160 192 224 256 288 320
352 384 416 448 'reserved)
- (mpeg1-layerII 'free 32 48 56 64 80 96 112 128 160 192
224 256 320 384 'reserved)
- (mpeg1-layerIII 'free 32 40 48 56 64 80 96 112 128 160
192 224 256 320 'reserved)
- (mpeg2x-layerI 'free 32 48 56 64 80 96 112 128 144 160
176 192 224 256 'reserved)
- (mpeg2x-layerII-III 'free 8 16 24 32 40 48 56 64 80 96
112 128 144 160 'reserved)))
-
-(defconst emms-info-native--mp3-samples-per-frame
- '((layerI . 384)
- (layerII . 1152)
- (layerIII-mpeg1 . 1152)
- (layerIII-mpeg2x . 576)))
-
-(defconst emms-info-native--mp3-sample-rates
- '((mpeg1 44100 48000 32000 'reserved)
- (mpeg2 22050 24000 16000 'reserved)
- (mpeg25 11025 12000 8000 'reserved)))
-
-(defconst emms-info-native--vbri-header-bindat-spec
- '((id vec 4)
- (version u16)
- (delay u16)
- (quality u16)
- (bytes u32)
- (frames u32))
- "VBR header, VBRI format.
-This spec is purposefully incomplete, as we are interested only
-in frame count.")
-
-(defconst emms-info-native--xing-header-bindat-spec
- '((id vec 4)
- (flags bits 4)
- (frames u32))
- "VBR header, Xing/Info format.
-This spec is purposefully incomplete, as we are is interested
-only in frame count.")
-
-(defun emms-info-native--valid-id3v2-frame-id-p (id)
- "Return t if ID is a proper id3v2 frame identifier, nil otherwise."
- (if (= emms-info-native--id3v2-version 2)
- (string-match "^[A-Z0-9]\\{3\\}$" id)
- (string-match "^[A-Z0-9]\\{4\\}$" id)))
-
-(defun emms-info-native--checked-id3v2-size (elt bytes)
- "Calculate id3v2 element ELT size from BYTES.
-ELT must be either \\='tag or \\='frame.
-
-Return the size. Signal an error if the size is zero."
- (let ((size (cond ((eq elt 'tag)
- (emms-info-native--decode-id3v2-size bytes t))
- ((eq elt 'frame)
- (if (= emms-info-native--id3v2-version 4)
- (emms-info-native--decode-id3v2-size bytes t)
- (emms-info-native--decode-id3v2-size bytes nil))))))
- (if (zerop size)
- (error "id3v2 tag/frame size is zero")
- size)))
-
-(defun emms-info-native--decode-id3v2-size (bytes syncsafe)
- "Decode id3v2 element size from BYTES.
-Depending on SYNCSAFE, BYTES are interpreted as 7- or 8-bit
-bytes, MSB first.
-
-Return the decoded size."
- (let ((num-bits (if syncsafe 7 8))
- (mask (if syncsafe #x7f #xff)))
- (apply '+ (seq-map-indexed (lambda (elt idx)
- (* (expt 2 (* num-bits idx))
- (logand elt mask)))
- (reverse bytes)))))
-
-(defun emms-info-native--decode-id3v2 (filename)
- "Read and decode id3v2 metadata from FILENAME.
-Return metadata in a list of (FIELD . VALUE) cons cells, or nil
-in case of errors or if there were no known fields in FILENAME.
-
-See `emms-info-native--id3v2-frame-to-info' for recognized
-fields."
- (condition-case nil
- (let* (emms-info-native--id3v2-version
- (header (emms-info-native--decode-id3v2-header filename))
- (tag-size (bindat-get-field header 'size))
- (unsync (memq 7 (bindat-get-field header 'flags)))
- (offset 10))
- (when (memq 6 (bindat-get-field header 'flags))
- ;; Skip the extended header.
- (cl-incf offset
- (emms-info-native--checked-id3v2-ext-header-size filename)))
- (let ((tags (emms-info-native--decode-id3v2-frames
- filename offset (+ tag-size 10) unsync))
- (playtime (emms-info-native--decode-mp3-duration
- filename (+ tag-size 10))))
- (nconc tags (when playtime
- (list (cons "playing-time" playtime))))))
- (error nil)))
-
-(defun emms-info-native--decode-id3v2-header (filename)
- "Read and decode id3v2 header from FILENAME."
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (insert-file-contents-literally filename nil 0 10)
- (bindat-unpack emms-info-native--id3v2-header-bindat-spec
- (buffer-string))))
-
-(defun emms-info-native--checked-id3v2-ext-header-size (filename)
- "Read and decode id3v2 extended header size from FILENAME.
-Return the size. Signal an error if the size is zero."
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (insert-file-contents-literally filename nil 10 14)
- (emms-info-native--checked-id3v2-size 'frame (buffer-string))))
-
-(defun emms-info-native--decode-id3v2-frames (filename begin end unsync)
- "Read and decode id3v2 text frames from FILENAME.
-BEGIN should be the offset of first byte of the first frame, and
-END should be the offset after the complete id3v2 tag.
-
-If UNSYNC is t, the frames are assumed to have gone through
-unsynchronization and decoded as such.
-
-Return metadata in a list of (FIELD . VALUE) cons cells."
- (let ((offset begin)
- (limit (- end (emms-info-native--id3v2-frame-header-size)))
- comments)
- (condition-case nil
- (while (< offset limit)
- (let* ((frame-data (emms-info-native--decode-id3v2-frame filename
- offset
- unsync))
- (next-frame-offset (car frame-data))
- (comment (cdr frame-data)))
- (when comment (push comment comments))
- (setq offset next-frame-offset)))
- (error nil))
- comments))
-
-(defun emms-info-native--id3v2-frame-header-size ()
- "Return the last decoded header size in bytes."
- (if (= emms-info-native--id3v2-version 2) 6 10))
-
-(defun emms-info-native--decode-id3v2-frame (filename offset unsync)
- (let* ((header (emms-info-native--decode-id3v2-frame-header filename
- offset))
- (info-id (emms-info-native--id3v2-frame-info-id header))
- (data-offset (car header))
- (size (bindat-get-field (cdr header) 'size)))
- (if (or info-id unsync)
- ;; Note that if unsync is t, we have to always read the frame
- ;; to determine next-frame-offset.
- (let* ((data (emms-info-native--read-id3v2-frame-data filename
- data-offset
- size
- unsync))
- (next-frame-offset (car data))
- (value (emms-info-native--decode-id3v2-frame-data (cdr data)
- info-id)))
- (cons next-frame-offset value))
- ;; Skip the frame.
- (cons (+ data-offset size) nil))))
-
-(defun emms-info-native--decode-id3v2-frame-header (filename begin)
- "Read and decode id3v2 frame header from FILENAME.
-Start reading from offset BEGIN.
-
-Return a cons cell (OFFSET . FRAME), where OFFSET is the byte
-offset after the frame header, and FRAME is the decoded frame."
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (let ((end (+ begin (emms-info-native--id3v2-frame-header-size))))
- (insert-file-contents-literally filename nil begin end)
- (cons end (bindat-unpack emms-info-native--id3v2-frame-header-bindat-spec
- (buffer-string))))))
-
-(defun emms-info-native--id3v2-frame-info-id (frame)
- "Return the emms-info identifier for FRAME.
-If there is no such identifier, return nil."
- (cdr (assoc (bindat-get-field frame 'id)
- emms-info-native--id3v2-frame-to-info)))
-
-(defun emms-info-native--read-id3v2-frame-data (filename
- begin
- num-bytes
- unsync)
- "Read NUM-BYTES of raw id3v2 frame data from FILENAME.
-Start reading from offset BEGIN. If UNSYNC is t, all 'FF 00'
-byte combinations are replaced by 'FF'. Replaced byte pairs are
-counted as one, instead of two, towards NUM-BYTES.
-
-Return a cons cell (OFFSET . DATA), where OFFSET is the byte
-offset after NUM-BYTES bytes have been read, and DATA is the raw
-data."
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (if unsync
- ;; Reverse unsynchronization.
- (let ((peek-end (+ begin (* 2 num-bytes)))
- (end num-bytes))
- (insert-file-contents-literally filename nil begin peek-end)
- (goto-char (point-min))
- (while (and (re-search-forward (string 255 0) nil t)
- (< (point) end))
- (replace-match (string 255))
- (cl-incf end 1))
- (delete-region (1+ num-bytes) (point-max))
- (cons (+ begin end) (buffer-string)))
- ;; No unsynchronization: read the data as-is.
- (let ((end (+ begin num-bytes)))
- (insert-file-contents-literally filename nil begin end)
- (cons end (buffer-string))))))
-
-(defun emms-info-native--decode-id3v2-frame-data (data info-id)
- "Decode id3v2 text frame data DATA.
-If INFO-ID is `user-defined', assume that DATA is a TXXX frame
-with key/value-pair. Extract the key and, if it is a mapped
-element in `emms-info-native--id3v2-frame-to-info', use it as
-INFO-ID.
-
-If INFO-ID is `genre', assume that DATA is either an integral
-id3v1 genre reference or a plain genre string. In the former
-case map the reference to a string via
-`emms-info-native--id3v1-genres'; in the latter case use the
-genre string verbatim.
-
-Return a cons cell (INFO-ID . VALUE) where VALUE is the decoded
-string."
- (when info-id
- (let ((str (emms-info-native--decode-id3v2-string data)))
- (cond ((stringp info-id) (cons info-id str))
- ((eq info-id 'genre)
- (if (string-match "^(?\\([0-9]+\\))?" str)
- (let ((v1-genre (assoc (string-to-number (match-string 1 str))
- emms-info-native--id3v1-genres)))
- (when v1-genre (cons "genre" (cdr v1-genre))))
- (cons "genre" str)))
- ((eq info-id 'user-defined)
- (let* ((key-val (split-string str (string 0)))
- (key (downcase (car key-val)))
- (val (cadr key-val)))
- (when (rassoc key emms-info-native--id3v2-frame-to-info)
- (cons key val))))))))
-
-(defun emms-info-native--decode-id3v2-string (bytes)
- "Decode id3v2 text information from BYTES.
-Remove the terminating null byte, if any.
-
-Return the text as string."
- (let* ((encoding (emms-info-native--id3v2-text-encoding bytes))
- (string (mapconcat #'byte-to-string (seq-rest bytes) ""))
- (decoded (decode-coding-string string encoding)))
- (when (> (length decoded) 0)
- (if (equal (substring decoded -1) "\0")
- (substring decoded 0 -1)
- decoded))))
-
-(defun emms-info-native--id3v2-text-encoding (bytes)
- "Return the encoding for text information BYTES."
- (cdr (assoc (seq-first bytes)
- emms-info-native--id3v2-text-encodings)))
-
-(defun emms-info-native--decode-mp3-duration (filename offset)
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (insert-file-contents-literally filename nil offset (+ offset 1024))
- (let* ((header (emms-info-native--find-and-decode-mp3-frame-header))
- (samples-per-frame (alist-get 'samples-per-frame header))
- (sample-rate (alist-get 'sample-rate header))
- (bit-rate (alist-get 'bit-rate header))
- (frames (or (emms-info-native--find-and-decode-xing-header)
- (emms-info-native--find-and-decode-vbri-header))))
- (cond ((and frames samples-per-frame sample-rate)
- (/ (* frames samples-per-frame) sample-rate))
- (bit-rate
- (emms-info-native--estimate-mp3-duration
- filename bit-rate))))))
-
-(defun emms-info-native--find-and-decode-mp3-frame-header ()
- (let (header)
- (goto-char (point-min))
- (condition-case nil
- (while (and (not header)
- (search-forward (string 255)))
- (let ((bytes (emms-info-native--msb-to-integer
- (buffer-substring-no-properties
- (- (point) 1) (+ (point) 3)))))
- (setq header
- (emms-info-native--decode-mp3-frame-header bytes))))
- (error nil))
- header))
-
-(defun emms-info-native--decode-mp3-frame-header (header)
- (when (= (logand header #xffe00000) #xffe00000)
- (let* ((version-bits
- (emms-info-native--extract-bits header 19 20))
- (layer-bits
- (emms-info-native--extract-bits header 17 18))
- (crc-bit
- (emms-info-native--extract-bits header 16))
- (bit-rate-bits
- (emms-info-native--extract-bits header 12 15))
- (sample-rate-bits
- (emms-info-native--extract-bits header 10 11))
- (padding-bit
- (emms-info-native--extract-bits header 9))
- (private-bit
- (emms-info-native--extract-bits header 8))
- (channel-mode-bits
- (emms-info-native--extract-bits header 6 7))
- (mode-extension-bits
- (emms-info-native--extract-bits header 4 5))
- (copyright-bit
- (emms-info-native--extract-bits header 3))
- (original-bit
- (emms-info-native--extract-bits header 2))
- (emphasis-bits
- (emms-info-native--extract-bits header 0 1))
- (version
- (alist-get version-bits
- emms-info-native--mp3-audio-versions))
- (layer
- (alist-get layer-bits
- emms-info-native--mp3-layers))
- (channel-mode
- (alist-get channel-mode-bits
- emms-info-native--mp3-channel-modes))
- (sample-rate
- (nth sample-rate-bits
- (alist-get version
- emms-info-native--mp3-sample-rates)))
- (samples-per-frame
- (emms-info-native--get-samples-per-frame
- version layer))
- (bit-rate
- (emms-info-native--decode-bit-rate
- version layer bit-rate-bits)))
- (list (cons 'version version)
- (cons 'layer layer)
- (cons 'crc crc-bit)
- (cons 'bit-rate bit-rate)
- (cons 'sample-rate sample-rate)
- (cons 'samples-per-frame samples-per-frame)
- (cons 'padding padding-bit)
- (cons 'private private-bit)
- (cons 'channel-mode channel-mode)
- (cons 'mode-extension mode-extension-bits)
- (cons 'copyright copyright-bit)
- (cons 'emphasis emphasis-bits)
- (cons 'original original-bit)))))
-
-(defun emms-info-native--find-and-decode-xing-header ()
- (goto-char (point-min))
- (when (re-search-forward "Xing\\|Info" (point-max) t)
- (let ((header (bindat-unpack
- emms-info-native--xing-header-bindat-spec
- (buffer-string) (1- (match-beginning 0)))))
- (when (memq 0 (bindat-get-field header 'flags))
- (bindat-get-field header 'frames)))))
-
-(defun emms-info-native--find-and-decode-vbri-header ()
- (goto-char (point-min))
- (when (re-search-forward "VBRI" (point-max) t)
- (let ((header (bindat-unpack
- emms-info-native--vbri-header-bindat-spec
- (buffer-string) (1- (match-beginning 0)))))
- (bindat-get-field header 'frames))))
-
-(defun emms-info-native--estimate-mp3-duration (filename bitrate)
- (let ((size (file-attribute-size
- (file-attributes (file-chase-links filename)))))
- (when bitrate (/ (* 8 size) (* 1000 bitrate)))))
-
-(defun emms-info-native--extract-bits (int from &optional to)
- "Extract consequent set bits FROM[..TO] from INT.
-The first (rightmost) bit is zero. Return the value of bits as
-if they would have been shifted to right by FROM positions."
- (unless to (setq to from))
- (let ((num-bits (1+ (- to from)))
- (mask (1- (expt 2 (1+ to)))))
- (when (> num-bits 0) (ash (logand int mask) (- from)))))
-
-(defun emms-info-native--msb-to-integer (vec)
- (emms-info-native--lsb-to-integer (reverse vec)))
-
-(defun emms-info-native--decode-bit-rate (version layer bits)
- (cond ((eq version 'mpeg1)
- (cond ((eq layer 'layerI)
- (nth bits (alist-get
- 'mpeg1-layerI
- emms-info-native--mp3-bit-rates)))
- ((eq layer 'layerII)
- (nth bits (alist-get
- 'mpeg1-layerII
- emms-info-native--mp3-bit-rates)))
- ((eq layer 'layerIII)
- (nth bits (alist-get
- 'mpeg1-layerIII
- emms-info-native--mp3-bit-rates)))))
- (t (cond ((eq layer 'layerI)
- (nth bits (alist-get
- 'mpeg2x-layerI
- emms-info-native--mp3-bit-rates)))
- (t (nth bits (alist-get
- 'mpeg2x-layerII-III
- emms-info-native--mp3-bit-rates)))))))
-
-(defun emms-info-native--get-samples-per-frame (version layer)
- (cond ((eq layer 'layerIII)
- (cond ((eq version 'mpeg1)
- (alist-get
- 'layerIII-mpeg1
- emms-info-native--mp3-samples-per-frame))
- (t (alist-get
- 'layerIII-mpeg2x
- emms-info-native--mp3-samples-per-frame))))
- ((eq layer 'layerII)
- (alist-get 'layerII
- emms-info-native--mp3-samples-per-frame))
- ((eq layer 'layerI)
- (alist-get 'layerI
- emms-info-native--mp3-samples-per-frame))))
-
-
-;;;; EMMS code
(defun emms-info-native (track)
"Set info fields for TRACK.
-Supports Ogg Vorbis/Opus, FLAC, and MP3 files."
+Supports Ogg Vorbis/Opus, FLAC, MP3 and SPC files."
(condition-case env
- (let* ((filename (emms-track-name track))
- (info-fields (emms-info-native--decode-info-fields filename)))
+ (let* ((filename
+ (emms-track-name track))
+ (info-fields
+ (emms-info-native--decode-info-fields filename)))
(dolist (field info-fields)
(let ((name (intern (concat "info-" (car field))))
(value (cdr field)))
- (when (stringp value) (setq value (string-trim-right value)))
+ (when (stringp value)
+ (setq value (string-trim-right value)))
(emms-track-set track name value))))
(error (message "emms-info-native error processing %s: %s"
(emms-track-name track) env))))
@@ -1348,13 +85,13 @@ info field and VALUE is the corresponding info value.
Both are
strings."
(let ((stream-type (emms-info-native--find-stream-type filename)))
(cond ((or (eq stream-type 'vorbis) (eq stream-type 'opus))
- (emms-info-native--decode-ogg-metadata filename stream-type))
+ (emms-info-ogg-decode-metadata filename stream-type))
((eq stream-type 'flac)
- (emms-info-native--decode-flac-metadata filename))
+ (emms-info-flac-decode-metadata filename))
((eq stream-type 'mp3)
- (emms-info-native--decode-id3v2 filename))
+ (emms-info-mp3-decode-metadata filename))
((eq stream-type 'spc)
- (emms-info-spc--decode-id666 filename))
+ (emms-info-spc-decode-id666 filename))
(t nil))))
(defun emms-info-native--find-stream-type (filename)
@@ -1362,7 +99,8 @@ strings."
This is a naive implementation that relies solely on filename
extension.
-Return one of symbols `vorbis', `opus', `flac', or `mp3'."
+Return one of `vorbis', `opus', `flac', `mp3' or `spc', or nil if
+the stream type cannot be deduced."
(let ((case-fold-search t))
(cond ((string-match ".ogg$" filename) 'vorbis)
((string-match ".opus$" filename) 'opus)
diff --git a/emms-info-ogg.el b/emms-info-ogg.el
new file mode 100644
index 0000000000..6f3ef68484
--- /dev/null
+++ b/emms-info-ogg.el
@@ -0,0 +1,326 @@
+;;; emms-info-ogg.el --- EMMS info functions for Ogg files -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2020-2023 Free Software Foundation, Inc.
+
+;; Author: Petteri Hintsanen <petterih@iki.fi>
+
+;; This file is part of EMMS.
+
+;; EMMS 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 3, or (at your option)
+;; any later version.
+
+;; EMMS 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.
+
+;; You should have received a copy of the GNU General Public License
+;; along with EMMS; see the file COPYING. If not, write to the Free
+;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+;; MA 02110-1301, USA.
+
+;;; Commentary:
+
+;; This file contains functions for extracting metadata from Ogg
+;; files, specifically from encapsulated Vorbis and Opus streams.
+;; Only elementary streams are supported.
+;;
+;; Ogg code is based on its programming documentation available at
+;; https://xiph.org/ogg/doc/.
+;;
+;; Vorbis code is based on xiph.org's Vorbis I specification,
+;; available at https://xiph.org/vorbis/doc/Vorbis_I_spec.html. See
+;; also emms-info-vorbis.el.
+;;
+;; Opus code is based on RFC 7845; see
+;; https://tools.ietf.org/html/rfc7845.html and emms-info-opus.el.
+
+;;; Code:
+
+(require 'bindat)
+(require 'emms)
+(require 'emms-info-opus)
+(require 'emms-info-vorbis)
+(eval-when-compile
+ (require 'cl-lib))
+
+(defconst emms-info-ogg--page-size 65307
+ "Maximum size for a single Ogg container page.")
+
+(defconst emms-info-ogg--max-peek-size (* 2048 1024)
+ "Maximum buffer size for metadata decoding.
+Functions in `emms-info-ogg' read certain amounts of data into a
+temporary buffer while decoding metadata. This variable controls
+the maximum size of that buffer: if more than
+`emms-info-ogg--max-peek-size' bytes are needed, an error is
+signaled.
+
+Technically metadata blocks can have almost arbitrary lengths,
+but in practice processing must be constrained to prevent memory
+exhaustion in case of garbled or malicious inputs.")
+
+(defconst emms-info-ogg--page-bindat-spec
+ '((capture-pattern vec 4)
+ (eval (unless (equal last emms-info-ogg--magic-pattern)
+ (error "Ogg framing mismatch: expected `%s', got `%s'"
+ emms-info-ogg--magic-pattern
+ last)))
+ (stream-structure-version u8)
+ (eval (unless (= last 0)
+ (error "Ogg version mismatch: expected 0, got %s" last)))
+ (header-type-flag u8)
+ (granule-position vec 8)
+ (stream-serial-number u32r)
+ (page-sequence-no u32r)
+ (page-checksum u32r)
+ (page-segments u8)
+ (segment-table vec (page-segments))
+ (payload vec (eval (seq-reduce #'+ last 0))))
+ "Ogg page structure specification.")
+
+(defconst emms-info-ogg--magic-pattern
+ (string-to-vector "OggS")
+ "Ogg format magic capture pattern.")
+
+(defconst emms-info-ogg--crc-table
+ [#x00000000 #x04C11DB7 #x09823B6E #x0D4326D9 #x130476DC
+ #x17C56B6B #x1A864DB2 #x1E475005 #x2608EDB8 #x22C9F00F
+ #x2F8AD6D6 #x2B4BCB61 #x350C9B64 #x31CD86D3 #x3C8EA00A
+ #x384FBDBD #x4C11DB70 #x48D0C6C7 #x4593E01E #x4152FDA9
+ #x5F15ADAC #x5BD4B01B #x569796C2 #x52568B75 #x6A1936C8
+ #x6ED82B7F #x639B0DA6 #x675A1011 #x791D4014 #x7DDC5DA3
+ #x709F7B7A #x745E66CD #x9823B6E0 #x9CE2AB57 #x91A18D8E
+ #x95609039 #x8B27C03C #x8FE6DD8B #x82A5FB52 #x8664E6E5
+ #xBE2B5B58 #xBAEA46EF #xB7A96036 #xB3687D81 #xAD2F2D84
+ #xA9EE3033 #xA4AD16EA #xA06C0B5D #xD4326D90 #xD0F37027
+ #xDDB056FE #xD9714B49 #xC7361B4C #xC3F706FB #xCEB42022
+ #xCA753D95 #xF23A8028 #xF6FB9D9F #xFBB8BB46 #xFF79A6F1
+ #xE13EF6F4 #xE5FFEB43 #xE8BCCD9A #xEC7DD02D #x34867077
+ #x30476DC0 #x3D044B19 #x39C556AE #x278206AB #x23431B1C
+ #x2E003DC5 #x2AC12072 #x128E9DCF #x164F8078 #x1B0CA6A1
+ #x1FCDBB16 #x018AEB13 #x054BF6A4 #x0808D07D #x0CC9CDCA
+ #x7897AB07 #x7C56B6B0 #x71159069 #x75D48DDE #x6B93DDDB
+ #x6F52C06C #x6211E6B5 #x66D0FB02 #x5E9F46BF #x5A5E5B08
+ #x571D7DD1 #x53DC6066 #x4D9B3063 #x495A2DD4 #x44190B0D
+ #x40D816BA #xACA5C697 #xA864DB20 #xA527FDF9 #xA1E6E04E
+ #xBFA1B04B #xBB60ADFC #xB6238B25 #xB2E29692 #x8AAD2B2F
+ #x8E6C3698 #x832F1041 #x87EE0DF6 #x99A95DF3 #x9D684044
+ #x902B669D #x94EA7B2A #xE0B41DE7 #xE4750050 #xE9362689
+ #xEDF73B3E #xF3B06B3B #xF771768C #xFA325055 #xFEF34DE2
+ #xC6BCF05F #xC27DEDE8 #xCF3ECB31 #xCBFFD686 #xD5B88683
+ #xD1799B34 #xDC3ABDED #xD8FBA05A #x690CE0EE #x6DCDFD59
+ #x608EDB80 #x644FC637 #x7A089632 #x7EC98B85 #x738AAD5C
+ #x774BB0EB #x4F040D56 #x4BC510E1 #x46863638 #x42472B8F
+ #x5C007B8A #x58C1663D #x558240E4 #x51435D53 #x251D3B9E
+ #x21DC2629 #x2C9F00F0 #x285E1D47 #x36194D42 #x32D850F5
+ #x3F9B762C #x3B5A6B9B #x0315D626 #x07D4CB91 #x0A97ED48
+ #x0E56F0FF #x1011A0FA #x14D0BD4D #x19939B94 #x1D528623
+ #xF12F560E #xF5EE4BB9 #xF8AD6D60 #xFC6C70D7 #xE22B20D2
+ #xE6EA3D65 #xEBA91BBC #xEF68060B #xD727BBB6 #xD3E6A601
+ #xDEA580D8 #xDA649D6F #xC423CD6A #xC0E2D0DD #xCDA1F604
+ #xC960EBB3 #xBD3E8D7E #xB9FF90C9 #xB4BCB610 #xB07DABA7
+ #xAE3AFBA2 #xAAFBE615 #xA7B8C0CC #xA379DD7B #x9B3660C6
+ #x9FF77D71 #x92B45BA8 #x9675461F #x8832161A #x8CF30BAD
+ #x81B02D74 #x857130C3 #x5D8A9099 #x594B8D2E #x5408ABF7
+ #x50C9B640 #x4E8EE645 #x4A4FFBF2 #x470CDD2B #x43CDC09C
+ #x7B827D21 #x7F436096 #x7200464F #x76C15BF8 #x68860BFD
+ #x6C47164A #x61043093 #x65C52D24 #x119B4BE9 #x155A565E
+ #x18197087 #x1CD86D30 #x029F3D35 #x065E2082 #x0B1D065B
+ #x0FDC1BEC #x3793A651 #x3352BBE6 #x3E119D3F #x3AD08088
+ #x2497D08D #x2056CD3A #x2D15EBE3 #x29D4F654 #xC5A92679
+ #xC1683BCE #xCC2B1D17 #xC8EA00A0 #xD6AD50A5 #xD26C4D12
+ #xDF2F6BCB #xDBEE767C #xE3A1CBC1 #xE760D676 #xEA23F0AF
+ #xEEE2ED18 #xF0A5BD1D #xF464A0AA #xF9278673 #xFDE69BC4
+ #x89B8FD09 #x8D79E0BE #x803AC667 #x84FBDBD0 #x9ABC8BD5
+ #x9E7D9662 #x933EB0BB #x97FFAD0C #xAFB010B1 #xAB710D06
+ #xA6322BDF #xA2F33668 #xBCB4666D #xB8757BDA #xB5365D03
+ #xB1F740B4]
+ "Lookup table for calculating Ogg checksums.")
+
+(defun emms-info-ogg-decode-metadata (filename stream-type)
+ "Read and decode metadata from Ogg file FILENAME.
+The file is assumed to contain a single stream of type
+STREAM-TYPE, which must either `vorbis' or `opus'.
+
+Return comments in a list of (FIELD . VALUE) cons cells.
+Additionally return stream duration in `playing-time' field.
+
+See `emms-info-vorbis--split-comment' for details."
+ (let* ((packets
+ (emms-info-ogg--read-and-decode-packets filename 2))
+ (headers
+ (emms-info-ogg--decode-headers packets stream-type))
+ (sample-rate
+ (bindat-get-field headers 'identification-header 'sample-rate))
+ (user-comments
+ (bindat-get-field headers 'comment-header 'user-comments))
+ (last-page
+ (emms-info-ogg--read-and-decode-last-page filename))
+ (granule-pos
+ (alist-get 'granule-position last-page))
+ (playing-time
+ (emms-info-ogg--decode-granule-pos granule-pos sample-rate))
+ (comments
+ (emms-info-vorbis-extract-comments user-comments)))
+ (nconc comments
+ (when playing-time
+ (list (cons "playing-time" playing-time))))))
+
+(defun emms-info-ogg--read-and-decode-packets (filename packets)
+ "Read and decode PACKETS packets from Ogg file FILENAME.
+Read in data from the start of FILENAME, remove Ogg packet
+frames, and concatenate payloads until at least PACKETS number of
+packets have been decoded. Return the decoded packets in a
+vector, concatenated.
+
+Data is read in `emms-info-ogg--page-size' chunks. If the total
+length of concatenated packets becomes greater than
+`emms-info-ogg--max-peek-size', an error is signaled.
+
+Only elementary streams are supported, that is, FILENAME should
+contain only a single logical stream. Note that this assumption
+is not verified: with non-elementary streams packets from
+different streams will be mixed together without an error."
+ (let ((num-packets 0) (offset 0) (stream (vector)))
+ (while (< num-packets packets)
+ (let ((page (emms-info-ogg--read-and-decode-page filename offset)))
+ (cl-incf num-packets (or (plist-get page :num-packets) 0))
+ (cl-incf offset (plist-get page :num-bytes))
+ (setq stream (vconcat stream (plist-get page :stream)))
+ (when (> (length stream) emms-info-ogg--max-peek-size)
+ (error "Ogg payload is too large"))))
+ stream))
+
+(defun emms-info-ogg--read-and-decode-page (filename offset)
+ "Read and decode a single Ogg page from FILENAME.
+Starting reading data from byte offset OFFSET.
+
+Return the plist from `emms-info-ogg--decode-page'."
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally
+ filename nil offset (+ offset emms-info-ogg--page-size))
+ (emms-info-ogg--decode-page (buffer-string))))
+
+(defun emms-info-ogg--decode-page (bytes)
+ "Decode a single Ogg page from a sequence of BYTES.
+Return a plist (:num-packets N :num-bytes B :stream S), where N
+is the number of packets in the page, B is the size of the page
+in bytes, and S is the unframed logical bitstream in a vector.
+Note that N can be zero."
+ (let* ((page
+ (bindat-unpack emms-info-ogg--page-bindat-spec bytes))
+ (num-packets
+ (emms-info-ogg--num-packets page))
+ (num-bytes
+ (bindat-length emms-info-ogg--page-bindat-spec page))
+ (stream
+ (bindat-get-field page 'payload)))
+ (list :num-packets num-packets
+ :num-bytes num-bytes
+ :stream stream)))
+
+(defun emms-info-ogg--num-packets (page)
+ "Return the number of packets in Ogg page PAGE.
+PAGE must correspond to `emms-info-ogg--page-bindat-spec'."
+ ;; Every element that is less than 255 in the segment table
+ ;; represents a packet boundary.
+ (length (seq-filter (lambda (elt) (< elt 255))
+ (bindat-get-field page 'segment-table))))
+
+(defun emms-info-ogg--decode-headers (packets stream-type)
+ "Decode first two stream headers from PACKETS for STREAM-TYPE.
+STREAM-TYPE must be either `vorbis' or `opus'.
+
+Return a structure that corresponds to either
+`emms-info-opus--headers-bindat-spec' or
+`emms-info-vorbis--headers-bindat-spec'."
+ (cond ((eq stream-type 'vorbis)
+ (bindat-unpack emms-info-vorbis--headers-bindat-spec
+ packets))
+ ((eq stream-type 'opus)
+ (let (emms-info-opus--channel-count)
+ (bindat-unpack emms-info-opus--headers-bindat-spec
+ packets)))
+ (t (error "Unknown stream type %s" stream-type))))
+
+(defun emms-info-ogg--read-and-decode-last-page (filename)
+ "Read and decode the last Ogg page from FILENAME.
+Return the page in bindat type structure."
+ (with-temp-buffer
+ (let* ((length (file-attribute-size (file-attributes filename)))
+ (begin (max 0 (- length emms-info-ogg--page-size))))
+ (set-buffer-multibyte nil)
+ (insert-file-contents-literally filename nil begin length)
+ (emms-info-ogg--decode-last-page))))
+
+(defun emms-info-ogg--decode-last-page ()
+ "Find and return the last valid Ogg page from the current buffer.
+Ensure page synchronization by verifying page checksum.
+
+Return the page in bindat type structure. If there is no valid
+Ogg page in the buffer, return nil."
+ (let (page)
+ (goto-char (point-max))
+ (while (and (not page)
+ (search-backward
+ (concat emms-info-ogg--magic-pattern) nil t))
+ (setq page (emms-info-ogg--verify-page)))
+ (when (and page
+ (> (logand (alist-get 'header-type-flag page) #x04) 0))
+ page)))
+
+(defun emms-info-ogg--verify-page ()
+ "Verify Ogg page starting from point.
+Unpack page into `emms-info-ogg--page-bindat-spec' structure and
+calculate its checksum. Return the page if the checksum is
+correct, or nil if the checksum does not match or the page is
+otherwise invalid."
+ (ignore-errors
+ (let* ((offset (point))
+ (page
+ (bindat-unpack emms-info-ogg--page-bindat-spec
+ (buffer-string)
+ (1- offset)))
+ (num-bytes
+ (bindat-length emms-info-ogg--page-bindat-spec page))
+ (buf
+ (buffer-substring-no-properties offset
+ (+ offset num-bytes)))
+ (checksum
+ (emms-info-ogg--checksum (concat (substring buf 0 22)
+ [0 0 0 0]
+ (substring buf 26)))))
+ (when (= (alist-get 'page-checksum page) checksum) page))))
+
+(defun emms-info-ogg--checksum (bytes)
+ "Calculate and return Ogg checksum for BYTES.
+See URL `https://xiph.org/vorbis/doc/framing.html' for details on
+checksum."
+ (let ((crc 0))
+ (dotimes (n (length bytes))
+ (setq crc (logxor (logand (ash crc 8) #xffffffff)
+ (aref emms-info-ogg--crc-table
+ (logxor (ash crc -24)
+ (aref bytes n))))))
+ crc))
+
+(defun emms-info-ogg--decode-granule-pos (vec rate)
+ "Decode Ogg granule position vector VEC for sampling rate RATE.
+Granule position is 64-bit little-endian signed integer counting
+the number of passed PCM samples per channel at the end of a
+packet. For a partial packet granule position is -1 and hence
+invalid.
+
+Return the granule position in seconds, or nil if VEC is nil or
+from a partial packet."
+ (when vec
+ (let* ((int (emms-le-to-int vec))
+ (pos (emms-from-twos-complement int 64)))
+ (unless (= pos -1) (/ pos rate)))))
+
+(provide 'emms-info-ogg)
+
+;;; emms-info-ogg.el ends here
diff --git a/emms-info-opus.el b/emms-info-opus.el
new file mode 100644
index 0000000000..27012b6b9b
--- /dev/null
+++ b/emms-info-opus.el
@@ -0,0 +1,98 @@
+;;; emms-info-opus.el --- EMMS Opus info support -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2020-2023 Free Software Foundation, Inc.
+
+;; Author: Petteri Hintsanen <petterih@iki.fi>
+
+;; This file is part of EMMS.
+
+;; EMMS 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 3, or (at your option)
+;; any later version.
+
+;; EMMS 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.
+
+;; You should have received a copy of the GNU General Public License
+;; along with EMMS; see the file COPYING. If not, write to the Free
+;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+;; MA 02110-1301, USA.
+
+;;; Commentary:
+
+;; This file contains Opus-specific code for `emms-info-ogg' feature.
+
+(require 'emms-info-vorbis)
+
+(defvar emms-info-opus--channel-count 0
+ "Last decoded Opus channel count.
+This is a kludge; it is needed because bindat spec cannot refer
+outside itself.")
+
+(defconst emms-info-opus--headers-bindat-spec
+ '((identification-header struct emms-info-opus--id-header-bindat-spec)
+ (comment-header struct emms-info-opus--comment-header-bindat-spec))
+ "Specification for two first Opus header packets.
+They are always an identification header followed by a comment
+header.")
+
+(defconst emms-info-opus--id-header-bindat-spec
+ '((opus-head vec 8)
+ (eval (unless (equal last emms-info-opus--id-magic-pattern)
+ (error "Opus framing mismatch: expected `%s', got `%s'"
+ emms-info-opus--id-magic-pattern
+ last)))
+ (opus-version u8)
+ (eval (unless (< last 16)
+ (error "Opus version mismatch: expected < 16, got %s"
+ last)))
+ (channel-count u8)
+ (eval (setq emms-info-opus--channel-count last))
+ (pre-skip u16r)
+ (sample-rate u32r)
+ (output-gain u16r)
+ (channel-mapping-family u8)
+ (union (channel-mapping-family)
+ (0 nil)
+ (t (struct emms-info-opus--channel-mapping-bindat-spec))))
+ "Opus identification header specification.")
+
+(defconst emms-info-opus--id-magic-pattern
+ (string-to-vector "OpusHead")
+ "Opus identification header magic pattern.")
+
+(defconst emms-info-opus--channel-mapping-bindat-spec
+ '((stream-count u8)
+ (coupled-count u8)
+ (channel-mapping vec (eval emms-info-opus--channel-count)))
+ "Opus channel mapping table specification.")
+
+(defconst emms-info-opus--comment-header-bindat-spec
+ '((opus-tags vec 8)
+ (eval (unless (equal last emms-info-opus--tags-magic-pattern)
+ (error "Opus framing mismatch: expected `%s', got `%s'"
+ emms-info-opus--tags-magic-pattern
+ last)))
+ (vendor-length u32r)
+ (eval (when (> last emms-info-vorbis--max-vendor-length)
+ (error "Opus vendor length %s is too long" last)))
+ (vendor-string vec (vendor-length))
+ (user-comments-list-length u32r)
+ (eval (when (> last emms-info-vorbis--max-comments)
+ (error "Opus user comment list length %s is too long"
+ last)))
+ (user-comments repeat
+ (user-comments-list-length)
+ (struct emms-info-vorbis--comment-field-bindat-spec)))
+ "Opus comment header specification.")
+
+(defconst emms-info-opus--tags-magic-pattern
+ (string-to-vector "OpusTags")
+ "Opus comment header magic pattern.")
+
+(provide 'emms-info-opus)
+
+;;; emms-info-opus.el ends here
diff --git a/emms-info-spc.el b/emms-info-spc.el
index fd092a52d0..db29488735 100644
--- a/emms-info-spc.el
+++ b/emms-info-spc.el
@@ -75,7 +75,7 @@ Sources:
(bindat-unpack emms-info-spc--id666-header-bindat-spec
(buffer-string))))
-(defun emms-info-spc--decode-id666 (filename)
+(defun emms-info-spc-decode-id666 (filename)
"Read and decode id666 metadata from FILENAME.
Return metadata in a list of (FIELD . VALUE) cons cells, or nil
in case of errors or if there were no known fields in FILENAME."
diff --git a/emms-info-vorbis.el b/emms-info-vorbis.el
new file mode 100644
index 0000000000..8b64b929c0
--- /dev/null
+++ b/emms-info-vorbis.el
@@ -0,0 +1,189 @@
+;;; emms-info-vorbis.el --- EMMS Vorbis info support -*- lexical-binding: t;
-*-
+
+;; Copyright (C) 2020-2023 Free Software Foundation, Inc.
+
+;; Author: Petteri Hintsanen <petterih@iki.fi>
+
+;; This file is part of EMMS.
+
+;; EMMS 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 3, or (at your option)
+;; any later version.
+
+;; EMMS 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.
+
+;; You should have received a copy of the GNU General Public License
+;; along with EMMS; see the file COPYING. If not, write to the Free
+;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+;; MA 02110-1301, USA.
+
+;;; Commentary:
+
+;; This file contains Vorbis-specific code for `emms-info-ogg'.
+
+;;; Code:
+
+(defconst emms-info-vorbis--max-comments 1024
+ "Maximum number of Vorbis comment fields in a stream.
+Technically a single Vorbis stream may have up to 2^32 comments,
+but in practice processing must be constrained to prevent memory
+exhaustion in case of garbled or malicious inputs.
+
+This limit is used with Opus and FLAC streams as well, since
+their comments have almost the same format as Vorbis.")
+
+(defconst emms-info-vorbis--max-comment-size (* 64 1024)
+ "Maximum length for a single Vorbis comment field.
+Technically a single Vorbis comment may have a length up to 2^32
+bytes, but in practice processing must be constrained to prevent
+memory exhaustion in case of garbled or malicious inputs.
+
+This limit is used with Opus and FLAC streams as well, since
+their comments have almost the same format as Vorbis.")
+
+(defconst emms-info-vorbis--max-vendor-length 1024
+ "Maximum length of Vorbis vendor string.
+Technically a vendor string can be up to 2^32 bytes long, but in
+practice processing must be constrained to prevent memory
+exhaustion in case of garbled or malicious inputs.
+
+This limit is used with Opus and FLAC streams as well, since
+their comments have almost the same format as Vorbis.")
+
+(defconst emms-info-vorbis--accepted-fields
+ '("album"
+ "albumartist"
+ "albumartistsort"
+ "albumsort"
+ "artist"
+ "artistsort"
+ "composer"
+ "composersort"
+ "date"
+ "discnumber"
+ "genre"
+ "label"
+ "originaldate"
+ "originalyear"
+ "performer"
+ "title"
+ "titlesort"
+ "tracknumber"
+ "year")
+ "EMMS info fields that are extracted from Vorbis comments.")
+
+(defconst emms-info-vorbis--headers-bindat-spec
+ '((identification-header struct emms-info-vorbis--id-header-bindat-spec)
+ (comment-header struct emms-info-vorbis--comment-header-bindat-spec))
+ "Specification for first two Vorbis header packets.
+They are always an identification header followed by a comment
+header.")
+
+(defconst emms-info-vorbis--id-header-bindat-spec
+ '((packet-type u8)
+ (eval (unless (= last 1)
+ (error "Vorbis header type mismatch: expected 1, got %s"
+ last)))
+ (vorbis vec 6)
+ (eval (unless (equal last emms-info-vorbis--header-magic-pattern)
+ (error "Vorbis framing mismatch: expected `%s', got `%s'"
+ emms-info-vorbis--header-magic-pattern
+ last)))
+ (vorbis-version u32r)
+ (eval (unless (= last 0)
+ (error "Vorbis version mismatch: expected 0, got %s"
+ last)))
+ (channel-count u8)
+ (sample-rate u32r)
+ (bitrate-maximum u32r)
+ (bitrate-nominal u32r)
+ (bitrate-minimum u32r)
+ (blocksize u8)
+ (framing-flag u8)
+ (eval (unless (= last 1))
+ (error "Vorbis framing bit mismatch: expected 1, got %s"
+ last)))
+ "Vorbis identification header specification.")
+
+(defconst emms-info-vorbis--header-magic-pattern
+ (string-to-vector "vorbis")
+ "Header packet magic pattern.")
+
+(defconst emms-info-vorbis--comment-header-bindat-spec
+ '((packet-type u8)
+ (eval (unless (= last 3)
+ (error "Vorbis header type mismatch: expected 3, got %s"
+ last)))
+ (vorbis vec 6)
+ (eval (unless (equal last emms-info-vorbis--header-magic-pattern)
+ (error "Vorbis framing mismatch: expected `%s', got `%s'"
+ emms-info-vorbis--header-magic-pattern
+ last)))
+ (vendor-length u32r)
+ (eval (when (> last emms-info-vorbis--max-vendor-length)
+ (error "Vorbis vendor length %s is too long" last)))
+ (vendor-string vec (vendor-length))
+ (user-comments-list-length u32r)
+ (eval (when (> last emms-info-vorbis--max-comments)
+ (error "Vorbis user comment list length %s is too long"
+ last)))
+ (user-comments repeat
+ (user-comments-list-length)
+ (struct emms-info-vorbis--comment-field-bindat-spec))
+ (framing-bit u8)
+ (eval (unless (= last 1))
+ (error "Vorbis framing bit mismatch: expected 1, got %s"
+ last)))
+ "Vorbis comment header specification.")
+
+(defconst emms-info-vorbis--comment-field-bindat-spec
+ '((length u32r)
+ (eval (when (> last emms-info-vorbis--max-comment-size)
+ (error "Vorbis comment length %s is too long" last)))
+ (user-comment vec (length)))
+ "Vorbis comment field specification.")
+
+(defun emms-info-vorbis-extract-comments (user-comments)
+ "Return a decoded list of comments from USER-COMMENTS.
+USER-COMMENTS should be a list of Vorbis comments according to
+`user-comments' field in
+`emms-info-vorbis--comment-header-bindat-spec'.
+
+Return comments in a list of (FIELD . VALUE) cons cells. Only
+FIELDs that are listed in `emms-info-vorbis--accepted-fields' are
+returned."
+ (let (comments)
+ (dolist (user-comment user-comments)
+ (let* ((comment (alist-get 'user-comment user-comment))
+ (pair (emms-info-vorbis--split-comment comment)))
+ (push pair comments)))
+ (seq-filter (lambda (elt)
+ (member (car elt)
+ emms-info-vorbis--accepted-fields))
+ comments)))
+
+(defun emms-info-vorbis--split-comment (comment)
+ "Split Vorbis COMMENT to a field-value pair.
+Vorbis comments are of form `FIELD=VALUE'. FIELD is a
+case-insensitive field name with a restricted set of ASCII
+characters. VALUE is an arbitrary UTF-8 encoded octet stream.
+Comments with empty FIELD or VALUE are ignored.
+
+Return a cons cell (FIELD . VALUE), where FIELD is converted to
+lower case and VALUE is the decoded value."
+ (let ((comment-string
+ (decode-coding-string (mapconcat #'byte-to-string
+ comment
+ nil)
+ 'utf-8)))
+ (when (string-match "^\\(.+?\\)=\\(.+\\)$" comment-string)
+ (cons (downcase (match-string 1 comment-string))
+ (match-string 2 comment-string)))))
+
+(provide 'emms-info-vorbis)
+
+;;; emms-info-vorbis.el ends here
diff --git a/emms.el b/emms.el
index 398ee9d2fe..67e1f4dc1c 100644
--- a/emms.el
+++ b/emms.el
@@ -1342,6 +1342,42 @@ If POS does not contain PROP, try to find PROP just
before POS."
(setq i (- i 1))))
vector)
+(defun emms-le-to-int (vec)
+ "Convert bytes in VEC to an integer.
+Bytes are assumed to be in little-endian order, that is, the
+least significant byte first.
+
+If VEC is nil, return zero."
+ (apply '+ (seq-map-indexed (lambda (elt idx)
+ (* (expt 2 (* 8 idx)) elt))
+ vec)))
+
+(defun emms-be-to-int (vec)
+ "Convert bytes in VEC to an integer.
+Bytes are assumed to be in big-endian order, that is, the most
+significant byte first.
+
+If VEC is nil, return zero."
+ (emms-le-to-int (reverse vec)))
+
+(defun emms-from-twos-complement (num bits)
+ "Convert integer NUM from two's complement with BITS bits."
+ (let ((signmask (ash 1 (1- bits))))
+ (if (= (logand num signmask) signmask)
+ ;; negative
+ (* -1 (1+ (logand (lognot num) (1- signmask))))
+ ;; positive
+ num)))
+
+(defun emms-extract-bits (int from &optional to)
+ "Extract consequent set bits FROM[..TO] from INT.
+The first (least significant, rightmost) bit is zero. Return the
+integer value of bits as if they would have been shifted to right
+by FROM positions."
+ (unless to (setq to from))
+ (let ((num-bits (1+ (- to from)))
+ (mask (1- (expt 2 (1+ to)))))
+ (when (> num-bits 0) (ash (logand int mask) (- from)))))
;;; ------------------------------------------------------------------
;;; Sources
diff --git a/test/emms-info-flac-tests.el b/test/emms-info-flac-tests.el
new file mode 100644
index 0000000000..3b40f1d1fb
--- /dev/null
+++ b/test/emms-info-flac-tests.el
@@ -0,0 +1,58 @@
+;;; emms-info-flac-tests.el --- Test suite for emms-info-flac -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; Author: Petteri Hintsanen <petterih@iki.fi>
+
+;; This file is part of EMMS.
+
+;; EMMS 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 3, or (at your option)
+;; any later version.
+
+;; EMMS 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.
+
+;; You should have received a copy of the GNU General Public License
+;; along with EMMS; see the file COPYING. If not, write to the Free
+;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+;; MA 02110-1301, USA.
+
+;;; Code:
+
+(require 'emms-info-flac)
+(require 'ert)
+
+(defmacro emms-flac-test-make-data-func (name bytes)
+ "Macro for defining test data generator.
+This macro defines a suitable function with NAME that outputs
+BYTES after FLAC signature. The function NAME can then be passed
+for `emms-info-flac--decode-meta-blocks'."
+ `(defun ,name (offset end)
+ (let ((bytes (seq-concatenate 'vector [102 76 97 67] ,bytes)))
+ (erase-buffer)
+ (mapcar #'insert (seq-subseq bytes offset end)))))
+
+(emms-flac-test-make-data-func emms-test-invalid-flac-block-length [1 200 200
200 0 1 2 3])
+(emms-flac-test-make-data-func emms-test-invalid-flac-block-type [9 0 0 0 0 1
2 3])
+(emms-flac-test-make-data-func emms-test-valid-flac-block [0 0 0 8 10 11 12 13
14 15 16 17 132 0 0 4 1 2 3 4])
+
+(ert-deftest emms-flac-test-meta-blocks ()
+ (should-error (emms-info-flac--decode-meta-blocks
+ #'emms-test-invalid-flac-block-length))
+ (should-error (emms-info-flac--decode-meta-blocks
+ #'emms-test-invalid-flac-block-type))
+ (should (equal (emms-info-flac--decode-meta-blocks
+ #'emms-test-valid-flac-block)
+ '([1 2 3 4] [10 11 12 13 14 15 16 17]))))
+
+(ert-deftest emms-flac-test-decode-duration ()
+ ;; The corresponding sample metadata bytes are [10 196 66 240 1 8 36 0].
+ (should (= (emms-info-flac--decode-duration 775818634391462912) 392)))
+
+(provide 'emms-info-flac-tests)
+
+;;; emms-info-flac-tests.el ends here
diff --git a/test/emms-info-mp3-tests.el b/test/emms-info-mp3-tests.el
new file mode 100644
index 0000000000..af5cf54b9a
--- /dev/null
+++ b/test/emms-info-mp3-tests.el
@@ -0,0 +1,104 @@
+;;; emms-info-mp3-tests.el --- Test suite for emms-info-mp3 -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; Author: Petteri Hintsanen <petterih@iki.fi>
+
+;; This file is part of EMMS.
+
+;; EMMS 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 3, or (at your option)
+;; any later version.
+
+;; EMMS 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.
+
+;; You should have received a copy of the GNU General Public License
+;; along with EMMS; see the file COPYING. If not, write to the Free
+;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+;; MA 02110-1301, USA.
+
+;;; Code:
+
+(require 'emms-info-mp3)
+(require 'ert)
+
+(ert-deftest emms-id3v2-test-valid-frame-id-p ()
+ (let ((emms-info-id3v2--version 2))
+ (should (emms-info-id3v2--valid-frame-id-p "A1B"))
+ (should (not (emms-info-id3v2--valid-frame-id-p "~B1")))
+ (should (not (emms-info-id3v2--valid-frame-id-p "XX")))
+ (should (not (emms-info-id3v2--valid-frame-id-p "XXXX"))))
+ (let ((emms-info-id3v2--version 3))
+ (should (emms-info-id3v2--valid-frame-id-p "ABC9"))
+ (should (not (emms-info-id3v2--valid-frame-id-p "~BCD")))
+ (should (not (emms-info-id3v2--valid-frame-id-p "XXX")))
+ (should (not (emms-info-id3v2--valid-frame-id-p "XXXXX")))))
+
+(ert-deftest emms-id3v2-test-checked-size ()
+ (should (= (emms-info-id3v2--checked-size 'tag [0 0 2 1]) 257))
+ (should (= (emms-info-id3v2--checked-size 'tag [1 1 1 1]) 2113665))
+ (should (= (emms-info-id3v2--checked-size 'tag [#xff #xff #xff #xff])
+ (1- (* 256 1024 1024))))
+ (should (= (emms-info-id3v2--checked-size 'tag [#x7f #x7f #x7f #x7f])
+ (1- (* 256 1024 1024))))
+ (should (= (emms-info-id3v2--checked-size 'tag [#x12 #x34 #x56 #x78])
+ 38611832))
+ (let ((emms-info-id3v2--version 4))
+ (should (= (emms-info-id3v2--checked-size 'frame [#xff #xff #xff #xff])
+ (1- (* 256 1024 1024)))))
+ (let ((emms-info-id3v2--version 3))
+ (should (= (emms-info-id3v2--checked-size 'frame [#xff #xff #xff #xff])
+ (1- (* 4 1024 1024 1024))))))
+
+(ert-deftest emms-id3v2-test-decode-size ()
+ (should (= (emms-info-id3v2--decode-size [01 01 01 01] nil)
+ 16843009))
+ (should (= (emms-info-id3v2--decode-size [01 01 01 01] t)
+ 2113665))
+ (should (= (emms-info-id3v2--decode-size [00 00 02 01] nil)
+ 513))
+ (should (= (emms-info-id3v2--decode-size [00 00 02 01] t)
+ 257)))
+
+(ert-deftest emms-mp3-test-find-and-decode-frame-header ()
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert (concat [#x00 #x00 #x00 #x00 #x00 #x00 #x00 #xff #xfb #xb0 #x04
#x00 #x00 #x00 #x00 #x00 #x69 #x06 #x00 #x00 #x00 #x00 #x00 #x0d #x20 #xc0 #x00
#x00 #x00 #x00 #x01 #xa4 #x1c #x00 #x00 #x00 #x00 #x00 #x34 #x83 #x80 #x00 #x00
#x4c #x41 #x4d #x45 #x33 #x2e #x39 #x31 #x55 #x55]))
+ (should (equal (emms-info-mp3--find-and-decode-frame-header)
+ '((version . mpeg1)
+ (layer . layerIII)
+ (crc . 1)
+ (bit-rate . 192)
+ (sample-rate . 44100)
+ (samples-per-frame . 1152)
+ (padding . 0)
+ (private . 0)
+ (channel-mode . stereo)
+ (mode-extension . 0)
+ (copyright . 0)
+ (emphasis . 0)
+ (original . 1))))))
+
+(ert-deftest emms-mp3-test-find-and-decode-xing-header ()
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert (concat [255 234 144 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 88 105 110 103 0 0 0 15 0 0 33 89 0 80 29 121 0 3 6 8 11 14
15 18 21 23 26 29 31 34 37 39 42 45 47 50 53 55 57 60 62 65 68 70 73 76 78 81
84 86 89 92 94 97 100 101 104 107 109 112 115 117 120 122 123 125 127 130 133
135 138 141 143 146 149 151 154 157 159 162 165 167 170 173 174 177 180 182 185
188 190 193 196 198 201 204 206 209 212 214 217 220 222 225 228 230 233 236 238
241 244 246 249 2 [...]
+ (should (= (emms-info-mp3--find-and-decode-xing-header) 8537))))
+
+(ert-deftest emms-mp3-test-find-decode-xing-header-2 ()
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert (concat [255 251 80 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 73 110 102 111 0 0 0 15 0 0 35 216 0 58 134 9 0 2 5 8 10 13
16 18 20 23 25 28 31 33 36 38 41 43 46 49 51 54 56 58 61 64 66 69 72 74 76 79
82 84 87 90 91 94 97 99 102 105 108 109 112 115 117 120 123 125 128 130 133 135
138 141 143 146 148 150 153 156 158 161 164 166 168 171 174 176 179 182 183 186
189 191 194 197 199 201 204 207 209 212 215 217 219 222 224 227 230 233 235 237
240 242 245 248 [...]
+ (should (= (emms-info-mp3--find-and-decode-xing-header) 9176))))
+
+(ert-deftest emms-mp3-test-find-and-decode-vbri-header ()
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert (concat [255 251 161 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 86 66 82 73 0 1 13 177 0 100 0 98 219 145 0 0 33 58 0 132 0
1 0 2 0 64 152 177 189 168 187 54 186 206 187 55 186 207 186 103 187 55 188 215
187 159 186 207 185 44 187 53 187 56 188 8 187 159 185 149 190 224 188 8 185
250 186 99 184 90 182]))
+ (should (= (emms-info-mp3--find-and-decode-vbri-header) 8506))))
+
+;;; emms-info-mp3-tests.el ends here
diff --git a/test/emms-info-native-tests.el b/test/emms-info-native-tests.el
deleted file mode 100644
index cc343ecd1c..0000000000
--- a/test/emms-info-native-tests.el
+++ /dev/null
@@ -1,272 +0,0 @@
-;; -*- lexical-binding: t; -*-
-
-(require 'emms-info-native)
-(require 'ert)
-
-(ert-deftest emms-test-extract-vorbis-comments ()
- (let ((comments '(((user-comment . [77 85 83 73 67 66 82 65 73 78 90 95 82
69 76 69 65 83 69 71 82 79 85 80 73 68 61 57 98 51 48 55 50 57 51 45 100 50 101
54 45 51 52 97 57 45 97 50 56 57 45 49 54 49 99 53 98 97 102 49 56 55 102])
(length . 63)) ;musicbrainz_releasegroupid
- ((user-comment . [79 82 73 71 73 78 65 76 68 65 84 69 61
49 57 57 55 45 48 51 45 51 49]) (length . 23)) ;originaldate
- ((user-comment . [79 82 73 71 73 78 65 76 89 69 65 82 61
49 57 57 55]) (length . 17)) ;originalyear
- ((user-comment . [82 69 76 69 65 83 69 84 89 80 69 61 97
108 98 117 109]) (length . 17)) ;releasetype
- ((user-comment . [66 65 82 67 79 68 69 61 55 54 57 50 51
51 48 48 52 55 50 55]) (length . 20)) ;barcode
- ((user-comment . [65 76 66 85 77 61 65 32 116 111 100 97
32 67 117 98 97 32 108 101 32 103 117 115 116 97]) (length . 26))))) ;album
- (should (equal (emms-info-native--extract-vorbis-comments comments)
- (quote (("album" . "A toda Cuba le gusta")
- ("originalyear" . "1997")
- ("originaldate" . "1997-03-31")))))))
-
-(ert-deftest emms-test-split-vorbis-comment ()
- (let ((utf-8-comment [75 101 121 61 206 159 225 189 144 207 135 225 189 182
32 206 164 206 177 225 189 144 207 132 225 189 176 10 ])) ;Key=Οὐχὶ Ταὐτὰ
- (should (equal (emms-info-native--split-vorbis-comment "") nil))
- (should (equal (emms-info-native--split-vorbis-comment "x") nil))
- (should (equal (emms-info-native--split-vorbis-comment "x=") nil))
- (should (equal (emms-info-native--split-vorbis-comment "=x") nil))
- (should (equal (emms-info-native--split-vorbis-comment "a=B")
- (cons "a" "B")))
- (should (equal (emms-info-native--split-vorbis-comment "abc=ABC=123")
- (cons "abc" "ABC=123")))
- (should (equal (emms-info-native--split-vorbis-comment utf-8-comment)
- (cons "key" "Οὐχὶ Ταὐτὰ")))))
-
-(ert-deftest emms-test-decode-ogg-page ()
- (let ((bytes [79 103 103 83 0 2 0 0 0 0 0 0 0 0 134 209 158 23 0 0 0 0 53 82
251 136 1 30 1 118 111 114 98 105 115 0 0 0 0 1 68 172 0 0 0 0 0 0 128 56 1 0 0
0 0 0 184 1]))
- (should (equal (emms-info-native--decode-ogg-page bytes)
- (list :num-packets 1
- :num-bytes 58
- :stream [1 118 111 114 98 105 115 0 0 0 0 1 68 172 0
0 0 0 0 0 128 56 1 0 0 0 0 0 184 1])))))
-
-(ert-deftest emms-test-decode-ogg-vorbis-headers ()
- (let ((bytes [1 118 111 114 98 105 115 0 0 0 0 1 68 172 0 0 0 0 0 0 128 56 1
0 0 0 0 0 184 1 3 118 111 114 98 105 115 52 0 0 0 88 105 112 104 46 79 114 103
32 108 105 98 86 111 114 98 105 115 32 73 32 50 48 50 48 48 55 48 52 32 40 82
101 100 117 99 105 110 103 32 69 110 118 105 114 111 110 109 101 110 116 41 2 0
0 0 7 0 0 0 102 111 111 61 98 97 114 27 0 0 0 75 101 121 61 206 159 225 189 144
207 135 225 189 182 32 206 164 206 177 225 189 144 207 132 225 189 176 1]))
- (should (equal (emms-info-native--decode-ogg-headers bytes 'vorbis)
- '((comment-header
- (framing-bit . 1)
- (user-comments
- ((user-comment . [102 111 111 61 98 97 114])
- (length . 7))
- ((user-comment . [75 101 121 61 206 159 225 189 144 207
135 225 189 182 32 206 164 206 177 225 189 144 207 132 225 189 176])
- (length . 27)))
- (user-comments-list-length . 2)
- (vendor-string . [88 105 112 104 46 79 114 103 32 108
105 98 86 111 114 98 105 115 32 73 32 50 48 50 48 48 55 48 52 32 40 82 101 100
117 99 105 110 103 32 69 110 118 105 114 111 110 109 101 110 116 41])
- (vendor-length . 52)
- (vorbis . [118 111 114 98 105 115])
- (packet-type . 3))
- (identification-header
- (framing-flag . 1)
- (blocksize . 184)
- (bitrate-minimum . 0)
- (bitrate-nominal . 80000)
- (bitrate-maximum . 0)
- (sample-rate . 44100)
- (channel-count . 1)
- (vorbis-version . 0)
- (vorbis . [118 111 114 98 105 115])
- (packet-type . 1)))))))
-
-(ert-deftest emms-test-decode-ogg-opus-headers ()
- (let ((bytes [79 112 117 115 72 101 97 100 1 1 56 1 68 172 0 0 0 0 0 79 112
117 115 84 97 103 115 13 0 0 0 108 105 98 111 112 117 115 32 49 46 51 46 49 3 0
0 0 38 0 0 0 69 78 67 79 68 69 82 61 111 112 117 115 101 110 99 32 102 114 111
109 32 111 112 117 115 45 116 111 111 108 115 32 48 46 49 46 49 48 7 0 0 0 102
111 111 61 98 97 114 27 0 0 0 75 101 121 61 206 159 225 189 144 207 135 225 189
182 32 206 164 206 177 225 189 144 207 132 225 189 176]))
- (should (equal (emms-info-native--decode-ogg-headers bytes 'opus)
- '((comment-header
- (user-comments
- ((user-comment . [69 78 67 79 68 69 82 61 111 112 117
115 101 110 99 32 102 114 111 109 32 111 112 117 115 45 116 111 111 108 115 32
48 46 49 46 49 48])
- (length . 38))
- ((user-comment . [102 111 111 61 98 97 114])
- (length . 7))
- ((user-comment . [75 101 121 61 206 159 225 189 144 207
135 225 189 182 32 206 164 206 177 225 189 144 207 132 225 189 176])
- (length . 27)))
- (user-comments-list-length . 3)
- (vendor-string . [108 105 98 111 112 117 115 32 49 46 51
46 49])
- (vendor-length . 13)
- (opus-tags . [79 112 117 115 84 97 103 115]))
- (identification-header
- (channel-mapping-family . 0)
- (output-gain . 0)
- (sample-rate . 44100)
- (pre-skip . 312)
- (channel-count . 1)
- (opus-version . 1)
- (opus-head . [79 112 117 115 72 101 97 100])))))))
-
-(defun emms-test--decode-last-ogg-page (bytes)
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (insert (concat bytes))
- (emms-info-native--decode-last-ogg-page)))
-
-(ert-deftest emms-test-decode-last-ogg-page()
- (let ((valid [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x04 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x19 #x4e
#xac #xa3 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36])
- (notlast [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x00 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x19 #x4e
#xac #xa3 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36])
- (invalid [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x04 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x01 #x02
#x03 #x04 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36]))
- (should (equal (emms-test--decode-last-ogg-page valid)
- '((payload . [79 103 103 83 49 50 51 52 53 54])
- (segment-table . [10])
- (page-segments . 1)
- (page-checksum . 2745978393)
- (page-sequence-no . 2112)
- (stream-serial-number . 1198406044)
- (granule-position . [0 36 8 1 0 0 0 0])
- (header-type-flag . 4)
- (stream-structure-version . 0)
- (capture-pattern . [79 103 103 83]))))
- (should (equal (emms-test--decode-last-ogg-page notlast) nil))
- (should (equal (emms-test--decode-last-ogg-page invalid) nil))))
-
-(ert-deftest emms-test-calculate-ogg-checksum ()
- (let ((bytes [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x04 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x19 #x4e
#xac #xa3 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36]))
- (should (equal (emms-info-native--calculate-ogg-checksum bytes)
445885580))))
-
-(ert-deftest emms-test-decode-ogg-granule-position ()
- (should (equal (emms-info-native--decode-ogg-granule-position [0 36 8 1 0 0
0 0] 44100)
- 392))
- (should (equal (emms-info-native--decode-ogg-granule-position [40 236 178 11
0 0 0 0] 48000)
- 4089))
- (should (equal (emms-info-native--decode-ogg-granule-position [255 255 255
255 255 255 255 255] nil)
- nil)))
-
-(ert-deftest emms-test-lsb-to-integer ()
- (should (equal (emms-info-native--lsb-to-integer [0]) 0))
- (should (equal (emms-info-native--lsb-to-integer [127]) 127))
- (should (equal (emms-info-native--lsb-to-integer [255]) 255))
- (should (equal (emms-info-native--lsb-to-integer [0 1]) 256))
- (should (equal (emms-info-native--lsb-to-integer [1 0]) 1))
- (should (equal (emms-info-native--lsb-to-integer [0 128]) 32768)))
-
-(ert-deftest emms-test-unsigned-to-signed ()
- (should (equal (emms-info-native--unsigned-to-signed 0 8) 0))
- (should (equal (emms-info-native--unsigned-to-signed 1 8) 1))
- (should (equal (emms-info-native--unsigned-to-signed 127 8) 127))
- (should (equal (emms-info-native--unsigned-to-signed 128 8) -128))
- (should (equal (emms-info-native--unsigned-to-signed 129 8) -127))
- (should (equal (emms-info-native--unsigned-to-signed 254 8) -2))
- (should (equal (emms-info-native--unsigned-to-signed 255 8) -1)))
-
-(defmacro emms-test-make-flac-data-func (name bytes)
- `(defun ,name (offset end replace)
- (let ((bytes (seq-concatenate 'vector [102 76 97 67] ,bytes)))
- (when replace (erase-buffer))
- (mapcar #'insert (seq-subseq bytes offset end)))))
-
-(emms-test-make-flac-data-func emms-test-invalid-flac-block-length [1 200 200
200 0 1 2 3])
-(emms-test-make-flac-data-func emms-test-invalid-flac-block-type [9 0 0 0 0 1
2 3])
-(emms-test-make-flac-data-func emms-test-valid-flac-block [0 0 0 8 10 11 12 13
14 15 16 17 132 0 0 4 1 2 3 4])
-
-(ert-deftest emms-test-decode-flac-meta-blocks ()
- (should-error (emms-info-native--decode-flac-meta-blocks
- #'emms-test-invalid-flac-block-length))
- (should-error (emms-info-native--decode-flac-meta-blocks
- #'emms-test-invalid-flac-block-type))
- (should (equal (emms-info-native--decode-flac-meta-blocks
- #'emms-test-valid-flac-block)
- '([1 2 3 4] [10 11 12 13 14 15 16 17]))))
-
-(ert-deftest emms-test-decode-flac-playtime ()
- ;; The corresponding sample metadata bytes are [10 196 66 240 1 8 36 0].
- (should (= (emms-info-native--decode-flac-playtime 775818634391462912) 392)))
-
-(ert-deftest emms-test-valid-id3v2-frame-id-p ()
- (let ((emms-info-native--id3v2-version 2))
- (should (emms-info-native--valid-id3v2-frame-id-p "A1B"))
- (should (not (emms-info-native--valid-id3v2-frame-id-p "~B1")))
- (should (not (emms-info-native--valid-id3v2-frame-id-p "XX")))
- (should (not (emms-info-native--valid-id3v2-frame-id-p "XXXX"))))
- (let ((emms-info-native--id3v2-version 3))
- (should (emms-info-native--valid-id3v2-frame-id-p "ABC9"))
- (should (not (emms-info-native--valid-id3v2-frame-id-p "~BCD")))
- (should (not (emms-info-native--valid-id3v2-frame-id-p "XXX")))
- (should (not (emms-info-native--valid-id3v2-frame-id-p "XXXXX")))))
-
-(ert-deftest emms-test-checked-id3v2-size ()
- (should (= (emms-info-native--checked-id3v2-size 'tag [0 0 2 1]) 257))
- (should (= (emms-info-native--checked-id3v2-size 'tag [1 1 1 1]) 2113665))
- (should (= (emms-info-native--checked-id3v2-size 'tag [#xff #xff #xff #xff])
- (1- (* 256 1024 1024))))
- (should (= (emms-info-native--checked-id3v2-size 'tag [#x7f #x7f #x7f #x7f])
- (1- (* 256 1024 1024))))
- (should (= (emms-info-native--checked-id3v2-size 'tag [#x12 #x34 #x56 #x78])
- 38611832))
- (let ((emms-info-native--id3v2-version 4))
- (should (= (emms-info-native--checked-id3v2-size 'frame [#xff #xff #xff
#xff])
- (1- (* 256 1024 1024)))))
- (let ((emms-info-native--id3v2-version 3))
- (should (= (emms-info-native--checked-id3v2-size 'frame [#xff #xff #xff
#xff])
- (1- (* 4 1024 1024 1024))))))
-
-(ert-deftest emms-test-decode-id3v2-size ()
- (should (= (emms-info-native--decode-id3v2-size [01 01 01 01] nil)
- 16843009))
- (should (= (emms-info-native--decode-id3v2-size [01 01 01 01] t)
- 2113665))
- (should (= (emms-info-native--decode-id3v2-size [00 00 02 01] nil)
- 513))
- (should (= (emms-info-native--decode-id3v2-size [00 00 02 01] t)
- 257)))
-
-(ert-deftest emms-test-extract-bits ()
- (should (= (emms-info-native--extract-bits 128 7) 1))
- (should (= (emms-info-native--extract-bits 64 6 7) 1))
- (should (= (emms-info-native--extract-bits 128 6 7) 2))
- (should (= (emms-info-native--extract-bits 192 6 7) 3))
- (should (eq (emms-info-native--extract-bits 192 7 6) nil))
- (should (= (emms-info-native--extract-bits 128 32) 0))
- (should (= (emms-info-native--extract-bits 4294688772 21 31) 2047)))
-
-(ert-deftest emms-test-decode-mp3-frame-header ()
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (insert (concat [#x00 #x00 #x00 #x00 #x00 #x00 #x00 #xff #xfb
- #xb0 #x04 #x00 #x00 #x00 #x00 #x00 #x69 #x06 #x00
#x00 #x00 #x00
- #x00 #x0d #x20 #xc0 #x00 #x00 #x00 #x00 #x01 #xa4
#x1c #x00 #x00
- #x00 #x00 #x00 #x34 #x83 #x80 #x00 #x00 #x4c #x41
#x4d #x45 #x33
- #x2e #x39 #x31 #x55 #x55]))
- (should (equal (emms-info-native--find-and-decode-mp3-frame-header)
- '((version . mpeg1)
- (layer . layerIII)
- (crc . 1)
- (bit-rate . 192)
- (sample-rate . 44100)
- (samples-per-frame . 1152)
- (padding . 0)
- (private . 0)
- (channel-mode . stereo)
- (mode-extension . 0)
- (copyright . 0)
- (emphasis . 0)
- (original . 1))))))
-
-(ert-deftest emms-test-decode-xing-header ()
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (insert (concat [255 234 144 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 88 105 110 103 0 0
0 15 0 0 33
- 89 0 80 29 121 0 3 6 8 11 14 15 18 21 23 26 29 31 34
37 39 42 45
- 47 50 53 55 57 60 62 65 68 70 73 76 78 81 84 86 89 92
94 97 100
- 101 104 107 109 112 115 117 120 122 123 125 127 130
133 135 138
- 141 143 146 149 151 154 157 159 162 165 167 170 173
174 177 180
- 182 185 188 190 193 196 198 201 204 206 209 212 214
217 220 222
- 225 228 230 233 236 238 241 244 246 249 251 253 255 0
0 0 88 76
- 65 77 69 51 46 56 56 32 40 97 108 112 104 97 41 0 0
]))
- (should (= (emms-info-native--find-and-decode-xing-header) 8537))))
-
-(ert-deftest emms-test-decode-xing-header-2 ()
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (insert (concat [255 251 80 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 73 110 102 111 0 0 0
15 0 0 35
- 216 0 58 134 9 0 2 5 8 10 13 16 18 20 23 25 28 31 33
36 38 41 43
- 46 49 51 54 56 58 61 64 66 69 72 74 76 79 82 84 87 90
91 94 97
- 99 102 105 108 109 112 115 117 120 123 125 128 130
133 135 138
- 141 143 146 148 150 153 156 158 161 164 166 168 171
174 176 179
- 182 183 186 189 191 194 197 199 201 204 207 209 212
215 217 219
- 222 224 227 230 233 235 237 240 242 245 248 250 253 0
0 0 0 76
- 97 118 99 53 57 46 51 55 0 0]))
- (should (= (emms-info-native--find-and-decode-xing-header) 9176))))
-
-(ert-deftest emms-test-decode-vbri-header ()
- (with-temp-buffer
- (set-buffer-multibyte nil)
- (insert (concat [255 251 161 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
- 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 86 66 82 73 0 1 13 177 0 100 0
- 98 219 145 0 0 33 58 0 132 0 1 0 2 0 64 152 177 189 168 187 54
- 186 206 187 55 186 207 186 103 187 55 188 215 187 159 186 207
- 185 44 187 53 187 56 188 8 187 159 185 149 190 224 188 8 185 250
- 186 99 184 90 182]))
- (should (= (emms-info-native--find-and-decode-vbri-header) 8506))))
diff --git a/test/emms-info-ogg-tests.el b/test/emms-info-ogg-tests.el
new file mode 100644
index 0000000000..e0e1a14e2d
--- /dev/null
+++ b/test/emms-info-ogg-tests.el
@@ -0,0 +1,128 @@
+;;; emms-info-ogg-tests.el --- Test suite for emms-info-ogg -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; Author: Petteri Hintsanen <petterih@iki.fi>
+
+;; This file is part of EMMS.
+
+;; EMMS 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 3, or (at your option)
+;; any later version.
+
+;; EMMS 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.
+
+;; You should have received a copy of the GNU General Public License
+;; along with EMMS; see the file COPYING. If not, write to the Free
+;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+;; MA 02110-1301, USA.
+
+;;; Code:
+
+(require 'emms-info-ogg)
+(require 'ert)
+
+(ert-deftest emms-ogg-test-decode-page ()
+ (let ((bytes [79 103 103 83 0 2 0 0 0 0 0 0 0 0 134 209 158 23 0 0 0 0 53 82
251 136 1 30 1 118 111 114 98 105 115 0 0 0 0 1 68 172 0 0 0 0 0 0 128 56 1 0 0
0 0 0 184 1]))
+ (should (equal (emms-info-ogg--decode-page bytes)
+ (list :num-packets 1
+ :num-bytes 58
+ :stream [1 118 111 114 98 105 115 0 0 0 0 1 68 172 0
0 0 0 0 0 128 56 1 0 0 0 0 0 184 1])))))
+
+(ert-deftest emms-ogg-test-decode-vorbis-headers ()
+ "Test `emms-info-ogg--decode-headers' with Vorbis data."
+ (let ((bytes [1 118 111 114 98 105 115 0 0 0 0 1 68 172 0 0 0 0 0 0 128 56 1
0 0 0 0 0 184 1 3 118 111 114 98 105 115 52 0 0 0 88 105 112 104 46 79 114 103
32 108 105 98 86 111 114 98 105 115 32 73 32 50 48 50 48 48 55 48 52 32 40 82
101 100 117 99 105 110 103 32 69 110 118 105 114 111 110 109 101 110 116 41 2 0
0 0 7 0 0 0 102 111 111 61 98 97 114 27 0 0 0 75 101 121 61 206 159 225 189 144
207 135 225 189 182 32 206 164 206 177 225 189 144 207 132 225 189 176 1]))
+ (should (equal (emms-info-ogg--decode-headers bytes 'vorbis)
+ '((comment-header
+ (framing-bit . 1)
+ (user-comments
+ ((user-comment . [102 111 111 61 98 97 114])
+ (length . 7))
+ ((user-comment . [75 101 121 61 206 159 225 189 144 207
135 225 189 182 32 206 164 206 177 225 189 144 207 132 225 189 176])
+ (length . 27)))
+ (user-comments-list-length . 2)
+ (vendor-string . [88 105 112 104 46 79 114 103 32 108
105 98 86 111 114 98 105 115 32 73 32 50 48 50 48 48 55 48 52 32 40 82 101 100
117 99 105 110 103 32 69 110 118 105 114 111 110 109 101 110 116 41])
+ (vendor-length . 52)
+ (vorbis . [118 111 114 98 105 115])
+ (packet-type . 3))
+ (identification-header
+ (framing-flag . 1)
+ (blocksize . 184)
+ (bitrate-minimum . 0)
+ (bitrate-nominal . 80000)
+ (bitrate-maximum . 0)
+ (sample-rate . 44100)
+ (channel-count . 1)
+ (vorbis-version . 0)
+ (vorbis . [118 111 114 98 105 115])
+ (packet-type . 1)))))))
+
+(ert-deftest emms-ogg-test-decode-opus-headers ()
+ "Test `emms-info-ogg--decode-headers' with Opus data."
+ (let ((bytes [79 112 117 115 72 101 97 100 1 1 56 1 68 172 0 0 0 0 0 79 112
117 115 84 97 103 115 13 0 0 0 108 105 98 111 112 117 115 32 49 46 51 46 49 3 0
0 0 38 0 0 0 69 78 67 79 68 69 82 61 111 112 117 115 101 110 99 32 102 114 111
109 32 111 112 117 115 45 116 111 111 108 115 32 48 46 49 46 49 48 7 0 0 0 102
111 111 61 98 97 114 27 0 0 0 75 101 121 61 206 159 225 189 144 207 135 225 189
182 32 206 164 206 177 225 189 144 207 132 225 189 176]))
+ (should (equal (emms-info-ogg--decode-headers bytes 'opus)
+ '((comment-header
+ (user-comments
+ ((user-comment . [69 78 67 79 68 69 82 61 111 112 117
115 101 110 99 32 102 114 111 109 32 111 112 117 115 45 116 111 111 108 115 32
48 46 49 46 49 48])
+ (length . 38))
+ ((user-comment . [102 111 111 61 98 97 114])
+ (length . 7))
+ ((user-comment . [75 101 121 61 206 159 225 189 144 207
135 225 189 182 32 206 164 206 177 225 189 144 207 132 225 189 176])
+ (length . 27)))
+ (user-comments-list-length . 3)
+ (vendor-string . [108 105 98 111 112 117 115 32 49 46 51
46 49])
+ (vendor-length . 13)
+ (opus-tags . [79 112 117 115 84 97 103 115]))
+ (identification-header
+ (channel-mapping-family . 0)
+ (output-gain . 0)
+ (sample-rate . 44100)
+ (pre-skip . 312)
+ (channel-count . 1)
+ (opus-version . 1)
+ (opus-head . [79 112 117 115 72 101 97 100])))))))
+
+(defun emms-ogg-test--decode-last-page (bytes)
+ "Call `emms-info-ogg--decode-last-page' with BYTES input.
+
+This is a helper function for `emms-ogg-test-decode-last-page'."
+ (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert (concat bytes))
+ (emms-info-ogg--decode-last-page)))
+
+(ert-deftest emms-ogg-test-decode-last-page()
+ (let ((valid [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x04 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x19 #x4e
#xac #xa3 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36])
+ (notlast [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x00 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x19 #x4e
#xac #xa3 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36])
+ (invalid [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x04 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x01 #x02
#x03 #x04 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36]))
+ (should (equal (emms-ogg-test--decode-last-page valid)
+ '((payload . [79 103 103 83 49 50 51 52 53 54])
+ (segment-table . [10])
+ (page-segments . 1)
+ (page-checksum . 2745978393)
+ (page-sequence-no . 2112)
+ (stream-serial-number . 1198406044)
+ (granule-position . [0 36 8 1 0 0 0 0])
+ (header-type-flag . 4)
+ (stream-structure-version . 0)
+ (capture-pattern . [79 103 103 83]))))
+ (should (equal (emms-ogg-test--decode-last-page notlast) nil))
+ (should (equal (emms-ogg-test--decode-last-page invalid) nil))))
+
+(ert-deftest emms-ogg-test-calculate-checksum ()
+ (let ((bytes [#x01 #x02 #x03 #x04 #x4f #x67 #x67 #x53 #x00 #x04 #x00 #x24
#x08 #x01 #x00 #x00 #x00 #x00 #x9c #x39 #x6e #x47 #x40 #x08 #x00 #x00 #x19 #x4e
#xac #xa3 #x01 #x0a #x4f #x67 #x67 #x53 #x31 #x32 #x33 #x34 #x35 #x36]))
+ (should (= (emms-info-ogg--checksum bytes) 445885580))))
+
+(ert-deftest emms-ogg-test-decode-granule-position ()
+ (should (= (emms-info-ogg--decode-granule-pos [0 36 8 1 0 0 0 0] 44100) 392))
+ (should (= (emms-info-ogg--decode-granule-pos [40 236 178 11 0 0 0 0] 48000)
4089))
+ (should (equal (emms-info-ogg--decode-granule-pos [255 255 255 255 255 255
255 255] nil) nil))
+ (should (equal (emms-info-ogg--decode-granule-pos nil nil) nil)))
+
+(provide 'emms-info-ogg-tests)
+
+;;; emms-info-ogg-tests.el ends here
diff --git a/test/emms-info-vorbis-tests.el b/test/emms-info-vorbis-tests.el
new file mode 100644
index 0000000000..b36192806f
--- /dev/null
+++ b/test/emms-info-vorbis-tests.el
@@ -0,0 +1,56 @@
+;;; emms-info-vorbis-tests.el --- Test suite for emms-info-vorbis -*-
lexical-binding: t; -*-
+
+;; Copyright (C) 2023 Free Software Foundation, Inc.
+
+;; Author: Petteri Hintsanen <petterih@iki.fi>
+
+;; This file is part of EMMS.
+
+;; EMMS 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 3, or (at your option)
+;; any later version.
+
+;; EMMS 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.
+
+;; You should have received a copy of the GNU General Public License
+;; along with EMMS; see the file COPYING. If not, write to the Free
+;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+;; MA 02110-1301, USA.
+
+;;; Code:
+
+(require 'emms-info-vorbis)
+(require 'ert)
+
+(ert-deftest emms-vorbis-test-extract-comments ()
+ (let ((comments '(((user-comment . [77 85 83 73 67 66 82 65 73 78 90 95 82
69 76 69 65 83 69 71 82 79 85 80 73 68 61 57 98 51 48 55 50 57 51 45 100 50 101
54 45 51 52 97 57 45 97 50 56 57 45 49 54 49 99 53 98 97 102 49 56 55 102])
(length . 63)) ;musicbrainz_releasegroupid
+ ((user-comment . [79 82 73 71 73 78 65 76 68 65 84 69 61
49 57 57 55 45 48 51 45 51 49]) (length . 23)) ;originaldate
+ ((user-comment . [79 82 73 71 73 78 65 76 89 69 65 82 61
49 57 57 55]) (length . 17)) ;originalyear
+ ((user-comment . [82 69 76 69 65 83 69 84 89 80 69 61 97
108 98 117 109]) (length . 17)) ;releasetype
+ ((user-comment . [66 65 82 67 79 68 69 61 55 54 57 50 51
51 48 48 52 55 50 55]) (length . 20)) ;barcode
+ ((user-comment . [65 76 66 85 77 61 65 32 116 111 100 97
32 67 117 98 97 32 108 101 32 103 117 115 116 97]) (length . 26))))) ;album
+ (should (equal (emms-info-vorbis-extract-comments comments)
+ (quote (("album" . "A toda Cuba le gusta")
+ ("originalyear" . "1997")
+ ("originaldate" . "1997-03-31")))))))
+
+(ert-deftest emms-vorbis-test-split-comment ()
+ (should (equal (emms-info-vorbis--split-comment "") nil))
+ (should (equal (emms-info-vorbis--split-comment "x") nil))
+ (should (equal (emms-info-vorbis--split-comment "x=") nil))
+ (should (equal (emms-info-vorbis--split-comment "=x") nil))
+ (should (equal (emms-info-vorbis--split-comment "a=B")
+ (cons "a" "B")))
+ (should (equal (emms-info-vorbis--split-comment "abc=ABC=123")
+ (cons "abc" "ABC=123")))
+ (let ((comment [75 101 121 61 206 159 225 189 144 207 135 225 189 182 32 206
164 206 177 225 189 144 207 132 225 189 176 10 ])) ;Key=Οὐχὶ Ταὐτὰ
+ (should (equal (emms-info-vorbis--split-comment comment)
+ (cons "key" "Οὐχὶ Ταὐτὰ")))))
+
+(provide 'emms-info-vorbis-tests)
+
+;;; emms-info-vorbis-tests.el ends here
diff --git a/test/emms-tests.el b/test/emms-tests.el
new file mode 100644
index 0000000000..b8bad762f7
--- /dev/null
+++ b/test/emms-tests.el
@@ -0,0 +1,40 @@
+;; -*- lexical-binding: t; -*-
+
+(require 'emms)
+(require 'ert)
+
+(ert-deftest emms-test-le-to-int ()
+ (should (= (emms-le-to-int nil) 0))
+ (should (= (emms-le-to-int [0]) 0))
+ (should (= (emms-le-to-int [127]) 127))
+ (should (= (emms-le-to-int [255]) 255))
+ (should (= (emms-le-to-int [0 1]) 256))
+ (should (= (emms-le-to-int [1 0]) 1))
+ (should (= (emms-le-to-int [0 128]) 32768))
+ (should (= (emms-le-to-int [1 2 4 8]) 134480385)))
+
+(ert-deftest emms-test-from-twos-complement ()
+ (should (= (emms-from-twos-complement 0 8) 0))
+ (should (= (emms-from-twos-complement 1 8) 1))
+ (should (= (emms-from-twos-complement 127 8) 127))
+ (should (= (emms-from-twos-complement 128 8) -128))
+ (should (= (emms-from-twos-complement 129 8) -127))
+ (should (= (emms-from-twos-complement 254 8) -2))
+ (should (= (emms-from-twos-complement 255 8) -1))
+ (should (= (emms-from-twos-complement 0 10) 0))
+ (should (= (emms-from-twos-complement 511 10) 511))
+ (should (= (emms-from-twos-complement 512 10) -512))
+ (should (= (emms-from-twos-complement 1023 10) -1)))
+
+(ert-deftest emms-test-extract-bits ()
+ (should (= (emms-extract-bits 128 7) 1))
+ (should (= (emms-extract-bits 64 6 7) 1))
+ (should (= (emms-extract-bits 128 6 7) 2))
+ (should (= (emms-extract-bits 192 6 7) 3))
+ (should (eq (emms-extract-bits 192 7 6) nil))
+ (should (= (emms-extract-bits 128 32) 0))
+ (should (= (emms-extract-bits 4294688772 21 31) 2047)))
+
+(provide 'emms-tests)
+
+;;; emms-tests.el ends here
- [elpa] externals/emms a6d2bbe484 18/42: Remove useless mapconcat, (continued)
- [elpa] externals/emms a6d2bbe484 18/42: Remove useless mapconcat, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 68a0dfa9b8 19/42: Add Unicode character to metadata in test files, ELPA Syncer, 2023/11/01
- [elpa] externals/emms fca5f3e7d5 26/42: Use bindat-type in emms-info-flac, ELPA Syncer, 2023/11/01
- [elpa] externals/emms a0c4d715f4 12/42: Use fixed sample rate when decoding Opus granule position, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 813c0058bd 28/42: Use bindat-type in emms-info-spc, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 32b394b2b1 34/42: Change tests to use unibyte strings, ELPA Syncer, 2023/11/01
- [elpa] externals/emms c9dce6b305 36/42: Fix tests, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 93c614dc24 32/42: Fix byte compilation errors, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 83490a7218 38/42: Change all tests to use emms-test- prefix, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 8a55be19ba 06/42: Ignore empty Vorbis comments, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 9d71a515f7 09/42: Split emms-info-native to several files,
ELPA Syncer <=
- [elpa] externals/emms 29260a991a 11/42: Replace emms-info-native test files, ELPA Syncer, 2023/11/01
- [elpa] externals/emms fe9b0fffe1 16/42: Use strings instead of vectors for Vorbis comments, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 75f1ee292e 20/42: Add copyright information, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 5014dfa5f3 21/42: Ignore empty tags, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 16f107b583 24/42: Use bindat-type in emms-info-vorbis, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 6c3f1d6ab2 25/42: Use bindat-type in emms-info-opus, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 59e999a259 05/42: Fix error reporting from emms-info-native--ogg-page-bindat-spec, ELPA Syncer, 2023/11/01
- [elpa] externals/emms b512ed7331 03/42: Change magic arrays to patterns, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 5a52c1b129 22/42: Remove dependency on cl-lib, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 399dd78230 23/42: Use bindat-type in emms-info-ogg, ELPA Syncer, 2023/11/01