emacs-elpa-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[elpa] externals/emms 32fd570ed7 42/42: Merge branch 'info-native'


From: ELPA Syncer
Subject: [elpa] externals/emms 32fd570ed7 42/42: Merge branch 'info-native'
Date: Wed, 1 Nov 2023 15:58:02 -0400 (EDT)

branch: externals/emms
commit 32fd570ed712d5ed591a6955e2927a3817acef06
Merge: cdea73e122 52dac8ccc4
Author: Petteri Hintsanen <petterih@iki.fi>
Commit: Petteri Hintsanen <petterih@iki.fi>

    Merge branch 'info-native'
---
 emms-info-native-flac.el                    | 203 ++++++
 emms-info-native-mp3.el                     | 756 +++++++++++++++++++++++
 emms-info-native-ogg.el                     | 324 ++++++++++
 emms-info-native-opus.el                    | 147 +++++
 emms-info-spc.el => emms-info-native-spc.el |  72 ++-
 emms-info-native-vorbis.el                  | 221 +++++++
 emms-info-native.el                         | 922 +---------------------------
 emms-volume-pulse.el                        |   1 +
 emms.el                                     |  55 ++
 test/emms-info-native-flac-tests.el         |  57 ++
 test/emms-info-native-mp3-tests.el          | 104 ++++
 test/emms-info-native-ogg-tests.el          | 130 ++++
 test/emms-info-native-tests.el              |  70 +++
 test/emms-info-native-vorbis-tests.el       |  61 ++
 test/emms-tests.el                          |  61 ++
 test/resources/sine.flac                    | Bin 0 -> 29811 bytes
 test/resources/sine.mp3                     | Bin 0 -> 7125 bytes
 test/resources/sine.ogg                     | Bin 0 -> 6783 bytes
 test/resources/sine.opus                    | Bin 0 -> 8081 bytes
 19 files changed, 2266 insertions(+), 918 deletions(-)

