aboutsummaryrefslogtreecommitdiff
path: root/emms-info-native.el
diff options
context:
space:
mode:
Diffstat (limited to 'emms-info-native.el')
-rw-r--r--emms-info-native.el584
1 files changed, 377 insertions, 207 deletions
diff --git a/emms-info-native.el b/emms-info-native.el
index 9f0ed2d..8601dd0 100644
--- a/emms-info-native.el
+++ b/emms-info-native.el
@@ -59,6 +59,8 @@
(require 'bindat)
(require 'cl-lib)
(require 'emms-info)
+(require 'seq)
+(require 'subr-x)
(defconst emms-info-native--max-peek-size (* 2048 1024)
"Maximum buffer size for metadata decoding.
@@ -72,127 +74,15 @@ 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.")
-;;;; Ogg code
-
-(defconst emms-info-native--ogg-magic-array
- [79 103 103 83]
- "Ogg format magic capture pattern `OggS'.")
-
-(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.")
-
-(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'.
+(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.")
-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))))
+(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
@@ -245,10 +135,6 @@ their comments have almost the same format as Vorbis.")
"year")
"EMMS info fields that are extracted from Vorbis comments.")
-(defconst emms-info-native--vorbis-magic-array
- [118 111 114 98 105 115]
- "Header packet magic pattern `vorbis'.")
-
(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))
@@ -282,6 +168,10 @@ header.")
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)
@@ -356,19 +246,6 @@ lower case and VALUE is the decoded value."
;;;; Opus code
-(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.")
-
-(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-tags-magic-array
- [79 112 117 115 84 97 103 115]
- "Opus comment header magic pattern `OpusTags'.")
-
(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))
@@ -397,6 +274,10 @@ header.")
(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)
@@ -422,6 +303,132 @@ header.")
(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
@@ -488,7 +495,7 @@ Return the comment block data in a vector."
(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"
+ (error "FLAC block type error: expected <= 6, got %s"
block-type))
(when (= block-type 4)
;; Comment block found, extract it.
@@ -499,15 +506,6 @@ Return the comment block data in a vector."
;;;; id3v2 (MP3) code
-(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.")
-
-(defconst emms-info-native--id3v2-magic-array
- [#x49 #x44 #x33]
- "id3v2 header magic pattern `ID3'.")
-
(defconst emms-info-native--id3v2-header-bindat-spec
'((file-identifier vec 3)
(eval (unless (equal last emms-info-native--id3v2-magic-array)
@@ -522,6 +520,10 @@ outside itself.")
(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)
@@ -546,7 +548,7 @@ outside itself.")
("TDRC" . "date")
("TPA" . "discnumber")
("TPOS" . "discnumber")
- ("TCON" . "genre")
+ ("TCON" . genre)
("TPUB" . "label")
("TDOR" . "originaldate")
("TOR" . "originalyear")
@@ -557,7 +559,8 @@ outside itself.")
("TRK" . "tracknumber")
("TRCK" . "tracknumber")
("TYE" . "year")
- ("TYER" . "year"))
+ ("TYER" . "year")
+ ("TXXX" . user-defined))
"Mapping from id3v2 frame identifiers to EMMS info fields.
Sources:
@@ -565,6 +568,135 @@ 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)
@@ -572,6 +704,38 @@ Sources:
(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
@@ -611,37 +775,10 @@ Return the size. Signal an error if the size is zero."
(insert-file-contents-literally filename nil 10 14)
(emms-info-native--checked-id3v2-size 'frame (buffer-string))))
-(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-frames (filename begin end unsync)
"Read and decode id3v2 text frames from FILENAME.
-BEGIN should be the offset of first byte after id3v2 header and
-extended header (if any), and END should be the offset after the
-complete id3v2 tag.
+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.
@@ -652,25 +789,13 @@ Return metadata in a list of (FIELD . VALUE) cons cells."
comments)
(condition-case nil
(while (< offset limit)
- (let* ((header (emms-info-native--decode-id3v2-frame-header filename
- offset))
- (info-id (emms-info-native--id3v2-frame-info-id header))
- (decoded-size (bindat-get-field (cdr header) 'size)))
- (setq offset (car header)) ;advance to frame data begin
- (if (or unsync info-id)
- ;; Note that if unsync is t, we have to always read a
- ;; frame to gets its true size so that we can adjust
- ;; offset correctly.
- (let ((data (emms-info-native--read-id3v2-frame-data filename
- offset
- decoded-size
- unsync)))
- (setq offset (car data))
- (when info-id
- (let ((value (emms-info-native--decode-id3v2-string (cdr data))))
- (push (cons info-id value) comments))))
- ;; Skip the frame.
- (cl-incf offset decoded-size))))
+ (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))
@@ -678,11 +803,25 @@ Return metadata in a list of (FIELD . VALUE) cons cells."
"Return the last decoded header size in bytes."
(if (= emms-info-native--id3v2-version 2) 6 10))
-(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--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.
@@ -697,6 +836,12 @@ offset after the frame header, and FRAME is the decoded frame."
(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
@@ -728,11 +873,36 @@ data."
(insert-file-contents-literally filename nil begin end)
(cons end (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--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.