[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
- [elpa] externals/emms c848c18727 33/42: Change to emms-info-native- prefix, (continued)
- [elpa] externals/emms c848c18727 33/42: Change to emms-info-native- prefix, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 342c44103a 35/42: Fix multi-channel mapping in Opus identification header, ELPA Syncer, 2023/11/01
- [elpa] externals/emms b3c2f9cf09 39/42: Use uintr for little-endian unsigned integer fields, ELPA Syncer, 2023/11/01
- [elpa] externals/emms c96afb7687 40/42: Use eval-when-compile with subr-x, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 2852a8f61b 10/42: Add tests for emms-info-native, ELPA Syncer, 2023/11/01
- [elpa] externals/emms e1f2810f39 13/42: Use string instead of vector as Ogg page payload type, ELPA Syncer, 2023/11/01
- [elpa] externals/emms dd72caba90 37/42: Doc fixes, ELPA Syncer, 2023/11/01
- [elpa] externals/emms f594f7edac 15/42: Use strings instead of vectors for passing data, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 2749fdb998 30/42: Allow empty metadata blocks, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 52dac8ccc4 41/42: Remove most length limits from Vorbis bindat specs, ELPA Syncer, 2023/11/01
- [elpa] externals/emms 32fd570ed7 42/42: Merge branch 'info-native',
ELPA Syncer <=