diff --git a/emms-info-native-flac.el b/emms-info-native-flac.el
new file mode 100644
index 0000000000..2315bb7fbe
--- /dev/null
+++ b/emms-info-native-flac.el
@@ -0,0 +1,203 @@
+;;; emms-info-native-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 'emms)
+(require 'emms-info-native-vorbis)
+(require 'bindat)
+
+(defvar bindat-raw)
+
+(defconst emms-info-native-flac--max-peek-size (* 16 1024 1024)
+  "Maximum buffer size for metadata decoding.
+Functions in `emms-info-native-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-native-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-native-flac--meta-header-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (flags u8)
+        (length uint 24)
+        (_ unit (when (or (> length emms-info-native-flac--max-peek-size)
+                          (= length 0))
+                  (error "FLAC block length %s is invalid" length))))
+    '((flags u8)
+      (length u24)
+      (eval (when (or (> last emms-info-native-flac--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
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (min-block-size uint 16)
+        (max-block-size uint 16)
+        (min-frame-size uint 24)
+        (max-frame-size uint 24)
+        (sample-metadata vec 8)
+        (md5 vec 16))
+    '((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
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (vendor-length uintr 32)
+        (_ unit (when (> vendor-length (length bindat-raw))
+                  (error "FLAC vendor length %s is too long"
+                         vendor-length)))
+        (vendor-string str vendor-length)
+        (user-comments-list-length uintr 32)
+        (_ unit (when (> user-comments-list-length (length bindat-raw))
+                  (error "FLAC user comment list length %s is too long"
+                         user-comments-list-length)))
+        (user-comments repeat user-comments-list-length
+                       type 
emms-info-native-vorbis--comment-field-bindat-spec))
+    '((vendor-length u32r)
+      (eval (when (> last (length bindat-raw))
+              (error "FLAC vendor length %s is too long" last)))
+      (vendor-string str (vendor-length))
+      (user-comments-list-length u32r)
+      (eval (when (> last (length bindat-raw))
+              (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-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-native-vorbis-extract-comments' for details."
+  (unless (emms-info-native-flac--has-signature filename)
+    (error "Invalid FLAC stream"))
+  (let* ((blocks
+          (emms-info-native-flac--decode-meta-blocks
+           (emms-info-native-flac--file-inserter filename)))
+         (comment-block
+          (and (car blocks)
+               (bindat-unpack emms-info-native-flac--comment-block-bindat-spec
+                              (car blocks))))
+         (stream-info-block
+          (and (cadr blocks)
+               (bindat-unpack 
emms-info-native-flac--stream-info-block-bindat-spec
+                              (cadr blocks))))
+         (user-comments
+          (and comment-block
+               (bindat-get-field comment-block 'user-comments)))
+         (comments
+          (and user-comments
+               (emms-info-native-vorbis-extract-comments user-comments)))
+         (playing-time
+          (and stream-info-block
+               (emms-info-native-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-native-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-native-flac--file-inserter (filename)
+  "Return a function for reading bytes from FILENAME.
+This is meant for `emms-info-native-flac--decode-meta-blocks'."
+  (lambda (offset end)
+    (insert-file-contents-literally filename nil offset end t)))
+
+(defun emms-info-native-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 (setq offset (+ offset 4)))
+        (let* ((header
+                (bindat-unpack emms-info-native-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 (buffer-string)))
+          (when (= block-type 4)
+            ;; Comment block found, extract it.
+            (funcall read-func offset end)
+            (setq comment-block (buffer-string)))
+          (setq offset end)))
+      (list comment-block stream-info-block))))
+
+(defun emms-info-native-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-native-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-native-flac)
+
+;;; emms-info-native-flac.el ends here
diff --git a/emms-info-native-mp3.el b/emms-info-native-mp3.el
new file mode 100644
index 0000000000..5b5b156327
--- /dev/null
+++ b/emms-info-native-mp3.el
@@ -0,0 +1,756 @@
+;;; emms-info-native-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 'emms)
+(require 'bindat)
+(eval-when-compile (require 'subr-x))
+
+
+;;; ID3 code
+
+(defvar emms-info-native-id3v2--version 0
+  "Last decoded ID3v2 version.")
+
+(defconst emms-info-native-id3v2--magic-pattern "ID3"
+  "ID3v2 header magic pattern.")
+
+(defconst emms-info-native-id3v2--header-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (file-identifier str 3)
+        (_ unit (unless (equal file-identifier 
emms-info-native-id3v2--magic-pattern)
+                  (error "ID3v2 framing mismatch: expected `%s', got `%s'"
+                         emms-info-native-id3v2--magic-pattern
+                         file-identifier)))
+        (version u8)
+        (_ unit (progn (setq emms-info-native-id3v2--version version) nil))
+        (revision u8)
+        (flags bits 1)
+        (size-bytes vec 4)
+        (size unit (emms-info-native-id3v2--checked-size 'tag size-bytes)))
+    '((file-identifier str 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-id3v2--checked-size 'tag last))))
+  "ID3v2 header specification.")
+
+(defconst emms-info-native-id3v2--frame-header-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (id str (if (= emms-info-native-id3v2--version 2) 3 4))
+        (_ unit (unless (emms-info-native-id3v2--valid-frame-id-p id)
+                  (error "ID3v2 frame id `%s' is invalid" id)))
+        (size-bytes vec (if (= emms-info-native-id3v2--version 2) 3 4))
+        (size unit (emms-info-native-id3v2--checked-size 'frame size-bytes))
+        (flags bits (if (= emms-info-native-id3v2--version 2) 0 2)))
+    '((id str (eval (if (= emms-info-native-id3v2--version 2) 3 4)))
+      (eval (unless (emms-info-native-id3v2--valid-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-id3v2--checked-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-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.")
+
+(defun emms-info-native-id3v2--valid-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-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-native-id3v2--decode-size bytes t))
+                    ((eq elt 'frame)
+                     (if (= emms-info-native-id3v2--version 4)
+                         (emms-info-native-id3v2--decode-size bytes t)
+                       (emms-info-native-id3v2--decode-size bytes nil))))))
+    (if (zerop size)
+        (error "ID3v2 tag/frame size is zero")
+      size)))
+
+(defun emms-info-native-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-native-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-native-id3v2--header-bindat-spec
+                   (buffer-string))))
+
+(defun emms-info-native-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-native-id3v2--checked-size 'frame (buffer-string))))
+
+(defun emms-info-native-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 non-nil, 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)
+    (ignore-errors
+      (while (< offset limit)
+        (let* ((frame-data (emms-info-native-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-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-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-native-id3v2--read-frame-data' for details on UNSYNC.
+
+Skip frames that do not map to any info-id in
+`emms-info-native-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-native-id3v2--decode-frame-data'."
+  (let* ((decoded (emms-info-native-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-native-id3v2--frame-to-info)))
+         (size (bindat-get-field header 'size)))
+    (if (or info-id unsync)
+        ;; Note that if unsync is non-nil, we have to always read the
+        ;; frame to determine next-frame-offset.
+        (let* ((data (emms-info-native-id3v2--read-frame-data
+                      filename data-offset size unsync))
+               (next-frame-offset (car data))
+               (value (emms-info-native-id3v2--decode-frame-data
+                       (cdr data) info-id)))
+          (cons next-frame-offset value))
+      ;; Skip the frame.
+      (cons (+ data-offset size) nil))))
+
+(defun emms-info-native-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-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--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 non-nil, 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))
+            (setq end (1+ end)))
+          (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-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-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-id3v1--genres';
+in the latter case use the genre string verbatim.
+
+Return a cons cell (INFO-ID . VALUE) where VALUE is the decoded
+string, or nil if the decoding failed."
+  (when info-id
+    (let ((str (emms-info-native-id3v2--decode-string data)))
+      (cond ((string-empty-p str) nil)
+            ((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 (and (rassoc key emms-info-native-id3v2--frame-to-info)
+                          (not (string-empty-p val)))
+                 (cons key val))))))))
+
+(defun emms-info-native-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-native-id3v2--text-encoding bytes))
+         (decoded (decode-coding-string (seq-rest bytes) encoding)))
+    (if (and (> (length decoded) 0)
+             (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."
+  (alist-get (seq-first bytes)
+             emms-info-native-id3v2--text-encodings))
+
+
+;;; MP3 code
+
+(defconst emms-info-native-mp3--versions
+  '((0 . mpeg25)
+    (1 . reserved)
+    (2 . mpeg2)
+    (3 . mpeg1))
+  "MPEG versions.")
+
+(defconst emms-info-native-mp3--layers
+  '((0 . reserved)
+    (1 . layerIII)
+    (2 . layerII)
+    (3 . layerI))
+  "MPEG Audio Layers.")
+
+(defconst emms-info-native-mp3--channel-modes
+  '((0 . stereo)
+    (1 . joint-stereo)
+    (2 . dual-channel)
+    (3 . single-channel))
+  "MPEG channel modes.")
+
+(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))
+  "Bit rates for each MPEG version/layer combination.")
+
+(defconst emms-info-native-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-native-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-native-mp3--vbri-header-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (id str 4)
+        (version uint 16)
+        (delay uint 16)
+        (quality uint 16)
+        (bytes uint 32)
+        (frames uint 32))
+    '((id str 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-native-mp3--xing-header-bindat-spec
+    (if (eval-when-compile (fboundp 'bindat-type))
+        (bindat-type
+          (id vec 4)
+          (flags bits 4)
+          (frames uint 32))
+      '((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-native-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-native-id3v2--frame-to-info' for recognized fields."
+  (let* (emms-info-native-id3v2--version
+         (header (emms-info-native-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.
+      (setq offset (+ offset
+                      (emms-info-native-id3v2--checked-ext-header-size
+                       filename))))
+    (let ((tags
+           (emms-info-native-id3v2--decode-frames
+            filename offset (+ tag-size 10) unsync))
+          (playtime
+           (emms-info-native-mp3--decode-duration filename (+ tag-size 10))))
+      (nconc tags (when playtime
+                    (list (cons "playing-time" playtime)))))))
+
+(defun emms-info-native-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-native-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-native-mp3--find-and-decode-xing-header)
+                (emms-info-native-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-native-mp3--estimate-duration filename bit-rate))))))
+
+(defun emms-info-native-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-native-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-native-mp3--decode-frame-header bytes)))))
+    header))
+
+(defun emms-info-native-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-native-mp3--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-mp3--samples-per-frame
+             version layer))
+           (bit-rate
+            (emms-info-native-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-native-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-native-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-native-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-native-mp3--vbri-header-bindat-spec
+                          (buffer-string)
+                          (1- (match-beginning 0)))))
+      (bindat-get-field header 'frames))))
+
+(defun emms-info-native-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-native-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-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-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-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))))
+
+(provide 'emms-info-native-mp3)
+
+;;; emms-info-native-mp3.el ends here
diff --git a/emms-info-native-ogg.el b/emms-info-native-ogg.el
new file mode 100644
index 0000000000..89c309e85f
--- /dev/null
+++ b/emms-info-native-ogg.el
@@ -0,0 +1,324 @@
+;;; emms-info-native-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-native-vorbis.el.
+;;
+;; Opus code is based on RFC 7845; see
+;; https://tools.ietf.org/html/rfc7845.html and
+;; emms-info-native-opus.el.
+
+;;; Code:
+
+(require 'emms-info-native-opus)
+(require 'emms-info-native-vorbis)
+(require 'bindat)
+
+(defconst emms-info-native-ogg--page-size 65307
+  "Maximum size for a single Ogg container page.")
+
+(defconst emms-info-native-ogg--max-peek-size (* 16 1024 1024)
+  "Maximum buffer size for metadata decoding.
+Functions in `emms-info-native-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-native-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-native-ogg--magic-pattern "OggS"
+  "Ogg format magic capture pattern.")
+
+(defconst emms-info-native-ogg--page-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (capture-pattern str 4)
+        (_ unit (unless (equal capture-pattern 
emms-info-native-ogg--magic-pattern)
+                  (error "Ogg framing mismatch: expected `%s', got `%s'"
+                         emms-info-native-ogg--magic-pattern
+                         capture-pattern)))
+        (stream-structure-version u8)
+        (_ unit (unless (= stream-structure-version 0)
+                  (error "Ogg version mismatch: expected 0, got %d"
+                         stream-structure-version)))
+        (header-type-flag u8)
+        (granule-position sint 64 'le)
+        (stream-serial-number uintr 32)
+        (page-sequence-no uintr 32)
+        (page-checksum uintr 32)
+        (page-segments u8)
+        (segment-table vec page-segments)
+        (payload str (seq-reduce #'+ segment-table 0)))
+    ;; For Emacsen older than 28
+    '((capture-pattern str 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-bytes vec 8)
+      (granule-position eval (emms-from-twos-complement
+                              (emms-le-to-int last) 64))
+      (stream-serial-number u32r)
+      (page-sequence-no u32r)
+      (page-checksum u32r)
+      (page-segments u8)
+      (segment-table vec (page-segments))
+      (payload str (eval (seq-reduce #'+ last 0)))))
+  "Ogg page structure specification.")
+
+(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-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-native-vorbis--split-comment' for details."
+  (let* ((packets
+          (emms-info-native-ogg--read-and-decode-packets filename 2))
+         (headers
+          (emms-info-native-ogg--decode-headers packets stream-type))
+         (user-comments
+          (bindat-get-field headers 'comment-header 'user-comments))
+         (comments
+          (emms-info-native-vorbis-extract-comments user-comments))
+         (last-page
+          (emms-info-native-ogg--read-and-decode-last-page filename))
+         (granule-pos
+          (alist-get 'granule-position last-page))
+         (sample-rate
+          (if (eq stream-type 'vorbis)
+              (bindat-get-field headers
+                                'identification-header
+                                'sample-rate)
+            ;; Opus assumes a fixed sample rate of 48 kHz for granule
+            ;; position.
+            48000))
+         (playing-time
+          (when (and granule-pos (> granule-pos 0))
+            (/ granule-pos sample-rate))))
+    (nconc comments
+           (when playing-time
+             (list (cons "playing-time" playing-time))))))
+
+(defun emms-info-native-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
+string, concatenated.
+
+Read data in `emms-info-native-ogg--page-size' chunks.  If more
+than `emms-info-native-ogg--max-peek-size' bytes of data would be
+read, signal an error.
+
+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 (list)))
+    (while (< num-packets packets)
+      (when (> offset emms-info-native-ogg--max-peek-size)
+        (error "Ogg payload is too large"))
+      (let ((page (emms-info-native-ogg--read-and-decode-page filename 
offset)))
+        (setq num-packets (+ num-packets
+                             (emms-info-native-ogg--num-packets page)))
+        (setq offset (+ offset
+                        (bindat-length
+                         emms-info-native-ogg--page-bindat-spec page)))
+        (push (bindat-get-field page 'payload) stream)))
+    (reverse (mapconcat #'nreverse stream nil))))
+
+(defun emms-info-native-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-native-ogg--decode-page'."
+  (with-temp-buffer
+    (set-buffer-multibyte nil)
+    (insert-file-contents-literally
+     filename nil offset (+ offset emms-info-native-ogg--page-size))
+    (bindat-unpack emms-info-native-ogg--page-bindat-spec
+                   (buffer-string))))
+
+(defun emms-info-native-ogg--num-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-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-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)
+         (bindat-unpack emms-info-native-opus--headers-bindat-spec
+                        packets))
+        (t (error "Unknown stream type %s" stream-type))))
+
+(defun emms-info-native-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
+                     (file-truename 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-ogg--decode-last-page))))
+
+(defun emms-info-native-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 emms-info-native-ogg--magic-pattern nil t))
+      (setq page (emms-info-native-ogg--verify-page)))
+    (when (and page
+               (> (logand (alist-get 'header-type-flag page) #x04) 0))
+      page)))
+
+(defun emms-info-native-ogg--verify-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 or the
+page is otherwise invalid."
+  (ignore-errors
+    (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-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-native-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))
+
+(provide 'emms-info-native-ogg)
+
+;;; emms-info-native-ogg.el ends here
diff --git a/emms-info-native-opus.el b/emms-info-native-opus.el
new file mode 100644
index 0000000000..15f0aa487f
--- /dev/null
+++ b/emms-info-native-opus.el
@@ -0,0 +1,147 @@
+;;; emms-info-native-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-native-ogg'
+;; feature.
+
+;;; Code:
+
+(require 'emms-info-native-vorbis)
+(require 'bindat)
+
+(defvar bindat-raw)
+
+(defvar emms-info-native-opus--channel-count 0
+  "Last decoded Opus channel count.")
+
+(defconst emms-info-native-opus--id-magic-pattern "OpusHead"
+  "Opus identification header magic pattern.")
+
+(defconst emms-info-native-opus--channel-mapping-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (stream-count u8)
+        (coupled-count u8)
+        (channel-mapping vec emms-info-native-opus--channel-count))
+  '((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--id-header-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (opus-head str 8)
+        (_ unit (unless (equal opus-head 
emms-info-native-opus--id-magic-pattern)
+                  (error "Opus framing mismatch: expected `%s', got `%s'"
+                         emms-info-native-opus--id-magic-pattern
+                         opus-head)))
+        (opus-version u8)
+        (_ unit (unless (< opus-version 16)
+                  (error "Opus version mismatch: expected < 16, got %s"
+                         opus-version)))
+        (channel-count u8)
+        (_ unit (progn (setq emms-info-native-opus--channel-count 
channel-count) nil))
+        (pre-skip uintr 16)
+        (sample-rate uintr 32)
+        (output-gain uintr 16)
+        (channel-mapping-family u8)
+        (_ . (if (> channel-mapping-family 0)
+                 (type emms-info-native-opus--channel-mapping-bindat-spec)
+               (unit nil))))
+    '((opus-head str 8)
+      (eval (unless (equal last emms-info-native-opus--id-magic-pattern)
+              (error "Opus framing mismatch: expected `%s', got `%s'"
+                     emms-info-native-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-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-bindat-spec)))))
+  "Opus identification header specification.")
+
+(defconst emms-info-native-opus--tags-magic-pattern "OpusTags"
+  "Opus comment header magic pattern.")
+
+(defconst emms-info-native-opus--comment-header-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (opus-tags str 8)
+        (_ unit (unless (equal opus-tags 
emms-info-native-opus--tags-magic-pattern)
+                  (error "Opus framing mismatch: expected `%s', got `%s'"
+                         emms-info-native-opus--tags-magic-pattern
+                         opus-tags)))
+        (vendor-length uintr 32)
+        (_ unit (when (> vendor-length (length bindat-raw))
+                  (error "Opus vendor length %s is too long"
+                         vendor-length)))
+        (vendor-string str vendor-length)
+        (user-comments-list-length uintr 32)
+        (_ unit (when (> user-comments-list-length (length bindat-raw))
+                  (error "Opus user comment list length %s is too long"
+                         user-comments-list-length)))
+        (user-comments repeat user-comments-list-length
+                       type 
emms-info-native-vorbis--comment-field-bindat-spec))
+    '((opus-tags str 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 (length bindat-raw))
+              (error "Opus vendor length %s is too long" last)))
+      (vendor-string str (vendor-length))
+      (user-comments-list-length u32r)
+      (eval (when (> last (length bindat-raw))
+              (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--headers-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (identification-header type 
emms-info-native-opus--id-header-bindat-spec)
+        (comment-header type 
emms-info-native-opus--comment-header-bindat-spec))
+    '((identification-header struct 
emms-info-native-opus--id-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.")
+
+(provide 'emms-info-native-opus)
+
+;;; emms-info-native-opus.el ends here
diff --git a/emms-info-spc.el b/emms-info-native-spc.el
similarity index 52%
rename from emms-info-spc.el
rename to emms-info-native-spc.el
index fd092a52d0..e159f1bff6 100644
--- a/emms-info-spc.el
+++ b/emms-info-native-spc.el
@@ -1,4 +1,4 @@
-;;; emms-info-spc.el --- Native Emacs Lisp info method for EMMS -*- 
lexical-binding: t; -*-
+;;; emms-info-native-spc.el --- EMMS info functions for SPC files  -*- 
lexical-binding: t; -*-
 
 ;; Copyright (C) 2023  Free Software Foundation, Inc.
 
@@ -24,23 +24,49 @@
 ;;; Commentary:
 
 ;; This file provides a native emms-info-method for SPC files. (well,
-;; actually the id666 tag embedded inside them). "Native" means a pure
-;; Emacs Lisp implementation instead of one relying on external tools
-;; or libraries.
+;; actually the ID666 tag embedded inside them).  "Native" means a
+;; pure Emacs Lisp implementation instead of one relying on external
+;; tools or libraries.
 
 ;;; Code:
 
 (require 'bindat)
 
-(defconst emms-info-spc--id666-magic-array
-  [#x53 #x4e #x45 #x53 #x2d #x53 #x50#x43 #x37 #x30 #x30 #x20 #x53 #x6f #x75 
#x6e #x64 #x20 #x46 #x69 #x6c #x65 #x20 #x44 #x61 #x74 #x61 #x20 #x76 #x30 #x2e 
#x33 #x30]
-  "id666 header magic pattern `SNES-SPC700 Sound File Data v0.30'")
-
-(defconst emms-info-spc--id666-header-bindat-spec
-  '((file-identifier vec 33)
-    (eval (unless (equal last emms-info-spc--id666-magic-array)
-            (error "id666 framing mismatch: expected `%s', got `%s'"
-                   emms-info-spc--id666-magic-array
+(defconst emms-info-native-spc--id666-magic-pattern
+  "SNES-SPC700 Sound File Data v0.30"
+  "ID666 header magic pattern.")
+
+(defconst emms-info-native-spc--id666-header-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (file-identifier str 33)
+        (_ unit (unless (equal file-identifier
+                               emms-info-native-spc--id666-magic-pattern)
+                  (error "ID666 framing mismatch: expected `%s', got `%s'"
+                         emms-info-native-spc--id666-magic-pattern
+                         file-identifier)))
+        (unused uint 16)
+        (has-id666 u8)
+        (revision u8)
+        (pc-reg uint 16)
+        (a-reg u8)
+        (x-reg u8)
+        (y-reg u8)
+        (psw-reg u8)
+        (sp-reg u8)
+        (res-reg uint 16)
+        (song-title strz 32)
+        (game-title strz 32)
+        (dumper strz 16)
+        (comment strz 32)
+        (date strz 11)
+        (fadeout vec 3)
+        (fadeout-length vec 5)
+        (artist strz 32))
+  '((file-identifier str 33)
+    (eval (unless (equal last emms-info-native-spc--id666-magic-pattern)
+            (error "ID666 framing mismatch: expected `%s', got `%s'"
+                   emms-info-native-spc--id666-magic-pattern
                    last)))
     (unused u16)
     (has-id666 u8)
@@ -59,28 +85,28 @@
     (date strz 11)
     (fadeout vec 3)
     (fadeout-length vec 5)
-    (artist strz 32))
-  "id666 header specification.
+    (artist strz 32)))
+  "ID666 header specification.
 
 Sources:
 
 - URL `https://ocremix.org/info/SPC_Format_Specification'
 - URL `https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html'")
 
-(defun emms-info-spc--decode-id666-header (filename)
-  "Read and decode id666 header from FILENAME."
+(defun emms-info-native-spc--decode-id666-header (filename)
+  "Read and decode ID666 header from FILENAME."
   (with-temp-buffer
     (set-buffer-multibyte nil)
     (insert-file-contents-literally filename nil 0 210)
-    (bindat-unpack emms-info-spc--id666-header-bindat-spec
+    (bindat-unpack emms-info-native-spc--id666-header-bindat-spec
                    (buffer-string))))
 
-(defun emms-info-spc--decode-id666 (filename)
-  "Read and decode id666 metadata from FILENAME.
+(defun emms-info-native-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."
   (condition-case nil
-      (let ((header (emms-info-spc--decode-id666-header filename)))
+      (let ((header (emms-info-native-spc--decode-id666-header filename)))
        (when (= 26 (bindat-get-field header 'has-id666))
          (list
           (cons 'info-title (bindat-get-field header 'song-title))
@@ -90,6 +116,6 @@ in case of errors or if there were no known fields in 
FILENAME."
           (cons 'info-note (bindat-get-field header 'comment)))))
     (error nil)))
 
-(provide 'emms-info-spc)
+(provide 'emms-info-native-spc)
 
-;;; emms-info-spc.el ends here
+;;; emms-info-native-spc.el ends here
diff --git a/emms-info-native-vorbis.el b/emms-info-native-vorbis.el
new file mode 100644
index 0000000000..7a8ccc6239
--- /dev/null
+++ b/emms-info-native-vorbis.el
@@ -0,0 +1,221 @@
+;;; emms-info-native-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-native-ogg'.
+
+;;; Code:
+
+(require 'bindat)
+
+(defvar bindat-raw)
+
+(defconst emms-info-native-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-native-vorbis--header-magic-pattern "vorbis"
+  "Header packet magic pattern.")
+
+(defconst emms-info-native-vorbis--id-header-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (packet-type u8)
+        (_ unit (unless (= packet-type 1)
+                  (error "Vorbis header type mismatch: expected 1, got %s"
+                         packet-type)))
+        (vorbis str 6)
+        (_ unit (unless (equal vorbis 
emms-info-native-vorbis--header-magic-pattern)
+                  (error "Vorbis framing mismatch: expected `%s', got `%s'"
+                         emms-info-native-vorbis--header-magic-pattern
+                         vorbis)))
+        (vorbis-version uintr 32)
+        (_ unit (unless (= vorbis-version 0)
+                  (error "Vorbis version mismatch: expected 0, got %s"
+                         vorbis-version)))
+        (channel-count u8)
+        (sample-rate uintr 32)
+        (bitrate-maximum uintr 32)
+        (bitrate-nominal uintr 32)
+        (bitrate-minimum uintr 32)
+        (blocksize u8)
+        (framing-flag u8)
+        (_ unit (unless (= framing-flag 1)
+                  (error "Vorbis framing bit mismatch: expected 1, got %s"
+                         framing-flag))))
+    '((packet-type u8)
+      (eval (unless (= last 1)
+              (error "Vorbis header type mismatch: expected 1, got %s"
+                     last)))
+      (vorbis str 6)
+      (eval (unless (equal last emms-info-native-vorbis--header-magic-pattern)
+              (error "Vorbis framing mismatch: expected `%s', got `%s'"
+                     emms-info-native-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-native-vorbis--comment-field-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (length uintr 32)
+        (_ unit (when (> length (length bindat-raw))
+                  (error "Vorbis comment length %s is too long"
+                         length)))
+        (user-comment str length))
+    '((length u32r)
+      (eval (when (> last (length bindat-raw))
+              (error "Vorbis comment length %s is too long" last)))
+      (user-comment str (length))))
+  "Vorbis comment field specification.")
+
+(defconst emms-info-native-vorbis--comment-header-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (packet-type u8)
+        (_ unit (unless (= packet-type 3)
+                  (error "Vorbis header type mismatch: expected 3, got %s"
+                         packet-type)))
+        (vorbis str 6)
+        (_ unit (unless (equal vorbis 
emms-info-native-vorbis--header-magic-pattern)
+                  (error "Vorbis framing mismatch: expected `%s', got `%s'"
+                         emms-info-native-vorbis--header-magic-pattern
+                         vorbis)))
+        (vendor-length uintr 32)
+        (_ unit (when (> vendor-length (length bindat-raw))
+                  (error "Vorbis vendor length %s is too long"
+                         vendor-length)))
+        (vendor-string str vendor-length)
+        (user-comments-list-length uintr 32)
+        (_ unit (when (> user-comments-list-length (length bindat-raw))
+                  (error "Vorbis user comment list length %s is too long"
+                         user-comments-list-length)))
+        (user-comments repeat user-comments-list-length
+                       type emms-info-native-vorbis--comment-field-bindat-spec)
+        (framing-bit u8)
+        (_ unit (unless (= framing-bit 1)
+                  (error "Vorbis framing bit mismatch: expected 1, got %s"
+                         framing-bit))))
+    '((packet-type u8)
+      (eval (unless (= last 3)
+              (error "Vorbis header type mismatch: expected 3, got %s"
+                     last)))
+      (vorbis str 6)
+      (eval (unless (equal last emms-info-native-vorbis--header-magic-pattern)
+              (error "Vorbis framing mismatch: expected `%s', got `%s'"
+                     emms-info-native-vorbis--header-magic-pattern
+                     last)))
+      (vendor-length u32r)
+      (eval (when (> last (length bindat-raw))
+              (error "Vorbis vendor length %s is too long" last)))
+      (vendor-string str (vendor-length))
+      (user-comments-list-length u32r)
+      (eval (when (> last (length bindat-raw))
+              (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--headers-bindat-spec
+  (if (eval-when-compile (fboundp 'bindat-type))
+      (bindat-type
+        (_ struct (identification-header type 
emms-info-native-vorbis--id-header-bindat-spec)
+                  (comment-header type 
emms-info-native-vorbis--comment-header-bindat-spec)))
+    '((identification-header struct 
emms-info-native-vorbis--id-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.")
+
+(defun emms-info-native-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-native-vorbis--comment-header-bindat-spec'.
+
+Return comments in a list of (FIELD . VALUE) cons cells.  Only
+FIELDs that are listed in `emms-info-native-vorbis--accepted-fields' are
+returned."
+  (let (comments)
+    (dolist (user-comment user-comments)
+      (let* ((comment (alist-get 'user-comment user-comment))
+             (pair (emms-info-native-vorbis--split-comment comment)))
+        (when (member (car pair) emms-info-native-vorbis--accepted-fields)
+          (push pair comments))))
+    comments))
+
+(defun emms-info-native-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 comment 'utf-8)))
+    (when (string-match "^\\(.+?\\)=\\(.+\\)$" comment-string)
+      (cons (downcase (match-string 1 comment-string))
+            (match-string 2 comment-string)))))
+
+(provide 'emms-info-native-vorbis)
+
+;;; emms-info-native-vorbis.el ends here
diff --git a/emms-info-native.el b/emms-info-native.el
index 15f2d4cfae..a498c815ba 100644
--- a/emms-info-native.el
+++ b/emms-info-native.el
@@ -25,8 +25,8 @@
 
 ;; This file provides a native emms-info-method for EMMS.  Here
 ;; "native" means a pure Emacs Lisp implementation instead of one
-;; relying on external tools or libraries like `emms-info-ogginfo' or
-;; `emms-info-libtag'.
+;; relying on external tools or libraries like
+;; `emms-info-native-ogginfo' or `emms-info-libtag'.
 ;;
 ;; To use this method, add `emms-info-native' to
 ;; `emms-info-functions'.
@@ -34,24 +34,19 @@
 ;; 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'.
+;; - MP3 files with extension `.mp3' and ID3v2 tags.  All ID3v2
+;;   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
+;; - 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
 ;;   found within Super Nintendos.
 ;;
@@ -60,890 +55,26 @@
 
 ;;; Code:
 
-(require 'bindat)
-(require 'cl-lib)
 (require 'emms-info)
-(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-array)
-            (error "Vorbis framing mismatch: expected `%s', got `%s'"
-                   emms-info-native--vorbis-magic-array
-                   last)))
-    (vorbis-version u32r)
-    (eval (unless (= last 0)
-            (error "Vorbis version mismatch: expected 0, got %s"
-                   last)))
-    (audio-channels u8)
-    (audio-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-array
-  [118 111 114 98 105 115]
-  "Header packet magic pattern `vorbis'.")
-
-(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-array)
-            (error "Vorbis framing mismatch: expected `%s', got `%s'"
-                   emms-info-native--vorbis-magic-array
-                   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.
-
-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-array)
-            (error "Opus framing mismatch: expected `%s', got `%s'"
-                   emms-info-native--opus-head-magic-array
-                   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-array
-  [79 112 117 115 72 101 97 100]
-  "Opus identification header magic pattern `OpusHead'.")
-
-(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-array)
-            (error "Opus framing mismatch: expected `%s', got `%s'"
-                   emms-info-native--opus-tags-magic-array
-                   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-array
-  [79 112 117 115 84 97 103 115]
-  "Opus comment header magic pattern `OpusTags'.")
-
-;;;; 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-array)
-            (error "Ogg framing mismatch: expected `%s', got `%s'"
-                   emms-info-native--ogg-magic-array
-                   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 vec 4)
-    (page-sequence-no vec 4)
-    (page-checksum vec 4)
-    (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-array
-  [79 103 103 83]
-  "Ogg format magic capture pattern `OggS'.")
-
-(defun emms-info-native--decode-ogg-comments (filename stream-type)
-  "Read and decode comments 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))
-         (comments (bindat-get-field headers
-                                     'comment-header
-                                     'user-comments)))
-    (emms-info-native--extract-vorbis-comments comments)))
-
-(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--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--decode-ogg-page (filename offset)
-  "Read and decode a single Ogg page from FILENAME.
-Starting reading data from byte offset OFFSET.
-
-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."
-  (with-temp-buffer
-    (set-buffer-multibyte nil)
-    (insert-file-contents-literally filename
-                                    nil
-                                    offset
-                                    (+ offset
-                                       emms-info-native--ogg-page-size))
-    (let* ((page (bindat-unpack emms-info-native--ogg-page-bindat-spec
-                                (buffer-string)))
-           (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))))
-
-;;;; 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-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-comments (filename)
-  "Read and decode comments 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."
-  (unless (emms-info-native--has-flac-signature filename)
-    (error "Invalid FLAC stream"))
-  (let* ((block (emms-info-native--decode-flac-comment-block
-                 filename))
-         (unpacked (bindat-unpack 
emms-info-native--flac-comment-block-bindat-spec
-                                  block))
-         (user-comments (bindat-get-field unpacked 'user-comments)))
-    (emms-info-native--extract-vorbis-comments user-comments)))
-
-(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--decode-flac-comment-block (filename)
-  "Read and decode a comment block from FLAC file FILENAME.
-Return the comment block data in a vector."
-  (with-temp-buffer
-    (set-buffer-multibyte nil)
-    (let (comment-block
-          last-flag
-          (offset 4))
-      (while (and (not comment-block) (not last-flag))
-        (insert-file-contents-literally filename
-                                        nil
-                                        offset
-                                        (cl-incf offset 4))
-        (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 4)
-            ;; Comment block found, extract it.
-            (insert-file-contents-literally filename nil offset end t)
-            (setq comment-block (vconcat (buffer-string))))
-          (setq offset end)))
-      comment-block)))
-
-;;;; id3v2 (MP3) code
-
-(defconst emms-info-native--id3v2-header-bindat-spec
-  '((file-identifier vec 3)
-    (eval (unless (equal last emms-info-native--id3v2-magic-array)
-            (error "id3v2 framing mismatch: expected `%s', got `%s'"
-                   emms-info-native--id3v2-magic-array
-                   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-array
-  [#x49 #x44 #x33]
-  "id3v2 header magic pattern `ID3'.")
-
-(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.")
-
-(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)))
-    (apply '+ (seq-map-indexed (lambda (elt idx)
-                                 (* (expt 2 (* num-bits idx)) elt))
-                               (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)))
-        (emms-info-native--decode-id3v2-frames filename
-                                               offset
-                                               (+ tag-size 10)
-                                               unsync))
-    (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)))
-
-;;;; EMMS code
+(require 'emms-info-native-flac)
+(require 'emms-info-native-ogg)
+(require 'emms-info-native-mp3)
+(require 'emms-info-native-spc)
 
 (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)))
-            (unless (zerop (length value))
-              (emms-track-set track
-                              name
-                              (if (eq name 'info-playing-time)
-                                 (string-to-number 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))))
 
@@ -954,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-comments filename stream-type))
+           (emms-info-native-ogg-decode-metadata filename stream-type))
           ((eq stream-type 'flac)
-           (emms-info-native--decode-flac-comments filename))
+           (emms-info-native-flac-decode-metadata filename))
           ((eq stream-type 'mp3)
-           (emms-info-native--decode-id3v2 filename))
+           (emms-info-native-mp3-decode-metadata filename))
          ((eq stream-type 'spc)
-          (emms-info-spc--decode-id666 filename))
+          (emms-info-native-spc-decode-id666 filename))
           (t nil))))
 
 (defun emms-info-native--find-stream-type (filename)
@@ -968,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-volume-pulse.el b/emms-volume-pulse.el
index 4df9ecfe3e..98c8880fc4 100644
--- a/emms-volume-pulse.el
+++ b/emms-volume-pulse.el
@@ -43,6 +43,7 @@
 ;;; Code:
 
 (require 'cl-lib)
+(require 'subr-x)
 
 ;; TODO: it would be great if custom could have
 ;; choices based on pactl list short sinks | cut -f1-2
diff --git a/emms.el b/emms.el
index cda9c9b34b..795bdab78f 100644
--- a/emms.el
+++ b/emms.el
@@ -44,6 +44,7 @@
 
 ;;; Code:
 (require 'emms-compat)
+(require 'seq)
 
 (defvar emms-version "16"
   "EMMS version string.")
@@ -1345,6 +1346,60 @@ 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)))))
+
+(defun emms-equal-lists (x y)
+  "Compare two lists X and Y for equality.
+List elements can be in any order, and Y can have more elements
+than X.
+
+This is a special helper function for tests.  It is not meant for
+general use."
+  (cond ((and (not (proper-list-p x))
+              (not (proper-list-p y)))
+         (equal x y))
+        ((and (proper-list-p x)
+              (proper-list-p y))
+         (seq-every-p (lambda (elt-x)
+                        (seq-find (lambda (elt-y)
+                                    (emms-equal-lists elt-x elt-y))
+                                  y))
+                      x))))
 
 ;;; ------------------------------------------------------------------
 ;;; Sources
diff --git a/test/emms-info-native-flac-tests.el 
b/test/emms-info-native-flac-tests.el
new file mode 100644
index 0000000000..cbd49e8e5d
--- /dev/null
+++ b/test/emms-info-native-flac-tests.el
@@ -0,0 +1,57 @@
+;;; emms-info-native-flac-tests.el --- Test suite for emms-info-native-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-native-flac)
+(require 'ert)
+
+(defmacro emms-test-flac-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-native-flac--decode-meta-blocks'."
+  `(defun ,name (offset end)
+     (let ((bytes (concat "fLaC" ,bytes)))
+       (erase-buffer)
+       (insert (substring bytes offset end)))))
+
+(emms-test-flac-make-data-func emms-test-invalid-flac-block-length 
"\x01\xff\xff\xff\x00\x01\x02\x03")
+(emms-test-flac-make-data-func emms-test-invalid-flac-block-type 
"\x09\x00\x00\x00\x00\x01\x02\x03")
+(emms-test-flac-make-data-func emms-test-valid-flac-block 
"\x00\x00\x00\x08\x10\x11\x12\x13\x14\x15\x16\x17\x84\x00\x00\x04\x01\x02\x03\x04")
+
+(ert-deftest emms-test-flac-meta-blocks ()
+  (should-error (emms-info-native-flac--decode-meta-blocks
+                 #'emms-test-invalid-flac-block-length))
+  (should-error (emms-info-native-flac--decode-meta-blocks
+                 #'emms-test-invalid-flac-block-type))
+  (should (equal (emms-info-native-flac--decode-meta-blocks
+                  #'emms-test-valid-flac-block)
+                 (list "\x01\x02\x03\x04"
+                       "\x10\x11\x12\x13\x14\x15\x16\x17"))))
+
+(ert-deftest emms-test-flac-decode-duration ()
+  ;; The corresponding sample metadata bytes are [10 196 66 240 1 8 36 0].
+  (should (= (emms-info-native-flac--decode-duration 775818634391462912) 392)))
+
+;;; emms-info-native-flac-tests.el ends here
diff --git a/test/emms-info-native-mp3-tests.el 
b/test/emms-info-native-mp3-tests.el
new file mode 100644
index 0000000000..683517b3f3
--- /dev/null
+++ b/test/emms-info-native-mp3-tests.el
@@ -0,0 +1,104 @@
+;;; emms-info-native-mp3-tests.el --- Test suite for emms-info-native-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-native-mp3)
+(require 'ert)
+
+(ert-deftest emms-test-id3v2-valid-frame-id-p ()
+  (let ((emms-info-native-id3v2--version 2))
+    (should (emms-info-native-id3v2--valid-frame-id-p "A1B"))
+    (should (not (emms-info-native-id3v2--valid-frame-id-p "~B1")))
+    (should (not (emms-info-native-id3v2--valid-frame-id-p "XX")))
+    (should (not (emms-info-native-id3v2--valid-frame-id-p "XXXX"))))
+  (let ((emms-info-native-id3v2--version 3))
+    (should (emms-info-native-id3v2--valid-frame-id-p "ABC9"))
+    (should (not (emms-info-native-id3v2--valid-frame-id-p "~BCD")))
+    (should (not (emms-info-native-id3v2--valid-frame-id-p "XXX")))
+    (should (not (emms-info-native-id3v2--valid-frame-id-p "XXXXX")))))
+
+(ert-deftest emms-test-id3v2-checked-size ()
+  (should (= (emms-info-native-id3v2--checked-size 'tag [0 0 2 1]) 257))
+  (should (= (emms-info-native-id3v2--checked-size 'tag [1 1 1 1]) 2113665))
+  (should (= (emms-info-native-id3v2--checked-size 'tag [#xff #xff #xff #xff])
+             (1- (* 256 1024 1024))))
+  (should (= (emms-info-native-id3v2--checked-size 'tag [#x7f #x7f #x7f #x7f])
+             (1- (* 256 1024 1024))))
+  (should (= (emms-info-native-id3v2--checked-size 'tag [#x12 #x34 #x56 #x78])
+             38611832))
+  (let ((emms-info-native-id3v2--version 4))
+    (should (= (emms-info-native-id3v2--checked-size 'frame [#xff #xff #xff 
#xff])
+               (1- (* 256 1024 1024)))))
+  (let ((emms-info-native-id3v2--version 3))
+    (should (= (emms-info-native-id3v2--checked-size 'frame [#xff #xff #xff 
#xff])
+               (1- (* 4 1024 1024 1024))))))
+
+(ert-deftest emms-test-id3v2-decode-size ()
+  (should (= (emms-info-native-id3v2--decode-size [01 01 01 01] nil)
+             16843009))
+  (should (= (emms-info-native-id3v2--decode-size [01 01 01 01] t)
+             2113665))
+  (should (= (emms-info-native-id3v2--decode-size [00 00 02 01] nil)
+             513))
+  (should (= (emms-info-native-id3v2--decode-size [00 00 02 01] t)
+             257)))
+
+(ert-deftest emms-test-mp3-find-and-decode-frame-header ()
+  (with-temp-buffer
+    (set-buffer-multibyte nil)
+    (insert 
"\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-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-test-mp3-find-and-decode-xing-header ()
+  (with-temp-buffer
+    (set-buffer-multibyte nil)
+    (insert 
"\xff\xea\x90\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x58\x69\x6e\x67\x00\x00\x00\x0f\x00\x00\x21\x59\x00\x50\x1d\x79\x00\x03\x06\x08\x0b\x0e\x0f\x12\x15\x17\x1a\x1d\x1f\x22\x25\x27\x2a\x2d\x2f\x32\x35\x37\x39\x3c\x3e\x41\x44\x46\x49\x4c\x4e\x51\x54\x56\x59\x5c\x5e\x61\x64\x65\x68\x6b\x6d\x70\x73\x75\x78\x7a\x7b\x7d\x7f\x82\x85\x87\x8a\x8d\x8f\x92\x95\x97\x9a\x9d\x9f\xa2\xa5\xa7\xaa\xa
 [...]
+    (should (= (emms-info-native-mp3--find-and-decode-xing-header) 8537))))
+
+(ert-deftest emms-test-mp3-find-decode-xing-header-2 ()
+  (with-temp-buffer
+    (set-buffer-multibyte nil)
+    (insert 
"\xff\xfb\x50\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x49\x6e\x66\x6f\x00\x00\x00\x0f\x00\x00\x23\xd8\x00\x3a\x86\x09\x00\x02\x05\x08\x0a\x0d\x10\x12\x14\x17\x19\x1c\x1f\x21\x24\x26\x29\x2b\x2e\x31\x33\x36\x38\x3a\x3d\x40\x42\x45\x48\x4a\x4c\x4f\x52\x54\x57\x5a\x5b\x5e\x61\x63\x66\x69\x6c\x6d\x70\x73\x75\x78\x7b\x7d\x80\x82\x85\x87\x8a\x8d\x8f\x92\x94\x96\x99\x9c\x9e\xa1\xa4\xa6\xa8\xa
 [...]
+    (should (= (emms-info-native-mp3--find-and-decode-xing-header) 9176))))
+
+(ert-deftest emms-test-mp3-find-and-decode-vbri-header ()
+  (with-temp-buffer
+    (set-buffer-multibyte nil)
+    (insert 
"\xff\xfb\xa1\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x56\x42\x52\x49\x00\x01\x0d\xb1\x00\x64\x00\x62\xdb\x91\x00\x00\x21\x3a\x00\x84\x00\x01\x00\x02\x00\x40\x98\xb1\xbd\xa8\xbb\x36\xba\xce\xbb\x37\xba\xcf\xba\x67\xbb\x37\xbc\xd7\xbb\x9f\xba\xcf\xb9\x2c\xbb\x35\xbb\x38\xbc\x08\xbb\x9f\xb9\x95\xbe\xe0\xbc\x08\xb9\xfa\xba\x63\xb8\x5a\xb6")
+    (should (= (emms-info-native-mp3--find-and-decode-vbri-header) 8506))))
+
+;;; emms-info-native-mp3-tests.el ends here
diff --git a/test/emms-info-native-ogg-tests.el 
b/test/emms-info-native-ogg-tests.el
new file mode 100644
index 0000000000..9cf1665d96
--- /dev/null
+++ b/test/emms-info-native-ogg-tests.el
@@ -0,0 +1,130 @@
+;;; 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)
+(require 'emms-info-native-ogg)
+(require 'ert)
+
+(ert-deftest emms-test-ogg-decode-page ()
+  (let* ((bytes 
"\x4f\x67\x67\x53\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x86\xd1\x9e\x17\x00\x00\x00\x00\x35\x52\xfb\x88\x01\x1e\x01\x76\x6f\x72\x62\x69\x73\x00\x00\x00\x00\x01\x44\xac\x00\x00\x00\x00\x00\x00\x80\x38\x01\x00\x00\x00\x00\x00\xb8\x01")
+         (page (bindat-unpack emms-info-native-ogg--page-bindat-spec bytes)))
+    (should (= (emms-info-native-ogg--num-packets page) 1))
+    (should (= (bindat-length emms-info-native-ogg--page-bindat-spec page) 58))
+    (should (equal (bindat-get-field page 'payload)
+                   
"\x01\x76\x6f\x72\x62\x69\x73\x00\x00\x00\x00\x01\x44\xac\x00\x00\x00\x00\x00\x00\x80\x38\x01\x00\x00\x00\x00\x00\xb8\x01"))))
+
+(ert-deftest emms-test-ogg-decode-vorbis-headers ()
+  "Test `emms-info-ogg--decode-headers' with Vorbis data."
+  (let ((bytes 
"\x01\x76\x6f\x72\x62\x69\x73\x00\x00\x00\x00\x01\x44\xac\x00\x00\x00\x00\x00\x00\x80\x38\x01\x00\x00\x00\x00\x00\xb8\x01\x03\x76\x6f\x72\x62\x69\x73\x34\x00\x00\x00\x58\x69\x70\x68\x2e\x4f\x72\x67\x20\x6c\x69\x62\x56\x6f\x72\x62\x69\x73\x20\x49\x20\x32\x30\x32\x30\x30\x37\x30\x34\x20\x28\x52\x65\x64\x75\x63\x69\x6e\x67\x20\x45\x6e\x76\x69\x72\x6f\x6e\x6d\x65\x6e\x74\x29\x02\x00\x00\x00\x07\x00\x00\x00\x66\x6f\x6f\x3d\x62\x61\x72\x1b\x00\x00\x00\x4b\x65\x79\x3d\xce\x9f\xe1
 [...]
+    (should
+     (emms-equal-lists
+      (emms-info-native-ogg--decode-headers bytes 'vorbis)
+      '((identification-header
+         (packet-type . 1)
+         (vorbis . "vorbis")
+         (vorbis-version . 0)
+         (channel-count . 1)
+         (sample-rate . 44100)
+         (bitrate-maximum . 0)
+         (bitrate-nominal . 80000)
+         (bitrate-minimum . 0)
+         (blocksize . 184)
+         (framing-flag . 1))
+        (comment-header
+         (packet-type . 3)
+         (vorbis . "vorbis")
+         (vendor-length . 52)
+         (vendor-string . "Xiph.Org libVorbis I 20200704 (Reducing 
Environment)")
+         (user-comments-list-length . 2)
+         (user-comments
+          ((length . 7)
+           (user-comment . "foo=bar"))
+          ((length . 27)
+           (user-comment . "Key=\316\237\341\275\220\317\207\341\275\266 
\316\244\316\261\341\275\220\317\204\341\275\260")))
+         (framing-bit . 1)))))))
+
+(ert-deftest emms-test-ogg-decode-opus-headers ()
+  "Test `emms-info-ogg--decode-headers' with Opus data."
+  (let ((bytes 
"\x4f\x70\x75\x73\x48\x65\x61\x64\x01\x01\x38\x01\x44\xac\x00\x00\x00\x00\x00\x4f\x70\x75\x73\x54\x61\x67\x73\x0d\x00\x00\x00\x6c\x69\x62\x6f\x70\x75\x73\x20\x31\x2e\x33\x2e\x31\x03\x00\x00\x00\x26\x00\x00\x00\x45\x4e\x43\x4f\x44\x45\x52\x3d\x6f\x70\x75\x73\x65\x6e\x63\x20\x66\x72\x6f\x6d\x20\x6f\x70\x75\x73\x2d\x74\x6f\x6f\x6c\x73\x20\x30\x2e\x31\x2e\x31\x30\x07\x00\x00\x00\x66\x6f\x6f\x3d\x62\x61\x72\x1b\x00\x00\x00\x4b\x65\x79\x3d\xce\x9f\xe1\xbd\x90\xcf\x87\xe1\xbd\xb6
 [...]
+    (emms-equal-lists
+     (emms-info-native-ogg--decode-headers bytes 'opus)
+     '((identification-header
+        (opus-head . "OpusHead")
+        (opus-version . 1)
+        (channel-count . 1)
+        (pre-skip . 312)
+        (sample-rate . 44100)
+        (output-gain . 0)
+        (channel-mapping-family . 0))
+       (comment-header
+        (opus-tags . "OpusTags")
+        (vendor-length . 13)
+        (vendor-string . "libopus 1.3.1")
+        (user-comments-list-length . 3)
+        (user-comments
+         ((length . 38)
+          (user-comment . "ENCODER=opusenc from opus-tools 0.1.10"))
+         ((length . 7)
+          (user-comment . "foo=bar"))
+         ((length . 27)
+          (user-comment . "Key=\316\237\341\275\220\317\207\341\275\266 
\316\244\316\261\341\275\220\317\204\341\275\260"))))))))
+
+(defun emms-test-ogg--decode-last-page (bytes)
+  "Call `emms-info-ogg--decode-last-page' with BYTES input.
+
+This is a helper function for `emms-test-ogg-decode-last-page'."
+  (with-temp-buffer
+    (set-buffer-multibyte nil)
+    (insert (concat bytes))
+    (emms-info-native-ogg--decode-last-page)))
+
+(ert-deftest emms-test-ogg-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")
+        (valid-result
+         (quote
+          ((capture-pattern . "OggS")
+           (stream-structure-version . 0)
+           (header-type-flag . 4)
+           (granule-position . 17310720)
+           (stream-serial-number . 1198406044)
+           (page-sequence-no . 2112)
+           (page-checksum . 2745978393)
+           (page-segments . 1)
+           (segment-table . [10])
+           (payload . "OggS123456")))))
+    (unless (eval-when-compile (fboundp 'bindat-type))
+      (push (cons 'granule-position-bytes [0 36 8 1 0 0 0 0]) valid-result))
+    (should (emms-equal-lists (emms-test-ogg--decode-last-page valid)
+                              valid-result))
+    (should (equal (emms-test-ogg--decode-last-page notlast) nil))
+    (should (equal (emms-test-ogg--decode-last-page invalid) nil))))
+
+(ert-deftest emms-test-ogg-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-native-ogg--checksum bytes) 445885580))))
+
+;;; emms-info-native-ogg-tests.el ends here
diff --git a/test/emms-info-native-tests.el b/test/emms-info-native-tests.el
new file mode 100644
index 0000000000..89f906cc90
--- /dev/null
+++ b/test/emms-info-native-tests.el
@@ -0,0 +1,70 @@
+;;; emms-info-native-tests.el --- Test suite for emms-info-native  -*- 
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.
+
+;;; Commentary:
+
+;; This test suite exercises `emms-info-native' with various input
+;; files.
+
+;;; Code:
+
+(require 'emms-info-native)
+(require 'ert)
+
+(ert-deftest emms-test-info-native-mp3 ()
+  (should (equal (emms-info-native--decode-info-fields
+                  "resources/sine.mp3")
+                 '(("year" . "2023")
+                   ("album" . "Test Data ☺")
+                   ("artist" . "EMMS project")
+                   ("title" . "440 Hz sine wave")
+                   ("playing-time" . 5)))))
+
+(ert-deftest emms-test-info-native-ogg ()
+  (should (equal (emms-info-native--decode-info-fields
+                  "resources/sine.ogg")
+                 '(("artist" . "EMMS project")
+                   ("date" . "2023-09-02")
+                   ("title" . "440 Hz sine wave")
+                   ("album" . "Test Data ☺")
+                   ("playing-time" . 5)))))
+
+(ert-deftest emms-test-info-native-flac ()
+  (should (equal (emms-info-native--decode-info-fields
+                  "resources/sine.flac")
+                 '(("artist" . "EMMS project")
+                   ("date" . "2023-09-02")
+                   ("title" . "440 Hz sine wave")
+                   ("album" . "Test Data ☺")
+                   ("playing-time" . 5)))))
+
+(ert-deftest emms-test-info-native-opus ()
+  (should (equal (emms-info-native--decode-info-fields
+                  "resources/sine.opus")
+                 '(("artist" . "EMMS project")
+                   ("date" . "2023-09-02")
+                   ("title" . "440 Hz sine wave")
+                   ("album" . "Test Data ☺")
+                   ("playing-time" . 5)))))
+
+;;; emms-info-native-tests.el ends here
diff --git a/test/emms-info-native-vorbis-tests.el 
b/test/emms-info-native-vorbis-tests.el
new file mode 100644
index 0000000000..bccdda6521
--- /dev/null
+++ b/test/emms-info-native-vorbis-tests.el
@@ -0,0 +1,61 @@
+;;; emms-info-native-vorbis-tests.el --- Test suite for 
emms-info-native-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-native-vorbis)
+(require 'ert)
+
+(ert-deftest emms-test-vorbis-extract-comments ()
+  (let ((comments
+         (quote (((user-comment . 
"MUSICBRAINZ_RELEASEGROUPID=9b307293-d2e6-34a9-a289-161c5baf187f")
+                  (length . 63))
+                 ((user-comment . "ORIGINALDATE=1997-03-31")
+                  (length . 23))
+                 ((user-comment . "ORIGINALYEAR=1997")
+                  (length . 17))
+                 ((user-comment . "RELEASETYPE=album")
+                  (length . 17))
+                 ((user-comment . "BARCODE=769233004727")
+                  (length . 20))
+                 ((user-comment . "ALBUM=A toda Cuba le gusta")
+                  (length . 26))))))
+    (should (equal (emms-info-native-vorbis-extract-comments comments)
+                   (quote (("album" . "A toda Cuba le gusta")
+                           ("originalyear" . "1997")
+                           ("originaldate" . "1997-03-31")))))))
+
+(ert-deftest emms-test-vorbis-split-comment ()
+  (should (equal (emms-info-native-vorbis--split-comment "") nil))
+  (should (equal (emms-info-native-vorbis--split-comment "x") nil))
+  (should (equal (emms-info-native-vorbis--split-comment "x=") nil))
+  (should (equal (emms-info-native-vorbis--split-comment "=x") nil))
+  (should (equal (emms-info-native-vorbis--split-comment "a=B")
+                 (cons "a" "B")))
+  (should (equal (emms-info-native-vorbis--split-comment "abc=ABC=123")
+                 (cons "abc" "ABC=123")))
+  (let ((comment "Key=\316\237\341\275\220\317\207\341\275\266 
\316\244\316\261\341\275\220\317\204\341\275\260"))
+    (should (equal (emms-info-native-vorbis--split-comment comment)
+                   (cons "key" "Οὐχὶ Ταὐτὰ")))))
+
+;;; emms-info-native-vorbis-tests.el ends here
diff --git a/test/emms-tests.el b/test/emms-tests.el
new file mode 100644
index 0000000000..217b4e2856
--- /dev/null
+++ b/test/emms-tests.el
@@ -0,0 +1,61 @@
+;;; emms-tests.el --- Test suite for EMMS core  -*- 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)
+(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)))
+
+;;; emms-tests.el ends here
diff --git a/test/resources/sine.flac b/test/resources/sine.flac
new file mode 100644
index 0000000000..d02d576475
Binary files /dev/null and b/test/resources/sine.flac differ
diff --git a/test/resources/sine.mp3 b/test/resources/sine.mp3
new file mode 100644
index 0000000000..0dca24caec
Binary files /dev/null and b/test/resources/sine.mp3 differ
diff --git a/test/resources/sine.ogg b/test/resources/sine.ogg
new file mode 100644
index 0000000000..bd53a4f830
Binary files /dev/null and b/test/resources/sine.ogg differ
diff --git a/test/resources/sine.opus b/test/resources/sine.opus
new file mode 100644
index 0000000000..0bda0bdee5
Binary files /dev/null and b/test/resources/sine.opus differ



reply via email to

[Prev in Thread] Current Thread [Next in Thread]