From a8792675fc79f0236f7d3f909e5400996cadffb0 Mon Sep 17 00:00:00 2001 From: Petteri Hintsanen Date: Wed, 24 Feb 2021 22:54:20 +0200 Subject: Decode id3v2 user-defined text frames Frames are assumed to be key/value pairs. If key is an info-field identifier, return the value for that info-field. --- emms-info-native.el | 145 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 86 insertions(+), 59 deletions(-) (limited to 'emms-info-native.el') diff --git a/emms-info-native.el b/emms-info-native.el index 9f0ed2d..97c1917 100644 --- a/emms-info-native.el +++ b/emms-info-native.el @@ -557,7 +557,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: @@ -572,6 +573,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 +644,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 +658,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 +672,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 +705,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 +742,24 @@ 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. + +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 '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. -- cgit v1.2.3 From 5531af7426bb5e162cd48ada4b65c5ac1b8bb00a Mon Sep 17 00:00:00 2001 From: Petteri Hintsanen Date: Wed, 24 Feb 2021 23:50:04 +0200 Subject: Add support for id3v1 genres --- emms-info-native.el | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 141 insertions(+), 1 deletion(-) (limited to 'emms-info-native.el') diff --git a/emms-info-native.el b/emms-info-native.el index 97c1917..e3181ef 100644 --- a/emms-info-native.el +++ b/emms-info-native.el @@ -546,7 +546,7 @@ outside itself.") ("TDRC" . "date") ("TPA" . "discnumber") ("TPOS" . "discnumber") - ("TCON" . "genre") + ("TCON" . genre) ("TPUB" . "label") ("TDOR" . "originaldate") ("TOR" . "originalyear") @@ -566,6 +566,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) @@ -749,11 +878,22 @@ 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 id3v1 genre +reference \"(XX)\" or plain genre string. In the former case, +map XX 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))) -- cgit v1.2.3 From b7684baef60f08a04e4e598ee21a5ea55ef23bd5 Mon Sep 17 00:00:00 2001 From: Petteri Hintsanen Date: Fri, 26 Feb 2021 21:35:44 +0200 Subject: Match id3v1 genres in id3v2.4 frame v2.4 does not enclose genre references in parentheses. --- emms-info-native.el | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'emms-info-native.el') diff --git a/emms-info-native.el b/emms-info-native.el index e3181ef..405bf25 100644 --- a/emms-info-native.el +++ b/emms-info-native.el @@ -878,10 +878,11 @@ 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 id3v1 genre -reference \"(XX)\" or plain genre string. In the former case, -map XX to a string via `emms-info-native--id3v1-genres'; in the -latter case use the genre string verbatim. +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." @@ -889,7 +890,7 @@ string." (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) + (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)))) -- cgit v1.2.3 From cd437ca45b8a70c3b946a70a5db1b3df5eb6fa99 Mon Sep 17 00:00:00 2001 From: Petteri Hintsanen Date: Sat, 27 Feb 2021 14:29:08 +0200 Subject: Fix byte compilation - Add requires to load seq-... and string-trim-right functions during compilation. - Reorder code to fix warnings about free and undeclared variables. --- emms-info-native.el | 296 ++++++++++++++++++++++++++-------------------------- 1 file changed, 149 insertions(+), 147 deletions(-) (limited to 'emms-info-native.el') diff --git a/emms-info-native.el b/emms-info-native.el index 405bf25..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) -- cgit v1.2.3